@agentforce/adk
Version:
AgentForce Agent Development Kit - A powerful framework for building AI agents and servers
331 lines (291 loc) • 13.8 kB
text/typescript
import type { AgentForceServer } from "../../server";
import type { AgentForceAgent } from "../../agent";
import type { Context } from "hono";
/**
* Schema configuration for route agent input and output validation
* @interface RouteAgentSchema
* @property {string[]} [input] - Expected input field names for validation
* @property {string[]} [output] - Output field names to include in response
*/
export interface RouteAgentSchema {
input?: string[];
output?: string[];
}
/**
* Configuration for HTTP route handled by an agent
* @interface RouteAgent
* @property {string} method - HTTP method (GET, POST, PUT, DELETE, etc.)
* @property {string} path - URL path for the route
* @property {AgentForceAgent} agent - Agent instance to handle requests
* @property {RouteAgentSchema} [schema] - Optional validation schema
*/
export interface RouteAgent {
method: string;
path: string;
agent: AgentForceAgent;
schema?: RouteAgentSchema;
}
/**
* Adds a route agent to handle specific HTTP endpoints (chainable method)
* @param this - The AgentForceServer instance (bound context)
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
* @param path - The route path (e.g., "/story", "/image")
* @param agent - The AgentForce agent instance to handle requests for this route
* @param schema - Optional schema configuration for input and output fields
* @returns {AgentForceServer} The server instance for method chaining
*/
export function addRouteAgent(
this: AgentForceServer,
method: string,
path: string,
agent: AgentForceAgent,
schema?: RouteAgentSchema,
): AgentForceServer {
// Validate inputs
if (!method || typeof method !== "string") {
throw new Error("HTTP method must be a non-empty string");
}
if (!path || typeof path !== "string") {
throw new Error("Route path must be a non-empty string");
}
if (!agent) {
throw new Error("Agent instance is required");
}
// Normalize method to uppercase
const normalizedMethod = method.toUpperCase();
// Validate HTTP method
const validMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
if (!validMethods.includes(normalizedMethod)) {
throw new Error(`Invalid HTTP method: ${method}. Valid methods are: ${validMethods.join(", ")}`);
}
// Ensure path starts with /
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
// Validate and normalize schema if provided
let normalizedSchema: RouteAgentSchema | undefined;
if (schema) {
normalizedSchema = {
input: schema.input || ["prompt"],
output: schema.output || ["success", "method", "path", "agentName", "prompt", "response"],
};
// Ensure "prompt" is always included in input (required field)
if (normalizedSchema.input && !normalizedSchema.input.includes("prompt")) {
normalizedSchema.input.push("prompt");
}
}
const log = this.getLogger();
const serverName = this.getName();
log.info({
serverName,
method: normalizedMethod,
path: normalizedPath,
agentName: "AgentForce Agent",
schema: normalizedSchema,
action: "route_agent_added",
}, `Adding route agent: ${normalizedMethod} ${normalizedPath}`);
// Store the route agent configuration
const routeAgent: RouteAgent = {
method: normalizedMethod,
path: normalizedPath,
agent,
schema: normalizedSchema,
};
// Add to the server's route agents collection
this.addToRouteAgents(routeAgent);
return this;
}
/**
* Creates a Hono route handler for an agent endpoint with schema support
* @param agent - The AgentForce agent to handle the request
* @param method - HTTP method for logging purposes
* @param path - Route path for logging purposes
* @param schema - Optional schema configuration for input and output
* @returns Hono route handler function
*/
export function createAgentRouteHandler(
agent: AgentForceAgent,
method: string,
path: string,
schema?: RouteAgentSchema,
): (c: Context) => Promise<Response> {
return async (c: Context): Promise<Response> => {
try {
let requestData: Record<string, unknown> = {};
let prompt: string;
// Get expected input fields from schema or use default
const expectedInputFields = schema?.input || ["prompt"];
// 🔍 DEBUG: Log incoming request details
console.log("=== INCOMING REQUEST DEBUG ===");
console.log("Method:", method);
console.log("Path:", path);
console.log("Request URL:", c.req.url);
console.log("Request Headers:", c.req.header());
console.log("Expected Input Fields:", expectedInputFields);
// Extract request data based on HTTP method
if (["POST", "PUT", "PATCH"].includes(method)) {
try {
requestData = await c.req.json();
// 🔍 DEBUG: Log the parsed request body
console.log("📝 Request Body (parsed JSON):", JSON.stringify(requestData, null, 2));
console.log("Request Body Type:", typeof requestData);
console.log("Request Body Keys:", Object.keys(requestData || {}));
} catch (jsonError) {
console.log("❌ JSON Parse Error:", jsonError);
return c.json({
error: "Invalid JSON in request body",
message: "Please provide valid JSON data",
}, 400);
}
// Validate required fields based on schema or default behavior
if (!requestData.prompt || typeof requestData.prompt !== "string") {
return c.json({
error: "Missing or invalid prompt",
message: "Request must include a \"prompt\" field with a string value",
example: { prompt: "create a story for an auth service in bun" },
expectedFields: expectedInputFields,
}, 400);
}
prompt = requestData.prompt as string;
// Strict validation when schema is provided
if (schema && schema.input) {
// Check for missing required fields (all fields in schema.input except 'prompt')
const missingFields: string[] = [];
const unexpectedFields: string[] = [];
// Validate that all schema-defined fields are present
for (const field of schema.input) {
if (field !== "prompt" && (!requestData[field] || requestData[field] === "")) {
missingFields.push(field);
}
}
// Check for unexpected fields (not in schema)
for (const field of Object.keys(requestData)) {
if (!schema.input.includes(field)) {
unexpectedFields.push(field);
}
}
// Return error if there are missing required fields
if (missingFields.length > 0) {
return c.json({
error: "Missing required fields",
message: `The following required fields are missing: ${missingFields.join(", ")}`,
missingFields,
expectedFields: schema.input,
providedFields: Object.keys(requestData),
}, 400);
}
// Return error if there are unexpected fields
if (unexpectedFields.length > 0) {
return c.json({
error: "Unexpected fields in request",
message: `The following fields are not allowed: ${unexpectedFields.join(", ")}`,
unexpectedFields,
expectedFields: schema.input,
providedFields: Object.keys(requestData),
}, 400);
}
}
} else if (["GET", "HEAD", "OPTIONS", "DELETE"].includes(method)) {
// For GET, HEAD, OPTIONS, DELETE requests, use query parameters
const url = new URL(c.req.url);
requestData = Object.fromEntries(url.searchParams.entries());
if (!requestData.prompt || typeof requestData.prompt !== "string") {
return c.json({
error: "Missing or invalid prompt",
message: "Request must include a \"prompt\" query parameter with a string value",
example: "?prompt=create a story for an auth service in bun",
expectedFields: expectedInputFields,
}, 400);
}
prompt = requestData.prompt as string;
// Strict validation when schema is provided
if (schema && schema.input) {
// Check for missing required fields (all fields in schema.input except 'prompt')
const missingFields: string[] = [];
const unexpectedFields: string[] = [];
// Validate that all schema-defined fields are present
for (const field of schema.input) {
if (field !== "prompt" && (!requestData[field] || requestData[field] === "")) {
missingFields.push(field);
}
}
// Check for unexpected fields (not in schema)
for (const field of Object.keys(requestData)) {
if (!schema.input.includes(field)) {
unexpectedFields.push(field);
}
}
// Return error if there are missing required fields
if (missingFields.length > 0) {
return c.json({
error: "Missing required fields",
message: `The following required fields are missing: ${missingFields.join(", ")}`,
missingFields,
expectedFields: schema.input,
providedFields: Object.keys(requestData),
}, 400);
}
// Return error if there are unexpected fields
if (unexpectedFields.length > 0) {
return c.json({
error: "Unexpected fields in request",
message: `The following fields are not allowed: ${unexpectedFields.join(", ")}`,
unexpectedFields,
expectedFields: schema.input,
providedFields: Object.keys(requestData),
}, 400);
}
}
} else {
throw new Error(`Unsupported HTTP method: ${method}`);
}
// Execute the agent with the extracted prompt
let response: string;
try {
console.log("🚀 Executing Agent with Prompt:", prompt);
response = await agent
.prompt(prompt)
.getResponse();
console.log("✅ Agent Response Received:", response.substring(0, 100) + (response.length > 100 ? "..." : ""));
} catch (error) {
console.error("❌ Error executing agent:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return c.json({
error: "Agent execution failed",
message: errorMessage,
success: false,
}, 500);
}
// Build response data based on schema configuration
const baseResponseData = {
success: true,
method,
path,
agentName: agent["getName"](),
prompt,
response,
// Include any additional input fields from the request
...Object.fromEntries(
Object.entries(requestData).filter(([key]) =>
expectedInputFields.includes(key) && key !== "prompt",
),
),
};
// Filter response data based on output schema or use default
const expectedOutputFields = schema?.output || ["success", "method", "path", "agentName", "agentType", "prompt", "response"];
const filteredResponseData = Object.fromEntries(
Object.entries(baseResponseData).filter(([key]) =>
expectedOutputFields.includes(key),
),
);
return c.json(filteredResponseData);
} catch (error) {
console.error(`Error in route agent ${method} ${path}:`, error);
return c.json({
success: false,
method,
path,
error: "Internal server error",
message: error instanceof Error ? error.message : "Unknown error occurred",
}, 500);
}
};
}