- cli.py: interactive rich REPL. After each answer it prints a "capabilities used this turn" panel that maps every tool call back to the capability that owns it, and /faq swaps to the reuse widget live. (adds rich dependency) - capabilities.py: record which capabilities fire per turn (CAPABILITIES_FIRED); gate escalation on the NATURE of the request (billing dispute, account issue, bug, explicit human ask) instead of on any knowledge-base miss. - orbit_kb.py: add grounded articles - "What is Orbit?" (KB-07) and an integrations article (KB-08) that addresses Slack honestly - and point the billing article at support for specific charges. Existing IDs unchanged. - main.py: per-response "composed from / fired" readout; realistic demo questions (KB answer, real billing escalation, grounded Slack question). - test_agent.py: assert real escalation escalates and an answerable question does not. Full suite green (11 unit + 7 live). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .gitignore | ||
| .python-version | ||
| agent_v1_style.py | ||
| capabilities.py | ||
| cli.py | ||
| main.py | ||
| orbit_kb.py | ||
| pyproject.toml | ||
| README.md | ||
| sample_output.txt | ||
| test_agent.py | ||
| uv.lock | ||
Orbit Support Agent — the Pydantic AI 2.0 capability pattern
A small, runnable demo of the one idea that changed in Pydantic AI 2.0: you stop configuring an agent through a giant constructor and start composing it out of capabilities.
The idea
A capability is one composable brick that bundles everything that used to be
scattered across the Agent(...) call — its instructions, its tools, its model
settings, and its lifecycle hooks — into a single self-contained unit. You build an
agent by snapping bricks together, and the same brick drops into a completely
different agent, unchanged.
from pydantic_ai import Agent
from pydantic_ai.capabilities import Thinking
agent = Agent(
"openrouter:anthropic/claude-sonnet-4.6",
capabilities=[
KnowledgeBaseCapability(), # its instructions + its tool + model settings + a guardrail
EscalationCapability(), # its own tool + its own instructions
Thinking(effort="low"), # a built-in capability, one line
audit_hooks(), # lifecycle hooks, no subclass needed
],
)
That is the whole thesis: compose, don't configure. Each brick carries its own instructions, tools, settings, and hooks, so nothing has to be hand-wired onto the agent and nothing is welded to one model. The payoff is reuse — pick a brick up and drop it into the next agent without touching it.
What's in here
| File | What it shows |
|---|---|
cli.py |
An interactive REPL. After every answer it prints a "capabilities used this turn" panel that maps each tool call back to the capability that owns it. /faq swaps to an agent that reuses the KnowledgeBase brick, live. |
main.py |
The agent composed from capabilities, then the reuse payoff: the same brick dropped into a second, unrelated agent without a single change. |
capabilities.py |
The capabilities themselves — each one a small class bundling its own instructions, tool, settings, and hooks. |
agent_v1_style.py |
The "before", for contrast: the identical behavior crammed into one bloated constructor, with every cross-cutting concern hand-wired because there's nowhere for it to live. |
test_agent.py |
Deterministic unit checks + optional live behavioral checks. |
Run it
uv sync
uv run python cli.py # interactive REPL — see which capabilities fire per turn
uv run python main.py # scripted, non-interactive version of the same demo
uv run python agent_v1_style.py # the bloated "before", for contrast
uv run python test_agent.py # unit tests (no API)
uv run python test_agent.py --live # + end-to-end LLM checks
In the REPL, type a question and watch the "capabilities used this turn" panel. Useful
commands: /faq (switch to the agent that reuses the KnowledgeBase brick), /support
(switch back), /caps, /reset, /help, /exit.
Or send a single prompt non-interactively:
uv run python main.py "How do I export my data before I cancel?"
uv run python agent_v1_style.py "How do I export my data before I cancel?"
Model / keys. Defaults to Claude Sonnet 4.6 via OpenRouter; keys load from a local
.env and are never printed. Override the model with MODEL=...:
MODEL=anthropic:claude-sonnet-4-6 uv run python main.py # direct Anthropic key
MODEL=openrouter:anthropic/claude-haiku-4.5 uv run python main.py # cheaper