Compare commits

...

8 Commits

@ -0,0 +1,2 @@
.env*
.dev.vars*

@ -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];
}

@ -3,7 +3,7 @@ import { Agent } from "../types.js";
export const chefAgent: Agent = {
id: "chef",
name: "Chef",
modelName: "mistral/ministral-8b",
modelName: "mistralai/mistral-nemo",
systemMessage:
"You are a master chef who is famous for his creative and original recipes, but also knows many standard, tried-and-true recipes.",
description:

@ -5,8 +5,10 @@ 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",
// modelName: "google/gemini-2.5-flash-preview",
modelName: "mistralai/mistral-nemo",
systemMessage:
singleSpace(`You are a personal assistant Agent who is helpful, friendly, and
responsible. You are my liason to other Agents who have specialized
@ -77,22 +79,34 @@ export const personalAssistantAgent: Agent = {
// }
// },
}),
say: tool({
description: "Say something.",
echo: tool({
description: "Echoes the message.",
parameters: jsonSchema<{ message: string }>({
type: "object",
properties: {
message: {
type: "string",
description: "The message to say.",
description: "The message to echo.",
},
},
}),
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 }>({
@ -123,7 +137,7 @@ export const personalAssistantAgent: Agent = {
},
},
}),
execute: async ({ min, max }: { min: number; max: number }) => {
execute: async ({ min = 0, max = 1 }: { min?: number; max?: number }) => {
return Math.random() * (max - min) + min;
},
}),

@ -0,0 +1,107 @@
import {
streamText,
generateText,
generateObject,
type Message,
jsonSchema,
} 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<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 {
const conversation: { messages: Array<Omit<Message, "id">> } = {
messages: [
{ role: "system", content: agent.systemMessage },
{ role: "user", content: prompt },
],
};
let isConversationDone = false;
while (!isConversationDone) {
// Generate a response from the agent using the prompt
const result = await generateText({
model: openrouter(agent.modelName),
messages: conversation.messages,
tools: agent.tools,
});
conversation.messages.push({ role: "assistant", content: result.text });
const sentimentIsConversationDone = await generateObject<{
isConversationDone: boolean;
}>({
model: openrouter("mistralai/mistral-nemo"),
messages: [
{
role: "system",
content:
"You are a tool to determine whether a conversation is done or should continue with another reply.",
},
{
role: "user",
content: conversation.messages
.map((message) => `${message.role}: ${message.content}`)
.join("\n"),
},
],
schema: jsonSchema({
type: "object",
properties: {
isConversationDone: {
type: "boolean",
description:
"Whether the conversation is done or should continue and requires a reply.",
},
},
}),
});
isConversationDone =
sentimentIsConversationDone.object.isConversationDone;
}
// const summaryText = await generateSummary();
// // Return the agent's response
// return summaryText;
return conversation.messages
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
} 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;

@ -1,3 +1,115 @@
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[],
dataStreamWriter: any
) {
const lastMessage = messages[messages.length - 1];
if (!lastMessage) {
return;
}
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: 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);
}

@ -1,77 +1,66 @@
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { streamText } from "ai";
import { streamText, Message, createDataStream } from "ai";
import { Hono } from "hono";
import { stream } from "hono/streaming";
import { personalAssistantAgent } from "./agents/assistant.js";
import { chefAgent } from "./agents/chef.js";
import { Agent } from "./types.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 { processPendingToolCalls } from "./util.js";
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 systemMessage = {
role: "system",
content:
"You are a wise old man named Dorf that answers questions succintly.",
};
app.post("/api/chat", async (c) => {
const input = await c.req.json();
console.log(input);
const result = streamText({
model: openrouter("mistral/ministral-8b"),
messages: [systemMessage, ...input.messages],
tools: {},
});
// Mark the response as a v1 data stream:
c.header("X-Vercel-AI-Data-Stream", "v1");
c.header("Content-Type", "text/plain; charset=utf-8");
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: ${agentId}` });
}
return stream(c, (stream) => stream.pipe(result.toDataStream()));
});
const dataStream = createDataStream({
execute: async (dataStreamWriter) => {
// dataStreamWriter.writeData('initialized call');
const agentsByName: Record<string, Agent> = {
assistant: personalAssistantAgent,
chef: chefAgent,
};
// Process any pending tool calls in the messages
// This modifies the messages array in place
await processPendingToolCalls(input.messages, dataStreamWriter);
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];
if (!agent) {
return c.json({ error: `No such agent: ${agentName}` });
}
console.log(input);
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) => {
// 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);
},
});
// Mark the response as a v1 data stream:
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;

@ -5,8 +5,6 @@ import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
import "virtual:uno.css";
// import App from "./App.tsx";
// Create a new router instance
const router = createRouter({ routeTree });

@ -1,6 +1,5 @@
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
// import "./__root.css";
export const Route = createRootRoute({
component: () => (

@ -8,7 +8,8 @@ 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}` }
@ -17,34 +18,61 @@ function Chat() {
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap">
<span className="py-1 px-2 bg-gray-200 rounded-4">
{message.role === "user" ? "User: " : "AI: "}
</span>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
return <span key={`${message.id}-${i}`}>{part.text}</span>;
case "tool-invocation":
return (
<pre key={`${message.id}-${i}`}>
{JSON.stringify(part.toolInvocation, null, 2)}
</pre>
<div key={`${message.id}-${i}`}>
<pre>{JSON.stringify(part.toolInvocation, null, 2)}</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":
return (
<div key={`${message.id}-${i}`}>
<span key={`${message.id}-${i}`}>
(Reasoning) {part.reasoning}
</div>
</span>
);
case "step-start":
return (
<div key={`${message.id}-${i}`}>
<span key={`${message.id}-${i}`}>
(Step Start) {JSON.stringify(part)}
</div>
</span>
);
case "source":
return (
<div key={`${message.id}-${i}`}>
<span key={`${message.id}-${i}`}>
(Source) {JSON.stringify(part.source)}
</div>
</span>
);
case "file":
return (
@ -55,9 +83,9 @@ function Chat() {
);
default:
return (
<div key={`${message.id}-${i}`}>
<span key={`${message.id}-${i}`}>
(?) {JSON.stringify(part)}
</div>
</span>
);
}
})}

@ -2,7 +2,6 @@ import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import reactLogo from "../assets/react.svg";
import viteLogo from "/vite.svg";
// import "./index.css";
export const Route = createFileRoute("/")({
component: App,

Loading…
Cancel
Save