@agentforce/adk
Version:
AgentForce Agent Development Kit - A powerful framework for building AI agents and servers
509 lines (420 loc) • 19.3 kB
text/typescript
import type { AgentForceServer } from "../../server";
import type { AgentForceAgent } from "../../agent";
import type { Context } from "hono";
/**
* Ollama generate request format
*/
export interface OllamaGenerateRequest {
model: string;
prompt: string;
format?: string;
options?: Record<string, unknown>;
system?: string;
template?: string;
context?: number[];
stream?: boolean;
raw?: boolean;
keep_alive?: string | number;
}
/**
* Ollama chat message format
*/
export interface OllamaChatMessage {
role: "system" | "user" | "assistant";
content: string;
images?: string[];
}
/**
* Ollama chat request format
*/
export interface OllamaChatRequest {
model: string;
messages: OllamaChatMessage[];
format?: string;
options?: Record<string, unknown>;
stream?: boolean;
keep_alive?: string | number;
}
/**
* Adds Ollama-compatible agents that respond to "/api/generate" and "/api/chat" endpoints (chainable method)
* @param this - The AgentForceServer instance (bound context)
* @param agent - The AgentForce agent instance to handle Ollama-compatible requests
* @returns {AgentForceServer} The server instance for method chaining
*/
export function useOllamaCompatibleRouting(
this: AgentForceServer,
agent: AgentForceAgent,
): AgentForceServer {
if (!agent) {
throw new Error("Agent instance is required");
}
// Validate that the agent has the required methods using bracket notation
if (typeof agent["getName"] !== "function") {
throw new Error("Agent instance is required");
}
const log = this.getLogger();
const serverName = this.getName();
// Add /api/generate route
const generatePath = "/api/generate";
const generateMethod = "POST";
log.info({
serverName,
method: generateMethod,
path: generatePath,
agentName: agent["getName"](),
action: "ollama_compatible_agent_added",
}, `Adding Ollama-compatible agent: ${generateMethod} ${generatePath}`);
const generateRouteAgent = {
method: generateMethod,
path: generatePath,
agent,
};
this.addToRouteAgents(generateRouteAgent);
// Add /api/chat route
const chatPath = "/api/chat";
const chatMethod = "POST";
log.info({
serverName,
method: chatMethod,
path: chatPath,
agentName: agent["getName"](),
action: "ollama_compatible_agent_added",
}, `Adding Ollama-compatible agent: ${chatMethod} ${chatPath}`);
const chatRouteAgent = {
method: chatMethod,
path: chatPath,
agent,
};
this.addToRouteAgents(chatRouteAgent);
return this;
}
/**
* Validates Ollama generate request format
* @param data - The request data to validate
* @returns {boolean} True if valid, throws error if invalid
*/
function validateOllamaGenerateRequest(data: any): data is OllamaGenerateRequest {
if (!data || typeof data !== "object") {
throw new Error("Request body must be a valid JSON object");
}
if (!data.model || typeof data.model !== "string") {
throw new Error("Missing or invalid \"model\" field. Must be a non-empty string");
}
if (!data.prompt || typeof data.prompt !== "string") {
throw new Error("Missing or invalid \"prompt\" field. Must be a non-empty string");
}
// Validate optional fields
if (data.format !== undefined && typeof data.format !== "string") {
throw new Error("Invalid \"format\" field. Must be a string if provided");
}
if (data.system !== undefined && typeof data.system !== "string") {
throw new Error("Invalid \"system\" field. Must be a string if provided");
}
if (data.template !== undefined && typeof data.template !== "string") {
throw new Error("Invalid \"template\" field. Must be a string if provided");
}
if (data.stream !== undefined && typeof data.stream !== "boolean") {
throw new Error("Invalid \"stream\" field. Must be a boolean if provided");
}
if (data.raw !== undefined && typeof data.raw !== "boolean") {
throw new Error("Invalid \"raw\" field. Must be a boolean if provided");
}
if (data.context !== undefined && !Array.isArray(data.context)) {
throw new Error("Invalid \"context\" field. Must be an array if provided");
}
return true;
}
/**
* Validates Ollama chat request format
* @param data - The request data to validate
* @returns {boolean} True if valid, throws error if invalid
*/
function validateOllamaChatRequest(data: any): data is OllamaChatRequest {
if (!data || typeof data !== "object") {
throw new Error("Request body must be a valid JSON object");
}
if (!data.model || typeof data.model !== "string") {
throw new Error("Missing or invalid \"model\" field. Must be a non-empty string");
}
if (!data.messages || !Array.isArray(data.messages)) {
throw new Error("Missing or invalid \"messages\" field. Must be an array");
}
if (data.messages.length === 0) {
throw new Error("Messages array cannot be empty");
}
// Validate each message
for (let i = 0; i < data.messages.length; i++) {
const message = data.messages[i];
if (!message || typeof message !== "object") {
throw new Error(`Message at index ${i} must be a valid object`);
}
if (!message.role || typeof message.role !== "string") {
throw new Error(`Message at index ${i} missing or invalid "role" field`);
}
if (!["system", "user", "assistant"].includes(message.role)) {
throw new Error(`Message at index ${i} has invalid role "${message.role}". Must be "system", "user", or "assistant"`);
}
if (!message.content || typeof message.content !== "string") {
throw new Error(`Message at index ${i} missing or invalid "content" field. Must be a non-empty string`);
}
// Optional field validations
if (message.images !== undefined && !Array.isArray(message.images)) {
throw new Error(`Message at index ${i} has invalid "images" field. Must be an array if provided`);
}
}
// Validate optional fields
if (data.format !== undefined && typeof data.format !== "string") {
throw new Error("Invalid \"format\" field. Must be a string if provided");
}
if (data.stream !== undefined && typeof data.stream !== "boolean") {
throw new Error("Invalid \"stream\" field. Must be a boolean if provided");
}
return true;
}
/**
* Converts Ollama chat messages to a conversation context string
* @param messages - Array of Ollama chat messages
* @returns {string} The full conversation formatted for the model
*/
function formatOllamaChatContext(messages: OllamaChatMessage[]): string {
if (messages.length === 0) {
throw new Error("Messages array cannot be empty");
}
// Find the last user message for validation
const userMessages = messages.filter(msg => msg.role === "user");
if (userMessages.length === 0) {
throw new Error("No user message found in messages array");
}
// If only one user message and no assistant messages, return just the user content
if (messages.length === 1 && messages[0]?.role === "user") {
return messages[0].content;
}
// Format the full conversation for context
const conversationLines: string[] = [];
for (const message of messages) {
switch (message.role) {
case "system":
conversationLines.push(`System: ${message.content}`);
break;
case "user":
conversationLines.push(`Human: ${message.content}`);
break;
case "assistant":
conversationLines.push(`Assistant: ${message.content}`);
break;
}
}
// Add the current context instruction
conversationLines.push("\nPlease respond as the Assistant, taking into account the full conversation history above.");
return conversationLines.join("\n");
}
/**
* Parses Ollama model parameter to extract provider and model
* @param modelParam - The model parameter from Ollama request (e.g., "llama3.2")
* @returns {object} Object containing provider and model
*/
function parseOllamaModelParameter(modelParam: string): { provider: "ollama"; model: string } {
if (!modelParam || typeof modelParam !== "string") {
throw new Error("Model parameter must be a non-empty string");
}
// Ollama models are typically just model names without provider prefix
// We'll assume ollama provider by default
return { provider: "ollama", model: modelParam.trim() };
}
/**
* Creates a Hono route handler for Ollama generate endpoint
* @param agent - The AgentForce agent to handle the request
* @param path - Route path for logging purposes
* @returns Hono route handler function
*/
export function createOllamaGenerateRouteHandler(agent: AgentForceAgent, path: string): (c: Context) => Promise<Response> {
return async (c: Context): Promise<Response> => {
try {
let requestData: Record<string, unknown> = {};
// 🔍 DEBUG: Log incoming request details
console.log("=== OLLAMA GENERATE REQUEST DEBUG ===");
console.log("Path:", path);
console.log("Request URL:", c.req.url);
console.log("Request Headers:", c.req.header());
try {
requestData = await c.req.json();
// 🔍 DEBUG: Log the parsed request body
console.log("📝 Request Body (parsed JSON):", JSON.stringify(requestData, null, 2));
} catch (jsonError) {
console.log("❌ JSON Parse Error:", jsonError);
return c.json({
error: "Invalid JSON in request body",
message: "Please provide valid JSON data",
}, 400);
}
console.log("🔍 Ollama Generate Endpoint - Validating Request...");
try {
validateOllamaGenerateRequest(requestData);
console.log("✅ Ollama Generate Request Validation Passed");
const ollamaRequest = requestData as unknown as OllamaGenerateRequest;
console.log("🤖 Ollama Model:", ollamaRequest.model);
console.log("💬 Ollama Prompt:", ollamaRequest.prompt);
// Parse and set provider/model from the request
try {
const { provider, model } = parseOllamaModelParameter(ollamaRequest.model);
console.log(`🔧 Setting agent to use provider: ${provider}, model: ${model}`);
// Configure the agent with the requested model
agent.useLLM(provider, model);
// Set system prompt if provided
if (ollamaRequest.system) {
agent.systemPrompt(ollamaRequest.system);
}
} catch (parseError) {
console.error("❌ Model parsing error:", parseError);
return c.json({
error: "Invalid model parameter",
message: parseError instanceof Error ? parseError.message : "Could not parse model parameter",
}, 400);
}
// Execute the agent with the prompt
let response: string;
try {
console.log("🤖 Executing agent with prompt...");
response = await agent.prompt(ollamaRequest.prompt).getResponse();
console.log("✅ Agent execution completed");
console.log("📤 Agent response:", response);
} catch (error) {
console.error("❌ Agent execution error:", error);
return c.json({
error: "Agent execution failed",
message: error instanceof Error ? error.message : "Unknown execution error",
}, 500);
}
// Return Ollama-compatible response
console.log("📤 Returning Ollama-compatible generate response");
const ollamaResponse = {
model: ollamaRequest.model,
created_at: new Date().toISOString(),
response: response,
done: true,
context: [], // Could be implemented to track conversation context
total_duration: 0, // Could be implemented to track timing
load_duration: 0,
prompt_eval_count: 0,
prompt_eval_duration: 0,
eval_count: 0,
eval_duration: 0,
};
return c.json(ollamaResponse);
} catch (error) {
console.error("❌ Ollama generate validation/execution error:", error);
return c.json({
error: "Invalid request",
message: error instanceof Error ? error.message : "Request validation failed",
}, 400);
}
} catch (error) {
console.error(`Error in Ollama generate route ${path}:`, error);
return c.json({
error: "Internal server error",
message: error instanceof Error ? error.message : "Unknown error occurred",
}, 500);
}
};
}
/**
* Creates a Hono route handler for Ollama chat endpoint
* @param agent - The AgentForce agent to handle the request
* @param path - Route path for logging purposes
* @returns Hono route handler function
*/
export function createOllamaChatRouteHandler(agent: AgentForceAgent, path: string): (c: Context) => Promise<Response> {
return async (c: Context): Promise<Response> => {
try {
let requestData: Record<string, unknown> = {};
// 🔍 DEBUG: Log incoming request details
console.log("=== OLLAMA CHAT REQUEST DEBUG ===");
console.log("Path:", path);
console.log("Request URL:", c.req.url);
console.log("Request Headers:", c.req.header());
try {
requestData = await c.req.json();
// 🔍 DEBUG: Log the parsed request body
console.log("📝 Request Body (parsed JSON):", JSON.stringify(requestData, null, 2));
} catch (jsonError) {
console.log("❌ JSON Parse Error:", jsonError);
return c.json({
error: "Invalid JSON in request body",
message: "Please provide valid JSON data",
}, 400);
}
console.log("🔍 Ollama Chat Endpoint - Validating Request...");
try {
validateOllamaChatRequest(requestData);
console.log("✅ Ollama Chat Request Validation Passed");
const ollamaRequest = requestData as unknown as OllamaChatRequest;
console.log("📧 Ollama Messages:", JSON.stringify(ollamaRequest.messages, null, 2));
console.log("🤖 Ollama Model:", ollamaRequest.model);
const prompt = formatOllamaChatContext(ollamaRequest.messages);
console.log("💬 Formatted Conversation Context:", prompt);
// Parse and set provider/model from the request
try {
const { provider, model } = parseOllamaModelParameter(ollamaRequest.model);
console.log(`🔧 Setting agent to use provider: ${provider}, model: ${model}`);
// Configure the agent with the requested model
agent.useLLM(provider, model);
// Set system prompt from messages if present
const systemMessage = ollamaRequest.messages.find(msg => msg.role === "system");
if (systemMessage) {
agent.systemPrompt(systemMessage.content);
}
} catch (parseError) {
console.error("❌ Model parsing error:", parseError);
return c.json({
error: "Invalid model parameter",
message: parseError instanceof Error ? parseError.message : "Could not parse model parameter",
}, 400);
}
// Execute the agent with the formatted conversation
let response: string;
try {
console.log("🤖 Executing agent with conversation...");
response = await agent.prompt(prompt).getResponse();
console.log("✅ Agent execution completed");
console.log("📤 Agent response:", response);
} catch (error) {
console.error("❌ Agent execution error:", error);
return c.json({
error: "Agent execution failed",
message: error instanceof Error ? error.message : "Unknown execution error",
}, 500);
}
// Return Ollama-compatible response
console.log("📤 Returning Ollama-compatible chat response");
const ollamaResponse = {
model: ollamaRequest.model,
created_at: new Date().toISOString(),
message: {
role: "assistant",
content: response,
},
done: true,
total_duration: 0, // Could be implemented to track timing
load_duration: 0,
prompt_eval_count: 0,
prompt_eval_duration: 0,
eval_count: 0,
eval_duration: 0,
};
return c.json(ollamaResponse);
} catch (error) {
console.error("❌ Ollama chat validation/execution error:", error);
return c.json({
error: "Invalid request",
message: error instanceof Error ? error.message : "Request validation failed",
}, 400);
}
} catch (error) {
console.error(`Error in Ollama chat route ${path}:`, error);
return c.json({
error: "Internal server error",
message: error instanceof Error ? error.message : "Unknown error occurred",
}, 500);
}
};
}