From 317f8695bad9b2c877f5e23acf45df95fffb3069 Mon Sep 17 00:00:00 2001
From: ABCxFF <79597906+abcxff@users.noreply.github.com>
Date: Sun, 20 Apr 2025 20:57:53 -0400
Subject: [PATCH] feat: ai-agent example
---
examples/ai-agent/.env.example | 3 +
examples/ai-agent/.gitignore | 5 ++
examples/ai-agent/actors/app.ts | 93 ++++++++++++++++++++++++++++
examples/ai-agent/package.json | 52 ++++++++++++++++
examples/ai-agent/public/github.css | 37 +++++++++++
examples/ai-agent/public/index.html | 27 ++++++++
examples/ai-agent/public/main.css | 93 ++++++++++++++++++++++++++++
examples/ai-agent/src/App.tsx | 95 +++++++++++++++++++++++++++++
examples/ai-agent/src/index.tsx | 7 +++
examples/ai-agent/tsconfig.json | 20 ++++++
examples/ai-agent/utils/weather.ts | 35 +++++++++++
yarn.lock | 30 +++++++++
12 files changed, 497 insertions(+)
create mode 100644 examples/ai-agent/.env.example
create mode 100644 examples/ai-agent/.gitignore
create mode 100644 examples/ai-agent/actors/app.ts
create mode 100644 examples/ai-agent/package.json
create mode 100644 examples/ai-agent/public/github.css
create mode 100644 examples/ai-agent/public/index.html
create mode 100644 examples/ai-agent/public/main.css
create mode 100644 examples/ai-agent/src/App.tsx
create mode 100644 examples/ai-agent/src/index.tsx
create mode 100644 examples/ai-agent/tsconfig.json
create mode 100644 examples/ai-agent/utils/weather.ts
diff --git a/examples/ai-agent/.env.example b/examples/ai-agent/.env.example
new file mode 100644
index 000000000..187098d35
--- /dev/null
+++ b/examples/ai-agent/.env.example
@@ -0,0 +1,3 @@
+# OpenAI Configuration
+OPENAI_API_KEY=sk-xxxxxxxxxxxx
+API_NINJA_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Optional [will give dummy weather if not (100 F)]
\ No newline at end of file
diff --git a/examples/ai-agent/.gitignore b/examples/ai-agent/.gitignore
new file mode 100644
index 000000000..58067ab77
--- /dev/null
+++ b/examples/ai-agent/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+build/
+.turbo/
+.env
+.env.local
\ No newline at end of file
diff --git a/examples/ai-agent/actors/app.ts b/examples/ai-agent/actors/app.ts
new file mode 100644
index 000000000..311d3c0a0
--- /dev/null
+++ b/examples/ai-agent/actors/app.ts
@@ -0,0 +1,93 @@
+import { actor, setup } from "actor-core";
+import { generateText, jsonSchema, tool } from "ai";
+import { openai } from "@ai-sdk/openai";
+import { getWeather } from "../utils/weather";
+import dotenv from "dotenv";
+
+dotenv.config();
+
+export type Message = { role: "user" | "assistant"; content: string; timestamp: number; }
+
+const aiAgent = actor({
+ // State is automatically persisted
+ state: {
+ messages: [] as Message[]
+ },
+
+ actions: {
+ // Get conversation history
+ getMessages: (c) => c.state.messages,
+
+ // Send a message to the AI and get a response
+ sendMessage: async (c, userMessage: string) => {
+ // Add user message to conversation
+ const userMsg: Message = {
+ role: "user",
+ content: userMessage,
+ timestamp: Date.now()
+ };
+ c.state.messages.push(userMsg);
+
+ // Generate AI response using Vercel AI SDK with tools
+ const out = await generateText({
+ model: openai("o3-mini"),
+ messages: c.state.messages,
+ tools: {
+ weather: tool({
+ description: 'Get the weather in a location',
+ parameters: jsonSchema<{ coords: { longitude: number, latitude: number } }>({
+ type: 'object',
+ properties: {
+ coords: {
+ type: 'object',
+ description: 'The location to get the weather for',
+ properties: {
+ longitude: {
+ type: 'number',
+ description: 'Longitude of the location'
+ },
+ latitude: {
+ type: 'number',
+ description: 'Latitude of the location'
+ }
+ },
+ required: ['longitude', 'latitude'],
+ additionalProperties: false
+ }
+ },
+ required: ['coords'],
+ additionalProperties: false
+ }),
+ execute: async ({ coords }) => {
+ return await getWeather(coords);
+ }
+ }),
+ },
+ maxSteps: 2,
+ });
+
+ const { text } = out;
+
+ // Add AI response to conversation
+ const assistantMsg: Message = {
+ role: "assistant",
+ content: text,
+ timestamp: Date.now()
+ };
+ c.state.messages.push(assistantMsg);
+
+ // Broadcast the new message to all connected clients
+ c.broadcast("messageReceived", assistantMsg);
+
+ return assistantMsg;
+ },
+ }
+});
+
+// Create and export the app
+export const app = setup({
+ actors: { aiAgent },
+});
+
+// Export type for client type checking
+export type App = typeof app;
\ No newline at end of file
diff --git a/examples/ai-agent/package.json b/examples/ai-agent/package.json
new file mode 100644
index 000000000..b15f72d83
--- /dev/null
+++ b/examples/ai-agent/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "ai-agent",
+ "version": "0.8.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "scripts": {
+ "dev:actors": "npx @actor-core/cli@latest dev actors/app.ts",
+ "dev:frontend": "react-scripts start",
+ "build": "react-scripts build",
+ "check-types": "tsc --noEmit",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@actor-core/react": "workspace:*",
+ "@ai-sdk/openai": "^1.3.16",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "actor-core": "workspace:*",
+ "dotenv": "^16.5.0",
+ "react": "^19",
+ "react-dom": "^19",
+ "react-scripts": "^5.0.1"
+ },
+ "devDependencies": {
+ "@actor-core/cli": "workspace:*",
+ "actor-core": "workspace:*",
+ "typescript": "^5.5.2"
+ },
+ "example": {
+ "platforms": [
+ "*"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "resolutions": {
+ "react@^19": "^19.0.0",
+ "react-dom@^19": "^19.0.0",
+ "react@^18": "^18.3"
+ }
+}
diff --git a/examples/ai-agent/public/github.css b/examples/ai-agent/public/github.css
new file mode 100644
index 000000000..49c7aca80
--- /dev/null
+++ b/examples/ai-agent/public/github.css
@@ -0,0 +1,37 @@
+body, html {
+ margin: 0;
+ padding: 0;
+ padding-top: 16px;
+ width: 100%;
+ height: 100%;
+}
+#example--repo-ref {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ cursor: pointer;
+ background-color: rgb(243, 243, 243);
+ height: 24px;
+ width: 100%;
+ padding: 8px 8px;
+}
+#example--github-icon {
+ height: 24px;
+ float: left;
+}
+#example--repo-link {
+ height: 24px;
+ margin-left: 8px;
+ color: rgb(45, 50, 55);
+ font-weight: bold;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ font-size: 15px;
+ vertical-align: middle;
+}
+
+#example--repo-ref:hover #example--repo-link {
+ color: black;
+}
+#example--repo-ref:hover svg {
+ fill: black !important;
+}
\ No newline at end of file
diff --git a/examples/ai-agent/public/index.html b/examples/ai-agent/public/index.html
new file mode 100644
index 000000000..dd611ed64
--- /dev/null
+++ b/examples/ai-agent/public/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ AI Agent
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/ai-agent/public/main.css b/examples/ai-agent/public/main.css
new file mode 100644
index 000000000..114997fdf
--- /dev/null
+++ b/examples/ai-agent/public/main.css
@@ -0,0 +1,93 @@
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+ font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
+}
+
+h1 {
+ text-align: center;
+ color: #333;
+}
+
+.ai-chat {
+ display: flex;
+ flex-direction: column;
+ height: 80vh;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+.message {
+ display: flex;
+ margin-bottom: 20px;
+ align-items: flex-start;
+}
+
+.message .avatar {
+ font-size: 24px;
+ margin-right: 12px;
+}
+
+.message .content {
+ background: #f5f5f5;
+ padding: 12px;
+ border-radius: 8px;
+ max-width: 70%;
+}
+
+.message.user .content {
+ background: #007bff;
+ color: white;
+}
+
+.message.assistant .content {
+ background: #f8f9fa;
+}
+
+.message.loading .content {
+ font-style: italic;
+ color: #666;
+}
+
+.empty-message {
+ text-align: center;
+ color: #666;
+ margin-top: 40px;
+}
+
+.input-area {
+ display: flex;
+ padding: 20px;
+ border-top: 1px solid #ddd;
+ background: white;
+}
+
+.input-area input {
+ flex: 1;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-right: 10px;
+}
+
+.input-area button {
+ padding: 10px 20px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.input-area button:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
\ No newline at end of file
diff --git a/examples/ai-agent/src/App.tsx b/examples/ai-agent/src/App.tsx
new file mode 100644
index 000000000..09d224b24
--- /dev/null
+++ b/examples/ai-agent/src/App.tsx
@@ -0,0 +1,95 @@
+import { createClient } from "actor-core/client";
+import { createReactActorCore } from "@actor-core/react";
+import { useState, useEffect } from "react";
+import type { App } from "../actors/app";
+import type { Message } from "../actors/app";
+
+const client = createClient("http://localhost:6420");
+const { useActor, useActorEvent } = createReactActorCore(client);
+
+function AIAssistant() {
+ const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } });
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Load initial messages
+ useEffect(() => {
+ if (actor) {
+ actor.getMessages().then(setMessages);
+ }
+ }, [actor]);
+
+ // Listen for real-time messages
+ useActorEvent({ actor, event: "messageReceived" }, (message) => {
+ setMessages(prev => [...prev, message as Message]);
+ setIsLoading(false);
+ });
+
+ const handleSendMessage = async () => {
+ if (actor && input.trim()) {
+ setIsLoading(true);
+
+ // Add user message to UI immediately
+ const userMessage = { role: "user", content: input, timestamp: Date.now() } as Message;
+ setMessages(prev => [...prev, userMessage]);
+
+ // Send to actor (AI response will come through the event)
+ await actor.sendMessage(input);
+ setInput("");
+ }
+ };
+
+ return (
+
+
+ {messages.length === 0 ? (
+
+ Ask the AI assistant a question to get started
+
+ ) : (
+ messages.map((msg, i) => (
+
+
+ {msg.role === "user" ? "👤" : "🤖"}
+
+
{msg.content}
+
+ ))
+ )}
+ {isLoading && (
+
+ )}
+
+
+
+ setInput(e.target.value)}
+ onKeyPress={e => e.key === "Enter" && handleSendMessage()}
+ placeholder="Ask the AI assistant..."
+ disabled={isLoading}
+ />
+
+
+
+ );
+}
+
+// Export the AIAssistant as the default React component
+export default function ReactApp() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/examples/ai-agent/src/index.tsx b/examples/ai-agent/src/index.tsx
new file mode 100644
index 000000000..f3e9c5fd1
--- /dev/null
+++ b/examples/ai-agent/src/index.tsx
@@ -0,0 +1,7 @@
+import ReactDOM from "react-dom/client";
+import ReactApp from "./App";
+
+const container = document.getElementById('root')!;
+const root = ReactDOM.createRoot(container);
+
+root.render();
\ No newline at end of file
diff --git a/examples/ai-agent/tsconfig.json b/examples/ai-agent/tsconfig.json
new file mode 100644
index 000000000..e2f828a40
--- /dev/null
+++ b/examples/ai-agent/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "lib": ["ESNext", "DOM"],
+ "jsx": "react-jsx",
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "types": ["node"],
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "checkJs": false,
+ "noEmit": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*", "actors/**/*", "tests/**/*"]
+}
\ No newline at end of file
diff --git a/examples/ai-agent/utils/weather.ts b/examples/ai-agent/utils/weather.ts
new file mode 100644
index 000000000..0801750fc
--- /dev/null
+++ b/examples/ai-agent/utils/weather.ts
@@ -0,0 +1,35 @@
+
+const celsiusToFahrenheit = (celsius: number) => (celsius * 9/5) + 32;
+
+export async function getWeather(coords: {longitude: number, latitude: number}): Promise<{ forecast: string, temperature: number }> {
+ try {
+ // Using OpenWeatherMap API which has a free tier
+ const apiKey = process.env.API_NINJA_API_KEY || null;
+
+ if (apiKey === null) {
+ return { forecast: "Cloudy with chance of meatballs", temperature: 100 }
+ }
+
+ const url = `https://api.api-ninjas.com/v1/weatherforecast?lon=${coords.longitude}&lat=${coords.latitude}&`;
+
+ const response = await fetch(url, {
+ headers: {
+ 'X-Api-Key': apiKey
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Weather API error: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ const today = data[0];
+ const forecast = today.weather;
+ const temperature = celsiusToFahrenheit(today.temp);
+
+ return {temperature: Math.floor(temperature), forecast: forecast};
+ } catch (error) {
+ console.error('Error fetching weather data:', error);
+ throw new Error('Failed to fetch weather data');
+ }
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 3c5634791..e094aabb4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -256,6 +256,18 @@ __metadata:
languageName: node
linkType: hard
+"@ai-sdk/openai@npm:^1.3.16":
+ version: 1.3.16
+ resolution: "@ai-sdk/openai@npm:1.3.16"
+ dependencies:
+ "@ai-sdk/provider": "npm:1.1.3"
+ "@ai-sdk/provider-utils": "npm:2.2.7"
+ peerDependencies:
+ zod: ^3.0.0
+ checksum: 10c0/e1c21d2e104611f91d05e5431ba6399a804c220185a8e14423723d6fa2dfafc546865702b2dd6e6a70de24c6ff485ba1f90f384ed12dde7e384fb17583498df0
+ languageName: node
+ linkType: hard
+
"@ai-sdk/provider-utils@npm:2.2.7":
version: 2.2.7
resolution: "@ai-sdk/provider-utils@npm:2.2.7"
@@ -6217,6 +6229,24 @@ __metadata:
languageName: node
linkType: hard
+"ai-agent@workspace:examples/ai-agent":
+ version: 0.0.0-use.local
+ resolution: "ai-agent@workspace:examples/ai-agent"
+ dependencies:
+ "@actor-core/cli": "workspace:*"
+ "@actor-core/react": "workspace:*"
+ "@ai-sdk/openai": "npm:^1.3.16"
+ "@types/react": "npm:^19"
+ "@types/react-dom": "npm:^19"
+ actor-core: "workspace:*"
+ dotenv: "npm:^16.5.0"
+ react: "npm:^19"
+ react-dom: "npm:^19"
+ react-scripts: "npm:^5.0.1"
+ typescript: "npm:^5.5.2"
+ languageName: unknown
+ linkType: soft
+
"ai@npm:^4.3.9":
version: 4.3.9
resolution: "ai@npm:4.3.9"