diff --git a/agent-reactflow/README.md b/agent-reactflow/README.md index a731269..9948865 100644 --- a/agent-reactflow/README.md +++ b/agent-reactflow/README.md @@ -2,9 +2,11 @@ A sample repository to have low code editor with React Flow of Agents built with Restack. +See the full documentation on our [Agent with React Flow](https://docs.restack.io/blueprints/agent-reactflow) page. + ### Apps -- `frontend`: another [Next.js](https://nextjs.org/) app +- `frontend`: a [Next.js](https://nextjs.org/) app - `backend`: a [Restack](https://restack.io/) app ## Requirements @@ -27,11 +29,91 @@ pnpm i pnpm run dev ``` -Leveraging turborepo, this will start both frontend and backend. +Leveraging TurboRepo, this will start both frontend and backend. Your code will be running and syncing with Restack to execute agents. ## Run agents -### from frontend +### From frontend + +![Run agents from frontend](./agent-reactflow.png) + +### from UI + +You can run agents from the UI by clicking the "Run" button. + +![Run agents from UI](./agent-post.png) + +### from API + +You can run agents from the API by using the generated endpoint: + +`POST http://localhost:6233/api/agents/agentFlow` + +### from any client + +You can run agents with any client connected to Restack, for example: + +```bash +pnpm schedule-agent +``` + +executes `scheduleAgent.ts` which will connect to Restack and execute the `agentFlow` agent. + +## Send events to the Agent + +### from Backend Developer UI + +You can send events like or end from the UI. + +![Send events from UI](./agent-event.png) + +And see the events in the run: + +![See events in UI](./agent-run.png) + +### from API + +You can send events to the agent by using the following endpoint: + +`PUT http://localhost:6233/api/agents/agentFlow/:agentId/:runId` + +with the payload: + +```json +{ + "name": "idVerification", + "input": { + "type": "id", + "documentNumber": "1234567890" + } +} +``` + +to send messages to the agent. + +or + +```json +{ + "eventName": "end" +} +``` + +to end the conversation with the agent. + +### from any client + +You can send event to the agent with any client connected to Restack, for example: + +Modify agentId and runId in eventAgent.ts and then run: + +```bash +pnpm event-agent +``` + +It will connect to Restack and send an events to the agent. + +## Deploy on Restack Cloud -![Run agents from frontend](./agent-reactflow.png) \ No newline at end of file +To deploy the application on Restack, you can create an account at [https://console.restack.io](https://console.restack.io) diff --git a/agent-reactflow/agent-event.png b/agent-reactflow/agent-event.png new file mode 100644 index 0000000..bac8a4f Binary files /dev/null and b/agent-reactflow/agent-event.png differ diff --git a/agent-reactflow/agent-post.png b/agent-reactflow/agent-post.png new file mode 100644 index 0000000..df80b61 Binary files /dev/null and b/agent-reactflow/agent-post.png differ diff --git a/agent-reactflow/agent-run.png b/agent-reactflow/agent-run.png new file mode 100644 index 0000000..3d9ef59 Binary files /dev/null and b/agent-reactflow/agent-run.png differ diff --git a/agent-reactflow/apps/backend/.gitignore b/agent-reactflow/apps/backend/.gitignore index 65e7c43..c437f27 100644 --- a/agent-reactflow/apps/backend/.gitignore +++ b/agent-reactflow/apps/backend/.gitignore @@ -1,4 +1,5 @@ .env node_modules media -dist \ No newline at end of file +dist +.eslintcache \ No newline at end of file diff --git a/agent-reactflow/apps/backend/eslint.config.mjs b/agent-reactflow/apps/backend/eslint.config.mjs new file mode 100644 index 0000000..cb5569a --- /dev/null +++ b/agent-reactflow/apps/backend/eslint.config.mjs @@ -0,0 +1,4 @@ +import { config } from "@agent-reactflow/eslint-config/base"; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/agent-reactflow/apps/backend/package.json b/agent-reactflow/apps/backend/package.json index 84e19ea..1550800 100644 --- a/agent-reactflow/apps/backend/package.json +++ b/agent-reactflow/apps/backend/package.json @@ -7,11 +7,11 @@ "start": "tsx src/services.ts", "start.watch": "nodemon src/services.ts", "dev": "open-cli http://localhost:5233 && tsx watch --include src src/services.ts", + "lint": "eslint src --fix --max-warnings 0 --cache", "build": "tsc --build", "clean": "rm -rf node_modules", "workflow": "tsx ./scheduleWorkflow.ts", - "event": "tsx ./eventAgent.ts", - "restack-up": "node restack_up.mjs" + "event": "tsx ./eventAgent.ts" }, "dependencies": { "@restackio/ai": "^0.0.119", diff --git a/agent-reactflow/apps/backend/scheduleAgent.ts b/agent-reactflow/apps/backend/scheduleAgent.ts new file mode 100644 index 0000000..ee33612 --- /dev/null +++ b/agent-reactflow/apps/backend/scheduleAgent.ts @@ -0,0 +1,29 @@ +import { client } from "./src/client"; + +export type InputSchedule = { + name: string; +}; + +async function scheduleAgent(input: InputSchedule) { + try { + const agentId = `${Date.now()}-agentFlow`; + const runId = await client.scheduleAgent({ + agentName: "agentFlow", + agentId, + input, + }); + + const result = await client.getAgentResult({ agentId, runId }); + + console.log("Agent result:", result); + + process.exit(0); // Exit the process successfully + } catch (error) { + console.error("Error scheduling agent:", error); + process.exit(1); // Exit the process with an error code + } +} + +scheduleAgent({ + name: "test", +}); diff --git a/agent-reactflow/apps/backend/src/agents/chat.ts b/agent-reactflow/apps/backend/src/agents/chat.ts index 2b8f60e..9bc3017 100644 --- a/agent-reactflow/apps/backend/src/agents/chat.ts +++ b/agent-reactflow/apps/backend/src/agents/chat.ts @@ -4,7 +4,6 @@ import { condition, log, step, - sleep, } from "@restackio/ai/agent"; import * as functions from "../functions"; @@ -17,11 +16,12 @@ type AgentChatOutput = { export async function agentChat(): Promise { let endReceived = false; - let messages: functions.Message[] = []; + const messages: functions.Message[] = []; - onEvent(messagesEvent, async ({ messages, stream = true }: { messages: functions.Message[], stream?: boolean }) => { + onEvent(messagesEvent, async ({ messages, tools, stream = true }: { messages: functions.Message[], tools: any, stream?: boolean }) => { const result = await step({}).llmChat({ messages, + tools, stream }); messages.push(result); diff --git a/agent-reactflow/apps/backend/src/agents/flow.ts b/agent-reactflow/apps/backend/src/agents/flow.ts index c22c49d..e48f303 100644 --- a/agent-reactflow/apps/backend/src/agents/flow.ts +++ b/agent-reactflow/apps/backend/src/agents/flow.ts @@ -44,7 +44,7 @@ export type AgentFlowOutput = { } export async function agentFlow({flowJson}: AgentFlowInput): Promise { let endReceived = false; - let eventResults: AgentFlowOutput['results'] = [] + const eventResults: AgentFlowOutput['results'] = [] try { diff --git a/agent-reactflow/apps/backend/src/functions/llmChat.ts b/agent-reactflow/apps/backend/src/functions/llmChat.ts index e31e3f8..0cc4666 100644 --- a/agent-reactflow/apps/backend/src/functions/llmChat.ts +++ b/agent-reactflow/apps/backend/src/functions/llmChat.ts @@ -1,10 +1,8 @@ -import { FunctionFailure, log } from "@restackio/ai/function"; -import { ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming, ChatCompletionTool } from "openai/resources/chat/completions"; +import { FunctionFailure, log, streamToWebsocket } from "@restackio/ai/function"; +import { ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming } from "openai/resources/chat/completions"; import { openaiClient } from "../utils/client"; import { apiAddress } from "../client"; -import { streamToWebsocket } from "./stream"; -import z from "zod"; export type Message = { role: "system" | "user" | "assistant"; @@ -16,13 +14,15 @@ export type OpenAIChatInput = { model?: string; messages: Message[]; stream?: boolean; + tools?: any; }; export const llmChat = async ({ systemContent = "", - model = "gpt-4o-mini", + model = "gpt-4o", messages, stream = true, + tools, }: OpenAIChatInput): Promise => { try { const openai = openaiClient({}); @@ -36,28 +36,7 @@ export const llmChat = async ({ : []), ...(messages ?? []), ], - tools: [ - { - type: "function", - function: { - name: "reactflow", - description: "Update flow", - parameters: { - type: "object", - properties: { - flow: { - type: "object", - description: "The json object of the flow to update" - } - }, - required: [ - "flow" - ], - additionalProperties: false, - }, - }, - }, - ], + tools, model, stream, }; diff --git a/agent-reactflow/apps/backend/src/functions/stream.ts b/agent-reactflow/apps/backend/src/functions/stream.ts deleted file mode 100644 index 5f25656..0000000 --- a/agent-reactflow/apps/backend/src/functions/stream.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { FunctionFailure, log, functionInfo, heartbeat } from "@restackio/ai/function"; -import WebSocket from "ws"; - -export async function streamToWebsocket({ - apiAddress = "localhost:9233", - data, -}: { - apiAddress?: string; - data: any; -}): Promise { - // Get workflow info from the current context - - const infoObj = { - activityId: functionInfo().activityId, - workflowId: functionInfo().workflowExecution.workflowId, - runId: functionInfo().workflowExecution.runId, - activityType: functionInfo().activityType, - taskQueue: functionInfo().taskQueue, - }; - - // Determine the protocol based on the provided API address - const protocol = apiAddress?.startsWith("localhost") ? "ws" : "wss"; - const websocketUrl = `${protocol}://${apiAddress}/stream/ws/agent?agentId=${functionInfo().workflowExecution.workflowId}&runId=${functionInfo().workflowExecution.runId}`; - - let ws: WebSocket | null = null; - let collectedMessages = ""; - - log.debug("Stream to websocket", { websocketUrl }); - - try { - // Open the WebSocket connection - try { - ws = new WebSocket(websocketUrl); - } catch (error: any) { - throw FunctionFailure.nonRetryable("Error restack stream websocket connection:", error?.message || error); - } - await new Promise((resolve, reject) => { - ws!.onopen = () => resolve(); - ws!.onerror = (err: any) => reject(err); - }); - - heartbeat(infoObj); - - log.info("data", data); - - // For asynchronous iteration - for await (const chunk of data) { - log.debug("Stream chunk", { chunk }); - const rawChunkJson = JSON.stringify(chunk); - heartbeat(rawChunkJson); - ws.send(rawChunkJson); - // Attempt to extract content from the chunk if using OpenAI-like stream responses - if ( - chunk && - Array.isArray(chunk.choices) && - chunk.choices.length > 0 && - chunk.choices[0].delta && - typeof chunk.choices[0].delta.content === "string" - ) { - collectedMessages += chunk.choices[0].delta.content; - } - } - - return collectedMessages; - } catch (error: any) { - const errorMessage = `Error with restack stream to websocket: ${error?.message || error}`; - log.error(errorMessage, { error }); - throw new FunctionFailure(errorMessage); - } finally { - // Ensure the WebSocket connection is closed properly - if (ws && ws.readyState === WebSocket.OPEN) { - try { - ws.send("[DONE]"); - ws.close(); - } catch (closeError) { - log.error("Error while closing websocket", { error: closeError }); - } - } - } -} \ No newline at end of file diff --git a/agent-reactflow/apps/backend/src/workflows/endFlow.ts b/agent-reactflow/apps/backend/src/workflows/endFlow.ts index 8b5e3c2..4005b09 100644 --- a/agent-reactflow/apps/backend/src/workflows/endFlow.ts +++ b/agent-reactflow/apps/backend/src/workflows/endFlow.ts @@ -10,16 +10,24 @@ export type EndFlowInput = { export type EndFlowOutput = { response: "success" | "failure"; - rawResponse: {}; + rawResponse: any; }; export async function endFlow(input: EndFlowInput): Promise { + + const agentId = workflowInfo().parent?.workflowId; + const runId = workflowInfo().parent?.runId; + + if (!agentId || !runId) { + throw new Error("Workflow ID or run ID is not available"); + } + await step({}).sendAgentEvent({ eventName: 'end', eventInput: {}, - agentId: workflowInfo().parent?.workflowId!, - runId: workflowInfo().parent?.runId!, + agentId, + runId, }); if (input.eventData.response === "success") { diff --git a/agent-reactflow/apps/backend/src/workflows/idVerification.ts b/agent-reactflow/apps/backend/src/workflows/idVerification.ts index 5211cfe..22dfc08 100644 --- a/agent-reactflow/apps/backend/src/workflows/idVerification.ts +++ b/agent-reactflow/apps/backend/src/workflows/idVerification.ts @@ -39,5 +39,4 @@ export async function idVerification(input: DocCaptureWorkflowInput): Promise { - console.log("updated flow", flow); return { flow, }; diff --git a/agent-reactflow/apps/frontend/components/agent-builder.tsx b/agent-reactflow/apps/frontend/components/agent-builder.tsx index a259d16..e18b272 100644 --- a/agent-reactflow/apps/frontend/components/agent-builder.tsx +++ b/agent-reactflow/apps/frontend/components/agent-builder.tsx @@ -12,7 +12,6 @@ import { ReactFlow, type Connection, type Edge, NodeTypes, - Panel, } from "@xyflow/react" import "@xyflow/react/dist/style.css" import { Button } from "./ui/button" diff --git a/agent-reactflow/apps/frontend/components/agent-chat.tsx b/agent-reactflow/apps/frontend/components/agent-chat.tsx index 6b63c21..b46cb5f 100644 --- a/agent-reactflow/apps/frontend/components/agent-chat.tsx +++ b/agent-reactflow/apps/frontend/components/agent-chat.tsx @@ -10,7 +10,8 @@ import { ScrollArea } from "./ui/scroll-area" import { useChat } from '@ai-sdk/react' import { Node, Edge, ReactFlowInstance } from "@xyflow/react" import { workflowData } from "../lib/workflowData" -// import { runAgent } from "../app/actions/agent" +import { runAgent } from "../app/actions/agent" +import Link from "next/link" interface AgentChatProps { onClose: () => void @@ -168,17 +169,17 @@ export default function AgentChat({ onClose, reactFlowInstance, setNodes, setEdg const [agentId, setAgentId] = useState(""); const [runId, setRunId] = useState(""); - // useEffect(() => { - // const createAgentChat = async () => { - // const { agentId, runId } = await runAgent({ - // agentName: "agentChat", - // input: { }, - // }) - // setAgentId(agentId); - // setRunId(runId); - // } - // createAgentChat(); - // }, []); + useEffect(() => { + const createAgentChat = async () => { + const { agentId, runId } = await runAgent({ + agentName: "agentChat", + input: { }, + }) + setAgentId(agentId); + setRunId(runId); + } + createAgentChat(); + }, []); const currentFlow = reactFlowInstance?.toObject(); @@ -194,8 +195,13 @@ export default function AgentChat({ onClose, reactFlowInstance, setNodes, setEdg - {runId &&
+ {runId &&
+
+

Dev debug info

+ Open on Restack +
{JSON.stringify({agentId, runId}, null, 2)}
+
}
diff --git a/agent-reactflow/apps/frontend/components/agent-test.tsx b/agent-reactflow/apps/frontend/components/agent-test.tsx index d68ba13..61f5f20 100644 --- a/agent-reactflow/apps/frontend/components/agent-test.tsx +++ b/agent-reactflow/apps/frontend/components/agent-test.tsx @@ -5,6 +5,7 @@ import { X, CheckCircle, AlertCircle, Clock } from "lucide-react" import { Button } from "./ui/button" import { runAgent, sendAgentEvent, getAgentResult } from "../app/actions/agent" import { ReactFlowInstance, Node } from "@xyflow/react" +import Link from "next/link" interface AgentTestPanelProps { onClose: () => void @@ -215,14 +216,13 @@ export default function AgentTestPanel({ onClose, workflowData, reactFlowInstanc {isRunning ? "Running..." : "Run agent"} - {/* {runId && ( -