diff --git a/server/agentRegistry.ts b/server/agentRegistry.ts new file mode 100644 index 0000000..3becd5b --- /dev/null +++ b/server/agentRegistry.ts @@ -0,0 +1,29 @@ +import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { personalAssistantAgent } from "./agents/personalAssistant.js"; +import { chefAgent } from "./agents/chef.js"; +import { Agent } from "./types.js"; + +// This declaration is primarily for providing type hints in your code +interface Env { + OPENROUTER_API_KEY: string; +} + +declare global { + const env: Env; +} + +// Create OpenRouter instance +export const openrouter = createOpenRouter({ + apiKey: import.meta.env.VITE_OPENROUTER_API_KEY || env.OPENROUTER_API_KEY, +}); + +// Define the agents by ID +export const agentsById: Record = { + "personal-assistant": personalAssistantAgent, + chef: chefAgent, +}; + +// Helper function to get an agent by ID +export function getAgentById(agentId: string): Agent | undefined { + return agentsById[agentId]; +} diff --git a/server/agents/personalAssistant.ts b/server/agents/personalAssistant.ts index c15cd72..29ab1e4 100644 --- a/server/agents/personalAssistant.ts +++ b/server/agents/personalAssistant.ts @@ -78,6 +78,18 @@ export const personalAssistantAgent: Agent = { // } // }, }), + echo: tool({ + description: "Echoes the message.", + parameters: jsonSchema<{ message: string }>({ + type: "object", + properties: { + message: { + type: "string", + description: "The message to echo.", + }, + }, + }), + }), // say: tool({ // description: "Say something.", diff --git a/server/tools.ts b/server/tools.ts index 71f5cf7..e601dba 100644 --- a/server/tools.ts +++ b/server/tools.ts @@ -1,5 +1,68 @@ -export default { - delegate: async function () { - return "Here's a vegetarian lasagna recipe for 4 people:"; +import { streamText } from "ai"; +import { getAgentById, openrouter } from "./agentRegistry.js"; + +// Define the tools with explicit type +export interface DelegateParams { + agentId?: string; + prompt?: string; + [key: string]: any; +} + +export interface EchoParams { + message: string; +} + +interface ToolFunctions { + [key: string]: (params: any) => Promise; +} + +const tools: ToolFunctions = { + delegate: async function ({ + agentId, + prompt, + }: DelegateParams): Promise { + // Validate required parameters + if (!agentId || !prompt) { + return "Error: Missing required parameters. Both 'agentId' and 'prompt' are required."; + } + + // Find the target agent + const agent = getAgentById(agentId); + if (!agent) { + return `Error: No such agent: ${agentId}`; + } + + try { + // Generate a response from the agent using the prompt + const result = streamText({ + model: openrouter(agent.modelName), + messages: [ + { role: "system", content: agent.systemMessage }, + { role: "user", content: prompt }, + ], + tools: agent.tools, + }); + + // Collect the response text + let responseText = ""; + + // The textStream is already an AsyncIterable, no need to call it as a function + for await (const chunk of result.textStream) { + responseText += chunk; + } + + // Return the agent's response + return responseText; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error("Error delegating to agent:", error); + return `Error delegating to agent ${agentId}: ${errorMessage}`; + } + }, + echo: async function ({ message }: EchoParams): Promise { + return message; }, }; + +export default tools; diff --git a/server/util.ts b/server/util.ts index 077b4c9..746e340 100644 --- a/server/util.ts +++ b/server/util.ts @@ -1,11 +1,15 @@ -import { Message } from "ai"; +import { formatDataStreamPart, Message } from "ai"; import tools from "./tools.js"; +import { DelegateParams } from "./tools.js"; export function singleSpace(str: string) { return str.replace(/\s+/g, " "); } -export async function processPendingToolCalls(messages: Message[]) { +export async function processPendingToolCalls( + messages: Message[], + dataStreamWriter: any +) { const lastMessage = messages[messages.length - 1]; if (!lastMessage) { return; @@ -13,18 +17,99 @@ export async function processPendingToolCalls(messages: Message[]) { if (!lastMessage.parts) { return; } + + console.log("Processing pending tool calls in message:", lastMessage.id); + /** Execute all the pending tool calls: */ lastMessage.parts = await Promise.all( - lastMessage.parts?.map(async (part) => { - const toolInvocation = part.toolInvocation; - if (toolInvocation?.state === "call") { - toolInvocation.state = "result"; - toolInvocation.result = - toolInvocation.result === "yes" - ? await tools[toolInvocation.toolName]?.() - : "Error: User denied tool call."; + lastMessage.parts?.map(async (part: any) => { + // Check if this part has a tool invocation + if (part.type === "tool-invocation" && part.toolInvocation) { + const toolInvocation = part.toolInvocation as any; + console.log("Found tool invocation:", toolInvocation); + + if (toolInvocation.state === "result") { + // Check if user approved the tool call + if (toolInvocation.result === "yes") { + try { + // Get the tool function + const toolName = toolInvocation.toolName || toolInvocation.name; + console.log(`Executing tool: ${toolName}`); + + const toolFunction = tools[toolName as keyof typeof tools]; + + if (toolFunction) { + // Extract parameters from the tool invocation + let parameters = {}; + try { + if (toolInvocation.parameters) { + parameters = JSON.parse(toolInvocation.parameters); + } else if (toolInvocation.args) { + parameters = toolInvocation.args; + } + console.log(`Tool parameters:`, parameters); + } catch (e) { + console.error("Error parsing tool parameters:", e); + } + + // Call the tool function with the parameters + console.log( + `Calling tool function with parameters:`, + parameters + ); + const result = await toolFunction(parameters as DelegateParams); + console.log(`Tool result:`, result); + + // forward updated tool result to the client: + dataStreamWriter.write( + formatDataStreamPart("tool_result", { + toolCallId: toolInvocation.toolCallId, + result, + }) + ); + + // update the message part: + return { + ...part, + toolInvocation: { ...toolInvocation, result }, + }; + + // // Set the result + // toolInvocation.result = result; + + // // Add a new message with the tool result + // if (result) { + // messages.push({ + // id: `tool-result-${Date.now()}`, + // role: "assistant", + // content: `Tool Result: ${result}`, + // parts: [ + // { + // type: "text", + // text: `Tool Result: ${result}`, + // }, + // ], + // }); + // } + } else { + const errorMsg = `Error: Tool '${toolName}' not found`; + console.error(errorMsg); + toolInvocation.result = errorMsg; + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error("Error executing tool:", error); + toolInvocation.result = `Error executing tool: ${errorMessage}`; + } + } else if (toolInvocation.result === "no") { + toolInvocation.result = "Error: User denied tool call."; + } + } } return part; }) ?? [] ); + + console.log("Finished processing tool calls. Updated messages:", messages); } diff --git a/server/worker.ts b/server/worker.ts index a3c5353..879ba94 100644 --- a/server/worker.ts +++ b/server/worker.ts @@ -1,35 +1,11 @@ -import { createOpenRouter } from "@openrouter/ai-sdk-provider"; -import { streamText, Message } from "ai"; +import { streamText, Message, createDataStream } from "ai"; import { Hono } from "hono"; import { stream } from "hono/streaming"; -import { personalAssistantAgent } from "./agents/personalAssistant.js"; -import { chefAgent } from "./agents/chef.js"; -import { Agent } from "./types.js"; import { processPendingToolCalls } from "./util.js"; - -// This declaration is primarily for providing type hints in your code -// and it doesn't directly define the *values* of the environment variables. - -interface Env { - OPENROUTER_API_KEY: string; - // Add other environment variables here -} - -declare global { - const env: Env; -} +import { agentsById, openrouter } from "./agentRegistry.js"; const app = new Hono(); -const openrouter = createOpenRouter({ - apiKey: import.meta.env.VITE_OPENROUTER_API_KEY || env.OPENROUTER_API_KEY, -}); - -const agentsById: Record = { - "personal-assistant": personalAssistantAgent, - chef: chefAgent, -}; - app.post("/api/chat/:agent_id", async (c) => { const input: { messages: Message[] } = await c.req.json(); const agentId = c.req.param("agent_id"); @@ -38,26 +14,43 @@ app.post("/api/chat/:agent_id", async (c) => { c.status(404); return c.json({ error: `No such agent: ${agentId}` }); } - await processPendingToolCalls(input.messages); - const result = streamText({ - model: openrouter(agent.modelName), - maxSteps: 5, - messages: [ - { role: "system", content: agent.systemMessage }, - ...Object.values(agentsById).map((agent) => ({ - role: "system" as const, - content: `Agent ${JSON.stringify({ - id: agent.id, - name: agent.name, - description: agent.description, - skills: agent.skills, - })}`, - })), - ...input.messages, - ], - tools: agent.tools, + + const dataStream = createDataStream({ + execute: async (dataStreamWriter) => { + // dataStreamWriter.writeData('initialized call'); + + // Process any pending tool calls in the messages + // This modifies the messages array in place + await processPendingToolCalls(input.messages, dataStreamWriter); + + const result = streamText({ + model: openrouter(agent.modelName), + maxSteps: 5, + messages: [ + { role: "system", content: agent.systemMessage }, + ...Object.values(agentsById).map((agent) => ({ + role: "system" as const, + content: `Agent ${JSON.stringify({ + id: agent.id, + name: agent.name, + description: agent.description, + skills: agent.skills, + })}`, + })), + ...input.messages, + ], + tools: agent.tools, + onError: (error) => { + console.error("Error in streamText:", error); + }, + }); + + result.mergeIntoDataStream(dataStreamWriter); + }, onError: (error) => { - console.log(error); + // Error messages are masked by default for security reasons. + // If you want to expose the error message to the client, you can do so here: + return error instanceof Error ? error.message : String(error); }, }); @@ -65,7 +58,9 @@ app.post("/api/chat/:agent_id", async (c) => { c.header("X-Vercel-AI-Data-Stream", "v1"); c.header("Content-Type", "text/plain; charset=utf-8"); - return stream(c, (stream) => stream.pipe(result.toDataStream())); + return stream(c, (stream) => + stream.pipe(dataStream.pipeThrough(new TextEncoderStream())) + ); }); export default app;