Skip to content

Commit 781dfe1

Browse files
feat: added Pydantic AI sync, async, temporal integration (#359)
1 parent bdf129c commit 781dfe1

86 files changed

Lines changed: 7404 additions & 32 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Environments
24+
.env**
25+
.venv
26+
env/
27+
venv/
28+
ENV/
29+
env.bak/
30+
venv.bak/
31+
32+
# IDE
33+
.idea/
34+
.vscode/
35+
*.swp
36+
*.swo
37+
38+
# Git
39+
.git
40+
.gitignore
41+
42+
# Misc
43+
.DS_Store
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# syntax=docker/dockerfile:1.3
2+
FROM python:3.12-slim
3+
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
htop \
8+
vim \
9+
curl \
10+
tar \
11+
python3-dev \
12+
postgresql-client \
13+
build-essential \
14+
libpq-dev \
15+
gcc \
16+
cmake \
17+
netcat-openbsd \
18+
&& apt-get clean \
19+
&& rm -rf /var/lib/apt/lists/*
20+
21+
RUN uv pip install --system --upgrade pip setuptools wheel
22+
23+
ENV UV_HTTP_TIMEOUT=1000
24+
25+
# Copy pyproject.toml and README.md to install dependencies
26+
COPY 00_sync/040_pydantic_ai/pyproject.toml /app/040_pydantic_ai/pyproject.toml
27+
COPY 00_sync/040_pydantic_ai/README.md /app/040_pydantic_ai/README.md
28+
29+
WORKDIR /app/040_pydantic_ai
30+
31+
# Copy the project code
32+
COPY 00_sync/040_pydantic_ai/project /app/040_pydantic_ai/project
33+
34+
# Copy the test files
35+
COPY 00_sync/040_pydantic_ai/tests /app/040_pydantic_ai/tests
36+
37+
# Copy shared test utilities
38+
COPY test_utils /app/test_utils
39+
40+
# Install the required Python packages with dev dependencies
41+
RUN uv pip install --system .[dev]
42+
43+
# Set environment variables
44+
ENV PYTHONPATH=/app
45+
46+
# Set test environment variables
47+
ENV AGENT_NAME=s040-pydantic-ai
48+
49+
# Run the agent using uvicorn
50+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Tutorial 040: Sync Pydantic AI Agent
2+
3+
This tutorial demonstrates how to build a **synchronous** Pydantic AI agent on AgentEx with:
4+
- Tool calling (Pydantic AI handles the tool loop internally)
5+
- Streaming token output (including token-by-token tool-call argument streaming)
6+
7+
## Key Concepts
8+
9+
### Sync ACP
10+
The sync ACP model uses HTTP request/response for communication. The `@acp.on_message_send` handler receives a message and yields streaming events back to the client.
11+
12+
### Pydantic AI Integration
13+
- **Agent**: A single `pydantic_ai.Agent` that owns the model and tools. No graph required — Pydantic AI runs its own tool-call loop until the model is done.
14+
- **`@agent.tool_plain`**: Registers a Python function as a tool. Pydantic AI infers the schema from type hints and docstring.
15+
- **`agent.run_stream_events(...)`**: Yields `AgentStreamEvent`s (PartStartEvent / PartDeltaEvent / PartEndEvent / FunctionToolResultEvent) as the model produces them.
16+
17+
### Streaming
18+
The agent streams tokens and tool-call arguments as they're generated using `convert_pydantic_ai_to_agentex_events()`, which adapts Pydantic AI's stream into AgentEx `TaskMessageUpdate` events. Notably, **tool-call arguments stream as `ToolRequestDelta` tokens** rather than arriving as a single complete payload — a richer experience than what OpenAI Agents SDK currently exposes.
19+
20+
## Files
21+
22+
| File | Description |
23+
|------|-------------|
24+
| `project/acp.py` | ACP server and message handler |
25+
| `project/agent.py` | Pydantic AI agent + tool registration |
26+
| `project/tools.py` | Tool definitions (weather example) |
27+
| `tests/test_agent.py` | Integration tests |
28+
| `manifest.yaml` | Agent configuration |
29+
30+
## Running Locally
31+
32+
```bash
33+
# From this directory
34+
agentex agents run
35+
```
36+
37+
## Running Tests
38+
39+
```bash
40+
pytest tests/test_agent.py -v
41+
```
42+
43+
## Notes
44+
45+
- Multi-turn conversation memory is not wired in this tutorial. Pydantic AI does not ship a checkpointer like LangGraph; to add memory, load prior messages via `adk.messages.list(task_id=...)` and pass them to `agent.run_stream_events(..., message_history=...)`.
46+
- Reasoning/thinking tokens are not exercised here because `gpt-4o-mini` does not emit `ThinkingPart`s. Swap to a reasoning-capable model (e.g. `openai:o1-mini` via Pydantic AI's appropriate provider) if you want to test that branch end-to-end.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
build:
2+
context:
3+
root: ../../
4+
include_paths:
5+
- 00_sync/040_pydantic_ai
6+
- test_utils
7+
dockerfile: 00_sync/040_pydantic_ai/Dockerfile
8+
dockerignore: 00_sync/040_pydantic_ai/.dockerignore
9+
10+
local_development:
11+
agent:
12+
port: 8000
13+
host_address: host.docker.internal
14+
paths:
15+
acp: project/acp.py
16+
17+
agent:
18+
acp_type: sync
19+
name: s040-pydantic-ai
20+
description: A sync Pydantic AI agent with tool calling and streaming
21+
22+
temporal:
23+
enabled: false
24+
25+
credentials:
26+
- env_var_name: OPENAI_API_KEY
27+
secret_name: openai-api-key
28+
secret_key: api-key
29+
- env_var_name: REDIS_URL
30+
secret_name: redis-url-secret
31+
secret_key: url
32+
- env_var_name: SGP_API_KEY
33+
secret_name: sgp-api-key
34+
secret_key: api-key
35+
- env_var_name: SGP_ACCOUNT_ID
36+
secret_name: sgp-account-id
37+
secret_key: account-id
38+
- env_var_name: SGP_CLIENT_BASE_URL
39+
secret_name: sgp-client-base-url
40+
secret_key: url
41+
42+
deployment:
43+
image:
44+
repository: ""
45+
tag: "latest"
46+
47+
global:
48+
agent:
49+
name: "s040-pydantic-ai"
50+
description: "A sync Pydantic AI agent with tool calling and streaming"
51+
replicaCount: 1
52+
resources:
53+
requests:
54+
cpu: "500m"
55+
memory: "1Gi"
56+
limits:
57+
cpu: "1000m"
58+
memory: "2Gi"

examples/tutorials/00_sync/040_pydantic_ai/project/__init__.py

Whitespace-only changes.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""ACP (Agent Communication Protocol) handler for Agentex.
2+
3+
This is the API layer — it owns the agent lifecycle and streams tokens
4+
and tool calls from the Pydantic AI agent to the Agentex frontend.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import os
10+
from typing import AsyncGenerator
11+
12+
from dotenv import load_dotenv
13+
14+
load_dotenv()
15+
16+
import agentex.lib.adk as adk
17+
from project.agent import create_agent
18+
from agentex.lib.adk import (
19+
create_pydantic_ai_tracing_handler,
20+
convert_pydantic_ai_to_agentex_events,
21+
)
22+
from agentex.lib.types.acp import SendMessageParams
23+
from agentex.lib.types.tracing import SGPTracingProcessorConfig
24+
from agentex.lib.utils.logging import make_logger
25+
from agentex.lib.sdk.fastacp.fastacp import FastACP
26+
from agentex.types.task_message_update import TaskMessageUpdate
27+
from agentex.types.task_message_content import TaskMessageContent
28+
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
29+
30+
logger = make_logger(__name__)
31+
32+
add_tracing_processor_config(
33+
SGPTracingProcessorConfig(
34+
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
35+
sgp_account_id=os.environ.get("SGP_ACCOUNT_ID", ""),
36+
sgp_base_url=os.environ.get("SGP_CLIENT_BASE_URL", ""),
37+
)
38+
)
39+
40+
acp = FastACP.create(acp_type="sync")
41+
42+
_agent = None
43+
44+
45+
def get_agent():
46+
"""Get or create the Pydantic AI agent instance."""
47+
global _agent
48+
if _agent is None:
49+
_agent = create_agent()
50+
return _agent
51+
52+
53+
@acp.on_message_send
54+
async def handle_message_send(
55+
params: SendMessageParams,
56+
) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
57+
"""Handle incoming messages from Agentex, streaming tokens and tool calls."""
58+
agent = get_agent()
59+
task_id = params.task.id
60+
61+
user_message = params.content.content
62+
logger.info(f"Processing message for task {task_id}")
63+
64+
async with adk.tracing.span(
65+
trace_id=task_id,
66+
task_id=task_id,
67+
name="message",
68+
input={"message": user_message},
69+
data={"__span_type__": "AGENT_WORKFLOW"},
70+
) as turn_span:
71+
tracing_handler = create_pydantic_ai_tracing_handler(
72+
trace_id=task_id,
73+
parent_span_id=turn_span.id if turn_span else None,
74+
task_id=task_id,
75+
)
76+
async with agent.run_stream_events(user_message) as stream:
77+
async for event in convert_pydantic_ai_to_agentex_events(stream, tracing_handler=tracing_handler):
78+
yield event
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Pydantic AI agent definition.
2+
3+
The Agent is the boundary between this module and the API layer (acp.py).
4+
Pydantic AI handles its own tool-call loop internally — no graph required.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from datetime import datetime
10+
11+
from pydantic_ai import Agent
12+
13+
from project.tools import get_weather
14+
15+
MODEL_NAME = "openai:gpt-4o-mini"
16+
SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools.
17+
18+
Current date and time: {timestamp}
19+
20+
Guidelines:
21+
- Be concise and helpful
22+
- Use tools when they would help answer the user's question
23+
- If you're unsure, ask clarifying questions
24+
- Always provide accurate information
25+
"""
26+
27+
28+
def create_agent() -> Agent:
29+
"""Build and return the Pydantic AI agent with tools registered."""
30+
agent = Agent(
31+
MODEL_NAME,
32+
system_prompt=SYSTEM_PROMPT.format(
33+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
34+
),
35+
)
36+
37+
agent.tool_plain(get_weather)
38+
39+
return agent
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Tool definitions for the Pydantic AI agent.
2+
3+
Pydantic AI tools are registered directly on the Agent via decorators
4+
(see project.agent). This module hosts the bare functions so they're
5+
easy to unit-test in isolation.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
11+
def get_weather(city: str) -> str:
12+
"""Get the current weather for a city.
13+
14+
Args:
15+
city: The name of the city to get weather for.
16+
17+
Returns:
18+
A string describing the weather conditions.
19+
"""
20+
return f"The weather in {city} is sunny and 72°F"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "s040-pydantic-ai"
7+
version = "0.1.0"
8+
description = "A sync Pydantic AI agent with tool calling and streaming"
9+
readme = "README.md"
10+
requires-python = ">=3.12"
11+
dependencies = [
12+
"agentex-sdk",
13+
"scale-gp",
14+
"pydantic-ai-slim[openai]>=1.0,<2",
15+
]
16+
17+
[project.optional-dependencies]
18+
dev = [
19+
"pytest",
20+
"pytest-asyncio",
21+
"httpx",
22+
"black",
23+
"isort",
24+
"flake8",
25+
]
26+
27+
[tool.hatch.build.targets.wheel]
28+
packages = ["project"]
29+
30+
[tool.black]
31+
line-length = 88
32+
target-version = ['py312']
33+
34+
[tool.isort]
35+
profile = "black"
36+
line_length = 88

0 commit comments

Comments
 (0)