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,11 @@
**/node_modules
.next
__pycache__
*.pyc
.git
**/vitest.config.ts
**/test-setup.ts
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
@@ -0,0 +1,9 @@
# API Keys (shared across integrations)
OPENAI_API_KEY=replace-with-your-key
ANTHROPIC_API_KEY=replace-with-your-key
# Agent backend URL (for the CopilotKit runtime proxy)
AGENT_URL=http://localhost:8000
# Showcase
NEXT_PUBLIC_BASE_URL=http://localhost:3000
@@ -0,0 +1,13 @@
node_modules/
.next/
.env.local
.env
*.pyc
__pycache__/
.venv/
dist/
playwright-report/
test-results/
# Shared modules (copied by CI)
shared_frontend/
@@ -0,0 +1,83 @@
# Stage 1: Build Next.js frontend
FROM node:22-slim AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps
COPY src/ ./src/
COPY public/ ./public/
COPY next.config.ts tsconfig.json postcss.config.mjs ./
RUN npm run build
# Stage 2: Build Python venv with agent deps
#
# True multi-stage split — the builder carries full `python:3.12` (compilers,
# build-essential, dev headers needed to compile wheels that don't ship
# pre-built for ``-slim``) and produces a self-contained ``/opt/venv`` that
# the runner COPYs in as a single opaque tree. The runner never runs
# ``pip install`` itself, so no compiler toolchain bloats the final image.
FROM python:3.12.13 AS agent-builder
WORKDIR /agent
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY requirements.txt ./requirements.txt
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
# Stage 3: Production image with Node.js + Python (runtime only — no pip,
# no build tools). Node.js is installed via NodeSource because the package
# runs BOTH Next.js (frontend) and the Python agent inside this single image;
# entrypoint.sh orchestrates both processes.
FROM python:3.12.13-slim AS runner
RUN apt-get update && apt-get install -y --no-install-recommends \
curl && \
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Create unprivileged runtime user BEFORE any COPY so --chown resolves
# by name and so recursive chown over /app is never needed (fast builds).
# Mirrors the starter Dockerfile pattern for parity — Railway / any
# platform that enforces non-root by policy needs this from the package
# image too, not just the generated starter.
RUN (groupadd --system --gid 1001 app 2>/dev/null || true) \
&& (useradd --system --uid 1001 --gid 1001 --no-create-home app 2>/dev/null || true) \
&& mkdir -p /home/app && chown app:app /home/app
# Python venv (prebuilt in agent-builder stage — no pip in the runner).
COPY --chown=app:app --from=agent-builder /opt/venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
# Next.js build artifacts
COPY --chown=app:app --from=frontend /app/.next ./.next
COPY --chown=app:app --from=frontend /app/node_modules ./node_modules
COPY --chown=app:app --from=frontend /app/package.json ./
COPY --chown=app:app --from=frontend /app/public ./public
# Agent code
COPY --chown=app:app src/agent_server.py ./
COPY --chown=app:app src/agents/ ./agents/
# Shared Python tools (dereferenced from symlink by CI)
COPY --chown=app:app tools/ /app/tools/
# Shared CVDIAG bootstrap module (single source: showcase/integrations/_shared;
# symlinked in source, dereferenced into this build context by the harness
# stage_shared step). Lands at /app/_shared; /app is already on PYTHONPATH.
COPY --chown=app:app _shared/ ./_shared/
ENV PYTHONPATH=/app
# Entrypoint
COPY --chown=app:app entrypoint.sh ./
RUN chmod +x entrypoint.sh
USER app
EXPOSE 10000
# Intentionally NOT setting `ENV NODE_ENV=production` at the image level.
# NODE_ENV=production at the image level would leak into every child process
# (Python agent, shell scripts, healthchecks) — most of which don't use it
# the way Next.js does. entrypoint.sh scopes NODE_ENV=production to the
# Next.js invocation only so non-Next children see the host's environment.
CMD ["./entrypoint.sh"]
+1
View File
@@ -0,0 +1 @@
../_shared
@@ -0,0 +1,41 @@
{
"framework": "ms-agent-python",
"features": {
"agentic-chat": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/quickstart",
"shell_docs_path": "/prebuilt-components"
},
"hitl-in-chat": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/human-in-the-loop",
"shell_docs_path": "/generative-ui/your-components/interactive"
},
"tool-rendering": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/generative-ui/tool-rendering",
"shell_docs_path": "/generative-ui/tool-rendering"
},
"gen-ui-tool-based": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/generative-ui/your-components/display-only",
"shell_docs_path": "/generative-ui/your-components/display-only"
},
"gen-ui-agent": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/generative-ui/state-rendering",
"shell_docs_path": "/generative-ui/state-rendering"
},
"shared-state-read-write": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/shared-state/in-app-agent-write",
"shell_docs_path": "/shared-state"
},
"shared-state-streaming": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/shared-state/predictive-state-updates",
"shell_docs_path": "/shared-state"
},
"subagents": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework",
"shell_docs_path": "/multi-agent/subagents"
},
"auth": {
"og_docs_url": "https://docs.copilotkit.ai/microsoft-agent-framework/auth",
"shell_docs_path": "/auth"
}
}
}
+108
View File
@@ -0,0 +1,108 @@
#!/bin/bash
set -e
cleanup() {
kill $AGENT_PID $NEXTJS_PID $WATCHDOG_PID 2>/dev/null || true
}
trap cleanup EXIT
# Disable Python stdout buffering so the FastAPI/uvicorn agent flushes
# tracebacks and log lines immediately. Without this a silent crash during
# module import can sit in Python's userspace buffer until the process
# exits, by which point the container is already gone.
export PYTHONUNBUFFERED=1
echo "========================================="
echo "[entrypoint] Starting showcase package: ms-agent-python"
echo "[entrypoint] Time: $(date -u)"
echo "[entrypoint] PORT=${PORT:-not set}"
echo "[entrypoint] NODE_ENV=${NODE_ENV:-not set}"
echo "========================================="
if [ -z "$OPENAI_API_KEY" ]; then
echo "[entrypoint] WARNING: OPENAI_API_KEY is not set! Agent will fail."
else
echo "[entrypoint] OPENAI_API_KEY: set (${#OPENAI_API_KEY} chars)"
fi
# Start agent backend on :8000 with log prefixing so its output is
# distinguishable from Next.js in the Railway log stream.
#
# Belt-and-suspenders log flushing: `PYTHONUNBUFFERED=1` above exports the env
# var, but a child process could in principle un-export or override it. The
# `-u` flag to the Python interpreter forces unbuffered stdout/stderr at the
# interpreter level and is not overridable by user code. Combined with the
# `fflush()` inside the awk pipe below, this guarantees uvicorn request lines
# and tracebacks reach Railway's log stream line-at-a-time rather than
# block-buffered in pipe buffers.
echo "[entrypoint] Starting Python agent on port 8000..."
python -u -m uvicorn agent_server:app --host 0.0.0.0 --port 8000 &> >(awk '{print "[agent] " $0; fflush()}') &
AGENT_PID=$!
sleep 2
if kill -0 $AGENT_PID 2>/dev/null; then
echo "[entrypoint] Agent started (PID: $AGENT_PID)"
else
echo "[entrypoint] ERROR: Agent failed to start — exiting"
exit 1
fi
echo "========================================="
echo "[entrypoint] Starting Next.js frontend on port ${PORT:-10000}..."
echo "========================================="
PORT=${PORT:-10000}
# Scope NODE_ENV=production to the Next.js invocation ONLY, not the whole
# container environment. `ENV NODE_ENV=production` at the image level would
# leak into every child process (Python agent, shell, healthchecks). `env`
# prefix binds the value to this single exec.
env NODE_ENV=production npx next start --port $PORT &> >(awk '{print "[nextjs] " $0; fflush()}') &
NEXTJS_PID=$!
echo "[entrypoint] Next.js started (PID: $NEXTJS_PID)"
# Watchdog: Railway deploys of showcase packages have been observed to hit a
# silent agent hang — the Python process stays alive (so `wait -n` never
# fires and the container never restarts) but stops responding on :8000.
# Poll the agent's /health endpoint every 30s; after 3 consecutive failures
# (90s of unreachable agent), kill the agent process so `wait -n` returns
# and Railway restarts the container. We kill the agent (not the whole
# script) first so `set -e` + `wait -n; exit $?` handles the restart
# through the normal path rather than a forced `exit` that would bypass
# logging. Generalized from showcase/integrations/crewai-crews/entrypoint.sh
# (PRs #4114 + #4115).
(
FAILS=0
while sleep 30; do
if ! kill -0 $AGENT_PID 2>/dev/null; then
# Agent already dead — wait -n in the main shell will handle it.
break
fi
if curl -fsS --max-time 5 http://127.0.0.1:8000/health > /dev/null 2>&1; then
FAILS=0
else
FAILS=$((FAILS + 1))
echo "[watchdog] Agent health probe failed (count=$FAILS)"
if [ $FAILS -ge 3 ]; then
echo "[watchdog] Agent unresponsive for ~90s — killing PID $AGENT_PID to trigger container restart"
kill -9 $AGENT_PID 2>/dev/null || true
break
fi
fi
done
) &
WATCHDOG_PID=$!
echo "[entrypoint] Watchdog started (PID: $WATCHDOG_PID)"
echo "[entrypoint] All processes running. Waiting..."
wait -n $AGENT_PID $NEXTJS_PID
EXIT_CODE=$?
if ! kill -0 $AGENT_PID 2>/dev/null; then
echo "[entrypoint] Agent (PID: $AGENT_PID) exited with code $EXIT_CODE"
elif ! kill -0 $NEXTJS_PID 2>/dev/null; then
echo "[entrypoint] Next.js (PID: $NEXTJS_PID) exited with code $EXIT_CODE"
else
echo "[entrypoint] A process exited with code $EXIT_CODE"
fi
exit $EXIT_CODE
@@ -0,0 +1,547 @@
name: MS Agent Framework (Python)
slug: ms-agent-python
category: enterprise-platform
language: python
logo: /logos/ms-agent-python.svg
description: CopilotKit integration with MS Agent Framework (Python)
partner_docs: null
repo: https://github.com/CopilotKit/CopilotKit/tree/main/showcase/integrations/ms-agent-python
copilotkit_version: 2.0.0
deployed: true
docs_mode: authored
sort_order: 150
generative_ui:
- constrained-explicit
- a2ui-fixed-schema
- a2ui-dynamic-schema
interaction_modalities:
- sidebar
- embedded
- chat
not_supported_features:
- shared-state-streaming
# QUARANTINED pending a @copilotkit/react-core release. gen-ui-interrupt /
# interrupt-headless fail turn-2 on a useInterrupt/useHeadlessInterrupt
# RESUME-PATH hook bug in @copilotkit/react-core/v2: backend resumes + streams
# (HTTP 200) but the frontend never appends the confirmation assistant bubble,
# so the harness DOM settle-check times out. Fix is a published-package change
# (out of scope here); honestly marked not-supported (skipped-incapable, not
# green, not red) rather than a regression. Demos remain wired below.
- gen-ui-interrupt
- interrupt-headless
- reasoning-default-render
- agentic-chat-reasoning
- tool-rendering-reasoning-chain
features:
- beautiful-chat
- cli-start
- agentic-chat
- prebuilt-sidebar
- prebuilt-popup
- chat-slots
- chat-customization-css
- headless-simple
- headless-complete
- reasoning-custom
- reasoning-default
- frontend-tools
- frontend-tools-async
- hitl
- hitl-in-app
- hitl-in-chat
- gen-ui-tool-based
- declarative-gen-ui
- a2ui-fixed-schema
- mcp-apps
- gen-ui-agent
- tool-rendering-default-catchall
- tool-rendering-custom-catchall
- tool-rendering
- shared-state-read-write
- subagents
- readonly-state-agent-context
- multimodal
- auth
- declarative-hashbrown
- declarative-json-render
- open-gen-ui
- open-gen-ui-advanced
- voice
- agent-config
- shared-state-read
a2ui_pattern: schema-loading
interrupt_pattern: promise-based
agent_config_pattern: shared-state
auth_pattern: microsoft-agent-framework
demos:
- id: cli-start
name: CLI Start Command
description: Copy-paste command to clone the canonical starter
tags:
- chat-ui
command: "npx copilotkit@latest init --framework ms-agent-python"
- id: agentic-chat
name: Agentic Chat
description: Natural conversation with frontend tool execution
tags:
- chat-ui
route: /demos/agentic-chat
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/agentic-chat/page.tsx
- src/app/api/copilotkit/route.ts
- id: chat-customization-css
name: Chat Customization (CSS)
description: Default CopilotChat re-themed via CopilotKitCSSProperties
tags:
- chat-ui
route: /demos/chat-customization-css
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/chat-customization-css/page.tsx
- src/app/demos/chat-customization-css/theme.css
- src/app/api/copilotkit/route.ts
- id: tool-rendering-default-catchall
name: Tool Rendering (Default Catch-all)
description: Out-of-the-box tool rendering — backend defines the tools; the frontend adds zero custom renderers and relies on CopilotKit's built-in default UI
tags:
- generative-ui
route: /demos/tool-rendering-default-catchall
animated_preview_url:
highlight:
- src/app/demos/tool-rendering-default-catchall/page.tsx
- src/agents/agent.py
- src/app/api/copilotkit/route.ts
- id: tool-rendering-custom-catchall
name: Tool Rendering (Custom Catch-all)
description: Single branded wildcard renderer via useDefaultRenderTool — the same app-designed card paints every tool call
tags:
- generative-ui
route: /demos/tool-rendering-custom-catchall
animated_preview_url:
highlight:
- src/app/demos/tool-rendering-custom-catchall/page.tsx
- src/app/demos/tool-rendering-custom-catchall/custom-catchall-renderer.tsx
- src/agents/agent.py
- src/app/api/copilotkit/route.ts
- id: frontend-tools
name: Frontend Tools (In-App Actions)
description: Agent invokes client-side handlers registered with useFrontendTool
tags:
- interactivity
route: /demos/frontend-tools
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/frontend-tools/page.tsx
- src/app/api/copilotkit/route.ts
- id: frontend-tools-async
name: Frontend Tools (Async)
description: useFrontendTool with an async handler — agent awaits a client-side async operation (simulated notes DB query) and uses the returned result
tags:
- interactivity
route: /demos/frontend-tools-async
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/frontend-tools-async/page.tsx
- src/app/demos/frontend-tools-async/notes-card.tsx
- src/app/api/copilotkit/route.ts
- id: hitl
name: In-Chat Human in the Loop
description: User approves agent actions before execution
tags:
- interactivity
route: /demos/hitl
animated_preview_url:
- id: hitl-in-app
name: In-App Human in the Loop (Frontend Tools + async HITL)
description: Agent requests approval via useFrontendTool with an async handler; the approval UI pops up as an app-level modal OUTSIDE the chat, and a completion callback resolves the pending tool Promise with the user's decision
tags:
- interactivity
route: /demos/hitl-in-app
animated_preview_url:
highlight:
- src/agents/hitl_in_app_agent.py
- src/app/demos/hitl-in-app/page.tsx
- src/app/demos/hitl-in-app/approval-dialog.tsx
- src/app/api/copilotkit/route.ts
- id: hitl-in-chat
name: In-Chat HITL (useHumanInTheLoop — ergonomic API)
description: Interactive approval/decision surface rendered inline in the chat via the high-level `useHumanInTheLoop` hook
tags:
- interactivity
route: /demos/hitl-in-chat
animated_preview_url:
highlight:
- src/agents/hitl_in_chat_agent.py
- src/app/demos/hitl-in-chat/page.tsx
- src/app/demos/hitl-in-chat/time-picker-card.tsx
- src/app/api/copilotkit/route.ts
- id: gen-ui-tool-based
name: Tool-Based Generative UI
description: Agent uses tools to trigger UI generation
tags:
- generative-ui
route: /demos/gen-ui-tool-based
animated_preview_url:
highlight:
- src/agents/gen_ui_tool_based_agent.py
- src/app/demos/gen-ui-tool-based/page.tsx
- src/app/demos/gen-ui-tool-based/bar-chart.tsx
- src/app/demos/gen-ui-tool-based/pie-chart.tsx
- src/app/api/copilotkit/route.ts
- id: tool-rendering
name: Tool Rendering
description: Backend agent tools rendered as UI components
tags:
- agent-capabilities
route: /demos/tool-rendering
animated_preview_url:
highlight:
- src/agents/agent.py
- tools/get_weather.py
- tools/query_data.py
- tools/schedule_meeting.py
- tools/search_flights.py
- src/app/demos/tool-rendering/page.tsx
- id: gen-ui-agent
name: Agentic Generative UI
description: Long-running agent tasks with generated UI
tags:
- generative-ui
route: /demos/gen-ui-agent
animated_preview_url:
- id: shared-state-read-write
name: Shared State (Read + Write)
description: Bidirectional agent state — UI writes preferences, agent writes notes back via state_update
tags:
- agent-state
route: /demos/shared-state-read-write
animated_preview_url:
highlight:
- src/agents/shared_state_read_write_agent.py
- src/app/demos/shared-state-read-write/page.tsx
- src/app/demos/shared-state-read-write/preferences-card.tsx
- src/app/demos/shared-state-read-write/notes-card.tsx
- src/app/api/copilotkit/route.ts
- id: subagents
name: Sub-Agents
description: Supervisor delegates to research / writing / critique sub-agents with a live delegation log
tags:
- multi-agent
route: /demos/subagents
animated_preview_url:
highlight:
- src/agents/subagents_agent.py
- src/app/demos/subagents/page.tsx
- src/app/demos/subagents/delegation-log.tsx
- src/app/api/copilotkit/route.ts
- id: shared-state-streaming
name: State Streaming
description: Per-token state delta streaming from agent to UI
tags:
- agent-state
route: /demos/shared-state-streaming
animated_preview_url:
- id: prebuilt-sidebar
name: "Pre-Built: Sidebar"
description: Docked sidebar chat via <CopilotSidebar />
tags:
- chat-ui
route: /demos/prebuilt-sidebar
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/prebuilt-sidebar/page.tsx
- src/app/api/copilotkit/route.ts
- id: prebuilt-popup
name: "Pre-Built: Popup"
description: Floating popup chat via <CopilotPopup />
tags:
- chat-ui
route: /demos/prebuilt-popup
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/prebuilt-popup/page.tsx
- src/app/api/copilotkit/route.ts
- id: chat-slots
name: Chat Customization (Slots)
description: Customize CopilotChat via its slot system
tags:
- chat-ui
route: /demos/chat-slots
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/chat-slots/page.tsx
- src/app/demos/chat-slots/slot-wrappers.tsx
- src/app/api/copilotkit/route.ts
- id: headless-simple
name: Headless Chat (Simple)
description: Minimal custom chat surface built on useAgent
tags:
- chat-ui
route: /demos/headless-simple
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/headless-simple/page.tsx
- src/app/api/copilotkit/route.ts
- id: headless-complete
name: Headless Chat (Complete)
description: Full chat implementation built from scratch on useAgent
tags:
- chat-ui
route: /demos/headless-complete
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/headless-complete/page.tsx
- src/app/demos/headless-complete/chat/chat.tsx
- src/app/demos/headless-complete/hooks/use-tool-renderers.tsx
- src/app/demos/headless-complete/hooks/use-frontend-components.ts
- src/app/demos/headless-complete/hooks/use-headless-suggestions.ts
- src/app/demos/headless-complete/attachments/use-attachments-config.ts
- src/app/api/copilotkit/route.ts
- id: reasoning-default
name: "Reasoning: Default"
description: Built-in CopilotChatReasoningMessage rendering with no slot override.
tags:
- chat-ui
route: /demos/reasoning-default
highlight:
- src/app/demos/reasoning-default/page.tsx
- src/app/demos/reasoning-default/suggestions.ts
- src/agents/reasoning_agent.py
- id: reasoning-custom
name: "Reasoning: Custom"
description: Visible reasoning/thinking chain alongside the final answer
tags:
- chat-ui
route: /demos/reasoning-custom
animated_preview_url:
highlight:
- src/app/demos/reasoning-custom/page.tsx
- src/app/demos/reasoning-custom/reasoning-block.tsx
- src/agents/reasoning_agent.py
- src/app/api/copilotkit/route.ts
- id: gen-ui-interrupt
name: In-Chat HITL (useInterrupt — low-level primitive)
description: Interactive component rendered inline in the chat via the lower-level `useInterrupt` primitive — direct control over the interrupt lifecycle
tags:
- generative-ui
route: /demos/gen-ui-interrupt
animated_preview_url:
highlight:
- src/agents/interrupt_agent.py
- src/app/demos/gen-ui-interrupt/page.tsx
- src/app/demos/gen-ui-interrupt/_components/time-picker-card.tsx
- src/app/api/copilotkit/route.ts
- id: declarative-gen-ui
name: Declarative Generative UI (A2UI)
description: Canonical A2UI BYOC — custom catalog (Card/StatusBadge/Metric/InfoRow/PrimaryButton) wired via a2ui.catalog on the provider; runtime injects the render_a2ui tool automatically
tags:
- generative-ui
route: /demos/declarative-gen-ui
animated_preview_url:
highlight:
- src/agents/a2ui_dynamic.py
- src/app/demos/declarative-gen-ui/page.tsx
- src/app/demos/declarative-gen-ui/a2ui/catalog.ts
- src/app/demos/declarative-gen-ui/a2ui/definitions.ts
- src/app/demos/declarative-gen-ui/a2ui/renderers.tsx
- src/app/api/copilotkit/route.ts
- id: a2ui-fixed-schema
name: Declarative Generative UI (A2UI — Fixed Schema)
description: A2UI rendering against a known client-side schema
tags:
- generative-ui
route: /demos/a2ui-fixed-schema
animated_preview_url:
highlight:
- src/agents/a2ui_fixed.py
- src/agents/a2ui_schemas/flight_schema.json
- src/agents/a2ui_schemas/booked_schema.json
- src/app/demos/a2ui-fixed-schema/page.tsx
- src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts
- src/app/demos/a2ui-fixed-schema/a2ui/definitions.ts
- src/app/demos/a2ui-fixed-schema/a2ui/renderers.tsx
- src/app/api/copilotkit/route.ts
- id: mcp-apps
name: MCP Apps
description: MCP server-driven UI via activity renderers
tags:
- generative-ui
route: /demos/mcp-apps
animated_preview_url:
highlight:
- src/agents/mcp_apps_agent.py
- src/app/demos/mcp-apps/page.tsx
- src/app/api/copilotkit-mcp-apps/[[...slug]]/route.ts
- id: interrupt-headless
name: Headless Interrupt (testing)
description: Resolve interrupts from a plain button grid — no chat, no useInterrupt render prop
tags:
- interactivity
route: /demos/interrupt-headless
animated_preview_url:
highlight:
- src/agents/interrupt_agent.py
- src/app/demos/interrupt-headless/page.tsx
- src/app/api/copilotkit/route.ts
- id: readonly-state-agent-context
name: Readonly State (Agent Context)
description: Frontend provides read-only context to the agent via useAgentContext
tags:
- agent-state
route: /demos/readonly-state-agent-context
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/readonly-state-agent-context/page.tsx
- src/app/api/copilotkit/route.ts
- id: tool-rendering-reasoning-chain
name: Tool Rendering + Reasoning Chain (testing)
description: Sequential tool calls with reasoning tokens rendered side-by-side
tags:
- generative-ui
route: /demos/tool-rendering-reasoning-chain
animated_preview_url:
highlight:
- src/agents/tool_rendering_reasoning_chain_agent.py
- src/app/demos/tool-rendering-reasoning-chain/page.tsx
- src/app/demos/tool-rendering-reasoning-chain/weather-card.tsx
- src/app/demos/tool-rendering-reasoning-chain/flight-list-card.tsx
- src/app/api/copilotkit/route.ts
- id: declarative-json-render
name: BYOC json-render
description: Streaming hierarchical JSON UI spec rendered via @json-render/react, with a Zod-validated catalog (MetricCard + PieChart + BarChart)
tags:
- generative-ui
route: /demos/declarative-json-render
animated_preview_url:
highlight:
- src/agents/byoc_json_render_agent.py
- src/app/demos/declarative-json-render/page.tsx
- src/app/demos/declarative-json-render/json-render-renderer.tsx
- src/app/demos/declarative-json-render/registry.tsx
- src/app/demos/declarative-json-render/catalog.ts
- src/app/demos/declarative-json-render/metric-card.tsx
- src/app/demos/declarative-json-render/charts/bar-chart.tsx
- src/app/demos/declarative-json-render/charts/pie-chart.tsx
- src/app/demos/declarative-json-render/types.ts
- src/app/demos/declarative-json-render/suggestions.ts
- src/app/api/copilotkit-declarative-json-render/route.ts
- id: beautiful-chat
name: Beautiful Chat
description: Canonical polished starter chat — brand fonts, theme tokens, suggestion pills
tags:
- chat-ui
route: /demos/beautiful-chat
animated_preview_url:
highlight:
- src/agents/beautiful_chat.py
- src/app/demos/beautiful-chat/page.tsx
- src/app/demos/beautiful-chat/components/example-layout/index.tsx
- src/app/demos/beautiful-chat/components/example-canvas/index.tsx
- src/app/demos/beautiful-chat/components/generative-ui/charts/bar-chart.tsx
- src/app/demos/beautiful-chat/components/generative-ui/charts/pie-chart.tsx
- src/app/demos/beautiful-chat/hooks/use-example-suggestions.tsx
- src/app/demos/beautiful-chat/hooks/use-generative-ui-examples.tsx
- src/app/api/copilotkit-beautiful-chat/route.ts
- id: multimodal
name: Multimodal Attachments
description: Image and PDF uploads via CopilotChat attachments, processed by a vision-capable agent
tags:
- chat-ui
route: /demos/multimodal
animated_preview_url:
highlight:
- src/agents/multimodal_agent.py
- src/app/demos/multimodal/page.tsx
- src/app/demos/multimodal/sample-attachment-buttons.tsx
- src/app/api/copilotkit-multimodal/route.ts
- id: auth
name: Authentication
description: Bearer-token gate via runtime onRequest hook with unauthenticated / authenticated states
tags:
- chat-ui
route: /demos/auth
animated_preview_url:
highlight:
- src/app/demos/auth/page.tsx
- src/app/demos/auth/auth-banner.tsx
- src/app/demos/auth/use-demo-auth.ts
- src/app/demos/auth/demo-token.ts
- src/app/api/copilotkit-auth/[[...slug]]/route.ts
- id: declarative-hashbrown
name: BYOC Hashbrown
description: Streaming structured output via @hashbrownai/react, rendering a sales dashboard catalog (MetricCard + PieChart + BarChart)
tags:
- generative-ui
route: /demos/declarative-hashbrown
animated_preview_url:
highlight:
- src/agents/byoc_hashbrown_agent.py
- src/app/demos/declarative-hashbrown/page.tsx
- src/app/demos/declarative-hashbrown/hashbrown-renderer.tsx
- src/app/demos/declarative-hashbrown/metric-card.tsx
- src/app/demos/declarative-hashbrown/charts/bar-chart.tsx
- src/app/demos/declarative-hashbrown/charts/pie-chart.tsx
- src/app/demos/declarative-hashbrown/types.ts
- src/app/demos/declarative-hashbrown/suggestions.ts
- src/app/api/copilotkit-declarative-hashbrown/route.ts
- id: open-gen-ui
name: Fully Open-Ended Generative UI
description: Agent generates UI from an arbitrary component library
tags:
- generative-ui
route: /demos/open-gen-ui
animated_preview_url:
highlight:
- src/agents/open_gen_ui_agent.py
- src/app/demos/open-gen-ui/page.tsx
- src/app/api/copilotkit-ogui/route.ts
- id: open-gen-ui-advanced
name: "Open-Ended Gen UI (Advanced: with frontend function calling)"
description: Agent-authored UI that can invoke frontend sandbox functions from inside the iframe
tags:
- generative-ui
route: /demos/open-gen-ui-advanced
animated_preview_url:
highlight:
- src/agents/open_gen_ui_advanced_agent.py
- src/app/demos/open-gen-ui-advanced/page.tsx
- src/app/demos/open-gen-ui-advanced/sandbox-functions.ts
- src/app/demos/open-gen-ui-advanced/suggestions.ts
- src/app/api/copilotkit-ogui/route.ts
- id: voice
name: Voice Input
description: Speech-to-text via @copilotkit/voice with a bundled sample audio button
tags:
- chat-ui
route: /demos/voice
animated_preview_url:
highlight:
- src/agents/agent.py
- src/app/demos/voice/page.tsx
- src/app/demos/voice/sample-audio-button.tsx
- src/app/api/copilotkit-voice/[[...slug]]/route.ts
- id: agent-config
name: Agent Config Object
description: Forward a typed config object (tone / expertise / response length) from the provider to the agent's dynamic system prompt
tags:
- platform
route: /demos/agent-config
animated_preview_url:
highlight:
- src/agents/agent_config_agent.py
- src/app/demos/agent-config/page.tsx
- src/app/demos/agent-config/config-card.tsx
- src/app/demos/agent-config/use-agent-config.ts
- src/app/demos/agent-config/config-types.ts
- src/app/api/copilotkit-agent-config/route.ts
@@ -0,0 +1,25 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Allow iframe embedding from the showcase shell
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "ALLOWALL",
},
{
key: "Content-Security-Policy",
value: "frame-ancestors *;",
},
],
},
];
},
typescript: { ignoreBuildErrors: true },
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
{
"name": "@copilotkit/showcase-ms-agent-python",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"next dev --turbopack\" \"PYTHONPATH=. python -m uvicorn agent_server:app --host 0.0.0.0 --port 8000 --reload\"",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test:e2e": "playwright test"
},
"dependencies": {
"@ag-ui/client": "0.0.57",
"@copilotkit/a2ui-renderer": "1.61.2",
"@copilotkit/react-core": "1.61.2",
"@copilotkit/runtime": "1.61.2",
"@copilotkit/shared": "1.61.2",
"@copilotkit/voice": "1.61.2",
"@hashbrownai/core": "0.5.0-beta.4",
"@hashbrownai/react": "0.5.0-beta.4",
"@json-render/core": "0.18.0",
"@json-render/react": "0.18.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-separator": "^1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.14.0",
"next": "^15.5.15",
"openai": "^5.9.0",
"radix-ui": "^1.4.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"recharts": "^2.15.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.6.0",
"yaml": "^2.8.4",
"zod": "^3.24.0"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"concurrently": "^9.1.0",
"postcss": "^8.5.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0"
},
"overrides": {
"@copilotkit/web-inspector": {
"@copilotkit/core": "1.61.2"
}
},
"pnpm": {
"overrides": {
"@copilotkit/web-inspector>@copilotkit/core": "1.61.2"
}
}
}
@@ -0,0 +1,45 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
// Local default of 1 retry covers the `agent_framework.Agent` concurrency
// flake: the shared Agent instance + single OpenAI HTTP client serialise
// SSE streams under high worker counts, so a handful of cells time out at
// 30s on the first attempt and pass cleanly on retry. CI keeps 2 retries
// as the safer ratchet. The underlying upstream fix is per-request agent
// instantiation; see the D5 sweep doc in Notion.
retries: process.env.CI ? 2 : 1,
// Local default of 4 workers caps the SSE concurrency the python agent has
// to absorb. The reused `agent_framework.Agent` + shared OpenAI HTTP client
// serialise concurrent streams; >4 workers makes 30s test timeouts inevitable
// for a few cells. 4 keeps things parallel without overloading the agent.
workers: process.env.CI ? 1 : 4,
reporter: "html",
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
trace: "on-first-retry",
extraHTTPHeaders: {
"X-AIMock-Context": "ms-agent-python",
},
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: process.env.CI
? undefined
: {
command: "pnpm dev",
url: "http://localhost:3000",
reuseExistingServer: true,
env: {
...process.env,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || "",
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
},
},
});
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
@@ -0,0 +1,30 @@
<svg viewBox="0 0 224.43 237.166" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.4556 75.7547C97.0552 48.81 114.152 22.1656 120.731 0.648813C120.908 0.0633572 121.594 -0.185355 122.103 0.152636C144.979 15.3013 186.646 25.2726 223.5 25.5066C224.134 25.5107 224.571 26.1359 224.342 26.7272C212.088 57.8146 197.122 113.518 196.54 177.128C196.54 178.073 195.21 178.412 194.742 177.59C173.768 140.886 106.586 89.3107 76.7986 77.138C76.2477 76.9114 76.0919 76.2307 76.4556 75.7547Z" fill="url(#paint0_linear)"/>
<path d="M145.956 59.273C113.757 69.4674 84.3336 75.1349 77.3077 76.4226C76.8608 76.5047 76.7673 77.1231 77.1934 77.2977C107.209 89.777 174.059 141.202 194.835 177.757C194.877 177.837 194.981 177.867 195.064 177.83C195.147 177.791 195.189 177.687 195.158 177.597L145.956 59.273Z" fill="url(#paint1_linear)"/>
<path d="M122.197 0.0860308C149.76 15.1211 181.615 21.8738 223.875 25.4319C224.135 25.4546 224.228 25.8103 223.989 25.9339C218.585 28.7116 187.623 44.4667 164.633 52.905C158.47 55.1657 152.275 57.263 146.174 59.1979C146.039 59.2402 145.894 59.1736 145.842 59.0446L121.563 0.655481C121.397 0.262302 121.823 -0.117886 122.197 0.0860308Z" fill="url(#paint2_linear)"/>
<path d="M121.361 0.145972C121.761 -0.0218955 122.214 0.121754 122.45 0.467128L122.536 0.6264L196.496 177.058L196.548 177.233C196.628 177.642 196.415 178.065 196.015 178.233C195.615 178.401 195.162 178.257 194.926 177.912L194.838 177.753L120.881 1.32093L120.826 1.14599C120.745 0.736568 120.961 0.313703 121.361 0.145972Z" fill="#513C9F"/>
<path d="M223.089 25.5869C223.52 25.3424 224.069 25.4925 224.313 25.9238C224.558 26.3552 224.406 26.9035 223.974 27.1483V27.1509H223.969C223.965 27.153 223.96 27.1575 223.953 27.1614C223.939 27.1695 223.916 27.1821 223.888 27.1979C223.832 27.2294 223.749 27.2755 223.64 27.3363C223.419 27.4593 223.091 27.6413 222.661 27.8768C221.8 28.3482 220.529 29.0371 218.885 29.9029C215.597 31.6348 210.813 34.0821 204.83 36.9423C192.865 42.6619 176.091 50.0388 156.86 56.6737C137.624 63.3089 117.782 68.4614 102.755 71.9534C95.2398 73.6998 88.9245 75.0317 84.4882 75.9274C82.2701 76.3752 80.5211 76.7135 79.3262 76.9405C78.7293 77.0538 78.2706 77.1391 77.9606 77.1963C77.8058 77.2249 77.6873 77.2472 77.6081 77.2616C77.5693 77.2687 77.5395 77.2737 77.5194 77.2773C77.5095 77.2791 77.501 77.2816 77.4959 77.2825H77.488L77.3052 77.2982C76.8881 77.2877 76.5205 76.9859 76.4436 76.5593C76.356 76.0711 76.6815 75.6027 77.1695 75.5149H77.1773C77.182 75.514 77.1888 75.5113 77.1982 75.5096C77.2176 75.5061 77.2481 75.501 77.287 75.494C77.3647 75.4798 77.4812 75.4595 77.6342 75.4313C77.9413 75.3746 78.3978 75.2882 78.992 75.1754C80.1806 74.9497 81.9227 74.6138 84.1331 74.1676C88.5551 73.2748 94.8521 71.9459 102.348 70.204C117.342 66.7197 137.119 61.5841 156.276 54.9766C175.426 48.3694 192.134 41.0193 204.055 35.3208C210.015 32.4718 214.777 30.0352 218.047 28.3128C219.682 27.4518 220.944 26.7694 221.796 26.3024C222.222 26.0693 222.546 25.8906 222.763 25.7697C222.871 25.7092 222.954 25.6619 223.008 25.6313C223.035 25.6163 223.055 25.6049 223.068 25.5974C223.075 25.5937 223.08 25.5914 223.084 25.5895L223.086 25.5869H223.089Z" fill="#513C9F"/>
<path d="M2.77027 236.611C2.20838 237.273 1.21525 237.353 0.553517 236.792C-0.107154 236.23 -0.18789 235.239 0.373357 234.577L2.77027 236.611ZM132.306 21.7584C133.136 22.0085 133.607 22.8858 133.358 23.7167L106.569 112.86H169.57L169.886 112.891C170.602 113.037 171.142 113.672 171.142 114.431C171.142 115.191 170.602 115.825 169.886 115.972L169.57 116.003H105.182L2.77027 236.611L1.57181 235.593L0.373357 234.577L103.044 113.664L130.347 22.8133C130.597 21.9819 131.474 21.5086 132.306 21.7584Z" fill="#ABABAB"/>
<path d="M72.9636 210.65L60.8346 212.356C67.1226 228.985 80.0206 236.249 95.4131 236.249C133.141 236.249 121.625 193.585 143.482 193.585C159.342 193.585 152.899 228.165 187.02 228.165C207.848 228.165 209.927 207.183 206.372 198.156C206.351 198.101 206.331 198.051 206.299 198.002L200.718 189.455C200.355 188.887 199.471 189.101 199.409 189.776L198.369 200.135C198.297 200.856 198.317 201.574 198.401 202.293C199.253 209.45 199.804 226.818 187.02 226.818C173.54 226.818 170.297 192.686 143.482 192.686C112.032 192.686 116.075 234.902 96.7643 234.902C84.0221 234.902 74.3043 220.531 72.9636 210.65Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="171.825" y1="13.8344" x2="135.895" y2="112.635" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="143.981" y1="69.5214" x2="97.7306" y2="158.891" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="164.633" y1="13.8337" x2="150.706" y2="57.3959" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="60.8346" y1="213.57" x2="207.775" y2="213.57" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.254755" stop-color="#1463B2"/>
<stop offset="0.498725" stop-color="#0A437D"/>
<stop offset="0.666667" stop-color="#2476C8"/>
<stop offset="0.972542" stop-color="#0C549A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,46 @@
<svg viewBox="0 0 1044.21 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(7.74 31.74)">
<g id="CopilotKit">
<path d="M111.689 53.5173H78.8802C78.647 50.8028 78.0267 48.3403 77.0185 46.1299C76.0487 43.9195 74.6918 42.0192 72.9466 40.4291C71.2403 38.8006 69.1654 37.5591 66.7223 36.7063C64.2791 35.8143 61.5062 35.3683 58.4038 35.3683C52.9745 35.3683 48.379 36.6867 44.6173 39.3235C40.8943 41.9611 38.0633 45.742 36.1243 50.6669C34.224 55.5919 33.2739 61.5063 33.2739 68.4092C33.2739 75.7001 34.2434 81.8084 36.1825 86.7334C38.1602 91.6199 41.0106 95.3042 44.7336 97.7855C48.4566 100.229 52.9358 101.45 58.1711 101.45C61.1573 101.45 63.8331 101.082 66.1988 100.345C68.5644 99.5695 70.6198 98.4647 72.3649 97.0293C74.11 95.5947 75.5258 93.8687 76.6117 91.8523C77.7362 89.7965 78.4923 87.4895 78.8802 84.9297L111.689 85.1629C111.301 90.2041 109.885 95.3426 107.442 100.578C104.999 105.774 101.528 110.583 97.0293 115.004C92.57 119.386 87.0435 122.916 80.4507 125.591C73.8579 128.267 66.1988 129.605 57.4731 129.605C46.5369 129.605 36.7254 127.259 28.0384 122.567C19.3904 117.874 12.5456 110.971 7.50404 101.858C2.50134 92.7443 0 81.5948 0 68.4092C0 55.1467 2.55952 43.9776 7.6786 34.9026C12.7977 25.7893 19.7006 18.9057 28.3875 14.252C37.0744 9.55957 46.7696 7.2133 57.4731 7.2133C64.9965 7.2133 71.9189 8.24101 78.2403 10.2964C84.5614 12.3518 90.1067 15.3572 94.877 19.3129C99.6472 23.2297 103.486 28.0578 106.395 33.7978C109.303 39.5371 111.068 46.1102 111.689 53.5173Z" fill="#010507"/>
<path d="M169.874 129.606C160.179 129.606 151.88 127.686 144.977 123.847C138.074 119.968 132.781 114.578 129.096 107.675C125.412 100.733 123.57 92.6864 123.57 83.5337C123.57 74.3818 125.412 66.3544 129.096 59.4515C132.781 52.5092 138.074 47.1186 144.977 43.2796C151.88 39.4014 160.179 37.4626 169.874 37.4626C179.569 37.4626 187.868 39.4014 194.771 43.2796C201.674 47.1186 206.968 52.5092 210.652 59.4515C214.336 66.3544 216.178 74.3818 216.178 83.5337C216.178 92.6864 214.336 100.733 210.652 107.675C206.968 114.578 201.674 119.968 194.771 123.847C187.868 127.686 179.569 129.606 169.874 129.606ZM170.107 105.872C172.822 105.872 175.168 104.96 177.145 103.138C179.123 101.315 180.655 98.7169 181.741 95.3428C182.827 91.9686 183.37 87.9553 183.37 83.3013C183.37 78.6087 182.827 74.5946 181.741 71.2597C180.655 67.8856 179.123 65.2873 177.145 63.4648C175.168 61.6422 172.822 60.7306 170.107 60.7306C167.237 60.7306 164.775 61.6422 162.719 63.4648C160.664 65.2873 159.093 67.8856 158.007 71.2597C156.922 74.5946 156.378 78.6087 156.378 83.3013C156.378 87.9553 156.922 91.9686 158.007 95.3428C159.093 98.7169 160.664 101.315 162.719 103.138C164.775 104.96 167.237 105.872 170.107 105.872Z" fill="#010507"/>
<path d="M229.208 161.484V38.6256H261.086V54.2155H261.784C262.948 51.1908 264.655 48.4182 266.903 45.8976C269.153 43.3377 271.945 41.3016 275.28 39.7893C278.615 38.2376 282.493 37.4626 286.914 37.4626C292.809 37.4626 298.413 39.0331 303.726 42.174C309.077 45.3157 313.421 50.2603 316.756 57.0078C320.13 63.7561 321.817 72.52 321.817 83.3013C321.817 93.6168 320.208 102.168 316.989 108.955C313.809 115.741 309.543 120.802 304.191 124.137C298.878 127.473 293.041 129.14 286.682 129.14C282.493 129.14 278.751 128.461 275.455 127.104C272.197 125.708 269.405 123.827 267.078 121.462C264.79 119.057 263.025 116.342 261.784 113.317H261.319V161.484H229.208ZM260.621 83.3013C260.621 87.6444 261.183 91.4064 262.308 94.5866C263.471 97.7275 265.101 100.171 267.195 101.916C269.327 103.622 271.868 104.476 274.815 104.476C277.762 104.476 280.263 103.642 282.319 101.974C284.413 100.268 286.003 97.8437 287.089 94.7028C288.214 91.5226 288.776 87.7221 288.776 83.3013C288.776 78.8804 288.214 75.0995 287.089 71.9578C286.003 68.7776 284.413 66.3544 282.319 64.6866C280.263 62.9803 277.762 62.1267 274.815 62.1267C271.868 62.1267 269.327 62.9803 267.195 64.6866C265.101 66.3544 263.471 68.7776 262.308 71.9578C261.183 75.0995 260.621 78.8804 260.621 83.3013Z" fill="#010507"/>
<path d="M335.269 127.977V38.6253H367.38V127.977H335.269ZM351.324 29.318C346.981 29.318 343.258 27.8834 340.156 25.0136C337.054 22.1438 335.502 18.6923 335.502 14.6592C335.502 10.6259 337.054 7.17442 340.156 4.30463C343.258 1.43485 346.981 0 351.324 0C355.707 0 359.429 1.43485 362.493 4.30463C365.596 7.17442 367.147 10.6259 367.147 14.6592C367.147 18.6923 365.596 22.1438 362.493 25.0136C359.429 27.8834 355.707 29.318 351.324 29.318Z" fill="#010507"/>
<path d="M415.851 8.84214V127.977H383.74V8.84214H415.851Z" fill="#010507"/>
<path d="M475.258 129.606C465.563 129.606 457.264 127.686 450.361 123.847C443.458 119.968 438.164 114.578 434.48 107.675C430.796 100.733 428.954 92.6864 428.954 83.5337C428.954 74.3818 430.796 66.3544 434.48 59.4515C438.164 52.5092 443.458 47.1186 450.361 43.2796C457.264 39.4014 465.563 37.4626 475.258 37.4626C484.954 37.4626 493.253 39.4014 500.156 43.2796C507.059 47.1186 512.352 52.5092 516.036 59.4515C519.72 66.3544 521.563 74.3818 521.563 83.5337C521.563 92.6864 519.72 100.733 516.036 107.675C512.352 114.578 507.059 119.968 500.156 123.847C493.253 127.686 484.954 129.606 475.258 129.606ZM475.491 105.872C478.205 105.872 480.552 104.96 482.53 103.138C484.508 101.315 486.039 98.7169 487.125 95.3428C488.211 91.9686 488.754 87.9553 488.754 83.3013C488.754 78.6087 488.211 74.5946 487.125 71.2597C486.039 67.8856 484.508 65.2873 482.53 63.4648C480.552 61.6422 478.205 60.7306 475.491 60.7306C472.621 60.7306 470.158 61.6422 468.103 63.4648C466.048 65.2873 464.477 67.8856 463.391 71.2597C462.305 74.5946 461.763 78.6087 461.763 83.3013C461.763 87.9553 462.305 91.9686 463.391 95.3428C464.477 98.7169 466.048 101.315 468.103 103.138C470.158 104.96 472.621 105.872 475.491 105.872Z" fill="#010507"/>
<path d="M587.877 38.6253V61.8941H529.008V38.6253H587.877ZM540.41 17.2186H572.52V99.2396C572.52 100.481 572.734 101.528 573.16 102.381C573.587 103.196 574.246 103.816 575.138 104.242C576.03 104.63 577.174 104.824 578.57 104.824C579.539 104.824 580.664 104.708 581.944 104.476C583.262 104.242 584.232 104.048 584.852 103.894L589.506 126.464C588.072 126.891 586.016 127.415 583.34 128.035C580.703 128.655 577.561 129.063 573.916 129.256C566.548 129.644 560.363 128.888 555.36 126.987C550.357 125.049 546.595 122.004 544.075 117.854C541.554 113.705 540.332 108.508 540.41 102.265V17.2186Z" fill="#010507"/>
<path d="M602.435 127.977V8.84214H634.778V57.0077H636.406L672.24 8.84214H709.935L669.681 61.8942L710.866 127.977H672.24L645.481 83.3012L634.778 97.2626V127.977H602.435Z" fill="#010507"/>
<path d="M725.597 127.977V38.6253H757.708V127.977H725.597ZM741.653 29.318C737.309 29.318 733.586 27.8834 730.484 25.0136C727.381 22.1438 725.831 18.6923 725.831 14.6592C725.831 10.6259 727.381 7.17442 730.484 4.30463C733.586 1.43485 737.309 0 741.653 0C746.035 0 749.758 1.43485 752.821 4.30463C755.924 7.17442 757.475 10.6259 757.475 14.6592C757.475 18.6923 755.924 22.1438 752.821 25.0136C749.758 27.8834 746.035 29.318 741.653 29.318Z" fill="#010507"/>
<path d="M827.354 38.6253V61.8941H768.484V38.6253H827.354ZM779.886 17.2186H811.994V99.2396C811.994 100.481 812.206 101.528 812.632 102.381C813.066 103.196 813.72 103.816 814.612 104.242C815.504 104.63 816.65 104.824 818.05 104.824C819.015 104.824 820.136 104.708 821.421 104.476C822.739 104.242 823.704 104.048 824.326 103.894L828.983 126.464C827.551 126.891 825.489 127.415 822.812 128.035C820.177 128.655 817.035 129.063 813.393 129.256C806.024 129.644 799.838 128.888 794.836 126.987C789.833 125.049 786.071 122.004 783.55 117.854C781.03 113.705 779.808 108.508 779.886 102.265V17.2186Z" fill="#010507"/>
</g>
</g>
<svg x="859.58" y="7.1" width="176.717" height="186.745" viewBox="0 0 176.717 186.745" fill="none">
<path d="M60.2013 59.6493C76.4214 38.4331 89.8836 17.4532 95.0639 0.510877C95.203 0.0498883 95.7431 -0.145948 96.1441 0.120187C114.156 12.0483 146.965 19.8996 175.984 20.0839C176.484 20.0871 176.827 20.5795 176.647 21.045C166.999 45.5233 155.214 89.384 154.756 139.471C154.756 140.215 153.708 140.482 153.34 139.835C136.825 110.934 83.9258 70.3234 60.4713 60.7386C60.0376 60.5602 59.9148 60.0242 60.2013 59.6493Z" fill="url(#paint0_linear)"/>
<path d="M114.926 46.6717C89.5724 54.6988 66.4044 59.1614 60.8722 60.1754C60.5203 60.24 60.4466 60.7269 60.7822 60.8644C84.4167 70.6906 137.054 111.183 153.414 139.967C153.446 140.03 153.528 140.052 153.594 140.024C153.659 139.993 153.692 139.911 153.667 139.841L114.926 46.6717Z" fill="url(#paint1_linear)"/>
<path d="M96.218 0.0677872C117.921 11.9064 143.004 17.2235 176.279 20.0252C176.484 20.043 176.558 20.3231 176.369 20.4204C172.114 22.6076 147.734 35.0132 129.632 41.6575C124.779 43.4376 119.902 45.089 115.098 46.6126C114.991 46.6459 114.877 46.5934 114.836 46.4919L95.7188 0.516173C95.5878 0.206583 95.9234 -0.0927773 96.218 0.0677872Z" fill="url(#paint2_linear)"/>
<path d="M95.5597 0.114978C95.875 -0.0172013 96.2317 0.0959082 96.417 0.367856L96.4849 0.493267L154.721 139.416L154.762 139.554C154.825 139.876 154.657 140.209 154.343 140.341C154.027 140.473 153.671 140.36 153.485 140.088L153.415 139.963L95.1814 1.04014L95.1383 0.902396C95.075 0.580014 95.2449 0.247049 95.5597 0.114978Z" fill="#513C9F"/>
<path d="M175.661 20.1472C176 19.9546 176.432 20.0729 176.625 20.4124C176.818 20.7522 176.697 21.1839 176.357 21.3767V21.3787H176.353C176.351 21.3803 176.346 21.3839 176.341 21.3869C176.33 21.3933 176.312 21.4033 176.29 21.4157C176.246 21.4405 176.18 21.4768 176.094 21.5247C175.921 21.6215 175.662 21.7648 175.323 21.9503C174.646 22.3215 173.645 22.8639 172.351 23.5456C169.761 24.9093 165.995 26.8363 161.283 29.0884C151.862 33.592 138.655 39.4006 123.512 44.625C108.365 49.8495 92.7416 53.9066 80.9093 56.6563C74.9919 58.0314 70.0192 59.0801 66.5261 59.7854C64.7796 60.138 63.4024 60.4044 62.4615 60.5831C61.9916 60.6723 61.6304 60.7395 61.3863 60.7845C61.2644 60.807 61.1711 60.8246 61.1087 60.8359C61.0782 60.8415 61.0547 60.8454 61.0388 60.8483C61.0311 60.8497 61.0244 60.8517 61.0203 60.8524H61.0142L60.8702 60.8647C60.5418 60.8565 60.2523 60.6188 60.1918 60.2829C60.1228 59.8986 60.3791 59.5297 60.7633 59.4605H60.7695C60.7732 59.4599 60.7786 59.4578 60.786 59.4564C60.8013 59.4537 60.8252 59.4497 60.8559 59.4441C60.9171 59.4329 61.0088 59.417 61.1293 59.3947C61.3711 59.3501 61.7305 59.2821 62.1984 59.1933C63.1343 59.0155 64.506 58.7511 66.2465 58.3997C69.7284 57.6967 74.6866 56.6503 80.5886 55.2788C92.3949 52.5352 107.968 48.4914 123.052 43.2887C138.13 38.0862 151.286 32.2987 160.673 27.8117C165.366 25.5684 169.116 23.6497 171.691 22.2936C172.978 21.6156 173.972 21.0783 174.643 20.7105C174.978 20.527 175.233 20.3863 175.404 20.2911C175.489 20.2435 175.554 20.2062 175.597 20.1822C175.618 20.1704 175.634 20.1614 175.644 20.1554C175.649 20.1525 175.654 20.1507 175.656 20.1493L175.658 20.1472H175.661Z" fill="#513C9F"/>
<path d="M2.18131 186.308C1.73888 186.829 0.956885 186.893 0.435837 186.45C-0.0843773 186.008 -0.147949 185.227 0.293978 184.707L2.18131 186.308ZM104.178 17.1327C104.832 17.3295 105.202 18.0203 105.006 18.6746L83.9124 88.8658H133.52L133.768 88.8905C134.333 89.0058 134.757 89.5053 134.757 90.1035C134.757 90.7016 134.333 91.2011 133.768 91.3164L133.52 91.3411H82.8207L2.18131 186.308L1.23765 185.506L0.293978 184.707L81.1369 89.499L102.636 17.9632C102.832 17.3086 103.523 16.9359 104.178 17.1327Z" fill="#ABABAB"/>
<path d="M57.4516 165.866L47.9012 167.209C52.8524 180.303 63.0083 186.023 75.1284 186.023C104.835 186.023 95.7677 152.43 112.978 152.43C125.466 152.43 120.393 179.658 147.26 179.658C163.66 179.658 165.297 163.136 162.498 156.029C162.481 155.985 162.465 155.946 162.44 155.907L158.046 149.177C157.759 148.73 157.064 148.898 157.015 149.43L156.196 157.587C156.139 158.154 156.155 158.72 156.221 159.286C156.892 164.921 157.326 178.597 147.26 178.597C136.645 178.597 134.092 151.722 112.978 151.722C88.2142 151.722 91.3977 184.962 76.1923 184.962C66.1591 184.962 58.5073 173.647 57.4516 165.866Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="135.295" y1="10.8932" x2="107.004" y2="88.6892" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="113.371" y1="54.7414" x2="76.9532" y2="125.111" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="129.632" y1="10.8927" x2="118.666" y2="45.1937" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="47.9012" y1="168.165" x2="163.603" y2="168.165" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.254755" stop-color="#1463B2"/>
<stop offset="0.498725" stop-color="#0A437D"/>
<stop offset="0.666667" stop-color="#2476C8"/>
<stop offset="0.972542" stop-color="#0C549A"/>
</linearGradient>
</defs>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd4aa7b049f1c3e324dfd15af4068d7f8fbf2eae1dd044df270dddc5f38a5c57
size 87078
@@ -0,0 +1,9 @@
# These two demo files are committed as real binaries (not LFS pointers)
# because the CI Docker build doesn't run `git lfs pull` — production
# would otherwise ship the pointer-text and the Try-with-sample-{image,pdf}
# buttons reject it with "Git LFS pointer, not the real asset".
#
# langgraph-python's demo-files have the same setup (their root
# `.gitattributes` `*.png filter=lfs` is overridden here per-path).
sample.png -filter -diff -merge
sample.pdf -filter -diff -merge
@@ -0,0 +1,22 @@
# Demo Files — Multimodal Demo
This directory bundles sample files referenced by the `/demos/multimodal` page.
Required files (must be committed as binaries; see `.gitattributes` at repo root):
- `sample.png` — a small (< 50 KB) PNG that the "Try with sample image" button
injects into the chat. A CopilotKit logo or other recognizable brand mark
works well so the vision-capable agent has something to describe.
- `sample.pdf` — a small (< 50 KB) one-page PDF that the "Try with sample PDF"
button injects. The content must mention "CopilotKit" so E2E soft
assertions hold (e.g. a one-page export of the CopilotKit quickstart).
The page at `src/app/demos/multimodal/page.tsx` fetches these via the public
path (`/demo-files/sample.png`, `/demo-files/sample.pdf`), wraps the fetched
blob in a `File`, and routes it through the same `AttachmentsConfig.onUpload`
callback the paperclip button uses — so the sample path exercises the exact
same queueing code as a real user upload.
If these files are missing, the demo page still renders but the sample
buttons will surface a fetch error. The paperclip / drag-and-drop paths
continue to work without them.
@@ -0,0 +1,80 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (CopilotKit) /CreationDate (D:20260424104452+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260424104452+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (CopilotKit Quickstart) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 1 /Kids [ 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 942
>>
stream
Gat=)gMRoa&:N^l7S$NN`GHCmThV;Z2^rJ`P):Y@]XYQU3:,AeX>cqAr-QI2/=2N"LfPX,loA_(*1m\5btm94UD2YX&6drn#XTAg+m:hr0`277L6Xugi+Zi-%K4(Zk-_X)UIg0#"[s_$PB)p"JhpIS$sekH(N!]Wm13pi-C<+2Z\%NM%.@X"c6;!4Z\-]lQ_FW]'b$98<b+A;AK8VL'gsRZ%'52u3[a]V,-i>M8Rs*Je-b?d:1;&?$,`ad5V$Xf0gWUq#g$2OoROl&VT9iG>Z7e:(6,L^5qWRe9f&aq&5@k4gFJA#"jE(QdPamJY0Hpqn>r_e:*FVtpYPMT?RZaJ8P7Ca0<':`;qM'##`XXPXO9RF6i%iKqn<L:oPEI$hcphT25S/nN3<DAK4NVdiJL>7#+eNHaXE@Y[_>W6oJG]uZn6;#DFBr_^iD&OGjVg%"ij8a:&7/qaE+7a=d``5hdQO":;.r]$4@TOAQt"^6*,kR%u>XHZrCjf@5l+)<U?'C_6MX"fJq<J@]>%B@BumKlTA+:A9AG5jqSXcFh[j7G)iN0D\N/+I[;hq]7D]ZAE\d8UW,N3+.Br<N@#$kD%W'f-CmKrYTKkeeuX8:pui+(3#6dK@@XC>.sl!SW4(X]/!H@1dH0Qp(55E^KJk1]UY=9;_"eb*-cL!Qq,hJ?,?l*JeVfts!/i_[Ejo:h;fDQSkbMmd[X?rTSO%M5Qeh<MleEs/@&O$fl?bGg9%rX0LW>9/:K"H3?p7"G:0Ha3Qj[6jL<;9nL312;])jCZe6\\@E*Ttr?Xg2lNp?68b2o5,cF;--/kSU`mMnHk40JE<",2f4YKj@2GX^Q[V>`.2@i]-rEiq2ARgR#W*>Hsb'riEWK24?;X\G?nL'id4l2fR3ocbEr3QJOgLr..O8N<DDWc>P*6r.',3Q1&LkGF@<;hrL[kDNqL~>endstream
endobj
xref
0 10
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000629 00000 n
0000000697 00000 n
0000000982 00000 n
0000001041 00000 n
trailer
<<
/ID
[<ad0be0ae447f61082137085c690fb230><ad0be0ae447f61082137085c690fb230>]
% ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R
/Root 6 0 R
/Size 10
>>
startxref
2073
%%EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

