diff --git a/examples/ai-agent-vercel/README.md b/examples/ai-agent-vercel/README.md index 041431340a..20e4b758c0 100644 --- a/examples/ai-agent-vercel/README.md +++ b/examples/ai-agent-vercel/README.md @@ -3,42 +3,42 @@ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Frivet-gg%2Frivet%2Ftree%2Fmain%2Fexamples%2Fai-agent-vercel&project-name=ai-agent-vercel) -# AI Agent Chat +# AI Agent -Example project demonstrating AI agent integration. +Example project demonstrating queue-driven Rivet Actor AI agents with streaming Vercel AI SDK responses. ## Getting Started ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/ai-agent -npm install -npm run dev +pnpm install +pnpm dev ``` - ## Features -- **AI SDK integration**: Use Vercel AI SDK with OpenAI within Rivet Actors -- **Persistent conversation state**: Message history automatically persisted across actor restarts -- **Real-time updates**: Broadcast AI responses to connected clients using actor events -- **Tool calling**: Integrate custom tools (weather lookup) that AI can invoke +- Actor-per-agent pattern with a coordinating manager Rivet Actor +- Queue-based intake using `c.queue.next` inside the run loop +- Streaming AI responses sent to the UI as they arrive +- Persistent history stored in Rivet Actor state +- Live status updates via events and polling -## Implementation +## Prerequisites -The AI agent is implemented as a Rivet Actor that maintains conversation state and integrates with OpenAI. Key implementation details: +- OpenAI API key set as `OPENAI_API_KEY` -- **Actor Definition** ([`src/backend/registry.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/backend/registry.ts)): Defines the `aiAgent` actor with persistent message history -- **Custom Tools** ([`src/backend/my-tools.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/backend/my-tools.ts)): Implements the weather lookup tool that the AI can invoke -- **Message Types** ([`src/backend/types.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/backend/types.ts)): TypeScript types for message structure +## Implementation -## Prerequisites +The AgentManager creates and tracks agent actors, while each AI agent Rivet Actor consumes queue messages in `run` and streams responses with the Vercel AI SDK. -- OpenAI API Key (set as `OPENAI_API_KEY` environment variable) +- **Actor definitions and queues** ([`src/actors.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/actors.ts)) +- **Frontend orchestration** ([`frontend/App.tsx`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/frontend/App.tsx)) +- **Server entry point** ([`src/server.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/server.ts)) ## Resources -Read more about [actions](/docs/actors/actions), [state](/docs/actors/state), and [events](/docs/actors/events). +Read more about [queues](https://rivet.dev/docs/actors/queues), [run handlers](https://rivet.dev/docs/actors/run), [state](https://rivet.dev/docs/actors/state), and [events](https://rivet.dev/docs/actors/events). ## License diff --git a/examples/ai-agent-vercel/frontend/App.tsx b/examples/ai-agent-vercel/frontend/App.tsx index cebc26c97f..900945b812 100644 --- a/examples/ai-agent-vercel/frontend/App.tsx +++ b/examples/ai-agent-vercel/frontend/App.tsx @@ -1,80 +1,244 @@ import { createRivetKit } from "@rivetkit/react"; import { useEffect, useState } from "react"; -import { registry } from "../src/actors.ts"; -import type { Message } from "../src/types.ts"; +import type { + AgentInfo, + AgentMessage, + AgentStatus, + registry, +} from "../src/actors.ts"; -const { useActor } = createRivetKit(`${window.location.origin}/api/rivet`); +const { useActor } = createRivetKit( + `${location.origin}/api/rivet`, +); -export function App() { - const aiAgent = useActor({ - name: "aiAgent", - key: ["default"], +type ResponseEvent = { + messageId: string; + delta: string; + content: string; + done: boolean; + error?: string; +}; + +function formatTime(timestamp: number) { + return new Date(timestamp).toLocaleTimeString(); +} + +function AgentPanel({ info }: { info: AgentInfo }) { + const agent = useActor({ + name: "agent", + key: [info.id], }); - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); + const [status, setStatus] = useState(null); const [input, setInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (aiAgent.connection) { - aiAgent.connection.getMessages().then(setMessages); + if (!agent.connection) { + return; } - }, [aiAgent.connection]); - aiAgent.useEvent("messageReceived", (message: Message) => { - setMessages((prev) => [...prev, message]); - setIsLoading(false); + agent.connection.getHistory().then(setMessages); + agent.connection.getStatus().then(setStatus); + }, [agent.connection]); + + agent.useEvent("messageAdded", (message: AgentMessage) => { + setMessages((prev) => { + const existingIndex = prev.findIndex((item) => item.id === message.id); + if (existingIndex !== -1) { + const next = [...prev]; + next[existingIndex] = message; + return next; + } + return [...prev, message].sort( + (a, b) => a.createdAt - b.createdAt, + ); + }); + }); + + agent.useEvent("response", (payload: ResponseEvent) => { + setMessages((prev) => + prev.map((message) => + message.id === payload.messageId + ? { ...message, content: payload.content } + : message, + ), + ); + }); + + agent.useEvent("status", (nextStatus: AgentStatus) => { + setStatus(nextStatus); }); - const handleSendMessage = async () => { - if (aiAgent.connection && input.trim()) { - setIsLoading(true); + const sendMessage = async () => { + if (!agent.connection) { + return; + } - const userMessage = { role: "user", content: input, timestamp: Date.now() } as Message; - setMessages((prev) => [...prev, userMessage]); + const trimmed = input.trim(); + if (!trimmed) { + return; + } - await aiAgent.connection.sendMessage(input); - setInput(""); + await agent.connection.queue.message.send({ + text: trimmed, + sender: "Operator", + }); + setInput(""); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + sendMessage(); } }; return ( -
-
+
+
+
+

{info.name}

+

{info.id}

+
+
+ + + {status?.state ?? "idle"} + +
+
+ +
{messages.length === 0 ? ( -
- Ask the AI assistant a question to get started -
+

+ Send a message to wake this Rivet Actor. +

) : ( - messages.map((msg, i) => ( -
-
{msg.role === "user" ? "👤" : "🤖"}
-
{msg.content}
-
+ messages.map((message) => ( +
+
+ {message.sender} + + {formatTime(message.createdAt)} + +
+

{message.content}

+
)) )} - {isLoading && ( -
-
🤖
-
Thinking...
-
- )}
-
- +