From 8b8c92f1d62a77cb2e3145c1c13d386401d44d53 Mon Sep 17 00:00:00 2001 From: Avraham Sakal Date: Sun, 4 May 2025 14:04:50 -0400 Subject: [PATCH] add: human-in-the-loop tool-calling --- .../{assistant.ts => personalAssistant.ts} | 31 +++++----- server/tools.ts | 5 ++ server/util.ts | 27 +++++++++ server/worker.ts | 34 +++++++---- src/routes/chats/$agentName.tsx | 60 ++++++++++++++----- 5 files changed, 116 insertions(+), 41 deletions(-) rename server/agents/{assistant.ts => personalAssistant.ts} (89%) create mode 100644 server/tools.ts diff --git a/server/agents/assistant.ts b/server/agents/personalAssistant.ts similarity index 89% rename from server/agents/assistant.ts rename to server/agents/personalAssistant.ts index 66bc993..c15cd72 100644 --- a/server/agents/assistant.ts +++ b/server/agents/personalAssistant.ts @@ -5,6 +5,7 @@ import { singleSpace } from "../util.js"; export const personalAssistantAgent: Agent = { id: "personal-assistant", name: "Personal Assistant", + // modelName: "qwen/qwen3-32b:free", // modelName: "mistral/ministral-8b", modelName: "google/gemini-2.5-flash-preview", systemMessage: @@ -78,21 +79,21 @@ export const personalAssistantAgent: Agent = { // }, }), - say: tool({ - description: "Say something.", - parameters: jsonSchema<{ message: string }>({ - type: "object", - properties: { - message: { - type: "string", - description: "The message to say.", - }, - }, - }), - execute: async ({ message }: { message: string }) => { - console.log(message); - }, - }), + // say: tool({ + // description: "Say something.", + // parameters: jsonSchema<{ message: string }>({ + // type: "object", + // properties: { + // message: { + // type: "string", + // description: "The message to say.", + // }, + // }, + // }), + // execute: async ({ message }: { message: string }) => { + // return message; + // }, + // }), exit: tool({ description: "Exits the conversation.", parameters: jsonSchema<{ exitCode: number }>({ diff --git a/server/tools.ts b/server/tools.ts new file mode 100644 index 0000000..71f5cf7 --- /dev/null +++ b/server/tools.ts @@ -0,0 +1,5 @@ +export default { + delegate: async function () { + return "Here's a vegetarian lasagna recipe for 4 people:"; + }, +}; diff --git a/server/util.ts b/server/util.ts index e3b5af1..077b4c9 100644 --- a/server/util.ts +++ b/server/util.ts @@ -1,3 +1,30 @@ +import { Message } from "ai"; +import tools from "./tools.js"; + export function singleSpace(str: string) { return str.replace(/\s+/g, " "); } + +export async function processPendingToolCalls(messages: Message[]) { + const lastMessage = messages[messages.length - 1]; + if (!lastMessage) { + return; + } + if (!lastMessage.parts) { + return; + } + /** 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."; + } + return part; + }) ?? [] + ); +} diff --git a/server/worker.ts b/server/worker.ts index 2911999..a3c5353 100644 --- a/server/worker.ts +++ b/server/worker.ts @@ -1,10 +1,11 @@ import { createOpenRouter } from "@openrouter/ai-sdk-provider"; -import { streamText } from "ai"; +import { streamText, Message } from "ai"; import { Hono } from "hono"; import { stream } from "hono/streaming"; -import { personalAssistantAgent } from "./agents/assistant.js"; +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. @@ -24,27 +25,40 @@ const openrouter = createOpenRouter({ apiKey: import.meta.env.VITE_OPENROUTER_API_KEY || env.OPENROUTER_API_KEY, }); -const agentsByName: Record = { - assistant: personalAssistantAgent, +const agentsById: Record = { + "personal-assistant": personalAssistantAgent, chef: chefAgent, }; -app.post("/api/chat/:agent_name", async (c) => { - const input = await c.req.json(); - const agentName = c.req.param("agent_name"); - const agent = agentsByName[agentName]; +app.post("/api/chat/:agent_id", async (c) => { + const input: { messages: Message[] } = await c.req.json(); + const agentId = c.req.param("agent_id"); + const agent = agentsById[agentId]; if (!agent) { c.status(404); - return c.json({ error: `No such agent: ${agentName}` }); + return c.json({ error: `No such agent: ${agentId}` }); } - console.log(input); + 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, + onError: (error) => { + console.log(error); + }, }); // Mark the response as a v1 data stream: diff --git a/src/routes/chats/$agentName.tsx b/src/routes/chats/$agentName.tsx index 5bc43bb..eb98bd6 100644 --- a/src/routes/chats/$agentName.tsx +++ b/src/routes/chats/$agentName.tsx @@ -8,43 +8,71 @@ export const Route = createFileRoute("/chats/$agentName")({ function Chat() { const { agentName } = Route.useParams(); - const { messages, input, handleInputChange, handleSubmit } = useChat( - /*{ + const { messages, input, handleInputChange, handleSubmit, addToolResult } = + useChat( + /*{ api: "http://localhost:8787/api/chat", }*/ { api: `/api/chat/${agentName}` } - ); + ); return (
{messages.map((message) => (
- {message.role === "user" ? "User: " : "AI: "} + + {message.role === "user" ? "User: " : "AI: "} + {message.parts.map((part, i) => { switch (part.type) { case "text": - return
{part.text}
; + return {part.text}; case "tool-invocation": return ( -
-                    {JSON.stringify(part.toolInvocation, null, 2)}
-                  
+
+
{JSON.stringify(part.toolInvocation, null, 2)}
+ {part.toolInvocation.state === "call" ? ( +
+ Continue? + + +
+ ) : null} +
); case "reasoning": return ( -
+ (Reasoning) {part.reasoning} -
+ ); case "step-start": return ( -
+ (Step Start) {JSON.stringify(part)} -
+ ); case "source": return ( -
+ (Source) {JSON.stringify(part.source)} -
+ ); case "file": return ( @@ -55,9 +83,9 @@ function Chat() { ); default: return ( -
+ (?) {JSON.stringify(part)} -
+ ); } })}