chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"""Barrel exports for all shared showcase tool implementations."""
|
||||
|
||||
from .types import (
|
||||
SalesStage,
|
||||
SalesTodo,
|
||||
Flight,
|
||||
WeatherResult,
|
||||
)
|
||||
from .get_weather import get_weather_impl
|
||||
from .query_data import query_data_impl
|
||||
from .sales_todos import (
|
||||
INITIAL_TODOS,
|
||||
manage_sales_todos_impl,
|
||||
get_sales_todos_impl,
|
||||
)
|
||||
from .search_flights import search_flights_impl
|
||||
from .generate_a2ui import (
|
||||
DESIGN_A2UI_SURFACE_TOOL_SCHEMA,
|
||||
generate_a2ui_impl,
|
||||
build_a2ui_operations_from_tool_call,
|
||||
)
|
||||
from .schedule_meeting import schedule_meeting_impl
|
||||
|
||||
__all__ = [
|
||||
# Types
|
||||
"SalesStage",
|
||||
"SalesTodo",
|
||||
"Flight",
|
||||
"WeatherResult",
|
||||
# Weather
|
||||
"get_weather_impl",
|
||||
# Query data
|
||||
"query_data_impl",
|
||||
# Sales todos
|
||||
"INITIAL_TODOS",
|
||||
"manage_sales_todos_impl",
|
||||
"get_sales_todos_impl",
|
||||
# Flight search (fixed-schema A2UI)
|
||||
"search_flights_impl",
|
||||
# Dynamic A2UI
|
||||
"DESIGN_A2UI_SURFACE_TOOL_SCHEMA",
|
||||
"generate_a2ui_impl",
|
||||
"build_a2ui_operations_from_tool_call",
|
||||
# Schedule meeting (HITL)
|
||||
"schedule_meeting_impl",
|
||||
]
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Dynamic A2UI tool: LLM-generated UI from conversation context.
|
||||
|
||||
This module provides the data preparation for a secondary LLM call that
|
||||
generates v0.9 A2UI components. The actual LLM call is made by the
|
||||
framework-specific wrapper (LangGraph, CrewAI, etc.) since each framework
|
||||
has its own way of invoking LLMs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
|
||||
# The _design_a2ui_surface tool schema that the secondary LLM is bound to.
|
||||
DESIGN_A2UI_SURFACE_TOOL_SCHEMA = {
|
||||
"name": "_design_a2ui_surface",
|
||||
"description": (
|
||||
"Render a dynamic A2UI v0.9 surface.\n\n"
|
||||
"Args:\n"
|
||||
" surfaceId: Unique surface identifier.\n"
|
||||
' catalogId: The catalog ID (use "copilotkit://app-dashboard-catalog").\n'
|
||||
" components: A2UI v0.9 component array (flat format). "
|
||||
'The root component must have id "root".\n'
|
||||
" data: Optional initial data model for the surface."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "Unique surface identifier.",
|
||||
},
|
||||
"catalogId": {"type": "string", "description": "The 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"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def generate_a2ui_impl(
|
||||
messages: list[dict[str, Any]],
|
||||
context_entries: Optional[list[dict[str, Any]]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prepare inputs for a secondary LLM call that generates A2UI components.
|
||||
|
||||
Returns a dict with:
|
||||
- system_prompt: The system prompt for the secondary LLM (built from context)
|
||||
- tool_schema: The _design_a2ui_surface tool schema to bind to the LLM
|
||||
- tool_choice: The tool name to force
|
||||
- messages: The conversation messages to pass through
|
||||
- catalog_id: The default catalog ID
|
||||
|
||||
The framework wrapper should:
|
||||
1. Make an LLM call with these inputs
|
||||
2. Extract the tool call args (surfaceId, catalogId, components, data)
|
||||
3. Build a2ui_operations from the args and return them
|
||||
"""
|
||||
context_text = ""
|
||||
if context_entries:
|
||||
context_text = "\n\n".join(
|
||||
entry.get("value", "")
|
||||
for entry in context_entries
|
||||
if isinstance(entry, dict) and entry.get("value")
|
||||
)
|
||||
|
||||
return {
|
||||
"system_prompt": context_text,
|
||||
"tool_schema": DESIGN_A2UI_SURFACE_TOOL_SCHEMA,
|
||||
"tool_choice": "_design_a2ui_surface",
|
||||
"messages": messages,
|
||||
"catalog_id": CUSTOM_CATALOG_ID,
|
||||
}
|
||||
|
||||
|
||||
def build_a2ui_operations_from_tool_call(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build a2ui_operations dict from the secondary LLM's tool call args.
|
||||
|
||||
Call this after the framework wrapper extracts the tool call arguments.
|
||||
"""
|
||||
surface_id = args.get("surfaceId", "dynamic-surface")
|
||||
catalog_id = args.get("catalogId", CUSTOM_CATALOG_ID)
|
||||
components = args.get("components", [])
|
||||
if not components:
|
||||
_logger.warning(
|
||||
"build_a2ui_operations_from_tool_call received empty components list"
|
||||
)
|
||||
data = args.get("data")
|
||||
|
||||
ops: list[dict[str, Any]] = [
|
||||
{
|
||||
"version": "v0.9",
|
||||
"createSurface": {"surfaceId": surface_id, "catalogId": catalog_id},
|
||||
},
|
||||
{
|
||||
"version": "v0.9",
|
||||
"updateComponents": {"surfaceId": surface_id, "components": components},
|
||||
},
|
||||
]
|
||||
if data:
|
||||
ops.append(
|
||||
{
|
||||
"version": "v0.9",
|
||||
"updateDataModel": {
|
||||
"surfaceId": surface_id,
|
||||
"path": "/",
|
||||
"value": data,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return {"a2ui_operations": ops}
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Mock weather data tool implementation."""
|
||||
|
||||
import random
|
||||
from .types import WeatherResult
|
||||
|
||||
_CONDITIONS = [
|
||||
"Sunny",
|
||||
"Partly Cloudy",
|
||||
"Cloudy",
|
||||
"Overcast",
|
||||
"Light Rain",
|
||||
"Heavy Rain",
|
||||
"Thunderstorm",
|
||||
"Snow",
|
||||
"Foggy",
|
||||
"Windy",
|
||||
]
|
||||
|
||||
|
||||
def get_weather_impl(city: str) -> WeatherResult:
|
||||
"""Return mock weather data for the given city.
|
||||
|
||||
Uses a seeded random based on the city name so repeated calls
|
||||
for the same city return consistent results within a session.
|
||||
"""
|
||||
rng = random.Random(city.lower())
|
||||
temperature = rng.randint(20, 95)
|
||||
humidity = rng.randint(30, 90)
|
||||
wind_speed = rng.randint(2, 30)
|
||||
feels_like = temperature + rng.randint(-5, 5)
|
||||
conditions = rng.choice(_CONDITIONS)
|
||||
|
||||
return WeatherResult(
|
||||
city=city,
|
||||
temperature=temperature,
|
||||
humidity=humidity,
|
||||
wind_speed=wind_speed,
|
||||
feels_like=feels_like,
|
||||
conditions=conditions,
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Query data tool implementation — reads db.csv at module load time."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
_csv_path = Path(__file__).resolve().parent.parent / "data" / "db.csv"
|
||||
|
||||
_MOCK_DATA = [
|
||||
{
|
||||
"date": "2026-01-05",
|
||||
"category": "Revenue",
|
||||
"subcategory": "Enterprise Subscriptions",
|
||||
"amount": "28000",
|
||||
"type": "income",
|
||||
"notes": "3 new enterprise customers",
|
||||
},
|
||||
{
|
||||
"date": "2026-01-10",
|
||||
"category": "Expenses",
|
||||
"subcategory": "Engineering Salaries",
|
||||
"amount": "42000",
|
||||
"type": "expense",
|
||||
"notes": "7 engineers + 2 contractors",
|
||||
},
|
||||
{
|
||||
"date": "2026-02-03",
|
||||
"category": "Revenue",
|
||||
"subcategory": "Pro Tier Upgrades",
|
||||
"amount": "22500",
|
||||
"type": "income",
|
||||
"notes": "31 upgrades + reduced churn",
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
with open(_csv_path) as _f:
|
||||
_cached_data: list[dict[str, Any]] = list(csv.DictReader(_f))
|
||||
if not _cached_data:
|
||||
_logger.warning("CSV at %s is empty, falling back to mock data", _csv_path)
|
||||
_cached_data = _MOCK_DATA
|
||||
except (FileNotFoundError, OSError) as exc:
|
||||
_logger.warning(
|
||||
"Could not load CSV at %s (%s), falling back to mock data", _csv_path, exc
|
||||
)
|
||||
_cached_data = _MOCK_DATA
|
||||
|
||||
|
||||
def query_data_impl(query: str) -> list[dict[str, Any]]:
|
||||
"""Query the database. Takes natural language.
|
||||
|
||||
Always call before showing a chart or graph. Returns the full
|
||||
dataset as a list of dicts (rows from the CSV).
|
||||
"""
|
||||
return _cached_data
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Sales todos tool implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from .types import SalesTodo
|
||||
|
||||
INITIAL_TODOS: list[SalesTodo] = [
|
||||
SalesTodo(
|
||||
id="st-001",
|
||||
title="Follow up with Acme Corp on enterprise proposal",
|
||||
stage="proposal",
|
||||
value=85000,
|
||||
dueDate="2026-04-15",
|
||||
assignee="Sarah Chen",
|
||||
completed=False,
|
||||
),
|
||||
SalesTodo(
|
||||
id="st-002",
|
||||
title="Qualify lead from TechFlow demo request",
|
||||
stage="prospect",
|
||||
value=42000,
|
||||
dueDate="2026-04-18",
|
||||
assignee="Mike Johnson",
|
||||
completed=False,
|
||||
),
|
||||
SalesTodo(
|
||||
id="st-003",
|
||||
title="Send contract to DataViz Inc for final review",
|
||||
stage="negotiation",
|
||||
value=120000,
|
||||
dueDate="2026-04-20",
|
||||
assignee="Sarah Chen",
|
||||
completed=False,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def manage_sales_todos_impl(todos: list[dict]) -> list[SalesTodo]:
|
||||
"""Assign UUIDs to any todos missing an ID, then return the updated list."""
|
||||
result: list[SalesTodo] = []
|
||||
for todo in todos:
|
||||
result.append(
|
||||
SalesTodo(
|
||||
id=todo.get("id") or str(uuid.uuid4()),
|
||||
title=todo.get("title", ""),
|
||||
stage=todo.get("stage", "prospect"),
|
||||
value=todo.get("value", 0),
|
||||
dueDate=todo.get("dueDate", ""),
|
||||
assignee=todo.get("assignee", ""),
|
||||
completed=todo.get("completed", False),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_sales_todos_impl(current_todos: Optional[list[dict]] = None) -> list[SalesTodo]:
|
||||
"""Return current todos or initial defaults if none provided."""
|
||||
if current_todos is not None:
|
||||
return manage_sales_todos_impl(current_todos)
|
||||
return list(INITIAL_TODOS)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Schedule meeting tool implementation.
|
||||
|
||||
The HITL gating happens on the frontend via useHumanInTheLoop.
|
||||
This tool just returns a pending approval status for the framework
|
||||
wrapper to surface.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def schedule_meeting_impl(
|
||||
reason: str,
|
||||
duration_minutes: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
"""Schedule a meeting (requires human approval).
|
||||
|
||||
Returns a pending_approval status. The actual gating is done by the
|
||||
frontend's useHumanInTheLoop hook — the agent pauses until the user
|
||||
approves or rejects.
|
||||
"""
|
||||
return {
|
||||
"status": "pending_approval",
|
||||
"reason": reason,
|
||||
"duration_minutes": duration_minutes,
|
||||
"message": (
|
||||
f"Meeting request: {reason} ({duration_minutes} min). "
|
||||
"Awaiting human approval."
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Fixed-schema A2UI tool: flight search results.
|
||||
|
||||
Packages flight data with an A2UI schema for rendering. The schema is loaded
|
||||
from the shared frontend package's flight_schema.json.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .types import Flight
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
SURFACE_ID = "flight-search-results"
|
||||
|
||||
# Resolve the flight schema from the shared frontend package.
|
||||
# Walk up from this file to showcase/shared/, then into frontend/src/a2ui/.
|
||||
_SHARED_DIR = Path(__file__).resolve().parent.parent.parent # showcase/shared/
|
||||
_SCHEMA_CANDIDATES = [
|
||||
_SHARED_DIR / "frontend" / "src" / "a2ui" / "flight-schema.json",
|
||||
_SHARED_DIR / "frontend" / "src" / "a2ui" / "flight_schema.json",
|
||||
]
|
||||
|
||||
_flight_schema: list[dict[str, Any]] | None = None
|
||||
for _candidate in _SCHEMA_CANDIDATES:
|
||||
if _candidate.exists():
|
||||
with open(_candidate) as _f:
|
||||
_flight_schema = json.load(_f)
|
||||
_logger.info("Loaded flight schema from shared frontend: %s", _candidate)
|
||||
break
|
||||
|
||||
# Fallback: use the schema from the examples directory if present
|
||||
if _flight_schema is None:
|
||||
try:
|
||||
_fallback = Path(__file__).resolve().parents[4] / (
|
||||
"examples/integrations/langgraph-python/apps/agent/src/a2ui/schemas/flight_schema.json"
|
||||
)
|
||||
if _fallback.exists():
|
||||
with open(_fallback) as _f:
|
||||
_flight_schema = json.load(_f)
|
||||
_logger.info("Loaded flight schema from examples fallback: %s", _fallback)
|
||||
except IndexError:
|
||||
# In Docker the file path is too shallow for parents[4]; skip this fallback.
|
||||
pass
|
||||
|
||||
# Last resort: inline minimal schema
|
||||
if _flight_schema is None:
|
||||
_logger.warning("No flight schema file found, using inline minimal schema")
|
||||
_flight_schema = [
|
||||
{
|
||||
"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"},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def search_flights_impl(flights: list[Flight]) -> dict[str, Any]:
|
||||
"""Package flight data with A2UI schema for rendering.
|
||||
|
||||
Returns a dict with a2ui_operations that the middleware detects in the
|
||||
TOOL_CALL_RESULT and renders automatically.
|
||||
|
||||
Each flight should have: airline, airlineLogo, flightNumber, origin,
|
||||
destination, date, departureTime, arrivalTime, duration, status,
|
||||
statusColor, price, currency.
|
||||
"""
|
||||
return {
|
||||
"a2ui_operations": [
|
||||
{
|
||||
"version": "v0.9",
|
||||
"createSurface": {"surfaceId": SURFACE_ID, "catalogId": CATALOG_ID},
|
||||
},
|
||||
{
|
||||
"version": "v0.9",
|
||||
"updateComponents": {
|
||||
"surfaceId": SURFACE_ID,
|
||||
"components": _flight_schema,
|
||||
},
|
||||
},
|
||||
{
|
||||
"version": "v0.9",
|
||||
"updateDataModel": {
|
||||
"surfaceId": SURFACE_ID,
|
||||
"path": "/",
|
||||
"value": {"flights": flights},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Shared type definitions for showcase tools."""
|
||||
|
||||
from typing import TypedDict, Literal
|
||||
|
||||
SalesStage = Literal[
|
||||
"prospect",
|
||||
"qualified",
|
||||
"proposal",
|
||||
"negotiation",
|
||||
"closed-won",
|
||||
"closed-lost",
|
||||
]
|
||||
|
||||
|
||||
class SalesTodo(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
stage: SalesStage
|
||||
value: int
|
||||
dueDate: str
|
||||
assignee: str
|
||||
completed: bool
|
||||
|
||||
|
||||
class Flight(TypedDict):
|
||||
airline: str
|
||||
airlineLogo: str
|
||||
flightNumber: str
|
||||
origin: str
|
||||
destination: str
|
||||
date: str
|
||||
departureTime: str
|
||||
arrivalTime: str
|
||||
duration: str
|
||||
status: str
|
||||
statusColor: str
|
||||
price: str
|
||||
currency: str
|
||||
|
||||
|
||||
class WeatherResult(TypedDict):
|
||||
city: str
|
||||
temperature: int
|
||||
humidity: int
|
||||
wind_speed: int
|
||||
feels_like: int
|
||||
conditions: str
|
||||
Reference in New Issue
Block a user