add: human-in-the-loop tool-calling
This commit is contained in:
@@ -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;
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+24
-10
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user