feat: personalAssistant successfully delegates to other agents
This commit is contained in:
@@ -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<string, Agent> = {
|
||||||
|
"personal-assistant": personalAssistantAgent,
|
||||||
|
chef: chefAgent,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get an agent by ID
|
||||||
|
export function getAgentById(agentId: string): Agent | undefined {
|
||||||
|
return agentsById[agentId];
|
||||||
|
}
|
||||||
@@ -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({
|
// say: tool({
|
||||||
// description: "Say something.",
|
// description: "Say something.",
|
||||||
|
|||||||
+66
-3
@@ -1,5 +1,68 @@
|
|||||||
export default {
|
import { streamText } from "ai";
|
||||||
delegate: async function () {
|
import { getAgentById, openrouter } from "./agentRegistry.js";
|
||||||
return "Here's a vegetarian lasagna recipe for 4 people:";
|
|
||||||
|
// 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<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools: ToolFunctions = {
|
||||||
|
delegate: async function ({
|
||||||
|
agentId,
|
||||||
|
prompt,
|
||||||
|
}: DelegateParams): Promise<string> {
|
||||||
|
// 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<string> {
|
||||||
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default tools;
|
||||||
|
|||||||
+95
-10
@@ -1,11 +1,15 @@
|
|||||||
import { Message } from "ai";
|
import { formatDataStreamPart, Message } from "ai";
|
||||||
import tools from "./tools.js";
|
import tools from "./tools.js";
|
||||||
|
import { DelegateParams } 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[]) {
|
export async function processPendingToolCalls(
|
||||||
|
messages: Message[],
|
||||||
|
dataStreamWriter: any
|
||||||
|
) {
|
||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
if (!lastMessage) {
|
if (!lastMessage) {
|
||||||
return;
|
return;
|
||||||
@@ -13,18 +17,99 @@ export async function processPendingToolCalls(messages: Message[]) {
|
|||||||
if (!lastMessage.parts) {
|
if (!lastMessage.parts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Processing pending tool calls in message:", lastMessage.id);
|
||||||
|
|
||||||
/** Execute all the pending tool calls: */
|
/** Execute all the pending tool calls: */
|
||||||
lastMessage.parts = await Promise.all(
|
lastMessage.parts = await Promise.all(
|
||||||
lastMessage.parts?.map(async (part) => {
|
lastMessage.parts?.map(async (part: any) => {
|
||||||
const toolInvocation = part.toolInvocation;
|
// Check if this part has a tool invocation
|
||||||
if (toolInvocation?.state === "call") {
|
if (part.type === "tool-invocation" && part.toolInvocation) {
|
||||||
toolInvocation.state = "result";
|
const toolInvocation = part.toolInvocation as any;
|
||||||
toolInvocation.result =
|
console.log("Found tool invocation:", toolInvocation);
|
||||||
toolInvocation.result === "yes"
|
|
||||||
? await tools[toolInvocation.toolName]?.()
|
if (toolInvocation.state === "result") {
|
||||||
: "Error: User denied tool call.";
|
// 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;
|
return part;
|
||||||
}) ?? []
|
}) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("Finished processing tool calls. Updated messages:", messages);
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-46
@@ -1,35 +1,11 @@
|
|||||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
import { streamText, Message, createDataStream } 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/personalAssistant.js";
|
|
||||||
import { chefAgent } from "./agents/chef.js";
|
|
||||||
import { Agent } from "./types.js";
|
|
||||||
import { processPendingToolCalls } from "./util.js";
|
import { processPendingToolCalls } from "./util.js";
|
||||||
|
import { agentsById, openrouter } from "./agentRegistry.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
const openrouter = createOpenRouter({
|
|
||||||
apiKey: import.meta.env.VITE_OPENROUTER_API_KEY || env.OPENROUTER_API_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
const agentsById: Record<string, Agent> = {
|
|
||||||
"personal-assistant": personalAssistantAgent,
|
|
||||||
chef: chefAgent,
|
|
||||||
};
|
|
||||||
|
|
||||||
app.post("/api/chat/:agent_id", async (c) => {
|
app.post("/api/chat/:agent_id", async (c) => {
|
||||||
const input: { messages: Message[] } = await c.req.json();
|
const input: { messages: Message[] } = await c.req.json();
|
||||||
const agentId = c.req.param("agent_id");
|
const agentId = c.req.param("agent_id");
|
||||||
@@ -38,26 +14,43 @@ app.post("/api/chat/:agent_id", async (c) => {
|
|||||||
c.status(404);
|
c.status(404);
|
||||||
return c.json({ error: `No such agent: ${agentId}` });
|
return c.json({ error: `No such agent: ${agentId}` });
|
||||||
}
|
}
|
||||||
await processPendingToolCalls(input.messages);
|
|
||||||
const result = streamText({
|
const dataStream = createDataStream({
|
||||||
model: openrouter(agent.modelName),
|
execute: async (dataStreamWriter) => {
|
||||||
maxSteps: 5,
|
// dataStreamWriter.writeData('initialized call');
|
||||||
messages: [
|
|
||||||
{ role: "system", content: agent.systemMessage },
|
// Process any pending tool calls in the messages
|
||||||
...Object.values(agentsById).map((agent) => ({
|
// This modifies the messages array in place
|
||||||
role: "system" as const,
|
await processPendingToolCalls(input.messages, dataStreamWriter);
|
||||||
content: `Agent ${JSON.stringify({
|
|
||||||
id: agent.id,
|
const result = streamText({
|
||||||
name: agent.name,
|
model: openrouter(agent.modelName),
|
||||||
description: agent.description,
|
maxSteps: 5,
|
||||||
skills: agent.skills,
|
messages: [
|
||||||
})}`,
|
{ role: "system", content: agent.systemMessage },
|
||||||
})),
|
...Object.values(agentsById).map((agent) => ({
|
||||||
...input.messages,
|
role: "system" as const,
|
||||||
],
|
content: `Agent ${JSON.stringify({
|
||||||
tools: agent.tools,
|
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) => {
|
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("X-Vercel-AI-Data-Stream", "v1");
|
||||||
c.header("Content-Type", "text/plain; charset=utf-8");
|
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;
|
export default app;
|
||||||
|
|||||||
Reference in New Issue
Block a user