chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,24 @@
"""Shared validators for A2UI dynamic-schema agents.
The dynamic-schema flow has a secondary LLM produce a flat array of
components. The renderer rejects entries missing `id` or `component`
("Cannot create component root without a type" infinite-loop), so every
agent that builds an A2UI surface dynamically needs to sanitize the
LLM's output before forwarding it. These helpers are factored out so
each agent's tool body stays focused on the demo-specific bits
(catalog id, system prompt, data shape).
"""
from __future__ import annotations
def sanitize_a2ui_components(raw: list) -> list[dict]:
"""Drop entries that aren't dicts or are missing `id`/`component`."""
return [
c for c in raw if isinstance(c, dict) and c.get("id") and c.get("component")
]
def has_root_component(components: list[dict]) -> bool:
"""True iff `components` contains an entry with `id == "root"`."""
return any(c.get("id") == "root" for c in components)
@@ -0,0 +1,430 @@
"""_cvdiag_backend.py — schema-v1 backend CVDIAG emitter for langgraph-python.
This is the LGP (langgraph-python) realization of the §3 backend layer: it wires
the 11 backend boundaries through the shared ``_shared.cvdiag_bootstrap.emit_cvdiag``
single-source emitter. It runs ALONGSIDE the legacy free-form ``_cvdiag()`` log
lines in ``_header_forwarding_middleware.py`` (dual-emit during the transition):
- legacy ``_cvdiag()`` keeps writing the human-grep ``CVDIAG component=...`` line,
- this module writes the structured schema-v1 ``CVDIAG {json}`` envelope.
Guard: every emit here is gated on ``CVDIAG_BACKEND_EMITTER=1`` (default OFF). With
the guard off this module is a pure no-op — it never validates, never writes, never
throws into the observed boundary.
The 11 backend boundaries (spec §3 / §5):
backend.request.ingress, backend.agent.enter, backend.llm.call.start,
backend.llm.call.heartbeat (VERBOSE tier — periodic ~10s asyncio task),
backend.llm.call.response, backend.sse.first_byte, backend.sse.event (DEBUG tier),
backend.sse.aborted, backend.agent.exit, backend.response.complete,
backend.error.caught.
Pure instrumentation: like the shared emitter, nothing here raises into the caller;
the one place we ``await`` (the heartbeat task) is cancelled cleanly in a finally.
Plan unit: L1-I.
"""
from __future__ import annotations
import asyncio
import os
import time
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from _shared.cvdiag_bootstrap import _resolve_tier, current_tier, emit_cvdiag
# ── Tier gating ───────────────────────────────────────────────────────────────
# The shared bootstrap already resolved the tier (default | verbose | debug) and
# applied the §6 fail-closed DEBUG guard. We mirror the §6 tier matrix locally so
# VERBOSE-only (heartbeat) and DEBUG-only (sse.event) boundaries are suppressed at
# the wrong tier rather than relying on the consumer to filter.
_VERBOSE_TIERS = frozenset({"verbose", "debug"})
_DEBUG_TIERS = frozenset({"debug"})
_SLUG = "langgraph-python"
_HEARTBEAT_INTERVAL_S = 10.0
def emitter_enabled() -> bool:
"""True when the schema-v1 backend emitter is armed (``CVDIAG_BACKEND_EMITTER=1``).
Default OFF: a missing/any-other value disables every emit in this module.
"""
return os.environ.get("CVDIAG_BACKEND_EMITTER") == "1"
def _active_tier() -> str:
"""Resolve the verbosity tier from a LIVE env read.
``emitter_enabled()`` reads ``CVDIAG_BACKEND_EMITTER`` live, so the tier MUST
be read from the same live source — otherwise flipping ``CVDIAG_VERBOSE`` /
``CVDIAG_DEBUG`` AFTER import arms the emitter but the tier stays frozen at
the import-time ``setup()`` value, silently no-op'ing every verbose/debug-
gated boundary (heartbeat, sse.event). We reuse the bootstrap's
``_resolve_tier`` so the §6 fail-closed DEBUG guard still applies (a
production / unresolved DEBUG request raises → degrade to the frozen tier).
"""
try:
return _resolve_tier(dict(os.environ))
except RuntimeError:
# Fail-closed DEBUG refusal: fall back to the import-time resolved tier
# (never silently escalate to debug in production).
return current_tier()
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _mono_ns() -> int:
return time.monotonic_ns()
def _span_id() -> str:
"""16 hex chars (8 random bytes) — matches SPAN_ID_PATTERN."""
return uuid.uuid4().hex[:16]
def _coerce_test_id(raw: Optional[str]) -> str:
"""Return a schema-valid UUIDv7 ``test_id``.
When the inbound header carries a valid UUIDv7 we keep it (this is the
propagation we are measuring). When it is absent/malformed we synthesize a
deterministic-shape UUIDv7 so the envelope still validates — but the
propagation gate measures the *inbound* presence, not this fallback.
"""
from _shared.cvdiag_schema import (
TEST_ID_PATTERN,
) # local import: cheap, avoids cycle
import re
if isinstance(raw, str) and re.match(TEST_ID_PATTERN, raw):
return raw
# Synthesize a UUIDv7-shaped value (version nibble 7, variant 8..b).
hexs = uuid.uuid4().hex
return f"{hexs[0:8]}-{hexs[8:12]}-7{hexs[13:16]}-8{hexs[17:20]}-{hexs[20:32]}"
def extract_test_id(headers: Dict[str, str]) -> Optional[str]:
"""Return the inbound ``x-test-id`` header value, or ``None`` when absent.
This is the raw inbound value used by the propagation-reliability gate — it
is NOT coerced/synthesized here so the gate can measure true propagation.
"""
raw = headers.get("x-test-id")
return raw if isinstance(raw, str) and raw else None
def _empty_edge_headers() -> Dict[str, Any]:
return {
"cf-ray": None,
"cf-mitigated": None,
"cf-cache-status": None,
"x-railway-edge": None,
"x-railway-request-id": None,
"x-hikari-trace": None,
"retry-after": None,
"via": None,
"server": None,
}
def _edge_headers_from(headers: Dict[str, str]) -> Dict[str, Any]:
"""Project the inbound header bag onto the closed 9-key edge-header shape.
Only the 9 allow-listed keys are carried; everything else is dropped (the
envelope's per-boundary model + EdgeHeaders ``extra=forbid`` enforce this).
"""
allow = _empty_edge_headers()
for key in list(allow.keys()):
val = headers.get(key)
if isinstance(val, str) and val:
allow[key] = val
return allow
def _emit(
boundary: str,
*,
headers: Dict[str, str],
trace_id: str,
outcome: str = "ok",
metadata: Optional[Dict[str, Any]] = None,
duration_ms: Optional[int] = None,
demo: str = "chat",
tier_gate: Optional[frozenset] = None,
) -> None:
"""Build + emit one schema-v1 envelope, guarded + tier-filtered.
No-op when the emitter is disabled OR the boundary's tier gate excludes the
resolved tier. Never raises (delegates to the shared emitter's safety).
"""
if not emitter_enabled():
return
if tier_gate is not None and _active_tier() not in tier_gate:
return
envelope = {
"schema_version": 1,
"test_id": _coerce_test_id(headers.get("x-test-id")),
"trace_id": trace_id,
"span_id": _span_id(),
"parent_span_id": None,
"layer": "backend",
"boundary": boundary,
"slug": _SLUG,
"demo": demo,
"ts": _now_iso(),
"mono_ns": _mono_ns(),
"duration_ms": duration_ms,
"outcome": outcome,
"edge_headers": _edge_headers_from(headers),
"metadata": metadata or {},
}
emit_cvdiag(envelope)
class CvdiagBackendRun:
"""Per-model-call CVDIAG run context for the LGP middleware.
Constructed inside ``awrap_model_call`` (and the sync ``wrap_model_call``);
owns the trace correlation id, the ingress monotonic anchor, and the
heartbeat asyncio task. All methods are no-ops when the emitter is disabled.
"""
def __init__(self, headers: Dict[str, str]) -> None:
self._headers = dict(headers)
# Correlate every boundary in this run under one trace_id. Prefer the
# inbound x-diag-run-id breadcrumb so probe/backend rows join; fall back
# to a synthesized id.
self._trace_id = (
headers.get("x-diag-run-id") or headers.get("x-test-id") or uuid.uuid4().hex
)
self._ingress_mono = _mono_ns()
self._first_byte_emitted = False
self._sse_seq = 0
self._heartbeat_task: Optional[asyncio.Task] = None
# ── Lifecycle boundaries ──────────────────────────────────────────────
def request_ingress(self) -> None:
_emit(
"backend.request.ingress",
headers=self._headers,
trace_id=self._trace_id,
metadata={"method": "POST", "path": "/threads", "content_length": None},
tier_gate=_VERBOSE_TIERS,
)
def agent_enter(
self, agent_name: Optional[str] = None, model_id: Optional[str] = None
) -> None:
_emit(
"backend.agent.enter",
headers=self._headers,
trace_id=self._trace_id,
metadata={"agent_name": agent_name, "model_id": model_id},
)
def llm_call_start(
self, provider: Optional[str] = None, model: Optional[str] = None
) -> None:
_emit(
"backend.llm.call.start",
headers=self._headers,
trace_id=self._trace_id,
metadata={
"provider": provider,
"model": model,
"prompt_token_count_estimate": None,
},
tier_gate=_VERBOSE_TIERS,
)
def llm_call_response(
self,
provider: Optional[str] = None,
model: Optional[str] = None,
latency_ms: Optional[int] = None,
error_class: Optional[str] = None,
) -> None:
_emit(
"backend.llm.call.response",
headers=self._headers,
trace_id=self._trace_id,
outcome="err" if error_class else "ok",
metadata={
"provider": provider,
"model": model,
"response_token_count": None,
"latency_ms": latency_ms,
"error_class": error_class,
},
tier_gate=_VERBOSE_TIERS,
)
def sse_first_byte(self) -> None:
"""Emit ``backend.sse.first_byte`` once, with the ingress→first-byte delta."""
if self._first_byte_emitted:
return
self._first_byte_emitted = True
delta_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
_emit(
"backend.sse.first_byte",
headers=self._headers,
trace_id=self._trace_id,
metadata={"delta_ms_from_ingress": delta_ms},
tier_gate=_VERBOSE_TIERS,
)
def sse_event(
self, event_type: Optional[str] = None, payload_size_bytes: Optional[int] = None
) -> None:
"""Emit ``backend.sse.event`` (DEBUG tier — suppressed below debug)."""
self._sse_seq += 1
_emit(
"backend.sse.event",
headers=self._headers,
trace_id=self._trace_id,
metadata={
"event_type": event_type,
"payload_size_bytes": payload_size_bytes,
"sequence_num": self._sse_seq,
},
tier_gate=_DEBUG_TIERS,
)
def sse_aborted(
self,
termination_kind: Optional[str] = None,
bytes_before_abort: Optional[int] = None,
) -> None:
_emit(
"backend.sse.aborted",
headers=self._headers,
trace_id=self._trace_id,
outcome="err",
metadata={
"termination_kind": termination_kind,
"bytes_before_abort": bytes_before_abort,
},
)
def agent_exit(self, terminal_outcome: str = "ok") -> None:
total_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
_emit(
"backend.agent.exit",
headers=self._headers,
trace_id=self._trace_id,
outcome="err" if terminal_outcome == "err" else "ok",
metadata={
"terminal_outcome": terminal_outcome,
"total_duration_ms": total_ms,
},
)
def response_complete(
self,
http_status: Optional[int] = 200,
sse_event_count: Optional[int] = None,
) -> None:
total_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
_emit(
"backend.response.complete",
headers=self._headers,
trace_id=self._trace_id,
metadata={
"http_status": http_status,
"content_length": None,
"total_duration_ms": total_ms,
"sse_event_count": sse_event_count
if sse_event_count is not None
else self._sse_seq,
},
)
def error_caught(self, exc: BaseException) -> None:
_emit(
"backend.error.caught",
headers=self._headers,
trace_id=self._trace_id,
outcome="err",
metadata={
"exception_type": type(exc).__name__,
"message_scrubbed": "<scrubbed>",
"stack_brief": None,
"truncated": False,
},
)
# ── Heartbeat (VERBOSE tier — periodic asyncio task) ──────────────────
async def _heartbeat_loop(self) -> None:
"""Emit ``backend.llm.call.heartbeat`` every ~10s while the LLM call runs."""
try:
while True:
await asyncio.sleep(_HEARTBEAT_INTERVAL_S)
elapsed_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
_emit(
"backend.llm.call.heartbeat",
headers=self._headers,
trace_id=self._trace_id,
outcome="info",
metadata={"elapsed_ms_since_start": elapsed_ms},
tier_gate=_VERBOSE_TIERS,
)
except asyncio.CancelledError:
# Clean cancellation when the LLM call finishes — swallow.
return
def start_heartbeat(self) -> None:
"""Arm the heartbeat task (no-op when disabled or below VERBOSE tier)."""
if not emitter_enabled() or _active_tier() not in _VERBOSE_TIERS:
return
if self._heartbeat_task is not None:
return
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return
self._heartbeat_task = loop.create_task(self._heartbeat_loop())
async def stop_heartbeat(self) -> None:
"""Cancel + await the heartbeat task. Safe to call when never started.
Cooperative cancellation: the legacy ``except (CancelledError,
Exception)`` swallowed the CALLER's CancelledError, breaking cooperative
cancellation (a client-disconnect / request-cancel that arrives while we
await the heartbeat task would be lost). We suppress ONLY the heartbeat
task's OWN cancellation — the one we just requested — and re-raise when
THIS task is being cancelled by the caller (a pending cancellation
request, ``current_task().cancelling() > 0``). ``Task.cancelling()`` is
3.11+ (production runs 3.12); on older runtimes the attribute is absent
and we degrade to suppressing (the legacy behavior).
"""
task = self._heartbeat_task
if task is None:
return
self._heartbeat_task = None
task.cancel()
try:
await task
except asyncio.CancelledError:
current = asyncio.current_task()
cancelling = getattr(current, "cancelling", None)
if current is not None and cancelling is not None and cancelling() > 0:
raise
except Exception: # noqa: BLE001 - heartbeat body must never throw out
return
def emit_heartbeat_once(self) -> None:
"""Synchronous single heartbeat emit (used by the sync wrap path + tests)."""
elapsed_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
_emit(
"backend.llm.call.heartbeat",
headers=self._headers,
trace_id=self._trace_id,
outcome="info",
metadata={"elapsed_ms_since_start": elapsed_ms},
tier_gate=_VERBOSE_TIERS,
)
@@ -0,0 +1,272 @@
"""Minimal header-forwarding-only AgentMiddleware.
Some showcase demos (reasoning, tool-rendering-reasoning-chain, the
sub-agents in `subagents.py`) intentionally avoid the full
`CopilotKitMiddleware` because they don't need its frontend-tool
injection, App-Context surfacing, or state-note features — they're
minimal demos of LangGraph capabilities.
But every showcase request goes through aimock (the locally-served
LLM mock), and aimock requires the ``x-aimock-context`` header (and
friends) on every ``/v1/responses`` and ``/v1/chat/completions``
request to match the right fixture. Without middleware to populate
the header-forwarding ContextVar from the LangGraph RunnableConfig
``configurable``, those requests go out without the header and aimock
returns 404, breaking the demo.
This middleware does ONLY that header propagation — nothing else.
It reuses copilotkit's own primitives (kept private but exported by
the installed package at the module level) so the propagation logic
is identical to the full middleware. No App-Context injection, no
tool-merging, no state-to-prompt surfacing, no Bedrock message
fix-up.
CVDIAG instrumentation (diagnostic only — DOES NOT change WHERE
headers come from): after the existing
``_extract_forwarded_headers_from_config()`` populates copilotkit's
forwarded-headers ContextVar, we read it back via
``get_forwarded_headers()`` and emit a structured ``CVDIAG`` log line
at the configurable-read boundary recording whether
``x-aimock-context`` actually arrived on the LangGraph configurable
channel (``header_present=false`` is the alarm we are hunting). We
also append this layer's hop tag to ``x-diag-hops`` on the SAME
ContextVar the httpx hook already forwards from — so the breadcrumb
and correlation headers (``x-diag-run-id``, ``x-diag-hops``) ride
along on the outbound LLM call exactly the way ``x-aimock-context``
does, without introducing any new forwarding source.
"""
from __future__ import annotations
import logging
import time
from typing import Any, Awaitable, Callable, Dict
from langchain.agents.middleware import (
AgentMiddleware,
AgentState,
ModelRequest,
ModelResponse,
)
# Reuse the installed copilotkit's existing header-forwarding helpers so
# the behaviour stays bit-identical to the full CopilotKitMiddleware's
# header-propagation step. These are module-level functions in
# copilotkit 0.1.94's copilotkit_lg_middleware module.
from copilotkit.copilotkit_lg_middleware import (
_extract_forwarded_headers_from_config,
_ensure_httpx_hook,
)
# CVDIAG-only: read/append the forwarded-header ContextVar copilotkit
# already populates. set_forwarded_headers is used SOLELY to append the
# diagnostic hop breadcrumb onto the SAME channel x-aimock-context rides;
# it does not introduce a new forwarding source.
from copilotkit.header_propagation import (
get_forwarded_headers,
set_forwarded_headers,
)
# CVDIAG schema-v1 backend emitter (L1-I). Dual-emit: this rides ALONGSIDE the
# legacy free-form _cvdiag() log lines below — it writes the structured
# schema-v1 CVDIAG envelope through the shared single-source emitter, guarded by
# CVDIAG_BACKEND_EMITTER (default OFF). With the guard off it is a pure no-op.
from src.agents._cvdiag_backend import CvdiagBackendRun
logger = logging.getLogger(__name__)
_CVDIAG_COMPONENT = "backend-langgraph-py"
_CVDIAG_HOP_TAG = "backend-langgraph-py"
def _cvdiag(
boundary: str,
headers: Dict[str, str],
status: str,
*,
hop: Any = "-",
error: str = "",
) -> None:
"""Emit a single CVDIAG log line in the shared cross-language convention.
Never logs full header values — only a 12-char prefix of
``x-aimock-context``.
"""
slug = headers.get("x-aimock-context")
header_present = isinstance(slug, str) and len(slug) > 0
run_id = headers.get("x-diag-run-id", "none")
test_id = headers.get("x-test-id", "none")
prefix = slug[:12] if header_present else ""
logger.info(
"CVDIAG component=%s boundary=%s run_id=%s slug=%s "
"header_present=%s header_value_prefix=%s hop=%s status=%s "
"test_id=%s error=%s",
_CVDIAG_COMPONENT,
boundary,
run_id,
slug if header_present else "MISSING",
str(header_present).lower(),
prefix,
hop,
status,
test_id,
error,
)
def _instrument_and_breadcrumb() -> None:
"""Read the configurable-read result, log it, and append the diag hop.
Called immediately AFTER
``_extract_forwarded_headers_from_config()`` has populated the
ContextVar. Reads the headers back, emits the configurable-read
CVDIAG line (wrapping the previously-silent "no x-aimock-context in
configurable" case as an alarm), then — only when x-aimock-context
is present — appends this layer's hop tag to ``x-diag-hops`` on the
SAME ContextVar so the breadcrumb rides the existing forwarding path.
"""
headers = dict(get_forwarded_headers())
has_context = (
isinstance(headers.get("x-aimock-context"), str)
and len(headers.get("x-aimock-context", "")) > 0
)
if has_context:
_cvdiag("configurable-read", headers, "ok")
else:
# The alarm we are hunting: the configurable channel reached this
# middleware without x-aimock-context. Surface it instead of the
# previous silent no-op.
_cvdiag(
"configurable-read",
headers,
"miss" if headers else "error",
error="x-aimock-context-absent-in-configurable"
if headers
else "no-forwarded-headers-in-configurable",
)
# Nothing to breadcrumb onto — do not invent a forwarding source.
return
# Append this layer's hop tag to x-diag-hops on the SAME ContextVar the
# httpx hook forwards from. This rides the existing path; no new source.
existing_hops = headers.get("x-diag-hops", "")
headers["x-diag-hops"] = (
f"{existing_hops},{_CVDIAG_HOP_TAG}"
if isinstance(existing_hops, str) and existing_hops
else _CVDIAG_HOP_TAG
)
set_forwarded_headers(headers)
hop = len([h for h in headers["x-diag-hops"].split(",") if h])
_cvdiag("outbound-llm", headers, "ok", hop=hop)
class HeaderForwardingMiddleware(AgentMiddleware[AgentState, Any]):
"""AgentMiddleware that only forwards inbound x-* headers.
Behaviourally a no-op except for two calls inside both
``wrap_model_call`` and ``awrap_model_call``:
1. ``_extract_forwarded_headers_from_config()`` — read the
``x-*`` keys from the active LangGraph RunnableConfig
(``context`` and ``configurable``) and populate the
header-forwarding ContextVar.
2. ``_ensure_httpx_hook(request.model)`` — install copilotkit's
httpx event hook on the model's underlying HTTP client(s)
so the next outgoing LLM request picks the headers up.
No App-Context injection, no tool-merging, no state-surfacing,
no Bedrock message fix-up — strictly header propagation.
CVDIAG: ``_instrument_and_breadcrumb()`` is inserted between the
two steps purely to OBSERVE the configurable-read boundary and tag
the existing breadcrumb. It does not change where headers come from.
"""
@property
def name(self) -> str:
return "HeaderForwardingMiddleware"
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
_extract_forwarded_headers_from_config()
_instrument_and_breadcrumb()
_ensure_httpx_hook(request.model)
# CVDIAG schema-v1 dual-emit (L1-I). No-op when CVDIAG_BACKEND_EMITTER off.
headers = dict(get_forwarded_headers())
run = CvdiagBackendRun(headers)
model_name = _model_name(request)
run.request_ingress()
run.agent_enter(agent_name=self.name, model_id=model_name)
run.llm_call_start(provider="langchain", model=model_name)
run.emit_heartbeat_once()
start_ns = time.monotonic_ns()
try:
response = handler(request)
except BaseException as exc: # noqa: BLE001 - re-raised after observing
run.error_caught(exc)
run.agent_exit(terminal_outcome="err")
raise
latency_ms = int((time.monotonic_ns() - start_ns) / 1_000_000)
run.llm_call_response(
provider="langchain", model=model_name, latency_ms=latency_ms
)
run.sse_first_byte()
run.sse_event(event_type="response", payload_size_bytes=None)
run.agent_exit(terminal_outcome="ok")
run.response_complete(http_status=200)
return response
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelResponse:
_extract_forwarded_headers_from_config()
_instrument_and_breadcrumb()
_ensure_httpx_hook(request.model)
# CVDIAG schema-v1 dual-emit (L1-I). No-op when CVDIAG_BACKEND_EMITTER off.
headers = dict(get_forwarded_headers())
run = CvdiagBackendRun(headers)
model_name = _model_name(request)
run.request_ingress()
run.agent_enter(agent_name=self.name, model_id=model_name)
run.llm_call_start(provider="langchain", model=model_name)
run.start_heartbeat()
start_ns = time.monotonic_ns()
try:
response = await handler(request)
except BaseException as exc: # noqa: BLE001 - re-raised after observing
await run.stop_heartbeat()
run.error_caught(exc)
run.agent_exit(terminal_outcome="err")
raise
await run.stop_heartbeat()
latency_ms = int((time.monotonic_ns() - start_ns) / 1_000_000)
run.llm_call_response(
provider="langchain", model=model_name, latency_ms=latency_ms
)
run.sse_first_byte()
run.sse_event(event_type="response", payload_size_bytes=None)
run.agent_exit(terminal_outcome="ok")
run.response_complete(http_status=200)
return response
def _model_name(request: ModelRequest) -> str:
"""Best-effort model identifier off the ModelRequest (never raises)."""
try:
model = getattr(request, "model", None)
for attr in ("model_name", "model", "model_id"):
val = getattr(model, attr, None)
if isinstance(val, str) and val:
return val
except Exception: # noqa: BLE001 - instrumentation must not throw
pass
return "unknown"
@@ -0,0 +1,80 @@
"""LangGraph agent for the Declarative Generative UI (A2UI — Dynamic Schema) demo."""
from __future__ import annotations
import os
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
# Cross-reference: showcase/integrations/google-adk/src/agents/declarative_gen_ui_agent.py
# Both integrations register the same a2ui catalog (Card / Row / Column /
# Text / Metric / PieChart / BarChart / DataTable / StatusBadge / InfoRow /
# PrimaryButton — see each integration's
# src/app/demos/declarative-gen-ui/a2ui/definitions.ts, which are
# byte-identical across LP and ADK).
#
# The fictional sales dataset and the per-question composition rules
# are injected via App Context from
# showcase/integrations/langgraph-python/src/app/demos/declarative-gen-ui/sales-context.ts
# (a frontend file shared byte-for-byte with the ADK integration — see
# its DUPLICATION NOTICE).
#
# Keep this SYSTEM_PROMPT and the ADK `_INSTRUCTION` aligned in spirit.
# Minor wording differences are tolerated (e.g. this prompt uses shape
# words — "table"/"pie"/"bar" — as question-category descriptors, while
# ADK names the rendered components — "DataTable"/"PieChart"/"BarChart"
# — in the analogous slot), but the structural rules and the component
# name set must match the catalog above.
SYSTEM_PROMPT = (
"You are the embedded sales analyst for Vantage Threads, the fictional "
"B2B apparel company described in your App Context. Answer every "
"business question by calling `generate_a2ui` to draw a rich visual "
"surface, and keep the chat reply to one short sentence.\n"
"\n"
"Ground every number in the sales dataset from App Context — never "
"invent figures that contradict it. Follow the dashboard composition "
"rules from App Context when choosing components: pick the component "
"by the shape of the question (snapshot → composed KPI dashboard with "
"charts; team performance → table; risk → status badges; single "
"account → info rows; part-of-whole → pie; trend/comparison → bar). "
"Never ask the user which chart they want. `generate_a2ui` takes no "
"arguments and handles the rendering automatically. Compose "
"generously — a dashboard should feel like a real analytics product, "
"not a single widget."
)
@tool
def generate_a2ui() -> dict:
"""Generate a dynamic A2UI dashboard surface from the current conversation.
Takes no arguments. The CopilotKit runtime middleware
(`a2ui.injectA2UITool: true`) intercepts the call and drives a
secondary-LLM `render_a2ui` planner to emit the surface ops; this
Python body should NEVER execute in normal operation. It exists only
so the LP agent's declared `tools=` list mirrors the ADK sibling
(`declarative_gen_ui_agent.py`) and the SYSTEM_PROMPT's
`generate_a2ui` reference resolves to a registered tool name.
If this body actually runs, the CopilotKit a2ui middleware is
misconfigured and silently returning an empty surface would hide the
real bug — fail loud per `fail-loud-discipline`.
"""
raise RuntimeError(
"generate_a2ui called directly — CopilotKit a2ui.injectA2UITool "
"middleware should intercept this call before it reaches the "
"agent. Check the route configuration at "
"app/api/copilotkit-declarative-gen-ui/route.ts."
)
graph = create_agent(
model=ChatOpenAI(model=os.getenv("OPENAI_MODEL", "gpt-4o")),
tools=[generate_a2ui],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,101 @@
"""
LangGraph agent for the Declarative Generative UI (A2UI — Fixed Schema) demo.
Fixed-schema A2UI: the component tree (schema) is authored ahead of time as
JSON and loaded at startup via `a2ui.load_schema(...)`. The agent only
streams *data* into the data model at runtime. The frontend registers a
matching catalog (see `src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts`)
that pins the schema's component names to real React implementations.
Reference:
examples/integrations/langgraph-python/agent/src/a2ui_fixed_schema.py
"""
# @region[backend-render-operations]
# @region[backend-schema-json-load]
from __future__ import annotations
from pathlib import Path
from typing import TypedDict
from copilotkit import CopilotKitMiddleware, a2ui
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
CATALOG_ID = "copilotkit://flight-fixed-catalog"
SURFACE_ID = "flight-fixed-schema"
_SCHEMAS_DIR = Path(__file__).parent / "a2ui_schemas"
# The schema is JSON so it can be authored and reviewed independently of the
# Python code. `a2ui.load_schema` is just a thin `json.load` wrapper.
FLIGHT_SCHEMA = a2ui.load_schema(_SCHEMAS_DIR / "flight_schema.json")
# @endregion[backend-schema-json-load]
class Flight(TypedDict):
"""Shape the LLM should fill in when calling `display_flight`.
LangGraph serializes this TypedDict into the tool's JSON schema, so
defining it narrowly is how we steer the LLM to produce data that fits
the frontend `FlightCard` component's props.
"""
origin: str
destination: str
airline: str
price: str
@tool
def display_flight(origin: str, destination: str, airline: str, price: str) -> str:
"""Show a flight card for the given trip.
Use short airport codes (e.g. "SFO", "JFK") for origin/destination and a
price string like "$289".
After this tool returns, the flight card is already rendered to the user
via the A2UI surface — the JSON returned here is the surface descriptor
the renderer consumes, NOT a status code. Do NOT call this tool again
for the same flight (the user already sees the card). Reply with one
short confirmation sentence and stop.
"""
# The A2UI middleware detects the `a2ui_operations` container in this
# tool result and forwards the ops to the frontend renderer. The frontend
# catalog resolves component names to the local React components.
#
# Note: schema-swap-on-action (e.g. swapping to a "booked" schema when
# the card's button is clicked) will be added once the Python SDK
# exposes `action_handlers=` on `a2ui.render`.
return a2ui.render(
operations=[
a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID),
a2ui.update_components(SURFACE_ID, FLIGHT_SCHEMA),
a2ui.update_data_model(
SURFACE_ID,
{
"origin": origin,
"destination": destination,
"airline": airline,
"price": price,
},
),
],
)
# @endregion[backend-render-operations]
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[display_flight],
middleware=[CopilotKitMiddleware()],
system_prompt=(
"You help users find flights. When asked about a flight, call "
"`display_flight` exactly ONCE with origin, destination, airline, "
"and price. The tool's JSON return value is an A2UI surface "
"descriptor — the flight card is already rendered to the user; do "
"NOT call `display_flight` again for the same trip. After the tool "
"returns, reply with one short confirmation sentence and stop."
),
)
@@ -0,0 +1,77 @@
[
{
"id": "root",
"component": "Card",
"child": "content"
},
{
"id": "content",
"component": "Column",
"children": ["title", "route", "meta", "bookButton"]
},
{
"id": "title",
"component": "Title",
"text": "Flight Details"
},
{
"id": "route",
"component": "Row",
"justify": "spaceBetween",
"align": "center",
"children": ["from", "arrow", "to"]
},
{
"id": "from",
"component": "Airport",
"code": { "path": "/origin" }
},
{
"id": "arrow",
"component": "Arrow"
},
{
"id": "to",
"component": "Airport",
"code": { "path": "/destination" }
},
{
"id": "meta",
"component": "Row",
"justify": "spaceBetween",
"align": "center",
"children": ["airline", "price"]
},
{
"id": "airline",
"component": "AirlineBadge",
"name": { "path": "/airline" }
},
{
"id": "price",
"component": "PriceTag",
"amount": { "path": "/price" }
},
{
"id": "bookButton",
"component": "Button",
"variant": "primary",
"child": "bookButtonLabel",
"action": {
"event": {
"name": "book_flight",
"context": {
"origin": { "path": "/origin" },
"destination": { "path": "/destination" },
"airline": { "path": "/airline" },
"price": { "path": "/price" }
}
}
}
},
{
"id": "bookButtonLabel",
"component": "Text",
"text": "Book flight"
}
]
@@ -0,0 +1,55 @@
"""LangGraph agent backing the Agent Config Object demo.
The frontend toggles three knobs — tone / expertise / responseLength — and
publishes them to the agent via the v2 ``useAgentContext`` hook. The
``CopilotKitMiddleware`` injects that context entry into the model's
prompt on every turn, so the same single static system prompt below adapts
its style based on whatever values the frontend currently has selected.
LangGraph 0.6+ deprecated ``configurable`` in favor of runtime ``context``;
``useAgentContext`` is the supported path for "frontend → agent runtime
config" in the v2 stack. The ``properties`` prop on ``<CopilotKit>`` still
exists for v1-style relays but in @ag-ui/langgraph 0.0.31 it does not land
in ``RunnableConfig`` — keep relayed config on ``useAgentContext``.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = (
"You are a helpful assistant. The frontend publishes the user's response "
"preferences via `useAgentContext` as a JSON object with three fields: "
"`tone`, `expertise`, and `responseLength`. Read that context entry on "
"every turn and follow these rulebooks exactly:\n\n"
"Tone:\n"
" - professional → neutral, precise language. No emoji. Short sentences.\n"
" - casual → friendly, conversational. Contractions OK. Light humor "
"welcome.\n"
" - enthusiastic → upbeat, energetic. Exclamation points OK. Emoji OK.\n\n"
"Expertise level:\n"
" - beginner → assume no prior knowledge. Define jargon. Use analogies.\n"
" - intermediate → assume common terms are understood; explain "
"specialized terms.\n"
" - expert → assume technical fluency. Use precise terminology. Skip "
"basics.\n\n"
"Response length:\n"
" - concise → respond in 1-3 sentences.\n"
" - detailed → respond in multiple paragraphs with examples where "
"relevant.\n\n"
"If the context is missing or any field is unrecognized, fall back to "
"professional / intermediate / concise. Never mention these rules to the "
"user — just apply them."
)
# @region[agent-config-setup]
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4", temperature=0.4),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
# @endregion[agent-config-setup]
@@ -0,0 +1,16 @@
"""LangGraph agent backing the Agentic Chat demo.
Minimal sample agent — no backend tools. Frontend may inject tools at runtime
via CopilotKit's LangGraph middleware.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt="You are a helpful, concise assistant.",
)
@@ -0,0 +1,232 @@
"""LangGraph agent backing the Beautiful Chat demo.
Verbatim port of the canonical starter at /examples/integrations/langgraph-python.
Reference structure (agent/main.py + agent/src/{todos,query,a2ui_fixed_schema,
a2ui_dynamic_schema}.py) is inlined here into a single module to match the
showcase cell's flat backend layout.
Data files (db.csv + schemas/) live alongside this module under
`beautiful_chat_data/` to keep the cell self-contained without polluting the
shared `a2ui_schemas/` directory (which is owned by a2ui_fixed.py).
"""
from __future__ import annotations
import csv
import uuid
from pathlib import Path
from typing import Literal, TypedDict
from copilotkit import (
CopilotKitMiddleware,
StateItem,
StateStreamingMiddleware,
a2ui,
)
from langchain.agents import AgentState as BaseAgentState
from langchain.agents import create_agent
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langchain_openai import ChatOpenAI
from langgraph.types import Command
# ─── Shared state schema ────────────────────────────────────────────
class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]
class AgentState(BaseAgentState):
todos: list[Todo]
# ─── Todo tools ─────────────────────────────────────────────────────
@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
"""
Manage the current todos.
"""
# Ensure all todos have IDs that are unique
for todo in todos:
if "id" not in todo or not todo["id"]:
todo["id"] = str(uuid.uuid4())
# Update the state
return Command(
update={
"todos": todos,
"messages": [
ToolMessage(
content="Successfully updated todos",
name="manage_todos",
id=str(uuid.uuid4()),
tool_call_id=runtime.tool_call_id,
)
],
}
)
@tool
def get_todos(runtime: ToolRuntime):
"""
Get the current todos.
"""
return runtime.state.get("todos", [])
todo_tools = [
manage_todos,
get_todos,
]
# ─── Data query tool ────────────────────────────────────────────────
# Read data at module load time to avoid file I/O issues in
# LangGraph Cloud's sandboxed tool execution environment.
_DATA_DIR = Path(__file__).parent / "beautiful_chat_data"
_csv_path = _DATA_DIR / "db.csv"
with open(_csv_path) as _f:
_cached_data = list(csv.DictReader(_f))
@tool
def query_data(query: str):
"""
Query the database, takes natural language. Always call before showing a chart or graph.
"""
return _cached_data
# ─── A2UI fixed-schema tool: flight search ──────────────────────────
CATALOG_ID = "copilotkit://app-dashboard-catalog"
SURFACE_ID = "flight-search-results"
class Flight(TypedDict, total=False):
# All fields marked optional (`total=False`) so the LLM (or aimock fixture)
# can omit auxiliary fields like `id` / `statusIcon` without tripping
# langchain's tool-arg validation. Previously these were required and any
# missing field surfaced as `Error invoking tool 'search_flights' with
# kwargs ... flights.N.id: Field required` — the agent treated the error
# string as the tool result and the surface never rendered.
airline: str
airlineLogo: str
flightNumber: str
origin: str
destination: str
date: str
departureTime: str
arrivalTime: str
duration: str
status: str
price: str
def _build_flight_components(flights: list[dict]) -> list[dict]:
"""Build a flat A2UI component tree with one literal FlightCard per flight.
Avoids the structural-children template form (Row.children = { componentId,
path }), which the GenericBinder only expands correctly for components whose
schema declares STRUCTURAL children — sibling demos work because their
schemas use literal-string-array children. Inlining the values per-flight
sidesteps the template path entirely and renders identically.
"""
flight_card_ids: list[str] = []
components: list[dict] = []
for index, flight in enumerate(flights):
card_id = f"flight-card-{index}"
flight_card_ids.append(card_id)
components.append(
{
"id": card_id,
"component": "FlightCard",
"airline": flight.get("airline", ""),
"airlineLogo": flight.get("airlineLogo", ""),
"flightNumber": flight.get("flightNumber", ""),
"origin": flight.get("origin", ""),
"destination": flight.get("destination", ""),
"date": flight.get("date", ""),
"departureTime": flight.get("departureTime", ""),
"arrivalTime": flight.get("arrivalTime", ""),
"duration": flight.get("duration", ""),
"status": flight.get("status", ""),
"price": flight.get("price", ""),
}
)
root: dict = {
"id": "root",
"component": "Row",
"children": flight_card_ids,
"gap": 16,
}
return [root, *components]
@tool
def search_flights(flights: list[Flight]) -> str:
"""Search for flights and display the results as rich cards. Return exactly 2 flights.
Each flight must have: airline (e.g. "United Airlines"),
airlineLogo (use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128
e.g. "https://www.google.com/s2/favicons?domain=united.com&sz=128" for United,
"https://www.google.com/s2/favicons?domain=delta.com&sz=128" for Delta,
"https://www.google.com/s2/favicons?domain=aa.com&sz=128" for American,
"https://www.google.com/s2/favicons?domain=alaskaair.com&sz=128" for Alaska),
flightNumber, origin, destination,
date (short readable format like "Tue, Mar 18" — use near-future dates),
departureTime, arrivalTime,
duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"),
and price (e.g. "$289").
"""
return a2ui.render(
operations=[
a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID),
a2ui.update_components(SURFACE_ID, _build_flight_components(flights)),
],
)
# ─── A2UI dynamic-schema tool: LLM-generated UI ─────────────────────
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
# ─── Graph ──────────────────────────────────────────────────────────
model = ChatOpenAI(model="gpt-5.4", model_kwargs={"parallel_tool_calls": False})
agent = create_agent(
model=model,
tools=[query_data, *todo_tools, search_flights],
middleware=[
CopilotKitMiddleware(),
StateStreamingMiddleware(
StateItem(state_key="todos", tool="manage_todos", tool_argument="todos")
),
],
state_schema=AgentState,
system_prompt="""
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
Tool guidance:
- Flights: call search_flights to show flight cards with a pre-built schema.
- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,
charts, tables, and cards. It handles rendering automatically.
- Charts: call query_data first, then render with the chart component.
- Todos: enable app mode first, then manage todos.
- A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"),
respond with a brief confirmation. The UI already updated on the frontend.
""",
)
graph = agent
@@ -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
1 date,category,subcategory,amount,type,notes
2 2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
3 2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
4 2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
5 2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
6 2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
7 2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
8 2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
9 2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
10 2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
11 2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
12 2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
13 2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
14 2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
15 2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
16 2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
17 2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
18 2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
19 2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
20 2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
21 2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
22 2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
23 2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
24 2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
25 2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
26 2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
27 2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
28 2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
29 2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
30 2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
31 2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
32 2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
33 2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
34 2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
35 2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
36 2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
37 2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
38 2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
39 2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
40 2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
41 2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
@@ -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,54 @@
"""LangGraph agent backing the declarative-hashbrown demo.
Emits hashbrown-shaped structured output that the ported HashBrownDashboard
renderer (`src/app/demos/declarative-hashbrown/hashbrown-renderer.tsx`) progressively
parses via `@hashbrownai/react`'s `useJsonParser` + `useUiKit`.
Wire format
-----------
`@hashbrownai/react`'s `useJsonParser(content, kit.schema)` expects the agent
to stream a JSON object literal matching `kit.schema` — NOT the `<ui>...</ui>`
XML-style examples shown inside `useUiKit({ examples })`. Those XML examples
are the hashbrown prompt DSL that hashbrown compiles into a schema description
when driving the LLM directly (e.g. `useUiChat`/`useUiCompletion`). Because
this demo drives the LLM via langgraph instead, we must mirror what
hashbrown's own schema wire format looks like:
{
"ui": [
{ "metric": { "props": { "label": "...", "value": "..." } } },
{ "pieChart": { "props": { "title": "...", "data": "[{...}]" } } },
{ "barChart": { "props": { "title": "...", "data": "[{...}]" } } },
{ "dealCard": { "props": { "title": "...", "stage": "prospect", "value": 100000 } } },
{ "Markdown": { "props": { "children": "## heading\\nbody" } } }
]
}
Every node is a single-key object `{tagName: {props: {...}}}`. The tag names
and prop schemas match `useSalesDashboardKit()` in
`hashbrown-renderer.tsx`. `pieChart` and `barChart` receive `data` as a
JSON-encoded string (this was intentional in PR #4252 to keep the schema
stable under partial streaming).
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
from src.agents.byoc_hashbrown_prompt import BYOC_HASHBROWN_SYSTEM_PROMPT
# Force JSON-object output mode. The frontend's `useJsonParser` bails to
# `null` on any non-JSON prefix (code fences, prose preamble, etc.), so
# leaving the model free to wander out of JSON is what left the renderer
# empty in practice. `response_format={"type": "json_object"}` tells
# OpenAI to refuse to emit anything but a single JSON object, which
# aligns the wire-level contract with what the parser accepts.
graph = create_agent(
model=ChatOpenAI(
model="gpt-5.4",
model_kwargs={"response_format": {"type": "json_object"}},
),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=BYOC_HASHBROWN_SYSTEM_PROMPT,
)
@@ -0,0 +1,60 @@
"""System prompt for byoc_hashbrown_agent.
Lives alongside the agent module so the agent file stays focused on
LangGraph setup. The prompt is long because it documents the
component-by-component wire format that `@hashbrownai/react`'s streaming
JSON parser expects on the frontend — see `byoc_hashbrown_agent.py` for
the why.
"""
BYOC_HASHBROWN_SYSTEM_PROMPT = """\
You are a sales analytics assistant that replies by emitting a single JSON
object consumed by a streaming JSON parser on the frontend.
ALWAYS respond with a single JSON object of the form:
{
"ui": [
{ <componentName>: { "props": { ... } } },
...
]
}
Do NOT wrap the response in code fences. Do NOT include any preface or
explanation outside the JSON object. The response MUST be valid JSON.
Available components and their prop schemas:
- "metric": { "props": { "label": string, "value": string } }
A KPI card. `value` is a pre-formatted string like "$1.2M" or "248".
- "pieChart": { "props": { "title": string, "data": string } }
A donut chart. `data` is a JSON-encoded STRING (embedded JSON) of an
array of {label, value} objects with at least 3 segments, e.g.
"data": "[{\\"label\\":\\"Enterprise\\",\\"value\\":600000}]".
- "barChart": { "props": { "title": string, "data": string } }
A vertical bar chart. `data` is a JSON-encoded STRING of an array of
{label, value} objects with at least 3 bars, typically time-ordered.
- "dealCard": { "props": { "title": string, "stage": string, "value": number } }
A single sales deal. `stage` MUST be one of: "prospect", "qualified",
"proposal", "negotiation", "closed-won", "closed-lost". `value` is a
raw number (no currency symbol or comma).
- "Markdown": { "props": { "children": string } }
Short explanatory text. Use for section headings and brief summaries.
Standard markdown is supported in `children`.
Rules:
- Always produce plausible sample data when the user asks for a dashboard or
chart — do not refuse for lack of data.
- Prefer 3-6 rows of data in charts; keep labels short.
- Use "Markdown" for short headings or linking sentences between visual
components. Do not emit long prose.
- Do not emit components that are not listed above.
- `data` props on charts MUST be a JSON STRING — escape inner quotes.
Example response (sales dashboard):
{"ui":[{"Markdown":{"props":{"children":"## Q4 Sales Summary"}}},{"metric":{"props":{"label":"Total Revenue","value":"$1.2M"}}},{"metric":{"props":{"label":"New Customers","value":"248"}}},{"pieChart":{"props":{"title":"Revenue by Segment","data":"[{\\"label\\":\\"Enterprise\\",\\"value\\":600000},{\\"label\\":\\"SMB\\",\\"value\\":400000},{\\"label\\":\\"Startup\\",\\"value\\":200000}]"}}},{"barChart":{"props":{"title":"Monthly Revenue","data":"[{\\"label\\":\\"Oct\\",\\"value\\":350000},{\\"label\\":\\"Nov\\",\\"value\\":400000},{\\"label\\":\\"Dec\\",\\"value\\":450000}]"}}}]}
"""
@@ -0,0 +1,160 @@
"""LangGraph agent backing the BYOC json-render demo.
Emits a single JSON object shaped like `@json-render/react`'s flat spec
format (`{ root, elements }`) so the frontend can feed it directly into
`<Renderer />` against a Zod-validated catalog of three components —
MetricCard, BarChart, PieChart.
The scenario mirrors the declarative-hashbrown demo so the two BYOC rows on the
dashboard are directly comparable. The only difference is the rendering
technology; the catalog shape and suggestion prompts are identical.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = """
You are a sales-dashboard UI generator for a BYOC json-render demo.
When the user asks for a UI, respond with **exactly one JSON object** and
nothing else — no prose, no markdown fences, no leading explanation. The
object must match this schema (the "flat element map" format consumed by
`@json-render/react`):
{
"root": "<id of the root element>",
"elements": {
"<id>": {
"type": "<component name>",
"props": { ... component-specific props ... },
"children": [ "<id>", ... ]
},
...
}
}
Available components (use each name verbatim as "type"):
- MetricCard
props: { "label": string, "value": string, "trend": string | null }
Example trend strings: "+12% vs last quarter", "-3% vs last month", null.
- BarChart
props: {
"title": string,
"description": string | null,
"data": [ { "label": string, "value": number }, ... ]
}
- PieChart
props: {
"title": string,
"description": string | null,
"data": [ { "label": string, "value": number }, ... ]
}
Rules:
1. Output **only** valid JSON. No markdown code fences. No text outside
the object.
2. Every id referenced in `root` or any `children` array must be a key
in `elements`.
3. For a multi-component dashboard, use a root MetricCard and list the
charts in its `children` array, OR pick any element as root and list
the others as its children. Do not emit orphan elements.
4. Use realistic sales-domain values (revenue, pipeline, conversion,
categories, months) — the demo is a sales dashboard.
5. `children` is optional but when present must be an array of strings.
6. Never invent component types outside the three listed above.
### Worked example — "Show me the sales dashboard with metrics and a revenue chart"
{
"root": "revenue-metric",
"elements": {
"revenue-metric": {
"type": "MetricCard",
"props": {
"label": "Revenue (Q3)",
"value": "$1.24M",
"trend": "+18% vs Q2"
},
"children": ["revenue-bar"]
},
"revenue-bar": {
"type": "BarChart",
"props": {
"title": "Monthly revenue",
"description": "Revenue by month across Q3",
"data": [
{ "label": "Jul", "value": 380000 },
{ "label": "Aug", "value": 410000 },
{ "label": "Sep", "value": 450000 }
]
}
}
}
}
### Worked example — "Break down revenue by category as a pie chart"
{
"root": "category-pie",
"elements": {
"category-pie": {
"type": "PieChart",
"props": {
"title": "Revenue by category",
"description": "Share of total revenue by product category",
"data": [
{ "label": "Enterprise", "value": 540000 },
{ "label": "SMB", "value": 310000 },
{ "label": "Self-serve", "value": 220000 },
{ "label": "Partner", "value": 170000 }
]
}
}
}
}
### Worked example — "Show me monthly expenses as a bar chart"
{
"root": "expense-bar",
"elements": {
"expense-bar": {
"type": "BarChart",
"props": {
"title": "Monthly expenses",
"description": "Operating expenses by month",
"data": [
{ "label": "Jul", "value": 210000 },
{ "label": "Aug", "value": 225000 },
{ "label": "Sep", "value": 240000 }
]
}
}
}
}
Respond with the JSON object only.
"""
# Force JSON-object output mode. The frontend's `parseSpec` already
# tolerates code fences and prose preamble via `extractJsonObject`, but
# locking the model to JSON at the API layer removes the ambiguity
# entirely — the only thing the LLM can emit is a single JSON object,
# which is exactly what `<Renderer />` needs.
graph = create_agent(
model=ChatOpenAI(
model="gpt-5.4",
temperature=0.2,
model_kwargs={"response_format": {"type": "json_object"}},
),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT.strip(),
)
@@ -0,0 +1,25 @@
"""LangGraph agent backing the Frontend Tools demo.
This cell demonstrates `useFrontendTool` with a synchronous handler.
The backend graph registers no tools of its own — CopilotKit forwards
the frontend tool schema(s) to the agent at runtime, and the handler
executes in the browser. CopilotKitMiddleware is attached so frontend
tools, shared state, and agent context flow into every turn.
Like the sibling `frontend_tools_async` cell, the agent has no custom
behavior beyond a permissive system prompt — the demo's value is in
showing the wiring contract, not the agent logic.
"""
# region: middleware
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt="You are a helpful, concise assistant.",
)
# endregion
@@ -0,0 +1,32 @@
"""LangGraph agent backing the Frontend Tools (Async) demo.
This cell demonstrates `useFrontendTool` with an ASYNC handler. The
frontend registers a `query_notes` tool whose handler awaits a simulated
client-side DB query (500ms latency) and returns matching notes. The
agent uses the returned result to summarize what it found.
Like the sibling `frontend_tools` cell, the backend graph registers no
tools of its own — CopilotKit forwards the frontend tool schema(s) to
the agent at runtime, and the handler executes in the browser.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = (
"You are a helpful assistant that can search the user's personal notes. "
"When the user asks about their notes, call the `query_notes` tool with "
"a concise keyword extracted from their request. The tool is provided "
"by the frontend at runtime and runs entirely in the user's browser — "
"you do not need to implement it yourself. After the tool returns, "
"summarize the matching notes clearly and concisely. If no notes match, "
"say so plainly and offer to try a different keyword."
)
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,97 @@
"""gen-ui-agent — minimal agent with explicit state + state-editing tool.
The agent plans a task as 3 steps and walks each pending -> in_progress
-> completed, calling `set_steps` after every transition. The frontend
subscribes to `state.steps` via `useAgent` and renders a live progress
card.
"""
from __future__ import annotations
import uuid
from typing import Annotated, Any, Literal
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware.types import AgentState, OmitFromInput
from langchain.chat_models import init_chat_model
from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, tool
from langgraph.types import Command
from typing_extensions import NotRequired, TypedDict
class Step(TypedDict):
id: str
title: str
status: Literal["pending", "in_progress", "completed"]
def _last_steps(_prev: list[Step] | None, new: list[Step] | None) -> list[Step]:
"""Reducer: last write wins (accepts parallel tool calls in one superstep)."""
return new if new is not None else (_prev or [])
class GenUiAgentState(AgentState):
"""Extends the base agent state with a typed `steps` field."""
steps: Annotated[NotRequired[list[Step]], _last_steps, OmitFromInput]
@tool
def set_steps(
steps: list[Step], tool_call_id: Annotated[str, InjectedToolCallId]
) -> Command[Any]:
"""Publish the current plan + step statuses. Call this every time a step
transitions (including the first enumeration of steps)."""
return Command(
update={
"steps": steps,
"messages": [
ToolMessage(
f"Published {len(steps)} step(s).",
name="set_steps",
id=str(uuid.uuid4()),
tool_call_id=tool_call_id,
)
],
}
)
SYSTEM_PROMPT = (
"You are an agentic planner. For each user request, follow this exact "
"sequence:\n"
"1. Plan exactly 3 concrete steps and call `set_steps` ONCE with all "
'three steps at status="pending".\n'
'2. Step 1: call `set_steps` with step 1 at status="in_progress", '
'then call `set_steps` again with step 1 at status="completed".\n'
'3. Step 2: call `set_steps` with step 2 at status="in_progress", '
'then call `set_steps` again with step 2 at status="completed".\n'
'4. Step 3: call `set_steps` with step 3 at status="in_progress", '
'then call `set_steps` again with step 3 at status="completed".\n'
"5. Send ONE final conversational assistant message summarizing the "
"plan, then stop. Do not call any more tools after step 3 is "
"completed.\n"
"\n"
"Rules: never call set_steps in parallel — always wait for one call to "
"return before the next. After all three steps are completed you MUST "
"send a final assistant message and terminate."
)
# Uses `langchain.agents.create_agent` (not `deepagents.create_deep_agent`)
# because `create_deep_agent`'s planner+sub-agent middleware ate enough
# supersteps on this single-tool ReAct loop to repeatedly trip
# LangGraph's default recursion limit. The ReAct loop here is two
# supersteps per LLM/tool cycle (model node + tool node); the prompt
# drives ~7 set_steps cycles + 1 final model turn, so nominal cost is
# ~15 supersteps. `recursion_limit=50` gives ~3× headroom for retries
# inside the LLM loop.
graph = create_agent(
model=init_chat_model("openai:gpt-4o-mini", temperature=0, use_responses_api=False),
tools=[set_steps],
system_prompt=SYSTEM_PROMPT,
state_schema=GenUiAgentState,
middleware=[CopilotKitMiddleware()],
).with_config({"recursion_limit": 50})
@@ -0,0 +1,34 @@
"""LangGraph agent backing the Tool-Based Generative UI demo.
The frontend registers `render_bar_chart` and `render_pie_chart` tools via
`useComponent`. CopilotKit's LangGraph middleware injects those tools into
the model request at runtime so the agent can call them.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = """You are a data visualization assistant.
When the user asks for a chart, call `render_bar_chart` or `render_pie_chart`
with a concise title, short description, and a `data` array of
`{label, value}` items. Pick bar for comparisons over a small set of
categories; pick pie for composition / share-of-whole.
If the user names a chart subject but does NOT supply concrete numbers
(e.g. "show me a pie chart of website traffic by source"), do NOT ask
them for data. Invent plausible illustrative sample values yourself,
call the appropriate `render_*` tool immediately, and briefly note in
the follow-up that the values are illustrative samples. Always render
the chart on the first turn -- never reply with a clarifying question
asking for the data.
Keep chat responses brief -- let the chart do the talking."""
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,110 @@
"""LangGraph agent backing the Headless Chat (Complete) demo.
The cell exists to prove that every CopilotKit rendering surface works
when the chat UI is composed manually (no <CopilotChatMessageView /> or
<CopilotChatAssistantMessage />). To exercise those surfaces we give
this agent:
- two mock backend tools (get_weather, get_stock_price) — render via
app-registered `useRenderTool` renderers on the frontend,
- access to a frontend-registered `useComponent` tool
(`highlight_note`) — the agent "calls" it and the UI flows through
the same `useRenderToolCall` path,
- MCP Apps wired through the runtime — the agent can invoke Excalidraw
MCP tools and the middleware emits activity events that
`useRenderActivityMessage` picks up.
The system prompt nudges the model toward the right surface per user
question and falls back to plain text otherwise.
"""
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = (
"You are a helpful, concise assistant wired into a headless chat "
"surface that demonstrates CopilotKit's full rendering stack. Pick the "
"right surface for each user question and fall back to plain text when "
"none of the tools fit.\n\n"
"Routing rules:\n"
" - If the user asks about weather for a place, call `get_weather` "
"with the location.\n"
" - If the user asks about a stock or ticker (AAPL, TSLA, MSFT, ...), "
"call `get_stock_price` with the ticker.\n"
" - If the user asks for a chart, graph, or visualization of revenue, "
"sales, or other metrics over time, call `get_revenue_chart`.\n"
" - If the user asks you to highlight, flag, or mark a short note or "
"phrase, call the frontend `highlight_note` tool with the text and a "
"color (yellow, pink, green, or blue). Do NOT ask the user for the "
"color — pick a sensible one if they didn't say.\n"
" - If the user asks to draw, sketch, or diagram something, use the "
"Excalidraw MCP tools that are available to you.\n"
" - Otherwise, reply in plain text.\n\n"
"After a tool returns, write one short sentence summarizing the "
"result. Never fabricate data a tool could provide."
)
@tool
def get_weather(location: str) -> dict:
"""Get the current weather for a given location.
Returns a mock payload with city, temperature in Fahrenheit, humidity,
wind speed, and conditions. Use this whenever the user asks about
weather anywhere.
"""
return {
"city": location,
"temperature": 68,
"humidity": 55,
"wind_speed": 10,
"conditions": "Sunny",
}
@tool
def get_stock_price(ticker: str) -> dict:
"""Get a mock current price for a stock ticker.
Returns a payload with the ticker symbol (uppercased), price in USD,
and percentage change for the day. Use this whenever the user asks
about a stock price.
"""
return {
"ticker": ticker.upper(),
"price_usd": 189.42,
"change_pct": 1.27,
}
@tool
def get_revenue_chart() -> dict:
"""Get a mock six-month revenue series for a chart visualization.
Returns a title, subtitle, and an array of {label, value} points. Use
this whenever the user asks for a chart, graph, or visualization of
revenue, sales, or other quarterly/monthly metrics.
"""
return {
"title": "Quarterly revenue",
"subtitle": "Last six months · USD thousands",
"data": [
{"label": "Jan", "value": 38},
{"label": "Feb", "value": 47},
{"label": "Mar", "value": 52},
{"label": "Apr", "value": 49},
{"label": "May", "value": 63},
{"label": "Jun", "value": 71},
],
}
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[get_weather, get_stock_price, get_revenue_chart],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,61 @@
"""LangGraph agent backing the In-App HITL (frontend-tool + popup) demo.
The agent is a support assistant that processes customer-care requests
(refunds, account changes, escalations). Any action that materially
affects a customer MUST be confirmed by the human operator via the
frontend-provided `request_user_approval` tool.
The tool is defined on the frontend via `useFrontendTool` with an async
handler that opens a modal dialog OUTSIDE the chat surface. The handler
awaits the user's decision and resolves with
`{"approved": bool, "reason": str}`. This agent treats that result as
authoritative: if `approved` is `True`, continue; otherwise, stop and
explain the decision back to the user.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = (
"You are a support operations copilot working alongside a human operator "
"inside an internal support console. The operator can see a list of open "
"support tickets on the left side of their screen and is chatting with "
"you on the right.\n"
"\n"
"Whenever the operator asks you to take an action that affects a "
"customer — for example: issuing a refund, updating a customer's plan, "
"cancelling a subscription, escalating a ticket, or sending an apology "
"credit — you MUST first call the frontend-provided "
"`request_user_approval` tool to obtain the operator's explicit consent.\n"
"\n"
"How to use `request_user_approval`:\n"
"- `message`: a short, plain-English summary of the exact action you "
" are about to take, including concrete numbers (e.g. '$50 refund to "
" customer #12345').\n"
"- `context`: optional extra context the operator might want to review "
" (the ticket ID, the policy rule you're applying, etc.). Keep it to "
" one or two short sentences.\n"
"\n"
"The tool returns an object of the shape "
'`{"approved": boolean, "reason": string | null}`.\n'
"- If `approved` is `true`: confirm in one short sentence that you are "
" processing the action. You do not actually need to call any other "
" tool — this is a demo. Just acknowledge.\n"
"- If `approved` is `false`: acknowledge the rejection in one short "
" sentence and, if `reason` is non-empty, reflect the operator's "
" reason back to them. Do NOT retry the action.\n"
"\n"
"Keep all chat replies to one or two short sentences. Never make up "
"customer data — always use whatever the operator told you in the "
"prompt."
)
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,22 @@
"""LangGraph agent backing the In-Chat HITL (useHumanInTheLoop) demo.
The `book_call` tool is defined on the frontend via `useHumanInTheLoop`,
so there is no backend tool here. CopilotKitMiddleware is attached so the
frontend suggestions and the time-picker render hook are picked up.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=(
"You help users book an onboarding call with the sales team. "
"When they ask to book a call, call the frontend-provided "
"`book_call` tool with a short topic and the user's name. "
"Keep any chat reply to one short sentence."
),
)
@@ -0,0 +1,109 @@
"""LangGraph agent for the Human-in-the-Loop (Interrupt-based) booking demo.
Defines a backend tool `schedule_meeting(topic, attendee)` that uses
LangGraph's `interrupt()` primitive to pause the run and surface a
structured booking payload to the frontend. The frontend `useInterrupt`
renderer shows a time picker inline in the chat and resolves with
`{chosen_time, chosen_label}` or `{cancelled: true}`, which this tool
turns into a human-readable result the agent uses to confirm the booking.
"""
# @region[backend-interrupt-tool]
from __future__ import annotations
from datetime import datetime, time, timedelta
from typing import Any, List, Optional
from zoneinfo import ZoneInfo
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.types import interrupt
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = (
"You are a scheduling assistant. Whenever the user asks you to book a "
"call / schedule a meeting, you MUST call the `schedule_meeting` tool. "
"Pass a short `topic` describing the purpose and `attendee` describing "
"who the meeting is with. After the tool returns, confirm briefly "
"whether the meeting was scheduled and at what time, or that the user "
"cancelled."
)
# Demo-only fixed timezone. A real app would use the user's calendar +
# locale (e.g. zoneinfo.ZoneInfo(user.timezone) and Google Calendar /
# Outlook availability); we hardcode Pacific so screenshots are stable.
_DEMO_TZ = ZoneInfo("America/Los_Angeles")
def _candidate_slots() -> List[dict]:
"""Upcoming candidate slots, relative to "now" so the picker never
shows stale dates."""
now = datetime.now(_DEMO_TZ)
tomorrow = (now + timedelta(days=1)).date()
# Skip a week when the result would collide with `tomorrow` — i.e.
# today is Mon (0 days away, picker would show two slots both
# labelled "Monday") or Sun (1 day away, picker would show
# "Tomorrow" and "Monday" both pointing at the same date).
days_to_monday = (7 - now.weekday()) % 7
if days_to_monday <= 1:
days_to_monday += 7
next_monday = (now + timedelta(days=days_to_monday)).date()
candidates = [
("Tomorrow 10:00 AM", tomorrow, time(10, 0)),
("Tomorrow 2:00 PM", tomorrow, time(14, 0)),
("Monday 9:00 AM", next_monday, time(9, 0)),
("Monday 3:30 PM", next_monday, time(15, 30)),
]
return [
{"label": label, "iso": datetime.combine(d, t, _DEMO_TZ).isoformat()}
for label, d, t in candidates
]
@tool
def schedule_meeting(topic: str, attendee: Optional[str] = None) -> str:
"""Ask the user to pick a time slot for a call, via an in-chat picker.
Args:
topic: Short human-readable description of the call's purpose.
attendee: Who the call is with (optional).
Returns:
Human-readable result string describing the chosen slot or
indicating the user cancelled.
"""
# `interrupt()` pauses the LangGraph run and forwards a structured
# payload to the client. The frontend v2 `useInterrupt` hook renders
# the picker inline in the chat, then calls `resolve(...)` with the
# user's selection — that value comes back here as `response`.
response: Any = interrupt(
{
"topic": topic,
"attendee": attendee,
"slots": _candidate_slots(),
}
)
if isinstance(response, dict):
if response.get("cancelled"):
return f"User cancelled. Meeting NOT scheduled: {topic}"
chosen_label = response.get("chosen_label") or response.get("chosen_time")
if chosen_label:
return f"Meeting scheduled for {chosen_label}: {topic}"
return f"User did not pick a time. Meeting NOT scheduled: {topic}"
# @endregion[backend-interrupt-tool]
model = ChatOpenAI(model="gpt-5.4")
graph = create_agent(
model=model,
tools=[schedule_meeting],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,29 @@
"""
Default LangGraph agent — neutral "helpful, concise assistant".
This is the fallthrough graph for demos that don't require anything more
specialized. Cells that need tailored behavior (chart viz, weather-only,
etc.) should have their own dedicated graph under `src/agents/` and
explicit wiring in the CopilotKit route.
"""
# CVDIAG runtime bootstrap (L1-H, folded into L1-I for LGP). MUST be the first
# non-stdlib import: importing this module configures the root logger so the
# agents._* CVDIAG loggers actually emit, resolves the verbosity tier (§6
# fail-closed DEBUG guard), and builds the threaded PocketBase writer — once, at
# process start. main.py is langgraph's default graph entrypoint (sample_agent)
# and is verified present by entrypoint.sh, so it is the reliable single
# bootstrap chokepoint for the LGP process.
import _shared.cvdiag_bootstrap # noqa: F401 (import side effects = the bootstrap)
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt="You are a helpful, concise assistant.",
)
@@ -0,0 +1,63 @@
"""
LangGraph agent for the CopilotKit MCP Apps demo.
This agent has no bespoke tools — the CopilotKit runtime is wired with
``mcpApps: { servers: [...] }`` pointing at the public Excalidraw MCP
server (see ``src/app/api/copilotkit-mcp-apps/route.ts``). The runtime
auto-applies the MCP Apps middleware, which exposes the remote MCP
server's tools to this agent at request time and emits the activity
events that CopilotKit's built-in ``MCPAppsActivityRenderer`` renders in
the chat as a sandboxed iframe.
Reference:
https://docs.copilotkit.ai/integrations/langgraph/generative-ui/mcp-apps
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
SYSTEM_PROMPT = """\
You draw simple diagrams in Excalidraw via the MCP tool.
SPEED MATTERS. Produce a correct-enough diagram fast; do not optimize
for polish. Target: one tool call, done in seconds.
When the user asks for a diagram:
1. Call `create_view` ONCE with 3-5 elements total: shapes + arrows +
an optional title text.
2. Use straightforward shapes (rectangle, ellipse, diamond) with plain
`label` fields (`{"text": "...", "fontSize": 18}`) on them.
3. Connect with arrows. Endpoints can be element centers or simple
coordinates — you don't need edge anchors / fixedPoint bindings.
4. Include ONE `cameraUpdate` at the END of the elements array that
frames the whole diagram. Use an approved 4:3 size (600x450 or
800x600). No opening camera needed.
5. Reply with ONE short sentence describing what you drew.
Every element needs a unique string `id` (e.g. `"b1"`, `"a1"`,
`"title"`). Standard sizes: rectangles 160x70, ellipses/diamonds
120x80, 40-80px gap between shapes.
Do NOT:
- Call `read_me`. You already know the basic shape API.
- Make multiple `create_view` calls.
- Iterate or refine. Ship on the first shot.
- Add decorative colors / fills / zone backgrounds unless the user
explicitly asks for them.
- Add labels on arrows unless crucial.
If the user asks for something specific (colors, more elements,
particular layout), follow their lead — but still in ONE call.
"""
graph = create_agent(
# gpt-4o-mini for speed — Excalidraw element emission is simple
# JSON and we're biasing hard toward sub-30s generation. A faster
# model produces shorter, quicker outputs with acceptable layouts.
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,252 @@
"""Multimodal LangGraph agent — accepts image + document (PDF) attachments.
Wave 2b design: a *dedicated* vision-capable graph scoped to the
`/demos/multimodal` cell. Other demos continue to use their own (cheaper,
text-only) models — this keeps vision cost isolated to the one demo that
exercises it.
Wire format the agent sees
==========================
Attachments arrive here after travelling through:
CopilotChat → AG-UI message content parts → @ag-ui/langgraph runtime
(ag-ui → LangChain converter)
→ this agent (LangChain HumanMessage content parts)
The ag-ui-langgraph converter only understands the legacy
``{ type: "binary", mimeType, data | url }`` AG-UI part shape — the page
at ``src/app/demos/multimodal/page.tsx`` installs an
``onRunInitialized`` shim that rewrites the modern
``{ type: "image" | "document", source: {...} }`` shape CopilotChat emits
to the legacy shape before it hits the runtime. Once the converter has
run, every attachment shows up in this agent as a LangChain
``image_url`` content part::
{"type": "image_url", "image_url": {"url": "data:<mime>;base64,<payload>"}}
regardless of whether the upstream modality was ``image`` or ``document``.
We therefore route on ``mimeType``, not the part ``type``:
``image/*`` parts are forwarded to GPT-4o unchanged (vision-native);
``application/pdf`` parts are flattened to inline text via ``pypdf`` so
the model can read them without needing file-part support.
References:
- src/agents/main.py, src/agents/agentic_chat.py (baseline pattern)
- packages/runtime/src/agent/converters/tanstack.ts (the modern content-
part shape — useful context when the runtime gets upgraded and this
agent can drop the pypdf flatten)
"""
from __future__ import annotations
import base64
import io
from typing import Any
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from pypdf import PdfReader
SYSTEM_PROMPT = (
"You are a helpful assistant. The user may attach images or documents "
"(PDFs). When they do, analyze the attachment carefully and answer the "
"user's question. If no attachment is present, answer the text question "
"normally. Keep responses concise (1-3 sentences) unless asked to go deep."
)
def _extract_data_url_parts(url: str) -> tuple[str, str]:
"""Split a ``data:<mime>;base64,<payload>`` URL into (mime, base64-payload).
Returns ("", url) if the input is not a base64 data URL — callers can
fall back to treating the url as a fetchable reference.
"""
if not url.startswith("data:"):
return "", url
header, _, payload = url.partition(",")
# Header looks like "data:application/pdf;base64" — take the piece
# between the colon and the first semicolon.
if ":" not in header:
return "", payload
meta = header.split(":", 1)[1]
mime = meta.split(";", 1)[0] if ";" in meta else meta
return mime, payload
def _extract_pdf_text(b64: str) -> str:
"""Decode an inline-base64 PDF and extract its text. Returns "" on
any failure so one malformed attachment doesn't tank the user turn —
callers must treat the extracted text as best-effort."""
try:
raw = base64.b64decode(b64, validate=False)
reader = PdfReader(io.BytesIO(raw))
pages = [page.extract_text() or "" for page in reader.pages]
return "\n\n".join(pages).strip()
except Exception as exc: # pragma: no cover - defensive
# One log line so a malformed attachment stays triageable in
# Railway logs without restoring the per-stage noise the
# cleanup removed.
print(f"[multimodal_agent] PDF extract failed: {exc!r}")
return ""
def _classify_attachment_part(part: Any) -> tuple[str, str, str] | None:
"""Inspect a content part and return (kind, mime, base64_payload).
``kind`` is one of ``"image"``, ``"pdf"``, ``"other"``. Returns
``None`` if the part is not an attachment we recognise (plain text,
unrelated dict, string, etc.).
Handles the shapes we actually see in practice:
- ``{"type": "image_url", "image_url": {"url": "data:..."}}``
(what the ag-ui-langgraph converter emits for every attachment
after the page rewrites to legacy ``binary``).
- ``{"type": "image_url", "image_url": "data:..."}``
(older LangChain/OpenAI shape where ``image_url`` is a raw string).
- ``{"type": "document", "source": {"type": "data",
"value": "<base64>", "mimeType": "application/pdf"}}``
(modern AG-UI shape — preserved for forward-compat if the runtime
ever starts forwarding modern parts directly).
"""
if not isinstance(part, dict):
return None
part_type = part.get("type")
if part_type == "image_url":
image_url = part.get("image_url")
url: str | None = None
if isinstance(image_url, str):
url = image_url
elif isinstance(image_url, dict):
raw_url = image_url.get("url")
if isinstance(raw_url, str):
url = raw_url
if not url:
return None
mime, payload = _extract_data_url_parts(url)
if not payload or not mime:
return None
if mime.startswith("image/"):
return ("image", mime, payload)
if "pdf" in mime.lower():
return ("pdf", mime, payload)
return ("other", mime, payload)
if part_type == "document":
source = part.get("source")
if not isinstance(source, dict) or source.get("type") != "data":
return None
value = source.get("value")
mime = source.get("mimeType", "")
if not isinstance(value, str) or not isinstance(mime, str):
return None
if "pdf" in mime.lower():
return ("pdf", mime, value)
return ("other", mime, value)
return None
def _preprocess_part(part: Any) -> Any:
"""Flatten PDF attachments to text; pass everything else through.
Images stay as-is so GPT-4o consumes them natively via its vision
adapter. PDFs (which gpt-4o cannot read directly) become a text part
prefixed with ``[Attached document]`` and the extracted body. If
extraction fails we emit a structured placeholder so the model can
tell the user the document was unreadable instead of pretending no
attachment was sent.
"""
classified = _classify_attachment_part(part)
if classified is None:
return part
kind, _mime, payload = classified
if kind != "pdf":
return part
text = _extract_pdf_text(payload)
if not text:
return {
"type": "text",
"text": "[Attached document: PDF could not be read.]",
}
return {"type": "text", "text": f"[Attached document]\n{text}"}
def _rewrite_messages(messages: list[Any]) -> list[Any]:
"""Rewrite user messages so non-image attachments become text parts.
Operates on the messages list stored in agent state. Returns a *new*
list; the input list is not mutated.
"""
rewritten: list[Any] = []
for message in messages:
# Only touch HumanMessage — assistant/tool messages stay as-is.
if not isinstance(message, HumanMessage):
rewritten.append(message)
continue
content = message.content
if not isinstance(content, list):
rewritten.append(message)
continue
new_parts = [_preprocess_part(part) for part in content]
rewritten.append(HumanMessage(content=new_parts, id=message.id))
return rewritten
class _PdfFlattenMiddleware(AgentMiddleware):
"""Flatten PDF content parts to text for the model call only.
Uses ``wrap_model_call`` instead of ``before_model`` so the PDF→text
rewrite is scoped to the outgoing model request and never persists
back into agent state. This matters because the agent state is
streamed verbatim to the chat UI: if we mutated state with a
``{"type": "text", "text": "[Attached document]\\n<pdf body>"}``
part, the chat would render that flattened text inline in the user
message bubble (in addition to the PDF chip preview the modern
``document`` part already drives), turning a clean attachment chip
into a wall of raw PDF text.
With ``wrap_model_call`` we copy the request, rewrite messages on
the copy, hand the copy to the model, and return the model's
response unchanged. The handler closure keeps state untouched.
"""
def wrap_model_call(self, request, handler): # type: ignore[override]
messages = list(request.messages) if request.messages else []
rewritten = _rewrite_messages(messages)
if rewritten == messages:
return handler(request)
return handler(request.override(messages=rewritten))
async def awrap_model_call(self, request, handler): # type: ignore[override]
messages = list(request.messages) if request.messages else []
rewritten = _rewrite_messages(messages)
if rewritten == messages:
return await handler(request)
return await handler(request.override(messages=rewritten))
# Vision-capable model. gpt-4o consumes `image_url` content parts natively.
_MODEL = ChatOpenAI(model="gpt-5.4", temperature=0.2)
graph = create_agent(
model=_MODEL,
tools=[],
middleware=[_PdfFlattenMiddleware(), CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
# Re-export under both names — `graph` matches the langgraph.json convention
# used by the rest of the package; `multimodal_agent` is a friendlier alias
# for any future non-langgraph.json import paths.
multimodal_agent = graph
__all__ = ["graph", "multimodal_agent"]
@@ -0,0 +1,90 @@
"""LangGraph agent for the Open-Ended Generative UI (Advanced) demo.
This is the "advanced" variant of the Open Generative UI demo. The key
distinguishing feature: the agent-authored, sandboxed UI can invoke
frontend-registered **sandbox functions** — functions the app defines on
the host page (see `src/app/demos/open-gen-ui-advanced/sandbox-functions.ts`)
and makes callable from inside the iframe via
`await Websandbox.connection.remote.<name>(args)`.
How it works end-to-end:
- The frontend passes `openGenerativeUI={{ sandboxFunctions }}` to the
`CopilotKitProvider`. The provider injects a JSON descriptor of those
functions into the agent context.
- `CopilotKitMiddleware` here picks up both the frontend-registered
`generateSandboxedUi` tool (auto-registered by the provider when OGUI
is enabled on the runtime) AND the sandbox-function descriptors (via
`copilotkit.context`), and merges them into what the LLM sees.
- The LLM then generates HTML + JS that calls
`Websandbox.connection.remote.<name>(...)` in response to user
interactions.
- The runtime's `OpenGenerativeUIMiddleware` converts the streaming
`generateSandboxedUi` tool call into `open-generative-ui` activity
events that the built-in renderer mounts inside a sandboxed iframe.
- The renderer wires each `sandboxFunctions` entry as a `localApi`
method on the websandbox connection so in-iframe code can call it.
The "minimal" sibling (`open_gen_ui_agent.py`) uses the same OGUI
pipeline without sandbox functions.
"""
from __future__ import annotations
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
SYSTEM_PROMPT = """You are a UI-generating assistant for the Open Generative UI (Advanced) demo.
On every user turn you MUST call the `generateSandboxedUi` frontend tool
exactly once. The generated UI must be INTERACTIVE and must invoke the
available host-side sandbox functions described in your agent context
(delivered via `copilotkit.context`) in response to user interactions.
Sandbox-function calling contract (inside the generated iframe):
- Call a host function with:
await Websandbox.connection.remote.<functionName>(args)
The call returns a Promise; await it.
- Each handler returns a plain object. Read the return shape from the
function's description in your context and use the EXACT field names
it returns (e.g. if the description says the handler returns
`{ ok, value }`, read `res.value` — not `res.result`).
- Descriptions, names, and JSON-schema parameter shapes for every
available sandbox function are listed in your context. Read them
carefully and wire at least one interactive UI element to call one.
Sandbox iframe restrictions (CRITICAL):
- The iframe runs with `sandbox="allow-scripts"` ONLY. Forms are NOT
allowed. You MUST NOT use `<form>` elements or `<button type="submit">`.
Clicking a submit button inside a sandboxed form is blocked by the
browser BEFORE any onsubmit handler runs, so the sandbox-function call
never fires.
- Use plain `<button type="button">` elements and wire them with
`addEventListener('click', ...)` or an inline click handler. Do the same
for "Enter" keypresses on inputs: attach a `keydown` listener that
checks `e.key === 'Enter'` and calls your handler directly — do NOT
wrap inputs in a `<form>`.
Generation guidance:
- Emit `initialHeight` and `placeholderMessages` first, then CSS, then
HTML, then `jsFunctions` / `jsExpressions` if helpful.
- Always include a visible result element (e.g. an output div) that you
UPDATE after the sandbox function resolves, so the user can *see* the
round-trip: "Button clicked -> remote call -> visible result".
- Use CDN scripts (Chart.js, D3, etc.) via <script> tags in the HTML head
when you need libraries.
- Do NOT use fetch/XHR, localStorage, or document.cookie — the sandbox has
no same-origin access. ONLY use `Websandbox.connection.remote.*` for
host-page interactions.
- Keep your own chat message brief (1 sentence max); the rendered UI is
the real output.
"""
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4", model_kwargs={"parallel_tool_calls": False}),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,65 @@
"""Minimal LangGraph agent for the Open-Ended Generative UI demo.
The simplest possible example that exercises the open-ended generative UI
pipeline. All the interesting work happens outside the agent:
- `CopilotKitMiddleware` merges the frontend-registered `generateSandboxedUi`
tool (auto-registered by `CopilotKitProvider` when the runtime has
`openGenerativeUI` enabled) into the agent's tool list. The LLM then sees
the tool via the normal AG-UI flow.
- When the LLM calls `generateSandboxedUi`, the runtime's
`OpenGenerativeUIMiddleware` (enabled via `openGenerativeUI` on the
runtime — see `src/app/api/copilotkit-ogui/route.ts`) converts that
streaming tool call into `open-generative-ui` activity events that the
built-in renderer mounts inside a sandboxed iframe.
This is the minimal variant: no sandbox functions, no app-side tools. The
agent simply asks the LLM to design and emit a single-shot sandboxed UI.
The "advanced" sibling (`open_gen_ui_advanced_agent.py`) builds on this
with sandbox-to-host function calling via `openGenerativeUI.sandboxFunctions`.
"""
from __future__ import annotations
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
SYSTEM_PROMPT = """You are a UI-generating assistant for an Open Generative UI demo
focused on intricate, educational visualisations (3D axes / rotations,
neural-network activations, sorting-algorithm walkthroughs, Fourier
series, wave interference, planetary orbits, etc.).
On every user turn you MUST call the `generateSandboxedUi` frontend tool
exactly once. Design a visually polished, self-contained HTML + CSS +
SVG widget that *teaches* the requested concept.
The frontend injects a detailed "design skill" as agent context
describing the palette, typography, labelling, and motion conventions
expected — follow it closely. Key invariants:
- Use inline SVG (or <canvas>) for geometric content, not stacks of <div>s.
- Every axis is labelled; every colour-coded series has a legend.
- Prefer CSS @keyframes / transitions over setInterval; loop cyclical
concepts with animation-iteration-count: infinite.
- Motion must teach — animate the actual step of the concept, not decoration.
- No fetch / XHR / localStorage — the sandbox has no same-origin access.
Output order:
- `initialHeight` (typically 480-560 for visualisations) first.
- A short `placeholderMessages` array (2-3 lines describing the build).
- `css` (complete).
- `html` (streams live — keep it tidy). CDN <script> tags for Chart.js /
D3 / etc. go inside the html.
Keep your own chat message brief (1 sentence) — the real output is the
rendered visualisation.
"""
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4", model_kwargs={"parallel_tool_calls": False}),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,35 @@
"""LangGraph agent backing the Shared State (Agent Read-Only) demo.
Demonstrates the `useAgentContext` hook from @copilotkit/react-core/v2:
the frontend provides READ-ONLY context *to* the agent. This is the
reverse direction of writable-shared-state — the UI cannot be edited by
the agent, but the agent reads this context on every turn via
`CopilotKitMiddleware`, which routes the context entries into the
model's message history.
No custom state, no tools: this is the minimal shape of the
useAgentContext pattern. The agent just reads whatever context the
frontend registered and answers accordingly.
"""
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
# @region[agent-context-setup]
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[],
middleware=[CopilotKitMiddleware()],
system_prompt=(
"You are a helpful, concise assistant. The frontend may provide "
"read-only context about the user (e.g. name, timezone, recent "
"activity) via the `useAgentContext` hook. Always consult that "
"context when it is relevant — address the user by name if known, "
"respect their timezone when mentioning times, and reference "
"recent activity when it helps you answer. Keep responses short."
),
)
# @endregion[agent-context-setup]
@@ -0,0 +1,48 @@
"""Reasoning agent — emits AG-UI REASONING_MESSAGE_* events.
Shared by reasoning-custom (custom amber ReasoningBlock) and
reasoning-default (CopilotKit's built-in reasoning slot).
Why a reasoning model + Responses API:
The OpenAI Responses API streams `response.reasoning_summary_text.delta`
items only for native reasoning models (gpt-5, o3, o4-mini, etc.).
CopilotKit's bridge translates those into AG-UI REASONING_MESSAGE_*
events with `role: "reasoning"`, which the frontend renders via the
`reasoningMessage` slot. gpt-4o / gpt-4o-mini do not emit reasoning
items, so a non-reasoning model would never light up the slot.
"""
from __future__ import annotations
import os
from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
from src.agents._header_forwarding_middleware import HeaderForwardingMiddleware
SYSTEM_PROMPT = (
"You are a helpful assistant. For each user question, first think "
"step-by-step about the approach, then give a concise answer."
)
REASONING_MODEL = os.environ.get("OPENAI_REASONING_MODEL", "gpt-5.4")
# No full CopilotKitMiddleware — this demo exercises only reasoning-token
# streaming through the OpenAI Responses API and doesn't consume frontend
# tools or app context. We still attach the minimal HeaderForwardingMiddleware
# so the inbound ``x-aimock-context`` (and other ``x-*``) headers reach the
# outgoing /v1/responses call; without it the LangGraph run swallows them
# inside ``configurable`` and aimock 404s with no fixture match. The minimal
# middleware does ONLY header propagation — no App-Context injection, no
# tool-merging, no state-surfacing.
graph = create_deep_agent(
model=init_chat_model(
f"openai:{REASONING_MODEL}",
use_responses_api=True,
reasoning={"effort": "medium", "summary": "detailed"},
),
tools=[],
system_prompt=SYSTEM_PROMPT,
middleware=[HeaderForwardingMiddleware()],
)
@@ -0,0 +1,88 @@
"""LangGraph agent for the A2UI Error Recovery demo (OSS-158 / OSS-375).
Same dynamic-schema A2UI setup as `a2ui_dynamic.py` (declarative-gen-ui), but
with the toolkit's validate->retry recovery loop made *visible*. The two
aimock pills drive the inner `render_a2ui` sub-agent two ways:
- HEAL pill: the model emits FREE-FORM / sloppy A2UI args (components and data
as JSON strings rather than structured arrays) — the toolkit heals them via
`parse_and_fix` into a valid surface in a single pass, which paints. (A
single deterministic response: no per-attempt fixture switching needed.)
- EXHAUST pill: every attempt is structurally invalid (the root references a
missing child), so the validate->retry loop hits the cap and the tool
returns the `a2ui_recovery_exhausted` hard-fail envelope, which the renderer
(`@ag-ui/a2ui-middleware`) surfaces as a tasteful `failed` state (no broken
surface).
Backend-owned wiring: unlike the declarative-gen-ui demo (which relies on the
CopilotKit runtime auto-injecting `generate_a2ui`), this agent OWNS the tool via
`ag_ui_langgraph.get_a2ui_tools`, whose body runs the `render_a2ui` sub-agent +
the toolkit recovery loop IN-GRAPH. The dedicated route sets
`injectA2UITool: false` so the runtime does not inject a second copy. Only this
backend-owned path surfaces the recovery loop + `a2ui_recovery_exhausted`
hard-fail explicitly (the runtime auto-injection path has no equivalent loop).
Mirrors `showcase/integrations/google-adk/src/agents/recovery_agent.py` (the
ADK sibling, which uses the singular `get_a2ui_tool`). Catalog is reused from
declarative-gen-ui ("declarative-gen-ui-catalog") so no new components are
introduced; the Vantage Threads sales dataset + composition rules arrive from
the frontend via App Context (declarative-gen-ui/sales-context.ts).
"""
from __future__ import annotations
import logging
import os
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from ag_ui_langgraph import get_a2ui_tools
logger = logging.getLogger(__name__)
def _log_attempt(record: dict) -> None:
"""Dev observability: log each recovery attempt (incl. rejected ones)."""
logger.info(
"[a2ui recovery] attempt %s: %s %s",
record.get("attempt"),
"valid" if record.get("ok") else "invalid",
record.get("errors"),
)
# Keep this aligned with the ADK `_INSTRUCTION` and the declarative-gen-ui
# SYSTEM_PROMPT: a sales analyst that answers every question by drawing a
# surface. `generate_a2ui` (owned by `get_a2ui_tools` below) handles the
# rendering — and its automatic recovery — internally.
SYSTEM_PROMPT = (
"You are the embedded sales analyst for Vantage Threads, the fictional "
"B2B apparel company described in your App Context. Answer every business "
"question by calling `generate_a2ui` to draw a rich visual surface, and "
"keep the chat reply to one short sentence. Ground every number in the "
"sales dataset from your App Context. `generate_a2ui` handles the "
"rendering — and its automatic recovery — for you."
)
_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o")
# Backend-owned A2UI with the recovery loop made explicit. `maxAttempts` is set
# so the renderer's "Retrying… (N/M)" label matches the adapter's cap. Recovery
# + the recovery-exhausted hard-fail are toolkit defaults; pinned here for the
# demo. Catalog/data arrive from the frontend via context (same as declarative).
graph = create_agent(
model=ChatOpenAI(model=_MODEL),
tools=[
get_a2ui_tools(
{
"model": ChatOpenAI(model=_MODEL),
"default_catalog_id": "declarative-gen-ui-catalog",
"recovery": {"maxAttempts": 3},
"on_a2ui_attempt": _log_attempt,
}
)
],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,152 @@
"""LangGraph agent backing the Shared State (Read + Write) demo.
Demonstrates the full bidirectional shared-state pattern between UI and
agent:
- **UI -> agent (write)**: The UI owns a `preferences` object (the user's
profile) that it writes into agent state via `agent.setState(...)`. A
middleware reads those preferences every turn and injects them into
the system prompt, so the LLM adapts accordingly.
- **agent -> UI (read)**: The agent can call `set_notes` to update a
`notes` slot in shared state. The UI reflects every update in real
time via `useAgent(...)`.
Together this shows the canonical LangGraph-Python bidirectional shared
state: frontend writes, backend reads AND writes, frontend re-renders.
"""
import uuid
from typing import Any, Awaitable, Callable, TypedDict
from langchain.agents import AgentState as BaseAgentState, create_agent
from langchain.agents.middleware import (
AgentMiddleware,
ModelRequest,
ModelResponse,
)
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import SystemMessage, ToolMessage
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from copilotkit import CopilotKitMiddleware
class Preferences(TypedDict, total=False):
name: str
tone: str # "formal" | "casual" | "playful"
language: str # "English", "Spanish", ...
interests: list[str]
class AgentState(BaseAgentState):
"""Bidirectional shared state between UI and agent.
- `preferences` is written by the UI (via agent.setState).
- `notes` is written by the agent (via the `set_notes` tool) and
read by the UI.
"""
preferences: Preferences
notes: list[str]
@tool
def set_notes(notes: list[str], runtime: ToolRuntime) -> Command:
"""Replace the notes array in shared state with the full updated list.
Use this tool whenever the user asks you to "remember" something, or
when you have an observation about the user worth surfacing in the
UI's notes panel. Always pass the FULL notes list (existing notes +
any new ones), not a diff. Keep each note short (< 120 chars).
"""
return Command(
update={
"notes": notes,
"messages": [
ToolMessage(
content="Notes updated.",
name="set_notes",
id=str(uuid.uuid4()),
tool_call_id=runtime.tool_call_id,
)
],
}
)
class PreferencesInjectorMiddleware(AgentMiddleware[AgentState, Any]):
"""Injects the UI-supplied `preferences` into the system prompt.
Every turn, we read the latest `preferences` from agent state and
prepend a SystemMessage that tells the LLM about them. This is how
UI-written state becomes visible to the agent.
"""
state_schema = AgentState
@property
def name(self) -> str:
return "PreferencesInjectorMiddleware"
def _build_prefs_message(self, prefs: Preferences) -> SystemMessage | None:
if not prefs:
return None
lines = ["The user has shared these preferences with you:"]
if prefs.get("name"):
lines.append(f"- Name: {prefs['name']}")
if prefs.get("tone"):
lines.append(f"- Preferred tone: {prefs['tone']}")
if prefs.get("language"):
lines.append(f"- Preferred language: {prefs['language']}")
interests = prefs.get("interests") or []
if interests:
lines.append(f"- Interests: {', '.join(interests)}")
lines.append(
"Tailor every response to these preferences. Address the user "
"by name when appropriate."
)
return SystemMessage(content="\n".join(lines))
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
prefs = request.state.get("preferences") or {}
prefs_message = self._build_prefs_message(prefs)
if prefs_message is None:
return handler(request)
return handler(request.override(messages=[prefs_message, *request.messages]))
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelResponse:
prefs = request.state.get("preferences") or {}
prefs_message = self._build_prefs_message(prefs)
if prefs_message is None:
return await handler(request)
return await handler(
request.override(messages=[prefs_message, *request.messages])
)
# @region[shared-state-setup]
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[set_notes],
middleware=[CopilotKitMiddleware(), PreferencesInjectorMiddleware()],
state_schema=AgentState,
system_prompt=(
"You are a helpful, concise assistant. "
"The user's preferences are supplied via shared state and will be "
"added as a system message at the start of every turn. Always "
"respect them. "
"When the user asks you to remember something, or when you observe "
"something worth surfacing in the UI, call `set_notes` with the "
"FULL updated list of short note strings (existing notes + new)."
),
)
# @endregion[shared-state-setup]
@@ -0,0 +1,94 @@
"""LangGraph agent backing the State Streaming demo.
Demonstrates per-token state-delta streaming. The agent writes a long
`document` string into shared agent state via a `write_document` tool;
`StateStreamingMiddleware(StateItem(...))` tells CopilotKit to forward
*every token* of the tool's `content` argument directly into the
`document` state key as it is generated. The UI (useAgent) sees
`state.document` grow token-by-token, without waiting for the tool call
to finish.
This is the canonical per-token state-streaming pattern:
docs.copilotkit.ai/integrations/langgraph/shared-state/predictive-state-updates
"""
# @region[state-streaming-middleware]
import uuid
from langchain.agents import AgentState as BaseAgentState, create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from copilotkit import (
CopilotKitMiddleware,
StateItem,
StateStreamingMiddleware,
)
class AgentState(BaseAgentState):
"""Shared state. `document` is streamed token-by-token."""
document: str
@tool
def write_document(document: str, runtime: ToolRuntime) -> Command:
"""Write a document for the user.
Always call this tool when the user asks you to write or draft
something of any length (an essay, poem, email, summary, etc.).
The `document` argument is streamed *per token* into shared agent
state under the `document` key, so the UI can render it as it is
generated.
"""
return Command(
update={
"document": document,
"messages": [
ToolMessage(
content="Document written to shared state.",
name="write_document",
id=str(uuid.uuid4()),
tool_call_id=runtime.tool_call_id,
)
],
}
)
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[write_document],
middleware=[
CopilotKitMiddleware(),
# Forward every token of write_document's `document` argument
# straight into state["document"] while the tool call is still
# streaming. Without this, `document` would only update once
# the tool call completes.
#
# NOTE: the frontend `usePredictStateSubscription` hook indexes
# the (partial-JSON-parsed) tool args by `state_key`, so the
# tool's argument name MUST match `state_key` ("document") for
# per-token deltas to land in `state.document`.
StateStreamingMiddleware(
StateItem(
state_key="document",
tool="write_document",
tool_argument="document",
)
),
],
state_schema=AgentState,
system_prompt=(
"You are a collaborative writing assistant. Whenever the user asks "
"you to write, draft, or revise any piece of text, ALWAYS call the "
"`write_document` tool with the full content as a single string in "
"the `document` argument. Never paste the document into a chat "
"message directly — the document belongs in shared state and the "
"UI renders it live as you type."
),
)
# @endregion[state-streaming-middleware]
@@ -0,0 +1,299 @@
"""LangGraph agent backing the Sub-Agents demo.
Demonstrates multi-agent delegation with a visible delegation log.
A top-level "supervisor" LLM orchestrates three specialized sub-agents,
exposed as tools:
- `research_agent` — gathers facts
- `writing_agent` — drafts prose
- `critique_agent` — reviews drafts
Each sub-agent is a full `create_agent(...)` under the hood. Every
delegation appends an entry to the `delegations` slot in shared agent
state so the UI can render a live "delegation log" as the supervisor
fans work out and collects results. This is the canonical LangGraph
sub-agents-as-tools pattern, adapted to surface delegation events to
the frontend via CopilotKit's shared-state channel.
"""
# @region[supervisor-delegation-tools]
# @region[subagent-setup]
import operator
import uuid
from typing import Annotated, Literal, TypedDict
from langchain.agents import AgentState as BaseAgentState, create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from copilotkit import CopilotKitMiddleware
from src.agents._header_forwarding_middleware import HeaderForwardingMiddleware
# ---------------------------------------------------------------------------
# Shared state
# ---------------------------------------------------------------------------
class Delegation(TypedDict):
id: str
sub_agent: Literal["research_agent", "writing_agent", "critique_agent"]
task: str
status: Literal["completed"]
result: str
# Cap the supervisor → critique sub-agent loop at a single iteration.
# Without this, the supervisor LLM occasionally re-calls `critique_agent`
# repeatedly on the same draft (visible as stacking 🧐 cards in the
# chat). The critic only adds value once per draft, so we hard-stop
# after `_MAX_CRITIQUE_ITERATIONS` invocations and return a no-op
# result.
_MAX_CRITIQUE_ITERATIONS = 1
class AgentState(BaseAgentState):
"""Shared state. `delegations` is rendered as a live log in the UI.
`delegations` uses an `operator.add` reducer so that concurrent
sub-agent emissions in the same supervisor step accumulate into a
single list instead of conflicting (LangGraph would otherwise raise
`INVALID_CONCURRENT_GRAPH_UPDATE` — "Can receive only one value per
step. Use an Annotated key to handle multiple values.").
"""
delegations: Annotated[list[Delegation], operator.add]
# ---------------------------------------------------------------------------
# Sub-agents (real LLM agents under the hood)
# ---------------------------------------------------------------------------
# Each sub-agent is a full-fledged `create_agent(...)` with its own
# system prompt. They don't share memory or tools with the supervisor —
# the supervisor only sees their return value.
_sub_model = ChatOpenAI(model="gpt-5.4")
# Each sub-agent gets the minimal HeaderForwardingMiddleware so the
# inbound x-aimock-context (and other x-*) headers from the supervisor's
# inbound HTTP request propagate to the sub-agent's outbound LLM call.
# We intentionally do NOT attach the full CopilotKitMiddleware here —
# the supervisor handles App-Context / frontend-tool injection for the
# whole run, and adding it on sub-agents would double-inject prompt
# state. Header forwarding alone is enough to keep aimock matching.
_research_agent = create_agent(
model=_sub_model,
tools=[],
system_prompt=(
"You are a research sub-agent. Given a topic, produce a concise "
"bulleted list of 3-5 key facts. No preamble, no closing."
),
middleware=[HeaderForwardingMiddleware()],
)
_writing_agent = create_agent(
model=_sub_model,
tools=[],
system_prompt=(
"You are a writing sub-agent. Given a brief and optional source "
"facts, produce a polished 1-paragraph draft. Be clear and "
"concrete. No preamble."
),
middleware=[HeaderForwardingMiddleware()],
)
_critique_agent = create_agent(
model=_sub_model,
tools=[],
system_prompt=(
"You are an editorial critique sub-agent. Given a draft, give "
"2-3 crisp, actionable critiques. No preamble."
),
middleware=[HeaderForwardingMiddleware()],
)
# @endregion[subagent-setup]
# Sentinel surfaced when a sub-agent run produces no usable text. Kept
# as a module-level constant so the harness probe (and any UI fallback)
# can match the exact phrase. The leading/trailing angle brackets keep
# it out of plausible LLM phrasing.
SUB_AGENT_EMPTY_SENTINEL = "<sub-agent produced no output>"
def _invoke_sub_agent(agent, task: str) -> str:
"""Run a sub-agent on `task` and return its final prose message."""
result = agent.invoke({"messages": [HumanMessage(content=task)]})
messages = result.get("messages", [])
# Walk newest -> oldest so we pick the answer for THIS task, not a stale
# intro. Skip empty AIMessages that only carry tool_calls.
for msg in reversed(messages):
if isinstance(msg, AIMessage):
content = msg.content
if isinstance(content, str) and content.strip():
return content
# Some providers stream content as a list of content blocks
# (e.g. {"type": "text", "text": "..."}); concatenate the text.
# The `isinstance(block.get("text"), str)` guard rejects
# `{"type": "text", "text": null}` payloads — a known provider
# quirk — that would otherwise crash `"".join(...)` with
# `TypeError: sequence item N: expected str instance, NoneType found`.
if isinstance(content, list):
parts = [
block["text"]
for block in content
if isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)
]
joined = "".join(parts).strip()
if joined:
return joined
return SUB_AGENT_EMPTY_SENTINEL
def _delegation_update(
sub_agent: str,
task: str,
result: str,
tool_call_id: str,
) -> Command:
"""Append a completed delegation entry to shared state.
Returns just the new entry (a one-element list). The reducer on
`AgentState.delegations` is `operator.add`, which concatenates the
new list with the prior state — so we must NOT echo back the
existing delegations here, or they would be duplicated each step.
"""
entry: Delegation = {
"id": str(uuid.uuid4()),
"sub_agent": sub_agent, # type: ignore[typeddict-item]
"task": task,
"status": "completed",
"result": result,
}
return Command(
update={
"delegations": [entry],
"messages": [
ToolMessage(
content=result,
name=sub_agent,
id=str(uuid.uuid4()),
tool_call_id=tool_call_id,
)
],
}
)
# ---------------------------------------------------------------------------
# Supervisor tools (each tool delegates to one sub-agent)
# ---------------------------------------------------------------------------
# Each @tool wraps a sub-agent invocation. The supervisor LLM "calls"
# these tools to delegate work; each call synchronously runs the
# matching sub-agent, records the delegation into shared state, and
# returns the sub-agent's output as a ToolMessage the supervisor can
# read on its next step.
@tool
def research_agent(task: str, runtime: ToolRuntime) -> Command:
"""Delegate a research task to the research sub-agent.
Use for: gathering facts, background, definitions, statistics.
Returns a bulleted list of key facts.
"""
result = _invoke_sub_agent(_research_agent, task)
return _delegation_update("research_agent", task, result, runtime.tool_call_id)
@tool
def writing_agent(task: str, runtime: ToolRuntime) -> Command:
"""Delegate a drafting task to the writing sub-agent.
Use for: producing a polished paragraph, draft, or summary. Pass
relevant facts from prior research inside `task`.
"""
result = _invoke_sub_agent(_writing_agent, task)
return _delegation_update("writing_agent", task, result, runtime.tool_call_id)
@tool
def critique_agent(task: str, runtime: ToolRuntime) -> Command:
"""Delegate a critique task to the critique sub-agent.
Use for: reviewing a draft and suggesting concrete improvements.
Capped at `_MAX_CRITIQUE_ITERATIONS` invocations per supervisor run
— the supervisor LLM occasionally re-calls the critic in a loop and
each rerun produces near-identical output, so additional calls are
short-circuited with a no-op result that nudges the supervisor to
finish.
"""
state: AgentState = runtime.state # type: ignore[assignment]
delegations = state.get("delegations") or []
prior_critiques = sum(
1 for d in delegations if d.get("sub_agent") == "critique_agent"
)
if prior_critiques >= _MAX_CRITIQUE_ITERATIONS:
# Short-circuit without appending another delegation entry — the
# UI renders one card per delegation and we want exactly one
# critic card per supervisor run, even if the LLM ignores the
# system prompt and re-issues the call.
skip_message = (
"Critique already produced for this run. "
"Stop calling critique_agent and return your final answer "
"to the user now."
)
return Command(
update={
"messages": [
ToolMessage(
content=skip_message,
name="critique_agent",
id=str(uuid.uuid4()),
tool_call_id=runtime.tool_call_id,
)
],
}
)
result = _invoke_sub_agent(_critique_agent, task)
return _delegation_update("critique_agent", task, result, runtime.tool_call_id)
# @endregion[supervisor-delegation-tools]
# ---------------------------------------------------------------------------
# Supervisor (the graph we export)
# ---------------------------------------------------------------------------
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[research_agent, writing_agent, critique_agent],
middleware=[CopilotKitMiddleware()],
state_schema=AgentState,
system_prompt=(
"You are a supervisor agent that coordinates three specialized "
"sub-agents to produce high-quality deliverables.\n\n"
"Available sub-agents (call them as tools):\n"
" - research_agent: gathers facts on a topic.\n"
" - writing_agent: turns facts + a brief into a polished draft.\n"
" - critique_agent: reviews a draft and suggests improvements.\n\n"
"For every non-trivial user request, delegate in sequence: "
"research_agent -> writing_agent -> critique_agent. "
"IMPORTANT: call EACH sub-agent EXACTLY ONCE per user request. "
"After critique_agent returns, do NOT call any sub-agent "
"again — return a concise final answer to the user that "
"incorporates the critique. Pass the relevant facts/draft "
"through the `task` argument of each tool. Keep your own "
"messages short — explain the plan once, delegate, then return "
"a concise summary once done. The UI shows the user a live log "
"of every sub-agent delegation."
),
)
@@ -0,0 +1,43 @@
"""Regression test for A2UI tool naming.
History: beautiful_chat.py and a2ui_dynamic.py used to hand-roll their own
dynamic-A2UI tool with an internal secondary-LLM helper named
`_design_a2ui_surface`. The rename away from `render_a2ui` was forced because
`@ag-ui/a2ui-middleware`'s default `a2uiToolNames` is `["render_a2ui"]` — any
tool by that name has its streaming args parsed into A2UI surface ops by the
middleware, bypassing the tool's own body.
Both agents now use the canonical path instead: the runtime injects
`generate_a2ui` (`a2ui.injectA2UITool: true`) and the `get_a2ui_tools` factory's
inner `render_a2ui` tool is intentionally intercepted by the middleware — that
interception IS the supported render mechanism, and catalog force-pinning /
malformed-root handling now live inside the factory (ag_ui_langgraph), not here.
So the old hand-rolled internal tool no longer exists to regression-test.
What still matters: the FIXED-schema demo (a2ui_fixed.py) owns an OUTER tool the
primary agent calls directly. If it were ever named into the middleware's
intercept list, the fixed-schema surface would render from raw streaming args
instead of the tool's validated output — the same class of bug. Guard that name.
"""
from __future__ import annotations
# `display_flight` is the OUTER tool the primary agent calls in the fixed-schema
# demo. Its name must stay out of the middleware's intercept list.
from src.agents.a2ui_fixed import display_flight as a2ui_fixed_display_flight
# A2UI middleware's default intercept list (mirrors `RENDER_A2UI_TOOL_NAME` in
# `@ag-ui/a2ui-middleware`). Any tool here gets its streaming args parsed as
# A2UI surface ops, bypassing the tool's own body.
A2UI_MIDDLEWARE_INTERCEPTED_NAMES = {"render_a2ui"}
def test_a2ui_fixed_display_flight_name_unchanged():
"""`display_flight` is the OUTER tool the primary agent calls. Renaming it
to anything in the middleware's intercept list would break the fixed-schema
demo by rendering from raw streaming args instead of the tool's output."""
assert a2ui_fixed_display_flight.name not in A2UI_MIDDLEWARE_INTERCEPTED_NAMES, (
f"a2ui_fixed.display_flight is named {a2ui_fixed_display_flight.name!r}, "
f"which collides with the A2UI middleware's intercept list."
)
@@ -0,0 +1,115 @@
"""Unit tests for agent_config_agent's prompt builder + defensive defaults."""
from src.agents.agent_config_agent import (
DEFAULT_EXPERTISE,
DEFAULT_RESPONSE_LENGTH,
DEFAULT_TONE,
build_system_prompt,
read_properties,
)
def test_read_properties_returns_defaults_when_missing():
result = read_properties(None)
assert result == {
"tone": DEFAULT_TONE,
"expertise": DEFAULT_EXPERTISE,
"response_length": DEFAULT_RESPONSE_LENGTH,
}
def test_read_properties_returns_defaults_when_configurable_missing():
result = read_properties({})
assert result == {
"tone": DEFAULT_TONE,
"expertise": DEFAULT_EXPERTISE,
"response_length": DEFAULT_RESPONSE_LENGTH,
}
def test_read_properties_returns_defaults_when_properties_missing():
result = read_properties({"configurable": {}})
assert result == {
"tone": DEFAULT_TONE,
"expertise": DEFAULT_EXPERTISE,
"response_length": DEFAULT_RESPONSE_LENGTH,
}
def test_read_properties_accepts_valid_values():
result = read_properties(
{
"configurable": {
"properties": {
"tone": "enthusiastic",
"expertise": "expert",
"responseLength": "detailed",
}
}
}
)
assert result == {
"tone": "enthusiastic",
"expertise": "expert",
"response_length": "detailed",
}
def test_read_properties_rejects_invalid_tone_to_default():
result = read_properties({"configurable": {"properties": {"tone": "sinister"}}})
assert result["tone"] == DEFAULT_TONE
def test_read_properties_rejects_invalid_expertise_to_default():
result = read_properties({"configurable": {"properties": {"expertise": "ninja"}}})
assert result["expertise"] == DEFAULT_EXPERTISE
def test_read_properties_rejects_invalid_length_to_default():
result = read_properties(
{"configurable": {"properties": {"responseLength": "epic"}}}
)
assert result["response_length"] == DEFAULT_RESPONSE_LENGTH
def test_read_properties_mixes_valid_and_invalid():
result = read_properties(
{
"configurable": {
"properties": {
"tone": "casual",
"expertise": "unknown",
"responseLength": "detailed",
}
}
}
)
assert result == {
"tone": "casual",
"expertise": DEFAULT_EXPERTISE,
"response_length": "detailed",
}
def test_build_system_prompt_mentions_each_axis():
prompt = build_system_prompt("casual", "expert", "detailed")
assert "Tone:" in prompt
assert "Expertise level:" in prompt
assert "Response length:" in prompt
assert "friendly" in prompt.lower()
assert "technical fluency" in prompt.lower()
assert "multiple paragraphs" in prompt.lower()
def test_build_system_prompt_professional_beginner_concise():
prompt = build_system_prompt("professional", "beginner", "concise")
assert "neutral, precise language" in prompt.lower()
assert "assume no prior knowledge" in prompt.lower()
assert "1-3 sentences" in prompt.lower()
def test_build_system_prompt_enthusiastic_intermediate_concise():
prompt = build_system_prompt("enthusiastic", "intermediate", "concise")
assert "upbeat" in prompt.lower()
assert "specialized terms" in prompt.lower()
assert "1-3 sentences" in prompt.lower()
@@ -0,0 +1,137 @@
"""
LangGraph agent for the CopilotKit Tool Rendering demos.
Backs the three tool-rendering cells:
- tool-rendering-default-catchall (no frontend renderers)
- tool-rendering-custom-catchall (wildcard renderer on frontend)
- tool-rendering (per-tool + catch-all on frontend)
All three share this backend — they differ only in how the frontend
renders the same tool calls. The `tool-rendering-reasoning-chain` cell
has its own agent (`tool_rendering_reasoning_chain_agent.py`) because
it routes through the OpenAI Responses API for reasoning streaming.
"""
# @region[weather-tool-backend]
from random import choice, randint
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitMiddleware
# Multi-tool-per-question prompt.
#
# This backend serves the tool-rendering demos, whose JOB is to show the
# rendering patterns (per-tool, catch-all, default fallback). The agent
# may call multiple tools per turn when the user asks for them. The
# `roll_d20` tool accepts a deterministic `value` parameter so the
# aimock fixtures can script the exact dice sequence the e2e tests
# assert against.
SYSTEM_PROMPT = (
"You are a travel & lifestyle concierge. Use the mock tools for "
"weather, flights, stock prices, or d20 rolls when the user asks; "
"otherwise reply in plain text. For flights, default origin to 'SFO' "
"if the user only names a destination. Call multiple tools in one "
"turn if asked. After tools return, summarize in one short sentence. "
"Never fabricate data a tool could provide."
)
@tool
def get_weather(location: str) -> dict:
"""Get the current weather for a given location."""
return {
"city": location,
"temperature": 68,
"humidity": 55,
"wind_speed": 10,
"conditions": "Sunny",
}
# @endregion[weather-tool-backend]
@tool
def search_flights(origin: str, destination: str) -> dict:
"""Search mock flights from an origin airport to a destination airport."""
return {
"origin": origin,
"destination": destination,
"flights": [
{
"airline": "United",
"flight": "UA231",
"depart": "08:15",
"arrive": "16:45",
"price_usd": 348,
},
{
"airline": "Delta",
"flight": "DL412",
"depart": "11:20",
"arrive": "19:55",
"price_usd": 312,
},
{
"airline": "JetBlue",
"flight": "B6722",
"depart": "17:05",
"arrive": "01:30",
"price_usd": 289,
},
],
}
@tool
def get_stock_price(
ticker: str,
price_usd: float | None = None,
change_pct: float | None = None,
) -> dict:
"""Get a mock current price for a stock ticker.
The optional `price_usd` and `change_pct` arguments let the LLM (or
aimock fixture) script a deterministic ticker quote for testing —
when supplied, the tool echoes them back verbatim. When omitted (or
`None`), the tool returns mock random values. Mirrors the
deterministic-`value` pattern on `roll_d20`.
"""
return {
"ticker": ticker.upper(),
"price_usd": (
round(float(price_usd), 2)
if price_usd is not None
else round(100 + randint(0, 400) + randint(0, 99) / 100, 2)
),
"change_pct": (
round(float(change_pct), 2)
if change_pct is not None
else round(choice([-1, 1]) * (randint(0, 300) / 100), 2)
),
}
@tool
def roll_d20(value: int = 0) -> dict:
"""Roll a 20-sided die.
The `value` argument lets the LLM (or aimock fixture) script a
deterministic roll for testing — the tool simply echoes it back as
the result. When called without `value` (or with 0), the tool
returns a random natural d20 roll.
"""
rolled = value if isinstance(value, int) and 1 <= value <= 20 else randint(1, 20)
return {"sides": 20, "value": rolled, "result": rolled}
model = ChatOpenAI(model="gpt-5.4")
graph = create_agent(
model=model,
tools=[get_weather, search_flights, get_stock_price, roll_d20],
middleware=[CopilotKitMiddleware()],
system_prompt=SYSTEM_PROMPT,
)
@@ -0,0 +1,149 @@
"""Tool Rendering (Reasoning Chain) — minimal deep agent with tools.
Routes through a reasoning-capable OpenAI model via the Responses API
so the chain of thought streams as AG-UI REASONING_MESSAGE_* events
alongside the tool calls. See `reasoning_agent.py` for the rationale.
"""
from __future__ import annotations
import os
from random import choice, randint
from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from src.agents._header_forwarding_middleware import HeaderForwardingMiddleware
@tool
def get_weather(location: str) -> dict:
"""Get the current weather for a given location."""
return {
"city": location,
"temperature": 68,
"humidity": 55,
"wind_speed": 10,
"conditions": "Sunny",
}
@tool
def search_flights(origin: str, destination: str) -> dict:
"""Search mock flights from an origin airport to a destination airport."""
return {
"origin": origin,
"destination": destination,
"flights": [
{
"airline": "United",
"flight": "UA231",
"depart": "08:15",
"arrive": "16:45",
"price_usd": 348,
},
{
"airline": "Delta",
"flight": "DL412",
"depart": "11:20",
"arrive": "19:55",
"price_usd": 312,
},
{
"airline": "JetBlue",
"flight": "B6722",
"depart": "17:05",
"arrive": "01:30",
"price_usd": 289,
},
],
}
@tool
def get_stock_price(
ticker: str,
price_usd: float | None = None,
change_pct: float | None = None,
) -> dict:
"""Get a mock current price for a stock ticker.
The optional `price_usd` and `change_pct` arguments let the LLM (or
aimock fixture) script a deterministic ticker quote for testing —
when supplied, the tool echoes them back verbatim. Mirrors the
basic tool-rendering agent's signature so the aimock fixtures shared
across both demos can script chained AAPL/MSFT comparisons.
"""
return {
"ticker": ticker.upper(),
"price_usd": (
round(float(price_usd), 2)
if price_usd is not None
else round(100 + randint(0, 400) + randint(0, 99) / 100, 2)
),
"change_pct": (
round(float(change_pct), 2)
if change_pct is not None
else round(choice([-1, 1]) * (randint(0, 300) / 100), 2)
),
}
@tool
def roll_dice(sides: int = 6) -> dict:
"""Roll a single die with the given number of sides."""
return {"sides": sides, "result": randint(1, max(2, sides))}
SYSTEM_PROMPT = (
"You are a helpful travel & lifestyle concierge with mock tools for "
"weather, flights, stock prices, and dice rolls — they all return "
"fake data, so call them liberally.\n\n"
"Your habit is to CHAIN tools when one answer naturally invites "
"another. For a single user question, call at least TWO tools in "
"succession when the topic allows, then compose your final reply. "
"Default chains:\n"
" - 'What's the weather in <city>?' -> call get_weather(<city>), "
"then call search_flights(origin='SFO', destination=<city>) so the "
"user also sees how to get there.\n"
" - 'How is <ticker> doing?' -> call get_stock_price(<ticker>), "
"then call get_stock_price on a comparable ticker (e.g. 'MSFT' or "
"'GOOGL') so the user can compare.\n"
" - 'Roll a 20-sided die' -> call roll_dice(sides=20), then call "
"roll_dice again with a different number of sides so the user sees "
"a contrast.\n"
" - 'Find flights from <a> to <b>' -> call search_flights(a, b), "
"then call get_weather(<b>) for the destination.\n\n"
"Only skip chaining when the user has clearly asked for a single, "
"atomic answer and more tool calls would feel intrusive. Never "
"fabricate data that a tool could provide."
)
REASONING_MODEL = os.environ.get("OPENAI_REASONING_MODEL", "gpt-5.4")
# No full CopilotKitMiddleware — this demo combines reasoning-token streaming
# with backend tool rendering, but doesn't consume any frontend tools or app
# context. We still attach the minimal HeaderForwardingMiddleware so inbound
# ``x-aimock-context`` (and other ``x-*``) headers reach the outgoing
# /v1/responses call; without it the LangGraph run swallows them inside
# ``configurable`` and aimock 404s with no fixture match. The minimal
# middleware does ONLY header propagation — no App-Context injection, no
# tool-merging, no state-surfacing.
graph = create_deep_agent(
model=init_chat_model(
f"openai:{REASONING_MODEL}",
use_responses_api=True,
# `summary: "detailed"` forces reasoning-summary emission on every
# response. The previous `"auto"` lets the model decide, and with
# tools present the model often skips reasoning summaries entirely
# (the chain-of-thought goes straight to a tool call without the
# summary step). That breaks the `<ReasoningBlock>` mount because
# no reasoning-role message lands. Match the working
# `reasoning_agent.py` config: medium effort + detailed summary.
reasoning={"effort": "medium", "summary": "detailed"},
),
tools=[get_weather, search_flights, get_stock_price, roll_dice],
system_prompt=SYSTEM_PROMPT,
middleware=[HeaderForwardingMiddleware()],
)