chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""Claude Agent SDK (Python) starter — AG-UI server.
|
||||
|
||||
Serves the agent (defined in ``src/agent.py``) over AG-UI using the official
|
||||
adapter's FastAPI helper: ``POST /`` streams the run, ``GET /health`` reports
|
||||
status. Runs on uvicorn (port 8000).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_claude_sdk import add_claude_fastapi_endpoint
|
||||
|
||||
from src.agent import adapter
|
||||
|
||||
app = FastAPI(title="Claude Agent SDK (Python) Starter")
|
||||
# The adapter's helper mounts POST / (streams the run) — the runtime connects here.
|
||||
add_claude_fastapi_endpoint(app, adapter)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=int(os.getenv("AGENT_PORT", "8000")))
|
||||
@@ -0,0 +1,18 @@
|
||||
[project]
|
||||
name = "claude-sdk-python-starter-agent"
|
||||
version = "0.1.0"
|
||||
description = "Claude Agent SDK integration server for AG-UI"
|
||||
requires-python = ">=3.12,<3.14"
|
||||
dependencies = [
|
||||
"anthropic==0.111.0",
|
||||
"claude-agent-sdk==0.2.104",
|
||||
"ag-ui-claude-sdk==0.1.5",
|
||||
"ag-ui-protocol==0.1.19",
|
||||
"copilotkit==0.1.94",
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn>=0.34.0",
|
||||
"python-dotenv>=1.0.1",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"id": "root",
|
||||
"component": "Row",
|
||||
"children": {
|
||||
"componentId": "flight-card",
|
||||
"path": "/flights"
|
||||
},
|
||||
"gap": 16
|
||||
},
|
||||
{
|
||||
"id": "flight-card",
|
||||
"component": "FlightCard",
|
||||
"airline": { "path": "airline" },
|
||||
"airlineLogo": { "path": "airlineLogo" },
|
||||
"flightNumber": { "path": "flightNumber" },
|
||||
"origin": { "path": "origin" },
|
||||
"destination": { "path": "destination" },
|
||||
"date": { "path": "date" },
|
||||
"departureTime": { "path": "departureTime" },
|
||||
"arrivalTime": { "path": "arrivalTime" },
|
||||
"duration": { "path": "duration" },
|
||||
"status": { "path": "status" },
|
||||
"price": { "path": "price" },
|
||||
"action": {
|
||||
"event": {
|
||||
"name": "book_flight",
|
||||
"context": {
|
||||
"flightNumber": { "path": "flightNumber" },
|
||||
"origin": { "path": "origin" },
|
||||
"destination": { "path": "destination" },
|
||||
"price": { "path": "price" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Dynamic A2UI tool — an LLM-designed dashboard.
|
||||
|
||||
A secondary Anthropic call designs a v0.9 A2UI surface via a structured
|
||||
``render_a2ui`` tool call; the result is wrapped as ``a2ui_operations`` for the
|
||||
frontend. The handler runs in this process (not the CLI subprocess), so it is
|
||||
free to make its own Anthropic request.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import anthropic
|
||||
from claude_agent_sdk import tool
|
||||
from copilotkit import a2ui
|
||||
|
||||
from src.model import resolve_model
|
||||
|
||||
CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
|
||||
# Structured-output schema handed to the secondary LLM to force one design call.
|
||||
_RENDER_A2UI_TOOL: dict[str, Any] = {
|
||||
"name": "render_a2ui",
|
||||
"description": (
|
||||
"Render a dynamic A2UI v0.9 surface. Provide a components array (flat "
|
||||
"v0.9 format; the root component must have id 'root') and an optional "
|
||||
"initial data model."
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"surfaceId": {"type": "string", "description": "Unique surface id."},
|
||||
"catalogId": {
|
||||
"type": "string",
|
||||
"description": f"Catalog id (use '{CATALOG_ID}').",
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "A2UI v0.9 component array (flat format).",
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Optional initial data model for the surface.",
|
||||
},
|
||||
},
|
||||
"required": ["surfaceId", "catalogId", "components"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@tool(
|
||||
"generate_a2ui",
|
||||
"Generate a dynamic A2UI dashboard (metrics, charts, tables, cards) based on "
|
||||
"the conversation. A secondary LLM designs the UI; it renders automatically.",
|
||||
{"context": str},
|
||||
)
|
||||
async def generate_a2ui(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Construct the client per call so it picks up ANTHROPIC_API_KEY /
|
||||
# ANTHROPIC_BASE_URL after the environment is loaded.
|
||||
client = anthropic.AsyncAnthropic()
|
||||
try:
|
||||
response = await client.messages.create(
|
||||
model=resolve_model(),
|
||||
max_tokens=4096,
|
||||
system=args.get("context") or "Generate a useful dashboard UI.",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Generate a dynamic A2UI dashboard based on the conversation.",
|
||||
}
|
||||
],
|
||||
tools=[_RENDER_A2UI_TOOL],
|
||||
tool_choice={"type": "tool", "name": "render_a2ui"},
|
||||
)
|
||||
except Exception:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps({"error": "Failed to generate A2UI dashboard"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
for block in response.content:
|
||||
if getattr(block, "type", None) == "tool_use" and block.name == "render_a2ui":
|
||||
spec = dict(block.input)
|
||||
surface_id = spec.get("surfaceId", "dynamic-surface")
|
||||
ops = [
|
||||
a2ui.create_surface(
|
||||
surface_id, catalog_id=spec.get("catalogId", CATALOG_ID)
|
||||
),
|
||||
a2ui.update_components(surface_id, spec.get("components", []) or []),
|
||||
]
|
||||
if spec.get("data"):
|
||||
ops.append(a2ui.update_data_model(surface_id, spec["data"]))
|
||||
return {"content": [{"type": "text", "text": a2ui.render(operations=ops)}]}
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps({"error": "LLM did not call render_a2ui"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Fixed-schema A2UI tool — flight search results.
|
||||
|
||||
The A2UI component schema is loaded from JSON; only the flight data changes per
|
||||
call. The tool result carries ``a2ui_operations``, which the frontend renders.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from claude_agent_sdk import tool
|
||||
from copilotkit import a2ui
|
||||
|
||||
CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
FLIGHT_SURFACE_ID = "flight-search-results"
|
||||
FLIGHT_SCHEMA = a2ui.load_schema(
|
||||
Path(__file__).parent / "a2ui" / "schemas" / "flight_schema.json"
|
||||
)
|
||||
|
||||
|
||||
class Flight(TypedDict):
|
||||
id: str
|
||||
airline: str
|
||||
airlineLogo: str
|
||||
flightNumber: str
|
||||
origin: str
|
||||
destination: str
|
||||
date: str
|
||||
departureTime: str
|
||||
arrivalTime: str
|
||||
duration: str
|
||||
status: str
|
||||
statusIcon: str
|
||||
price: str
|
||||
|
||||
|
||||
@tool(
|
||||
"search_flights",
|
||||
"Search for flights and display the results as rich cards. Return exactly 2 "
|
||||
"flights. Each flight must have: id, airline, airlineLogo (Google favicon API "
|
||||
"URL for the airline domain), flightNumber, origin, destination, date (e.g. "
|
||||
'"Tue, Mar 18" — use near-future dates), departureTime, arrivalTime, duration '
|
||||
'(e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"), statusIcon (colored '
|
||||
"dot URL: https://placehold.co/12/22c55e/22c55e.png for On Time, "
|
||||
'https://placehold.co/12/eab308/eab308.png for Delayed), and price (e.g. "$289").',
|
||||
{"flights": list[Flight]},
|
||||
)
|
||||
async def search_flights(args: dict[str, Any]) -> dict[str, Any]:
|
||||
flights = args.get("flights", [])
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": a2ui.render(
|
||||
operations=[
|
||||
a2ui.create_surface(FLIGHT_SURFACE_ID, catalog_id=CATALOG_ID),
|
||||
a2ui.update_components(FLIGHT_SURFACE_ID, FLIGHT_SCHEMA),
|
||||
a2ui.update_data_model(FLIGHT_SURFACE_ID, {"flights": flights}),
|
||||
]
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"""The Claude Agent SDK agent — backend tools + the official AG-UI adapter.
|
||||
|
||||
Three backend tools live in their own modules (``src/query.py``,
|
||||
``src/a2ui_fixed_schema.py``, ``src/a2ui_dynamic_schema.py``). The official
|
||||
``ClaudeAgentAdapter`` does everything else: it drives Claude via the Claude
|
||||
Agent SDK, bridges CopilotKit frontend tools + human-in-the-loop, and manages
|
||||
the shared ``todos`` state via its built-in ``ag_ui_update_state`` tool.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from ag_ui_claude_sdk import ClaudeAgentAdapter
|
||||
from claude_agent_sdk import create_sdk_mcp_server
|
||||
|
||||
from src.model import resolve_model
|
||||
from src.query import query_data
|
||||
from src.a2ui_fixed_schema import search_flights
|
||||
from src.a2ui_dynamic_schema import generate_a2ui
|
||||
|
||||
# Load .env from the starter root before building the adapter (which reads the
|
||||
# model from the environment); fall back to the current working directory.
|
||||
for _env in (Path(__file__).resolve().parents[2] / ".env", Path(".env")):
|
||||
if _env.is_file():
|
||||
load_dotenv(_env)
|
||||
break
|
||||
else:
|
||||
load_dotenv()
|
||||
|
||||
SYSTEM_PROMPT = dedent(
|
||||
"""
|
||||
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
|
||||
|
||||
- Flights: call search_flights to show flight cards.
|
||||
- Dashboards: call generate_a2ui to build a rich dashboard UI; it renders itself.
|
||||
- Charts: call query_data first, then render with the chart component.
|
||||
- Todos: the todo board is shared state under `todos`; call ag_ui_update_state
|
||||
with the COMPLETE list to add or change todos.
|
||||
"""
|
||||
).strip()
|
||||
|
||||
# The Claude Agent SDK exposes custom tools through an in-process MCP server
|
||||
# (create_sdk_mcp_server). The model calls them as mcp__<server>__<tool>, and
|
||||
# `allowed_tools` pre-approves those names so they run without a permission prompt.
|
||||
# (`tools` is a different field — Claude Code's BUILT-IN toolset; `[]` disables it
|
||||
# so the model only uses ours + the AG-UI protocol tools.)
|
||||
SERVER_NAME = "copilotkit"
|
||||
BACKEND_TOOLS = [query_data, search_flights, generate_a2ui]
|
||||
|
||||
adapter = ClaudeAgentAdapter(
|
||||
name="claude-sdk-python",
|
||||
description="CopilotKit × Claude Agent SDK (Python) starter",
|
||||
options={
|
||||
"model": resolve_model(),
|
||||
"system_prompt": SYSTEM_PROMPT,
|
||||
"mcp_servers": {
|
||||
SERVER_NAME: create_sdk_mcp_server(
|
||||
SERVER_NAME, "1.0.0", tools=BACKEND_TOOLS
|
||||
),
|
||||
},
|
||||
"allowed_tools": [f"mcp__{SERVER_NAME}__{tool.name}" for tool in BACKEND_TOOLS],
|
||||
"tools": [],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
date,category,subcategory,amount,type,notes
|
||||
2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
|
||||
2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
|
||||
2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
|
||||
2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
|
||||
2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
|
||||
2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
|
||||
2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
|
||||
2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
|
||||
2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
|
||||
2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
|
||||
2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
|
||||
2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
|
||||
2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
|
||||
2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
|
||||
2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
|
||||
2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
|
||||
2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
|
||||
2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
|
||||
2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
|
||||
2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
|
||||
2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
|
||||
2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
|
||||
2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
|
||||
2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
|
||||
2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
|
||||
2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
|
||||
2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
|
||||
2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
|
||||
2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
|
||||
2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
|
||||
2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
|
||||
2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
|
||||
2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
|
||||
2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
|
||||
2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
|
||||
2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
|
||||
2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
|
||||
2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
|
||||
2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
|
||||
2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
|
||||
|
@@ -0,0 +1,22 @@
|
||||
"""Model selection shared across the agent's tools and the adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
DEFAULT_CLAUDE_MODEL = "claude-sonnet-5"
|
||||
|
||||
|
||||
def resolve_model() -> str:
|
||||
"""Resolve the Claude model id from the environment.
|
||||
|
||||
Prefers ``CLAUDE_MODEL``, then ``ANTHROPIC_MODEL``, then the default. A
|
||||
dotted marketing name (e.g. ``claude-sonnet-4.5``) is normalized to the API
|
||||
id (``claude-sonnet-4-5``).
|
||||
"""
|
||||
raw = (
|
||||
os.getenv("CLAUDE_MODEL")
|
||||
or os.getenv("ANTHROPIC_MODEL")
|
||||
or DEFAULT_CLAUDE_MODEL
|
||||
)
|
||||
return raw.replace(".", "-")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""query_data tool — returns rows from the sample financial database."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from claude_agent_sdk import tool
|
||||
|
||||
# Read the CSV once at import. The notes column can contain unquoted commas, so
|
||||
# keep the first N-1 fields and join the remainder into the last column.
|
||||
_CSV_PATH = Path(__file__).parent / "db.csv"
|
||||
with open(_CSV_PATH, newline="") as _f:
|
||||
_reader = csv.reader(_f)
|
||||
_header = next(_reader)
|
||||
_last = len(_header) - 1
|
||||
_CACHED_DATA = [
|
||||
{
|
||||
**{_header[i]: (row[i] if i < len(row) else "") for i in range(_last)},
|
||||
_header[_last]: ",".join(row[_last:]),
|
||||
}
|
||||
for row in _reader
|
||||
if row
|
||||
]
|
||||
|
||||
|
||||
@tool(
|
||||
"query_data",
|
||||
"Query the financial database with a natural-language query. Always call "
|
||||
"this before rendering a chart so the UI has data to plot.",
|
||||
{"query": str},
|
||||
)
|
||||
async def query_data(args: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"content": [{"type": "text", "text": json.dumps(_CACHED_DATA)}]}
|
||||
+1352
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user