From bbbb3166b0be1ad7bd70a9ef0932ac4a03effbf0 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Wed, 25 Jun 2025 09:51:31 +0100 Subject: [PATCH] feat: PydanticAI support Add support for the PydanticAI integration. Add missing background change confirmation. Increase agentic generation ui page size to allow it to display results from the PydanticAI integration better. Add a description for the human in the loop generate_task_steps tool, so the PydanticAI example works as expected. Merge write_document and confirm_changes tool into one, which allows it work correctly with the PydanticAI example, without the need for the agent to perform two separate tool calls. Sort the features, so they are easier to find when reading the code and implementing new integrations. Fixes: #5 --- typescript-sdk/apps/dojo/package.json | 1 + typescript-sdk/apps/dojo/src/agents.ts | 26 +++ .../feature/agentic_chat/page.tsx | 4 + .../feature/agentic_generative_ui/page.tsx | 4 +- .../feature/human_in_the_loop/page.tsx | 1 + .../feature/predictive_state_updates/page.tsx | 47 +++-- typescript-sdk/apps/dojo/src/config.ts | 28 +-- typescript-sdk/apps/dojo/src/menu.ts | 12 ++ .../agents/predictive_state_updates/agent.py | 19 +- .../integrations/pydantic-ai/.env-sample | 4 + .../integrations/pydantic-ai/.npmignore | 12 ++ .../integrations/pydantic-ai/README.md | 163 ++++++++++++++++++ .../integrations/pydantic-ai/jest.config.js | 9 + .../integrations/pydantic-ai/package.json | 35 ++++ .../integrations/pydantic-ai/src/index.ts | 3 + .../integrations/pydantic-ai/tsconfig.json | 24 +++ .../integrations/pydantic-ai/tsup.config.ts | 11 ++ typescript-sdk/pnpm-lock.yaml | 31 ++++ 18 files changed, 385 insertions(+), 49 deletions(-) create mode 100644 typescript-sdk/integrations/pydantic-ai/.env-sample create mode 100644 typescript-sdk/integrations/pydantic-ai/.npmignore create mode 100644 typescript-sdk/integrations/pydantic-ai/README.md create mode 100644 typescript-sdk/integrations/pydantic-ai/jest.config.js create mode 100644 typescript-sdk/integrations/pydantic-ai/package.json create mode 100644 typescript-sdk/integrations/pydantic-ai/src/index.ts create mode 100644 typescript-sdk/integrations/pydantic-ai/tsconfig.json create mode 100644 typescript-sdk/integrations/pydantic-ai/tsup.config.ts diff --git a/typescript-sdk/apps/dojo/package.json b/typescript-sdk/apps/dojo/package.json index 50b90a16..5efc52ab 100644 --- a/typescript-sdk/apps/dojo/package.json +++ b/typescript-sdk/apps/dojo/package.json @@ -14,6 +14,7 @@ "@ag-ui/llamaindex": "workspace:*", "@ag-ui/mastra": "workspace:*", "@ag-ui/middleware-starter": "workspace:*", + "@ag-ui/pydantic-ai": "workspace:*", "@ag-ui/server-starter": "workspace:*", "@ag-ui/server-starter-all-features": "workspace:*", "@ag-ui/vercel-ai-sdk": "workspace:*", diff --git a/typescript-sdk/apps/dojo/src/agents.ts b/typescript-sdk/apps/dojo/src/agents.ts index 0222e59c..b3437a94 100644 --- a/typescript-sdk/apps/dojo/src/agents.ts +++ b/typescript-sdk/apps/dojo/src/agents.ts @@ -10,6 +10,7 @@ import { LangGraphAgent } from "@ag-ui/langgraph"; import { AgnoAgent } from "@ag-ui/agno"; import { LlamaIndexAgent } from "@ag-ui/llamaindex"; import { CrewAIAgent } from "@ag-ui/crewai"; +import { PydanticAIAgent } from "@ag-ui/pydantic-ai"; export const agentsIntegrations: AgentIntegrationConfig[] = [ { @@ -20,6 +21,31 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [ }; }, }, + { + id: "pydantic-ai", + agents: async () => { + return { + agentic_chat: new PydanticAIAgent({ + url: "http://localhost:9000/agentic_chat", + }), + agentic_generative_ui: new PydanticAIAgent({ + url: "http://localhost:9000/agentic_generative_ui", + }), + human_in_the_loop: new PydanticAIAgent({ + url: "http://localhost:9000/human_in_the_loop", + }), + predictive_state_updates: new PydanticAIAgent({ + url: "http://localhost:9000/predictive_state_updates", + }), + shared_state: new PydanticAIAgent({ + url: "http://localhost:9000/shared_state", + }), + tool_based_generative_ui: new PydanticAIAgent({ + url: "http://localhost:9000/tool_based_generative_ui", + }), + }; + }, + }, { id: "server-starter", agents: async () => { diff --git a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_chat/page.tsx b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_chat/page.tsx index 164bc489..1eccbd55 100644 --- a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_chat/page.tsx +++ b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_chat/page.tsx @@ -70,6 +70,10 @@ const Chat = () => { ], handler: ({ background }) => { setBackground(background); + return { + status: "success", + message: `Background changed to ${background}`, + }; }, }); diff --git a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_generative_ui/page.tsx b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_generative_ui/page.tsx index 120e7223..c44cd0b1 100644 --- a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_generative_ui/page.tsx +++ b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/agentic_generative_ui/page.tsx @@ -42,7 +42,7 @@ const Chat = () => { return (
-
+
{state.steps.map((step, index) => { if (step.status === "completed") { return ( @@ -55,7 +55,7 @@ const Chat = () => { index === state.steps.findIndex((s) => s.status === "pending") ) { return ( -
+
{step.description}
diff --git a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/human_in_the_loop/page.tsx b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/human_in_the_loop/page.tsx index 5b88a130..0b674d10 100644 --- a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/human_in_the_loop/page.tsx +++ b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/human_in_the_loop/page.tsx @@ -99,6 +99,7 @@ const Chat = () => { }); useCopilotAction({ name: "generate_task_steps", + description: "Generates a list of steps for the user to perform", parameters: [ { name: "steps", diff --git a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/predictive_state_updates/page.tsx b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/predictive_state_updates/page.tsx index 78e95c5e..c96f1e95 100644 --- a/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/predictive_state_updates/page.tsx +++ b/typescript-sdk/apps/dojo/src/app/[integrationId]/feature/predictive_state_updates/page.tsx @@ -128,23 +128,36 @@ const DocumentEditor = () => { }, [text]); useCopilotAction({ - name: "confirm_changes", - renderAndWaitForResponse: ({ args, respond, status }) => ( - { - editor?.commands.setContent(fromMarkdown(currentDocument)); - setAgentState({ document: currentDocument }); - }} - onConfirm={() => { - editor?.commands.setContent(fromMarkdown(agentState?.document || "")); - setCurrentDocument(agentState?.document || ""); - setAgentState({ document: agentState?.document || "" }); - }} - /> - ), + name: "write_document", + description: `Present the proposed changes to the user for review`, + parameters: [ + { + name: "document", + type: "string", + description: "The full updated document in markdown format", + }, + ], + renderAndWaitForResponse({ args, status, respond }) { + if (status === "executing") { + return ( + { + editor?.commands.setContent(fromMarkdown(currentDocument)); + setAgentState({ document: currentDocument }); + }} + onConfirm={() => { + editor?.commands.setContent(fromMarkdown(agentState?.document || "")); + setCurrentDocument(agentState?.document || ""); + setAgentState({ document: agentState?.document || "" }); + }} + /> + ); + } + return <>; + }, }); return ( diff --git a/typescript-sdk/apps/dojo/src/config.ts b/typescript-sdk/apps/dojo/src/config.ts index 14a90fa6..64b71921 100644 --- a/typescript-sdk/apps/dojo/src/config.ts +++ b/typescript-sdk/apps/dojo/src/config.ts @@ -23,12 +23,6 @@ export const featureConfig: FeatureConfig[] = [ description: "Chat with your Copilot and call frontend tools", tags: ["Chat", "Tools", "Streaming"], }), - createFeatureConfig({ - id: "human_in_the_loop", - name: "Human in the loop", - description: "Plan a task together and direct the Copilot to take the right steps", - tags: ["HITL", "Interactivity"], - }), createFeatureConfig({ id: "agentic_generative_ui", name: "Agentic Generative UI", @@ -36,10 +30,16 @@ export const featureConfig: FeatureConfig[] = [ tags: ["Generative ui (agent)", "Long running task"], }), createFeatureConfig({ - id: "tool_based_generative_ui", - name: "Tool Based Generative UI", - description: "Haiku generator that uses tool based generative UI.", - tags: ["Generative ui (action)", "Tools"], + id: "human_in_the_loop", + name: "Human in the loop", + description: "Plan a task together and direct the Copilot to take the right steps", + tags: ["HITL", "Interactivity"], + }), + createFeatureConfig({ + id: "predictive_state_updates", + name: "Predictive State Updates", + description: "Use collaboration to edit a document in real time with your Copilot", + tags: ["State", "Streaming", "Tools"], }), createFeatureConfig({ id: "shared_state", @@ -48,10 +48,10 @@ export const featureConfig: FeatureConfig[] = [ tags: ["Agent State", "Collaborating"], }), createFeatureConfig({ - id: "predictive_state_updates", - name: "Predictive State Updates", - description: "Use collaboration to edit a document in real time with your Copilot", - tags: ["State", "Streaming", "Tools"], + id: "tool_based_generative_ui", + name: "Tool Based Generative UI", + description: "Haiku generator that uses tool based generative UI.", + tags: ["Generative ui (action)", "Tools"], }), ]; diff --git a/typescript-sdk/apps/dojo/src/menu.ts b/typescript-sdk/apps/dojo/src/menu.ts index b9060d6d..3f73ba8f 100644 --- a/typescript-sdk/apps/dojo/src/menu.ts +++ b/typescript-sdk/apps/dojo/src/menu.ts @@ -6,6 +6,18 @@ export const menuIntegrations: MenuIntegrationConfig[] = [ name: "Middleware Starter", features: ["agentic_chat"], }, + { + id: "pydantic-ai", + name: "Pydantic AI", + features: [ + "agentic_chat", + "human_in_the_loop", + "agentic_generative_ui", + "tool_based_generative_ui", + "shared_state", + "predictive_state_updates", + ], + }, { id: "server-starter", name: "Server Starter", diff --git a/typescript-sdk/integrations/langgraph/examples/agents/predictive_state_updates/agent.py b/typescript-sdk/integrations/langgraph/examples/agents/predictive_state_updates/agent.py index 325f0014..ffb10570 100644 --- a/typescript-sdk/integrations/langgraph/examples/agents/predictive_state_updates/agent.py +++ b/typescript-sdk/integrations/langgraph/examples/agents/predictive_state_updates/agent.py @@ -127,22 +127,9 @@ async def chat_node(state: AgentState, config: RunnableConfig): "content": "Document written.", "tool_call_id": tool_call_id } - - # Add confirmation tool call - confirm_tool_call = { - "role": "assistant", - "content": "", - "tool_calls": [{ - "id": str(uuid.uuid4()), - "function": { - "name": "confirm_changes", - "arguments": "{}" - } - }] - } - - messages = messages + [tool_response, confirm_tool_call] - + + messages = messages + [tool_response] + # Return Command to route to end return Command( goto=END, diff --git a/typescript-sdk/integrations/pydantic-ai/.env-sample b/typescript-sdk/integrations/pydantic-ai/.env-sample new file mode 100644 index 00000000..6d5290d1 --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/.env-sample @@ -0,0 +1,4 @@ +## OpenAI API Settings +# Get your Open AI API Key by following these instructions - +# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key +OPENAI_API_KEY="sk-proj-...." diff --git a/typescript-sdk/integrations/pydantic-ai/.npmignore b/typescript-sdk/integrations/pydantic-ai/.npmignore new file mode 100644 index 00000000..aaacf159 --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/.npmignore @@ -0,0 +1,12 @@ +.turbo +.DS_Store +.git +.gitignore +.idea +.vscode +.env +__tests__ +src +tsup.config.ts +tsconfig.json +jest.config.js diff --git a/typescript-sdk/integrations/pydantic-ai/README.md b/typescript-sdk/integrations/pydantic-ai/README.md new file mode 100644 index 00000000..099f8aa5 --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/README.md @@ -0,0 +1,163 @@ +# PydanticAI + +Implementation of the AG-UI protocol for PydanticAI. + +## Prerequisites + +This example uses a PydanticAI agent using an OpenAI model and the AG-UI dojo. + +1. An [OpenAI API key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) +2. A clone of the [AG-UI protocol repository](https://github.com/ag-ui-protocol/ag-ui) + +## Running + +To run this integration you need to: + +1. Make a copy of `.env-sample` as `.env` in the `typescript-sdk/integrations/pydantic-ai` directory +2. Open it in your editor and set `OPENAI_API_KEY` to a valid OpenAI key +3. Open terminal in the `typescript-sdk/integrations/pydantic-ai` of the `ag-ui` repo +4. Install the `pydantic-ai-examples` package, for example: + + ```shell + pip install pydantic-ai-examples + ``` + + or: + + ```shell + uv venv + uv add pydantic-ai-examples + ``` + +5. Run the example dojo server + + ```shell + python -m pydantic_ai_ag_ui_examples.dojo_server + ``` + +6. Open another terminal in root directory of the `ag-ui` repository clone +7. Start the integration ag-ui dojo: + + ```shell + cd typescript-sdk + pnpm install && pnpm run dev + ``` + +8. Finally visit [http://localhost:3000/pydantic-ai](http://localhost:3000/pydantic-ai) + +## Feature Demos + +### [Agentic Chat](http://localhost:3000/pydantic-ai/feature/agentic_chat) + +This demonstrates a basic agent interaction including PydanticAI server side +tools and AG-UI client side tools. + +#### Agent Tools + +- `time` - PydanticAI tool to check the current time for a time zone +- `background` - AG-UI tool to set the background color of the client window + +#### Agent Prompts + +```text +What is the time in New York? +``` + +```text +Change the background to blue +``` + +A complex example which mixes both AG-UI and PydanticAI tools: + +```text +Perform the following steps, waiting for the response of each step before continuing: +1. Get the time +2. Set the background to red +3. Get the time +4. Report how long the background set took by diffing the two times +``` + +### [Agentic Generative UI](http://localhost:3000/pydantic-ai/feature/agentic_generative_ui) + +Demonstrates a long running task where the agent sends updates to the frontend +to let the user know what's happening. + +#### Plan Prompts + +```text +Create a plan for breakfast and execute it +``` + +### [Human in the Loop](http://localhost:3000/pydantic-ai/feature/human_in_the_loop) + +Demonstrates simple human in the loop workflow where the agent comes up with a +plan and the user can approve it using checkboxes. + +#### Task Planning Tools + +- `generate_task_steps` - AG-UI tool to generate and confirm steps + +#### Task Planning Prompt + +```text +Generate a list of steps for cleaning a car for me to review +``` + +### [Predictive State Updates](http://localhost:3000/pydantic-ai/feature/predictive_state_updates) + +Demonstrates how to use the predictive state updates feature to update the state +of the UI based on agent responses, including user interaction via user +confirmation. + +#### Story Tools + +- `write_document` - AG-UI tool to write the document to a window +- `document_predict_state` - PydanticAI tool that enables document state + prediction for the `write_document` tool + +This also shows how to use custom instructions based on shared state information. + +#### Story Example + +Starting document text + +```markdown +Bruce was a good dog, +``` + +Agent prompt + +```text +Help me complete my story about bruce the dog, is should be no longer than a sentence. +``` + +### [Shared State](http://localhost:3000/pydantic-ai/feature/shared_state) + +Demonstrates how to use the shared state between the UI and the agent. + +State sent to the agent is detected by a function based instruction. This then +validates the data using a custom pydantic model before using to create the +instructions for the agent to follow and send to the client using a AG-UI tool. + +#### Recipe Tools + +- `display_recipe` - AG-UI tool to display the recipe in a graphical format + +#### Recipe Example + +1. Customise the basic settings of your recipe +2. Click `Improve with AI` + +### [Tool Based Generative UI](http://localhost:3000/pydantic-ai/feature/tool_based_generative_ui) + +Demonstrates customised rendering for tool output with used confirmation. + +#### Haiku Tools + +- `generate_haiku` - AG-UI tool to display a haiku in English and Japanese + +#### Haiku Prompt + +```text +Generate a haiku about formula 1 +``` diff --git a/typescript-sdk/integrations/pydantic-ai/jest.config.js b/typescript-sdk/integrations/pydantic-ai/jest.config.js new file mode 100644 index 00000000..a0db5c6f --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/*.test.ts"], + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + }, +}; diff --git a/typescript-sdk/integrations/pydantic-ai/package.json b/typescript-sdk/integrations/pydantic-ai/package.json new file mode 100644 index 00000000..a5be4e33 --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/package.json @@ -0,0 +1,35 @@ +{ + "name": "@ag-ui/pydantic-ai", + "author": "Steven Hartland ", + "version": "0.0.1", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "rm -rf dist .turbo node_modules", + "typecheck": "tsc --noEmit", + "test": "jest", + "link:global": "pnpm link --global", + "unlink:global": "pnpm unlink --global" + }, + "dependencies": { + "@ag-ui/client": "workspace:*" + }, + "peerDependencies": { + "rxjs": "7.8.1" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "^20.11.19", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "tsup": "^8.0.2", + "typescript": "^5.3.3" + } +} diff --git a/typescript-sdk/integrations/pydantic-ai/src/index.ts b/typescript-sdk/integrations/pydantic-ai/src/index.ts new file mode 100644 index 00000000..f3a3190a --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/src/index.ts @@ -0,0 +1,3 @@ +import { HttpAgent } from "@ag-ui/client"; + +export class PydanticAIAgent extends HttpAgent {} diff --git a/typescript-sdk/integrations/pydantic-ai/tsconfig.json b/typescript-sdk/integrations/pydantic-ai/tsconfig.json new file mode 100644 index 00000000..d12ec063 --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "skipLibCheck": true, + "strict": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "stripInternal": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript-sdk/integrations/pydantic-ai/tsup.config.ts b/typescript-sdk/integrations/pydantic-ai/tsup.config.ts new file mode 100644 index 00000000..12b69b8f --- /dev/null +++ b/typescript-sdk/integrations/pydantic-ai/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/typescript-sdk/pnpm-lock.yaml b/typescript-sdk/pnpm-lock.yaml index cf343ab1..a81c616b 100644 --- a/typescript-sdk/pnpm-lock.yaml +++ b/typescript-sdk/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@ag-ui/proto': specifier: workspace:* version: link:../../packages/proto + '@ag-ui/pydantic-ai': + specifier: workspace:* + version: link:../../integrations/pydantic-ai '@ag-ui/server-starter': specifier: workspace:* version: link:../../integrations/server-starter @@ -412,6 +415,34 @@ importers: specifier: ^5.3.3 version: 5.8.2 + integrations/pydantic-ai: + dependencies: + '@ag-ui/client': + specifier: workspace:* + version: link:../../packages/client + rxjs: + specifier: 7.8.1 + version: 7.8.1 + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^20.11.19 + version: 20.17.50 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.17.50) + ts-jest: + specifier: ^29.1.2 + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) + tsup: + specifier: ^8.0.2 + version: 8.5.0(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.8.0) + typescript: + specifier: ^5.3.3 + version: 5.8.2 + integrations/server-starter: dependencies: '@ag-ui/client':