add: human-in-the-loop tool-calling

master
Avraham Sakal 2 months ago
parent e292cebb1d
commit 8b8c92f1d6

@ -5,6 +5,7 @@ import { singleSpace } from "../util.js";
export const personalAssistantAgent: Agent = { export const personalAssistantAgent: Agent = {
id: "personal-assistant", id: "personal-assistant",
name: "Personal Assistant", name: "Personal Assistant",
// modelName: "qwen/qwen3-32b:free",
// modelName: "mistral/ministral-8b", // modelName: "mistral/ministral-8b",
modelName: "google/gemini-2.5-flash-preview", modelName: "google/gemini-2.5-flash-preview",
systemMessage: systemMessage:
@ -78,21 +79,21 @@ export const personalAssistantAgent: Agent = {
// }, // },
}), }),
say: tool({ // say: tool({
description: "Say something.", // description: "Say something.",
parameters: jsonSchema<{ message: string }>({ // parameters: jsonSchema<{ message: string }>({
type: "object", // type: "object",
properties: { // properties: {
message: { // message: {
type: "string", // type: "string",
description: "The message to say.", // description: "The message to say.",
}, // },
}, // },
}), // }),
execute: async ({ message }: { message: string }) => { // execute: async ({ message }: { message: string }) => {
console.log(message); // return message;
}, // },
}), // }),
exit: tool({ exit: tool({
description: "Exits the conversation.", description: "Exits the conversation.",
parameters: jsonSchema<{ exitCode: number }>({ parameters: jsonSchema<{ exitCode: number }>({

@ -0,0 +1,5 @@
export default {
delegate: async function () {
return "Here's a vegetarian lasagna recipe for 4 people:";
},
};

@ -1,3 +1,30 @@
import { Message } from "ai";
import tools from "./tools.js";
export function singleSpace(str: string) { export function singleSpace(str: string) {
return str.replace(/\s+/g, " "); 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;
}) ?? []
);
}

@ -1,10 +1,11 @@
import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { streamText } from "ai"; import { streamText, Message } from "ai";
import { Hono } from "hono"; import { Hono } from "hono";
import { stream } from "hono/streaming"; import { stream } from "hono/streaming";
import { personalAssistantAgent } from "./agents/assistant.js"; import { personalAssistantAgent } from "./agents/personalAssistant.js";
import { chefAgent } from "./agents/chef.js"; import { chefAgent } from "./agents/chef.js";
import { Agent } from "./types.js"; import { Agent } from "./types.js";
import { processPendingToolCalls } from "./util.js";
// This declaration is primarily for providing type hints in your code // This declaration is primarily for providing type hints in your code
// and it doesn't directly define the *values* of the environment variables. // 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, apiKey: import.meta.env.VITE_OPENROUTER_API_KEY || env.OPENROUTER_API_KEY,
}); });
const agentsByName: Record<string, Agent> = { const agentsById: Record<string, Agent> = {
assistant: personalAssistantAgent, "personal-assistant": personalAssistantAgent,
chef: chefAgent, chef: chefAgent,
}; };
app.post("/api/chat/:agent_name", async (c) => { app.post("/api/chat/:agent_id", async (c) => {
const input = await c.req.json(); const input: { messages: Message[] } = await c.req.json();
const agentName = c.req.param("agent_name"); const agentId = c.req.param("agent_id");
const agent = agentsByName[agentName]; const agent = agentsById[agentId];
if (!agent) { if (!agent) {
c.status(404); 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({ const result = streamText({
model: openrouter(agent.modelName), model: openrouter(agent.modelName),
maxSteps: 5,
messages: [ messages: [
{ role: "system", content: agent.systemMessage }, { 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, ...input.messages,
], ],
tools: agent.tools, tools: agent.tools,
onError: (error) => {
console.log(error);
},
}); });
// Mark the response as a v1 data stream: // Mark the response as a v1 data stream:

@ -8,7 +8,8 @@ export const Route = createFileRoute("/chats/$agentName")({
function Chat() { function Chat() {
const { agentName } = Route.useParams(); const { agentName } = Route.useParams();
const { messages, input, handleInputChange, handleSubmit } = useChat( const { messages, input, handleInputChange, handleSubmit, addToolResult } =
useChat(
/*{ /*{
api: "http://localhost:8787/api/chat", api: "http://localhost:8787/api/chat",
}*/ { api: `/api/chat/${agentName}` } }*/ { api: `/api/chat/${agentName}` }
@ -17,34 +18,61 @@ function Chat() {
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => ( {messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap"> <div key={message.id} className="whitespace-pre-wrap">
<span className="py-1 px-2 bg-gray-200 rounded-4">
{message.role === "user" ? "User: " : "AI: "} {message.role === "user" ? "User: " : "AI: "}
</span>
{message.parts.map((part, i) => { {message.parts.map((part, i) => {
switch (part.type) { switch (part.type) {
case "text": case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>; return <span key={`${message.id}-${i}`}>{part.text}</span>;
case "tool-invocation": case "tool-invocation":
return ( return (
<pre key={`${message.id}-${i}`}> <div key={`${message.id}-${i}`}>
{JSON.stringify(part.toolInvocation, null, 2)} <pre>{JSON.stringify(part.toolInvocation, null, 2)}</pre>
</pre> {part.toolInvocation.state === "call" ? (
<div className="flex gap-2">
<span>Continue?</span>
<button
onClick={() => {
addToolResult({
toolCallId: part.toolInvocation.toolCallId,
result: "yes",
});
}}
>
Yes
</button>
<button
onClick={() => {
addToolResult({
toolCallId: part.toolInvocation.toolCallId,
result: "no",
});
}}
>
No
</button>
</div>
) : null}
</div>
); );
case "reasoning": case "reasoning":
return ( return (
<div key={`${message.id}-${i}`}> <span key={`${message.id}-${i}`}>
(Reasoning) {part.reasoning} (Reasoning) {part.reasoning}
</div> </span>
); );
case "step-start": case "step-start":
return ( return (
<div key={`${message.id}-${i}`}> <span key={`${message.id}-${i}`}>
(Step Start) {JSON.stringify(part)} (Step Start) {JSON.stringify(part)}
</div> </span>
); );
case "source": case "source":
return ( return (
<div key={`${message.id}-${i}`}> <span key={`${message.id}-${i}`}>
(Source) {JSON.stringify(part.source)} (Source) {JSON.stringify(part.source)}
</div> </span>
); );
case "file": case "file":
return ( return (
@ -55,9 +83,9 @@ function Chat() {
); );
default: default:
return ( return (
<div key={`${message.id}-${i}`}> <span key={`${message.id}-${i}`}>
(?) {JSON.stringify(part)} (?) {JSON.stringify(part)}
</div> </span>
); );
} }
})} })}

Loading…
Cancel
Save