diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db754c9..fa0ba9a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,6 +16,16 @@ updates: dependencies: patterns: ['*'] exclude-patterns: ['@aparte/*'] + - package-ecosystem: pip + directories: ['/langgraph-ag-ui', '/pydantic-ai-ag-ui', '/google-adk-ag-ui'] + schedule: + interval: weekly + ignore: + - dependency-name: '*' + update-types: ['version-update:semver-major'] + groups: + dependencies: + patterns: ['*'] - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94b1f4a..c5706c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,13 @@ jobs: runs-on: ubuntu-latest outputs: examples: ${{ steps.find.outputs.examples }} + agents: ${{ steps.find.outputs.agents }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - id: find - run: echo "examples=$(ls -d */package.json | xargs -n1 dirname | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" + run: | + echo "examples=$(ls -d */package.json | xargs -n1 dirname | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" + echo "agents=$(ls -d */requirements.txt | xargs -n1 dirname | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" example: needs: list @@ -42,11 +45,33 @@ jobs: - run: npm run check - run: npm run build + # Every folder with a requirements.txt holds a Python agent: it is installed and run, with its scripted model. + agent: + needs: list + name: ${{ matrix.example }} (agent) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + example: ${{ fromJson(needs.list.outputs.agents) }} + defaults: + run: + working-directory: ${{ matrix.example }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.13' + cache: pip + cache-dependency-path: ${{ matrix.example }}/requirements.txt + - run: pip install -r requirements.txt + - run: python smoke_test.py + # The one check main requires: it passes when every example does, however many there are. all: name: all examples if: always() - needs: [list, example] + needs: [list, example, agent] runs-on: ubuntu-latest steps: - - run: test "${{ needs.list.result }}" = success && test "${{ needs.example.result }}" = success + - run: test "${{ needs.list.result }}" = success && test "${{ needs.example.result }}" = success && test "${{ needs.agent.result }}" = success diff --git a/.gitignore b/.gitignore index a98acd0..e436021 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ node_modules +dist +.venv +__pycache__ .svelte-kit build .output diff --git a/README.md b/README.md index de997dc..e7ee114 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # aparté examples -Apps built on [aparté](https://github.com/apartejs/aparte), an MIT chat UI for AI made of Web Components. Each folder is a whole app: open it in StackBlitz, or copy it and run it. +Apps built on [aparté](https://github.com/apartejs/aparte), an MIT chat UI for AI made of Web Components. Each folder is a whole app: copy it and run it, or open it in StackBlitz when it runs there. | Example | Stack | What it shows | Try it | |---|---|---|---| | [sveltekit-ai-sdk](./sveltekit-ai-sdk) | SvelteKit, Vercel AI SDK | A tool the model can only run once the user approves it, drawn by a Svelte component; Markdown formatted as it streams | [StackBlitz](https://stackblitz.com/github/apartejs/examples/tree/main/sveltekit-ai-sdk?file=src/lib/chat.ts) | +| [langgraph-ag-ui](./langgraph-ag-ui) | LangGraph, AG-UI | A Python agent streaming into the chat, and a tool that waits for a person through `interrupt()` | On your machine | +| [pydantic-ai-ag-ui](./pydantic-ai-ag-ui) | Pydantic AI, AG-UI | A Python agent streaming into the chat, and a tool that waits for a person through `requires_approval` | On your machine | +| [google-adk-ag-ui](./google-adk-ag-ui) | Google ADK, AG-UI | A Python agent streaming into the chat, a tool that waits for a person through `require_confirmation`, and ADK's confirmation tool kept off the transcript | On your machine | ## Run one on your machine @@ -15,7 +18,7 @@ npm install npm run dev ``` -With no API key, an example answers with a scripted model, which is how it runs on StackBlitz. Each folder's `.env.example` names the key that switches it to a real model. +With no API key, an example answers with a scripted model, which is how it runs on StackBlitz. Each folder's `.env.example` names the key that switches it to a real model. An example with a Python agent starts that agent first: its README has the commands. ## Adding an example @@ -23,5 +26,5 @@ With no API key, an example answers with a scripted model, which is how it runs - **Name it after its stack**, framework first: `sveltekit-ai-sdk`, `nextjs-ai-sdk`. Add the topic only when two examples share a stack. - **aparté comes from npm**, at a published version, with the lockfile committed: the example installs what a reader's app would. - **It runs without a key.** A scripted model answers when the key is missing, and its answer says it is scripted. -- **It has `check` and `build` scripts.** CI runs `npm ci`, `npm run check` and `npm run build` in every folder that has a `package.json`, and Dependabot updates each folder's lockfile. -- **Its README opens with a StackBlitz link**, and it gets a row in the table above. +- **It has `check` and `build` scripts.** CI runs `npm ci`, `npm run check` and `npm run build` in every folder that has a `package.json`, and Dependabot updates each folder's lockfile. A folder with a `requirements.txt` holds a Python agent: CI installs it and runs its `smoke_test.py`. +- **Its README says how to try it**, with a StackBlitz link when it runs there (a Python agent does not), and it gets a row in the table above. diff --git a/google-adk-ag-ui/.env.example b/google-adk-ag-ui/.env.example new file mode 100644 index 0000000..5f073a9 --- /dev/null +++ b/google-adk-ag-ui/.env.example @@ -0,0 +1,3 @@ +# Optional. Without MODEL, a scripted model answers (scripted_model.py). +MODEL= +GOOGLE_API_KEY= diff --git a/google-adk-ag-ui/README.md b/google-adk-ag-ui/README.md new file mode 100644 index 0000000..6648203 --- /dev/null +++ b/google-adk-ag-ui/README.md @@ -0,0 +1,34 @@ +# Google ADK + aparté + +A [Google ADK](https://google.github.io/adk-docs/) agent served over [AG-UI](https://docs.ag-ui.com), with [aparté](https://apartejs.dev) as its chat. Replies stream, tool calls show as they run, and one tool waits for a person before it runs. + +## Run it + +The agent runs in Python, so this one runs on your machine and not on StackBlitz. Tested with Python 3.13 and Node 24. + +```bash +npx degit apartejs/examples/google-adk-ag-ui my-agent +cd my-agent +python -m venv .venv +source .venv/bin/activate # on Windows: .venv\Scripts\activate +pip install -r requirements.txt +uvicorn agent:app --port 8000 +``` + +In a second terminal: + +```bash +npm install +npm run dev +``` + +Open http://localhost:5173 and ask it to send an email. + +With nothing configured, a scripted model answers, and says it is scripted. For a real model, copy `.env.example` to `.env` and set `MODEL` to a Gemini model and `GOOGLE_API_KEY` to your key. + +## What to look at + +- [`agent.py`](./agent.py): the agent and its FastAPI route. `require_confirmation=True` on the tool pauses the run for a person, and `emit_interrupt_outcome=True` ends that run the way AG-UI 1.0 describes it. +- [`src/main.ts`](./src/main.ts): the whole page. ADK asks for a confirmation through a tool of its own, `adk_request_confirmation`, so `registerToolDisplay` hides that row and has the question name the real tool. The rest knows nothing about ADK: `@aparte/runner-ag-ui` reads the agent's AG-UI stream. +- [`scripted_model.py`](./scripted_model.py): the stand-in model. +- [`smoke_test.py`](./smoke_test.py): what CI runs against the route: a reply, a pause, and a resume. diff --git a/google-adk-ag-ui/agent.py b/google-adk-ag-ui/agent.py new file mode 100644 index 0000000..499cd81 --- /dev/null +++ b/google-adk-ag-ui/agent.py @@ -0,0 +1,41 @@ +"""A Google ADK agent served over AG-UI, with a tool that waits for a person.""" + +import os + +from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint +from dotenv import load_dotenv +from fastapi import FastAPI +from google.adk.agents import LlmAgent +from google.adk.tools import FunctionTool + +load_dotenv() + + +def send_email(to: str, subject: str) -> str: + """Send an email. A person has to approve it first.""" + return f"Email sent to {to}: {subject}" + + +def make_model(): + if os.environ.get("MODEL"): + return os.environ["MODEL"] + + from scripted_model import ScriptedModel + + return ScriptedModel() + + +agent = LlmAgent( + name="assistant", + model=make_model(), + instruction="You help the user. Send an email only when asked to.", + tools=[FunctionTool(send_email, require_confirmation=True)], +) + +app = FastAPI() +add_adk_fastapi_endpoint( + app, + # emit_interrupt_outcome: a pause ends the run the way AG-UI 1.0 describes it, which is what the chat reads. + ADKAgent(adk_agent=agent, app_name="example", user_id="reader", emit_interrupt_outcome=True), + path="/agent", +) diff --git a/google-adk-ag-ui/index.html b/google-adk-ag-ui/index.html new file mode 100644 index 0000000..8e71140 --- /dev/null +++ b/google-adk-ag-ui/index.html @@ -0,0 +1,15 @@ + + + + + + Google ADK + aparté + + + + + + + diff --git a/google-adk-ag-ui/package-lock.json b/google-adk-ag-ui/package-lock.json new file mode 100644 index 0000000..fd05f8a --- /dev/null +++ b/google-adk-ag-ui/package-lock.json @@ -0,0 +1,891 @@ +{ + "name": "google-adk-ag-ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "google-adk-ag-ui", + "version": "0.0.1", + "dependencies": { + "@aparte/core": "^1.0.2", + "@aparte/plugin-marked": "^1.0.2", + "@aparte/runner-ag-ui": "^1.0.2", + "marked": "^18.1.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vite": "^8.3.0" + } + }, + "node_modules/@aparte/core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/core/-/core-1.0.2.tgz", + "integrity": "sha512-fOZ7/B9lPPpGvzrF9YYWrqRFNGskm9g41b4QLE5eKa3UOySuWQIytfFUVHIkqV9y8F5NouO87BKGfj1Nmud5Qg==", + "license": "MIT", + "dependencies": { + "@aparte/engine": "1.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@aparte/engine": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/engine/-/engine-1.0.2.tgz", + "integrity": "sha512-ZkC5gL9tPanB48rm2bNXbK/0ZvV7h2K87ujJYLtFBaqZV/nUbRLRZtS4O2VLeeC/PbOQP6ve+UXIBaYYFSrFlw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependenciesMeta": { + "@aparte/core": { + "optional": true + } + } + }, + "node_modules/@aparte/plugin-marked": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/plugin-marked/-/plugin-marked-1.0.2.tgz", + "integrity": "sha512-WVpKWNB+EZslxij8faXthOb16/H7EEruBvLfEmbvDC0FW55OT0L0k3yDy5yEp6yfiDkxnwZuXC4MT6OQeFxf8w==", + "license": "MIT", + "dependencies": { + "remend": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aparte/core": ">=1.0.2 <2.0.0", + "marked": "^18.0.0" + } + }, + "node_modules/@aparte/runner-ag-ui": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/runner-ag-ui/-/runner-ag-ui-1.0.2.tgz", + "integrity": "sha512-EaClxTttxf8Jim7Zqt2VLhTT39o5pIwuMOrRb4NQSG9xtIYm2dCFYf5y8+A3graZIklHGN/7B+zUaTxiFr+eiw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aparte/core": ">=1.0.2 <2.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/marked": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.3.tgz", + "integrity": "sha512-9ChMH8EVVgjgID49nsL6IWxWk5c6GtAsq/xw7U1ggkjkU5AGuv68oLTVlFRtvcUYZtpbNyjyvGo3YDu0fGapiw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/remend": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/remend/-/remend-1.4.0.tgz", + "integrity": "sha512-mHiYhhwphs1IgIZmkToKLwJn0U99IjPXuutkA2acrNcG0vlEq2G4TGnd2eT0saIHwh2KjaCpvleHcsd5SAz96A==", + "license": "Apache-2.0" + }, + "node_modules/rolldown": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.156.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", + "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.32", + "rolldown": "~1.2.16", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/google-adk-ag-ui/package.json b/google-adk-ag-ui/package.json new file mode 100644 index 0000000..0f5a5bb --- /dev/null +++ b/google-adk-ag-ui/package.json @@ -0,0 +1,21 @@ +{ + "name": "google-adk-ag-ui", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "check": "tsc" + }, + "dependencies": { + "@aparte/core": "^1.0.2", + "@aparte/plugin-marked": "^1.0.2", + "@aparte/runner-ag-ui": "^1.0.2", + "marked": "^18.1.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vite": "^8.3.0" + } +} diff --git a/google-adk-ag-ui/requirements.txt b/google-adk-ag-ui/requirements.txt new file mode 100644 index 0000000..f1672df --- /dev/null +++ b/google-adk-ag-ui/requirements.txt @@ -0,0 +1,6 @@ +ag-ui-adk==0.12.0 +fastapi==0.162.2 +google-adk==2.14.0 +httpx==0.28.1 +python-dotenv==1.2.4 +uvicorn==0.54.0 diff --git a/google-adk-ag-ui/scripted_model.py b/google-adk-ag-ui/scripted_model.py new file mode 100644 index 0000000..953ca09 --- /dev/null +++ b/google-adk-ag-ui/scripted_model.py @@ -0,0 +1,37 @@ +"""Answers when no model is configured, and says so. It streams word by word, as a real model does.""" + +from collections.abc import AsyncGenerator +from uuid import uuid4 + +from google.adk.models.base_llm import BaseLlm +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.genai import types + +NOTE = "This is a scripted reply: set MODEL in .env to talk to a real model." + + +def text_response(text: str, partial: bool) -> LlmResponse: + return LlmResponse(content=types.Content(role="model", parts=[types.Part(text=text)]), partial=partial) + + +class ScriptedModel(BaseLlm): + model: str = "scripted" + + async def generate_content_async(self, llm_request: LlmRequest, stream: bool = False) -> AsyncGenerator[LlmResponse, None]: + last = llm_request.contents[-1].parts[-1] + if last.function_response: + response = last.function_response.response or {} + text = f"{response.get('result') or response.get('error')} {NOTE}" + elif "email" in (last.text or "").lower(): + call = types.FunctionCall(id=f"call_{uuid4().hex}", name="send_email", args={"to": "ada@example.com", "subject": "Hello from Google ADK"}) + yield LlmResponse(content=types.Content(role="model", parts=[types.Part(function_call=call)])) + return + else: + text = f"{NOTE} Ask me to **send an email** to see a tool wait for your approval." + + if stream: + words = text.split(" ") + for i, word in enumerate(words): + yield text_response(word if i == 0 else f" {word}", partial=True) + yield text_response(text, partial=False) diff --git a/google-adk-ag-ui/smoke_test.py b/google-adk-ag-ui/smoke_test.py new file mode 100644 index 0000000..015c036 --- /dev/null +++ b/google-adk-ag-ui/smoke_test.py @@ -0,0 +1,46 @@ +"""Runs the agent with the scripted model, through the same route the page calls. CI runs this file.""" + +import json +from uuid import uuid4 + +from fastapi.testclient import TestClient + +from agent import app + +client = TestClient(app) + + +def run(thread_id: str, **body) -> list[dict]: + payload = {"threadId": thread_id, "runId": uuid4().hex, "state": {}, "messages": [], "tools": [], "context": [], "forwardedProps": {}, **body} + response = client.post("/agent", json=payload, headers={"accept": "text/event-stream"}) + assert response.status_code == 200, response.text + return [json.loads(line[6:]) for line in response.text.splitlines() if line.startswith("data: ")] + + +def user(text: str) -> dict: + return {"id": uuid4().hex, "role": "user", "content": text} + + +def of_type(events: list[dict], name: str) -> list[dict]: + return [e for e in events if e["type"] == name] + + +events = run(uuid4().hex, messages=[user("Hello")]) +assert events[0]["type"] == "RUN_STARTED" and events[-1]["type"] == "RUN_FINISHED", events +text = "".join(e["delta"] for e in of_type(events, "TEXT_MESSAGE_CONTENT")) +assert "scripted reply" in text, text +assert len(of_type(events, "TEXT_MESSAGE_CONTENT")) > 1, "the reply did not stream" + +thread = uuid4().hex +ask = user("Send an email") +paused = run(thread, messages=[ask]) +assert of_type(paused, "TOOL_CALL_START")[0]["toolCallName"] == "send_email", paused +interrupts = paused[-1]["outcome"]["interrupts"] +assert interrupts[0]["reason"] == "confirmation", interrupts + +resumed = run(thread, messages=[ask], resume=[{"interruptId": interrupts[0]["id"], "status": "resolved", "payload": {"approved": True}}]) +result = of_type(resumed, "TOOL_CALL_RESULT")[0]["content"] +assert "Email sent" in result, resumed +assert resumed[-1]["type"] == "RUN_FINISHED" and resumed[-1].get("outcome", {"type": "success"})["type"] == "success", resumed + +print("ok") diff --git a/google-adk-ag-ui/src/main.ts b/google-adk-ag-ui/src/main.ts new file mode 100644 index 0000000..c177080 --- /dev/null +++ b/google-adk-ag-ui/src/main.ts @@ -0,0 +1,19 @@ +import '@aparte/core/styles.css'; +import { AparteClient, aparteGlobalConfig } from '@aparte/core'; +import { setupMarkedProvider } from '@aparte/plugin-marked'; +import { createAgUiRunner } from '@aparte/runner-ag-ui'; + +setupMarkedProvider(); + +// ADK asks for an approval through a tool of its own, whose arguments are its protocol. +// Keep it off the transcript, and have the question name the tool it is really about. +type Confirmation = { originalFunctionCall?: { name?: string; args?: unknown } }; + +aparteGlobalConfig.registerToolDisplay('adk_request_confirmation', { + hidden: true, + label: (segment) => (segment.toolCall.input as Confirmation).originalFunctionCall?.name ?? 'this tool', + formatInput: (input) => JSON.stringify((input as Confirmation).originalFunctionCall?.args ?? {}, null, 2), +}); + +// The rest of the page knows nothing about Google ADK: it reads the agent's AG-UI stream. +new AparteClient({ streamRunner: createAgUiRunner({ endpoint: '/agent' }) }).start(); diff --git a/google-adk-ag-ui/tsconfig.json b/google-adk-ag-ui/tsconfig.json new file mode 100644 index 0000000..69fa3be --- /dev/null +++ b/google-adk-ag-ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/google-adk-ag-ui/vite.config.ts b/google-adk-ag-ui/vite.config.ts new file mode 100644 index 0000000..f22eab8 --- /dev/null +++ b/google-adk-ag-ui/vite.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { proxy: { '/agent': 'http://localhost:8000' } }, +}); diff --git a/langgraph-ag-ui/.env.example b/langgraph-ag-ui/.env.example new file mode 100644 index 0000000..e959120 --- /dev/null +++ b/langgraph-ag-ui/.env.example @@ -0,0 +1,5 @@ +# Optional. Without MODEL, a scripted model answers (scripted_model.py). +MODEL= +OPENAI_API_KEY= +# For any other OpenAI-compatible API, a local one included. +OPENAI_BASE_URL= diff --git a/langgraph-ag-ui/README.md b/langgraph-ag-ui/README.md new file mode 100644 index 0000000..2e8ba98 --- /dev/null +++ b/langgraph-ag-ui/README.md @@ -0,0 +1,36 @@ +# LangGraph + aparté + +A [LangGraph](https://langchain-ai.github.io/langgraph/) agent served over [AG-UI](https://docs.ag-ui.com), with [aparté](https://apartejs.dev) as its chat. Replies stream, tool calls show as they run, and one tool waits for a person before it runs. + +The page knows nothing about LangGraph: `@aparte/runner-ag-ui` reads the agent's AG-UI stream. + +## Run it + +The agent runs in Python, so this one runs on your machine and not on StackBlitz. Tested with Python 3.13 and Node 24. + +```bash +npx degit apartejs/examples/langgraph-ag-ui my-agent +cd my-agent +python -m venv .venv +source .venv/bin/activate # on Windows: .venv\Scripts\activate +pip install -r requirements.txt +uvicorn agent:app --port 8000 +``` + +In a second terminal: + +```bash +npm install +npm run dev +``` + +Open http://localhost:5173 and ask it to send an email. + +With nothing configured, a scripted model answers, and says it is scripted. For a real model, copy `.env.example` to `.env` and set `MODEL` and `OPENAI_API_KEY`. `OPENAI_BASE_URL` points it at any OpenAI-compatible API, a local one included. + +## What to look at + +- [`agent.py`](./agent.py): the graph and its FastAPI route. `send_email` calls `interrupt()` with a `reason` and the call's id, which is what opens the chat's approval panel. `emit_interrupt_outcome=True` ends a paused run the way AG-UI 1.0 describes it. +- [`src/main.ts`](./src/main.ts): the whole page. +- [`scripted_model.py`](./scripted_model.py): the stand-in model. +- [`smoke_test.py`](./smoke_test.py): what CI runs against the route: a reply, a pause, and a resume. diff --git a/langgraph-ag-ui/agent.py b/langgraph-ag-ui/agent.py new file mode 100644 index 0000000..764865c --- /dev/null +++ b/langgraph-ag-ui/agent.py @@ -0,0 +1,60 @@ +"""A LangGraph agent served over AG-UI, with a tool that waits for a person.""" + +import os +from typing import Annotated + +from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint +from dotenv import load_dotenv +from fastapi import FastAPI +from langchain_core.tools import InjectedToolCallId, tool +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import START, MessagesState, StateGraph +from langgraph.prebuilt import ToolNode, tools_condition +from langgraph.types import interrupt + +load_dotenv() + + +@tool +def send_email(to: str, subject: str, tool_call_id: Annotated[str, InjectedToolCallId]) -> str: + """Send an email. A person has to approve it first.""" + # The reason and the call's id are what make the chat open its approval panel. + answer = interrupt({"reason": "tool_call", "toolCallId": tool_call_id, "message": f"Send this email to {to}?"}) + if not (isinstance(answer, dict) and answer.get("approved")): + return "The user refused: the email was not sent." + return f"Email sent to {to}: {subject}" + + +def make_model(): + if os.environ.get("MODEL"): + from langchain_openai import ChatOpenAI + + return ChatOpenAI(model=os.environ["MODEL"], streaming=True) + + from scripted_model import ScriptedModel + + return ScriptedModel() + + +tools = [send_email] +model = make_model().bind_tools(tools) + + +async def call_model(state: MessagesState): + return {"messages": [await model.ainvoke(state["messages"])]} + + +builder = StateGraph(MessagesState) +builder.add_node("agent", call_model) +builder.add_node("tools", ToolNode(tools)) +builder.add_edge(START, "agent") +builder.add_conditional_edges("agent", tools_condition) +builder.add_edge("tools", "agent") + +app = FastAPI() +add_langgraph_fastapi_endpoint( + app, + # emit_interrupt_outcome: a pause ends the run the way AG-UI 1.0 describes it, which is what the chat reads. + LangGraphAgent(name="assistant", graph=builder.compile(checkpointer=MemorySaver()), emit_interrupt_outcome=True), + path="/agent", +) diff --git a/langgraph-ag-ui/index.html b/langgraph-ag-ui/index.html new file mode 100644 index 0000000..fd5f975 --- /dev/null +++ b/langgraph-ag-ui/index.html @@ -0,0 +1,15 @@ + + + + + + LangGraph + aparté + + + + + + + diff --git a/langgraph-ag-ui/package-lock.json b/langgraph-ag-ui/package-lock.json new file mode 100644 index 0000000..861c1a8 --- /dev/null +++ b/langgraph-ag-ui/package-lock.json @@ -0,0 +1,891 @@ +{ + "name": "langgraph-ag-ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "langgraph-ag-ui", + "version": "0.0.1", + "dependencies": { + "@aparte/core": "^1.0.2", + "@aparte/plugin-marked": "^1.0.2", + "@aparte/runner-ag-ui": "^1.0.2", + "marked": "^18.1.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vite": "^8.3.0" + } + }, + "node_modules/@aparte/core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/core/-/core-1.0.2.tgz", + "integrity": "sha512-fOZ7/B9lPPpGvzrF9YYWrqRFNGskm9g41b4QLE5eKa3UOySuWQIytfFUVHIkqV9y8F5NouO87BKGfj1Nmud5Qg==", + "license": "MIT", + "dependencies": { + "@aparte/engine": "1.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@aparte/engine": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/engine/-/engine-1.0.2.tgz", + "integrity": "sha512-ZkC5gL9tPanB48rm2bNXbK/0ZvV7h2K87ujJYLtFBaqZV/nUbRLRZtS4O2VLeeC/PbOQP6ve+UXIBaYYFSrFlw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependenciesMeta": { + "@aparte/core": { + "optional": true + } + } + }, + "node_modules/@aparte/plugin-marked": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/plugin-marked/-/plugin-marked-1.0.2.tgz", + "integrity": "sha512-WVpKWNB+EZslxij8faXthOb16/H7EEruBvLfEmbvDC0FW55OT0L0k3yDy5yEp6yfiDkxnwZuXC4MT6OQeFxf8w==", + "license": "MIT", + "dependencies": { + "remend": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aparte/core": ">=1.0.2 <2.0.0", + "marked": "^18.0.0" + } + }, + "node_modules/@aparte/runner-ag-ui": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/runner-ag-ui/-/runner-ag-ui-1.0.2.tgz", + "integrity": "sha512-EaClxTttxf8Jim7Zqt2VLhTT39o5pIwuMOrRb4NQSG9xtIYm2dCFYf5y8+A3graZIklHGN/7B+zUaTxiFr+eiw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aparte/core": ">=1.0.2 <2.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/marked": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.3.tgz", + "integrity": "sha512-9ChMH8EVVgjgID49nsL6IWxWk5c6GtAsq/xw7U1ggkjkU5AGuv68oLTVlFRtvcUYZtpbNyjyvGo3YDu0fGapiw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/remend": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/remend/-/remend-1.4.0.tgz", + "integrity": "sha512-mHiYhhwphs1IgIZmkToKLwJn0U99IjPXuutkA2acrNcG0vlEq2G4TGnd2eT0saIHwh2KjaCpvleHcsd5SAz96A==", + "license": "Apache-2.0" + }, + "node_modules/rolldown": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.156.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", + "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.32", + "rolldown": "~1.2.16", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/langgraph-ag-ui/package.json b/langgraph-ag-ui/package.json new file mode 100644 index 0000000..6661343 --- /dev/null +++ b/langgraph-ag-ui/package.json @@ -0,0 +1,21 @@ +{ + "name": "langgraph-ag-ui", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "check": "tsc" + }, + "dependencies": { + "@aparte/core": "^1.0.2", + "@aparte/plugin-marked": "^1.0.2", + "@aparte/runner-ag-ui": "^1.0.2", + "marked": "^18.1.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vite": "^8.3.0" + } +} diff --git a/langgraph-ag-ui/requirements.txt b/langgraph-ag-ui/requirements.txt new file mode 100644 index 0000000..fcd9b5c --- /dev/null +++ b/langgraph-ag-ui/requirements.txt @@ -0,0 +1,7 @@ +ag-ui-langgraph==0.0.47 +fastapi==0.162.2 +httpx==0.28.1 +langchain-openai==1.7.4 +langgraph==1.2.20 +python-dotenv==1.2.4 +uvicorn==0.54.0 diff --git a/langgraph-ag-ui/scripted_model.py b/langgraph-ag-ui/scripted_model.py new file mode 100644 index 0000000..54b498f --- /dev/null +++ b/langgraph-ag-ui/scripted_model.py @@ -0,0 +1,43 @@ +"""Answers when no model is configured, and says so. It streams word by word, as a real model does.""" + +import json +from typing import Any, Iterator +from uuid import uuid4 + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, ToolMessage +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult + +NOTE = "This is a scripted reply: set MODEL in .env to talk to a real model." + + +class ScriptedModel(BaseChatModel): + @property + def _llm_type(self) -> str: + return "scripted" + + def bind_tools(self, tools: Any, **kwargs: Any) -> "ScriptedModel": + return self + + def _reply(self, messages: list[BaseMessage]) -> AIMessage: + last = messages[-1] + if isinstance(last, ToolMessage): + return AIMessage(content=f"{last.content} {NOTE}") + if "email" in str(last.content).lower(): + args = {"to": "ada@example.com", "subject": "Hello from LangGraph"} + return AIMessage(content="", tool_calls=[{"id": f"call_{uuid4().hex}", "name": "send_email", "args": args}]) + return AIMessage(content=f"{NOTE} Ask me to **send an email** to see a tool wait for your approval.") + + def _generate(self, messages: list[BaseMessage], stop: Any = None, run_manager: Any = None, **kwargs: Any) -> ChatResult: + return ChatResult(generations=[ChatGeneration(message=self._reply(messages))]) + + def _stream(self, messages: list[BaseMessage], stop: Any = None, run_manager: Any = None, **kwargs: Any) -> Iterator[ChatGenerationChunk]: + reply = self._reply(messages) + if reply.tool_calls: + call = reply.tool_calls[0] + chunk = {"id": call["id"], "name": call["name"], "args": json.dumps(call["args"]), "index": 0} + yield ChatGenerationChunk(message=AIMessageChunk(content="", tool_call_chunks=[chunk])) + return + words = str(reply.content).split(" ") + for i, word in enumerate(words): + yield ChatGenerationChunk(message=AIMessageChunk(content=word if i == 0 else f" {word}")) diff --git a/langgraph-ag-ui/smoke_test.py b/langgraph-ag-ui/smoke_test.py new file mode 100644 index 0000000..782a61a --- /dev/null +++ b/langgraph-ag-ui/smoke_test.py @@ -0,0 +1,45 @@ +"""Runs the agent with the scripted model, through the same route the page calls. CI runs this file.""" + +import json +from uuid import uuid4 + +from fastapi.testclient import TestClient + +from agent import app + +client = TestClient(app) + + +def run(thread_id: str, **body) -> list[dict]: + payload = {"threadId": thread_id, "runId": uuid4().hex, "state": {}, "messages": [], "tools": [], "context": [], "forwardedProps": {}, **body} + response = client.post("/agent", json=payload, headers={"accept": "text/event-stream"}) + assert response.status_code == 200, response.text + return [json.loads(line[6:]) for line in response.text.splitlines() if line.startswith("data: ")] + + +def user(text: str) -> dict: + return {"id": uuid4().hex, "role": "user", "content": text} + + +def of_type(events: list[dict], name: str) -> list[dict]: + return [e for e in events if e["type"] == name] + + +events = run(uuid4().hex, messages=[user("Hello")]) +assert events[0]["type"] == "RUN_STARTED" and events[-1]["type"] == "RUN_FINISHED", events +text = "".join(e["delta"] for e in of_type(events, "TEXT_MESSAGE_CONTENT")) +assert "scripted reply" in text, text +assert len(of_type(events, "TEXT_MESSAGE_CONTENT")) > 1, "the reply did not stream" + +thread = uuid4().hex +paused = run(thread, messages=[user("Send an email")]) +assert of_type(paused, "TOOL_CALL_START")[0]["toolCallName"] == "send_email", paused +interrupts = paused[-1]["outcome"]["interrupts"] +assert interrupts[0]["reason"] == "tool_call" and interrupts[0]["toolCallId"], interrupts + +resumed = run(thread, resume=[{"interruptId": interrupts[0]["id"], "status": "resolved", "payload": {"approved": True}}]) +result = of_type(resumed, "TOOL_CALL_RESULT")[0]["content"] +assert "Email sent" in result, resumed +assert of_type(resumed, "RUN_FINISHED")[-1].get("outcome", {"type": "success"})["type"] == "success", resumed + +print("ok") diff --git a/langgraph-ag-ui/src/main.ts b/langgraph-ag-ui/src/main.ts new file mode 100644 index 0000000..65b2f73 --- /dev/null +++ b/langgraph-ag-ui/src/main.ts @@ -0,0 +1,9 @@ +import '@aparte/core/styles.css'; +import { AparteClient } from '@aparte/core'; +import { setupMarkedProvider } from '@aparte/plugin-marked'; +import { createAgUiRunner } from '@aparte/runner-ag-ui'; + +setupMarkedProvider(); + +// The page knows nothing about LangGraph: it reads the agent's AG-UI stream. +new AparteClient({ streamRunner: createAgUiRunner({ endpoint: '/agent' }) }).start(); diff --git a/langgraph-ag-ui/tsconfig.json b/langgraph-ag-ui/tsconfig.json new file mode 100644 index 0000000..69fa3be --- /dev/null +++ b/langgraph-ag-ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/langgraph-ag-ui/vite.config.ts b/langgraph-ag-ui/vite.config.ts new file mode 100644 index 0000000..f22eab8 --- /dev/null +++ b/langgraph-ag-ui/vite.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { proxy: { '/agent': 'http://localhost:8000' } }, +}); diff --git a/pydantic-ai-ag-ui/.env.example b/pydantic-ai-ag-ui/.env.example new file mode 100644 index 0000000..e959120 --- /dev/null +++ b/pydantic-ai-ag-ui/.env.example @@ -0,0 +1,5 @@ +# Optional. Without MODEL, a scripted model answers (scripted_model.py). +MODEL= +OPENAI_API_KEY= +# For any other OpenAI-compatible API, a local one included. +OPENAI_BASE_URL= diff --git a/pydantic-ai-ag-ui/README.md b/pydantic-ai-ag-ui/README.md new file mode 100644 index 0000000..411c627 --- /dev/null +++ b/pydantic-ai-ag-ui/README.md @@ -0,0 +1,36 @@ +# Pydantic AI + aparté + +A [Pydantic AI](https://ai.pydantic.dev) agent served over [AG-UI](https://docs.ag-ui.com), with [aparté](https://apartejs.dev) as its chat. Replies stream, tool calls show as they run, and one tool waits for a person before it runs. + +The page knows nothing about Pydantic AI: `@aparte/runner-ag-ui` reads the agent's AG-UI stream. + +## Run it + +The agent runs in Python, so this one runs on your machine and not on StackBlitz. Tested with Python 3.13 and Node 24. + +```bash +npx degit apartejs/examples/pydantic-ai-ag-ui my-agent +cd my-agent +python -m venv .venv +source .venv/bin/activate # on Windows: .venv\Scripts\activate +pip install -r requirements.txt +uvicorn agent:app --port 8000 +``` + +In a second terminal: + +```bash +npm install +npm run dev +``` + +Open http://localhost:5173 and ask it to send an email. + +With nothing configured, a scripted model answers, and says it is scripted. For a real model, copy `.env.example` to `.env` and set `MODEL` and `OPENAI_API_KEY`. `OPENAI_BASE_URL` points it at any OpenAI-compatible API, a local one included. + +## What to look at + +- [`agent.py`](./agent.py): the agent and its route. `requires_approval=True` on the tool, with `DeferredToolRequests` in the agent's output, is all it takes to pause for a person. The agent keeps nothing between runs: the page sends the conversation back with its answer. +- [`src/main.ts`](./src/main.ts): the whole page. +- [`scripted_model.py`](./scripted_model.py): the stand-in model. +- [`smoke_test.py`](./smoke_test.py): what CI runs against the route: a reply, a pause, and a resume. diff --git a/pydantic-ai-ag-ui/agent.py b/pydantic-ai-ag-ui/agent.py new file mode 100644 index 0000000..8bc0bdc --- /dev/null +++ b/pydantic-ai-ag-ui/agent.py @@ -0,0 +1,41 @@ +"""A Pydantic AI agent served over AG-UI, with a tool that waits for a person.""" + +import os + +from dotenv import load_dotenv +from pydantic_ai import Agent, DeferredToolRequests +from pydantic_ai.ui.ag_ui import AGUIAdapter +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route + +load_dotenv() + + +def make_model(): + if os.environ.get("MODEL"): + from pydantic_ai.models.openai import OpenAIChatModel + + return OpenAIChatModel(os.environ["MODEL"]) + + from scripted_model import scripted_model + + return scripted_model + + +# DeferredToolRequests in the output is what lets a run stop and wait for an approval. +agent = Agent(make_model(), output_type=[str, DeferredToolRequests]) + + +@agent.tool_plain(requires_approval=True) +def send_email(to: str, subject: str) -> str: + """Send an email. A person has to approve it first.""" + return f"Email sent to {to}: {subject}" + + +async def run_agent(request: Request) -> Response: + return await AGUIAdapter.dispatch_request(request, agent=agent) + + +app = Starlette(routes=[Route("/agent", run_agent, methods=["POST"])]) diff --git a/pydantic-ai-ag-ui/index.html b/pydantic-ai-ag-ui/index.html new file mode 100644 index 0000000..69787be --- /dev/null +++ b/pydantic-ai-ag-ui/index.html @@ -0,0 +1,15 @@ + + + + + + Pydantic AI + aparté + + + + + + + diff --git a/pydantic-ai-ag-ui/package-lock.json b/pydantic-ai-ag-ui/package-lock.json new file mode 100644 index 0000000..99558ef --- /dev/null +++ b/pydantic-ai-ag-ui/package-lock.json @@ -0,0 +1,891 @@ +{ + "name": "pydantic-ai-ag-ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pydantic-ai-ag-ui", + "version": "0.0.1", + "dependencies": { + "@aparte/core": "^1.0.2", + "@aparte/plugin-marked": "^1.0.2", + "@aparte/runner-ag-ui": "^1.0.2", + "marked": "^18.1.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vite": "^8.3.0" + } + }, + "node_modules/@aparte/core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/core/-/core-1.0.2.tgz", + "integrity": "sha512-fOZ7/B9lPPpGvzrF9YYWrqRFNGskm9g41b4QLE5eKa3UOySuWQIytfFUVHIkqV9y8F5NouO87BKGfj1Nmud5Qg==", + "license": "MIT", + "dependencies": { + "@aparte/engine": "1.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@aparte/engine": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/engine/-/engine-1.0.2.tgz", + "integrity": "sha512-ZkC5gL9tPanB48rm2bNXbK/0ZvV7h2K87ujJYLtFBaqZV/nUbRLRZtS4O2VLeeC/PbOQP6ve+UXIBaYYFSrFlw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependenciesMeta": { + "@aparte/core": { + "optional": true + } + } + }, + "node_modules/@aparte/plugin-marked": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/plugin-marked/-/plugin-marked-1.0.2.tgz", + "integrity": "sha512-WVpKWNB+EZslxij8faXthOb16/H7EEruBvLfEmbvDC0FW55OT0L0k3yDy5yEp6yfiDkxnwZuXC4MT6OQeFxf8w==", + "license": "MIT", + "dependencies": { + "remend": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aparte/core": ">=1.0.2 <2.0.0", + "marked": "^18.0.0" + } + }, + "node_modules/@aparte/runner-ag-ui": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aparte/runner-ag-ui/-/runner-ag-ui-1.0.2.tgz", + "integrity": "sha512-EaClxTttxf8Jim7Zqt2VLhTT39o5pIwuMOrRb4NQSG9xtIYm2dCFYf5y8+A3graZIklHGN/7B+zUaTxiFr+eiw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@aparte/core": ">=1.0.2 <2.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/marked": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.3.tgz", + "integrity": "sha512-9ChMH8EVVgjgID49nsL6IWxWk5c6GtAsq/xw7U1ggkjkU5AGuv68oLTVlFRtvcUYZtpbNyjyvGo3YDu0fGapiw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/remend": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/remend/-/remend-1.4.0.tgz", + "integrity": "sha512-mHiYhhwphs1IgIZmkToKLwJn0U99IjPXuutkA2acrNcG0vlEq2G4TGnd2eT0saIHwh2KjaCpvleHcsd5SAz96A==", + "license": "Apache-2.0" + }, + "node_modules/rolldown": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.156.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", + "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.32", + "rolldown": "~1.2.16", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/pydantic-ai-ag-ui/package.json b/pydantic-ai-ag-ui/package.json new file mode 100644 index 0000000..80472a9 --- /dev/null +++ b/pydantic-ai-ag-ui/package.json @@ -0,0 +1,21 @@ +{ + "name": "pydantic-ai-ag-ui", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "check": "tsc" + }, + "dependencies": { + "@aparte/core": "^1.0.2", + "@aparte/plugin-marked": "^1.0.2", + "@aparte/runner-ag-ui": "^1.0.2", + "marked": "^18.1.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vite": "^8.3.0" + } +} diff --git a/pydantic-ai-ag-ui/requirements.txt b/pydantic-ai-ag-ui/requirements.txt new file mode 100644 index 0000000..075d9ca --- /dev/null +++ b/pydantic-ai-ag-ui/requirements.txt @@ -0,0 +1,4 @@ +httpx==0.28.1 +pydantic-ai-slim[ag-ui,openai]==2.66.0 +python-dotenv==1.2.4 +uvicorn==0.54.0 diff --git a/pydantic-ai-ag-ui/scripted_model.py b/pydantic-ai-ag-ui/scripted_model.py new file mode 100644 index 0000000..2a49449 --- /dev/null +++ b/pydantic-ai-ag-ui/scripted_model.py @@ -0,0 +1,27 @@ +"""Answers when no model is configured, and says so. It streams word by word, as a real model does.""" + +import json +from collections.abc import AsyncIterator +from uuid import uuid4 + +from pydantic_ai.messages import ModelMessage, ToolReturnPart +from pydantic_ai.models.function import AgentInfo, DeltaToolCall, DeltaToolCalls, FunctionModel + +NOTE = "This is a scripted reply: set MODEL in .env to talk to a real model." + + +async def stream(messages: list[ModelMessage], info: AgentInfo) -> AsyncIterator[str | DeltaToolCalls]: + last = messages[-1].parts[-1] + if isinstance(last, ToolReturnPart): + text = f"{last.content} {NOTE}" + elif "email" in str(last.content).lower(): + args = {"to": "ada@example.com", "subject": "Hello from Pydantic AI"} + yield {0: DeltaToolCall(name="send_email", json_args=json.dumps(args), tool_call_id=f"call_{uuid4().hex}")} + return + else: + text = f"{NOTE} Ask me to **send an email** to see a tool wait for your approval." + for i, word in enumerate(text.split(" ")): + yield word if i == 0 else f" {word}" + + +scripted_model = FunctionModel(stream_function=stream) diff --git a/pydantic-ai-ag-ui/smoke_test.py b/pydantic-ai-ag-ui/smoke_test.py new file mode 100644 index 0000000..cec093f --- /dev/null +++ b/pydantic-ai-ag-ui/smoke_test.py @@ -0,0 +1,58 @@ +"""Runs the agent with the scripted model, through the same route the page calls. CI runs this file.""" + +import json +from uuid import uuid4 + +from starlette.testclient import TestClient + +from agent import app + +client = TestClient(app) + + +def run(thread_id: str, **body) -> list[dict]: + payload = {"threadId": thread_id, "runId": uuid4().hex, "state": {}, "messages": [], "tools": [], "context": [], "forwardedProps": {}, **body} + response = client.post("/agent", json=payload, headers={"accept": "text/event-stream"}) + assert response.status_code == 200, response.text + return [json.loads(line[6:]) for line in response.text.splitlines() if line.startswith("data: ")] + + +def user(text: str) -> dict: + return {"id": uuid4().hex, "role": "user", "content": text} + + +def of_type(events: list[dict], name: str) -> list[dict]: + return [e for e in events if e["type"] == name] + + +events = run(uuid4().hex, messages=[user("Hello")]) +assert events[0]["type"] == "RUN_STARTED" and events[-1]["type"] == "RUN_FINISHED", events +text = "".join(e["delta"] for e in of_type(events, "TEXT_MESSAGE_CONTENT")) +assert "scripted reply" in text, text +assert len(of_type(events, "TEXT_MESSAGE_CONTENT")) > 1, "the reply did not stream" + +thread = uuid4().hex +ask = user("Send an email") +paused = run(thread, messages=[ask]) +call = of_type(paused, "TOOL_CALL_START")[0] +assert call["toolCallName"] == "send_email", paused +args = "".join(e["delta"] for e in of_type(paused, "TOOL_CALL_ARGS")) +interrupts = paused[-1]["outcome"]["interrupts"] +assert interrupts[0]["reason"] == "tool_call" and interrupts[0]["toolCallId"] == call["toolCallId"], interrupts + +# The agent keeps nothing between runs: the page sends the conversation back with its answer. +assistant = { + "id": uuid4().hex, + "role": "assistant", + "toolCalls": [{"id": call["toolCallId"], "type": "function", "function": {"name": "send_email", "arguments": args}}], +} +resumed = run( + thread, + messages=[ask, assistant], + resume=[{"interruptId": interrupts[0]["id"], "status": "resolved", "payload": {"approved": True}}], +) +result = of_type(resumed, "TOOL_CALL_RESULT")[0]["content"] +assert "Email sent" in result, resumed +assert of_type(resumed, "RUN_FINISHED")[-1].get("outcome", {"type": "success"})["type"] == "success", resumed + +print("ok") diff --git a/pydantic-ai-ag-ui/src/main.ts b/pydantic-ai-ag-ui/src/main.ts new file mode 100644 index 0000000..5e182c6 --- /dev/null +++ b/pydantic-ai-ag-ui/src/main.ts @@ -0,0 +1,9 @@ +import '@aparte/core/styles.css'; +import { AparteClient } from '@aparte/core'; +import { setupMarkedProvider } from '@aparte/plugin-marked'; +import { createAgUiRunner } from '@aparte/runner-ag-ui'; + +setupMarkedProvider(); + +// The page knows nothing about Pydantic AI: it reads the agent's AG-UI stream. +new AparteClient({ streamRunner: createAgUiRunner({ endpoint: '/agent' }) }).start(); diff --git a/pydantic-ai-ag-ui/tsconfig.json b/pydantic-ai-ag-ui/tsconfig.json new file mode 100644 index 0000000..69fa3be --- /dev/null +++ b/pydantic-ai-ag-ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/pydantic-ai-ag-ui/vite.config.ts b/pydantic-ai-ag-ui/vite.config.ts new file mode 100644 index 0000000..f22eab8 --- /dev/null +++ b/pydantic-ai-ag-ui/vite.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { proxy: { '/agent': 'http://localhost:8000' } }, +});