@@ -0,0 +1,62 @@
# QA: Agentic Chat — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the agentic-chat demo page
- [ ] Verify the chat interface loads with a text input placeholder "Type a message"
- [ ] Verify the background container (`data-testid="background-container"`) is visible
- [ ] Verify the default background color is the theme default (rgb(250, 250, 249))
- [ ] Send a basic message (e.g. "Hello")
- [ ] Verify the agent responds with a text message
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Change background" suggestion button is visible
- [ ] Verify "Generate sonnet" suggestion button is visible
- [ ] Click the "Change background" suggestion
- [ ] Verify the suggestion either populates the input or sends the message
#### Background Change (useFrontendTool)
- [ ] Ask "Change the background to a sunset gradient"
- [ ] Verify the background container style changes from the default
- [ ] Verify the change_background tool returns a success status
#### Weather Render Tool (useRenderTool)
- [ ] Type "What's the weather in Tokyo?"
- [ ] Verify loading state shows "Loading weather..." (`data-testid="weather-info-loading"`)
- [ ] Verify WeatherCard renders (`data-testid="weather-info"`) with:
- [ ] City name displayed
- [ ] Temperature in degrees C
- [ ] Humidity percentage
- [ ] Wind speed in mph
- [ ] Conditions text
#### Agent Context
- [ ] Verify the agent knows the user's name is "Bob" (provided via useAgentContext)
- [ ] Ask "What is my name?" and verify the agent responds with "Bob"
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
- [ ] Send a very long message and verify no UI breakage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Background changes are instant after tool execution
- Weather card renders with all data fields populated
- No UI errors or broken layouts
@@ -0,0 +1,62 @@
# QA: Agentic Generative UI — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the gen-ui-agent demo page
- [ ] Verify the chat interface loads in a centered full-height layout
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Simple plan" suggestion button is visible (plan to go to Mars in 5 steps)
- [ ] Verify "Complex plan" suggestion button is visible (plan to make pizza in 10 steps)
#### Task Progress Tracker (useAgent with state streaming)
- [ ] Click "Simple plan" suggestion or type "Build a plan to go to Mars in 5 steps"
- [ ] Verify the TaskProgress component renders (`data-testid="task-progress"`)
- [ ] Verify the progress bar appears with a gradient fill
- [ ] Verify step items appear with descriptions (`data-testid="task-step-text"`)
- [ ] Verify the "N/N Complete" counter updates as steps complete
- [ ] Verify completed steps show:
- Green background gradient
- Check icon
- Green text color
- [ ] Verify the current pending step shows:
- Blue/purple background gradient
- Spinner icon with "Processing..." text
- Pulsing animation
- [ ] Verify future pending steps show:
- Gray background
- Clock icon
- Muted text color
#### Complex Plan
- [ ] Type "Plan to make pizza in 10 steps"
- [ ] Verify 10 steps appear in the progress tracker
- [ ] Verify progress bar width increases as steps complete
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Task progress tracker shows live step completion
- Progress bar animates smoothly
- No UI errors or broken layouts
@@ -0,0 +1,59 @@
# QA: Tool-Based Generative UI — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the gen-ui-tool-based demo page
- [ ] Verify the CopilotSidebar opens by default with title "Haiku Generator"
- [ ] Verify a placeholder haiku card is displayed on the main area
- [ ] Send a basic message via the sidebar
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Nature Haiku" suggestion button is visible
- [ ] Verify "Ocean Haiku" suggestion button is visible
- [ ] Verify "Spring Haiku" suggestion button is visible
#### Haiku Generation (useFrontendTool)
- [ ] Click the "Nature Haiku" suggestion or type "Write me a haiku about nature"
- [ ] Verify a HaikuCard renders (`data-testid="haiku-card"`) with:
- [ ] Three Japanese text lines (`data-testid="haiku-japanese-line"`)
- [ ] Three English translation lines (`data-testid="haiku-english-line"`)
- [ ] A background gradient style applied to the card
- [ ] Verify the Japanese text contains actual Japanese characters (not Latin)
- [ ] Verify the English lines are a readable English translation
#### Image Display
- [ ] After haiku generation, verify an image renders (`data-testid="haiku-image"`) if the agent provides an image_name
- [ ] Verify the image src points to /images/ with a valid filename from the predefined list
#### Multiple Haikus
- [ ] Generate a second haiku (e.g. "Ocean Haiku")
- [ ] Verify the new haiku card appears at the top
- [ ] Verify the previous haiku card is still visible below
- [ ] Verify the initial placeholder haiku is removed
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Sidebar loads within 3 seconds
- Agent responds and generates haiku within 10 seconds
- Haiku cards display with Japanese and English text
- Generated haikus stack with newest on top
- No UI errors or broken layouts
@@ -0,0 +1,57 @@
# QA: Human in the Loop — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the HITL demo page (`/demos/hitl`) > _Note: the URL path `/demos/hitl` is intentionally shorter than the feature id `hitl-in-chat`._
- [ ] Verify the chat interface loads in a centered max-w-4xl container
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Simple plan" suggestion button is visible
- [ ] Verify "Complex plan" suggestion button is visible
- [ ] Click the "Simple plan" suggestion
- [ ] Verify it triggers a message about planning a trip to Mars in 5 steps
#### Step Selection and Approval
The HITL demo renders a single StepSelector card regardless of whether
the underlying flow uses an interrupt hook or a frontend HITL tool. The
framework-specific hook name is an implementation detail covered in each
package's demo source; QA below validates the user-visible card behavior.
- [ ] Send "Plan a trip to Mars in 5 steps"
- [ ] Verify the StepSelector card appears (`data-testid="select-steps"`)
- [ ] Verify step items are displayed with checkboxes (`data-testid="step-item"`)
- [ ] Verify step text is visible (`data-testid="step-text"`)
- [ ] Verify the selected count display shows "N/N selected"
- [ ] Toggle a step checkbox off and verify the count decreases
- [ ] Toggle it back on and verify the count increases
- [ ] Click "Perform Steps (N)" / "Confirm (N)" button
- [ ] Verify the agent continues processing after confirmation
- [ ] In a new conversation, trigger the same flow and click "Reject" (where present)
- [ ] Verify the card reflects the decision (Accepted / Rejected) and buttons disable
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Step selector renders with toggleable checkboxes
- Accept/Reject flow completes without errors
- No UI errors or broken layouts
@@ -0,0 +1,61 @@
# QA: Shared State (Read + Write) — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible at `/demos/shared-state-read-write` on the dashboard host
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; the agent server has the `shared-state-read-write` agent mounted at `/shared-state-read-write`
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to `/demos/shared-state-read-write`; verify the page renders within 3s with the left sidebar (preferences + notes cards) and the right-side `CopilotChat` pane
- [ ] Verify `data-testid="preferences-card"` is visible with heading "Your preferences"
- [ ] Verify `data-testid="notes-card"` is visible with heading "Agent notes" and empty-state `data-testid="notes-empty"` reading "No notes yet. Ask the agent to remember something."
- [ ] Verify the chat input placeholder is "Chat with the agent..."
- [ ] Verify all 3 suggestion pills are visible with verbatim titles: "Greet me", "Remember something", "Plan a weekend"
- [ ] Send "Hello" and verify an assistant text response appears within 10s
### 2. Feature-Specific Checks
#### UI Writes -> Agent Reads (preferences via `agent.setState`)
- [ ] Type "Atai" into `data-testid="pref-name"`; verify `data-testid="pref-state-json"` updates to include `"name": "Atai"`
- [ ] Change `data-testid="pref-tone"` to `formal`; verify the JSON preview reflects `"tone": "formal"`
- [ ] Change `data-testid="pref-language"` to `Spanish`; verify the JSON preview reflects `"language": "Spanish"`
- [ ] Click the `Cooking` and `Travel` interest pills; verify both show the selected style (border `#BEC2FF`, bg `#BEC2FF1A`) and the JSON preview's `interests` array contains both entries
- [ ] Send "What do you know about me?"; verify within 10s the assistant reply references the name "Atai", a formal tone, Spanish, and the Cooking/Travel interests (the AG-UI runtime injects the full `state_schema`-typed shared state into a system context message every turn)
- [ ] Click the "Plan a weekend" suggestion; verify the reply is tailored to the selected interests
#### Agent Writes -> UI Reads (notes via `set_notes` tool)
- [ ] Click the "Remember something" suggestion (sends "Remember that I prefer morning meetings and that I don't eat dairy.")
- [ ] Within 15s verify `data-testid="notes-list"` appears in the notes card and contains at least 2 `data-testid="note-item"` entries mentioning "morning meetings" and "dairy"
- [ ] Verify `data-testid="notes-empty"` is no longer rendered
- [ ] Send "Also remember I live in Berlin."; verify within 15s the notes list grows (previous notes preserved, new note added) — confirms the agent passes the FULL updated list per `set_notes` contract, and `state_update(...)` deterministically pushes the new state via a `StateSnapshotEvent`
#### UI Writes Back to Agent-Authored Slice (clear notes)
- [ ] With notes present, verify `data-testid="notes-clear-button"` is visible
- [ ] Click the Clear button; verify the notes list disappears and `data-testid="notes-empty"` re-renders
- [ ] Ask "What do you remember about me?"; verify the agent no longer cites the cleared notes (state was written back by the UI via `agent.setState({ notes: [] })`)
#### Multi-Turn State Persistence
- [ ] Change tone to `playful` and add the `Music` interest; send "Write me a one-line haiku greeting."; verify the reply is playful and references music
- [ ] Send a follow-up "Do it again in French."; verify the reply remains playful, switches to French, and still acknowledges the music interest — confirms preferences persist across turns without being re-sent
- [ ] Reload the page; verify preferences reset to defaults (`tone: casual`, `language: English`, empty interests, empty name) and notes reset to empty (state is per-session, seeded by the page's `useEffect`)
### 3. Error Handling
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no assistant response)
- [ ] Deselect all interests and clear the name; send "Who am I?"; verify the agent answers without crashing (the `_inject_state_context` helper short-circuits when `current_state` is empty)
- [ ] Verify DevTools -> Console shows no uncaught errors during any flow above
## Expected Results
- Page loads within 3 seconds; assistant text response within 10 seconds
- Preferences writes are reflected in `pref-state-json` synchronously on change
- Agent-authored notes appear in `notes-card` within 15 seconds of a "remember" prompt, and the full prior list is preserved on subsequent `set_notes` calls
- Clear button round-trips UI -> agent state and the agent loses access to the cleared notes on the next turn
- No UI layout breaks, no uncaught console errors
@@ -0,0 +1,75 @@
# QA: Shared State (Reading) — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-read demo page
- [ ] Verify the recipe card form loads (`data-testid="recipe-card"`)
- [ ] Verify the CopilotSidebar opens by default with title "AI Recipe Assistant"
- [ ] Send a message via the sidebar
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Initial Recipe State
- [ ] Verify the recipe title input shows "Make Your Recipe"
- [ ] Verify the cooking time dropdown defaults to "45 min"
- [ ] Verify the skill level dropdown defaults to "Intermediate"
- [ ] Verify the default ingredients are displayed:
- [ ] Carrots (3 large, grated) with carrot emoji
- [ ] All-Purpose Flour (2 cups) with wheat emoji
- [ ] Verify the default instruction is displayed: "Preheat oven to 350 F"
#### Suggestions
- [ ] Verify "Create Italian recipe" suggestion is visible
- [ ] Verify "Make it healthier" suggestion is visible
- [ ] Verify "Suggest variations" suggestion is visible
#### Recipe Editing (Local State)
- [ ] Edit the recipe title and verify it updates
- [ ] Change the skill level dropdown and verify it updates
- [ ] Change the cooking time dropdown and verify it updates
- [ ] Toggle a dietary preference checkbox (e.g. "Vegetarian") and verify it's checked
- [ ] Click "+ Add Ingredient" (`data-testid="add-ingredient-button"`) and verify a new empty row appears
- [ ] Edit an ingredient name and amount
- [ ] Remove an ingredient by clicking the "x" button
- [ ] Click "+ Add Step" and verify a new instruction row appears
- [ ] Edit an instruction and verify it saves
- [ ] Remove an instruction by clicking the "x" button
#### AI-Powered Recipe Updates (useAgent with shared state)
- [ ] Click "Create Italian recipe" suggestion
- [ ] Verify the agent updates the recipe title, ingredients, and instructions
- [ ] Verify the ping indicator appears on changed sections
- [ ] Verify the "Improve with AI" button (`data-testid="improve-button"`) changes to "Please Wait..." while loading
- [ ] Click "Improve with AI" and verify the recipe is enhanced
#### Agent Reads Frontend State
- [ ] Edit the recipe (change title, add ingredients)
- [ ] Ask the agent "What recipe am I making?"
- [ ] Verify the agent's response references the current recipe state
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
- [ ] Verify the "Improve with AI" button is disabled while loading
## Expected Results
- Recipe card and sidebar load within 3 seconds
- Agent responds within 10 seconds
- Recipe state syncs bidirectionally between UI and agent
- Ping indicators highlight changed sections
- No UI errors or broken layouts
@@ -0,0 +1,41 @@
# QA: State Streaming — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-streaming demo page
- [ ] Verify the chat interface loads with title "State Streaming"
- [ ] Verify the chat input placeholder "Type a message..." is visible
- [ ] Send a basic message (e.g. "Hello! What can you do?")
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Get started" suggestion button is visible
#### Note: Stub Demo
> **Status: Stub** — This demo is currently a stub (TODO: implement)
- [ ] Verify the basic CopilotChat loads and accepts messages
- [ ] Verify the agent responds to messages
- [ ] No custom UI components are expected beyond the chat interface
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- No UI errors or broken layouts
@@ -0,0 +1,41 @@
# QA: Shared State (Writing) — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-write demo page
- [ ] Verify the chat interface loads with title "Shared State (Writing)"
- [ ] Verify the chat input placeholder "Type a message..." is visible
- [ ] Send a basic message (e.g. "Hello! What can you do?")
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Get started" suggestion button is visible
#### Note: Stub Demo
> **Status: Stub** — This demo is currently a stub (TODO: implement)
- [ ] Verify the basic CopilotChat loads and accepts messages
- [ ] Verify the agent responds to messages
- [ ] No custom UI components are expected beyond the chat interface
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- No UI errors or broken layouts
@@ -0,0 +1,60 @@
# QA: Sub-Agents — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible at `/demos/subagents` on the dashboard host
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; the agent server has the `subagents` supervisor mounted at `/subagents`
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to `/demos/subagents`; verify the page renders within 3s with the delegation log on the left and the `CopilotChat` pane on the right
- [ ] Verify `data-testid="delegation-log"` is visible with heading "Sub-agent delegations"
- [ ] Verify `data-testid="delegation-count"` shows "0 calls" on first render
- [ ] Verify the empty-state copy "Ask the supervisor to complete a task. Every sub-agent it calls will appear here." is visible
- [ ] Verify the chat input placeholder is "Give the supervisor a task..."
- [ ] Verify all 3 suggestion pills are visible with verbatim titles: "Write a blog post", "Explain a topic", "Summarize a topic"
### 2. Feature-Specific Checks
#### Single Delegation — Research
- [ ] Send "Give me 3 facts about reusable rockets."
- [ ] Within 15s verify at least one `data-testid="delegation-entry"` appears
- [ ] Verify the entry's badge reads "Research" (research_agent)
- [ ] Verify `data-testid="delegation-count"` reads "1 calls" (or higher)
- [ ] Verify the `Task: ...` line shows the research brief and the body contains a bulleted list of facts
#### Full Pipeline — Research -> Write -> Critique
- [ ] Click the "Write a blog post" suggestion (sends a brief about cold exposure training)
- [ ] Within 30s verify the delegation log contains at least 3 entries: one Research, one Writing, one Critique (in that order)
- [ ] Verify each entry's `status` reads "completed"
- [ ] Verify the supervisor's chat reply summarises the work in 1-2 sentences (it should NOT dump the full draft inline; the draft lives in the log)
#### Live Updates While Running
- [ ] Send "Explain how LLMs do tool calling. Research, write a paragraph, then critique."
- [ ] While the supervisor is running, verify `data-testid="supervisor-running"` badge ("Supervisor running") appears next to the title
- [ ] Watch the delegation log: verify entries arrive incrementally as each sub-agent finishes (NOT all at once at the end) — confirms `state_update(...)` is emitted as a `StateSnapshotEvent` per tool call
- [ ] After the supervisor finishes, verify the running badge disappears
#### Multiple Turns
- [ ] Send a second prompt "Summarize the current state of reusable rockets in 1 polished paragraph, with research and critique."
- [ ] Verify the delegation log GROWS — prior entries are preserved and new ones append (no clobber). Confirms each delegation tool reads the prior list out of the agent's `current_state`-driven contextvar before pushing.
### 3. Error Handling
- [ ] Send an empty message; verify it is a no-op
- [ ] Send "Hello" (no delegation needed); verify the supervisor replies without delegating, and the delegation log stays empty
- [ ] Verify DevTools -> Console shows no uncaught errors during any flow above
## Expected Results
- Page loads within 3 seconds
- Single research-only prompts complete within 15 seconds
- Full research -> write -> critique pipelines complete within 30 seconds
- Delegation entries arrive incrementally and persist across turns
- No UI layout breaks, no uncaught console errors
@@ -0,0 +1,61 @@
# QA: Tool Rendering — MS Agent Framework (Python)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the tool-rendering demo page
- [ ] Verify the chat interface loads in a centered full-height layout
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Weather in San Francisco" suggestion button is visible
- [ ] Verify "Weather in New York" suggestion button is visible
- [ ] Verify "Weather in Tokyo" suggestion button is visible
- [ ] Click a weather suggestion and verify it populates the input or sends the message
#### Weather Card Rendering (useRenderTool)
- [ ] Type "What's the weather in San Francisco?"
- [ ] Verify loading state shows "Retrieving weather..." with a spinner
- [ ] Verify the WeatherCard renders (`data-testid="weather-card"`) with:
- [ ] City name displayed (`data-testid="weather-city"`)
- [ ] Temperature in both Celsius and Fahrenheit
- [ ] Humidity percentage (`data-testid="weather-humidity"`)
- [ ] Wind speed in mph (`data-testid="weather-wind"`)
- [ ] Feels-like temperature (`data-testid="weather-feels-like"`)
- [ ] Conditions text with appropriate weather icon (sun/rain/cloud)
- [ ] Verify the card background color matches the weather condition theme:
- Clear/Sunny: #667eea (blue-purple)
- Rain/Storm: #4A5568 (dark gray)
- Cloudy: #718096 (medium gray)
- Snow: #63B3ED (light blue)
#### Multiple Weather Queries
- [ ] Ask about weather in a second city
- [ ] Verify a second WeatherCard renders without breaking the first
- [ ] Verify each card shows the correct city name
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Weather cards render with all data fields populated
- Weather icon and theme color match the conditions
- No UI errors or broken layouts
@@ -0,0 +1,10 @@
agent-framework-ag-ui>=1.0.0b251117
agent-framework-openai>=1.0.0rc6
httpx>=0.28.0
langchain-openai>=1.1.0
langchain-core>=0.3.0
python-dotenv==1.0.1
uvicorn==0.37.0
fastapi>=0.115.0
starlette<1.0.0
pypdf>=4.0.0,<7.0.0
@@ -0,0 +1,270 @@
"""
Agent Server for MS Agent Framework (Python)
FastAPI server that hosts the Microsoft Agent Framework agent backend.
The Next.js CopilotKit runtime proxies requests here via AG-UI protocol.
"""
from __future__ import annotations
import os
# CVDIAG bootstrap — MUST be the first non-stdlib import (folded in from the
# dropped L1-H slot). Importing this module configures the root logger via
# ``logging.basicConfig`` so the ``agents._header_forwarding`` (and sibling
# ``agents.*``) CVDIAG loggers actually EMIT (fixes the silent-drop bug), and
# resolves the verbosity tier + PB writer. It imports pydantic/starlette only
# (NOT agent_framework), so it is safe to run before the agent imports below.
import _shared.cvdiag_bootstrap # noqa: F401,E402 (first non-stdlib import — bootstrap side effects)
# ORDER-CRITICAL: install the global httpx hook BEFORE any agent module /
# agent_framework / agent_framework_openai imports. The
# ``OpenAIChatCompletionClient`` constructs its httpx client at
# ``_build_chat_client()`` time below — which runs at module-import scope
# (line ~79) — so the patch must be in place before that import resolves.
from agents._cvdiag_backend import CvdiagBackendMiddleware
from agents._header_forwarding import (
HeaderForwardingHTTPMiddleware,
install_executor_contextvar_propagation,
install_global_httpx_hook,
)
install_global_httpx_hook()
# agent_framework dispatches SYNC tools (e.g. the declarative gen-ui
# `generate_a2ui` tool, which makes a secondary OpenAI call) onto the
# default ThreadPoolExecutor via loop.run_in_executor(...), which does NOT
# propagate ContextVars to the worker thread. Without this, the
# forwarded-header ContextVar set on the inbound request task is empty by
# the time the secondary call's outbound httpx hook fires, and aimock
# can't match the right fixture for the request.
install_executor_contextvar_propagation()
import uvicorn
from agent_framework import BaseChatClient
from agent_framework_openai import OpenAIChatCompletionClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from agents.agent import create_agent
from agents.voice_agent import create_voice_agent
from agents.a2ui_dynamic import create_agent as create_a2ui_dynamic_agent
from agents.a2ui_fixed import create_agent as create_a2ui_fixed_agent
from agents.agent_config_agent import create_agent_config_agent
from agents.beautiful_chat import create_beautiful_chat_agent
from agents.byoc_hashbrown_agent import create_byoc_hashbrown_agent
from agents.byoc_json_render_agent import create_byoc_json_render_agent
from agents.gen_ui_agent import create_gen_ui_agent
from agents.gen_ui_tool_based_agent import create_gen_ui_tool_based_agent
from agents.headless_complete_agent import create_headless_complete_agent
from agents.hitl_in_app_agent import create_hitl_in_app_agent
from agents.hitl_in_chat_agent import create_hitl_in_chat_agent
from agents.interrupt_agent import create_interrupt_agent
from agents.mcp_apps_agent import create_mcp_apps_agent
from agents.multimodal_agent import create_multimodal_agent
from agents.open_gen_ui_advanced_agent import create_open_gen_ui_advanced_agent
from agents.open_gen_ui_agent import create_open_gen_ui_agent
from agents.readonly_state_agent_context import create_readonly_state_agent_context
from agents.reasoning_agent import create_reasoning_agent
from agents.shared_state_read_write_agent import (
create_shared_state_read_write_agent,
)
from agents.shared_state_streaming import create_shared_state_streaming_agent
from agents.subagents_agent import create_subagents_agent
from agents.tool_rendering_agent import create_tool_rendering_agent
from agents.tool_rendering_reasoning_chain_agent import (
create_tool_rendering_reasoning_chain_agent,
)
load_dotenv()
def _build_chat_client(model_override: str | None = None) -> BaseChatClient:
# Use ChatCompletions, not Responses. The Responses API is stateful and
# only sends NEW items per leg (relying on `previous_response_id` for
# history); aimock has no view of that server-side state, so second-leg
# requests arrive without the user message — fixture matchers keyed on
# `userMessage` can't fire and the run falls through to real OpenAI.
# ChatCompletions sends full message history on every call, matching the
# LangGraph Python reference and letting the shared aimock fixtures match.
try:
if bool(os.getenv("OPENAI_API_KEY")):
return OpenAIChatCompletionClient(
model=model_override
or os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("OPENAI_API_KEY"),
)
raise ValueError("OPENAI_API_KEY environment variable is required")
except Exception as exc:
raise RuntimeError(
"Unable to initialize the chat client. Double-check your API credentials."
) from exc
chat_client = _build_chat_client()
my_agent = create_agent(chat_client)
voice_agent = create_voice_agent(chat_client)
agent_config_agent = create_agent_config_agent(chat_client)
reasoning_agent = create_reasoning_agent()
readonly_state_agent_context = create_readonly_state_agent_context(chat_client)
shared_state_streaming_agent = create_shared_state_streaming_agent(chat_client)
tool_rendering_agent = create_tool_rendering_agent(chat_client)
tool_rendering_reasoning_chain_agent = create_tool_rendering_reasoning_chain_agent(
chat_client
)
a2ui_dynamic_agent = create_a2ui_dynamic_agent(chat_client)
a2ui_fixed_agent = create_a2ui_fixed_agent(chat_client)
open_gen_ui_agent = create_open_gen_ui_agent(chat_client)
open_gen_ui_advanced_agent = create_open_gen_ui_advanced_agent(chat_client)
byoc_hashbrown_agent = create_byoc_hashbrown_agent(chat_client)
byoc_json_render_agent = create_byoc_json_render_agent(chat_client)
mcp_apps_agent = create_mcp_apps_agent(chat_client)
gen_ui_agent = create_gen_ui_agent(chat_client)
gen_ui_tool_based_agent = create_gen_ui_tool_based_agent(chat_client)
headless_complete_agent = create_headless_complete_agent(chat_client)
hitl_in_app_agent = create_hitl_in_app_agent(chat_client)
hitl_in_chat_agent = create_hitl_in_chat_agent(chat_client)
interrupt_agent = create_interrupt_agent(chat_client)
shared_state_read_write_agent = create_shared_state_read_write_agent(chat_client)
subagents_agent = create_subagents_agent(chat_client)
# Multimodal: vision-capable; gpt-4o-mini natively handles `image` parts.
# Scoped to its own endpoint so other demos don't silently upgrade to vision.
multimodal_chat_client = _build_chat_client("gpt-4o-mini")
multimodal_agent = create_multimodal_agent(multimodal_chat_client)
# Beautiful Chat: flagship polished sales dashboard demo. Combines A2UI
# (fixed + dynamic), Open Generative UI, shared-state todos, and HITL.
beautiful_chat_agent = create_beautiful_chat_agent(chat_client)
app = FastAPI(title="CopilotKit + Microsoft Agent Framework (Python)")
# Serve /health via middleware so it short-circuits BEFORE route resolution.
# `add_agent_framework_fastapi_endpoint(..., path="/")` installs a catch-all
# at the root that shadows any later `@app.get("/health")` decorator.
# Middleware runs above the routing layer, so /health stays reachable.
class HealthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if request.url.path == "/health" and request.method == "GET":
return JSONResponse({"status": "ok"})
return await call_next(request)
app.add_middleware(HealthMiddleware)
# Capture inbound CopilotKit ``x-*`` headers (e.g. ``x-aimock-context``)
# into a per-request ContextVar so any outbound LLM/provider httpx call
# made inside the request scope copies them onto its outbound request.
# Paired with ``install_global_httpx_hook`` at the top of this file.
app.add_middleware(HeaderForwardingHTTPMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# CVDIAG backend emitter (spec §3 Layer 2) — emits the HTTP-observable backend
# boundaries (request.ingress, sse.first_byte, sse.event, sse.aborted,
# response.complete, error.caught) as structured CVDIAG envelopes. Added LAST so
# it is the OUTERMOST layer: it observes ingress before any inner layer mutates
# the request and wraps the response stream so SSE boundaries fire as chunks
# flow. Gated behind ``CVDIAG_BACKEND_EMITTER`` (default OFF, canary-safe) — the
# middleware fast-paths to a bare pass-through when the flag is unset.
app.add_middleware(CvdiagBackendMiddleware)
# IMPORTANT: mount specific-path agents BEFORE the catch-all `/` agent.
# `add_agent_framework_fastapi_endpoint(..., path="/")` installs a catch-all
# at the root that shadows any route registered AFTER it. FastAPI resolves
# routes in registration order, so specific paths must come first.
add_agent_framework_fastapi_endpoint(
app=app, agent=multimodal_agent, path="/multimodal"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=beautiful_chat_agent, path="/beautiful-chat"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=agent_config_agent, path="/agent-config"
)
add_agent_framework_fastapi_endpoint(app=app, agent=reasoning_agent, path="/reasoning")
add_agent_framework_fastapi_endpoint(
app=app, agent=tool_rendering_agent, path="/tool-rendering"
)
add_agent_framework_fastapi_endpoint(
app=app,
agent=tool_rendering_reasoning_chain_agent,
path="/tool-rendering-reasoning-chain",
)
add_agent_framework_fastapi_endpoint(
app=app, agent=a2ui_dynamic_agent, path="/a2ui_dynamic"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=a2ui_fixed_agent, path="/a2ui_fixed"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=open_gen_ui_agent, path="/open-gen-ui"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=open_gen_ui_advanced_agent, path="/open-gen-ui-advanced"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=byoc_hashbrown_agent, path="/byoc-hashbrown"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=byoc_json_render_agent, path="/byoc-json-render"
)
add_agent_framework_fastapi_endpoint(app=app, agent=mcp_apps_agent, path="/mcp-apps")
add_agent_framework_fastapi_endpoint(
app=app, agent=hitl_in_app_agent, path="/hitl-in-app"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=hitl_in_chat_agent, path="/hitl-in-chat"
)
add_agent_framework_fastapi_endpoint(app=app, agent=gen_ui_agent, path="/gen-ui-agent")
add_agent_framework_fastapi_endpoint(
app=app, agent=gen_ui_tool_based_agent, path="/gen-ui-tool-based"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=headless_complete_agent, path="/headless-complete"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=interrupt_agent, path="/interrupt-adapted"
)
add_agent_framework_fastapi_endpoint(
app=app, agent=shared_state_read_write_agent, path="/shared-state-read-write"
)
add_agent_framework_fastapi_endpoint(
app=app,
agent=shared_state_streaming_agent,
path="/shared-state-streaming",
)
add_agent_framework_fastapi_endpoint(
app=app,
agent=readonly_state_agent_context,
path="/readonly-state-agent-context",
)
add_agent_framework_fastapi_endpoint(app=app, agent=subagents_agent, path="/subagents")
add_agent_framework_fastapi_endpoint(app=app, agent=voice_agent, path="/voice")
# Shared agent for the rest of the demos (must be last: `/` is a catch-all).
add_agent_framework_fastapi_endpoint(app=app, agent=my_agent, path="/")
def main():
"""Run the uvicorn server."""
host = os.getenv("AGENT_HOST", "0.0.0.0")
port = int(os.getenv("AGENT_PORT", "8000"))
uvicorn.run("agent_server:app", host=host, port=port, reload=True)
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
@@ -0,0 +1,812 @@
"""_cvdiag_backend.py — backend-layer CVDIAG boundary instrumentation.
This module wires the spec §3 / §5 **11 backend boundaries** into a Python
showcase integration, emitting schema-v1 CVDIAG envelopes through the shared
``_shared.cvdiag_bootstrap.emit_cvdiag`` sink. It is the per-integration
companion to the header-forwarding shim (``_header_forwarding.py``): that file
forwards correlation headers onto outbound LLM calls and logs lightweight
``CVDIAG component=backend-<fw> boundary=...`` breadcrumbs; THIS file emits the
full structured ``CVDIAG {<json>}`` envelopes the harness/classifier consume.
The 11 backend boundaries (spec §5 / §6 tier matrix):
1. ``backend.request.ingress`` — HTTP request received (default)
2. ``backend.agent.enter`` — agent loop entered (default)
3. ``backend.llm.call.start`` — outbound LLM call dispatched (verbose)
4. ``backend.llm.call.heartbeat`` — fires ~10s while an LLM call is
outstanding (verbose)
5. ``backend.llm.call.response`` — LLM response received (verbose)
6. ``backend.sse.first_byte`` — first SSE byte written (verbose)
7. ``backend.sse.event`` — every SSE event written (debug)
8. ``backend.sse.aborted`` — stream terminated abnormally (default)
9. ``backend.agent.exit`` — agent loop exited (default)
10. ``backend.response.complete`` — HTTP response stream closed (default)
11. ``backend.error.caught`` — exception caught in the agent loop
(default)
Guarding
--------
ALL emission is gated behind the ``CVDIAG_BACKEND_EMITTER`` env flag, default
OFF. With the flag off this module is byte-for-byte inert — no envelope is
built, no stdout line is written, the middleware passes the request straight
through. This is the canary-safe default: the flag is flipped ON only after a
deploy is confirmed healthy.
Tier gating
-----------
Each boundary carries a tier per the §6 matrix. ``_shared.cvdiag_bootstrap``
resolves the active tier (default | verbose | debug) once at import; this
module suppresses a boundary whose tier exceeds the active tier so the
default-tier production emit stays within the §7 event-count budget.
Pure instrumentation
--------------------
Nothing here may throw into the request path. ``emit_cvdiag`` already swallows
its own errors; the helpers below additionally guard envelope construction so a
malformed metadata bag degrades to a dropped emit, never a 500.
Plan unit: L1-C.
"""
from __future__ import annotations
import asyncio
import logging
import os
import re
import secrets
import time
import uuid
from typing import Any, Dict, Optional
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from _shared.cvdiag_bootstrap import _resolve_tier, current_tier, emit_cvdiag
logger = logging.getLogger(__name__)
# Framework tag — mirrors ``_header_forwarding._CVDIAG_FRAMEWORK`` so the
# structured envelopes and the breadcrumb log lines agree on the integration
# identity. (L1-D: change this single constant when copying to a sibling.)
_CVDIAG_FRAMEWORK = "ms-agent-python"
# ── Env gate ─────────────────────────────────────────────────────────────────
_BACKEND_EMITTER_ENV = "CVDIAG_BACKEND_EMITTER"
def cvdiag_backend_enabled() -> bool:
"""True iff the backend emitter is explicitly enabled (default OFF).
Read live (not cached) so a test can toggle the env var per-case via
``monkeypatch.setenv``; the cost is one ``os.environ`` lookup per emit,
which is negligible against the JSON serialization that follows.
"""
return os.environ.get(_BACKEND_EMITTER_ENV) == "1"
# ── Tier ordering (spec §6) ────────────────────────────────────────────────
_TIER_RANK = {"default": 0, "verbose": 1, "debug": 2}
# Per-boundary minimum tier required to emit (spec §6 matrix, backend rows).
_BOUNDARY_TIER: Dict[str, str] = {
"backend.request.ingress": "verbose",
"backend.agent.enter": "default",
"backend.llm.call.start": "verbose",
"backend.llm.call.heartbeat": "verbose",
"backend.llm.call.response": "verbose",
"backend.sse.first_byte": "verbose",
"backend.sse.event": "debug",
"backend.sse.aborted": "default",
"backend.agent.exit": "default",
"backend.response.complete": "default",
"backend.error.caught": "default",
}
def _active_tier() -> str:
"""Resolve the verbosity tier from a LIVE env read.
``cvdiag_backend_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. 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 _tier_permits(boundary: str) -> bool:
"""True iff the active tier is at-or-above the boundary's minimum tier."""
need = _TIER_RANK.get(_BOUNDARY_TIER.get(boundary, "default"), 0)
have = _TIER_RANK.get(_active_tier(), 0)
return have >= need
# ── Edge headers (spec §5 — 9-key allow-list + 12-name deny-list) ───────────
# The closed 9-key edge-header allow-list. Always-present in the envelope;
# absent header → ``None``.
_EDGE_ALLOW = (
"cf-ray",
"cf-mitigated",
"cf-cache-status",
"x-railway-edge",
"x-railway-request-id",
"x-hikari-trace",
"retry-after",
"via",
"server",
)
# Exact-match deny-list (spec §5). REJECTED even if accidentally present in the
# allow-list — these carry client IP / geo PII and must never round-trip.
_EDGE_DENY = frozenset(
{
"cf-ipcountry",
"cf-connecting-ip",
"cf-ipcity",
"cf-iplatitude",
"cf-iplongitude",
"cf-iptimezone",
"cf-visitor",
"cf-worker",
"true-client-ip",
"x-forwarded-for",
"x-real-ip",
"forwarded",
}
)
def extract_edge_headers(headers: Any) -> Dict[str, Optional[str]]:
"""Build the closed 9-key ``edge_headers`` bag from a headers mapping.
All nine keys are ALWAYS present; an absent (or deny-listed) header maps to
``None``. ``headers`` is any case-insensitive mapping exposing ``.get`` /
iteration of ``(name, value)`` pairs (Starlette ``Headers``, httpx, dict).
"""
bag: Dict[str, Optional[str]] = {k: None for k in _EDGE_ALLOW}
if headers is None:
return bag
try:
getter = headers.get
except AttributeError:
return bag
for key in _EDGE_ALLOW:
if key in _EDGE_DENY: # belt-and-braces: never emit a deny-listed key
continue
val = getter(key)
if val is not None:
bag[key] = str(val)
return bag
# ── PII scrub (spec §6) ──────────────────────────────────────────────────────
# Bearer tokens, OpenAI/Stripe-style secret keys, publishable keys, and URL
# userinfo. Applied to any captured free-text metadata value
# (``message_scrubbed``, stack frames) before it is emitted. The ``sk-``/``pk-``
# key bodies allow hyphens/underscores so test-style keys such as the spec
# regression fixture ``sk-test-12345`` are redacted alongside real production
# keys (``sk-<48+ base62>``).
#
# Parity with the canonical TS scrubber (``harness/src/cvdiag/scrub.ts``):
# * Bearer — grabs the WHOLE token (``\S+``) to match TS ``Bearer\s+\S+``;
# the legacy ``[A-Za-z0-9._\-]+`` stopped at ``/``/``+``/``=`` and left an
# un-redacted JWT tail (e.g. ``Bearer a.b.c/sig+more=`` → ``…/sig+more=``).
# * URL userinfo — redacts BOTH ``scheme://user:pw@host`` AND colon-less
# ``scheme://token@host`` (TS ``([scheme]://)[^/\s?#]*@``); the legacy
# ``[^/\s:@]+:[^/\s@]+@`` required a mandatory ``:`` so a bare-token
# authority such as ``https://ghp_xxx@host`` LEAKED. The userinfo class
# excludes ``?``/``#`` so the match never crosses into the query/fragment.
_SCRUB_PATTERNS = (
re.compile(r"Bearer\s+\S+", re.IGNORECASE),
re.compile(r"\bsk-[A-Za-z0-9][A-Za-z0-9_-]{3,}"),
re.compile(r"\bpk-[A-Za-z0-9][A-Za-z0-9_-]{3,}"),
re.compile(r"(?P<scheme>[a-z][a-z0-9+.\-]*://)[^/\s?#]*@", re.IGNORECASE),
)
# Per-event field byte caps (spec §5). message_scrubbed ≤512B.
_MESSAGE_CAP = 512
# Hard input-size guard (mirrors TS ``SCRUB_MAX_SCAN_LEN``): no regex ever runs
# on a string longer than this. A longer value has only its bounded prefix
# scanned and a self-describing ``…[unscanned:<N>]`` marker records the dropped
# tail length, so an adversarial multi-KB string can never make the regex
# engine scan unbounded input. 2 KB covers any legitimate metadata value with
# headroom. Set below the byte cap so the marker survives the §5 byte clamp.
_SCRUB_MAX_SCAN_LEN = 400
def _run_scrub_regexes(s: str) -> str:
"""Apply the secret regexes in sequence (TS ``runScrubRegexes`` parity)."""
for pat in _SCRUB_PATTERNS:
if pat.groupindex.get("scheme"):
s = pat.sub(r"\g<scheme>[REDACTED]@", s)
else:
s = pat.sub("[REDACTED]", s)
return s
def scrub(text: Any) -> str:
"""Redact secrets from a free-text value and cap it at 512 bytes.
Returns ``"[REDACTED]"`` substitutions for any matched secret pattern so a
synthetic ``sk-test-12345`` in an exception message can never reach the
emitted envelope. A value longer than ``_SCRUB_MAX_SCAN_LEN`` has only its
bounded prefix scanned, with an ``…[unscanned:<N>]`` marker (TS parity).
"""
if text is None:
return ""
s = str(text)
if len(s) > _SCRUB_MAX_SCAN_LEN:
dropped_tail = len(s) - _SCRUB_MAX_SCAN_LEN
s = f"{_run_scrub_regexes(s[:_SCRUB_MAX_SCAN_LEN])}…[unscanned:{dropped_tail}]"
else:
s = _run_scrub_regexes(s)
encoded = s.encode("utf-8")
if len(encoded) > _MESSAGE_CAP:
s = encoded[:_MESSAGE_CAP].decode("utf-8", errors="ignore")
return s
# ── Envelope construction ──────────────────────────────────────────────────
_TEST_ID_HEADER = "x-test-id"
_AIMOCK_CONTEXT_HEADER = "x-aimock-context"
# UUIDv7 variant/version nibbles (RFC 9562) the schema regex requires.
_SLUG_FALLBACK = "unknown"
_DEMO_FALLBACK = "default"
def _uuid7() -> str:
"""Generate a lowercase-hyphenated UUIDv7 (RFC 9562) string.
48-bit Unix-ms timestamp, version nibble 7, variant 10 — matches the
schema ``TEST_ID_PATTERN``. Used as the fallback ``test_id`` when no
inbound ``x-test-id`` correlation header is present.
"""
unix_ms = int(time.time() * 1000) & ((1 << 48) - 1)
rand_a = secrets.randbits(12)
rand_b = secrets.randbits(62)
msb = (unix_ms << 16) | (0x7 << 12) | rand_a
lsb = (0b10 << 62) | rand_b
return str(uuid.UUID(int=(msb << 64) | lsb))
_UUID7_RE = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
)
def normalize_test_id(raw: Optional[str]) -> str:
"""Return a schema-valid lowercased UUIDv7, minting one if ``raw`` is
absent or not a well-formed UUIDv7."""
if raw:
candidate = raw.strip().lower()
if _UUID7_RE.match(candidate):
return candidate
return _uuid7()
def _span_id() -> str:
"""16-hex span id, unique per emit (schema ``SPAN_ID_PATTERN``)."""
return secrets.token_hex(8)
_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]{0,63}$")
def _normalize_slug(raw: Optional[str]) -> str:
"""Coerce the inbound ``x-aimock-context`` slug into the closed slug shape
(``^[a-z][a-z0-9-]{0,63}$``), falling back to ``unknown`` when unusable."""
if raw:
candidate = raw.strip().lower()
if _SLUG_RE.match(candidate):
return candidate
return _SLUG_FALLBACK
def build_envelope(
*,
boundary: str,
outcome: str,
test_id: str,
slug: str,
demo: str,
metadata: Optional[Dict[str, Any]] = None,
edge_headers: Optional[Dict[str, Optional[str]]] = None,
duration_ms: Optional[int] = None,
parent_span_id: Optional[str] = None,
) -> Dict[str, Any]:
"""Assemble a schema-v1 backend envelope (``layer=backend``).
All envelope-required fields are populated; ``edge_headers`` defaults to the
closed 9-key all-null bag when not supplied. ``metadata`` is passed through
verbatim — unknown keys are stamped ``_metadata_dropped`` by the schema
validator inside ``emit_cvdiag``.
"""
return {
"schema_version": 1,
"test_id": test_id,
"trace_id": test_id,
"span_id": _span_id(),
"parent_span_id": parent_span_id,
"layer": "backend",
"boundary": boundary,
"slug": slug,
"demo": demo,
"ts": _now_iso(),
"mono_ns": time.monotonic_ns(),
"duration_ms": duration_ms,
"outcome": outcome,
"edge_headers": edge_headers or {k: None for k in _EDGE_ALLOW},
"metadata": metadata or {},
}
def _now_iso() -> str:
"""ISO-8601 millisecond-precision timestamp with a ``Z`` suffix."""
# ``time.gmtime`` + manual ms keeps this dependency-free and 3.9-safe.
now = time.time()
secs = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(now))
ms = int((now - int(now)) * 1000)
return f"{secs}.{ms:03d}Z"
def emit_backend_boundary(
boundary: str,
*,
outcome: str = "info",
test_id: str,
slug: str,
demo: str,
metadata: Optional[Dict[str, Any]] = None,
edge_headers: Optional[Dict[str, Optional[str]]] = None,
duration_ms: Optional[int] = None,
parent_span_id: Optional[str] = None,
) -> None:
"""Emit one backend boundary envelope, honoring the env gate + tier matrix.
No-op when the emitter is disabled or the active tier does not permit this
boundary. Never raises into the caller.
"""
if not cvdiag_backend_enabled():
return
if not _tier_permits(boundary):
return
try:
envelope = build_envelope(
boundary=boundary,
outcome=outcome,
test_id=test_id,
slug=slug,
demo=demo,
metadata=metadata,
edge_headers=edge_headers,
duration_ms=duration_ms,
parent_span_id=parent_span_id,
)
emit_cvdiag(envelope)
except Exception as err: # noqa: BLE001 - instrumentation must not throw
logger.warning("CVDIAG backend emit-failed boundary=%s error=%s", boundary, err)
# ── Per-request correlation context ─────────────────────────────────────────
class _RequestCtx:
"""Holds the per-request correlation identity + timing the boundaries share.
Carried on ``request.state`` so the middleware, the LLM hook, and the agent
hooks all stamp the same ``test_id`` / ``slug`` / ``demo`` onto their
envelopes.
"""
__slots__ = (
"test_id",
"slug",
"demo",
"ingress_mono_ns",
"sse_seq",
"first_byte_emitted",
"bytes_streamed",
)
def __init__(self, *, test_id: str, slug: str, demo: str) -> None:
self.test_id = test_id
self.slug = slug
self.demo = demo
self.ingress_mono_ns = time.monotonic_ns()
self.sse_seq = 0
self.first_byte_emitted = False
self.bytes_streamed = 0
def _demo_from_path(path: str) -> str:
"""Derive the ``demo`` label from the mounted sub-app path.
Each demo is mounted at ``/<demo>`` (e.g. ``/voice``, ``/byoc-hashbrown``);
the root agent serves the default demo. Strip the leading slash and any
trailing AG-UI segment so ``/byoc-hashbrown/`` → ``byoc-hashbrown`` and
``/`` → ``default``.
"""
trimmed = path.strip("/")
if not trimmed:
return _DEMO_FALLBACK
return trimmed.split("/", 1)[0] or _DEMO_FALLBACK
# ── HTTP middleware: ingress / first_byte / sse.event / sse.aborted /
# response.complete / error.caught ─────────────────────────────────────────
class CvdiagBackendMiddleware(BaseHTTPMiddleware):
"""Starlette middleware emitting the HTTP-observable backend boundaries.
Wires six of the eleven boundaries around the request lifecycle:
* ``backend.request.ingress`` on entry
* ``backend.sse.first_byte`` on the first streamed chunk
* ``backend.sse.event`` per streamed chunk (debug tier)
* ``backend.sse.aborted`` on premature stream termination
* ``backend.response.complete`` on clean stream close
* ``backend.error.caught`` on any exception escaping the inner app
The agent/LLM boundaries (``agent.enter``, ``llm.call.*``, ``agent.exit``)
are emitted by the agent hooks / LLM httpx hook installed separately, all
keyed on the same ``test_id`` this middleware stamps onto ``request.state``.
Inert when ``CVDIAG_BACKEND_EMITTER`` is off: the dispatch fast-paths to a
bare ``call_next`` with no envelope construction and no response wrapping.
"""
async def dispatch(self, request: Request, call_next) -> Response:
if not cvdiag_backend_enabled():
return await call_next(request)
headers = request.headers
ctx = _RequestCtx(
test_id=normalize_test_id(headers.get(_TEST_ID_HEADER)),
slug=_normalize_slug(headers.get(_AIMOCK_CONTEXT_HEADER)),
demo=_demo_from_path(request.url.path),
)
request.state.cvdiag = ctx
emit_backend_boundary(
"backend.request.ingress",
outcome="info",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
edge_headers=extract_edge_headers(headers),
metadata={
"method": request.method,
"path": request.url.path,
"content_length": _int_or_none(headers.get("content-length")),
},
)
try:
response = await call_next(request)
except Exception as exc: # noqa: BLE001 - observe then re-raise
emit_backend_boundary(
"backend.error.caught",
outcome="err",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
metadata={
"exception_type": type(exc).__name__,
"message_scrubbed": scrub(str(exc)),
"stack_brief": [],
"truncated": False,
},
)
raise
return self._wrap_response(request, response, ctx)
def _wrap_response(
self, request: Request, response: Response, ctx: "_RequestCtx"
) -> Response:
"""Wrap a streaming response so SSE boundaries fire as chunks flow.
Non-streaming responses are returned unwrapped after emitting
``backend.response.complete`` directly.
NOTE: ``BaseHTTPMiddleware`` re-wraps the inner ``StreamingResponse`` as
a private ``_StreamingResponse`` before it reaches us, so an
``isinstance(response, StreamingResponse)`` check is always False here.
Detect streaming by the presence of a ``body_iterator`` (which both the
public and the private response carry) instead.
"""
if not hasattr(response, "body_iterator"):
emit_backend_boundary(
"backend.response.complete",
outcome="ok",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
duration_ms=_elapsed_ms(ctx.ingress_mono_ns),
edge_headers=extract_edge_headers(response.headers),
metadata={
"http_status": response.status_code,
"content_length": _int_or_none(
response.headers.get("content-length")
),
"total_duration_ms": _elapsed_ms(ctx.ingress_mono_ns),
"sse_event_count": ctx.sse_seq,
},
)
return response
inner = response.body_iterator
edge = extract_edge_headers(response.headers)
status = response.status_code
async def _instrumented():
# ``completed`` distinguishes a clean stream exhaustion (→
# response.complete) from an early termination (→ sse.aborted).
#
# IMPORTANT (Starlette ``BaseHTTPMiddleware`` quirk): when the INNER
# endpoint generator raises mid-stream, Starlette swallows the error
# internally and our ``async for`` simply ends — we never see an
# exception there. The abort surface we CAN observe is the consumer
# tearing the stream down early (client disconnect), which closes
# this generator and raises ``GeneratorExit`` / ``CancelledError``
# into it. We therefore catch ``BaseException`` (not just
# ``Exception``) so a disconnect-driven abort is captured, and emit
# ``backend.response.complete`` only on a clean exhaustion.
completed = False
terminated_kind = "rst"
try:
async for chunk in inner:
ctx.bytes_streamed += len(chunk) if chunk else 0
if not ctx.first_byte_emitted:
ctx.first_byte_emitted = True
emit_backend_boundary(
"backend.sse.first_byte",
outcome="info",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
edge_headers=edge,
metadata={
"delta_ms_from_ingress": _elapsed_ms(
ctx.ingress_mono_ns
)
},
)
emit_backend_boundary(
"backend.sse.event",
outcome="info",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
metadata={
"event_type": "chunk",
"payload_size_bytes": len(chunk) if chunk else 0,
"sequence_num": ctx.sse_seq,
},
)
ctx.sse_seq += 1
yield chunk
completed = True
except BaseException as exc: # noqa: BLE001 - observe abort then re-raise
# GeneratorExit (disconnect) and CancelledError carry no
# message; an in-iterator error would. Pick a termination_kind.
terminated_kind = (
"rst"
if isinstance(exc, (GeneratorExit,))
else (
"timeout"
if isinstance(exc, asyncio.CancelledError)
else "chunk_error"
)
)
raise
finally:
if completed:
emit_backend_boundary(
"backend.response.complete",
outcome="ok",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
duration_ms=_elapsed_ms(ctx.ingress_mono_ns),
edge_headers=edge,
metadata={
"http_status": status,
"content_length": ctx.bytes_streamed,
"total_duration_ms": _elapsed_ms(ctx.ingress_mono_ns),
"sse_event_count": ctx.sse_seq,
},
)
else:
emit_backend_boundary(
"backend.sse.aborted",
outcome="err",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
edge_headers=edge,
metadata={
"termination_kind": terminated_kind,
"bytes_before_abort": ctx.bytes_streamed,
},
)
response.body_iterator = _instrumented()
return response
def _int_or_none(raw: Any) -> Optional[int]:
"""Parse an int header value, returning ``None`` on absence / malformed."""
if raw is None:
return None
try:
return int(raw)
except (TypeError, ValueError):
return None
def _elapsed_ms(start_mono_ns: int) -> int:
"""Whole milliseconds elapsed since a ``time.monotonic_ns`` start mark."""
return max(0, (time.monotonic_ns() - start_mono_ns) // 1_000_000)
# ── Agent + LLM boundaries ──────────────────────────────────────────────────
# The LLM-call boundaries (start / heartbeat / response) and the agent
# enter/exit boundaries are emitted via the explicit helpers below. They are
# called from the agent factory's hook points (strands ``HookProvider``) and
# from the outbound httpx event hook, all keyed on the request ``ctx``.
def emit_agent_enter(ctx: "_RequestCtx", *, agent_name: str, model_id: str) -> None:
"""Emit ``backend.agent.enter`` (default tier)."""
emit_backend_boundary(
"backend.agent.enter",
outcome="info",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
metadata={"agent_name": agent_name, "model_id": model_id},
)
def emit_agent_exit(
ctx: "_RequestCtx", *, terminal_outcome: str, total_duration_ms: int
) -> None:
"""Emit ``backend.agent.exit`` (default tier)."""
emit_backend_boundary(
"backend.agent.exit",
outcome="ok" if terminal_outcome == "ok" else "err",
test_id=ctx.test_id,
slug=ctx.slug,
demo=ctx.demo,
duration_ms=total_duration_ms,
metadata={
"terminal_outcome": terminal_outcome,
"total_duration_ms": total_duration_ms,
},
)
class LlmCallScope:
"""Async context manager spanning one outbound LLM call.
On ``__aenter__`` emits ``backend.llm.call.start`` and launches a heartbeat
task that emits ``backend.llm.call.heartbeat`` every ``interval_s`` (≈10s)
while the call is outstanding (verbose tier). On ``__aexit__`` emits
``backend.llm.call.response`` with the measured latency.
All emission is gated/tiered through ``emit_backend_boundary``, so with the
emitter off or at default tier this scope is effectively free (the
heartbeat task still ticks but every emit is suppressed; callers that want
zero task overhead can skip the scope when ``cvdiag_backend_enabled()`` is
false).
"""
def __init__(
self,
ctx: "_RequestCtx",
*,
provider: str,
model: str,
prompt_token_count_estimate: int = 0,
interval_s: float = 10.0,
) -> None:
self._ctx = ctx
self._provider = provider
self._model = model
self._prompt_tokens = prompt_token_count_estimate
self._interval_s = interval_s
self._start_mono_ns = 0
self._hb_task: Optional[asyncio.Task] = None
async def __aenter__(self) -> "LlmCallScope":
self._start_mono_ns = time.monotonic_ns()
emit_backend_boundary(
"backend.llm.call.start",
outcome="info",
test_id=self._ctx.test_id,
slug=self._ctx.slug,
demo=self._ctx.demo,
metadata={
"provider": self._provider,
"model": self._model,
"prompt_token_count_estimate": self._prompt_tokens,
},
)
self._hb_task = asyncio.ensure_future(self._heartbeat())
return self
async def _heartbeat(self) -> None:
try:
while True:
await asyncio.sleep(self._interval_s)
emit_backend_boundary(
"backend.llm.call.heartbeat",
outcome="info",
test_id=self._ctx.test_id,
slug=self._ctx.slug,
demo=self._ctx.demo,
metadata={
"elapsed_ms_since_start": _elapsed_ms(self._start_mono_ns)
},
)
except asyncio.CancelledError: # normal shutdown on call completion
return
async def __aexit__(self, exc_type, exc, tb) -> bool:
if self._hb_task is not None:
hb_task = self._hb_task
self._hb_task = None
hb_task.cancel()
try:
await hb_task
except asyncio.CancelledError:
# Cooperative cancellation (was ``except (CancelledError,
# Exception)``, which swallowed the CALLER's cancel and broke
# cooperative cancellation). Suppress ONLY the heartbeat task's
# OWN cancellation — the one we just requested. If THIS task is
# being cancelled by the caller (a pending cancellation request,
# ``current_task().cancelling() > 0``), the CancelledError is the
# caller's and MUST propagate. ``Task.cancelling()`` is 3.11+
# (production runs 3.12); on older runtimes the attribute is
# absent and we degrade to suppressing (the legacy behavior).
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
pass
emit_backend_boundary(
"backend.llm.call.response",
outcome="err" if exc_type is not None else "ok",
test_id=self._ctx.test_id,
slug=self._ctx.slug,
demo=self._ctx.demo,
duration_ms=_elapsed_ms(self._start_mono_ns),
metadata={
"provider": self._provider,
"model": self._model,
"response_token_count": None,
"latency_ms": _elapsed_ms(self._start_mono_ns),
"error_class": type(exc).__name__ if exc is not None else None,
},
)
return False # never suppress the underlying exception
@@ -0,0 +1,467 @@
"""Standalone header-forwarding shim for showcase integrations.
Forward CopilotKit request-context headers (e.g. ``x-aimock-context``)
onto outbound LLM/provider HTTP calls so the locally-served aimock test
server can match the right fixture for each in-flight showcase request.
This module is a SELF-CONTAINED port of the langgraph-python reference
shim at ``copilotkit/header_propagation.py`` plus a small Starlette HTTP
middleware that extracts inbound ``x-*`` headers at request scope.
It is intentionally duplicated into every Python showcase integration
that does NOT already depend on the ``copilotkit`` SDK so each backend
has a single self-contained file it can import without adding a heavy
``copilotkit`` (langchain-pulling) dependency.
What this module does
---------------------
Three things, kept deliberately small:
1. ``HeaderForwardingHTTPMiddleware`` — a Starlette/FastAPI HTTP
middleware that, on every inbound request, extracts ``x-*`` prefixed
headers and stashes them on a per-request ``contextvars.ContextVar``.
2. ``install_httpx_hook(client)`` — attaches an httpx request event hook
to the given LLM client's underlying httpx client (walking the
``._client`` chain that modern provider SDKs wrap their httpx client
behind). The hook copies the recorded headers onto outbound requests.
3. ``set_forwarded_headers`` / ``get_forwarded_headers`` — direct
ContextVar accessors for integrations that need to populate the
header set from a non-HTTP source (e.g. LangGraph's RunnableConfig
``configurable`` channel).
Scope and limits
----------------
* Only ``x-*`` prefixed headers are forwarded. ``authorization``,
``content-type``, and any other non-``x-*`` headers are dropped.
* Nothing is collected, persisted, or sent anywhere — the module only
attaches headers to an HTTP request that the caller was already going
to make. No telemetry, no out-of-band channel. (Diagnostic CVDIAG
breadcrumbs ARE logged via the stdlib ``logging`` module: header
PRESENCE plus a short value prefix only — never full header values.)
"""
from __future__ import annotations
import contextvars
import logging
import warnings
from typing import Any, Dict, Optional
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
logger = logging.getLogger(__name__)
# CVDIAG correlation-header instrumentation tag for this integration. Each
# showcase backend that copies this shim sets a distinct framework tag so the
# CVDIAG breadcrumb trail identifies which backend captured/forwarded headers.
_CVDIAG_FRAMEWORK = "ms-agent-python"
# Correlation headers carried end-to-end through the showcase request chain.
_DIAG_RUN_ID_HEADER = "x-diag-run-id"
_DIAG_HOPS_HEADER = "x-diag-hops"
_AIMOCK_CONTEXT_HEADER = "x-aimock-context"
_TEST_ID_HEADER = "x-test-id"
def _cvdiag(
boundary: str,
headers: Dict[str, str],
*,
status: str,
hop: object = "-",
error: str = "",
) -> None:
"""Emit a single standardized CVDIAG breadcrumb line.
Logs ONLY header presence + a short value prefix (never full header
values). ``headers`` is the lowercased ``x-*`` header mapping for the
current request context.
"""
slug = headers.get(_AIMOCK_CONTEXT_HEADER)
run_id = headers.get(_DIAG_RUN_ID_HEADER, "none")
test_id = headers.get(_TEST_ID_HEADER, "none")
present = slug is not None
prefix = (slug or "")[:12]
logger.info(
"CVDIAG component=backend-%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_FRAMEWORK,
boundary,
run_id,
slug if present else "MISSING",
"true" if present else "false",
prefix,
hop,
status,
test_id,
error,
)
# Per-request storage for the headers the application has asked to forward
# onto outbound LLM/provider calls.
_forwarded_headers: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
"copilotkit_forwarded_headers"
)
# Marker used to identify hooks we have already installed so the install
# call is idempotent across repeated invocations on the same client.
_HOOK_MARKER = "_copilotkit_forwarded_header_hook"
# Bound on how deep we'll walk a ``._client`` chain looking for event_hooks.
# Modern provider SDKs (OpenAI, Anthropic, pydantic-ai wrappers, agno's
# OpenAIChat, strands' OpenAIModel) wrap their httpx client behind 2-4
# layers of ``._client`` indirection; 5 hops is enough headroom without
# risking pathological loops.
_MAX_CHAIN_DEPTH = 5
def set_forwarded_headers(headers: Dict[str, str]) -> None:
"""Record headers to forward onto outbound LLM/provider calls.
Only ``x-*`` prefixed headers are kept; everything else is dropped.
"""
filtered = {k.lower(): v for k, v in headers.items() if k.lower().startswith("x-")}
_forwarded_headers.set(filtered)
def get_forwarded_headers() -> Dict[str, str]:
"""Return the headers recorded for the current request context."""
return _forwarded_headers.get({})
class HeaderForwardingHTTPMiddleware(BaseHTTPMiddleware):
"""Starlette/FastAPI middleware that captures inbound ``x-*`` headers.
On every inbound HTTP request, copies all ``x-*`` prefixed headers
onto the per-request ContextVar so any outbound httpx call made
inside the request scope (the LLM call hop 2) sees them via
``get_forwarded_headers()`` and the installed httpx event hook.
"""
async def dispatch(self, request: Request, call_next) -> Response:
headers = {
k: v for k, v in request.headers.items() if k.lower().startswith("x-")
}
set_forwarded_headers(headers)
captured = {k.lower(): v for k, v in headers.items()}
_cvdiag(
"contextvar-capture",
captured,
status="ok" if _AIMOCK_CONTEXT_HEADER in captured else "miss",
)
return await call_next(request)
def _find_event_hooks_target(client: Any) -> Optional[Any]:
"""Walk ``._client`` chain looking for the first httpx-style event_hooks.
Returns the target object, or ``None`` if not found within
``_MAX_CHAIN_DEPTH`` hops.
"""
current = client
for _ in range(_MAX_CHAIN_DEPTH + 1):
if current is None:
return None
if hasattr(current, "event_hooks"):
return current
nxt = getattr(current, "_client", None)
if nxt is current or nxt is None:
return None
current = nxt
return None
def _is_async_httpx_target(target: Any) -> bool:
"""Best-effort detection: is this an httpx async client?
Detection is HIGH-CONFIDENCE when ``isinstance`` against the real
``httpx.AsyncClient`` / ``httpx.Client`` succeeds. The MRO name-only
fallback (matching a class literally named ``AsyncClient``) is
LOW-CONFIDENCE: a wrapped/duck-typed client whose class happens to be
named ``AsyncClient`` (or that is async but is NOT so named) can be
misclassified, which would install a sync hook on an async client (an
un-awaited coroutine → silent header drop) or vice versa. Each path
emits a CVDIAG breadcrumb tagged with the chosen confidence so a
misdetection is greppable in the logs. The return values themselves are
unchanged — only the diagnostics are new.
"""
try:
import httpx
if isinstance(target, httpx.AsyncClient):
_cvdiag(
"async-detect",
{},
status="ok",
error="path=isinstance-async confidence=high",
)
return True
if isinstance(target, httpx.Client):
_cvdiag(
"async-detect",
{},
status="ok",
error="path=isinstance-sync confidence=high",
)
return False
except ImportError: # pragma: no cover
pass
# Fall back to exact class-name match for wrapped/duck-typed clients.
# LOW-CONFIDENCE: this can misdetect async-vs-sync for oddly-named
# wrappers; the breadcrumb records the fallback so a wrong hook kind is
# traceable to this path.
for cls in type(target).__mro__:
if cls.__name__ == "AsyncClient":
_cvdiag(
"async-detect",
{},
status="ok",
error=(
"path=mro-name-match confidence=low "
f"target_type={type(target).__name__}"
),
)
return True
_cvdiag(
"async-detect",
{},
status="ok",
error=(f"path=default-sync confidence=low target_type={type(target).__name__}"),
)
return False
def _inject_diag_hop(request: Any, headers: Dict[str, str]) -> None:
"""Append this backend's hop tag to ``x-diag-hops`` on the outbound
request and emit the ``outbound-llm`` CVDIAG breadcrumb.
``x-diag-hops`` is a comma-separated trail of the backends that touched
the request; appending ``backend-<framework>`` here records that this
integration forwarded the correlation headers onto the LLM/provider
call. ``x-diag-run-id`` is carried verbatim (already copied above via
the ``headers`` loop) the same way ``x-aimock-context`` is.
GATED on diagnostic-header presence: the breadcrumb append and the
outbound CVDIAG log fire ONLY when the forwarded headers carry a
diagnostic header (``x-diag-run-id`` OR ``x-aimock-context``). When
NEITHER is present this is a no-op, so the outbound request is
byte-identical to pre-instrumentation behavior.
"""
if _DIAG_RUN_ID_HEADER not in headers and _AIMOCK_CONTEXT_HEADER not in headers:
return
hop_tag = f"backend-{_CVDIAG_FRAMEWORK}"
existing = headers.get(_DIAG_HOPS_HEADER, "")
trail = [h for h in (existing.split(",") if existing else []) if h]
trail.append(hop_tag)
new_hops = ",".join(trail)
request.headers[_DIAG_HOPS_HEADER] = new_hops
_cvdiag(
"outbound-llm",
headers,
status="ok" if _AIMOCK_CONTEXT_HEADER in headers else "miss",
hop=len(trail),
)
def install_httpx_hook(client: Any) -> None:
"""Attach an httpx request event hook to ``client``'s httpx client.
Walks the ``._client`` chain to find the first object with an
``event_hooks`` mapping, then appends a request hook that copies the
ContextVar-recorded headers onto each outbound request.
Works with OpenAI / Anthropic / pydantic-ai / agno / strands client
wrappers (all wrap httpx internally), as well as raw
``httpx.Client`` / ``httpx.AsyncClient`` instances.
Idempotent: a marker attribute on the installed callable prevents
double-installation on the same target.
"""
target = _find_event_hooks_target(client)
if target is None:
msg = (
f"install_httpx_hook: client of type {type(client).__name__} has no "
"recognized event_hooks attribute; x-* headers will NOT be forwarded "
"for this client"
)
warnings.warn(msg, stacklevel=2)
# warnings.warn is invisible in most prod runtimes (filtered/once);
# ALSO log at WARNING so a non-forwarding client surfaces.
logger.warning("CVDIAG boundary=hook-install status=error error=%s", msg)
_cvdiag("hook-install", {}, status="error", error="no-event-hooks-target")
return
request_hooks = target.event_hooks.get("request", [])
# Idempotency: don't double-install on the same target.
for existing in request_hooks:
if getattr(existing, _HOOK_MARKER, False):
return
is_async = _is_async_httpx_target(target)
if is_async:
async def _inject_headers_async(request):
headers = get_forwarded_headers()
for key, value in headers.items():
request.headers[key] = value
_inject_diag_hop(request, headers)
setattr(_inject_headers_async, _HOOK_MARKER, True)
request_hooks.append(_inject_headers_async)
else:
def _inject_headers(request):
headers = get_forwarded_headers()
for key, value in headers.items():
request.headers[key] = value
_inject_diag_hop(request, headers)
setattr(_inject_headers, _HOOK_MARKER, True)
request_hooks.append(_inject_headers)
target.event_hooks["request"] = request_hooks
# Module-scope sentinel preventing repeated global patching.
_GLOBAL_HTTPX_PATCHED = False
def install_global_httpx_hook() -> None:
"""Patch ``httpx.Client`` / ``httpx.AsyncClient`` so EVERY future
instance auto-attaches the forwarded-header hook on construction.
Use this when the LLM client is buried behind opaque framework
machinery (AG2's ``ConversableAgent`` constructs OpenAI clients
lazily, CrewAI uses litellm which constructs httpx clients per-call,
etc.) and there is no single client instance to call
:func:`install_httpx_hook` on at startup.
Safe to call at import time. Idempotent: a module-scope sentinel
prevents repeated patching, and the per-instance idempotency check
in :func:`install_httpx_hook` prevents double-hooking on each new
client. Pre-existing ``httpx.Client`` instances are not retroactively
hooked — only those constructed AFTER this call.
"""
global _GLOBAL_HTTPX_PATCHED
if _GLOBAL_HTTPX_PATCHED:
return
try:
import httpx
except ImportError: # pragma: no cover
return
_orig_sync_init = httpx.Client.__init__
_orig_async_init = httpx.AsyncClient.__init__
def _patched_sync_init(self, *args, **kwargs):
_orig_sync_init(self, *args, **kwargs)
try:
install_httpx_hook(self)
except Exception as exc: # pragma: no cover - never break client construction
# A failed hook install means x-aimock-context silently never
# forwards (the whole point of this shim). Keep swallowing the
# exception so client construction never breaks, but FAIL LOUD:
# log at ERROR with the FULL detail (not 80-char-truncated) so a
# broken install is visible, not buried at INFO.
detail = f"sync-init {type(exc).__name__}: {exc}"
logger.error(
"CVDIAG boundary=hook-install status=error error=%s",
detail,
exc_info=True,
)
_cvdiag("hook-install", {}, status="error", error=detail)
def _patched_async_init(self, *args, **kwargs):
_orig_async_init(self, *args, **kwargs)
try:
install_httpx_hook(self)
except Exception as exc: # pragma: no cover
# See _patched_sync_init: swallow to protect construction, but
# FAIL LOUD at ERROR with full detail so a broken install (which
# silently drops x-aimock-context forwarding) is visible.
detail = f"async-init {type(exc).__name__}: {exc}"
logger.error(
"CVDIAG boundary=hook-install status=error error=%s",
detail,
exc_info=True,
)
_cvdiag("hook-install", {}, status="error", error=detail)
httpx.Client.__init__ = _patched_sync_init
httpx.AsyncClient.__init__ = _patched_async_init
_GLOBAL_HTTPX_PATCHED = True
# Module-scope sentinel preventing repeated executor patching.
_EXECUTOR_CTXVAR_PATCHED = False
def install_executor_contextvar_propagation() -> None:
"""Patch ``asyncio.events.AbstractEventLoop.run_in_executor`` so the
parent task's ContextVars are propagated into the executor thread.
Why this exists
---------------
Frameworks that dispatch a SYNC callable (e.g. agent_framework running
a sync tool, or a hand-rolled secondary LLM call) onto the default
thread pool via ``loop.run_in_executor(None, functools.partial(...))``
lose the caller's context: the stock ``run_in_executor`` does NOT copy
the caller's :pep:`567` context to the worker thread — so the
:class:`HeaderForwardingHTTPMiddleware` ContextVar (set on the inbound
request task) is empty inside the executor, and our outbound httpx
hook sees no headers to forward.
``asyncio.to_thread`` (Python 3.9+) does copy context the right way;
this patch makes plain ``run_in_executor`` behave the same. It only
affects functions submitted via ``run_in_executor`` — coroutines and
other constructs are unaffected.
Safe to call at import time. Idempotent via a module-scope sentinel.
Scope caveat: this patches ``asyncio.base_events.BaseEventLoop`` only.
Pre-existing *stdlib asyncio* event-loop instances inherit the patch
(``run_in_executor`` is defined on ``BaseEventLoop`` and resolved
per-call via normal method resolution). It is INERT under uvloop —
uvloop's loop does not subclass ``BaseEventLoop`` and resolves
``run_in_executor`` from its own C implementation, so the stdlib
method this patch rebinds is never consulted. Under uvloop, ContextVar
propagation into ``run_in_executor`` worker threads is NOT provided by
this shim.
"""
global _EXECUTOR_CTXVAR_PATCHED
if _EXECUTOR_CTXVAR_PATCHED:
return
import asyncio.base_events as _base_events
_orig_run_in_executor = _base_events.BaseEventLoop.run_in_executor
def _patched_run_in_executor(self, executor, func, *args):
# Capture the CURRENT task's context at submit time, then run the
# submitted callable inside that context on the worker thread.
ctx = contextvars.copy_context()
def _ctx_wrapper(*a, **kw):
return ctx.run(func, *a, **kw)
# Preserve __name__/__qualname__ for nicer tracebacks where possible.
try:
_ctx_wrapper.__wrapped__ = func # type: ignore[attr-defined]
except Exception: # pragma: no cover
pass
return _orig_run_in_executor(self, executor, _ctx_wrapper, *args)
_base_events.BaseEventLoop.run_in_executor = _patched_run_in_executor
_EXECUTOR_CTXVAR_PATCHED = True
@@ -0,0 +1,166 @@
"""
MS Agent Framework agent for the Declarative Generative UI (A2UI — Dynamic Schema) demo.
Pattern (ported from the LangGraph reference
`showcase/integrations/langgraph-python/src/agents/a2ui_dynamic.py`):
- The agent binds an explicit `generate_a2ui` tool. When called, it invokes a
secondary LLM bound to `_design_a2ui_surface` (tool_choice forced) and returns the
resulting `a2ui_operations` container.
- The runtime (see `src/app/api/copilotkit-declarative-gen-ui/route.ts`) uses
`injectA2UITool: false` because the tool binding is owned by the agent here
(double-injection would duplicate the tool slot).
"""
from __future__ import annotations
import json
from textwrap import dedent
from typing import Annotated, Any
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent
from pydantic import Field
from tools import build_a2ui_operations_from_tool_call
CUSTOM_CATALOG_ID = "declarative-gen-ui-catalog"
@tool(
name="generate_a2ui",
description=(
"Generate dynamic A2UI components based on the conversation. "
"A secondary LLM designs the UI schema and data."
),
)
def generate_a2ui(
context: Annotated[
str,
# Default to empty so the primary LLM can call generate_a2ui() with
# no args (aimock fixtures return `arguments: "{}"`); pydantic rejects
# missing-context calls with "Argument parsing failed" before the
# function body runs. Same pattern as
# `src/agents/beautiful_chat.py::generate_a2ui`.
Field(default="", description="Conversation context to generate UI from."),
] = "",
session: Any = None,
) -> str:
"""Generate dynamic A2UI dashboard from conversation context."""
from openai import OpenAI
# Pull the latest user message from the active agent session so the
# secondary LLM call sees what the user actually asked for. Without this,
# aimock's substring matcher can't distinguish between "KPI dashboard",
# "pie chart", and "bar chart" pills — they'd all hit the first matching
# fixture. `session` is the AgentSession injected by agent_framework
# (see `_tools.py:1483`). When unavailable (e.g. direct calls in tests),
# we fall back to the caller-supplied `context` string.
latest_user_message = ""
if session is not None:
try:
messages = list(getattr(session, "input_messages", []) or [])
for msg in reversed(messages):
if getattr(msg, "role", None) == "user":
text = getattr(msg, "text", None) or str(
getattr(msg, "content", "") or ""
)
if text:
latest_user_message = text
break
except Exception:
latest_user_message = ""
client = OpenAI()
tool_schema = {
"type": "function",
"function": {
"name": "_design_a2ui_surface",
"description": "Render a dynamic A2UI v0.9 surface.",
"parameters": {
"type": "object",
"properties": {
"surfaceId": {"type": "string"},
"catalogId": {"type": "string"},
"components": {"type": "array", "items": {"type": "object"}},
"data": {"type": "object"},
},
"required": ["surfaceId", "catalogId", "components"],
},
},
}
# Build the secondary-LLM user message. Priority:
# 1. `latest_user_message` from the active AgentSession (preferred —
# lets aimock's substring matcher pick the right fixture per pill).
# 2. The caller-supplied `context` arg (LangGraph-style summary).
# 3. A generic catch-all that contains all four d5 demo keywords so
# direct/test invocations still match SOME fixture instead of
# falling through to the real-OpenAI proxy.
user_content = (
latest_user_message
or context
or "KPI dashboard with 3-4 metrics, pie chart sales by region, "
"bar chart quarterly revenue, status report."
)
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{
"role": "system",
"content": (
f"Generate a useful dashboard UI. Use catalogId='{CUSTOM_CATALOG_ID}'."
),
},
{"role": "user", "content": user_content},
],
tools=[tool_schema],
tool_choice={"type": "function", "function": {"name": "_design_a2ui_surface"}},
)
if not response.choices[0].message.tool_calls:
return json.dumps({"error": "LLM did not call _design_a2ui_surface"})
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
# Default the catalog to the dynamic-gen-ui catalog if the LLM omitted it.
args.setdefault("catalogId", CUSTOM_CATALOG_ID)
result = build_a2ui_operations_from_tool_call(args)
return json.dumps(result)
SYSTEM_PROMPT = dedent(
"""
You are a demo assistant for Declarative Generative UI (A2UI — Dynamic
Schema). Whenever a response would benefit from a rich visual — a
dashboard, status report, KPI summary, card layout, info grid, a
pie/donut chart of part-of-whole breakdowns, a bar chart comparing
values across categories, or anything more structured than plain text —
call `generate_a2ui` to draw it. The registered catalog includes
`Card`, `StatusBadge`, `Metric`, `InfoRow`, `PrimaryButton`, `PieChart`,
and `BarChart` (in addition to the basic A2UI primitives). Prefer
`PieChart` for part-of-whole breakdowns (sales by region, traffic
sources, portfolio allocation) and `BarChart` for comparisons across
categories (quarterly revenue, headcount by team, signups per month).
`generate_a2ui` takes a `context` string summarising the user's request
and handles the rendering automatically. Keep chat replies to one short
sentence; let the UI do the talking.
"""
).strip()
def create_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the MS-Agent-backed declarative-gen-ui agent."""
base_agent = Agent(
client=chat_client,
name="declarative_gen_ui_agent",
instructions=SYSTEM_PROMPT,
tools=[generate_a2ui],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMicrosoftAgentFrameworkAgent",
description="Dynamic A2UI generator that designs rich UI surfaces on demand.",
require_confirmation=False,
)
@@ -0,0 +1,139 @@
"""
MS Agent Framework 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. 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:
showcase/integrations/langgraph-python/src/agents/a2ui_fixed.py
"""
# @region[backend-render-operations]
# @region[backend-schema-json-load]
from __future__ import annotations
import json
from pathlib import Path
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent
from pydantic import Field
CATALOG_ID = "copilotkit://flight-fixed-catalog"
SURFACE_ID = "flight-fixed-schema"
_SCHEMAS_DIR = Path(__file__).parent / "a2ui_schemas"
def _load_schema(path: Path) -> list[dict]:
"""Thin JSON loader — mirrors `a2ui.load_schema` from the Python SDK."""
with open(path) as f:
return json.load(f)
FLIGHT_SCHEMA = _load_schema(_SCHEMAS_DIR / "flight_schema.json")
BOOKED_SCHEMA = _load_schema(_SCHEMAS_DIR / "booked_schema.json") # noqa: F841 — kept for parity with LangGraph reference
# @endregion[backend-schema-json-load]
def _build_a2ui_ops(*, origin: str, destination: str, airline: str, price: str) -> dict:
"""Return the `a2ui_operations` payload for the flight card.
Mirrors `a2ui.render(operations=[create_surface, update_components,
update_data_model])` from the Python SDK. The A2UI middleware detects this
container in the tool result and forwards the ops to the frontend renderer.
"""
ops = [
{
"version": "v0.9",
"createSurface": {
"surfaceId": SURFACE_ID,
"catalogId": CATALOG_ID,
},
},
{
"version": "v0.9",
"updateComponents": {
"surfaceId": SURFACE_ID,
"components": FLIGHT_SCHEMA,
},
},
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": SURFACE_ID,
"path": "/",
"value": {
"origin": origin,
"destination": destination,
"airline": airline,
"price": price,
},
},
},
]
return {"a2ui_operations": ops}
# @endregion[backend-render-operations]
@tool(
name="display_flight",
description=(
"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'."
),
)
def display_flight(
origin: Annotated[
str, Field(description="3-letter origin airport code (e.g. 'SFO').")
],
destination: Annotated[
str, Field(description="3-letter destination airport code (e.g. 'JFK').")
],
airline: Annotated[str, Field(description="Airline name (e.g. 'United').")],
price: Annotated[
str, Field(description="Price string including currency, e.g. '$289'.")
],
) -> str:
"""Emit an `a2ui_operations` container describing the flight card."""
return json.dumps(
_build_a2ui_ops(
origin=origin,
destination=destination,
airline=airline,
price=price,
)
)
SYSTEM_PROMPT = dedent(
"""
You help users find flights. When asked about a flight, call
`display_flight` with origin, destination, airline, and price.
Keep any chat reply to one short sentence.
"""
).strip()
def create_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the MS-Agent-backed a2ui-fixed-schema agent."""
base_agent = Agent(
client=chat_client,
name="a2ui_fixed_agent",
instructions=SYSTEM_PROMPT,
tools=[display_flight],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMicrosoftAgentFrameworkAgent",
description="Fixed-schema A2UI flight search demo.",
require_confirmation=False,
)
@@ -0,0 +1,20 @@
[
{
"id": "root",
"component": "Column",
"gap": 8,
"children": ["title", "detail"]
},
{
"id": "title",
"component": "Text",
"text": { "path": "/title" },
"variant": "h2"
},
{
"id": "detail",
"component": "Text",
"text": { "path": "/detail" },
"variant": "body"
}
]
@@ -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,273 @@
"""
MS Agent Framework agent with sales todos state, weather tool, query data,
and HITL schedule meeting tool.
Adapted from examples/integrations/ms-agent-framework-python/agent/src/agent.py
"""
# @region[weather-tool-backend]
from __future__ import annotations
import json
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent
from pydantic import Field
# =====================================================================
# Shared tool implementations
# =====================================================================
from tools import (
get_weather_impl,
query_data_impl,
manage_sales_todos_impl,
get_sales_todos_impl,
schedule_meeting_impl,
search_flights_impl,
build_a2ui_operations_from_tool_call,
)
STATE_SCHEMA: dict[str, object] = {
"salesTodos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"stage": {"type": "string"},
"value": {"type": "number"},
"dueDate": {"type": "string"},
"assignee": {"type": "string"},
"completed": {"type": "boolean"},
},
},
"description": "Ordered list of the user's sales pipeline todos.",
}
}
PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
"salesTodos": {
"tool": "manage_sales_todos",
"tool_argument": "todos",
}
}
@tool(
name="manage_sales_todos",
description=(
"Replace the entire list of sales todos with the provided values. "
"Always include every todo you want to keep."
),
)
def manage_sales_todos(
todos: Annotated[
list[dict],
Field(
description=(
"The complete source of truth for the user's sales todos. "
"Maintain ordering and include the full list on each call."
)
),
],
) -> str:
"""Persist the provided set of sales todos."""
result = manage_sales_todos_impl(todos)
return f"Sales todos updated. Tracking {len(result)} item(s)."
@tool(
name="get_sales_todos",
description="Get the current list of sales todos.",
)
def get_sales_todos() -> str:
"""Return the current sales todos or defaults."""
result = get_sales_todos_impl()
return json.dumps(result)
@tool(
name="get_weather",
description="Get the current weather for a location. Use this to render the frontend weather card.",
)
def get_weather(
location: Annotated[
str,
Field(
description="The city or region to describe. Use fully spelled out names."
),
],
) -> str:
"""Return weather data as JSON for UI rendering."""
result = get_weather_impl(location)
return json.dumps(result)
# @endregion[weather-tool-backend]
@tool(
name="query_data",
description="Query the database. Takes natural language. Always call before showing a chart or graph.",
)
def query_data(
query: Annotated[
str, Field(description="Natural language query to run against the database.")
],
) -> str:
"""Query the database and return results as JSON."""
result = query_data_impl(query)
return json.dumps(result)
@tool(
name="schedule_meeting",
description="Schedule a meeting. The user will be asked to pick a time via the meeting time picker UI.",
approval_mode="always_require",
)
def schedule_meeting(
reason: Annotated[str, Field(description="Reason for scheduling the meeting.")],
duration_minutes: Annotated[
int, Field(description="Duration of the meeting in minutes.")
] = 30,
) -> str:
"""Request human approval to schedule a meeting."""
result = schedule_meeting_impl(reason, duration_minutes)
return json.dumps(result)
@tool(
name="search_flights",
description=(
"Search for flights and display the results as rich A2UI cards. Return exactly 2 flights. "
"Each flight must have: airline, airlineLogo, flightNumber, origin, destination, "
"date, departureTime, arrivalTime, duration, status, statusColor, price, currency."
),
)
def search_flights(
flights: Annotated[
list[dict],
Field(description="List of flight objects to search and display."),
],
) -> str:
"""Search for flights and display as rich cards."""
result = search_flights_impl(flights)
return json.dumps(result)
@tool(
name="generate_a2ui",
description=(
"Generate dynamic A2UI components based on the conversation. "
"A secondary LLM designs the UI schema and data."
),
)
def generate_a2ui(
context: Annotated[
str, Field(description="Conversation context to generate UI from.")
],
) -> str:
"""Generate dynamic A2UI dashboard from conversation context."""
from openai import OpenAI
client = OpenAI()
tool_schema = {
"type": "function",
"function": {
"name": "_design_a2ui_surface",
"description": "Render a dynamic A2UI v0.9 surface.",
"parameters": {
"type": "object",
"properties": {
"surfaceId": {"type": "string"},
"catalogId": {"type": "string"},
"components": {"type": "array", "items": {"type": "object"}},
"data": {"type": "object"},
},
"required": ["surfaceId", "catalogId", "components"],
},
},
}
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": context or "Generate a useful dashboard UI."},
{
"role": "user",
"content": "Generate a dynamic A2UI dashboard based on the conversation.",
},
],
tools=[tool_schema],
tool_choice={"type": "function", "function": {"name": "_design_a2ui_surface"}},
)
if not response.choices[0].message.tool_calls:
return json.dumps({"error": "LLM did not call _design_a2ui_surface"})
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
result = build_a2ui_operations_from_tool_call(args)
return json.dumps(result)
def create_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the CopilotKit demo agent backed by Microsoft Agent Framework."""
base_agent = Agent(
client=chat_client,
name="sales_agent",
instructions=dedent(
"""
You help users manage their sales pipeline, check weather, query data, and schedule meetings.
State sync:
- The current list of sales todos is provided in the conversation context.
- When you add, remove, or reorder todos, call `manage_sales_todos` with the full list.
Never send partial updates--always include every todo that should exist.
- CRITICAL: When asked to "add" a todo, you must:
1. First, identify ALL existing todos from the conversation history
2. Create EXACTLY ONE new todo (never more than one unless explicitly requested)
3. Call manage_sales_todos with: [all existing todos] + [the one new todo]
- When asked to "remove" a todo, remove exactly ONE item unless user specifies otherwise.
Tool usage rules:
- When user asks to schedule a meeting, you MUST call the `schedule_meeting` tool immediately.
Do NOT ask for approval yourself--the tool's approval workflow and the client UI will handle it.
Frontend integrations:
- `get_weather` renders a weather card in the UI. Only call this tool when the user explicitly
asks for weather. Do NOT call it after unrelated tasks or approvals.
- `query_data` fetches database records. Always call before showing charts or graphs.
- `schedule_meeting` requires explicit user approval before you proceed. Only use it when a
user asks to schedule or set up a meeting. Always call the tool instead of asking manually.
Conversation tips:
- Reference the latest todo list before suggesting changes.
- Keep responses concise and friendly unless the user requests otherwise.
- After you finish executing tools for the user's request, provide a brief, final assistant
message summarizing exactly what changed. Do NOT call additional tools or switch topics
after that summary unless the user asks. ALWAYS send this conversational summary so the message persists.
""".strip()
),
tools=[
manage_sales_todos,
get_sales_todos,
get_weather,
query_data,
schedule_meeting,
search_flights,
generate_a2ui,
],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMicrosoftAgentFrameworkAgent",
description="Manages sales pipeline todos, weather, data queries, and meeting scheduling.",
predict_state_config=PREDICT_STATE_CONFIG,
require_confirmation=False,
)
@@ -0,0 +1,162 @@
"""MS Agent Framework agent backing the Agent Config Object demo.
Reads three forwarded properties -- tone, expertise, responseLength -- from the
AG-UI run input's ``forwardedProps`` and composes its system prompt dynamically
per turn.
The CopilotKit provider's ``properties`` prop is wired through the runtime as
``forwardedProps`` on each AG-UI run. Because Microsoft Agent Framework agents
store their system prompt in ``default_options["instructions"]``, we subclass
``AgentFrameworkAgent`` and intercept ``run`` to swap in a freshly-built
instruction string for the duration of each invocation.
Invalid or missing values fall back to the corresponding ``DEFAULT_*``
constant -- this function never raises so the demo can't deadlock on a bad
payload.
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from textwrap import dedent
from typing import Any, Literal
from ag_ui.core import BaseEvent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
Tone = Literal["professional", "casual", "enthusiastic"]
Expertise = Literal["beginner", "intermediate", "expert"]
ResponseLength = Literal["concise", "detailed"]
DEFAULT_TONE: Tone = "professional"
DEFAULT_EXPERTISE: Expertise = "intermediate"
DEFAULT_RESPONSE_LENGTH: ResponseLength = "concise"
VALID_TONES: set[str] = {"professional", "casual", "enthusiastic"}
VALID_EXPERTISE: set[str] = {"beginner", "intermediate", "expert"}
VALID_RESPONSE_LENGTHS: set[str] = {"concise", "detailed"}
def read_properties(forwarded_props: Any) -> dict[str, str]:
"""Read forwarded props with defensive defaults.
Any missing or unrecognized value falls back to the corresponding
``DEFAULT_*`` constant. Never raises.
"""
props = forwarded_props if isinstance(forwarded_props, dict) else {}
tone = props.get("tone", DEFAULT_TONE)
expertise = props.get("expertise", DEFAULT_EXPERTISE)
response_length = props.get("responseLength", DEFAULT_RESPONSE_LENGTH)
if tone not in VALID_TONES:
tone = DEFAULT_TONE
if expertise not in VALID_EXPERTISE:
expertise = DEFAULT_EXPERTISE
if response_length not in VALID_RESPONSE_LENGTHS:
response_length = DEFAULT_RESPONSE_LENGTH
return {
"tone": tone,
"expertise": expertise,
"response_length": response_length,
}
def build_system_prompt(tone: str, expertise: str, response_length: str) -> str:
"""Compose the system prompt from the three axes."""
tone_rules = {
"professional": ("Use neutral, precise language. No emoji. Short sentences."),
"casual": (
"Use friendly, conversational language. Contractions OK. "
"Light humor welcome."
),
"enthusiastic": (
"Use upbeat, energetic language. Exclamation points OK. Emoji OK."
),
}
expertise_rules = {
"beginner": "Assume no prior knowledge. Define jargon. Use analogies.",
"intermediate": (
"Assume common terms are understood; explain specialized terms."
),
"expert": ("Assume technical fluency. Use precise terminology. Skip basics."),
}
length_rules = {
"concise": "Respond in 1-3 sentences.",
"detailed": ("Respond in multiple paragraphs with examples where relevant."),
}
return (
"You are a helpful assistant.\n\n"
f"Tone: {tone_rules[tone]}\n"
f"Expertise level: {expertise_rules[expertise]}\n"
f"Response length: {length_rules[response_length]}"
)
class AgentConfigFrameworkAgent(AgentFrameworkAgent):
"""AgentFrameworkAgent that rebuilds its system prompt per request.
Overrides ``run`` to read ``forwardedProps`` from the AG-UI input
and temporarily replace the wrapped agent's ``instructions`` option before
delegating to the standard orchestrator chain.
"""
async def run( # type: ignore[override]
self,
input_data: dict[str, Any],
) -> AsyncGenerator[BaseEvent, None]:
props = read_properties(input_data.get("forwardedProps"))
system_prompt = build_system_prompt(
props["tone"], props["expertise"], props["response_length"]
)
options = getattr(self.agent, "default_options", None)
if not isinstance(options, dict):
async for event in super().run(input_data):
yield event
return
previous_instructions = options.get("instructions")
options["instructions"] = system_prompt
try:
async for event in super().run(input_data):
yield event
finally:
if previous_instructions is None:
options.pop("instructions", None)
else:
options["instructions"] = previous_instructions
def create_agent_config_agent(chat_client: BaseChatClient) -> AgentConfigFrameworkAgent:
"""Instantiate the Agent Config demo agent.
The base MS Agent Framework ``Agent`` carries only a neutral fallback
instruction. The real behavioural steering happens in the per-request
instruction string applied by ``AgentConfigFrameworkAgent.run``.
"""
base_agent = Agent(
client=chat_client,
name="agent_config",
instructions=dedent(
"""
You are a helpful assistant. Follow the tone, expertise level, and
response-length directives provided in the system message for each
turn. If no directive is provided, use professional / intermediate
/ concise defaults.
""".strip()
),
tools=[],
)
return AgentConfigFrameworkAgent(
agent=base_agent,
name="AgentConfigObjectDemo",
description=(
"Reads tone / expertise / responseLength from forwardedProps "
"and builds its system prompt per turn."
),
require_confirmation=False,
)
@@ -0,0 +1,413 @@
"""
Beautiful Chat -- flagship MS Agent Framework showcase.
A polished sales-dashboard agent that exercises A2UI (fixed + dynamic),
Open Generative UI, and controlled generative UI components simultaneously.
The Python agent hosts the tool surface; the frontend wires a dedicated
runtime endpoint that enables A2UI (without injecting the default A2UI
tool), Open Generative UI, and MCP Apps (pointed at Excalidraw by default).
Reference:
- showcase/integrations/langgraph-python/src/agents/beautiful_chat.py
- showcase/integrations/ms-agent-python/src/agents/agent.py (shared tool patterns)
"""
from __future__ import annotations
import csv
import json
import uuid
from collections.abc import AsyncGenerator
from pathlib import Path
from textwrap import dedent
from typing import Annotated, Any
from ag_ui.core import BaseEvent
from agent_framework import Agent, BaseChatClient, Content, tool
from agent_framework_ag_ui import AgentFrameworkAgent, state_update
from pydantic import Field
# Shared tool implementations live in `showcase/shared/python/tools/`.
from tools import (
build_a2ui_operations_from_tool_call,
search_flights_impl,
)
# ---------------------------------------------------------------------
# Local data for the sales dashboard -- a self-contained copy of the
# sample CSV the LangGraph beautiful-chat cell uses. We inline it here
# (rather than reading from shared/python/data/db.csv) so the sales
# dashboard demo stays self-contained even if the shared data ever drifts.
# ---------------------------------------------------------------------
_DATA_DIR = Path(__file__).parent / "beautiful_chat_data"
_CSV_PATH = _DATA_DIR / "db.csv"
if _CSV_PATH.exists():
with open(_CSV_PATH) as _f:
_CACHED_DATA: list[dict[str, Any]] = list(csv.DictReader(_f))
else:
_CACHED_DATA = []
# ---------------------------------------------------------------------
# State schema for the todos panel. The frontend reads `todos` off of
# `agent.state` and renders the kanban-style canvas.
# ---------------------------------------------------------------------
STATE_SCHEMA: dict[str, object] = {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"emoji": {"type": "string"},
"status": {"type": "string"},
},
},
"description": "Ordered list of the user's todos for the canvas panel.",
}
}
PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
"todos": {
"tool": "manage_todos",
"tool_argument": "todos",
}
}
# ---------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------
@tool(
name="manage_todos",
description=(
"Replace the entire list of todos with the provided values. Always "
"include every todo you want to keep. Each todo needs: id, title, "
"description, emoji (🎯/🔥/✅/💡/🚀), and status (pending|completed)."
),
)
def manage_todos(
todos: Annotated[
list[dict],
Field(
description=(
"Complete source of truth for the user's todos. Maintain "
"ordering and include the full list on each call."
)
),
],
) -> Content:
"""Persist the provided set of todos and push them to AG-UI shared state.
Uses `state_update()` to deterministically update the frontend's
`state.todos` after the tool runs — this is the MS Agent Framework
equivalent of LangGraph's `Command(update={"todos": [...]})`. Without this,
the frontend's ExampleCanvas (which reads `agent.state.todos`) never sees
the manage_todos result and the todo column stays empty.
"""
normalized: list[dict[str, Any]] = []
for todo in todos:
normalized.append(
{
"id": todo.get("id") or str(uuid.uuid4()),
"title": todo.get("title", ""),
"description": todo.get("description", ""),
"emoji": todo.get("emoji", "🎯"),
"status": todo.get("status", "pending"),
}
)
return state_update(
text=f"Todos updated. Tracking {len(normalized)} item(s).",
state={"todos": normalized},
)
@tool(
name="query_data",
description=(
"Query the sales database. Takes natural language. Always call "
"before showing a chart or graph."
),
)
def query_data(
query: Annotated[
str,
Field(description="Natural language query to run against the database."),
],
) -> str:
"""Return the full dataset as JSON (the model filters/aggregates client-side)."""
del query # the whole CSV is small enough to return verbatim
return json.dumps(_CACHED_DATA)
@tool(
name="search_flights",
description=(
"Search for flights and display the results as rich A2UI cards. "
"Return exactly 2 flights. Each flight must have: airline, "
"airlineLogo, flightNumber, origin, destination, date, "
"departureTime, arrivalTime, duration, status, statusColor, price, "
"currency."
),
)
def search_flights(
flights: Annotated[
list[dict],
Field(description="List of flight objects to search and display."),
],
) -> str:
"""Display search results as A2UI cards via the fixed flight schema.
Mirrors LangGraph reference `_build_flight_components` — flat
literal-children layout instead of the structural-children template form
(Row.children = {componentId, path}) used by `search_flights_impl`. The
GenericBinder only expands templates correctly for components whose schema
declares STRUCTURAL children; inlining the values per-flight sidesteps the
template path and renders reliably under aimock.
"""
CATALOG_ID = "copilotkit://app-dashboard-catalog"
SURFACE_ID = "flight-search-results"
flight_card_ids: list[str] = []
components: list[dict[str, Any]] = []
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[str, Any] = {
"id": "root",
"component": "Row",
"children": flight_card_ids,
"gap": 16,
}
operations = [
{
"version": "v0.9",
"createSurface": {"surfaceId": SURFACE_ID, "catalogId": CATALOG_ID},
},
{
"version": "v0.9",
"updateComponents": {
"surfaceId": SURFACE_ID,
"components": [root, *components],
},
},
]
return json.dumps({"a2ui_operations": operations})
@tool(
name="generate_a2ui",
description=(
"Generate a dynamic A2UI dashboard based on the conversation. A "
"secondary LLM designs the UI schema and data. Use this for rich "
"custom dashboards (sales metrics, charts, tables, cards)."
),
)
def generate_a2ui(
context: Annotated[
str,
# Default to empty so the primary LLM can call `generate_a2ui()` with
# no args (e.g. aimock fixture returns `arguments: "{}"`). Without a
# default, pydantic rejects the empty-args call with "Argument parsing
# failed" before the function body ever runs, and the secondary LLM
# path never gets to fire.
Field(default="", description="Conversation context to generate UI from."),
] = "",
) -> str:
"""Generate a dynamic A2UI dashboard from conversation context."""
from openai import OpenAI
client = OpenAI()
tool_schema = {
"type": "function",
"function": {
"name": "_design_a2ui_surface",
"description": "Render a dynamic A2UI v0.9 surface.",
"parameters": {
"type": "object",
"properties": {
"surfaceId": {"type": "string"},
"catalogId": {"type": "string"},
"components": {"type": "array", "items": {"type": "object"}},
"data": {"type": "object"},
},
"required": ["surfaceId", "catalogId", "components"],
},
},
}
# The secondary LLM's user message includes the caller-provided context.
# Without this, aimock fixtures that match on the original user query's
# substring (e.g. "with total revenue, new customers, and conversion rate
# metrics") never match and aimock falls through to real-OpenAI proxy.
# In LangGraph the equivalent path passes `*messages` (the full
# conversation) into the secondary LLM. agent_framework doesn't expose
# conversation history to tool functions, so we relay the caller's
# `context` string verbatim — the calling LLM is responsible for
# populating it with whatever the user asked for. Under aimock the
# primary fixture's `arguments="{}"` empties this; the fallback below
# keeps the request shape close enough to match the canonical sales
# dashboard fixture (`userMessage: "with total revenue, new customers,
# and conversion rate metrics"`) without needing a live LLM.
user_content = (
context
or "Show me a sales dashboard with total revenue, new customers, "
"and conversion rate metrics. Include a pie chart of revenue by "
"category and a bar chart of monthly sales."
)
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": "Generate a useful A2UI dashboard."},
{"role": "user", "content": user_content},
],
tools=[tool_schema],
tool_choice={"type": "function", "function": {"name": "_design_a2ui_surface"}},
)
if not response.choices[0].message.tool_calls:
return json.dumps({"error": "LLM did not call _design_a2ui_surface"})
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
result = build_a2ui_operations_from_tool_call(args)
return json.dumps(result)
# ---------------------------------------------------------------------
# Agent factory
# ---------------------------------------------------------------------
SYSTEM_PROMPT = dedent(
"""
You are a polished, professional demo assistant for a sales team. Keep
responses to 1-2 sentences.
Tool guidance:
- Flights: call `search_flights` to show flight cards with the pre-built
A2UI 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 frontend chart
components (`pieChart` / `barChart`).
- Todos: enable app mode first (via the `enableAppMode` frontend tool),
then call `manage_todos` with the full list.
- Meetings: call the `scheduleTime` frontend tool (human-in-the-loop)
when the user asks to schedule a meeting -- do NOT ask for approval
yourself, the tool's picker handles it.
- Theme: call the `toggleTheme` frontend tool to flip light/dark.
State sync:
- The current list of todos is provided in the conversation context.
- When you add, remove, or reorder todos, call `manage_todos` with the
full list. Never send partial updates.
After executing tools, send a brief final message summarizing exactly
what changed.
"""
).strip()
def _has_tool_calls(message: dict[str, Any]) -> bool:
tool_calls = message.get("tool_calls") or message.get("toolCalls") or []
return isinstance(tool_calls, list) and len(tool_calls) > 0
def _last_user_message_index(messages: list[dict[str, Any]]) -> int:
for index in range(len(messages) - 1, -1, -1):
if messages[index].get("role") == "user":
return index
return -1
def _drop_historical_tool_messages(messages: Any) -> list[dict[str, Any]]:
"""Remove completed tool-call history before the current Beautiful Chat turn."""
if not isinstance(messages, list):
return []
typed_messages = [message for message in messages if isinstance(message, dict)]
last_user_index = _last_user_message_index(typed_messages)
clean: list[dict[str, Any]] = []
for index, message in enumerate(typed_messages):
if index < last_user_index:
if message.get("role") == "tool":
continue
if message.get("role") == "assistant" and _has_tool_calls(message):
continue
clean.append(message)
return clean
class BeautifulChatFrameworkAgent(AgentFrameworkAgent):
"""AgentFrameworkAgent that scopes tool-result history to the active turn."""
async def run( # type: ignore[override]
self,
input_data: dict[str, Any],
) -> AsyncGenerator[BaseEvent, None]:
patched_input = dict(input_data)
patched_input["messages"] = _drop_historical_tool_messages(
input_data.get("messages")
)
async for event in super().run(patched_input):
yield event
def create_beautiful_chat_agent(
chat_client: BaseChatClient,
) -> BeautifulChatFrameworkAgent:
"""Instantiate the flagship Beautiful Chat demo agent."""
base_agent = Agent(
client=chat_client,
name="beautiful_chat_agent",
instructions=SYSTEM_PROMPT,
tools=[manage_todos, query_data, search_flights, generate_a2ui],
default_options={"allow_multiple_tool_calls": False},
)
# predict_state_config (predictive streaming from LLM tool-call arg deltas)
# is intentionally omitted: the manifest declares
# `not_supported_features: [shared-state-streaming]` and the predictive
# path errors mid-stream with "An internal error has occurred while
# streaming events" on multi-item arrays. The deterministic state push
# via `state_update()` in manage_todos delivers todos after the tool runs,
# which matches the manifest's no-streaming contract.
return BeautifulChatFrameworkAgent(
agent=base_agent,
name="CopilotKitMicrosoftAgentFrameworkBeautifulChat",
description=(
"Flagship showcase agent. Combines A2UI (fixed + dynamic), "
"Open Generative UI, shared state (todos), and HITL via frontend "
"tools to render a polished sales dashboard."
),
require_confirmation=False,
)
@@ -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,114 @@
"""MS Agent Framework 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 MS Agent Framework 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 to keep the
schema stable under partial streaming.
"""
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
BYOC_HASHBROWN_SYSTEM_PROMPT = dedent(
"""
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}]"}}}]}
"""
).strip()
def create_byoc_hashbrown_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the BYOC hashbrown demo agent."""
base_agent = Agent(
client=chat_client,
name="byoc_hashbrown",
instructions=BYOC_HASHBROWN_SYSTEM_PROMPT,
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitByocHashbrownAgent",
description=(
"Emits hashbrown-shaped <ui>...</ui> structured output that the "
"@hashbrownai/react renderer progressively parses into a sales "
"dashboard (MetricCard + PieChart + BarChart + DealCard)."
),
require_confirmation=False,
)
@@ -0,0 +1,168 @@
"""MS Agent Framework 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.
Mirrors the langgraph-python implementation so the two backends are
directly comparable; only the agent runtime differs.
"""
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
BYOC_JSON_RENDER_SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def create_byoc_json_render_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the BYOC json-render demo agent."""
base_agent = Agent(
client=chat_client,
name="byoc_json_render",
instructions=BYOC_JSON_RENDER_SYSTEM_PROMPT,
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitByocJsonRenderAgent",
description=(
"Emits a flat { root, elements } JSON spec consumed by "
"@json-render/react against a Zod-validated catalog of "
"MetricCard + BarChart + PieChart."
),
require_confirmation=False,
)
@@ -0,0 +1,143 @@
"""gen-ui-agent — minimal MAF agent with explicit `steps` state schema.
Mirrors LangGraph's `langgraph-python/src/agents/gen_ui_agent.py`. The
frontend (`src/app/demos/gen-ui-agent/page.tsx`) subscribes to
`agent.state.steps` via `useAgent` and renders a live progress card; the
backend's job is to plan exactly 3 steps and walk each pending →
in_progress → completed by calling the `set_steps` tool. Every call
to `set_steps` triggers a `state_update` so the UI re-renders
in-place.
State shape (mirrors LGP `GenUiAgentState.steps`):
[
{"id": "...", "title": "...", "status": "pending" | "in_progress" | "completed"},
...
]
"""
from __future__ import annotations
import json
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent, state_update
from pydantic import Field
STATE_SCHEMA: dict[str, object] = {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
},
},
},
"description": "Ordered list of plan steps with live status.",
}
}
PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
"steps": {
"tool": "set_steps",
"tool_argument": "steps",
}
}
@tool(
name="set_steps",
description=(
"Publish the current plan and step statuses. Call this every "
"time a step transitions (including the first enumeration of "
"steps). Always include the full list of steps on each call."
),
)
def set_steps(
steps: Annotated[
list[dict],
Field(
description=(
"The complete source of truth for the plan: every step "
"with `id`, `title`, and `status` ('pending' | "
"'in_progress' | 'completed')."
)
),
],
):
"""Persist the current plan + statuses to shared state.
Uses `state_update()` (MAF equivalent of LangGraph's
`Command(update={"steps": [...]})`) so the frontend's progress card
re-renders with the new statuses after every transition.
"""
return state_update(
text=f"Published {len(steps)} step(s).",
state={"steps": steps},
)
SYSTEM_PROMPT = dedent(
"""
You are an agentic planner. For each user request, follow this exact
sequence:
1. Plan exactly 3 concrete steps and call `set_steps` ONCE with all
three steps at status="pending".
2. Step 1: call `set_steps` with step 1 at status="in_progress",
then call `set_steps` again with step 1 at status="completed".
3. Step 2: call `set_steps` with step 2 at status="in_progress",
then call `set_steps` again with step 2 at status="completed".
4. Step 3: call `set_steps` with step 3 at status="in_progress",
then call `set_steps` again with step 3 at status="completed".
5. Send ONE final conversational assistant message summarizing the
plan, then stop. Do not call any more tools after step 3 is
completed.
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.
"""
).strip()
def create_gen_ui_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the gen-ui-agent MAF agent."""
base_agent = Agent(
client=chat_client,
name="gen_ui_agent",
instructions=SYSTEM_PROMPT,
tools=[set_steps],
)
# NB: `predict_state_config` (predictive streaming from LLM tool-call arg
# deltas) is intentionally omitted. `agent_framework_ag_ui._orchestration
# ._predictive_state.PredictiveStateHandler` emits StateDeltaEvents using
# JSON Patch `op: "replace"` against `/<state_key>`. When the run starts
# with `current_state = {}`, the very first StateDelta tries to replace
# `/steps` — a path that doesn't exist — and the browser-side patch
# application throws `OPERATION_PATH_UNRESOLVABLE: Cannot perform the
# operation at a path that does not exist`. The run stream completes
# (RUN_FINISHED arrives), but the chat UI's run-state machine stays in
# "streaming" forever because the patch failure short-circuits the
# `complete` transition. `state_update()` inside `set_steps` already
# emits a full `StateSnapshotEvent` after every tool call, so the
# progress card still updates step-by-step; we just lose the
# mid-stream predictive flicker (matches beautiful_chat's manage_todos
# workaround for the same bug).
return AgentFrameworkAgent(
agent=base_agent,
name="GenUiAgent",
description=(
"Plans 3 steps and walks each pending → in_progress → "
"completed via set_steps. Drives the `gen-ui-agent` demo's "
"live progress card."
),
require_confirmation=False,
)
@@ -0,0 +1,56 @@
"""MS Agent Framework agent backing the Tool-Based Generative UI demo.
The frontend registers `render_bar_chart` and `render_pie_chart` tools via
`useComponent`. CopilotKit's runtime forwards those frontend tool definitions
to the agent at request time, so the agent can call them by name.
There are no backend tools here -- the agent's job is to recognize chart
intent in the user's message and emit a tool call with structured chart data.
The frontend then renders the result inline.
"""
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
Keep chat responses brief -- let the chart do the talking. After you
finish executing tools, send a brief final assistant message so it
persists in the conversation.
"""
).strip()
def create_gen_ui_tool_based_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the Tool-Based Generative UI demo agent."""
base_agent = Agent(
client=chat_client,
name="gen_ui_tool_based_agent",
instructions=SYSTEM_PROMPT,
# Both rendering tools (`render_bar_chart`, `render_pie_chart`) are
# registered on the frontend via `useComponent`. The runtime forwards
# them as tool definitions at request time.
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMSAgentGenUiToolBasedAgent",
description=(
"Data-visualization assistant that turns chart requests into "
"frontend-rendered bar and pie charts via tool calls."
),
require_confirmation=False,
)
@@ -0,0 +1,131 @@
"""MAF agent backing the Headless Chat (Complete) demo.
Mirrors the LangGraph reference at
`showcase/integrations/langgraph-python/src/agents/headless_complete.py`.
Three deterministic mock tools (`get_weather`, `get_stock_price`,
`get_revenue_chart`) feed the four headless pills the e2e spec drives.
"""
from __future__ import annotations
import json
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent
from pydantic import Field
@tool(
name="get_weather",
description=(
"Get the current weather for a given location. Returns city, "
"temperature (F), humidity, wind, and conditions."
),
)
def get_weather(
location: Annotated[str, Field(description="City or region name.")],
) -> str:
"""Return mock weather data as JSON. Deterministic for testing."""
return json.dumps(
{
"city": location,
"temperature": 68,
"humidity": 55,
"wind_speed": 10,
"conditions": "Sunny",
}
)
@tool(
name="get_stock_price",
description=(
"Get a mock current price for a stock ticker. Returns ticker, "
"price_usd, and change_pct."
),
)
def get_stock_price(
ticker: Annotated[str, Field(description="Stock ticker symbol.")],
) -> str:
"""Return mock stock data as JSON. Deterministic for testing."""
return json.dumps(
{
"ticker": ticker.upper(),
"price_usd": 189.42,
"change_pct": 1.27,
}
)
@tool(
name="get_revenue_chart",
description=(
"Get a mock six-month revenue series for a chart visualization. "
"Returns title, subtitle, and an array of {label, value} points."
),
)
def get_revenue_chart() -> str:
"""Return a deterministic six-month revenue series."""
return json.dumps(
{
"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},
],
}
)
SYSTEM_PROMPT = dedent(
"""
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.
Routing rules:
- If the user asks about weather for a place, call `get_weather`
with the location.
- If the user asks about a stock or ticker (AAPL, TSLA, MSFT, ...),
call `get_stock_price` with the ticker.
- If the user asks for a chart, graph, or visualization of revenue,
sales, or other metrics over time, call `get_revenue_chart`.
- 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.
- Otherwise, reply in plain text.
After a tool returns, write one short sentence summarizing the
result. Never fabricate data a tool could provide.
"""
).strip()
def create_headless_complete_agent(
chat_client: BaseChatClient,
) -> AgentFrameworkAgent:
"""Instantiate the headless-complete MAF agent."""
base_agent = Agent(
client=chat_client,
name="headless_complete_agent",
instructions=SYSTEM_PROMPT,
tools=[get_weather, get_stock_price, get_revenue_chart],
)
return AgentFrameworkAgent(
agent=base_agent,
name="HeadlessCompleteAgent",
description=(
"Mock weather, stock, and chart tools for the Headless Chat "
"(Complete) demo."
),
)
@@ -0,0 +1,159 @@
"""
MS Agent Framework 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 | None}``. This agent treats that
result as authoritative: if ``approved`` is ``True``, continue;
otherwise, stop and explain the decision back to the user.
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from textwrap import dedent
from typing import Any
from ag_ui.core import BaseEvent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
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.
How to use `request_user_approval`:
- `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').
- `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.
The tool returns an object of the shape
`{"approved": boolean, "reason": string | null}`.
- 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.
- 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.
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.
"""
).strip()
def _tool_call_ids(message: dict[str, Any]) -> set[str]:
tool_calls = message.get("tool_calls") or message.get("toolCalls") or []
if not isinstance(tool_calls, list):
return set()
ids: set[str] = set()
for call in tool_calls:
if isinstance(call, dict) and isinstance(call.get("id"), str):
ids.add(call["id"])
return ids
def _tool_result_ids(messages: list[dict[str, Any]], start_index: int) -> set[str]:
ids: set[str] = set()
for message in messages[start_index + 1 :]:
if message.get("role") == "user":
break
if message.get("role") != "tool":
continue
call_id = message.get("tool_call_id") or message.get("toolCallId")
if isinstance(call_id, str):
ids.add(call_id)
return ids
def _last_user_message_index(messages: list[dict[str, Any]]) -> int:
for index in range(len(messages) - 1, -1, -1):
if messages[index].get("role") == "user":
return index
return -1
def _sanitize_approval_history(messages: Any) -> list[dict[str, Any]]:
"""Keep only the active approval tool pair; summarize older pairs via text."""
if not isinstance(messages, list):
return []
typed_messages = [message for message in messages if isinstance(message, dict)]
last_user_index = _last_user_message_index(typed_messages)
clean: list[dict[str, Any]] = []
for index, message in enumerate(typed_messages):
if index < last_user_index:
if message.get("role") == "tool":
continue
if message.get("role") == "assistant" and _tool_call_ids(message):
continue
clean.append(message)
continue
if message.get("role") == "assistant":
call_ids = _tool_call_ids(message)
if call_ids and not call_ids.issubset(
_tool_result_ids(typed_messages, index)
):
continue
clean.append(message)
return clean
class HitlInAppFrameworkAgent(AgentFrameworkAgent):
"""AgentFrameworkAgent that drops malformed historical approval tool calls."""
async def run( # type: ignore[override]
self,
input_data: dict[str, Any],
) -> AsyncGenerator[BaseEvent, None]:
patched_input = dict(input_data)
patched_input["messages"] = _sanitize_approval_history(
input_data.get("messages")
)
async for event in super().run(patched_input):
yield event
def create_hitl_in_app_agent(chat_client: BaseChatClient) -> HitlInAppFrameworkAgent:
"""Instantiate the In-App HITL demo agent backed by Microsoft Agent Framework."""
base_agent = Agent(
client=chat_client,
name="hitl_in_app_agent",
instructions=SYSTEM_PROMPT,
tools=[],
default_options={"allow_multiple_tool_calls": False},
)
return HitlInAppFrameworkAgent(
agent=base_agent,
name="CopilotKitMSAgentHitlInAppAgent",
description=(
"Support copilot that asks for explicit operator approval via a "
"frontend-provided tool before taking any customer-affecting action."
),
require_confirmation=False,
)
@@ -0,0 +1,58 @@
"""MS Agent Framework agent backing the In-Chat HITL (useHumanInTheLoop) demo.
The `book_call` tool is defined entirely on the frontend via
`useHumanInTheLoop`. CopilotKit's runtime forwards the frontend tool
definition to the agent at request time, so this agent has no backend tools
of its own -- it just needs to recognize "book a call" intent and emit the
tool call.
When the user picks a slot (or cancels), CopilotKit returns that choice as
the tool result and the agent confirms in a short follow-up message.
"""
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
You help users book an onboarding or intro call with the sales team.
When the user asks to book a call, schedule a meeting, or set up a 1:1,
call the frontend-provided `book_call` tool with:
- `topic`: a short summary of what the call is about (e.g. 'Intro with
sales', 'Q2 goals review').
- `attendee`: who the call is with, if known (e.g. 'Alice from Sales').
The tool surfaces a time-picker UI inside the chat. The user will pick a
slot or cancel. After the tool returns, send one short confirmation
sentence reflecting the user's choice (or noting cancellation). Do NOT
ask for approval yourself -- always call the tool and let the picker
handle the decision. Keep all replies to one sentence.
"""
).strip()
def create_hitl_in_chat_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the In-Chat HITL demo agent."""
base_agent = Agent(
client=chat_client,
name="hitl_in_chat_agent",
instructions=SYSTEM_PROMPT,
# `book_call` is registered on the frontend via `useHumanInTheLoop`.
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMSAgentHitlInChatAgent",
description=(
"Scheduling assistant that delegates the time-picker interaction "
"to a frontend-defined `book_call` tool rendered inline in the chat."
),
require_confirmation=False,
)
@@ -0,0 +1,76 @@
"""
MS Agent Framework scheduling agent — interrupt-adapted.
This agent powers two demos (gen-ui-interrupt, interrupt-headless) that in the
LangGraph showcase rely on the native `interrupt()` primitive with
checkpoint/resume. The MS Agent Framework does NOT have that primitive, so we
adapt by delegating the time-picker interaction to a **frontend tool** that the
agent calls by name (`schedule_meeting`). The frontend registers the tool via
`useFrontendTool` with an async handler; that handler renders the interactive
picker, waits for the user to choose a slot (or cancel), and resolves the tool
call with the result. The backend only defines the system prompt and advertises
no local `schedule_meeting` implementation — the agent's tool call is satisfied
entirely by the frontend.
See `src/agents/agent.py` for the related `approval_mode="always_require"`
pattern used elsewhere in this package.
"""
# @region[backend-interrupt-tool]
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
# @region[backend-tool-call]
SYSTEM_PROMPT = dedent(
"""
You are a scheduling assistant. Whenever the user asks you to book a call
or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a
short `topic` describing the purpose of the meeting and, if known, an
`attendee` describing who the meeting is with.
The `schedule_meeting` tool is implemented on the client: it surfaces a
time-picker UI to the user and returns the user's selection. After the
tool returns, briefly confirm whether the meeting was scheduled and at
what time, or note that the user cancelled. Do NOT ask for approval
yourself — always call the tool and let the picker handle the decision.
Keep responses short and friendly. After you finish executing tools,
always send a brief final assistant message summarizing what happened so
the message persists.
""".strip()
)
def create_interrupt_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the scheduling-only agent used by the interrupt-adapted demos."""
base_agent = Agent(
client=chat_client,
name="scheduling_agent",
instructions=SYSTEM_PROMPT,
# No backend tools. `schedule_meeting` is registered on the frontend
# via `useFrontendTool` and dispatched through the CopilotKit runtime.
# When the agent calls `schedule_meeting`, the request is routed to
# the frontend handler, which returns a Promise that only resolves
# once the user picks a slot — equivalent to `interrupt()` in the
# LangGraph reference.
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMicrosoftAgentFrameworkInterruptAgent",
description=(
"Scheduling assistant for the interrupt-adapted demos. Delegates "
"the time-picker interaction to a frontend tool."
),
require_confirmation=False,
)
# @endregion[backend-tool-call]
# @endregion[backend-interrupt-tool]
@@ -0,0 +1,75 @@
"""
MS Agent Framework 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/[[...slug]]/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/microsoft-agent-framework/generative-ui/mcp-apps
"""
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def create_mcp_apps_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the MCP Apps demo agent backed by Microsoft Agent Framework."""
base_agent = Agent(
client=chat_client,
name="mcp_apps_agent",
instructions=SYSTEM_PROMPT,
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMSAgentMCPAppsAgent",
description="Draws simple diagrams in Excalidraw via the MCP Apps middleware.",
require_confirmation=False,
)
@@ -0,0 +1,187 @@
"""
Multimodal MS Agent Framework agent -- accepts image + document (PDF)
attachments.
A *dedicated* vision-capable agent scoped to the `/demos/multimodal` cell.
Other demos continue to use their own (cheaper, text-only) models via
`agents/agent.py` -- 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 -> agent_framework_ag_ui
(AG-UI -> AF adapter)
-> this agent
The deployed AG-UI adapter recognizes 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 ``binary`` shape before it hits the
runtime. We therefore route on ``mimeType``, not the part ``type``:
- ``image/*`` parts are forwarded to GPT-4o-mini unchanged (vision-native).
- ``application/pdf`` parts are flattened to inline text via ``pypdf`` so
the model can read them without needing file-part support.
Reference:
- showcase/integrations/langgraph-python/src/agents/multimodal_agent.py
"""
import base64
import io
from textwrap import dedent
from typing import Any
from agent_framework import (
Agent,
BaseChatClient,
ChatContext,
ChatMiddleware,
Content,
)
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def _extract_pdf_text(b64: str) -> str:
"""Decode an inline-base64 PDF and extract its text.
Returns an empty string if decoding or extraction fails -- callers must
treat the extracted text as best-effort. Any exception here is logged
and swallowed so one malformed attachment does not tank the whole
user turn.
"""
try:
raw = base64.b64decode(b64, validate=False)
except Exception as exc: # pragma: no cover - defensive
print(f"[multimodal_agent] base64 decode failed: {exc}")
return ""
try:
# Lazy import -- keeps the module importable even if pypdf is missing
# at dev-server boot (we only need it when a PDF actually arrives).
from pypdf import PdfReader # type: ignore[import-not-found]
except ImportError as exc: # pragma: no cover - defensive
print(
"[multimodal_agent] pypdf not installed -- PDF text extraction "
f"unavailable: {exc}",
)
return ""
try:
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
print(f"[multimodal_agent] pypdf extraction failed: {exc}")
return ""
def _content_pdf_payload(content: Content) -> tuple[str, str] | None:
"""If a Content holds an inline PDF, return ``(base64_payload, mime_type)``.
Returns ``None`` for any other content type, including PDFs delivered via
``ag-ui://binary/<id>`` or external HTTPS URLs — those cannot be inlined
without a separate fetch and are left for the chat client to handle.
"""
media_type = (content.media_type or "").lower()
if "pdf" not in media_type:
return None
uri = content.uri or ""
if not uri.startswith("data:"):
return None
_, _, payload = uri.partition(",")
if not payload:
return None
return payload, media_type or "application/pdf"
class _PdfFlattenChatMiddleware(ChatMiddleware):
"""Flatten inline PDF content parts to text for the model call only.
Scoping the rewrite to ``ChatMiddleware.process`` (LGP's equivalent of
``wrap_model_call``) is what keeps the flattened ``[Attached document]\\n``
dump from leaking back into the AG-UI ``MESSAGES_SNAPSHOT``: the agent's
canonical message state stays intact, the chat client sees the text-only
version, and the user's chat bubble keeps showing the original PDF chip
instead of the raw PDF body.
Originally the multimodal agent mutated ``input_data["messages"]`` inside
an ``AgentFrameworkAgent.run`` override, but that mutation flows into the
outbound snapshot serializer (``agent_framework_ag_ui._message_adapters
._normalize_snapshot_content``) which then bleeds the flattened text into
every subsequent chat-bubble render. Restoring the original ``contents``
after ``call_next`` is the discipline that prevents that bleed.
"""
async def process(
self,
context: ChatContext,
call_next: Any,
) -> None:
messages = context.messages or []
snapshots: list[tuple[Any, list[Content] | None]] = []
for message in messages:
contents = getattr(message, "contents", None)
if not contents:
continue
rewritten: list[Content] = []
mutated = False
for content in contents:
pdf = _content_pdf_payload(content)
if pdf is None:
rewritten.append(content)
continue
payload, _ = pdf
text = _extract_pdf_text(payload)
replacement = Content.from_text(
text=(
f"[Attached document]\n{text}"
if text
else "[Attached document]\n(unable to extract text)"
)
)
rewritten.append(replacement)
mutated = True
if mutated:
snapshots.append((message, list(contents)))
message.contents = rewritten # type: ignore[attr-defined]
try:
await call_next()
finally:
for message, original in snapshots:
message.contents = original # type: ignore[attr-defined]
def create_multimodal_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the vision-capable multimodal demo agent."""
base_agent = Agent(
client=chat_client,
name="multimodal_agent",
instructions=SYSTEM_PROMPT,
tools=[],
middleware=[_PdfFlattenChatMiddleware()],
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMicrosoftAgentFrameworkMultimodalAgent",
description=(
"Vision-capable agent that answers questions about attached "
"images and PDFs."
),
require_confirmation=False,
)
@@ -0,0 +1,109 @@
"""MS Agent Framework agent for the Open-Ended Generative UI (Advanced) demo.
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
`CopilotKit` provider. The provider injects a JSON descriptor of those
functions into the agent context.
- The CopilotKit runtime forwards both the auto-registered
`generateSandboxedUi` frontend tool AND the sandbox-function descriptors
(via AG-UI context) to the MS agent on each turn.
- 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 textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def create_open_gen_ui_advanced_agent(
chat_client: BaseChatClient,
) -> AgentFrameworkAgent:
"""Instantiate the advanced Open Generative UI agent."""
base_agent = Agent(
client=chat_client,
name="open_gen_ui_advanced_agent",
instructions=SYSTEM_PROMPT,
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="OpenGenUiAdvancedAgent",
description=(
"Generates interactive sandboxed UI that calls host-side "
"sandbox functions via `Websandbox.connection.remote.*`."
),
require_confirmation=False,
)
@@ -0,0 +1,81 @@
"""MS Agent Framework agent for the Open-Ended Generative UI demo (minimal).
The simplest possible Open Generative UI agent. All the heavy lifting
happens outside the agent:
- The CopilotKit runtime is configured with `openGenerativeUI: true` for
this agent (see `src/app/api/copilotkit-ogui/route.ts`). The provider
auto-registers the `generateSandboxedUi` frontend tool, which the
runtime forwards to the MS agent via the AG-UI protocol on each turn.
- When the LLM calls `generateSandboxedUi`, the runtime's
`OpenGenerativeUIMiddleware` 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 textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def create_open_gen_ui_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the minimal Open Generative UI agent."""
base_agent = Agent(
client=chat_client,
name="open_gen_ui_agent",
instructions=SYSTEM_PROMPT,
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="OpenGenUiAgent",
description=(
"Generates self-contained, educational sandboxed UI "
"(HTML + CSS + SVG) via the `generateSandboxedUi` frontend tool."
),
require_confirmation=False,
)
@@ -0,0 +1,115 @@
"""readonly-state-agent-context — minimal MAF agent for `useAgentContext`.
Mirrors LangGraph's
`langgraph-python/src/agents/readonly_state_agent_context.py`. Demonstrates
the `useAgentContext` hook from `@copilotkit/react-core/v2`: the frontend
provides read-only context *to* the agent (e.g. user name, timezone,
recent activity). The agent reads that context on every turn and
incorporates it into its response. No custom state, no tools — the
minimal shape of the useAgentContext pattern.
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from textwrap import dedent
from typing import Any
from ag_ui.core import BaseEvent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def build_context_system_message(context: Any) -> str | None:
"""Format frontend-provided AG-UI context as a model-visible message."""
if not isinstance(context, list) or len(context) == 0:
return None
lines: list[str] = ["## Context from the application"]
for entry in context:
if not isinstance(entry, dict):
continue
description = entry.get("description")
value = entry.get("value")
if description is None or value is None:
continue
lines.append("")
lines.append(str(description))
lines.append(str(value))
if len(lines) == 1:
return None
return "\n".join(lines)
class ReadonlyContextFrameworkAgent(AgentFrameworkAgent):
"""AgentFrameworkAgent that forwards `useAgentContext` to the model.
LangGraph gets this behavior from CopilotKitMiddleware. The MS Agent
adapter receives the AG-UI `context` entries in `input_data`, so this
shim appends them to the wrapped agent's instruction string before
delegating to the standard Agent Framework runner.
"""
async def run( # type: ignore[override]
self,
input_data: dict[str, Any],
) -> AsyncGenerator[BaseEvent, None]:
context_prompt = build_context_system_message(input_data.get("context"))
if not context_prompt:
async for event in super().run(input_data):
yield event
return
options = getattr(self.agent, "default_options", None)
if not isinstance(options, dict):
async for event in super().run(input_data):
yield event
return
previous_instructions = options.get("instructions")
options["instructions"] = f"{SYSTEM_PROMPT}\n\n{context_prompt}"
try:
async for event in super().run(input_data):
yield event
finally:
if previous_instructions is None:
options.pop("instructions", None)
else:
options["instructions"] = previous_instructions
def create_readonly_state_agent_context(
chat_client: BaseChatClient,
) -> ReadonlyContextFrameworkAgent:
"""Instantiate the readonly-state-agent-context MAF agent."""
base_agent = Agent(
client=chat_client,
name="readonly_state_agent_context",
instructions=SYSTEM_PROMPT,
tools=[],
)
return ReadonlyContextFrameworkAgent(
agent=base_agent,
name="ReadOnlyStateAgentContext",
description=(
"Reads frontend-provided `useAgentContext` entries on every "
"turn; no tools, no custom state."
),
)
@@ -0,0 +1,71 @@
"""Reasoning agent — backs `reasoning-default` and `reasoning-custom` cells.
Why this agent uses OpenAIChatClient (Responses API) instead of the
ChatCompletions client used by the other MAF agents in this showcase:
the OpenAI Responses API streams `response.reasoning_summary_text.delta`
items only for native reasoning models (gpt-5, o3, o4-mini, etc.). The
`agent_framework_openai` Responses client translates those into AG-UI
`REASONING_MESSAGE_*` events with `role: "reasoning"`, which the frontend
renders via the built-in `reasoningMessage` slot. Without the Responses
API path, the reasoning slot never lights up — that's the bug the LGP
parity port closes.
Mirrors LangGraph's `langgraph-python/src/agents/reasoning_agent.py`,
which configures `init_chat_model("openai:gpt-5", use_responses_api=True,
reasoning={"effort": "medium", "summary": "detailed"})`.
"""
from __future__ import annotations
import os
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework.openai import OpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
You are a helpful assistant. For each user question, first think
step-by-step about the approach, then give a concise answer.
"""
).strip()
def _build_reasoning_chat_client() -> BaseChatClient:
"""Build a Responses-API chat client for reasoning-event streaming.
The model env var defaults to `gpt-5` to match the LGP reference; the
deployment can override via `OPENAI_REASONING_MODEL`.
"""
return OpenAIChatClient(
model=os.environ.get("OPENAI_REASONING_MODEL", "gpt-5.4"),
api_key=os.environ.get("OPENAI_API_KEY"),
)
def create_reasoning_agent(
_chat_client_ignored: BaseChatClient | None = None,
) -> AgentFrameworkAgent:
"""Instantiate the reasoning agent.
The shared `chat_client` from `agent_server.py` is intentionally
ignored — this cell needs the Responses API specifically.
"""
base_agent = Agent(
client=_build_reasoning_chat_client(),
name="reasoning_agent",
instructions=SYSTEM_PROMPT,
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="ReasoningAgent",
description=(
"Reasoning-token streaming via the OpenAI Responses API. "
"Drives `reasoning-default` (built-in slot) and "
"`reasoning-custom` (custom amber ReasoningBlock) demos."
),
)
@@ -0,0 +1,175 @@
"""MS Agent Framework agent backing the Shared State (Read + Write) demo.
Mirrors the bidirectional shared-state pattern used by other showcase
integrations:
- UI -> agent (write): The UI owns a `preferences` object and writes it
into agent state via `agent.setState({preferences: ...})`. The
AG-UI runtime injects the current shared state (including
`preferences`) as a system context message before each turn, so the
LLM adapts.
- agent -> UI (read): The agent calls `set_notes` to update a `notes`
list in shared state. The `predict_state_config` mechanism extracts
the `notes` value from the tool call's `notes` argument and pushes
a StateSnapshotEvent to the UI. The UI reflects every update in
real time via `useAgent`.
"""
from __future__ import annotations
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent
from pydantic import Field
# ---------------------------------------------------------------------------
# State schema
#
# Declared so the AG-UI runtime auto-injects `current_state` as a system
# context message every turn. That is how UI-written `preferences`
# become visible to the LLM without us writing any custom middleware.
# ---------------------------------------------------------------------------
STATE_SCHEMA: dict[str, object] = {
"preferences": {
"type": "object",
"description": (
"User-supplied preferences. Adapt every response to match. "
"Address the user by name when appropriate."
),
"properties": {
"name": {"type": "string"},
"tone": {"type": "string"},
"language": {"type": "string"},
"interests": {"type": "array", "items": {"type": "string"}},
},
},
"notes": {
"type": "array",
"items": {"type": "string"},
"description": (
"Short notes the agent has chosen to remember about the "
"user. Updated by calling `set_notes` with the FULL list."
),
},
}
# ---------------------------------------------------------------------------
# predict_state_config — agent -> UI write path
#
# Instead of returning a Content/state_update object from the tool, we
# use predict_state_config which extracts the state value directly from
# the tool call's argument. This is the same mechanism the main sales
# agent uses for salesTodos and is more compatible with the MS Agent
# Framework's tool execution pipeline.
# ---------------------------------------------------------------------------
PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
"notes": {
"tool": "set_notes",
"tool_argument": "notes",
},
}
# ---------------------------------------------------------------------------
# Tool: set_notes — agent -> UI write path
#
# Returns a plain string so the MS Agent Framework can serialize it as
# a tool result and send it back to the LLM for the follow-up text.
# The actual state update is handled by predict_state_config above.
# ---------------------------------------------------------------------------
@tool(
name="set_notes",
description=(
"Replace the notes array in shared state with the full updated "
"list. Always pass the FULL list of short note strings "
"(existing notes + any new ones), not a diff. Keep each note "
"short (< 120 chars)."
),
)
def set_notes(
notes: Annotated[
list[str],
Field(
description=(
"The complete updated list of short note strings. "
"Must include every existing note you want to keep "
"plus any new ones."
)
),
],
) -> str:
"""Push the agent-authored notes into AG-UI shared state."""
return f"Notes updated. Tracking {len(notes)} note(s)."
# ---------------------------------------------------------------------------
# Agent factory
# ---------------------------------------------------------------------------
SYSTEM_PROMPT = dedent(
"""
You are a helpful, concise assistant.
The user's preferences are supplied via shared state and will be
added as a system context message at the start of every turn.
Always respect them:
- address the user by their `name` when present,
- match the requested `tone` (formal / casual / playful),
- reply in the user's preferred `language`,
- take their `interests` into account when making suggestions.
Notes — agent-authored memory surfaced to the UI:
- When the user asks you to remember something, OR you observe
something worth surfacing in the UI's notes panel, call
`set_notes` with the FULL updated list of short note strings
(existing notes from shared state + any new ones).
- Never send partial updates -- the call replaces the entire
list. Read the current `notes` value out of the injected
shared-state context and re-send it plus your additions.
- Keep each note short (under 120 characters).
After executing tools, reply with one short conversational
sentence so the message persists in the chat surface.
"""
).strip()
def create_shared_state_read_write_agent(
chat_client: BaseChatClient,
) -> AgentFrameworkAgent:
"""Instantiate the Shared State (Read + Write) demo agent."""
base_agent = Agent(
client=chat_client,
name="shared_state_read_write_agent",
instructions=SYSTEM_PROMPT,
tools=[set_notes],
# Disable server-side conversation storage so the OpenAI client
# sends the full message history on every request instead of
# relying on `previous_response_id`. aimock (our local fixture
# server) doesn't implement conversation-ID lookup, so the
# tool-result continuation call would 404 without this flag.
default_options={"store": False},
)
return AgentFrameworkAgent(
agent=base_agent,
name="CopilotKitMSAgentSharedStateReadWriteAgent",
description=(
"Bidirectional shared-state demo. Reads UI-written "
"`preferences` from shared state every turn and writes "
"agent-authored `notes` back via the `set_notes` tool."
),
state_schema=STATE_SCHEMA,
predict_state_config=PREDICT_STATE_CONFIG,
require_confirmation=False,
)
@@ -0,0 +1,101 @@
"""shared-state-streaming — MAF agent that streams `document` per token.
Mirrors LangGraph's `langgraph-python/src/agents/shared_state_streaming.py`.
The frontend (`src/app/demos/shared-state-streaming/page.tsx`) subscribes
to `agent.state.document` via `useAgent` and re-renders the document
view as content arrives. This agent's job is to call `write_document`
with a full document string; the `predict_state_config` here mirrors
LGP's `StateStreamingMiddleware(StateItem(state_key="document",
tool="write_document", tool_argument="document"))` — it tells the
runtime to forward every token of the tool's `document` argument
directly into `state.document` while the tool call is still streaming.
"""
from __future__ import annotations
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework_ag_ui import AgentFrameworkAgent, state_update
from pydantic import Field
STATE_SCHEMA: dict[str, object] = {
"document": {
"type": "string",
"description": "The full document body, streamed token-by-token.",
}
}
# Tells the runtime to stream tool-argument deltas straight into
# `state.document` while `write_document` is still streaming — matches
# LGP's StateStreamingMiddleware setup.
PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
"document": {
"tool": "write_document",
"tool_argument": "document",
}
}
@tool(
name="write_document",
description=(
"Write a document for the user. Always call this tool when the "
"user asks you to write, draft, or revise any text. The "
"`document` argument is streamed per-token into shared state "
"under the `document` key so the UI renders the body live."
),
)
def write_document(
document: Annotated[
str,
Field(description="The full document content as a single string."),
],
):
"""Commit the final document body to shared state.
Per-token streaming of the `document` arg is handled by the runtime
via `predict_state_config`; this final `state_update` is the
authoritative commit after the tool finishes streaming.
"""
return state_update(
text="Document written to shared state.",
state={"document": document},
)
SYSTEM_PROMPT = dedent(
"""
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.
"""
).strip()
def create_shared_state_streaming_agent(
chat_client: BaseChatClient,
) -> AgentFrameworkAgent:
"""Instantiate the shared-state-streaming MAF agent."""
base_agent = Agent(
client=chat_client,
name="shared_state_streaming_agent",
instructions=SYSTEM_PROMPT,
tools=[write_document],
)
return AgentFrameworkAgent(
agent=base_agent,
name="SharedStateStreamingAgent",
description=(
"Per-token state streaming: `write_document` arg deltas land "
"in `state.document` as the tool call is generated."
),
predict_state_config=PREDICT_STATE_CONFIG,
require_confirmation=False,
)
@@ -0,0 +1,492 @@
"""MS Agent Framework agent backing the Sub-Agents demo.
Mirrors langgraph-python/src/agents/subagents.py and
google-adk/src/agents/subagents_agent.py:
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 real `agent_framework.Agent` with its own system
prompt. Each delegation appends an entry to the `delegations` slot in
AG-UI shared state via `state_update(...)`, so the UI can render a
live delegation log via `useAgent`.
Subagent invocation contract: each delegation tool returns
`state_update(...)` containing the FULL updated `delegations` list. We
read the prior list out of a per-request `ContextVar` populated by an
`agent_middleware` that captures the AG-UI session metadata
(specifically `current_state`, which the AG-UI runtime stuffs into
`session.metadata` on every turn) before the supervisor runs.
"""
# @region[supervisor-delegation-tools]
# @region[subagent-setup]
from __future__ import annotations
import asyncio
import contextvars
import json
import logging
import threading
import uuid
from collections.abc import AsyncGenerator, Awaitable, Callable
from textwrap import dedent
from typing import Annotated, Any
from ag_ui.core import BaseEvent
from agent_framework import (
Agent,
AgentContext,
BaseChatClient,
Content,
agent_middleware,
tool,
)
from agent_framework_ag_ui import AgentFrameworkAgent, state_update
from pydantic import Field
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# State schema — `delegations` is rendered as a live log in the UI.
# ---------------------------------------------------------------------------
STATE_SCHEMA: dict[str, object] = {
"delegations": {
"type": "array",
"description": (
"Append-only log of supervisor -> sub-agent delegations. "
"Each entry is a Delegation = "
"{id, sub_agent, task, status, result}."
),
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"sub_agent": {"type": "string"},
"task": {"type": "string"},
"status": {"type": "string"},
"result": {"type": "string"},
},
},
}
}
# ---------------------------------------------------------------------------
# Per-request current_state bridge
#
# Tools cannot directly receive `current_state` from the AG-UI runtime,
# but `agent_middleware` runs once per agent invocation with full
# session context. We snapshot the latest `delegations` list into a
# ContextVar before `call_next()`, so each delegation tool (running in
# the same task / contextvar scope) can read it back, append, and
# return the FULL list via `state_update`.
# ---------------------------------------------------------------------------
_current_delegations: contextvars.ContextVar[list[dict[str, Any]]] = (
contextvars.ContextVar("ms_subagents_current_delegations", default=[])
)
def _extract_delegations(raw: Any) -> list[dict[str, Any]]:
"""Pull a clean delegations list out of session metadata.
`session.metadata["current_state"]` is JSON-serialized by the
AG-UI runtime (see `_build_safe_metadata`) so we tolerate either
a plain dict or its string form.
"""
payload: Any = raw
if isinstance(payload, str):
try:
payload = json.loads(payload)
except json.JSONDecodeError:
logger.warning(
"subagents: current_state was not valid JSON; "
"starting from empty delegations list"
)
return []
if not isinstance(payload, dict):
return []
delegations = payload.get("delegations")
if not isinstance(delegations, list):
return []
return [dict(d) for d in delegations if isinstance(d, dict)]
@agent_middleware
async def capture_current_state(
context: AgentContext, call_next: Callable[[], Awaitable[None]]
) -> None:
"""Snapshot `delegations` from session metadata into a ContextVar."""
snapshot: list[dict[str, Any]] = []
session = context.session
metadata = getattr(session, "metadata", None) if session else None
if isinstance(metadata, dict):
snapshot = _extract_delegations(metadata.get("current_state"))
token = _current_delegations.set(snapshot)
try:
await call_next()
finally:
_current_delegations.reset(token)
# ---------------------------------------------------------------------------
# Sub-agent factory
#
# Each sub-agent is a full `Agent(...)` with its own system prompt.
# They share the chat client with the supervisor but otherwise have no
# shared memory or tools — the supervisor only sees their final text.
# ---------------------------------------------------------------------------
# Each sub-agent is a full-fledged `Agent(...)` with its own system
# prompt. They don't share memory or tools with the supervisor — the
# supervisor only sees their return value (final text content).
_RESEARCH_INSTRUCTIONS = (
"You are a research sub-agent. Given a topic, produce a concise "
"bulleted list of 3-5 key facts. No preamble, no closing."
)
_WRITING_INSTRUCTIONS = (
"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."
)
_CRITIQUE_INSTRUCTIONS = (
"You are an editorial critique sub-agent. Given a draft, give "
"2-3 crisp, actionable critiques. No preamble."
)
def _make_sub_agent(chat_client: BaseChatClient, name: str, instructions: str) -> Agent:
return Agent(
client=chat_client,
name=name,
instructions=instructions,
tools=[],
)
# @endregion[subagent-setup]
# Module-level holder so the delegation tools can reach the
# pre-built sub-agents without rebuilding them on every tool call.
# Populated lazily by `create_subagents_agent(...)`.
_SUB_AGENTS: dict[str, Agent] = {}
async def _invoke_sub_agent_async(sub_agent_name: str, task: str) -> str:
"""Run a sub-agent on `task` and return its final text content."""
agent = _SUB_AGENTS.get(sub_agent_name)
if agent is None:
raise RuntimeError(
f"sub-agent '{sub_agent_name}' is not registered; call "
"create_subagents_agent(...) first"
)
response = await agent.run(task)
text = (getattr(response, "text", "") or "").strip()
if text:
return text
# Fall back to scanning messages — `Agent.run` always returns
# an `AgentRunResponse`, but `.text` may be empty if the chat
# client only emitted reasoning content or tool calls.
messages = getattr(response, "messages", None) or []
for message in reversed(messages):
for content in getattr(message, "contents", None) or []:
content_text = getattr(content, "text", None)
if content_text:
fallback = str(content_text).strip()
if fallback:
return fallback
raise RuntimeError(f"sub-agent '{sub_agent_name}' returned no text content")
def _invoke_sub_agent(sub_agent_name: str, task: str) -> str:
"""Sync bridge: drive the async invocation from inside a tool callback.
`@tool` reflects on the underlying callable's signature, so the
tool entry points are sync. The supervisor's chat client typically
runs inside an existing event loop (FastAPI request handler), so
`asyncio.run` would refuse — fall through to a worker thread that
spins up its own loop.
"""
try:
return asyncio.run(_invoke_sub_agent_async(sub_agent_name, task))
except RuntimeError as exc:
if "asyncio.run() cannot be called" not in str(exc):
raise
container: dict[str, Any] = {}
def _runner() -> None:
try:
container["result"] = asyncio.run(
_invoke_sub_agent_async(sub_agent_name, task)
)
except Exception as inner: # pragma: no cover -- defensive
container["error"] = inner
worker = threading.Thread(target=_runner, daemon=True)
worker.start()
worker.join()
if "error" in container:
raise container["error"]
return str(container["result"])
def _delegate(sub_agent_name: str, task: str) -> Content:
"""Common delegation flow: invoke sub-agent, append entry, push state."""
delegations = list(_current_delegations.get())
entry_id = str(uuid.uuid4())
try:
result_text = _invoke_sub_agent(sub_agent_name, task)
except Exception as exc:
logger.exception("subagents: %s delegation failed", sub_agent_name)
delegations.append(
{
"id": entry_id,
"sub_agent": sub_agent_name,
"task": task,
"status": "failed",
# Surface only the exception class — sub-agent error
# messages can leak chat client URLs / quota details
# in deployed environments.
"result": (f"sub-agent error: {exc.__class__.__name__}"),
}
)
# Mirror the contextvar so a follow-up sub-agent call within the
# same supervisor turn sees this entry.
_current_delegations.set(delegations)
return state_update(
text=(f"{sub_agent_name} failed; surfaced in delegation log."),
state={"delegations": delegations},
)
delegations.append(
{
"id": entry_id,
"sub_agent": sub_agent_name,
"task": task,
"status": "completed",
"result": result_text,
}
)
_current_delegations.set(delegations)
return state_update(
text=result_text,
state={"delegations": delegations},
)
# ---------------------------------------------------------------------------
# Supervisor delegation tools — each one wraps a sub-agent invocation.
# ---------------------------------------------------------------------------
# Each @tool wraps a sub-agent invocation. The supervisor LLM "calls"
# these tools to delegate work; each call synchronously runs the
# matching sub-agent (via `_delegate`), appends the entry to the
# `delegations` shared-state slot, and returns a `state_update(...)` so
# the AG-UI emitter pushes a deterministic StateSnapshotEvent — both
# surfacing the result to the supervisor and refreshing the live
# delegation log in the UI.
@tool(
name="research_agent",
description=(
"Delegate a research task to the research sub-agent. Use for "
"gathering facts, background, definitions, statistics. Returns "
"a bulleted list of key facts."
),
)
def research_agent(
task: Annotated[
str,
Field(description="The research question or topic to investigate."),
],
) -> Content:
"""Delegate a research task to the research sub-agent."""
return _delegate("research_agent", task)
@tool(
name="writing_agent",
description=(
"Delegate a drafting task to the writing sub-agent. Use for "
"producing a polished paragraph, draft, or summary. Pass any "
"relevant facts from prior research inside `task`."
),
)
def writing_agent(
task: Annotated[
str,
Field(
description=(
"The drafting brief, including any relevant source "
"facts the writer should weave in."
)
),
],
) -> Content:
"""Delegate a drafting task to the writing sub-agent."""
return _delegate("writing_agent", task)
@tool(
name="critique_agent",
description=(
"Delegate a critique task to the critique sub-agent. Use for "
"reviewing a draft and suggesting concrete improvements."
),
)
def critique_agent(
task: Annotated[
str,
Field(
description=(
"The draft text to critique. Provide the full text -- "
"the critique sub-agent has no other context."
)
),
],
) -> Content:
"""Delegate a critique task to the critique sub-agent."""
return _delegate("critique_agent", task)
# @endregion[supervisor-delegation-tools]
# ---------------------------------------------------------------------------
# Supervisor agent factory
# ---------------------------------------------------------------------------
SUPERVISOR_PROMPT = dedent(
"""
You are a supervisor agent that coordinates three specialized
sub-agents to produce high-quality deliverables.
Available sub-agents (call them as tools):
- research_agent: gathers facts on a topic.
- writing_agent: turns facts + a brief into a polished draft.
- critique_agent: reviews a draft and suggests improvements.
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.
"""
).strip()
def _tool_call_ids(message: dict[str, Any]) -> set[str]:
tool_calls = message.get("tool_calls") or message.get("toolCalls") or []
if not isinstance(tool_calls, list):
return set()
ids: set[str] = set()
for call in tool_calls:
if isinstance(call, dict) and isinstance(call.get("id"), str):
ids.add(call["id"])
return ids
def _tool_result_ids(messages: list[dict[str, Any]], start_index: int) -> set[str]:
ids: set[str] = set()
for message in messages[start_index + 1 :]:
if message.get("role") == "user":
break
if message.get("role") != "tool":
continue
call_id = message.get("tool_call_id") or message.get("toolCallId")
if isinstance(call_id, str):
ids.add(call_id)
return ids
def _drop_orphan_assistant_tool_calls(messages: Any) -> list[dict[str, Any]]:
"""Remove historical assistant tool calls that lack tool result messages.
The MS Agent Framework AG-UI bridge can preserve the assistant tool-call
snapshot while omitting the corresponding tool-role results. OpenAI rejects
that history on the next turn, so keep the final assistant text/state but
omit malformed historical tool-call entries before the supervisor runs.
"""
if not isinstance(messages, list):
return []
clean: list[dict[str, Any]] = []
for index, message in enumerate(messages):
if not isinstance(message, dict):
continue
if message.get("role") == "assistant":
call_ids = _tool_call_ids(message)
if call_ids and not call_ids.issubset(_tool_result_ids(messages, index)):
continue
clean.append(message)
return clean
class SubagentsFrameworkAgent(AgentFrameworkAgent):
"""AgentFrameworkAgent that removes invalid historical tool-call snapshots."""
async def run( # type: ignore[override]
self,
input_data: dict[str, Any],
) -> AsyncGenerator[BaseEvent, None]:
patched_input = dict(input_data)
patched_input["messages"] = _drop_orphan_assistant_tool_calls(
input_data.get("messages")
)
async for event in super().run(patched_input):
yield event
def create_subagents_agent(chat_client: BaseChatClient) -> SubagentsFrameworkAgent:
"""Instantiate the Sub-Agents demo supervisor."""
# Build (and cache) the three sub-agents so the @tool entry points
# can find them via the module-level registry.
_SUB_AGENTS["research_agent"] = _make_sub_agent(
chat_client, "research_agent", _RESEARCH_INSTRUCTIONS
)
_SUB_AGENTS["writing_agent"] = _make_sub_agent(
chat_client, "writing_agent", _WRITING_INSTRUCTIONS
)
_SUB_AGENTS["critique_agent"] = _make_sub_agent(
chat_client, "critique_agent", _CRITIQUE_INSTRUCTIONS
)
base_agent = Agent(
client=chat_client,
name="subagents_supervisor",
instructions=SUPERVISOR_PROMPT,
tools=[research_agent, writing_agent, critique_agent],
default_options={"allow_multiple_tool_calls": False},
middleware=[capture_current_state],
)
return SubagentsFrameworkAgent(
agent=base_agent,
name="CopilotKitMSAgentSubagentsSupervisor",
description=(
"Supervisor agent. Delegates research / writing / critique "
"to specialized sub-agents and surfaces the live "
"delegation log to the UI via shared state."
),
state_schema=STATE_SCHEMA,
require_confirmation=False,
)
@@ -0,0 +1,67 @@
"""Tool Rendering agent — backs the three tool-rendering cells.
Mirrors LangGraph's `langgraph-python/src/agents/tool_rendering_agent.py`:
this agent serves
- `tool-rendering` — per-tool + catch-all on frontend
- `tool-rendering-default-catchall` — no frontend renderers
- `tool-rendering-custom-catchall` — wildcard renderer 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
is intentionally NOT served by this agent — it has its own variant
(`tool_rendering_reasoning_chain_agent.py`) that routes through the
OpenAI Responses API for reasoning streaming. Mixing reasoning events
into the catchall renderers breaks the default-catchall cell's spec
because the built-in default-renderer doesn't paint reasoning blocks.
Tool surface is identical to the reasoning-chain variant — get_weather,
search_flights, get_stock_price, roll_d20 — sourced via direct imports
from that module so the two agents can never drift apart.
"""
from __future__ import annotations
from textwrap import dedent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
from agents.tool_rendering_reasoning_chain_agent import (
get_stock_price,
get_weather,
roll_d20,
search_flights,
)
SYSTEM_PROMPT = dedent(
"""
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 the user asks for them. After tools return, summarize in
one short sentence. Never fabricate data a tool could provide.
"""
).strip()
def create_tool_rendering_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate the tool-rendering agent (non-reasoning)."""
base_agent = Agent(
client=chat_client,
name="tool_rendering_agent",
instructions=SYSTEM_PROMPT,
tools=[get_weather, search_flights, get_stock_price, roll_d20],
)
return AgentFrameworkAgent(
agent=base_agent,
name="ToolRenderingAgent",
description=(
"Weather + flights + stocks + d20 mock tools, multi-tool per "
"turn, no reasoning-event emission. Drives `tool-rendering`, "
"`tool-rendering-default-catchall`, and "
"`tool-rendering-custom-catchall`."
),
)
@@ -0,0 +1,265 @@
"""Tool Rendering (Reasoning Chain) agent for MS Agent Framework.
Backs the three tool-rendering showcase cells:
- tool-rendering-default-catchall (no frontend renderers)
- tool-rendering-custom-catchall (wildcard renderer on frontend)
- tool-rendering-reasoning-chain (per-tool + reasoning + catch-all)
The tools mirror the LangGraph `tool_rendering_agent` so the frontends
can be shared one-to-one with the LangGraph showcase. Each tool returns
a small JSON payload suitable for rich per-tool renderers on the
frontend.
"""
from __future__ import annotations
import json
import os
from random import choice, randint
from textwrap import dedent
from typing import Annotated
from agent_framework import Agent, BaseChatClient, tool
from agent_framework.openai import OpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
from pydantic import Field
@tool(
name="get_weather",
description=(
"Get the current weather for a given location. Useful on its "
"own for weather questions, and a great companion to "
"search_flights."
),
)
def get_weather(
location: Annotated[str, Field(description="The city or region to describe.")],
) -> str:
"""Return mock weather data as JSON."""
return json.dumps(
{
"city": location,
"temperature": 68,
"humidity": 55,
"wind_speed": 10,
"conditions": "Sunny",
}
)
@tool(
name="search_flights",
description=(
"Search mock flights from an origin airport to a destination "
"airport. Pairs naturally with get_weather: after searching "
"flights, check the weather at the destination."
),
)
def search_flights(
origin: Annotated[str, Field(description="Origin airport code, e.g. SFO.")],
destination: Annotated[
str, Field(description="Destination airport code, e.g. JFK.")
],
) -> str:
"""Return mock flight search results as JSON."""
return json.dumps(
{
"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(
name="get_stock_price",
description=(
"Get a mock current price for a stock ticker. When the user "
"asks about one ticker, consider pulling a related ticker for "
"comparison."
),
)
def get_stock_price(
ticker: Annotated[str, Field(description="Stock ticker symbol, e.g. AAPL.")],
price_usd: Annotated[
float | None,
Field(default=None, description="Deterministic price; None = random."),
] = None,
change_pct: Annotated[
float | None,
Field(default=None, description="Deterministic change percent; None = random."),
] = None,
) -> str:
"""Return mock stock price data as JSON.
Mirrors the LangGraph reference's deterministic-`value` pattern: when
`price_usd` / `change_pct` are supplied by the aimock fixture, the
tool echoes them back verbatim so the e2e spec can assert on a fixed
quote. When omitted, the tool returns mock random values.
"""
return json.dumps(
{
"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(
name="roll_d20",
description=(
"Roll a 20-sided die. The `value` argument lets the aimock fixture "
"script a deterministic roll for testing — when provided in the "
"valid range [1, 20], it is echoed back as the result; otherwise "
"a random natural d20 is rolled. Mirrors the LangGraph reference."
),
)
def roll_d20(
value: Annotated[
int,
Field(description="Deterministic roll value [1..20]; 0 = random."),
] = 0,
) -> str:
"""Return a mock d20 roll as JSON, mirroring the LangGraph signature."""
rolled = value if isinstance(value, int) and 1 <= value <= 20 else randint(1, 20)
return json.dumps({"sides": 20, "value": rolled, "result": rolled})
@tool(
name="roll_dice",
description=(
"Compat alias for `roll_d20`. Some fixtures call `roll_dice` with "
"a `sides` arg; we route those to a d20 roll for compatibility."
),
)
def roll_dice(
sides: Annotated[int, Field(description="Number of sides on the die.")] = 6,
) -> str:
"""Return a mock dice roll as JSON."""
return json.dumps({"sides": sides, "result": randint(1, max(2, sides))})
SYSTEM_PROMPT = dedent(
"""
You are a helpful travel & lifestyle concierge. You have mock tools
for weather, flights, stock prices, and dice rolls — they all return
fake data, so call them liberally.
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 before composing your final reply.
Examples of helpful chains you should default to:
- "What's the weather in Tokyo?" -> call get_weather("Tokyo"), then
call search_flights(origin="SFO", destination="Tokyo") so the user
also sees how to get there.
- "How is AAPL doing?" -> call get_stock_price("AAPL"), then call
get_stock_price on a related ticker (e.g. "MSFT" or "GOOGL") for
comparison.
- "Roll a d20" -> call roll_dice(20), then call roll_dice again with
a different number of sides so the user sees a contrast.
- "Find flights from SFO to JFK" -> call search_flights, then call
get_weather on the destination city.
Between tool calls, briefly narrate your reasoning so the user can
follow along. Only skip chaining when the user has clearly asked for
a single, atomic answer. Never fabricate data that a tool could
provide.
"""
).strip()
def _build_reasoning_chain_chat_client() -> BaseChatClient:
"""Build a Responses-API chat client for reasoning-token streaming.
Mirrors ``reasoning_agent.py::_build_reasoning_chat_client`` — the model
env var defaults to ``OPENAI_REASONING_MODEL`` and then the canonical
``gpt-5.4`` so the fixture's ``response.reasoning_summary_text.delta``
events surface as AG-UI ``REASONING_MESSAGE_*`` events.
"""
return OpenAIChatClient(
model=os.environ.get("OPENAI_REASONING_MODEL", "gpt-5.4"),
api_key=os.environ.get("OPENAI_API_KEY"),
)
def create_tool_rendering_reasoning_chain_agent(
_chat_client_ignored: BaseChatClient,
) -> AgentFrameworkAgent:
"""Instantiate the tool-rendering reasoning-chain demo agent.
The shared ChatCompletions client from ``agent_server.py`` is intentionally
ignored — this cell needs the OpenAI Responses API specifically so the
fixture's per-leg ``reasoning`` summaries surface as AG-UI
``REASONING_MESSAGE_*`` events (mirrors ``reasoning_agent.py`` and the
LGP reference's ``use_responses_api=True`` config). ChatCompletions emits
reasoning as ``protected_data`` only — no visible text reaches the
``<CopilotChatReasoningMessage>`` slot, which is the whole point of this
cell vs the plain ``tool-rendering`` demo.
"""
base_agent = Agent(
client=_build_reasoning_chain_chat_client(),
name="tool_rendering_reasoning_chain_agent",
instructions=SYSTEM_PROMPT,
tools=[get_weather, search_flights, get_stock_price, roll_d20, roll_dice],
# Disable server-side conversation storage so the OpenAI Responses
# client sends the full message history (including the original
# user prompt) on every leg of a tool-rendering chain instead of
# compressing prior context behind ``previous_response_id``. aimock
# is stateless and cannot resolve that ID, so chain-leg fixtures
# keyed on ``userMessage`` would otherwise miss and the chain would
# fall through to the real-OpenAI proxy with a ``ChatClientException``.
# Same pattern as ``shared_state_read_write_agent.py``.
default_options={"store": False},
)
return AgentFrameworkAgent(
agent=base_agent,
name="ToolRenderingReasoningChainAgent",
description=(
"Travel & lifestyle concierge that chains tool calls "
"(weather, flights, stocks, dice) for tool-rendering demos."
),
require_confirmation=False,
)
def build_default_chat_client() -> BaseChatClient:
"""Create a default OpenAI chat client from environment variables."""
if not os.getenv("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY environment variable is required")
return OpenAIChatClient(
model=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("OPENAI_API_KEY"),
)
@@ -0,0 +1,30 @@
"""
Simple voice agent for MS Agent Framework — no tools.
The voice demo tests transcription and basic chat, not tool execution.
Using a tool-free agent avoids the tool-call loop problem where the backend
agent executes a tool but doesn't loop back to the LLM for a text summary,
resulting in empty assistant responses in the AG-UI stream.
"""
from __future__ import annotations
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
def create_voice_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
"""Instantiate a simple voice demo agent with no tools."""
base_agent = Agent(
client=chat_client,
name="voice_agent",
instructions="You are a helpful, concise assistant.",
tools=[],
)
return AgentFrameworkAgent(
agent=base_agent,
name="VoiceDemoAgent",
description="Simple assistant for voice demo — no tools.",
require_confirmation=False,
)
@@ -0,0 +1,52 @@
// Dedicated runtime for the A2UI — Fixed Schema demo. Splitting into its own
// endpoint lets us set `a2ui.injectA2UITool: false` — the backend agent owns
// the `display_flight` tool which emits its own `a2ui_operations` container
// (see `src/agents/a2ui_fixed.py`).
//
// Reference:
// - showcase/integrations/langgraph-python/src/app/api/copilotkit-a2ui-fixed-schema/route.ts
// - src/agents/a2ui_fixed.py (backend agent, mounted at `/a2ui_fixed`)
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const a2uiFixedSchemaAgent = new HttpAgent({
url: `${AGENT_URL}/a2ui_fixed`,
});
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents: { "a2ui-fixed-schema": a2uiFixedSchemaAgent },
a2ui: {
// The backend emits its own `a2ui_operations` container via the
// `display_flight` tool (see src/agents/a2ui_fixed.py). We still run
// the A2UI middleware so it detects the container in tool results and
// forwards surfaces to the frontend — but we do NOT inject a runtime
// `render_a2ui` tool on top of the agent's existing tools.
injectA2UITool: false,
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-a2ui-fixed-schema",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,55 @@
// Dedicated runtime for the Agent Config Object demo.
//
// Proxies CopilotKit runtime requests to the MS Agent Framework backend's
// `/agent-config` endpoint, where `agent_config_agent.py` reads forwarded
// props (tone / expertise / responseLength) and rebuilds its system prompt
// per turn. Scoping to its own route + agent name keeps non-demo cells out
// of the dynamic-prompt path.
//
// References:
// - src/agents/agent_config_agent.py (backend graph)
// - src/app/demos/agent-config/page.tsx (provider properties source)
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent() {
return new HttpAgent({ url: `${AGENT_URL}/agent-config` });
}
const agents: Record<string, AbstractAgent> = {
// The page mounts <CopilotKit agent="agent-config-demo">.
"agent-config-demo": createAgent(),
// useAgent() with no args defaults to "default"; alias so internal lookups
// resolve to the same agent.
default: createAgent(),
};
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-agent-config",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in
// MaybePromise<NonEmptyRecord<...>> which rejects plain Records;
// fixed in source, pending release.
agents,
}),
});
return await handleRequest(req);
} catch (error: unknown) {
const err = error as Error;
return NextResponse.json(
{ error: err.message, stack: err.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,79 @@
// Dedicated runtime for the /demos/auth cell.
//
// Demonstrates framework-native request authentication via the V2 runtime's
// `onRequest` hook, which runs before routing and can short-circuit the
// request by throwing a Response. We validate a static `Authorization: Bearer
// <DEMO_TOKEN>` header; mismatch throws 401 before the request reaches the
// agent.
//
// Implementation note: the V1 Next.js adapter
// (`copilotRuntimeNextJSAppRouterEndpoint`) does NOT forward the `hooks`
// option to the V2 fetch handler. To get `onRequest` wired, this route uses
// `createCopilotRuntimeHandler` from `@copilotkit/runtime/v2` directly — the
// framework-agnostic fetch handler that returns a plain
// `(Request) => Promise<Response>`, which composes cleanly with a Next.js
// App Router route export.
//
// References:
// - packages/runtime/src/v2/runtime/core/hooks.ts (onRequest semantics)
// - packages/runtime/src/v2/runtime/__tests__/hooks.test.ts (throw Response pattern)
import type { NextRequest } from "next/server";
import {
CopilotRuntime,
createCopilotRuntimeHandler,
} from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { DEMO_AUTH_HEADER } from "@/app/demos/auth/demo-token";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// Reuse the shared MS Agent Framework backend for the authenticated path.
// The point of this demo is the gate mechanism, not per-user agent branching —
// authenticated users get the same behavior as any other neutral demo.
function createAgent() {
return new HttpAgent({ url: `${AGENT_URL}/` });
}
const runtime = new CopilotRuntime({
agents: {
"auth-demo": createAgent(),
// Fallback: useAgent() with no args resolves "default" — alias to the
// same agent so hooks in the demo page resolve cleanly.
default: createAgent(),
},
});
const BASE_PATH = "/api/copilotkit-auth";
// Framework-agnostic fetch handler with the auth gate wired up.
const handler = createCopilotRuntimeHandler({
runtime,
basePath: BASE_PATH,
hooks: {
onRequest: ({ request }) => {
const authHeader = request.headers.get("authorization");
if (authHeader !== DEMO_AUTH_HEADER) {
// Throwing a Response short-circuits the pipeline. The runtime maps
// thrown Responses to the HTTP response verbatim (status + body).
throw new Response(
JSON.stringify({
error: "unauthorized",
message:
"Missing or invalid Authorization header. Click Authenticate above to send messages.",
}),
{
status: 401,
headers: { "content-type": "application/json" },
},
);
}
},
},
});
// Next.js App Router bindings. The handler is framework-agnostic — it takes
// a web Request and returns a web Response — so it drops straight into the
// POST/GET exports without any adapter shim.
export const POST = (req: NextRequest) => handler(req);
export const GET = (req: NextRequest) => handler(req);
@@ -0,0 +1,87 @@
// Dedicated runtime for the Beautiful Chat flagship showcase cell.
//
// Beautiful Chat simultaneously exercises A2UI (dynamic + fixed schema),
// Open Generative UI, and MCP Apps. The main `/api/copilotkit` runtime
// keeps those global flags OFF so per-demo `useFrontendTool` /
// `useComponent` registrations in non-flagship cells stay isolated. This
// route enables the combined-runtime shape for the one cell that needs it.
//
// References:
// - showcase/integrations/langgraph-python/src/app/api/copilotkit-beautiful-chat/route.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import type { AbstractAgent } from "@ag-ui/client";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// Single shared agent instance. Earlier this file created two independent
// HttpAgent objects (one for "beautiful-chat", one for "default"); the chat
// drove the "beautiful-chat" instance, the canvas read `useAgent()` which
// resolved to "default", and the STATE_SNAPSHOT delivered by the chat run
// never reached the canvas's agent.state. Sharing one instance fixes the
// Task Manager pill — the canvas's `agent.state.todos` updates as soon as
// the chat agent receives a STATE_SNAPSHOT from `manage_todos`.
const beautifulChatAgent: AbstractAgent = new HttpAgent({
url: `${AGENT_URL}/beautiful-chat`,
});
const agents: Record<string, AbstractAgent> = {
// The page's <CopilotKit agent="beautiful-chat"> resolves here.
"beautiful-chat": beautifulChatAgent,
// Internal components (example-canvas) call `useAgent()` with no args,
// which defaults to agentId "default". Alias to the SAME instance so
// state pushed via the chat reaches the canvas.
default: beautifulChatAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
// Canonical: openGenerativeUI: true, a2ui.injectA2UITool: false, mcpApps.
openGenerativeUI: true,
a2ui: {
// The backend agent owns `generate_a2ui`, so we must NOT inject the
// runtime's default A2UI tool on top (that would double-bind the tool
// slot and confuse the LLM).
injectA2UITool: false,
// Models follow the tool-usage guide and omit `catalogId`, and the
// middleware then falls back to the unregistered spec basic catalog
// ("Catalog not found" render error). Pin the catalog the page registers.
defaultCatalogId: "copilotkit://app-dashboard-catalog",
},
mcpApps: {
servers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
// Stable serverId so persisted threads keep restoring the same MCP
// server across URL changes.
serverId: "beautiful_chat_mcp",
},
],
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-beautiful-chat",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,64 @@
// Dedicated runtime for the Declarative Generative UI (A2UI — Dynamic Schema)
// demo. Splitting into its own endpoint (mirroring the LangGraph reference)
// lets us set `a2ui.injectA2UITool: false` — the backend agent owns the
// `generate_a2ui` tool itself (see `src/agents/a2ui_dynamic.py`), so
// double-binding from the runtime would duplicate the tool slot and confuse
// the LLM.
//
// Reference:
// - showcase/integrations/langgraph-python/src/app/api/copilotkit-declarative-gen-ui/route.ts
// - src/agents/a2ui_dynamic.py (backend agent, mounted at `/a2ui_dynamic`)
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
// The MS Agent backend runs as a separate process on port 8000. This runtime
// proxies CopilotKit requests to it via AG-UI protocol. The A2UI — dynamic
// agent is mounted at `/a2ui_dynamic` by `agent_server.py`.
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const declarativeGenUiAgent = new HttpAgent({
url: `${AGENT_URL}/a2ui_dynamic`,
});
const runtime = new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in source, pending release
agents: { "declarative-gen-ui": declarativeGenUiAgent },
a2ui: {
// The backend agent owns the `generate_a2ui` tool explicitly (see
// src/agents/a2ui_dynamic.py), so the runtime MUST NOT auto-inject its
// own A2UI tool on top. The A2UI middleware still runs — it serialises
// the registered client catalog into the agent's `copilotkit.context`
// so the secondary LLM inside `generate_a2ui` knows which components
// to emit — and it still detects the `a2ui_operations` container in
// the tool result and streams rendered surfaces to the frontend.
injectA2UITool: false,
// Models follow the tool-usage guide and omit `catalogId`, and the
// middleware then falls back to the unregistered spec basic catalog
// ("Catalog not found" render error). Pin the catalog the page registers.
defaultCatalogId: "declarative-gen-ui-catalog",
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-gen-ui",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,52 @@
// Dedicated runtime for the declarative-hashbrown demo.
//
// The demo page (`src/app/demos/declarative-hashbrown/page.tsx`) wraps
// CopilotChat in the HashBrownDashboard provider and overrides the
// assistant message slot with a renderer that consumes hashbrown-shaped
// structured output via `@hashbrownai/react`'s `useUiKit` + `useJsonParser`.
// The MS Agent behind this endpoint (see `src/agents/byoc_hashbrown_agent.py`,
// mounted at `/byoc-hashbrown` in `agent_server.py`) has a system prompt
// tuned to emit that shape. The legacy `byoc_hashbrown` Python module name
// is retained (mirrors LangGraph's convention — see
// `langgraph-python/src/app/api/copilotkit-declarative-hashbrown/route.ts`);
// only the user-facing slug, route, and frontend folder use the
// `declarative-` prefix so the manifest is one-to-one with LGP.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const declarativeHashbrownAgent = new HttpAgent({
url: `${AGENT_URL}/byoc-hashbrown`,
});
const runtime = new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in
// MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in
// source, pending release.
agents: { "declarative-hashbrown-demo": declarativeHashbrownAgent },
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-hashbrown",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,48 @@
// Dedicated runtime for the declarative-json-render demo.
//
// The demo page (`src/app/demos/declarative-json-render/page.tsx`) swaps
// in `JsonRenderAssistantMessage` and renders an agent-emitted JSON spec
// via `@json-render/react` against a Zod-validated catalog (MetricCard,
// BarChart, PieChart). The MS Agent behind this endpoint (see
// `src/agents/byoc_json_render_agent.py`, mounted at `/byoc-json-render`
// in `agent_server.py`) emits that JSON envelope. The legacy
// `byoc_json_render` Python module name is retained (matches LGP's
// convention); only the slug, route, and frontend folder use the
// `declarative-` prefix.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const declarativeJsonRenderAgent = new HttpAgent({
url: `${AGENT_URL}/byoc-json-render`,
});
const runtime = new CopilotRuntime({
// @ts-ignore -- see hashbrown route
agents: { byoc_json_render: declarativeJsonRenderAgent },
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-json-render",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,84 @@
// CopilotKit runtime for the MCP Apps cell (MS Agent Framework backend).
//
// The runtime's `mcpApps` config auto-applies the MCP Apps middleware to the
// agent: when the agent calls a tool backed by an MCP UI resource, the
// middleware fetches the resource and emits the activity event that the
// built-in `MCPAppsActivityRenderer` (registered by CopilotKit internally)
// renders in the chat as a sandboxed iframe.
//
// The optional catch-all route mirrors the langgraph-python north star. MCP
// Apps resource proxy requests are addressed below `/api/copilotkit-mcp-apps`,
// so a plain `route.ts` at the parent segment handles the chat POST but misses
// those subpath requests.
//
// Reference:
// https://docs.copilotkit.ai/integrations/microsoft-agent-framework/generative-ui/mcp-apps
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// No trailing slash on the URL. FastAPI mounts this agent at `/mcp-apps`
// exactly (via `add_agent_framework_fastapi_endpoint(path="/mcp-apps")` in
// agent_server.py); posting to `/mcp-apps/` triggers FastAPI's
// redirect-to-canonical 307, which kills the streaming SSE response and
// surfaces as `fetch failed` / `INCOMPLETE_STREAM` in the runtime.
const mcpAppsAgent = new HttpAgent({ url: `${AGENT_URL}/mcp-apps` });
// headless-complete shares this runtime because its cell also exercises
// MCP Apps activity rendering (the "Sketch a diagram" pill exercises the
// Excalidraw MCP server via the same middleware). The catch-all `/` agent
// on the Python backend backs it -- no dedicated headless endpoint.
const headlessCompleteAgent = new HttpAgent({
url: `${AGENT_URL}/headless-complete`,
});
// @region[runtime-mcpapps-config]
// The `mcpApps.servers` config is all you need server-side. The runtime
// auto-applies the MCP Apps middleware to every registered agent: on each
// MCP tool call it fetches the associated UI resource and emits an
// `activity` event that the built-in `MCPAppsActivityRenderer` renders
// inline in the chat.
const runtime = new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in source, pending release
agents: {
"mcp-apps": mcpAppsAgent,
"headless-complete": headlessCompleteAgent,
},
mcpApps: {
servers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
// Keep the server id 1:1 with langgraph-python so persisted MCP Apps
// and fixture-backed resource calls use the same identity.
serverId: "excalidraw",
},
],
},
});
// @endregion[runtime-mcpapps-config]
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-mcp-apps",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,54 @@
// Dedicated runtime for the Multimodal Attachments demo.
//
// Why its own route? The backing MS Agent Framework agent (mounted at
// `/multimodal` on the Python agent server) runs a vision-capable model
// (gpt-4o-mini). Every other cell in this showcase uses the shared default
// agent. Registering the multimodal agent under the main `/api/copilotkit`
// runtime would mix concerns; a dedicated route keeps the vision capability
// scoped to exactly the cell that exercises it, matching the pattern used
// by the LangGraph showcase.
//
// The page at src/app/demos/multimodal/page.tsx points its `runtimeUrl` at
// this endpoint and sets `agent="multimodal-demo"` (the slug registered below).
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createMultimodalAgent() {
return new HttpAgent({ url: `${AGENT_URL}/multimodal` });
}
const agents: Record<string, AbstractAgent> = {
"multimodal-demo": createMultimodalAgent(),
// Alias for any internal component that calls `useAgent()` without args.
default: createMultimodalAgent(),
};
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-multimodal",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
// @ts-ignore -- see main route.ts; published CopilotRuntime's `agents`
// type wraps Record in MaybePromise<NonEmptyRecord<...>> which rejects
// plain Records. Fixed in source, pending release.
agents,
}),
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
// Dedicated runtime for the Open Generative UI demos.
//
// Isolated from the main `/api/copilotkit` runtime because the
// `openGenerativeUI` flag flips `openGenerativeUIEnabled: true` globally
// on the runtime info probe, which causes the CopilotKit provider's
// setTools effect to wipe per-demo `useFrontendTool` / `useComponent`
// registrations in the default runtime.
//
// Each OGUI agent is backed by a dedicated sub-path on the MS Agent
// Framework FastAPI server (see `src/agent_server.py`):
// - /open-gen-ui -> the minimal OGUI agent
// - /open-gen-ui-advanced -> the advanced OGUI agent (sandbox functions)
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const agents: Record<string, AbstractAgent> = {
"open-gen-ui": new HttpAgent({ url: `${AGENT_URL}/open-gen-ui` }),
"open-gen-ui-advanced": new HttpAgent({
url: `${AGENT_URL}/open-gen-ui-advanced`,
}),
};
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-ogui",
serviceAdapter: new ExperimentalEmptyAdapter(),
// Server-side config is identical for the minimal and advanced cells --
// the advanced behaviour (sandbox -> host function calls) is wired
// entirely on the frontend via `openGenerativeUI.sandboxFunctions` on
// the provider. The single `openGenerativeUI` flag below turns on
// Open Generative UI for the listed agent(s); the runtime middleware
// converts each agent's streamed `generateSandboxedUi` tool call into
// `open-generative-ui` activity events.
// @region[minimal-runtime-flag]
// @region[advanced-runtime-config]
runtime: new CopilotRuntime({
// @ts-ignore -- see main route.ts for type-comment rationale
agents,
openGenerativeUI: {
agents: ["open-gen-ui", "open-gen-ui-advanced"],
},
}),
// @endregion[advanced-runtime-config]
// @endregion[minimal-runtime-flag]
});
return await handleRequest(req);
} catch (error: unknown) {
const err = error as Error;
return NextResponse.json(
{ error: err.message, stack: err.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,113 @@
// Dedicated runtime for the /demos/voice cell (MS Agent Python).
//
// Goals
// -----
// 1. Advertise `audioFileTranscriptionEnabled: true` on `/info` so the chat
// composer renders the mic button.
// 2. Handle `POST /transcribe` by invoking an OpenAI-backed
// `TranscriptionServiceOpenAI` (from `@copilotkit/voice`).
// 3. Return a deterministic 4xx when `OPENAI_API_KEY` is not configured.
//
// Wires the V2 `CopilotRuntime` directly because the V1 wrapper drops the
// `transcriptionService` option. V2 URL-routes on `/info`, `/agent/:id/run`,
// `/transcribe`, etc., so the route lives at `[[...slug]]/route.ts`.
// @region[voice-runtime]
// @region[transcription-service-guard]
import type { NextRequest } from "next/server";
import {
CopilotRuntime,
TranscriptionService,
createCopilotRuntimeHandler,
} from "@copilotkit/runtime/v2";
import type { TranscribeFileOptions } from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { TranscriptionServiceOpenAI } from "@copilotkit/voice";
import OpenAI from "openai";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// Point at the tool-free /voice endpoint so aimock returns a direct text
// response instead of a tool call that the agent can't summarize.
//
// No trailing slash on the URL. FastAPI mounts this agent at `/voice`
// exactly (via `add_agent_framework_fastapi_endpoint(path="/voice")` in
// agent_server.py); posting to `/voice/` triggers FastAPI's
// redirect-to-canonical 307, which kills the streaming SSE response and
// surfaces as `fetch failed` / `INCOMPLETE_STREAM` in the runtime.
const voiceDemoAgent = new HttpAgent({ url: `${AGENT_URL}/voice` });
/**
* Transcription service wrapper that reports a clean, typed auth error when
* OPENAI_API_KEY is not configured. When the key is present we delegate to
* the real OpenAI-backed service; any upstream Whisper error keeps its
* natural categorization.
*
* Note: We pin `baseURL` to real OpenAI (or `OPENAI_TRANSCRIPTION_BASE_URL`
* when explicitly set) instead of falling through to `OPENAI_BASE_URL`. In
* local docker / Railway preview environments `OPENAI_BASE_URL` points at
* aimock so LLM completions stay deterministic, but aimock has a catchall
* `endpoint: "transcription"` fixture that would otherwise intercept every
* real mic recording and return the canned "What is the weather in Tokyo?"
* phrase regardless of what the user actually said — and on production
* aimock's transcription proxy returns a 502 "Invalid file format" before
* any phrase reaches the user. The sample-audio button is the deterministic
* affordance (synchronous text injection); the mic is the only path that
* should exercise real Whisper.
*
* Mirrors langgraph-python's voice route exactly.
*/
class GuardedOpenAITranscriptionService extends TranscriptionService {
private delegate: TranscriptionServiceOpenAI | null;
constructor() {
super();
const apiKey = process.env.OPENAI_API_KEY;
const baseURL =
process.env.OPENAI_TRANSCRIPTION_BASE_URL ?? "https://api.openai.com/v1";
this.delegate = apiKey
? new TranscriptionServiceOpenAI({
openai: new OpenAI({ apiKey, baseURL }),
})
: null;
}
async transcribeFile(options: TranscribeFileOptions): Promise<string> {
if (!this.delegate) {
throw new Error(
"OPENAI_API_KEY not configured for this deployment (api key missing). " +
"Set OPENAI_API_KEY to enable voice transcription.",
);
}
return this.delegate.transcribeFile(options);
}
}
// @endregion[transcription-service-guard]
let cachedHandler: ((req: Request) => Promise<Response>) | null = null;
function getHandler(): (req: Request) => Promise<Response> {
if (cachedHandler) return cachedHandler;
const runtime = new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in
// MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in
// source, pending release.
agents: {
"voice-demo": voiceDemoAgent,
default: voiceDemoAgent,
},
transcriptionService: new GuardedOpenAITranscriptionService(),
});
cachedHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit-voice",
});
return cachedHandler;
}
export const POST = (req: NextRequest) => getHandler()(req);
export const GET = (req: NextRequest) => getHandler()(req);
export const PUT = (req: NextRequest) => getHandler()(req);
export const DELETE = (req: NextRequest) => getHandler()(req);
// @endregion[voice-runtime]
@@ -0,0 +1,750 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import type { AbstractAgent, BaseEvent } from "@ag-ui/client";
import { EventType, FunctionMiddleware, HttpAgent } from "@ag-ui/client";
import { Observable } from "rxjs";
// The agent backend runs as a separate process on port 8000.
// This runtime proxies CopilotKit requests to it via AG-UI protocol.
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const REPLAY_SAFE_TOOL_CALL_ID_SUFFIX = /__ck_run_[0-9a-f-]+$/i;
// Per-request request/response logging is gated behind this flag (default off).
// Under d6 probe fan-out, unconditional per-request logs flooded Railway's
// 500-logs/sec cap and killed the replica ("Messages dropped" → container stop).
// Set SHOWCASE_ROUTE_DEBUG=1 to re-enable verbose per-request tracing locally.
const ROUTE_DEBUG =
process.env.SHOWCASE_ROUTE_DEBUG === "1" ||
process.env.SHOWCASE_ROUTE_DEBUG === "true";
console.log("[copilotkit/route] Initializing CopilotKit runtime");
console.log(`[copilotkit/route] AGENT_URL: ${AGENT_URL}`);
function createAgent(path = "/") {
const agent = new HttpAgent({ url: `${AGENT_URL}${path}` });
// Universal strip middleware (no decision-suffix). Runs as the first
// registered middleware so EVERY outbound request carries clean
// canonical toolCallIds (the replay-safe `__ck_run_<uuid>` suffix is
// removed) before any agent-specific middleware sees the input.
// Decision suffixing (`__approved` / `__rejected` / `__cancelled`)
// is intentionally NOT applied here — only `createReplaySafeAgent`
// does that, because the suffix is non-idempotent and the inner
// replay-safe middleware re-runs the same logic after this one.
agent.use(
new FunctionMiddleware((input, next) => {
return next.run({
...input,
messages: (input.messages ?? []).map(
stripReplaySafeToolCallIdsFromMessage,
),
});
}),
);
return agent;
}
function stripReplaySafeToolCallId(id: string): string {
return id.replace(REPLAY_SAFE_TOOL_CALL_ID_SUFFIX, "");
}
function makeReplaySafeToolCallId(id: string, runId: string): string {
return `${stripReplaySafeToolCallId(id)}__ck_run_${runId}`;
}
function stripReplaySafeToolCallIdsFromMessage(message: unknown): unknown {
if (!message || typeof message !== "object") return message;
const next = { ...(message as Record<string, unknown>) };
let changed = false;
if (typeof next.toolCallId === "string") {
next.toolCallId = stripReplaySafeToolCallId(next.toolCallId);
changed = true;
}
if (typeof next.tool_call_id === "string") {
next.tool_call_id = stripReplaySafeToolCallId(next.tool_call_id);
changed = true;
}
// Strip on BOTH the camelCase (AG-UI canonical) and snake_case (OpenAI
// wire format) tool-call arrays. Some runtimes / message converters
// pass the OpenAI shape through unchanged; without this branch the
// replay-safe `__ck_run_<uuid>` suffix slips through to aimock and
// toolCallId-keyed fixtures fail to match on follow-up turns.
const stripToolCallArrayEntry = (toolCall: unknown) => {
if (!toolCall || typeof toolCall !== "object") return toolCall;
const call = { ...(toolCall as Record<string, unknown>) };
if (typeof call.id === "string") {
call.id = stripReplaySafeToolCallId(call.id);
}
// OpenAI nests the tool_call_id under `function` in some shapes; strip
// there too just to keep the surface clean before aimock sees it.
if (call.function && typeof call.function === "object") {
const fn = { ...(call.function as Record<string, unknown>) };
if (typeof fn.tool_call_id === "string") {
fn.tool_call_id = stripReplaySafeToolCallId(fn.tool_call_id);
call.function = fn;
}
}
return call;
};
if (Array.isArray(next.toolCalls)) {
next.toolCalls = next.toolCalls.map(stripToolCallArrayEntry);
changed = true;
}
if (Array.isArray(next.tool_calls)) {
next.tool_calls = next.tool_calls.map(stripToolCallArrayEntry);
changed = true;
}
return changed ? next : message;
}
function textFromMessageContent(content: unknown): string | undefined {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return undefined;
const text = content
.map((part) => {
if (!part || typeof part !== "object") return "";
const text = (part as { text?: unknown }).text;
return typeof text === "string" ? text : "";
})
.join("");
return text || undefined;
}
function textFromContextValue(value: unknown): string | undefined {
if (typeof value === "string") return value;
if (value === undefined || value === null) return undefined;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function buildContextSystemMessage(context: unknown): string | undefined {
if (!Array.isArray(context) || context.length === 0) return undefined;
const lines = ["## Context from the application"];
for (const entry of context) {
if (!entry || typeof entry !== "object") continue;
const record = entry as Record<string, unknown>;
const description =
typeof record.description === "string" ? record.description : undefined;
const value = textFromContextValue(record.value);
if (!description || !value) continue;
lines.push("", description, value);
}
return lines.length > 1 ? lines.join("\n") : undefined;
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object"
? (value as Record<string, unknown>)
: undefined;
}
function buildSharedStateReadWriteSystemMessage(state: unknown): string {
const stateRecord = readRecord(state);
const prefs = readRecord(stateRecord?.preferences) ?? {};
const name = typeof prefs.name === "string" ? prefs.name : "";
const tone = typeof prefs.tone === "string" ? prefs.tone : "casual";
const language =
typeof prefs.language === "string" ? prefs.language : "English";
const interests = Array.isArray(prefs.interests)
? prefs.interests.filter(
(interest): interest is string => typeof interest === "string",
)
: [];
return [
"You are a helpful, concise assistant. The user's preferences are supplied via shared state and added as a system message at the start of every turn - always respect them. When the user asks you to remember something, or you observe something worth surfacing in the UI's notes panel, call `set_notes` with the FULL updated list of short notes (existing notes + new). Keep each note short.",
"",
"[shared-state-read-write] preferences:",
"{",
` "name": ${JSON.stringify(name)},`,
` "tone": ${JSON.stringify(tone)},`,
` "language": ${JSON.stringify(language)},`,
` "interests": ${JSON.stringify(interests)}`,
"}",
"Tailor every response to these preferences. Address the user by name when appropriate.",
].join("\n");
}
type ToolResultDecision = "approved" | "rejected" | "cancelled";
function toolDecisionFromContent(
content: unknown,
): ToolResultDecision | undefined {
const text = textFromMessageContent(content);
if (!text) return undefined;
try {
const parsed = JSON.parse(text);
if (parsed?.approved === true || parsed?.accepted === true) {
return "approved";
}
if (parsed?.approved === false || parsed?.accepted === false) {
return "rejected";
}
if (parsed?.cancelled === true || parsed?.canceled === true) {
return "cancelled";
}
} catch {
const normalized = text.toLowerCase();
if (
(normalized.includes("cancelled") || normalized.includes("canceled")) &&
(normalized.includes("not scheduled") ||
normalized.includes("not booked") ||
normalized.includes("no time"))
) {
return "cancelled";
}
}
return undefined;
}
function makeDecisionToolCallId(id: string, decision: ToolResultDecision) {
return `${id}__${decision}`;
}
function applyToolResultDecisionSuffix(
message: unknown,
decisionsByToolCallId: Map<string, ToolResultDecision>,
): unknown {
if (!message || typeof message !== "object") return message;
const next = { ...(message as Record<string, unknown>) };
let changed = false;
if (typeof next.toolCallId === "string") {
const decision = decisionsByToolCallId.get(next.toolCallId);
if (decision) {
next.toolCallId = makeDecisionToolCallId(next.toolCallId, decision);
changed = true;
}
}
if (typeof next.tool_call_id === "string") {
const decision = decisionsByToolCallId.get(next.tool_call_id);
if (decision) {
next.tool_call_id = makeDecisionToolCallId(next.tool_call_id, decision);
changed = true;
}
}
const suffixArrayEntry = (toolCall: unknown) => {
if (!toolCall || typeof toolCall !== "object") return toolCall;
const call = { ...(toolCall as Record<string, unknown>) };
if (typeof call.id === "string") {
const decision = decisionsByToolCallId.get(call.id);
if (decision) {
call.id = makeDecisionToolCallId(call.id, decision);
}
}
return call;
};
if (Array.isArray(next.toolCalls)) {
next.toolCalls = next.toolCalls.map(suffixArrayEntry);
changed = true;
}
if (Array.isArray(next.tool_calls)) {
next.tool_calls = next.tool_calls.map(suffixArrayEntry);
changed = true;
}
return changed ? next : message;
}
function messageRole(message: unknown): string | undefined {
if (!message || typeof message !== "object") return undefined;
const role = (message as Record<string, unknown>).role;
return typeof role === "string" ? role : undefined;
}
function hasTextContent(message: Record<string, unknown>): boolean {
const content = textFromMessageContent(message.content);
return Boolean(content?.trim());
}
function dropStaleToolInteractionsBeforeLatestUser(messages: unknown[]) {
const latestUserIndex = messages.findLastIndex(
(message) => messageRole(message) === "user",
);
if (latestUserIndex < 0 || latestUserIndex !== messages.length - 1) {
return messages;
}
let changed = false;
const nextMessages: unknown[] = [];
messages.forEach((message, index) => {
if (index >= latestUserIndex || !message || typeof message !== "object") {
nextMessages.push(message);
return;
}
const record = message as Record<string, unknown>;
if (record.role === "tool") {
changed = true;
return;
}
if (
record.role === "assistant" &&
(Array.isArray(record.toolCalls) || Array.isArray(record.tool_calls))
) {
const next = { ...record };
delete next.toolCalls;
delete next.tool_calls;
changed = true;
if (hasTextContent(next)) {
nextMessages.push(next);
}
return;
}
nextMessages.push(message);
});
return changed ? nextMessages : messages;
}
function prepareReplaySafeMessages(messages: unknown[] = []) {
const stripped = messages.map(stripReplaySafeToolCallIdsFromMessage);
const decisionsByToolCallId = new Map<string, ToolResultDecision>();
for (const message of stripped) {
if (!message || typeof message !== "object") continue;
const record = message as Record<string, unknown>;
const toolCallId =
typeof record.tool_call_id === "string"
? record.tool_call_id
: typeof record.toolCallId === "string"
? record.toolCallId
: undefined;
if (record.role !== "tool" || !toolCallId) {
continue;
}
const decision = toolDecisionFromContent(record.content);
if (decision) {
decisionsByToolCallId.set(toolCallId, decision);
}
}
const decisionAwareMessages =
decisionsByToolCallId.size === 0
? stripped
: stripped.map((message) =>
applyToolResultDecisionSuffix(message, decisionsByToolCallId),
);
return dropStaleToolInteractionsBeforeLatestUser(decisionAwareMessages);
}
function createReplaySafeAgent(path: string, replaySafeToolNames: string[]) {
const agent = createAgent(path);
const replaySafeTools = new Set(replaySafeToolNames);
agent.use(
new FunctionMiddleware((input, next) => {
return new Observable<BaseEvent>((subscriber) => {
const toolCallIds = new Map<string, string>();
const sanitizedInput = {
...input,
messages: prepareReplaySafeMessages(input.messages),
};
const subscription = next.run(sanitizedInput).subscribe({
next(event) {
const e = event as BaseEvent & {
toolCallId?: string;
toolCallName?: string;
};
if (
(e.type === EventType.TOOL_CALL_START ||
e.type === EventType.TOOL_CALL_CHUNK) &&
e.toolCallName &&
replaySafeTools.has(e.toolCallName) &&
e.toolCallId
) {
const originalId = stripReplaySafeToolCallId(e.toolCallId);
const rewrittenId = makeReplaySafeToolCallId(
originalId,
input.runId,
);
toolCallIds.set(originalId, rewrittenId);
subscriber.next({ ...event, toolCallId: rewrittenId });
return;
}
if (e.toolCallId) {
const originalId = stripReplaySafeToolCallId(e.toolCallId);
const rewrittenId = toolCallIds.get(originalId);
if (rewrittenId) {
subscriber.next({ ...event, toolCallId: rewrittenId });
return;
}
}
subscriber.next(event);
},
error(error) {
subscriber.error(error);
},
complete() {
subscriber.complete();
},
});
return () => subscription.unsubscribe();
});
}),
);
return agent;
}
function createGenUiAgent() {
const agent = createAgent("/gen-ui-agent");
agent.use(
new FunctionMiddleware((input, next) => {
return new Observable<BaseEvent>((subscriber) => {
const setStepsToolCallIds = new Set<string>();
const argsByToolCallId = new Map<string, string>();
const emitStateSnapshotFromArgs = (toolCallId: string) => {
const args = argsByToolCallId.get(toolCallId);
if (!args) return;
try {
const parsed = JSON.parse(args);
if (!Array.isArray(parsed?.steps)) return;
subscriber.next({
type: EventType.STATE_SNAPSHOT,
snapshot: { steps: parsed.steps },
} as BaseEvent);
} catch {
// Args may arrive in chunks; wait until the buffered JSON is whole.
}
};
const subscription = next.run(input).subscribe({
next(event) {
if (
event.type === EventType.TOOL_CALL_START &&
(event as { toolCallName?: string }).toolCallName === "set_steps"
) {
const toolCallId = (event as { toolCallId?: string }).toolCallId;
if (toolCallId) setStepsToolCallIds.add(toolCallId);
return;
}
if (event.type === EventType.TOOL_CALL_ARGS) {
const toolCallId = (event as { toolCallId?: string }).toolCallId;
if (toolCallId && setStepsToolCallIds.has(toolCallId)) {
const delta = String((event as { delta?: string }).delta ?? "");
argsByToolCallId.set(
toolCallId,
`${argsByToolCallId.get(toolCallId) ?? ""}${delta}`,
);
emitStateSnapshotFromArgs(toolCallId);
return;
}
}
if (
event.type === EventType.TOOL_CALL_END ||
event.type === EventType.TOOL_CALL_RESULT
) {
const toolCallId = (event as { toolCallId?: string }).toolCallId;
if (toolCallId && setStepsToolCallIds.has(toolCallId)) {
if (event.type === EventType.TOOL_CALL_RESULT) {
setStepsToolCallIds.delete(toolCallId);
argsByToolCallId.delete(toolCallId);
}
return;
}
}
subscriber.next(event);
},
error(error) {
subscriber.error(error);
},
complete() {
subscriber.complete();
},
});
return () => subscription.unsubscribe();
});
}),
);
return agent;
}
function createReadonlyContextAgent() {
const agent = createAgent("/readonly-state-agent-context");
agent.use(
new FunctionMiddleware((input, next) => {
const contextMessage = buildContextSystemMessage(
(input as { context?: unknown }).context,
);
if (!contextMessage) {
return next.run(input);
}
return next.run({
...input,
messages: [
{
id: `${input.runId ?? crypto.randomUUID()}-app-context`,
role: "system",
content: contextMessage,
},
...(input.messages ?? []),
],
});
}),
);
return agent;
}
function createSharedStateReadWriteAgent() {
const agent = createAgent("/shared-state-read-write");
agent.use(
new FunctionMiddleware((input, next) => {
return next.run({
...input,
messages: [
{
id: `${input.runId ?? crypto.randomUUID()}-shared-state`,
role: "system",
content: buildSharedStateReadWriteSystemMessage(
(input as { state?: unknown }).state,
),
},
...(input.messages ?? []),
],
});
}),
);
return agent;
}
// Register the same agent under all names used by demo pages.
const agentNames = [
"agentic_chat",
"human_in_the_loop",
"tool-rendering",
"shared-state-read",
"prebuilt-sidebar",
"prebuilt-popup",
"chat-slots",
"chat-customization-css",
"headless-simple",
"frontend-tools",
"frontend-tools-async",
// Aliases for ADK/LGP-style underscore names (frontend pages use these).
"frontend_tools",
"frontend_tools_async",
];
// Agent names routed to the interrupt-adapted scheduling backend. Both
// gen-ui-interrupt and interrupt-headless share the same MS Agent Framework
// scheduling agent; only the frontend UX differs (inline in chat vs. external
// popup driven from a button grid).
const interruptAgentNames = ["gen-ui-interrupt", "interrupt-headless"];
const agents: Record<string, AbstractAgent> = {};
for (const name of agentNames) {
agents[name] = createAgent();
}
// human_in_the_loop wraps the catch-all "/" agent with the replay-safe
// middleware so the human_in_the_loop demo's `generate_task_steps` tool
// calls get the run-scoped suffix on the wire AND the suffix is stripped
// on follow-up turns so fixture matchers keyed on the deterministic
// toolCallId continue to fire.
agents["human_in_the_loop"] = createReplaySafeAgent("/", [
"generate_task_steps",
]);
agents["headless-complete"] = createAgent("/headless-complete");
// Interrupt-adapted demos — frontend-tool shim for LangGraph `interrupt()`.
// Both gen-ui-interrupt and interrupt-headless share the same scheduling agent;
// only the frontend UX differs (inline time-picker vs. external popup).
for (const name of interruptAgentNames) {
agents[name] = createReplaySafeAgent("/interrupt-adapted", [
"schedule_meeting",
]);
}
// In-App HITL -- async frontend-tool + app-level modal (outside chat).
// Dedicated hitl-in-app agent mounted at /hitl-in-app on the FastAPI
// backend; agent has tools=[] and relies on the frontend-provided
// `request_user_approval` tool injected by CopilotKit at request time.
// Wrapped in the replay-safe middleware so toolCallId-keyed fixtures
// continue to match across 2nd-turn requests after the run-id suffix.
agents["hitl-in-app"] = createReplaySafeAgent("/hitl-in-app", [
"request_user_approval",
]);
// In-Chat HITL -- frontend-defined `book_call` tool rendered inline in the
// chat via `useHumanInTheLoop`. Backend agent has tools=[] and routes to
// /hitl-in-chat on the FastAPI backend.
agents["hitl-in-chat"] = createReplaySafeAgent("/hitl-in-chat", ["book_call"]);
// Generative UI Agent — backend with `set_steps` tool + `steps` state
// schema mirrored from LGP's gen_ui_agent. The frontend renders a live
// progress card subscribed to `agent.state.steps`.
agents["gen-ui-agent"] = createGenUiAgent();
// Tool-Based Generative UI -- frontend registers `render_bar_chart` and
// `render_pie_chart` via `useComponent`; backend agent has tools=[] and a
// system prompt that picks the right chart type for the user's request.
agents["gen-ui-tool-based"] = createAgent("/gen-ui-tool-based");
// Shared State (Streaming) — `write_document` tool with `predict_state_config`
// that streams the tool's `document` arg into `state.document` per-token.
// See `src/agents/shared_state_streaming.py`.
agents["shared-state-streaming"] = createAgent("/shared-state-streaming");
// Readonly state via `useAgentContext` — minimal agent, no tools, reads
// frontend-provided context entries on every turn.
agents["readonly-state-agent-context"] = createReadonlyContextAgent();
// Shared State (Read + Write) — bidirectional state via state_schema +
// state_update. Backend exposes a dedicated agent at /shared-state-read-write
// with `preferences` + `notes` slots; UI writes preferences via setState,
// agent writes notes via the `set_notes` tool.
agents["shared-state-read-write"] = createSharedStateReadWriteAgent();
// Sub-Agents — supervisor agent at /subagents that delegates to research /
// writing / critique sub-agents and surfaces a live `delegations` log to the
// UI via shared state.
agents["subagents"] = createAgent("/subagents");
agents["default"] = createAgent();
// Tool-rendering demos — share the dedicated reasoning-chain agent
// mounted at /tool-rendering-reasoning-chain on the Python backend. All
// three cells call the same agent; they differ only in how the frontend
// renders tool calls.
// Reasoning cells (`reasoning-default` + `reasoning-custom`) share a
// dedicated backend mounted at `/reasoning` that uses the OpenAI Responses
// API (gpt-5/o-series) — the only chat client that emits AG-UI
// `REASONING_MESSAGE_*` events. See `src/agents/reasoning_agent.py`.
agents["reasoning-default"] = createAgent("/reasoning");
agents["reasoning-custom"] = createAgent("/reasoning");
// Tool-rendering demos — the plain `tool-rendering` cell and the two
// catchall variants share a non-reasoning backend (mounted at
// `/tool-rendering`). The reasoning-chain cell has its own dedicated
// backend (mounted at `/tool-rendering-reasoning-chain`) that routes
// through OpenAI's Responses API for reasoning streaming; mixing
// reasoning blocks into the catchall renderers breaks the
// default-catchall cell's spec.
agents["tool-rendering"] = createAgent("/tool-rendering");
agents["tool-rendering-default-catchall"] = createAgent("/tool-rendering");
agents["tool-rendering-custom-catchall"] = createAgent("/tool-rendering");
agents["tool-rendering-reasoning-chain"] = createAgent(
"/tool-rendering-reasoning-chain",
);
console.log(
`[copilotkit/route] Registered ${Object.keys(agents).length} agent names: ${Object.keys(agents).join(", ")}`,
);
export const POST = async (req: NextRequest) => {
const url = req.url;
const contentType = req.headers.get("content-type");
if (ROUTE_DEBUG) {
console.log(
`[copilotkit/route] POST ${url} (content-type: ${contentType})`,
);
}
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in source, pending release
agents,
}),
});
const response = await handleRequest(req);
if (!response.ok) {
console.log(`[copilotkit/route] Response status: ${response.status}`);
} else if (ROUTE_DEBUG) {
console.log(`[copilotkit/route] Response status: ${response.status}`);
}
return response;
} catch (error: unknown) {
const err = error as Error;
console.error(`[copilotkit/route] ERROR: ${err.message}`);
console.error(`[copilotkit/route] Stack: ${err.stack}`);
return NextResponse.json(
{ error: err.message, stack: err.stack },
{ status: 500 },
);
}
};
export const GET = async () => {
if (ROUTE_DEBUG) {
console.log("[copilotkit/route] GET /api/copilotkit (health probe)");
}
let agentStatus = "unknown";
try {
const res = await fetch(`${AGENT_URL}/health`, {
signal: AbortSignal.timeout(3000),
});
agentStatus = res.ok ? "reachable" : `error (${res.status})`;
} catch (e: unknown) {
agentStatus = `unreachable (${(e as Error).message})`;
}
return NextResponse.json({
status: "ok",
agent_url: AGENT_URL,
agent_status: agentStatus,
env: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY ? "set" : "NOT SET",
NODE_ENV: process.env.NODE_ENV,
},
});
};
@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
// Token-gated: SHOWCASE_DEBUG_TOKEN must be set in env and matched
const token =
req.headers.get("x-debug-token") || req.nextUrl.searchParams.get("token");
const expectedToken = process.env.SHOWCASE_DEBUG_TOKEN;
if (!expectedToken || !token || token !== expectedToken) {
return NextResponse.json({ error: "unauthorized" }, { status: 403 });
}
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// Agent connectivity
let agentStatus = "unknown";
let agentDetail = "";
try {
const res = await fetch(`${AGENT_URL}/health`, {
signal: AbortSignal.timeout(3000),
});
agentStatus = res.ok ? "ok" : "error";
agentDetail = `HTTP ${res.status}`;
} catch (e: unknown) {
agentStatus = "down";
agentDetail = (e as Error).message;
}
const uptime = process.uptime();
const mem = process.memoryUsage();
return NextResponse.json({
integration: "ms-agent-python",
uptime: `${Math.floor(uptime / 60)}m ${Math.floor(uptime % 60)}s`,
agent: { url: AGENT_URL, status: agentStatus, detail: agentDetail },
memory: {
rss: `${Math.round(mem.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(mem.heapUsed / 1024 / 1024)}MB`,
},
env: {
NODE_ENV: process.env.NODE_ENV,
OPENAI_API_KEY: process.env.OPENAI_API_KEY ? "set" : "NOT SET",
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ? "set" : "NOT SET",
},
nodeVersion: process.version,
});
}
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
integration: "ms-agent-python",
timestamp: new Date().toISOString(),
});
}
@@ -0,0 +1,121 @@
import { NextResponse } from "next/server";
const INTEGRATION_SLUG = "ms-agent-python";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export async function GET() {
const start = Date.now();
// Hit our own /api/copilotkit endpoint — tests the full deployed stack
const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL ||
`http://localhost:${process.env.PORT || 3000}`;
try {
const res = await fetch(`${baseUrl}/api/copilotkit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
method: "agent/run",
params: { agentId: "agentic_chat" },
body: {
threadId: `smoke-${Date.now()}`,
runId: `smoke-run-${Date.now()}`,
state: {},
messages: [
{
id: `smoke-msg-${Date.now()}`,
role: "user",
content: "Respond with exactly: OK",
},
],
tools: [],
context: [],
forwardedProps: {},
},
}),
signal: AbortSignal.timeout(45000),
});
const latency = Date.now() - start;
if (!res.ok) {
const errBody = await res.text().catch(() => "");
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage: "runtime_response",
error: `Runtime returned ${res.status}: ${errBody.slice(0, 200)}`,
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
// TTFB: read first chunk only to confirm SSE stream started, then cancel
const reader = res.body?.getReader();
if (!reader) {
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage: "response_empty",
error: "Runtime returned no readable body",
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
const { value, done } = await reader.read();
reader.cancel();
if (done || !value || value.length === 0) {
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage: "response_empty",
error: "Runtime returned empty response body",
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
return NextResponse.json({
status: "ok",
integration: INTEGRATION_SLUG,
latency_ms: latency,
timestamp: new Date().toISOString(),
});
} catch (e: unknown) {
const err = e instanceof Error ? e : new Error(String(e));
const latency = Date.now() - start;
let stage = "unknown";
if (err.name === "AbortError" || err.message.includes("timeout"))
stage = "timeout";
else if (
err.message.includes("fetch") ||
err.message.includes("ECONNREFUSED")
)
stage = "agent_unreachable";
else stage = "pipeline_error";
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage,
error: err.message,
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
}
@@ -0,0 +1,55 @@
// Shared fallback time-slot generator for the interrupt demos
// (`gen-ui-interrupt`, `interrupt-headless`). The interrupt backend
// (`src/agents/interrupt_agent.py`) supplies its own candidate slots
// inside the interrupt payload — these fallbacks only run if the
// payload arrives without them. Generating relative to `Date.now()`
// keeps the fallback from rotting, which previously had hardcoded
// dates that decayed within a week of being authored.
export interface TimeSlot {
label: string;
iso: string;
}
function atLocal(date: Date, hour: number, minute = 0): Date {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
hour,
minute,
0,
0,
);
}
function nextMonday(from: Date): Date {
// `getDay()` is 0=Sun, 1=Mon, ..., 6=Sat. We want the next Monday
// that's at LEAST 2 days away — otherwise "Monday" would collide
// with "Tomorrow" on Sunday (offset would be 1) or with itself on
// Monday (offset would be 0). Mirrors interrupt_agent.py.
const day = from.getDay();
let offset = (1 - day + 7) % 7;
if (offset <= 1) offset += 7;
const next = new Date(from);
next.setDate(from.getDate() + offset);
return next;
}
export function generateFallbackSlots(now: Date = new Date()): TimeSlot[] {
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
const monday = nextMonday(now);
const candidates: Array<[string, Date]> = [
["Tomorrow 10:00 AM", atLocal(tomorrow, 10)],
["Tomorrow 2:00 PM", atLocal(tomorrow, 14)],
["Monday 9:00 AM", atLocal(monday, 9)],
["Monday 3:30 PM", atLocal(monday, 15, 30)],
];
return candidates.map(([label, date]) => ({
label,
iso: date.toISOString(),
}));
}
@@ -0,0 +1,12 @@
// Coerces a tool-call result into a typed object. Tool results arrive
// as strings when the agent emits JSON or as already-parsed objects
// when the runtime decoded them upstream — this helper handles both
// shapes and returns `{}` if the result is missing or unparseable.
export function parseJsonResult<T>(result: unknown): T {
if (!result) return {} as T;
try {
return (typeof result === "string" ? JSON.parse(result) : result) as T;
} catch {
return {} as T;
}
}
@@ -0,0 +1,21 @@
// Helper for the CopilotChat slot overrides. The slot prop types in
// `@copilotkit/react-core` are nominally typed against the *exact*
// default component identity, but a custom wrapper that returns a
// structurally compatible ReactElement is functionally a drop-in. This
// helper names that fact and centralizes the type assertion in one
// place — readers see `makeSlotOverride` and know it's about the slot
// contract, not arbitrary type-system gymnastics. Once the slot prop
// types accept structural compatibility, this helper can be deleted
// and the casts will resolve automatically.
import type { ComponentType } from "react";
// `any` on the input is intentional: the helper's purpose is to accept
// any component shape and assert it as the slot's expected type. A
// stricter constraint would defeat the whole point.
export function makeSlotOverride<TDefault>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: ComponentType<any>,
): TDefault {
return component as unknown as TDefault;
}
@@ -0,0 +1,31 @@
import * as React from "react";
/**
* ShadCN-style Badge primitive (inline-cloned for this demo).
* Plain Tailwind classes, no `cn()`/`cva` helpers.
*/
type Variant = "default" | "secondary" | "outline" | "success";
const variantClasses: Record<Variant, string> = {
default: "border-transparent bg-neutral-900 text-neutral-50",
secondary: "border-transparent bg-neutral-100 text-neutral-900",
outline: "border-neutral-200 text-neutral-700 bg-white",
success: "border-transparent bg-emerald-100 text-emerald-700",
};
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: Variant;
}
export function Badge({
className = "",
variant = "default",
...props
}: BadgeProps) {
return (
<div
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium tracking-wide ${variantClasses[variant]} ${className}`}
{...props}
/>
);
}
@@ -0,0 +1,48 @@
import * as React from "react";
/**
* ShadCN-style Button primitive (inline-cloned for this demo).
* Plain Tailwind classes, no `cn()`/`cva` helpers.
*/
type Variant = "default" | "outline" | "secondary" | "ghost" | "success";
type Size = "default" | "sm" | "lg";
const baseClasses =
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60";
const variantClasses: Record<Variant, string> = {
default: "bg-neutral-900 text-neutral-50 shadow-sm hover:bg-neutral-800",
outline:
"border border-neutral-200 bg-white text-neutral-900 shadow-sm hover:bg-neutral-100",
secondary: "bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-200",
ghost: "hover:bg-neutral-100 hover:text-neutral-900",
success:
"bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm hover:bg-emerald-50",
};
const sizeClasses: Record<Size, string> = {
default: "h-10 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-11 rounded-md px-6",
};
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className = "", variant = "default", size = "default", ...props },
ref,
) => {
return (
<button
ref={ref}
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
/>
);
},
);
Button.displayName = "Button";
@@ -0,0 +1,61 @@
import * as React from "react";
/**
* ShadCN-style Card primitive (inline-cloned for this demo).
* Plain Tailwind classes, no `cn()`/`cva` helpers.
*/
export const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div
ref={ref}
className={`rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow-sm ${className}`}
{...props}
/>
));
Card.displayName = "Card";
export const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div
ref={ref}
className={`flex flex-col space-y-1.5 p-5 pb-3 ${className}`}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
export const CardTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className = "", ...props }, ref) => (
<h3
ref={ref}
className={`text-base font-semibold leading-none tracking-tight text-neutral-900 ${className}`}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
export const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div ref={ref} className={`p-5 pt-0 ${className}`} {...props} />
));
CardContent.displayName = "CardContent";
export const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div
ref={ref}
className={`flex items-center p-5 pt-0 ${className}`}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
@@ -0,0 +1,26 @@
import * as React from "react";
/**
* ShadCN-style Separator primitive (inline-cloned for this demo).
* Plain Tailwind classes; uses a div instead of Radix to keep dependencies minimal.
*/
export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: "horizontal" | "vertical";
}
export function Separator({
className = "",
orientation = "horizontal",
...props
}: SeparatorProps) {
const orientationClasses =
orientation === "horizontal" ? "h-px w-full" : "h-full w-px";
return (
<div
role="separator"
aria-orientation={orientation}
className={`shrink-0 bg-neutral-200 ${orientationClasses} ${className}`}
{...props}
/>
);
}
@@ -0,0 +1,23 @@
"use client";
/**
* Fixed A2UI catalog — wires definitions to renderers.
*
* `includeBasicCatalog: true` merges CopilotKit's built-in components
* (Card, Column, Row, Text, Button, Divider, …) into this catalog, so
* the agent's fixed schema (src/agents/a2ui_schemas/flight_schema.json) can
* compose custom and basic components interchangeably.
*/
// @region[catalog-creation]
import { createCatalog } from "@copilotkit/a2ui-renderer";
import { definitions } from "./definitions";
import { renderers } from "./renderers";
export const CATALOG_ID = "copilotkit://flight-fixed-catalog";
export const catalog = createCatalog(definitions, renderers, {
catalogId: CATALOG_ID,
includeBasicCatalog: true,
});
// @endregion[catalog-creation]
@@ -0,0 +1,107 @@
/**
* A2UI catalog DEFINITIONS — platform-agnostic.
*
* Each entry declares a component name + its Zod props schema. The basic
* catalog (Card, Column, Row, Text, Button, …) ships with CopilotKit and
* is mixed in via `createCatalog(..., { includeBasicCatalog: true })`, so
* we only declare the project-specific additions and the visual overrides
* here. (Custom entries with the same name as a basic component override
* the basic one — Catalog dedupes by `comp.name`, last-write-wins.)
*
* IMPORTANT — path bindings: fields that can be bound to a data-model path
* (e.g. `{ path: "/origin" }` in the fixed schema JSON) must declare their
* Zod type as a union of `z.string()` and `z.object({ path: z.string() })`.
* The A2UI `GenericBinder` uses this union to detect the field as dynamic
* and resolve the path against the current data model at render time. Using
* plain `z.string()` causes the raw `{ path }` object to reach the
* renderer, which React then throws on (error #31 "object with keys {path}").
* This matches the canonical catalog's `DynString` helper:
* examples/integrations/langgraph-python/src/app/declarative-generative-ui/definitions.ts
*/
// @region[definitions-types]
import { z } from "zod";
import type { CatalogDefinitions } from "@copilotkit/a2ui-renderer";
/**
* Dynamic string: literal OR a data-model path binding. The GenericBinder
* resolves path bindings to the actual value at render time.
*/
const DynString = z.union([z.string(), z.object({ path: z.string() })]);
export const definitions = {
/**
* Card override: gives the outer flight-card container a ShadCN look
* (rounded-xl, neutral-200 border, soft shadow). The basic catalog's
* Card uses inline styles; overriding here lets the demo's renderer
* adopt the demo's Tailwind aesthetic without touching the schema JSON.
*/
Card: {
description: "A container card with a single child.",
props: z.object({
child: z.string(),
}),
},
Title: {
description: "A prominent heading for the flight card.",
props: z.object({
text: DynString,
}),
},
Airport: {
description: "A 3-letter airport code, displayed large.",
props: z.object({
code: DynString,
}),
},
Arrow: {
description: "A right-pointing arrow used between airports.",
props: z.object({}),
},
AirlineBadge: {
description: "A pill-styled airline name tag.",
props: z.object({
name: DynString,
}),
},
PriceTag: {
description: "A stylized price display (e.g. '$289').",
props: z.object({
amount: DynString,
}),
},
/**
* Button override: swaps in an ActionButton renderer that tracks
* its own `done` state so clicking "Book flight" visually updates to
* a "Booked ✓" confirmation. The basic catalog's Button is stateless,
* so without this override the click fires the action but the button
* looks unchanged. Mirrors the pattern in beautiful-chat
* (src/app/demos/beautiful-chat/declarative-generative-ui/renderers.tsx).
*/
Button: {
description:
"An interactive button with an action event. Use 'child' with a Text component ID for the label. After click, the button shows a confirmation state.",
props: z.object({
child: z
.string()
.describe(
"The ID of the child component (e.g. a Text component for the label).",
),
variant: z.enum(["primary", "secondary", "ghost"]).optional(),
// Union with { event } so GenericBinder resolves this as ACTION → callable () => void.
action: z
.union([
z.object({
event: z.object({
name: z.string(),
context: z.record(z.any()).optional(),
}),
}),
z.null(),
])
.optional(),
}),
},
} satisfies CatalogDefinitions;
// @endregion[definitions-types]
export type Definitions = typeof definitions;
@@ -0,0 +1,110 @@
"use client";
/**
* A2UI catalog RENDERERS — React implementations for the custom components
* declared in `./definitions`. TypeScript enforces that the renderer map's
* keys and prop shapes match the definitions exactly.
*
* Visual style: ShadCN aesthetic (neutral palette, rounded-xl, subtle
* borders, clean typography). Tailwind utility classes only — no `cn()` /
* `cva` helpers, no shadcn CLI install. Inline-cloned primitives live in
* `../_components/`.
*/
import React from "react";
import type { CatalogRenderers } from "@copilotkit/a2ui-renderer";
import type { Definitions } from "./definitions";
import { Card } from "../_components/card";
import { Badge } from "../_components/badge";
import { Button as UIButton } from "../_components/button";
import { Separator } from "../_components/separator";
// `DynString` props are typed as `string | { path }` (see definitions.ts), but
// the A2UI binder resolves path bindings before render — renderers only ever
// see resolved strings. One shared helper keeps that narrowing in one place.
const s = (v: unknown): string => (typeof v === "string" ? v : "");
// @region[renderers-tsx]
export const renderers: CatalogRenderers<Definitions> = {
/**
* Card override: ShadCN-style outer container. The basic catalog's Card
* uses inline styles; overriding here keeps the demo's tailwind aesthetic.
* The flight schema renders Card > Column > [Title, Row, …]; the inner
* Column adds the vertical spacing.
*/
Card: ({ props, children }) => (
<Card className="w-full max-w-md p-5" data-testid="a2ui-fixed-card">
{props.child ? children(props.child) : null}
</Card>
),
Title: ({ props }) => (
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-[11px] font-medium uppercase tracking-[0.14em] text-neutral-500">
Itinerary
</p>
<h3 className="text-base font-semibold leading-none tracking-tight text-neutral-900">
{s(props.text)}
</h3>
</div>
<Badge variant="outline" className="font-mono">
1-stop · economy
</Badge>
</div>
),
Airport: ({ props }) => (
<div className="flex flex-col items-center">
<span className="font-mono text-2xl font-semibold tracking-wider text-neutral-900">
{s(props.code)}
</span>
</div>
),
Arrow: () => (
<div className="flex flex-1 items-center px-3">
<Separator className="flex-1 bg-neutral-200" />
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mx-1 text-neutral-400"
aria-hidden
>
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="12 5 19 12 12 19" />
</svg>
<Separator className="flex-1 bg-neutral-200" />
</div>
),
AirlineBadge: ({ props }) => (
<Badge variant="secondary" className="uppercase tracking-[0.08em]">
{s(props.name)}
</Badge>
),
PriceTag: ({ props }) => (
<div className="flex items-baseline gap-1">
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-neutral-500">
Total
</span>
<span className="font-mono text-base font-semibold text-neutral-900">
{s(props.amount)}
</span>
</div>
),
/**
* Button override: this is a pure-presentation demo, so the button just
* renders its label. The schema declares an `action` for visual fidelity,
* but the click handler is inert until the Python SDK exposes
* `action_handlers=` on `a2ui.render` (see `src/agents/a2ui_fixed.py`).
*/
Button: ({ props, children }) => (
<UIButton className="w-full">
{props.child ? children(props.child) : null}
</UIButton>
),
};
// @endregion[renderers-tsx]
@@ -0,0 +1,11 @@
"use client";
import { CopilotChat } from "@copilotkit/react-core/v2";
import { useA2UIFixedSchemaSuggestions } from "./suggestions";
export function Chat() {
useA2UIFixedSchemaSuggestions();
return (
<CopilotChat agentId="a2ui-fixed-schema" className="h-full rounded-2xl" />
);
}
@@ -0,0 +1,41 @@
"use client";
/**
* Declarative Generative UI — A2UI Fixed Schema demo.
*
* In the fixed-schema flavor of A2UI, the component tree (schema) lives on
* the frontend and the agent only streams *data* into the data model. The
* flight card is ASSEMBLED from small sub-components in
* `src/agents/a2ui_schemas/flight_schema.json` (Card > Column > [Title, Row, …]).
*
* - Definitions (zod schemas): `./a2ui/definitions.ts`
* - Renderers (React): `./a2ui/renderers.tsx`
* - Catalog wiring: `./a2ui/catalog.ts` (includes the basic catalog)
* - Agent: `src/agents/a2ui_fixed.py` (emits an `a2ui_operations` container)
*
* Reference:
* https://docs.copilotkit.ai/integrations/langgraph/generative-ui/a2ui/fixed-schema
*/
import React from "react";
import { CopilotKit } from "@copilotkit/react-core/v2";
import { catalog } from "./a2ui/catalog";
import { Chat } from "./chat";
export default function A2UIFixedSchemaDemo() {
return (
// `a2ui.catalog` wires the fixed catalog into the A2UI activity renderer.
<CopilotKit
runtimeUrl="/api/copilotkit-a2ui-fixed-schema"
agent="a2ui-fixed-schema"
a2ui={{ catalog: catalog }}
>
<div className="flex justify-center items-center h-screen w-full bg-neutral-50">
<div className="h-full w-full max-w-4xl border-x border-neutral-200 bg-white">
<Chat />
</div>
</div>
</CopilotKit>
);
}
@@ -0,0 +1,13 @@
import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
export function useA2UIFixedSchemaSuggestions() {
useConfigureSuggestions({
suggestions: [
{
title: "Find SFO → JFK",
message: "Find me a flight from SFO to JFK on United for $289.",
},
],
available: "always",
});
}
@@ -0,0 +1,93 @@
"use client";
import type { ChangeEvent } from "react";
import {
EXPERTISE_OPTIONS,
RESPONSE_LENGTH_OPTIONS,
TONE_OPTIONS,
} from "./config-types";
import type {
AgentConfig,
Expertise,
ResponseLength,
Tone,
} from "./config-types";
interface ConfigCardProps {
config: AgentConfig;
onToneChange: (tone: Tone) => void;
onExpertiseChange: (expertise: Expertise) => void;
onResponseLengthChange: (length: ResponseLength) => void;
}
export function ConfigCard({
config,
onToneChange,
onExpertiseChange,
onResponseLengthChange,
}: ConfigCardProps) {
return (
<div
data-testid="agent-config-card"
className="flex flex-col gap-2 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-4 text-sm"
>
<h2 className="text-sm font-semibold">Agent Config</h2>
<p className="text-xs text-[var(--text-muted)]">
Change these and send a message to see the agent adapt.
</p>
<div className="flex flex-wrap gap-3">
<label className="flex flex-col gap-1">
<span className="text-xs font-medium">Tone</span>
<select
data-testid="agent-config-tone-select"
value={config.tone}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
onToneChange(e.target.value as Tone)
}
className="rounded border border-[var(--border)] bg-[var(--bg-muted)] px-2 py-1 text-sm"
>
{TONE_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs font-medium">Expertise</span>
<select
data-testid="agent-config-expertise-select"
value={config.expertise}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
onExpertiseChange(e.target.value as Expertise)
}
className="rounded border border-[var(--border)] bg-[var(--bg-muted)] px-2 py-1 text-sm"
>
{EXPERTISE_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs font-medium">Response length</span>
<select
data-testid="agent-config-length-select"
value={config.responseLength}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
onResponseLengthChange(e.target.value as ResponseLength)
}
className="rounded border border-[var(--border)] bg-[var(--bg-muted)] px-2 py-1 text-sm"
>
{RESPONSE_LENGTH_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
</div>
</div>
);
}
@@ -0,0 +1,25 @@
"use client";
/**
* Publishes the current agent-config toggles to the agent runtime via
* `useAgentContext`. Lives inside the `<CopilotKit>` provider so the
* context store is reachable. The middleware on the Python side reads
* this entry off the agent's runtime context on every turn and routes
* it into the model's prompt.
*/
import { useAgentContext } from "@copilotkit/react-core/v2";
import type { AgentConfig } from "./config-types";
export function ConfigContextRelay({ config }: { config: AgentConfig }) {
useAgentContext({
description:
"Agent response preferences. Apply tone, expertise level, and response length to every reply.",
value: {
tone: config.tone,
expertise: config.expertise,
responseLength: config.responseLength,
},
});
return null;
}
@@ -0,0 +1,26 @@
export type Tone = "professional" | "casual" | "enthusiastic";
export type Expertise = "beginner" | "intermediate" | "expert";
export type ResponseLength = "concise" | "detailed";
export interface AgentConfig {
tone: Tone;
expertise: Expertise;
responseLength: ResponseLength;
}
export const DEFAULT_AGENT_CONFIG: AgentConfig = {
tone: "professional",
expertise: "intermediate",
responseLength: "concise",
};
export const TONE_OPTIONS: Tone[] = ["professional", "casual", "enthusiastic"];
export const EXPERTISE_OPTIONS: Expertise[] = [
"beginner",
"intermediate",
"expert",
];
export const RESPONSE_LENGTH_OPTIONS: ResponseLength[] = [
"concise",
"detailed",
];
@@ -0,0 +1,38 @@
"use client";
import React from "react";
import { CopilotChat } from "@copilotkit/react-core/v2";
import { ConfigCard } from "./config-card";
import type { AgentConfig } from "./config-types";
interface DemoLayoutProps {
config: AgentConfig;
onToneChange: (tone: AgentConfig["tone"]) => void;
onExpertiseChange: (expertise: AgentConfig["expertise"]) => void;
onResponseLengthChange: (length: AgentConfig["responseLength"]) => void;
}
export function DemoLayout({
config,
onToneChange,
onExpertiseChange,
onResponseLengthChange,
}: DemoLayoutProps) {
return (
<div className="flex h-screen flex-col gap-3 p-6">
<ConfigCard
config={config}
onToneChange={onToneChange}
onExpertiseChange={onExpertiseChange}
onResponseLengthChange={onResponseLengthChange}
/>
<div className="flex-1 overflow-hidden rounded-md border border-[var(--border)]">
<CopilotChat
agentId="agent-config-demo"
className="h-full rounded-md"
/>
</div>
</div>
);
}
@@ -0,0 +1,40 @@
"use client";
/**
* Agent Config Object - typed config knobs (tone / expertise / responseLength)
* forwarded from the provider into the agent so its behavior changes per turn.
*
* Wiring: the toggles live in `useAgentConfig`. Each render publishes the
* resolved config through both CopilotKit `properties` and `useAgentContext`.
* The Microsoft Agent Framework route reads `properties` as AG-UI
* `forwardedProps`; the context relay keeps the demo aligned with the
* LangGraph Python v2 pattern.
*/
import { CopilotKit } from "@copilotkit/react-core/v2";
import { DemoLayout } from "./demo-layout";
import { ConfigContextRelay } from "./config-context-relay";
import { useAgentConfig } from "./use-agent-config";
export default function AgentConfigDemoPage() {
const { config, setTone, setExpertise, setResponseLength } = useAgentConfig();
return (
// @region[provider-setup]
<CopilotKit
runtimeUrl="/api/copilotkit-agent-config"
agent="agent-config-demo"
properties={config}
>
<ConfigContextRelay config={config} />
<DemoLayout
config={config}
onToneChange={setTone}
onExpertiseChange={setExpertise}
onResponseLengthChange={setResponseLength}
/>
</CopilotKit>
// @endregion[provider-setup]
);
}
@@ -0,0 +1,39 @@
"use client";
import { useCallback, useState } from "react";
import {
type AgentConfig,
DEFAULT_AGENT_CONFIG,
type Expertise,
type ResponseLength,
type Tone,
} from "./config-types";
export interface UseAgentConfigHandle {
config: AgentConfig;
setTone: (tone: Tone) => void;
setExpertise: (expertise: Expertise) => void;
setResponseLength: (length: ResponseLength) => void;
reset: () => void;
}
export function useAgentConfig(): UseAgentConfigHandle {
const [config, setConfig] = useState<AgentConfig>(DEFAULT_AGENT_CONFIG);
const setTone = useCallback(
(tone: Tone) => setConfig((prev) => ({ ...prev, tone })),
[],
);
const setExpertise = useCallback(
(expertise: Expertise) => setConfig((prev) => ({ ...prev, expertise })),
[],
);
const setResponseLength = useCallback(
(responseLength: ResponseLength) =>
setConfig((prev) => ({ ...prev, responseLength })),
[],
);
const reset = useCallback(() => setConfig(DEFAULT_AGENT_CONFIG), []);
return { config, setTone, setExpertise, setResponseLength, reset };
}
@@ -0,0 +1,28 @@
# Agentic Chat
## What This Demo Shows
The simplest CopilotKit surface: a plain agentic chat backed by a LangGraph (Python) agent.
- **Natural Conversation**: Chat with your Copilot in a familiar chat interface
- **Streaming Responses**: Assistant messages stream in token-by-token via AG-UI
- **Suggestion Chips**: A starter suggestion is rendered as a quick-action chip
## How to Interact
Click the suggestion chip, or type your own prompt. For example:
- "Write a short sonnet about AI"
- "Explain the difference between an LLM and an agent"
- "Give me three ideas for a weekend project"
## Technical Details
**Provider**`CopilotKit` wires the page to the runtime:
- `runtimeUrl="/api/copilotkit"` points at the Next.js route that proxies to the agent
- `agent="agentic_chat"` selects the LangGraph agent defined in `langgraph.json`
**Chat surface**`CopilotChat` renders the full chat UI with input, message list, and streaming.
**Suggestions**`useConfigureSuggestions` registers a static suggestion that appears as a clickable chip below the chat input.

Some files were not shown because too many files have changed in this diff Show More