chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
**/node_modules
|
||||
.next/
|
||||
.env.local
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.venv/
|
||||
dist/
|
||||
playwright-report/
|
||||
test-results/
|
||||
tests/
|
||||
qa/
|
||||
.git/
|
||||
.gitignore
|
||||
README.md
|
||||
render.yaml
|
||||
playwright.config.ts
|
||||
**/vitest.config.ts
|
||||
**/test-setup.ts
|
||||
**/*.test.ts
|
||||
**/*.test.tsx
|
||||
**/*.spec.ts
|
||||
**/*.spec.tsx
|
||||
@@ -0,0 +1,11 @@
|
||||
# API Keys
|
||||
OPENAI_API_KEY=replace-with-your-key
|
||||
|
||||
# LangGraph server URL (langgraph dev runs on port 8123 by default)
|
||||
LANGGRAPH_DEPLOYMENT_URL=http://localhost:8123
|
||||
|
||||
# LangSmith (optional, for tracing)
|
||||
LANGSMITH_API_KEY=
|
||||
|
||||
# Showcase
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
@@ -0,0 +1,8 @@
|
||||
# Sample multimodal demo binaries (PNG + PDF) must stay as regular
|
||||
# binaries — not LFS pointers — so deploy environments without `git lfs pull`
|
||||
# (Railway, in particular) serve the actual files. LFS tracking for other
|
||||
# PNG/PDFs under this package inherits from the repo-root .gitattributes.
|
||||
public/demo-files/sample.png -filter -diff -merge binary
|
||||
public/demo-files/sample.pdf -filter -diff -merge binary
|
||||
# Voice demo sample audio follows the same convention.
|
||||
public/demo-audio/sample.wav -filter -diff -merge binary
|
||||
@@ -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,91 @@
|
||||
# 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 . .
|
||||
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 \
|
||||
&& chown app:app /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 + config
|
||||
COPY --chown=app:app langgraph.json ./
|
||||
COPY --chown=app:app src/agents/ ./src/agents/
|
||||
|
||||
# Manifest is read at runtime by `src/app/demos/layout.tsx` (`generateMetadata`
|
||||
# calls `headers()`, which opts every `/demos/*` route into dynamic rendering,
|
||||
# so Next can't bake the demo titles into the build). Without this copy every
|
||||
# demo page throws "An error occurred in the Server Components render"
|
||||
# (ENOENT on /app/manifest.yaml) — the home page is unaffected because it has
|
||||
# no dynamic APIs and is statically prerendered at build time.
|
||||
COPY --chown=app:app manifest.yaml ./
|
||||
|
||||
# Shared Python tools (symlinked in source, copied into build context 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.
|
||||
ENV PORT=10000
|
||||
CMD ["./entrypoint.sh"]
|
||||
@@ -0,0 +1 @@
|
||||
../_shared
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"framework": "langgraph-python",
|
||||
"features": {
|
||||
"beautiful-chat": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/prebuilt-components",
|
||||
"shell_docs_path": "/prebuilt-components"
|
||||
},
|
||||
"cli-start": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/quickstart",
|
||||
"shell_docs_path": "/quickstart"
|
||||
},
|
||||
"agentic-chat": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/prebuilt-components",
|
||||
"shell_docs_path": "/prebuilt-components"
|
||||
},
|
||||
"prebuilt-sidebar": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/prebuilt-components",
|
||||
"shell_docs_path": "/prebuilt-components"
|
||||
},
|
||||
"prebuilt-popup": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/prebuilt-components",
|
||||
"shell_docs_path": "/prebuilt-components"
|
||||
},
|
||||
"chat-slots": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/custom-look-and-feel/slots",
|
||||
"shell_docs_path": "/custom-look-and-feel/slots"
|
||||
},
|
||||
"chat-customization-css": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/custom-look-and-feel",
|
||||
"shell_docs_path": "/custom-look-and-feel/css"
|
||||
},
|
||||
"headless-simple": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/custom-look-and-feel/headless-ui",
|
||||
"shell_docs_path": "/custom-look-and-feel/headless-ui"
|
||||
},
|
||||
"headless-complete": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/custom-look-and-feel/headless-ui",
|
||||
"shell_docs_path": "/custom-look-and-feel/headless-ui"
|
||||
},
|
||||
"reasoning-custom": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/custom-look-and-feel/reasoning-messages",
|
||||
"shell_docs_path": "/custom-look-and-feel/reasoning-messages"
|
||||
},
|
||||
"reasoning-default-render": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/custom-look-and-feel/reasoning-messages",
|
||||
"shell_docs_path": "/custom-look-and-feel/reasoning-messages"
|
||||
},
|
||||
"frontend-tools": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/frontend-tools",
|
||||
"shell_docs_path": "/frontend-tools"
|
||||
},
|
||||
"frontend-tools-async": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/frontend-tools",
|
||||
"shell_docs_path": "/frontend-tools"
|
||||
},
|
||||
"gen-ui-tool-based": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/your-components/display-only",
|
||||
"shell_docs_path": "/generative-ui/your-components/display-only"
|
||||
},
|
||||
"hitl-in-chat": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/your-components/interactive",
|
||||
"shell_docs_path": "/generative-ui/your-components/interactive"
|
||||
},
|
||||
"hitl-in-app": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/frontend-tools",
|
||||
"shell_docs_path": "/frontend-tools"
|
||||
},
|
||||
"gen-ui-interrupt": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/your-components/interrupt-based",
|
||||
"shell_docs_path": "/human-in-the-loop/interrupt-flow"
|
||||
},
|
||||
"interrupt-headless": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/human-in-the-loop/interrupt-flow",
|
||||
"shell_docs_path": "/human-in-the-loop/headless"
|
||||
},
|
||||
"declarative-gen-ui": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/a2ui/dynamic-schema",
|
||||
"shell_docs_path": "/generative-ui/a2ui/dynamic-schema"
|
||||
},
|
||||
"a2ui-fixed-schema": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/a2ui/fixed-schema",
|
||||
"shell_docs_path": "/generative-ui/a2ui/fixed-schema"
|
||||
},
|
||||
"mcp-apps": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/mcp-apps",
|
||||
"shell_docs_path": "/generative-ui/mcp-apps"
|
||||
},
|
||||
"open-gen-ui": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/generative-ui/open-generative-ui",
|
||||
"shell_docs_path": "/generative-ui/open-generative-ui"
|
||||
},
|
||||
"open-gen-ui-advanced": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/generative-ui/open-generative-ui",
|
||||
"shell_docs_path": "/generative-ui/open-generative-ui"
|
||||
},
|
||||
"gen-ui-agent": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/state-rendering",
|
||||
"shell_docs_path": "/generative-ui/state-rendering"
|
||||
},
|
||||
"tool-rendering-default-catchall": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/tool-rendering",
|
||||
"shell_docs_path": "/generative-ui/tool-rendering"
|
||||
},
|
||||
"tool-rendering-custom-catchall": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/tool-rendering",
|
||||
"shell_docs_path": "/generative-ui/tool-rendering"
|
||||
},
|
||||
"tool-rendering": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/tool-rendering",
|
||||
"shell_docs_path": "/generative-ui/tool-rendering"
|
||||
},
|
||||
"tool-rendering-reasoning-chain": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/tool-rendering",
|
||||
"shell_docs_path": "/generative-ui/tool-rendering"
|
||||
},
|
||||
"shared-state-read-write": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/in-app-agent-write",
|
||||
"shell_docs_path": "/shared-state/in-app-agent-write"
|
||||
},
|
||||
"shared-state-streaming": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/predictive-state-updates",
|
||||
"shell_docs_path": "/shared-state/predictive-state-updates"
|
||||
},
|
||||
"readonly-state-agent-context": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/agent-app-context",
|
||||
"shell_docs_path": "/agent-app-context"
|
||||
},
|
||||
"subagents": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/multi-agent-flows",
|
||||
"shell_docs_path": "/multi-agent-flows"
|
||||
},
|
||||
"auth": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/langgraph/auth",
|
||||
"shell_docs_path": "/auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire CopilotKit middleware into your graph
|
||||
|
||||
Agent config flows from the UI into the agent via `useAgentContext` — the
|
||||
frontend publishes a typed object and `CopilotKitMiddleware` injects it
|
||||
into the model's prompt on every turn. Make sure the middleware is in
|
||||
your `create_agent` call.
|
||||
|
||||
<DemoCode file="src/agents/agent_config_agent.py" region="agent-config-setup" />
|
||||
|
||||
Read the resulting config inside your system prompt or a custom
|
||||
middleware — see `src/agents/agent_config_agent.py` for the full
|
||||
rulebook-driven shape used in the showcase.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,19 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire read-only agent context into your graph
|
||||
|
||||
`useAgentContext` publishes read-only UI values through
|
||||
`CopilotKitMiddleware`. Attach the middleware to the agent and describe
|
||||
how the model should use the frontend-supplied context in the system
|
||||
prompt.
|
||||
|
||||
<DemoCode file="src/agents/readonly_state_agent_context.py" region="agent-context-setup" />
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,16 @@
|
||||
First, add `CopilotKitMiddleware` to your `create_agent` call. The middleware
|
||||
is what makes every CopilotKit feature on the frontend — frontend tools,
|
||||
shared state, agent context, and generative UI components — visible to your
|
||||
LangGraph agent on every turn.
|
||||
|
||||
<DemoCode file="src/agents/frontend_tools.py" region="middleware" />
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="Install the SDK">
|
||||
If `copilotkit` isn't already in your project, add it so the import
|
||||
above resolves. Pick the package manager that matches your project:
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
@@ -0,0 +1,18 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire it into your graph
|
||||
|
||||
Import `CopilotKitMiddleware` and pass it to `create_agent`. This is the
|
||||
single line of CopilotKit-specific setup; everything else is standard
|
||||
LangGraph.
|
||||
|
||||
<DemoCode file="src/agents/frontend_tools.py" region="middleware" />
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,19 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire CopilotKit middleware into your graph
|
||||
|
||||
Frontend tools registered with `useFrontendTool` are forwarded to your
|
||||
agent at runtime. `CopilotKitMiddleware` is the bridge — drop it into
|
||||
`create_agent`'s `middleware` list and the LLM sees the forwarded tool
|
||||
definitions on every turn.
|
||||
|
||||
<DemoCode file="src/agents/frontend_tools.py" region="middleware" />
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,22 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire CopilotKit middleware into your graph
|
||||
|
||||
For `useHumanInTheLoop` tool-based HITL, the tool is defined entirely on
|
||||
the frontend and forwarded to the agent. `CopilotKitMiddleware` is what
|
||||
forwards it — drop it into your `create_agent` call.
|
||||
|
||||
<DemoCode file="src/agents/frontend_tools.py" region="middleware" />
|
||||
|
||||
For the `useInterrupt` graph-paused pattern, you'll also use LangGraph's
|
||||
native `interrupt(...)` primitive inside a graph node — no extra
|
||||
CopilotKit setup beyond the middleware above.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,24 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire CopilotKit middleware into your graph
|
||||
|
||||
Programmatic control (`copilotkit.runAgent`, `agent.subscribe`,
|
||||
`agent.addMessage`) drives runs through the same agent your chat UI
|
||||
uses, so the backend wiring is the same one-line `CopilotKitMiddleware`
|
||||
setup.
|
||||
|
||||
<DemoCode file="src/agents/frontend_tools.py" region="middleware" />
|
||||
|
||||
For the headless `useInterrupt` pattern, also use LangGraph's native
|
||||
`interrupt(...)` inside a graph node and resume with
|
||||
`forwardedProps: { command: { resume, interruptEvent } }` from the
|
||||
frontend.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,19 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire CopilotKit middleware into your graph
|
||||
|
||||
Shared state flows between your UI and your agent through `agent.setState`
|
||||
on the frontend and `state.get(...)` in your graph nodes. Attach
|
||||
`CopilotKitMiddleware` to your `create_agent` call so CopilotKit-specific
|
||||
state is picked up alongside your own.
|
||||
|
||||
<DemoCode file="src/agents/shared_state_read_write.py" region="shared-state-setup" />
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,18 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Add state streaming middleware
|
||||
|
||||
`StateStreamingMiddleware` maps a tool argument to an agent state key and
|
||||
forwards partial tool-call arguments as state updates while the model is
|
||||
still generating.
|
||||
|
||||
<DemoCode file="src/agents/shared_state_streaming.py" region="state-streaming-middleware" />
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,21 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the LangGraph Python SDK
|
||||
|
||||
<InstallPythonSDK />
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wire CopilotKit middleware into your graph
|
||||
|
||||
Sub-agents are wired as tools on a supervisor `create_agent` call. The
|
||||
supervisor's middleware list includes `CopilotKitMiddleware()` so the
|
||||
delegation log slot streams back to the UI through shared state.
|
||||
|
||||
<DemoCode file="src/agents/subagents.py" region="subagent-setup" />
|
||||
|
||||
See `src/agents/subagents.py` for the canonical supervisor + three
|
||||
sub-agents pattern with a live delegation log.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cleanup() {
|
||||
kill $LANGGRAPH_PID $NEXTJS_PID $WATCHDOG_PID 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Disable Python stdout buffering so langgraph_cli's dev server and any
|
||||
# tracebacks it emits reach the Railway log stream immediately rather than
|
||||
# sitting in Python's userspace buffer until the process exits. Paired with
|
||||
# `python -u` on the langgraph_cli invocation below.
|
||||
export PYTHONUNBUFFERED=1
|
||||
|
||||
echo "========================================="
|
||||
echo "[entrypoint] Starting showcase: langgraph-python"
|
||||
echo "[entrypoint] Time: $(date -u)"
|
||||
echo "[entrypoint] PWD: $(pwd)"
|
||||
echo "[entrypoint] PORT=${PORT:-not set}"
|
||||
echo "[entrypoint] NODE_ENV=${NODE_ENV:-not set}"
|
||||
echo "========================================="
|
||||
|
||||
# Check critical env vars
|
||||
echo "[entrypoint] Checking environment variables..."
|
||||
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
|
||||
|
||||
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
||||
echo "[entrypoint] INFO: ANTHROPIC_API_KEY is not set"
|
||||
else
|
||||
echo "[entrypoint] ANTHROPIC_API_KEY: set (${#ANTHROPIC_API_KEY} chars)"
|
||||
fi
|
||||
|
||||
if [ -z "$LANGSMITH_API_KEY" ]; then
|
||||
echo "[entrypoint] INFO: LANGSMITH_API_KEY is not set (tracing disabled)"
|
||||
else
|
||||
echo "[entrypoint] LANGSMITH_API_KEY: set (${#LANGSMITH_API_KEY} chars)"
|
||||
fi
|
||||
|
||||
# Verify files exist
|
||||
echo "[entrypoint] Checking files..."
|
||||
ls -la langgraph.json 2>/dev/null && echo "[entrypoint] langgraph.json: OK" || echo "[entrypoint] ERROR: langgraph.json missing!"
|
||||
ls -la src/agents/main.py 2>/dev/null && echo "[entrypoint] src/agents/main.py: OK" || echo "[entrypoint] ERROR: src/agents/main.py missing!"
|
||||
ls -la .next/server 2>/dev/null > /dev/null && echo "[entrypoint] .next/server: OK" || echo "[entrypoint] ERROR: .next build missing!"
|
||||
|
||||
echo "[entrypoint] langgraph.json contents:"
|
||||
cat langgraph.json
|
||||
|
||||
echo "========================================="
|
||||
echo "[entrypoint] Starting LangGraph agent server on port 8123..."
|
||||
echo "========================================="
|
||||
|
||||
# Disable langgraph_runtime_inmem's pickle-flush-to-disk loop. Without this,
|
||||
# the inmem runtime periodically flushes unbounded thread/checkpoint state to
|
||||
# .langgraph_api/*.pckl files, which is a slow-burn OOM risk on Railway.
|
||||
# The env var is checked at import time in langgraph_runtime_inmem
|
||||
# _persistence.py and checkpoint.py (langgraph-api==0.7.101 / runtime==0.27.4).
|
||||
export LANGGRAPH_DISABLE_FILE_PERSISTENCE=true
|
||||
|
||||
# `python -u` forces unbuffered stdout/stderr at the interpreter level
|
||||
# (belt-and-suspenders with PYTHONUNBUFFERED=1 above) so langgraph_cli boot
|
||||
# failures surface in the Railway log stream immediately rather than sitting
|
||||
# in a pipe buffer until the process exits. `awk ... fflush()` replaces the
|
||||
# previous `sed` formulation — process substitution leaves $! pointing at
|
||||
# the real python process (pipe form made $! point at sed).
|
||||
# `--no-reload` disables watchfiles hot-reload, which fires on every request
|
||||
# and causes "1 change detected" log spam → Railway 500-logs/sec kill.
|
||||
python -u -m langgraph_cli dev \
|
||||
--config langgraph.json \
|
||||
--host 0.0.0.0 \
|
||||
--port 8123 \
|
||||
--no-browser \
|
||||
--no-reload &> >(awk '{print "[langgraph] " $0; fflush()}') &
|
||||
LANGGRAPH_PID=$!
|
||||
|
||||
# Give langgraph a moment to start
|
||||
sleep 3
|
||||
|
||||
# Check if langgraph is still running
|
||||
if kill -0 $LANGGRAPH_PID 2>/dev/null; then
|
||||
echo "[entrypoint] LangGraph agent server started (PID: $LANGGRAPH_PID)"
|
||||
else
|
||||
echo "[entrypoint] ERROR: LangGraph agent server failed to start!"
|
||||
echo "[entrypoint] Continuing with Next.js only (demos will show agent errors)"
|
||||
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 the Python langgraph process and any shell subprocesses; scope
|
||||
# it here so non-Next children see the host's environment.
|
||||
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 langgraph process stays alive (so `wait -n` never
|
||||
# fires and the container never restarts) but stops responding on :8123.
|
||||
# Poll the langgraph_cli /ok endpoint every 30s; after 3 consecutive failures
|
||||
# (~90s of unreachable agent), kill the agent process so `wait -n` returns
|
||||
# and Railway restarts the container. Generalized from
|
||||
# showcase/integrations/crewai-crews/entrypoint.sh (PRs #4114 + #4115).
|
||||
#
|
||||
# Startup grace: langgraph_cli dev does a heavy cold-start (graph compile
|
||||
# + uvicorn boot). On fresh Railway containers this can exceed the 90s
|
||||
# (3-strike) budget introduced in PR #4116, matching the restart loop
|
||||
# observed on langgraph-typescript (deployment
|
||||
# 58bbebe8-7a94-4f99-b6e4-ffcbb4eb78b9, 04-20 17:05 UTC). Wait up to 180s
|
||||
# for the first healthy /ok probe before arming the strike counter; if
|
||||
# /ok comes up sooner, fall through immediately. If 180s elapses without
|
||||
# success, arm the counter anyway — the steady-state watchdog will then
|
||||
# handle a true hang.
|
||||
(
|
||||
GRACE=180
|
||||
echo "[watchdog] Startup grace: waiting up to ${GRACE}s for first successful health probe before arming strike counter"
|
||||
ELAPSED=0
|
||||
while [ $ELAPSED -lt $GRACE ]; do
|
||||
if ! kill -0 $LANGGRAPH_PID 2>/dev/null; then
|
||||
# Agent died during startup — wait -n in the main shell will handle it.
|
||||
exit 0
|
||||
fi
|
||||
if curl -fsS --max-time 5 http://127.0.0.1:8123/ok > /dev/null 2>&1; then
|
||||
echo "[watchdog] Agent healthy after ${ELAPSED}s — arming strike counter"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
ELAPSED=$((ELAPSED + 5))
|
||||
done
|
||||
if [ $ELAPSED -ge $GRACE ]; then
|
||||
echo "[watchdog] Grace window elapsed without successful probe — arming strike counter anyway"
|
||||
fi
|
||||
FAILS=0
|
||||
while sleep 30; do
|
||||
if ! kill -0 $LANGGRAPH_PID 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
if curl -fsS --max-time 5 http://127.0.0.1:8123/ok > /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 $LANGGRAPH_PID to trigger container restart"
|
||||
kill -9 $LANGGRAPH_PID 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
) &
|
||||
WATCHDOG_PID=$!
|
||||
echo "[entrypoint] Watchdog started (PID: $WATCHDOG_PID, probing http://127.0.0.1:8123/ok, startup grace 180s)"
|
||||
echo "[entrypoint] All processes running. Waiting..."
|
||||
|
||||
# Only wait on agent + next.js — NOT the watchdog. The watchdog's job is to
|
||||
# kill the agent when it hangs; if the watchdog exits first (e.g. because it
|
||||
# killed the agent), wait -n would otherwise return with the watchdog's exit
|
||||
# code and short-circuit before the agent's true exit status is observable.
|
||||
wait -n $LANGGRAPH_PID $NEXTJS_PID
|
||||
EXIT_CODE=$?
|
||||
if ! kill -0 $LANGGRAPH_PID 2>/dev/null; then
|
||||
echo "[entrypoint] LangGraph (PID: $LANGGRAPH_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,39 @@
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"env": ".env",
|
||||
"http": {
|
||||
"configurable_headers": {
|
||||
"include": ["x-*"]
|
||||
}
|
||||
},
|
||||
"graphs": {
|
||||
"sample_agent": "./src/agents/main.py:graph",
|
||||
"tool_rendering": "./src/agents/tool_rendering_agent.py:graph",
|
||||
"tool_rendering_reasoning_chain": "./src/agents/tool_rendering_reasoning_chain_agent.py:graph",
|
||||
"a2ui_dynamic": "./src/agents/a2ui_dynamic.py:graph",
|
||||
"a2ui_fixed": "./src/agents/a2ui_fixed.py:graph",
|
||||
"a2ui_recovery": "./src/agents/recovery_agent.py:graph",
|
||||
"reasoning_agent": "./src/agents/reasoning_agent.py:graph",
|
||||
"interrupt_agent": "./src/agents/interrupt_agent.py:graph",
|
||||
"open_gen_ui": "./src/agents/open_gen_ui_agent.py:graph",
|
||||
"open_gen_ui_advanced": "./src/agents/open_gen_ui_advanced_agent.py:graph",
|
||||
"mcp_apps": "./src/agents/mcp_apps_agent.py:graph",
|
||||
"hitl_in_chat": "./src/agents/hitl_in_chat_agent.py:graph",
|
||||
"hitl_in_app": "./src/agents/hitl_in_app.py:graph",
|
||||
"shared_state_read_write": "./src/agents/shared_state_read_write.py:graph",
|
||||
"readonly_state_agent_context": "./src/agents/readonly_state_agent_context.py:graph",
|
||||
"shared_state_streaming": "./src/agents/shared_state_streaming.py:graph",
|
||||
"subagents": "./src/agents/subagents.py:graph",
|
||||
"headless_complete": "./src/agents/headless_complete.py:graph",
|
||||
"agentic_chat": "./src/agents/agentic_chat.py:graph",
|
||||
"frontend_tools": "./src/agents/frontend_tools.py:graph",
|
||||
"frontend_tools_async": "./src/agents/frontend_tools_async.py:graph",
|
||||
"gen_ui_agent": "./src/agents/gen_ui_agent.py:graph",
|
||||
"gen_ui_tool_based": "./src/agents/gen_ui_tool_based.py:graph",
|
||||
"beautiful_chat": "./src/agents/beautiful_chat.py:graph",
|
||||
"multimodal": "./src/agents/multimodal_agent.py:graph",
|
||||
"agent_config_agent": "./src/agents/agent_config_agent.py:graph",
|
||||
"byoc_hashbrown": "./src/agents/byoc_hashbrown_agent.py:graph",
|
||||
"byoc_json_render": "./src/agents/byoc_json_render_agent.py:graph"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
name: LangGraph (Python)
|
||||
slug: langgraph-python
|
||||
category: popular
|
||||
language: python
|
||||
logo: /logos/langgraph-python.svg
|
||||
description: >-
|
||||
The most feature-complete CopilotKit integration. LangGraph provides a flexible
|
||||
graph-based framework for building agents with Python. Supports the full range of
|
||||
CopilotKit features including generative UI, shared state, human-in-the-loop,
|
||||
sub-agents, and streaming.
|
||||
partner_docs: https://docs.copilotkit.ai/integrations/langgraph
|
||||
repo: https://github.com/CopilotKit/CopilotKit/tree/main/showcase/integrations/langgraph-python
|
||||
copilotkit_version: 2.0.0
|
||||
deployed: true
|
||||
docs_mode: generated
|
||||
sort_order: 10
|
||||
generative_ui:
|
||||
- constrained-explicit
|
||||
- a2ui-fixed-schema
|
||||
- a2ui-dynamic-schema
|
||||
interaction_modalities:
|
||||
- sidebar
|
||||
- embedded
|
||||
- chat
|
||||
managed_platform:
|
||||
name: LangGraph Platform
|
||||
url: https://langsmith.com
|
||||
not_supported_features:
|
||||
# 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. This is
|
||||
# the reference integration, but the bug blocks it too.
|
||||
- gen-ui-interrupt
|
||||
- interrupt-headless
|
||||
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
|
||||
- gen-ui-tool-based
|
||||
- hitl-in-chat
|
||||
- hitl-in-app
|
||||
- declarative-gen-ui
|
||||
- a2ui-fixed-schema
|
||||
- a2ui-recovery
|
||||
- mcp-apps
|
||||
- gen-ui-agent
|
||||
- tool-rendering-default-catchall
|
||||
- tool-rendering-custom-catchall
|
||||
- tool-rendering
|
||||
- tool-rendering-reasoning-chain
|
||||
- shared-state-read
|
||||
- shared-state-read-write
|
||||
- shared-state-streaming
|
||||
- readonly-state-agent-context
|
||||
- subagents
|
||||
- multimodal
|
||||
- auth
|
||||
- declarative-hashbrown
|
||||
- declarative-json-render
|
||||
- open-gen-ui
|
||||
- open-gen-ui-advanced
|
||||
- voice
|
||||
- agent-config
|
||||
a2ui_pattern: schema-loading
|
||||
interrupt_pattern: native
|
||||
thread_persistence_pattern: langgraph
|
||||
agent_config_pattern: shared-state
|
||||
auth_pattern: langgraph
|
||||
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 langgraph-python"
|
||||
- id: agentic-chat
|
||||
name: "Pre-Built: CopilotChat"
|
||||
description: Vanilla CopilotChat with three starter-prompt suggestions — the minimum-viable surface for free-form chat
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/agentic-chat
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/agentic_chat.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/main.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: "Generative UI: Tool Rendering (Default)"
|
||||
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/tool_rendering_agent.py
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: tool-rendering-custom-catchall
|
||||
name: "Generative UI: Tool Rendering (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/tool_rendering_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/frontend_tools.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/frontend_tools_async.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-in-chat
|
||||
name: "Human In the Loop: In-chat"
|
||||
description: Time-slot picker rendered inline in the chat via `useHumanInTheLoop` — the agent requests user input, the frontend renders a TimePickerCard, and the user's choice resumes the agent.
|
||||
tags:
|
||||
- interactivity
|
||||
route: /demos/hitl-in-chat
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/hitl-in-chat/page.tsx
|
||||
- src/app/demos/hitl-in-chat/time-picker-card.tsx
|
||||
- src/agents/hitl_in_chat_agent.py
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: hitl-in-app
|
||||
name: "Human in the Loop: In-app"
|
||||
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.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: tool-rendering
|
||||
name: "Generative UI: Tool Rendering (Custom)"
|
||||
description: Custom per-tool renderers (WeatherCard, FlightListCard) plus a wildcard catch-all for every other tool
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/tool-rendering
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/tool_rendering_agent.py
|
||||
- src/app/demos/tool-rendering/page.tsx
|
||||
- src/app/demos/tool-rendering/weather-card.tsx
|
||||
- src/app/demos/tool-rendering/flight-list-card.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: tool-rendering-reasoning-chain
|
||||
name: "Generative UI: Tool calls + reasoning"
|
||||
description: "Per-tool renderers (WeatherCard, FlightListCard, custom catchall) interleaved with a reasoningMessage slot rendering the agent's reasoning tokens — combines reasoning-display + tool-rendering in one chat surface."
|
||||
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/reasoning-block.tsx
|
||||
- src/app/demos/tool-rendering-reasoning-chain/weather-card.tsx
|
||||
- src/app/demos/tool-rendering-reasoning-chain/flight-list-card.tsx
|
||||
- src/app/demos/tool-rendering-reasoning-chain/custom-catchall-renderer.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: gen-ui-tool-based
|
||||
name: "Generative UI: useComponent"
|
||||
description: Agent uses tools to trigger UI generation
|
||||
tags:
|
||||
- controlled-generative-ui
|
||||
route: /demos/gen-ui-tool-based
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/gen_ui_tool_based.py
|
||||
- src/app/demos/gen-ui-tool-based/page.tsx
|
||||
- src/app/demos/gen-ui-tool-based/bar-chart.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: gen-ui-agent
|
||||
name: "Generative UI: Agent State"
|
||||
description: "Agent-state-driven Gen UI — the agent plans a live step list via Command(update={steps}); the frontend subscribes through useAgent and renders the steps inside CopilotChat's messageView.children slot."
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/gen-ui-agent
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/gen_ui_agent.py
|
||||
- src/app/demos/gen-ui-agent/page.tsx
|
||||
- src/app/demos/gen-ui-agent/InlineAgentStateCard.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: shared-state-streaming
|
||||
name: "Shared State: Streaming"
|
||||
description: Per-token state delta streaming from agent to UI
|
||||
tags:
|
||||
- agent-state
|
||||
route: /demos/shared-state-streaming
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/shared_state_streaming.py
|
||||
- src/app/demos/shared-state-streaming/page.tsx
|
||||
- src/app/demos/shared-state-streaming/document-view.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: shared-state-read-write
|
||||
name: "Shared State: Read + Write"
|
||||
description: Bidirectional shared state — UI writes preferences via agent.setState; agent writes notes via a Command-returning tool
|
||||
tags:
|
||||
- agent-state
|
||||
route: /demos/shared-state-read-write
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/shared_state_read_write.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: shared-state-read
|
||||
name: "Shared State: Read-only"
|
||||
description: "Recipe editor publishes form state via agent.setState; the agent reads the recipe context but does not mutate it (no backend tool — neutral default agent)."
|
||||
tags:
|
||||
- agent-state
|
||||
route: /demos/shared-state-read
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/shared-state-read/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: subagents
|
||||
name: Sub-Agents
|
||||
description: Multiple agents with visible task delegation
|
||||
tags:
|
||||
- multi-agent
|
||||
route: /demos/subagents
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/subagents.py
|
||||
- src/app/demos/subagents/page.tsx
|
||||
- src/app/demos/subagents/delegation-log.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- 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/main.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/main.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/main.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 UI: Simple"
|
||||
description: Minimum viable headless chat — useAgent + useCopilotKit dressed in shadcn/ui primitives. No tool rendering, no generative UI, just text in / text out.
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/headless-simple
|
||||
highlight:
|
||||
- src/app/demos/headless-simple/page.tsx
|
||||
- src/app/demos/headless-simple/chat.tsx
|
||||
- src/app/demos/headless-simple/composer.tsx
|
||||
- src/app/demos/headless-simple/empty-state.tsx
|
||||
- src/app/demos/headless-simple/message-bubble.tsx
|
||||
- id: headless-complete
|
||||
name: "Headless UI: Complete"
|
||||
description: Full headless surface — hand-rolled CopilotChat replacement that wires every render hook (useRenderTool, useDefaultRenderTool, useComponent, useRenderToolCall, useRenderActivityMessage, useSuggestions, useAttachments) on top of shadcn primitives.
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/headless-complete
|
||||
highlight:
|
||||
- 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/demos/headless-complete/tools/weather-card.tsx
|
||||
- src/app/demos/headless-complete/tools/stock-card.tsx
|
||||
- src/app/demos/headless-complete/tools/chart-card.tsx
|
||||
- src/app/demos/headless-complete/tools/highlight-note.tsx
|
||||
- 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: "Human in the Loop: Interrupts"
|
||||
description: Interactive component rendered inline in the chat via the lower-level `useInterrupt` primitive — direct control over the LangGraph interrupt lifecycle
|
||||
tags:
|
||||
- interactivity
|
||||
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: interrupt-headless
|
||||
name: "Human in the Loop: Headless Interrupts"
|
||||
description: "Same interrupt(...) backend pattern as gen-ui-interrupt, but the time-picker mounts in a separate app-surface pane (left) instead of inline inside the chat — uses useHeadlessInterrupt (custom-event subscribe + manual runAgent forwardedProps.command.resume)."
|
||||
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: declarative-gen-ui
|
||||
name: "Declarative UI: Dynamic A2UI"
|
||||
description: Canonical A2UI BYOC — custom catalog (Card/StatusBadge/Metric/InfoRow/PrimaryButton) wired via a2ui.catalog on the provider
|
||||
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 UI: Fixed A2UI"
|
||||
description: Render an A2UI tree from a fixed, server-side schema — the agent streams data into a pre-authored component tree
|
||||
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/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: a2ui-recovery
|
||||
name: A2UI Error Recovery
|
||||
description: Makes the A2UI validate->retry recovery loop visible — an invalid first render heals to a valid one, and an always-invalid render shows a graceful recovery-exhausted fallback. Backend-owned via get_a2ui_tools (injectA2UITool=false); reuses the declarative-gen-ui catalog.
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/a2ui-recovery
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agents/recovery_agent.py
|
||||
- src/app/demos/a2ui-recovery/page.tsx
|
||||
- src/app/demos/a2ui-recovery/suggestions.ts
|
||||
- src/app/api/copilotkit-a2ui-recovery/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: readonly-state-agent-context
|
||||
name: "Shared State: Frontend 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/readonly_state_agent_context.py
|
||||
- src/app/demos/readonly-state-agent-context/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: declarative-json-render
|
||||
name: "Declarative UI: 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: 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:
|
||||
- platform
|
||||
route: /demos/auth
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/auth/page.tsx
|
||||
- src/app/demos/auth/sign-in-card.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: "Declarative UI: 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: "Open Generative UI: Default"
|
||||
description: "Agent composes UI from a registered component library — distinct from Tool Rendering, which attaches a custom renderer to a *named* backend tool. Use Open Generative UI when you want the agent to assemble UI freely from a palette you control."
|
||||
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 Generative UI: Custom"
|
||||
description: "Same Open Generative UI surface, with the iframe-rendered components able to call back into frontend sandbox functions (e.g., a Calculator app whose buttons trigger handlers in the host page)."
|
||||
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
|
||||
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/main.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;
|
||||
+12838
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@copilotkit/showcase-langgraph-python",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"next dev --turbopack\" \"python -u -m langgraph_cli dev --config langgraph.json --host 0.0.0.0 --port 8123 --no-browser\"",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"cmdk": "^0.2.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"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.5.0",
|
||||
"yaml": "^2.8.4",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@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,38 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || "http://localhost:3000",
|
||||
trace: "on-first-retry",
|
||||
extraHTTPHeaders: {
|
||||
"X-AIMock-Context": "langgraph-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,
|
||||
// Default to local aimock (Docker-exposed) when not set.
|
||||
// In CI the env is inherited from docker-compose (http://aimock:4010/v1).
|
||||
OPENAI_BASE_URL:
|
||||
process.env.OPENAI_BASE_URL || "http://localhost:4010/v1",
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "sk-mock-local-dev",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,18 @@
|
||||
# Voice demo audio
|
||||
|
||||
Drop a small (<100KB) WAV file named `sample.wav` in this directory. The voice
|
||||
demo (`/demos/voice`) fetches it client-side and POSTs to the transcription
|
||||
endpoint so the flow works without mic permissions.
|
||||
|
||||
Expected content: an audio clip speaking the phrase
|
||||
**"What is the weather in Tokyo?"** — the demo caption advertises that phrase
|
||||
to the user, and the bundled QA checklist + E2E spec assert the transcribed
|
||||
text contains "weather" and/or "Tokyo".
|
||||
|
||||
Generate locally, for example:
|
||||
|
||||
- macOS: `say -o sample.aiff "What is the weather in Tokyo?" && ffmpeg -i sample.aiff -ar 16000 -ac 1 sample.wav`
|
||||
- Linux: `espeak-ng -w sample.wav "What is the weather in Tokyo?"`
|
||||
- Windows: PowerShell `System.Speech.Synthesis.SpeechSynthesizer` → `SetOutputToWaveFile`
|
||||
|
||||
Target: 16kHz mono, 3-5s duration, <100KB.
|
||||
Binary file not shown.
@@ -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,55 @@
|
||||
# QA: Declarative Generative UI (A2UI — Fixed Schema) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/a2ui-fixed-schema` on the dashboard host
|
||||
- Agent backend is healthy; `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `a2ui_fixed` graph (registered as agent name `a2ui-fixed-schema` — see `src/app/api/copilotkit-a2ui-fixed-schema/route.ts`)
|
||||
- Note: the demo source contains no `data-testid` attributes. Checks below rely on verbatim visible text, DOM structure, and the JSON schema at `src/agents/a2ui_schemas/flight_schema.json`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/a2ui-fixed-schema`; verify the page renders within 3s and a single `CopilotChat` pane is centered (max-width ~896px, rounded-2xl, full-height)
|
||||
- [ ] Verify the chat is wired to `runtimeUrl="/api/copilotkit-a2ui-fixed-schema"` and `agent="a2ui-fixed-schema"` (DevTools → Network: sending a message hits that endpoint)
|
||||
- [ ] Verify the single suggestion pill is visible with verbatim title "Find SFO → JFK" (message body: "Find me a flight from SFO to JFK on United for $289.")
|
||||
- [ ] Send "Hello" and verify an assistant text response appears within 10s (no flight card for plain text)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Schema Wiring (fixed catalog + `includeBasicCatalog`)
|
||||
|
||||
- [ ] DevTools → Network: after the first successful `display_flight` call, verify the response stream contains an `a2ui_operations` container with `catalogId: "copilotkit://flight-fixed-catalog"` (matches `CATALOG_ID` in `src/agents/a2ui_fixed.py` and `src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts`)
|
||||
- [ ] Verify the same container carries the full `FLIGHT_SCHEMA` component tree (12 nodes from `src/agents/a2ui_schemas/flight_schema.json`: `root`, `content`, `title`, `route`, `from`, `arrow`, `to`, `meta`, `airline`, `price`, `bookButton`, `bookButtonLabel`)
|
||||
|
||||
#### Search-Flights Prompt (`display_flight` tool → `flight_schema.json`)
|
||||
|
||||
- [ ] Click the "Find SFO → JFK" suggestion; within 20s verify a single flight card renders in-transcript assembled per `flight_schema.json`:
|
||||
- outer `Card` with a `Column` of children in this order: title row, route row, meta row, book button
|
||||
- `Title` node renders the literal text "Flight Details" (1.15rem / 600 weight, color `#010507`)
|
||||
- `route` row shows `Airport` "SFO" → `Arrow` (`→`, color `#AFAFB7`) → `Airport` "JFK" (both monospaced, 1.5rem, 600 weight, 0.05em letter-spacing)
|
||||
- `meta` row shows `AirlineBadge` "UNITED" (uppercase pill, lilac `#BEC2FF` border, 0.08em tracking) on the left and `PriceTag` "$289" (monospaced, color `#189370`, 1.1rem / 600 weight) on the right
|
||||
- `Button` renders full-width with label "Book flight" (black `#010507` background, white text, 12px radius)
|
||||
- [ ] Verify all four data-model fields resolved correctly (origin=`SFO`, destination=`JFK`, airline=`United`, price=`$289`) — each is a `{ path: "/..." }` binding in the schema and must reach the DOM as a plain string via the `GenericBinder` (no literal `{path}` leak and no React error #31)
|
||||
|
||||
#### Book-Flight Button (inert — pure presentation)
|
||||
|
||||
- [ ] Verify the "Book flight" button is rendered with the schema-declared label and is clickable, but the click is a no-op: the agent is not invoked, no schema swap occurs, and the button does not transition to a "Booked" state. Schema-swap-on-action will be wired up once the Python SDK exposes `action_handlers=` on `a2ui.render` (see comment in `src/agents/a2ui_fixed.py`).
|
||||
|
||||
#### Follow-up Prompt (data-model refresh)
|
||||
|
||||
- [ ] Send "Find me a flight from LAX to ORD on Delta for $412."; within 20s verify the card updates in place with origin=`LAX`, destination=`ORD`, airline=`DELTA`, price=`$412` (same schema, new data model — proves the fixed-schema pattern: schema once, data streams)
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message; verify it is a no-op (no user bubble, no assistant response)
|
||||
- [ ] Send "What is the capital of France?"; verify the agent replies in plain text without invoking `display_flight` (no flight card rendered, no `a2ui_operations` in the response)
|
||||
- [ ] DevTools → Console: walk through all flows above; verify no uncaught errors and specifically no React error #31 ("objects are not valid as a React child, found: object with keys {path}") — the `DynString` union in `a2ui/definitions.ts` is what prevents this, so a single occurrence is a regression
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3s; plain-text response within 10s; flight card renders within 20s of the search prompt
|
||||
- `display_flight` is called exactly once per search prompt; result contains an `a2ui_operations` container with `catalogId: "copilotkit://flight-fixed-catalog"` and the full 12-node flight schema
|
||||
- All custom renderers in `a2ui/renderers.tsx` (`Card`, `Title`, `Airport`, `Arrow`, `AirlineBadge`, `PriceTag`, `Button`) render at least once per search-flights run
|
||||
- Clicking "Book flight" is a no-op (inert presentation button — see comment in `src/agents/a2ui_fixed.py`)
|
||||
- No UI layout breaks, no `{path}` leak into the DOM, no uncaught console errors
|
||||
@@ -0,0 +1,44 @@
|
||||
# QA: A2UI Error Recovery — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/a2ui-recovery` on the dashboard host
|
||||
- Agent backend is healthy; `OPENAI_API_KEY` is set; `LANGGRAPH_DEPLOYMENT_URL` points at the LangGraph deployment exposing the `a2ui_recovery` graph (registered as agent name `a2ui-recovery` — see `src/app/api/copilotkit-a2ui-recovery/route.ts`)
|
||||
- Requires `ag-ui-langgraph >= 0.0.41` (the `get_a2ui_tools` validate→retry recovery loop + `a2ui_recovery_exhausted` hard-fail envelope) and the `@copilotkit` A2UI renderer (the `building`/`retrying`/`failed` lifecycle rendering)
|
||||
- Backend-owned wiring: the route sets `injectA2UITool: false`; the agent owns `generate_a2ui` via `get_a2ui_tools({ recovery: { maxAttempts: 3 } })` (see `src/agents/recovery_agent.py`)
|
||||
- Reuses the **declarative-gen-ui** catalog (`catalogId: "declarative-gen-ui-catalog"`) and the Vantage Threads sales context — no new components
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/a2ui-recovery`; verify the page renders within 3s and a single `CopilotChat` pane is centered (max-width ~896px, rounded-2xl, full-height)
|
||||
- [ ] Verify the chat is wired to `runtimeUrl="/api/copilotkit-a2ui-recovery"` and `agent="a2ui-recovery"` (DevTools → Network: sending a message hits that endpoint, not `/api/copilotkit`)
|
||||
- [ ] Verify both suggestion pills are visible with verbatim titles:
|
||||
- "Recover a bad render"
|
||||
- "Show an unrecoverable failure"
|
||||
|
||||
### 2. Healing path
|
||||
|
||||
- [ ] Click "Recover a bad render" ("Render my Q2 sales dashboard, recovering if the first attempt is malformed.")
|
||||
- [ ] The inner `render_a2ui` returns **free-form / sloppy** A2UI args (components & data as JSON strings rather than structured arrays). Verify the middleware **heals** them via `parse_and_fix` into a valid surface that paints (no broken surface, no error banner)
|
||||
- [ ] Verify the **painted** surface is valid: a `declarative-metric` row ("Quarterly Revenue $4.2M", "Win Rate 31%") — i.e., the sloppy render was repaired and rendered
|
||||
- [ ] DevTools → Network: verify the final tool result carries an `a2ui_operations` container (no `a2ui_recovery_exhausted`)
|
||||
- [ ] Verify the chat reply is one short sentence noting the heal
|
||||
|
||||
### 3. Hard-fail (recovery exhausted) path
|
||||
|
||||
- [ ] Click "Show an unrecoverable failure" ("Render a dashboard that keeps failing validation so I can see the fallback.")
|
||||
- [ ] Verify the lifecycle ends in a tasteful `failed` state (NOT a broken/half-rendered surface and NOT a silent drop)
|
||||
- [ ] DevTools → Network: verify `render_a2ui` was attempted up to the cap (3 attempts, all invalid) and the tool returned an `a2ui_recovery_exhausted` envelope (no `a2ui_operations` painted)
|
||||
- [ ] Verify the chat reply gracefully explains the fallback (one short sentence)
|
||||
|
||||
### 4. Regression / isolation
|
||||
|
||||
- [ ] Verify the recovery demo does not affect the declarative-gen-ui or beautiful-chat demos (separate routes/agents)
|
||||
- [ ] Re-run each pill a second time and verify the same lifecycle
|
||||
|
||||
## Notes
|
||||
|
||||
- The malformed renders are forced by aimock fixtures (`showcase/aimock/d6/langgraph-python/a2ui-recovery.json`): the inner `render_a2ui` call is matched by `userMessage` + `toolName=render_a2ui`. Healing itself is performed live by the toolkit recovery loop inside `ag_ui_langgraph.get_a2ui_tools`, not the fixture.
|
||||
- This is the LangGraph-Python sibling of the Google-ADK `a2ui-recovery` demo; the backend recovery loop is provided by `ag_ui_langgraph` (`get_a2ui_tools`) rather than the ADK middleware (`get_a2ui_tool`).
|
||||
@@ -0,0 +1,64 @@
|
||||
# QA: Agent Config Object — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed and accessible at `/demos/agent-config`
|
||||
- Railway service `showcase-langgraph-python` healthy
|
||||
- `OPENAI_API_KEY` set on Railway
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Initial state
|
||||
|
||||
- [ ] Navigate to `/demos/agent-config`
|
||||
- [ ] Header "Agent Config Object" visible
|
||||
- [ ] `agent-config-card` is visible with the heading "Agent Config"
|
||||
- [ ] Tone dropdown (`data-testid="agent-config-tone-select"`) shows "professional"
|
||||
- [ ] Expertise dropdown (`data-testid="agent-config-expertise-select"`) shows "intermediate"
|
||||
- [ ] Response length dropdown (`data-testid="agent-config-length-select"`) shows "concise"
|
||||
- [ ] `<CopilotChat />` composer visible below the card
|
||||
|
||||
### 2. Default send
|
||||
|
||||
- [ ] Type "Tell me about black holes" and send
|
||||
- [ ] Agent responds within 15 seconds
|
||||
- [ ] Response is brief (1-3 sentences), professional tone, no emoji (consistent with the default config)
|
||||
|
||||
### 3. Enthusiastic + detailed
|
||||
|
||||
- [ ] Change Tone to "enthusiastic"
|
||||
- [ ] Change Response length to "detailed"
|
||||
- [ ] Verify both select values updated in the DOM
|
||||
- [ ] Send "Tell me about black holes" again
|
||||
- [ ] Response is noticeably longer (multiple paragraphs) and uses upbeat / energetic language
|
||||
- [ ] Compare to Step 2's response — the style difference is visible
|
||||
|
||||
### 4. Beginner expertise
|
||||
|
||||
- [ ] Change Expertise to "beginner"
|
||||
- [ ] Send "What is quantum entanglement?"
|
||||
- [ ] Response defines jargon and uses analogies
|
||||
|
||||
### 5. Expert expertise
|
||||
|
||||
- [ ] Change Expertise to "expert"
|
||||
- [ ] Send the same question
|
||||
- [ ] Response uses precise terminology, skips basics
|
||||
|
||||
### 6. Reactivity mid-thread
|
||||
|
||||
- [ ] Without reloading the page, with previous replies visible, change Tone to "casual"
|
||||
- [ ] Send a follow-up
|
||||
- [ ] Reply reflects the casual tone; previous replies in the transcript remain unchanged
|
||||
|
||||
### 7. Error handling
|
||||
|
||||
- [ ] Send an empty message; verify no-op or graceful empty-message handling
|
||||
- [ ] Verify no console errors during any of the above steps
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Dropdown value changes appear in the DOM within 100ms of selection
|
||||
- Agent responses arrive within 15s per send
|
||||
- Visible style differences across tone / expertise / length changes (qualitative, but clear side-by-side)
|
||||
- Transcript preserves history when config changes mid-thread
|
||||
@@ -0,0 +1,52 @@
|
||||
# QA: Agentic Chat — LangGraph (Python)
|
||||
|
||||
The minimum-viable CopilotChat demo: vanilla `<CopilotChat>` wired to a
|
||||
neutral helpful-assistant agent, with three starter-prompt suggestions.
|
||||
No tools, no custom rendering — anything richer belongs in dedicated
|
||||
demos (frontend-tools, tool-rendering, hitl-in-chat, etc.).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check `/api/health`)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Initial render
|
||||
|
||||
- [ ] Navigate to `/demos/agentic-chat`
|
||||
- [ ] Verify the chat input renders with placeholder "Type a message..."
|
||||
- [ ] Verify all three suggestion pills are visible:
|
||||
- "Write a sonnet"
|
||||
- "Tell me a joke"
|
||||
- "Is 17 prime?"
|
||||
|
||||
### 2. Free-form chat
|
||||
|
||||
- [ ] Type a basic message (e.g. "Say hello") and press Enter
|
||||
- [ ] Verify the assistant streams back a text response
|
||||
|
||||
### 3. Suggestion pills
|
||||
|
||||
- [ ] Click the "Tell me a joke" pill
|
||||
- [ ] Verify the message is sent and the assistant streams back a joke
|
||||
|
||||
### 4. Multi-turn context
|
||||
|
||||
- [ ] Send "My name is Alice."
|
||||
- [ ] Wait for the assistant response
|
||||
- [ ] Send "What name did I just give you?"
|
||||
- [ ] Verify the assistant's second response contains "Alice"
|
||||
|
||||
### 5. Hygiene
|
||||
|
||||
- [ ] No console errors during normal usage
|
||||
- [ ] No layout breakage with a very long input
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat input mounts within ~3 seconds
|
||||
- Assistant first-token latency is under ~5 seconds for short prompts;
|
||||
full responses complete within ~30 seconds
|
||||
- Suggestion pills render alongside an empty chat and disappear once a
|
||||
conversation is in progress
|
||||
@@ -0,0 +1,68 @@
|
||||
# QA: Authentication — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed and accessible at /demos/auth
|
||||
- Railway service `showcase-langgraph-python` healthy
|
||||
- OPENAI_API_KEY set on Railway
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Initial authenticated state
|
||||
|
||||
- [ ] Navigate to /demos/auth
|
||||
- [ ] Verify the banner is visible with a green/success appearance (data-testid="auth-banner", data-authenticated="true")
|
||||
- [ ] Verify auth-status text reads "✓ Signed in as demo user"
|
||||
- [ ] Verify the "Sign out" button is visible and enabled (data-testid="auth-sign-out-button")
|
||||
- [ ] Verify the "Sign in" button is NOT present
|
||||
- [ ] Verify <CopilotChat /> is mounted below the banner
|
||||
- [ ] Verify no auth-demo-error surface is shown (data-testid="auth-demo-error" absent)
|
||||
- [ ] Verify no console errors on page load (the `/info` handshake should succeed)
|
||||
|
||||
### 2. Authenticated send → assistant response
|
||||
|
||||
- [ ] Type "Hello" and click send
|
||||
- [ ] Within 30 seconds, an assistant response is rendered in the transcript
|
||||
- [ ] No auth-demo-error surface appears
|
||||
|
||||
### 3. Sign out flips the banner and surfaces 401 without crashing
|
||||
|
||||
- [ ] Click "Sign out"
|
||||
- [ ] Within 1 second, the banner flips to amber/warning appearance (data-authenticated="false")
|
||||
- [ ] Verify auth-status text reads "⚠ Signed out — the agent will reject your messages until you sign in."
|
||||
- [ ] Verify the "Sign in" button is visible (data-testid="auth-authenticate-button")
|
||||
- [ ] Verify the "Sign out" button is no longer present
|
||||
- [ ] Type "Hello again" and click send
|
||||
- [ ] Within 15 seconds, the page-level error surface appears:
|
||||
- `data-testid="auth-demo-error"` visible with text containing "401" and/or "Unauthorized"
|
||||
- [ ] Verify the banner is STILL visible — the page must not white-screen
|
||||
- [ ] Verify no assistant response appears for the unauthenticated send
|
||||
|
||||
### 4. Sign in clears the error and restores sends
|
||||
|
||||
- [ ] Click "Sign in"
|
||||
- [ ] Within 1 second, the banner flips back to green (data-authenticated="true")
|
||||
- [ ] Verify the auth-demo-error surface is cleared
|
||||
- [ ] Type "Hello" and click send
|
||||
- [ ] Within 30 seconds, an assistant response is rendered
|
||||
|
||||
### 5. Refresh resets state to authenticated
|
||||
|
||||
- [ ] Hard-reload the page
|
||||
- [ ] Banner is green on first render (default state is authenticated; state does NOT persist)
|
||||
- [ ] No error surface on first render
|
||||
|
||||
### 6. Error Handling
|
||||
|
||||
- [ ] With DevTools Network panel blocking /api/copilotkit-auth, send a message while authenticated
|
||||
- [ ] Verify a network-level error surfaces cleanly (no uncaught promise rejection in console)
|
||||
- [ ] Restore network; verify sends work again without a page reload
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page loads authenticated by default — no 401 crash on initial `/info` fetch
|
||||
- Banner state flips within 1s of Sign out / Sign in clicks
|
||||
- Post-sign-out sends produce a visible 401 error within 15s via auth-demo-error
|
||||
- Page never white-screens after sign out — banner and composer remain mounted
|
||||
- Authenticated sends produce an assistant response within 30s
|
||||
- Refresh fully resets auth state (back to authenticated)
|
||||
@@ -0,0 +1,90 @@
|
||||
# QA: Beautiful Chat — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/beautiful-chat` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `beautiful_chat` graph
|
||||
- Note: the demo source contains no `data-testid` attributes. Checks below rely on verbatim visible text and DOM structure.
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/beautiful-chat`; verify the page renders within 3s with the CopilotKit logo (`img[alt="CopilotKit"]`, src `/copilotkit-logo.svg`) top-left of the chat pane
|
||||
- [ ] Verify the `Chat` / `App` mode pill is fixed top-right, `Chat` active (highlighted) by default, and the right-side canvas region is collapsed (width 0)
|
||||
- [ ] Verify the `CopilotChat` input is rendered with no disclaimer text below it
|
||||
- [ ] Verify all 9 suggestion pills are visible with verbatim titles:
|
||||
- "Pie Chart (Controlled Generative UI)"
|
||||
- "Bar Chart (Controlled Generative UI)"
|
||||
- "Schedule Meeting (Human In The Loop)"
|
||||
- "Search Flights (A2UI Fixed Schema)"
|
||||
- "Sales Dashboard (A2UI Dynamic)"
|
||||
- "Excalidraw Diagram (MCP App)"
|
||||
- "Calculator App (Open Generative UI)"
|
||||
- "Toggle Theme (Frontend Tools)"
|
||||
- "Task Manager (Shared State)"
|
||||
- [ ] Send "Hello" and verify an assistant text response appears within 10s
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Mode Toggle (frontend tools `enableAppMode` / `enableChatMode`)
|
||||
|
||||
- [ ] Click `App`; verify the canvas expands to ~2/3 width showing the TodoList empty state: pencil emoji, heading "No todos yet", subtext "Create your first task to get started", enabled "Add a task" button
|
||||
- [ ] Click `Chat`; verify the canvas collapses back to width 0
|
||||
|
||||
#### Shared State — Task Manager (agent tools `manage_todos`, `get_todos`)
|
||||
|
||||
- [ ] Click the "Task Manager (Shared State)" pill; verify the mode auto-switches to App and within 15s the "To Do" column renders exactly 3 todo cards (each with emoji, title, description)
|
||||
- [ ] Verify the "Done" column is empty (shows "No completed todos yet")
|
||||
- [ ] Click a todo's checkbox; verify the card moves from "To Do" to "Done"
|
||||
|
||||
#### Controlled Generative UI — Pie Chart (agent tool `query_data` + frontend component `pieChart`)
|
||||
|
||||
- [ ] Click "Pie Chart (Controlled Generative UI)"; within 15s verify a pie-chart card renders in-transcript with a non-empty `CardTitle` and `CardDescription`
|
||||
- [ ] Verify the donut SVG renders at least 2 `<circle>` slice elements inside the card
|
||||
- [ ] Verify the legend renders one row per slice: colored dot, label, comma-formatted value, and a percentage ending in "%"; percentages sum to 100%
|
||||
|
||||
#### Controlled Generative UI — Bar Chart (agent tool `query_data` + frontend component `barChart`)
|
||||
|
||||
- [ ] Click "Bar Chart (Controlled Generative UI)"; within 15s verify a bar-chart card renders with `CardTitle`, `CardDescription`, and a bar-chart icon in the header
|
||||
- [ ] Verify the recharts `ResponsiveContainer` (height 280px) renders at least 2 bar rectangles with X-axis labels matching the `label` field values; bars animate in via the `barSlideIn` keyframe on first render
|
||||
|
||||
#### Human-in-the-Loop — Schedule Meeting (frontend tool `scheduleTime`)
|
||||
|
||||
- [ ] Click "Schedule Meeting (Human In The Loop)"; within 15s verify a MeetingTimePicker card renders with a clock icon, a heading (agent-supplied reason or default "Schedule a Meeting"), 3 time-slot buttons each with date + time + a "30 min" duration badge, and a "None of these work" ghost button
|
||||
- [ ] Click a time slot; verify the card switches to the confirmed state with heading "Meeting Scheduled", the chosen date/time, and a green check icon
|
||||
- [ ] Re-trigger, click "None of these work"; verify the card shows heading "No Time Selected" and subtext "Looking for a better time that works for you"
|
||||
|
||||
#### A2UI Fixed Schema — Search Flights (agent tool `search_flights`)
|
||||
|
||||
- [ ] Click "Search Flights (A2UI Fixed Schema)"; within 20s verify exactly 2 flight cards render in-transcript, each with airline name, airline logo image, flight number, origin/destination, date, departure/arrival times, duration, a colored status dot, a status label (e.g. "On Time"), and a price starting with "$"
|
||||
|
||||
#### A2UI Dynamic — Sales Dashboard (agent tool `generate_a2ui`)
|
||||
|
||||
- [ ] Click "Sales Dashboard (A2UI Dynamic)"; within 30s verify a dynamic dashboard surface renders containing total-revenue metric, new-customers metric, conversion-rate metric, a pie chart (revenue by category), and a bar chart (monthly sales)
|
||||
|
||||
#### MCP App — Excalidraw Diagram
|
||||
|
||||
- [ ] Click "Excalidraw Diagram (MCP App)"; within 30s verify an Excalidraw embed renders a diagram with a router, 2 switches, and 4 computers (no console errors referencing the MCP server URL)
|
||||
|
||||
#### Open Generative UI — Calculator App
|
||||
|
||||
- [ ] Click "Calculator App (Open Generative UI)"; within 30s verify a sandboxed calculator UI renders with digit/operator buttons plus labeled metric shortcut buttons
|
||||
- [ ] Click a metric shortcut button; verify its value is inserted into the calculator display
|
||||
|
||||
#### Frontend Tool — Toggle Theme (`toggleTheme`)
|
||||
|
||||
- [ ] Click "Toggle Theme (Frontend Tools)"; verify the `html` element's `class` attribute toggles between containing `dark` and not containing `dark`, and the CopilotKit logo inverts via the `dark:invert` class
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no assistant response)
|
||||
- [ ] Send a ~500-character message; verify it wraps in-transcript without horizontal scroll or layout break
|
||||
- [ ] With the backend stopped, send a message; verify the UI surfaces a visible error path rather than hanging silently, and DevTools → Console shows no uncaught errors during any flow above
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds; plain-text response within 10 seconds
|
||||
- Controlled charts (pie/bar) render within 15 seconds of prompt; A2UI surfaces within 20–30 seconds
|
||||
- No UI layout breaks, no flash of unstyled content, no uncaught console errors
|
||||
- All 5 agent tools (`manage_todos`, `get_todos`, `query_data`, `search_flights`, `generate_a2ui`) are exercised by at least one check above
|
||||
@@ -0,0 +1,71 @@
|
||||
# QA: Chat Customization (CSS) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/chat-customization-css` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the neutral `sample_agent` graph
|
||||
- The demo wires `agent="chat-customization-css"` at `/api/copilotkit` (neutral assistant cell)
|
||||
- Note: the demo source contains no `data-testid` attributes. Checks below rely on CopilotKit built-in class names (`copilotKitChat`, `copilotKitMessages`, `copilotKitMessage`, `copilotKitUserMessage`, `copilotKitAssistantMessage`, `copilotKitInput`) scoped under the `.chat-css-demo-scope` wrapper defined in `theme.css`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/chat-customization-css`; verify the page renders within 3s with a single centered `<CopilotChat />` container inside a `.chat-css-demo-scope` wrapper (max-width 4xl, rounded corners via `rounded-2xl`)
|
||||
- [ ] Verify the chat input is visible with a textarea placeholder (default CopilotKit placeholder, e.g. "Type a message")
|
||||
- [ ] Send "Hello" and verify an assistant text response appears within 10s
|
||||
- [ ] Verify the input clears after send and a user-message bubble is appended to the transcript
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Theme Applied On Load (CopilotKit CSS Variables)
|
||||
|
||||
- [ ] In DevTools, inspect the `.chat-css-demo-scope` element and verify `getComputedStyle(el).getPropertyValue('--copilot-kit-primary-color').trim()` equals `#ff006e`
|
||||
- [ ] Verify `--copilot-kit-background-color` is `#fff8f0`, `--copilot-kit-secondary-color` is `#fde047`, and `--copilot-kit-separator-color` is `#ff006e`
|
||||
- [ ] Verify the `.copilotKitChat` background renders the themed cream color `#fff8f0` (not the default white/neutral)
|
||||
|
||||
#### User Message Bubble (hot pink gradient, serif, bold)
|
||||
|
||||
- [ ] Send "Hi there"; locate the newly rendered `.copilotKitMessage.copilotKitUserMessage` bubble
|
||||
- [ ] Verify its `background-image` computed style starts with `linear-gradient(135deg, rgb(255, 0, 110)` (i.e. `#ff006e`) and its text `color` is `rgb(255, 255, 255)`
|
||||
- [ ] Verify `font-family` contains `Georgia`, `font-weight` is `700`, and `border-radius` is `22px 22px 4px 22px`
|
||||
- [ ] Verify the bubble has a 2px solid border in `#ff6fa5` and a pink drop shadow (`box-shadow` includes `rgba(255, 0, 110, 0.35)`)
|
||||
|
||||
#### Assistant Message Bubble (amber, monospace, boxy)
|
||||
|
||||
- [ ] After the agent responds, locate the `.copilotKitMessage.copilotKitAssistantMessage` bubble
|
||||
- [ ] Verify its `background-color` computed style is `rgb(253, 224, 71)` (i.e. `#fde047`) and text `color` is `rgb(30, 27, 75)` (i.e. `#1e1b4b`)
|
||||
- [ ] Verify `font-family` contains `JetBrains Mono` / `Fira Code` / `Menlo` / `Consolas` (monospace stack)
|
||||
- [ ] Verify the bubble uses a boxy shape with `border-radius: 4px 22px 22px 22px`, a `2px solid rgb(30, 27, 75)` border, and a hard offset shadow `4px 4px 0 rgb(30, 27, 75)`
|
||||
|
||||
#### Input Area (cream background, dashed pink border, serif)
|
||||
|
||||
- [ ] Verify the `.copilotKitInput` element has `background-color: rgb(254, 243, 199)` (i.e. `#fef3c7`), a `3px dashed rgb(255, 0, 110)` border, and `border-radius: 18px`
|
||||
- [ ] Verify the inner `textarea` uses a `Georgia` serif font at `1.1rem`, with text color `#2c1810`
|
||||
- [ ] Verify the placeholder text renders in italic `#c2185b` (type-drain the textarea to expose the placeholder)
|
||||
|
||||
#### Theme Persists Across Message Rounds
|
||||
|
||||
- [ ] Send a second message ("Tell me a joke"); wait for the assistant reply
|
||||
- [ ] Verify both the new user bubble (pink gradient, serif) and the new assistant bubble (amber, monospace, boxy) retain the same computed styles as round 1 — no fallback to default CopilotKit theme between rounds
|
||||
- [ ] Verify the `.copilotKitMessages` container continues to use the `Georgia` serif font family and the `#fff8f0` background through scrolling
|
||||
|
||||
#### Chat Functions Identically To Default
|
||||
|
||||
- [ ] Type a multi-line message using Shift+Enter and verify Enter submits, Shift+Enter inserts a newline (same as a default `<CopilotChat />`)
|
||||
- [ ] Verify the agent's response streams token-by-token into the amber assistant bubble without the theme resetting mid-stream
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble added)
|
||||
- [ ] Send a ~500-character message; verify it wraps inside the pink user bubble without horizontal scroll, and the bubble still respects the hot-pink gradient + serif styling
|
||||
- [ ] With the backend stopped, send a message; verify a visible error path surfaces in the UI and DevTools → Console shows no uncaught errors caused by the themed styles
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds; plain-text response within 10 seconds
|
||||
- All eight `--copilot-kit-*` CSS variables from `theme.css` resolve on `.chat-css-demo-scope`
|
||||
- User bubbles: hot pink gradient, white bold serif text, asymmetric rounded corners, 2px pink border, shadow
|
||||
- Assistant bubbles: amber `#fde047` background, monospace dark text, boxy corners, 2px dark border, hard offset shadow
|
||||
- Input: cream background, 3px dashed pink border, serif font, italic pink placeholder
|
||||
- No flash of unstyled content between the default CopilotKit stylesheet and `theme.css`; no uncaught console errors
|
||||
@@ -0,0 +1,59 @@
|
||||
# QA: Chat Customization (Slots) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/chat-slots` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health` or `/api/copilotkit` GET); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the neutral `sample_agent` graph (registered to the `chat-slots` agent name)
|
||||
- Note: this demo DOES include `data-testid` attributes on every custom slot. Use them as the primary selectors. The underlying agent is the neutral "helpful, concise assistant" (no frontend tools, no agent tools) — this demo exercises frontend slot customization only.
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/chat-slots`; verify the page renders a centered chat surface (max-width 5xl, full viewport height) within 3s
|
||||
- [ ] Verify the custom welcome screen is visible (`data-testid="custom-welcome-screen"`), replacing the default welcome
|
||||
- [ ] Verify the nested welcomeMessage sub-slot renders inside the welcome screen (`data-testid="custom-welcome-message"`) with body text reading "Hover any region to see its slot path · click the badge to copy"
|
||||
- [ ] Verify the welcome card wraps the default chat `input` element and a `suggestionView` row beneath it (both passed in as props by CopilotChatView)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Welcome Screen Slot (`welcomeScreen`)
|
||||
|
||||
- [ ] Confirm the welcome card displays a hoverable SlotMarker badge (the gradient indigo/violet ring) — visually distinct from the default CopilotChat welcome
|
||||
- [ ] Confirm NO default CopilotChat welcome heading is rendered (the custom card fully replaces it)
|
||||
|
||||
#### Suggestions (`useConfigureSuggestions`)
|
||||
|
||||
- [ ] Verify two suggestion pills render in the `suggestionView` slot beneath the input with verbatim titles:
|
||||
- "Write a sonnet"
|
||||
- "Tell me a joke"
|
||||
- [ ] Click "Tell me a joke"; verify it sends the message "Tell me a short joke." and an assistant text response appears within 10s
|
||||
|
||||
#### Disclaimer Slot (`input.disclaimer`) — visible after first message
|
||||
|
||||
- [ ] After sending the first message, verify the custom disclaimer renders below the chat input (`data-testid="custom-disclaimer"`) containing:
|
||||
- a small badge reading "slot" (indigo background, lowercase bold)
|
||||
- body text "Custom disclaimer injected via `input.disclaimer`." (the phrase `input.disclaimer` is in monospace)
|
||||
- [ ] Verify the default CopilotChat disclaimer text (if any) is NOT present — the custom disclaimer replaces it
|
||||
|
||||
#### Assistant Message Slot (`messageView.assistantMessage`)
|
||||
|
||||
- [ ] After the assistant response arrives, verify the assistant message is wrapped in the custom container (`data-testid="custom-assistant-message"`) with:
|
||||
- an indigo-tinted card background (`bg-indigo-50/60` in light mode)
|
||||
- an indigo border (`border-indigo-200`)
|
||||
- a small absolute-positioned "slot" badge at the top-left corner (indigo-600 background, white uppercase bold text)
|
||||
- [ ] Verify the user message bubble is NOT wrapped in the custom container (user messages use the default styling)
|
||||
- [ ] Send a second prompt ("Write a one-line sonnet"); verify the second assistant response is also wrapped in the `custom-assistant-message` container
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no network request)
|
||||
- [ ] Send a ~500-character message; verify it wraps within the max-w-5xl container without horizontal scroll or layout break; the custom assistant-message card grows to fit the response
|
||||
- [ ] Verify DevTools → Console shows no uncaught errors or missing-prop warnings during any flow above
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat surface renders within 3 seconds with the custom welcome card visible
|
||||
- Assistant text response within 10 seconds; wrapped in the custom assistant-message slot on every turn
|
||||
- All three custom slots (`welcomeScreen`, `input.disclaimer`, `messageView.assistantMessage`) replace their defaults and are visually distinguishable via their "slot" badges / gradient styling
|
||||
- No UI layout breaks, no uncaught console errors
|
||||
@@ -0,0 +1,39 @@
|
||||
# QA: CLI Start — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Dashboard host accessible; the `cli-start` cell is an informational command cell (no `/demos/cli-start/` route)
|
||||
- Local machine with Node.js 18+, npm 9+, and Python 3.11+ available; network access to the npm registry and GitHub
|
||||
- Canonical starter for comparison: `showcase/integrations/langgraph-python/` in the CopilotKit repo
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Dashboard Cell
|
||||
|
||||
- [ ] Open the LangGraph (Python) provider view on the dashboard; locate the "CLI Start Command" cell
|
||||
- [ ] Verify the cell displays the exact command `npx copilotkit@latest init --framework langgraph-python`
|
||||
- [ ] If a copy-to-clipboard button is present, click it; verify clipboard contents equal the command above (paste into a text editor to confirm)
|
||||
|
||||
### 2. Scaffold a Fresh Project
|
||||
|
||||
- [ ] In a fresh, empty directory, run `npx copilotkit@latest init --framework langgraph-python`
|
||||
- [ ] Verify the CLI completes without errors and scaffolds a project tree that matches the canonical starter at `showcase/integrations/langgraph-python/` (same top-level files: `package.json`, `next.config.ts`, `langgraph.json`, `showcase.json`, `tsconfig.json`, `postcss.config.mjs`, `Dockerfile`, `entrypoint.sh`, `src/`)
|
||||
- [ ] Verify `package.json` references `@copilotkit/*` at version `2.0.0` or newer (matches `copilotkit_version` in the provider manifest)
|
||||
|
||||
### 3. Install + Boot
|
||||
|
||||
- [ ] Run `npm install` in the scaffolded directory; verify it completes with no error-level output
|
||||
- [ ] Start the Next.js dev server per the scaffolded project's README / `package.json` `dev` script (typically `npm run dev`); verify it binds and logs a local URL (e.g. `http://localhost:3000`)
|
||||
- [ ] Start the LangGraph backend per the scaffolded project's instructions (typically `langgraph dev` using `langgraph.json`); verify it binds without error
|
||||
- [ ] Open the local URL in a browser; verify the starter demo renders (the "Sales Dashboard" surface per the manifest `starter.name`) and a basic chat round-trip works
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
- [ ] Re-run the CLI in a non-empty directory; verify it either refuses or prompts before overwriting (does not silently clobber)
|
||||
- [ ] Verify the scaffolded project has no lockfile merge conflicts and no unresolved peer-dependency errors in `npm install` output
|
||||
|
||||
## Expected Results
|
||||
|
||||
- CLI command copies cleanly and runs without interactive blockers (aside from any documented prompts)
|
||||
- Scaffolded tree matches the canonical starter shape
|
||||
- `npm install` completes without error; dev server + LangGraph backend boot; starter demo is interactive
|
||||
@@ -0,0 +1,72 @@
|
||||
# QA: Declarative Generative UI (A2UI — Dynamic Schema) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/declarative-gen-ui` on the dashboard host
|
||||
- Agent backend is healthy (`/api/copilotkit/health` or the host's `/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `a2ui_dynamic` graph (registered as agent name `declarative-gen-ui` — see `src/app/api/copilotkit-declarative-gen-ui/route.ts`)
|
||||
- The demo plays a sales analyst for the fictional **Vantage Threads** company. The dataset and per-question composition rules are registered as agent context in `src/app/demos/declarative-gen-ui/sales-context.ts` — surfaces should reflect those numbers ($4.2M Q2 revenue, 4 regions, 5 reps, 3 at-risk accounts, Meridian Apparel Group as top account)
|
||||
- Each custom renderer carries a stable `data-testid`: `declarative-card`, `declarative-metric`, `declarative-pie-chart`, `declarative-bar-chart`, `declarative-status-badge`, `declarative-data-table`, `declarative-info-row` (see `src/app/demos/declarative-gen-ui/a2ui/renderers.tsx`)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/declarative-gen-ui`; verify the page renders within 3s and a single `CopilotChat` pane is centered (max-width ~896px, rounded-2xl, full-height)
|
||||
- [ ] Verify the chat is wired to `runtimeUrl="/api/copilotkit-declarative-gen-ui"` and `agent="declarative-gen-ui"` (DevTools → Network: sending a message hits that endpoint, not `/api/copilotkit`)
|
||||
- [ ] Verify all 4 suggestion pills are visible with verbatim titles:
|
||||
- "Show my sales dashboard"
|
||||
- "Team performance"
|
||||
- "Anything at risk?"
|
||||
- "Top account details"
|
||||
- [ ] Verify no pill mentions a chart type — chart steering lives in the system prompt, not the user prompt (OSS-136)
|
||||
- [ ] Send "Hello" and verify an assistant text response appears within 10s (no A2UI surface rendered for plain text)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Catalog Wiring (provider `a2ui={{ catalog: myCatalog }}`)
|
||||
|
||||
- [ ] DevTools → Network: on first tool-driven response, verify the response stream contains an `a2ui_operations` container with `catalogId: "declarative-gen-ui-catalog"` (matches `createCatalog(..., { catalogId: "declarative-gen-ui-catalog" })` in `a2ui/catalog.ts`)
|
||||
|
||||
#### Hero Pill — Composed Sales Dashboard
|
||||
|
||||
- [ ] Click "Show my sales dashboard"; within 60s verify ONE composed surface renders containing ALL of (no surrounding Card — the charts carry their own card chrome):
|
||||
- a bare row of 4 `declarative-metric` KPI tiles (uppercase label, large value, trend arrow with delta — green `↑` for up, red `↓` for down, e.g. "↑ 12% QoQ")
|
||||
- a `declarative-pie-chart` (recharts donut mirroring beautiful-chat: `innerRadius` 40 / `outerRadius` 80, one `.recharts-pie-sector` per slice, tooltip on hover, no legend) showing revenue by region
|
||||
- a `declarative-bar-chart` (recharts, height 200, single blue `#3b82f6` bars with rounded tops, dashed grid) showing monthly revenue
|
||||
- [ ] Verify the surface is a single composed dashboard, NOT a lonely single widget — this is the regression OSS-136 was filed about
|
||||
- [ ] Verify the pie slices cycle through the shared palette (`#3b82f6`, `#8b5cf6`, `#ec4899`, `#f59e0b`, `#10b981`, `#6366f1`) and bars are uniform blue `#3b82f6` — identical chrome to beautiful-chat's sales dashboard (12px-radius cards, 20px padding, soft shadow)
|
||||
- [ ] Verify the chat reply text beneath the surface is one short sentence (per `SYSTEM_PROMPT`: let the UI do the talking)
|
||||
- [ ] Verify metric numbers match the Vantage Threads dataset (revenue $4.2M, 186 new customers, 31% win rate, $22.6k avg deal)
|
||||
|
||||
#### Team Performance — DataTable
|
||||
|
||||
- [ ] Click "Team performance"; within 60s verify a `declarative-data-table` renders inside a Card: uppercase column headers (Rep / Attainment / Pipeline), one body row per rep (5 reps, Dana Whitfield through Elena Vasquez), tabular numerals
|
||||
- [ ] Verify a quota-attainment BarChart renders alongside the table (dashboardy, not a bare table); no StatusBadge or InfoRow
|
||||
|
||||
#### At Risk — StatusBadge Cards
|
||||
|
||||
- [ ] Click "Anything at risk?"; within 60s verify a risk panel: a KPI strip of Metric tiles (ARR at risk $615k, accounts at risk 3, biggest exposure Northwind $340k) above three side-by-side account Cards (Northwind Retail, Cascadia Outfitters, Atlas Goods), each with a content-sized `declarative-status-badge` (error = high severity, warning = medium) and a one-line reason + recommended next action
|
||||
- [ ] Verify no charts or tables render for this pill
|
||||
|
||||
#### Top Account — InfoRow Facts
|
||||
|
||||
- [ ] Click "Top account details"; within 60s verify a Card for Meridian Apparel Group with at least 3 `declarative-info-row` label/value rows (Owner, Region, ARR, Renewal, Last contact), each separated by a 1px bottom border
|
||||
- [ ] Verify a product-line PieChart renders next to the fact card (grounded in Meridian's product mix); no DataTable or StatusBadge
|
||||
|
||||
#### Cross-Pill Differentiation (mirrors the D5 probe)
|
||||
|
||||
- [ ] Run all 4 pills in one conversation; verify each pill mounts its distinguishing component fresh (the D5 probe `showcase/harness/src/probes/scripts/d5-gen-ui-declarative.ts` asserts a newly-mounted testid per pill — leftovers from earlier pills must not be the only match)
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message; verify it is a no-op (no user bubble, no assistant response)
|
||||
- [ ] Send "What is 2+2?"; verify the agent replies in plain text without invoking `generate_a2ui` (no `a2ui_operations` in the response stream, no surface rendered)
|
||||
- [ ] DevTools → Console: walk through all flows above; verify no uncaught errors, no React error #31, and no A2UI render-error banners ("Cannot create component root without a type", "Catalog not found")
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3s; plain-text response within 10s; A2UI surfaces render within 60s of prompt (secondary-LLM pass can be slow on cold start)
|
||||
- `generate_a2ui` is called exactly once per surface-producing prompt; result contains a valid `a2ui_operations` container with `catalogId: "declarative-gen-ui-catalog"`
|
||||
- The hero pill produces a composed dashboard (4 KPI tile metrics + 1 PieChart + 1 BarChart in one surface, with NO surrounding Card per OSS-136); pills 2-4 produce their distinguishing component (data-table / status-badge / info-row)
|
||||
- Numbers are consistent with the Vantage Threads dataset across all four pills
|
||||
- No UI layout breaks, no flash of unstyled content, no uncaught console errors
|
||||
@@ -0,0 +1,69 @@
|
||||
# QA: Declarative UI — Hashbrown — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed at `/demos/declarative-hashbrown`
|
||||
- Railway service `showcase-langgraph-python-production` healthy
|
||||
- `OPENAI_API_KEY` set in the Railway environment
|
||||
- `@hashbrownai/core` + `@hashbrownai/react` installed in the package
|
||||
- `byoc_hashbrown` graph registered in `langgraph.json`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Page load
|
||||
|
||||
- [ ] Navigate to `/demos/declarative-hashbrown`
|
||||
- [ ] Header "Declarative UI: Hashbrown" visible
|
||||
- [ ] Short description mentioning `@hashbrownai/react` visible
|
||||
- [ ] Chat composer visible at the bottom of the chat area
|
||||
- [ ] 3 suggestion pills visible inside the composer with labels:
|
||||
"Sales dashboard", "Revenue by category", "Expense trend"
|
||||
- [ ] No red console errors (amber hydration warnings tolerated)
|
||||
|
||||
### 2. Sales dashboard suggestion
|
||||
|
||||
- [ ] Click the "Sales dashboard" pill
|
||||
- [ ] The prompt is dispatched automatically (useConfigureSuggestions sends
|
||||
the message on pill click)
|
||||
- [ ] Within 45 seconds, at least one MetricCard (`data-testid="metric-card"`)
|
||||
renders in the transcript
|
||||
- [ ] Within 45 seconds, at least one chart
|
||||
(`data-testid="bar-chart"` or `data-testid="pie-chart"`) renders
|
||||
- [ ] Rendered content streams progressively — partial UI appears before the
|
||||
full response completes (optional visual check)
|
||||
|
||||
### 3. Revenue by category
|
||||
|
||||
- [ ] Click "Revenue by category"
|
||||
- [ ] Within 45s, a pie chart (`data-testid="pie-chart"`) renders
|
||||
- [ ] Legend shows at least 4 segments with readable labels and values
|
||||
|
||||
### 4. Expense trend
|
||||
|
||||
- [ ] Click "Expense trend"
|
||||
- [ ] Within 45s, a bar chart (`data-testid="bar-chart"`) renders
|
||||
- [ ] Chart has at least 3 bars with month-like labels
|
||||
|
||||
### 5. Free-form prompt
|
||||
|
||||
- [ ] Type "Show me revenue trends for the last six months" and press Enter
|
||||
- [ ] Verify at least one catalog component renders (metric, chart, or deal)
|
||||
|
||||
### 6. Multi-turn
|
||||
|
||||
- [ ] After a first render completes, send a follow-up prompt
|
||||
(e.g. "Now break it down by region")
|
||||
- [ ] A new render appears alongside prior renders in the transcript
|
||||
|
||||
### 7. Error handling
|
||||
|
||||
- [ ] Empty send is a no-op (button stays disabled)
|
||||
- [ ] Console remains clean during successful flows
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Suggestion pills produce a hashbrown render within 45 seconds
|
||||
- Streaming renders assemble progressively as JSON chunks arrive
|
||||
- No uncaught errors; no `useHashBrownKit must be used within
|
||||
HashBrownDashboard` errors
|
||||
- Multi-turn works without clearing prior renders
|
||||
@@ -0,0 +1,56 @@
|
||||
# QA: BYOC json-render — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed at `/demos/declarative-json-render`.
|
||||
- Railway backend healthy (`showcase-langgraph-python-production.up.railway.app`).
|
||||
- `OPENAI_API_KEY` and `LANGGRAPH_DEPLOYMENT_URL` configured on the Next.js app.
|
||||
- `@json-render/core` + `@json-render/react` present in `package.json` (pinned to `0.18.0`).
|
||||
- `byoc_json_render` graph registered in `langgraph.json`.
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Page load
|
||||
|
||||
- [ ] Navigate to `/demos/declarative-json-render`.
|
||||
- [ ] Chat composer is visible.
|
||||
- [ ] Three suggestion pills appear with titles: "Sales dashboard", "Revenue by category", "Expense trend".
|
||||
- [ ] No console errors.
|
||||
|
||||
### 2. Sales dashboard suggestion
|
||||
|
||||
- [ ] Click the "Sales dashboard" suggestion.
|
||||
- [ ] Within 60 seconds, a `data-testid="json-render-root"` wrapper appears in the assistant bubble.
|
||||
- [ ] A `data-testid="metric-card"` renders inside the wrapper.
|
||||
- [ ] A chart (`data-testid="bar-chart"` or `data-testid="pie-chart"`) renders inside the wrapper.
|
||||
- [ ] No raw JSON text is shown once rendering finishes — the streaming JSON is replaced by components.
|
||||
|
||||
### 3. Revenue by category
|
||||
|
||||
- [ ] Click the "Revenue by category" suggestion.
|
||||
- [ ] Within 60 seconds, a `data-testid="pie-chart"` renders with multiple category slices + legend.
|
||||
|
||||
### 4. Expense trend
|
||||
|
||||
- [ ] Click the "Expense trend" suggestion.
|
||||
- [ ] Within 60 seconds, a `data-testid="bar-chart"` renders with month labels.
|
||||
|
||||
### 5. Free-form prompt
|
||||
|
||||
- [ ] Type "Show me a metric for quarterly revenue" and send.
|
||||
- [ ] Verify at least one `metric-card` renders; no console errors.
|
||||
|
||||
### 6. Multi-turn
|
||||
|
||||
- [ ] After a previous render is visible, send a follow-up prompt ("Now break that down by region").
|
||||
- [ ] A new assistant message appears with a new json-render rendering — prior renders stay in the transcript.
|
||||
|
||||
### 7. Malformed output handling
|
||||
|
||||
- [ ] If the agent ever replies with non-JSON text (force it by asking "tell me a joke"), the chat falls back to rendering that raw text via the default assistant bubble. No crash, no stuck spinner.
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Suggestion renders land within 60 seconds. Budget is slightly higher than the hashbrown demo because a JSON `{ root, elements }` spec is more verbose than hashbrown's token stream.
|
||||
- No uncaught errors in the console.
|
||||
- Streaming falls back to plain text until the JSON parses, then swaps to rendered components.
|
||||
@@ -0,0 +1,74 @@
|
||||
# QA: Frontend Tools (Async) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/frontend-tools-async` on the dashboard host
|
||||
- Agent backend is healthy (`/api/copilotkit` GET returns `langgraph_status: "reachable"`); `OPENAI_API_KEY` is set; `LANGGRAPH_DEPLOYMENT_URL` points at a deployment exposing the `frontend_tools_async` graph (registered under agent name `frontend-tools-async`)
|
||||
- Backend `frontend_tools_async.py` registers NO server-side tools; the frontend registers exactly ONE tool via `useFrontendTool`: **`query_notes`** (parameter: `keyword: string`)
|
||||
- The async handler sleeps 500ms (simulated client-side DB latency) then filters an in-memory `NOTES_DB` of 7 hard-coded notes, returning up to 5 matches against `title`, `excerpt`, or `tags` (case-insensitive). The tool has a custom `render` that mounts `NotesCard`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/frontend-tools-async`; verify the `CopilotChat` panel renders centered (max width 4xl, rounded-2xl corners) within 3s
|
||||
- [ ] Verify the input placeholder "Type a message" is visible
|
||||
- [ ] Send "Hello"; verify the agent responds with plain text within 10s and does NOT invoke `query_notes`
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Suggestion Pills
|
||||
|
||||
- [ ] Verify all three suggestion pills are visible with verbatim titles:
|
||||
- "Find project-planning notes"
|
||||
- "Search for 'auth'"
|
||||
- "What do I have about reading?"
|
||||
- [ ] Click "Find project-planning notes"; verify the prompt "Find my notes about project planning." is sent
|
||||
|
||||
#### `query_notes` — Async Handler Loading State
|
||||
|
||||
- [ ] After triggering the `query_notes` flow (via pill or typing "Find my notes about planning"), verify within 10s a `data-testid="notes-card"` element renders in the transcript
|
||||
- [ ] While the handler's 500ms sleep is in flight (tool status ≠ `complete`), verify the card's header shows:
|
||||
- [ ] An uppercase "Notes DB" label
|
||||
- [ ] A heading `data-testid="notes-keyword"` reading `Matching "<keyword>"` where `<keyword>` is the agent's chosen search term (e.g. `planning`, `project planning`)
|
||||
- [ ] The subtext "Querying local notes DB..."
|
||||
- [ ] A "..." placeholder glyph (not the 📓 book emoji)
|
||||
|
||||
#### `query_notes` — Resolved State (Simulated DB Query)
|
||||
|
||||
- [ ] After the 500ms sleep resolves, verify the card's loading state ends within 2s:
|
||||
- [ ] The placeholder glyph flips from "..." to "📓"
|
||||
- [ ] The subtext shows "`N` match" or "`N` matches" (singular when N=1)
|
||||
- [ ] A `data-testid="notes-list"` `<ul>` renders (assuming N > 0)
|
||||
- [ ] Verify the list contains between 1 and 5 `<li>` entries, each with `data-testid="note-<id>"` (IDs from `n1`–`n7`)
|
||||
- [ ] For the prompt "Find my notes about project planning", verify the returned notes include at least `note-n1` (Q2 project planning kickoff) and `note-n5` (Project planning retrospective notes)
|
||||
- [ ] Verify each note row renders title (bold), excerpt (grey small text), and tag pills (uppercase, rounded-full)
|
||||
|
||||
#### Round-Trip — Agent Consumes Async Handler Result
|
||||
|
||||
- [ ] After the card renders, verify the agent emits a follow-up assistant text message within 10s summarizing the matches
|
||||
- [ ] Verify the summary references at least one note title or tag from the `notes-card` (confirms the agent awaited the async handler's resolved value, not just fired-and-forgot)
|
||||
- [ ] Ask a follow-up like "Which of those is about onboarding?"; verify the agent references note `n1`'s excerpt ("new onboarding flow") — proving the previous tool result is retained in context
|
||||
|
||||
#### Zero-Match Branch — Notes Card Empty State
|
||||
|
||||
- [ ] Send "Search my notes for xyzzy-nonsense-keyword"
|
||||
- [ ] Verify a `data-testid="notes-card"` renders with heading `Matching "xyzzy-nonsense-keyword"` (or close variant)
|
||||
- [ ] Verify the card shows italic grey text "No notes matched." (no `notes-list` element)
|
||||
- [ ] Verify the agent's follow-up text says no matches were found and offers to try a different keyword
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message; verify it is a no-op (no notes-card, no user bubble)
|
||||
- [ ] Send a ~500-character unrelated prompt; verify the agent responds without calling `query_notes`
|
||||
- [ ] Trigger two `query_notes` invocations in quick succession (e.g. "Find auth notes" then immediately "Find reading notes"); verify both resolve independently and render two separate `notes-card` instances in the transcript
|
||||
- [ ] Open DevTools → Console; verify no uncaught errors, no Zod parse failures, no unresolved-Promise warnings
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds; `notes-card` loading state appears within 10 seconds of prompt
|
||||
- Async handler's 500ms sleep is observable — the loading state ("Querying local notes DB...") is visible before resolution
|
||||
- After resolution, the `notes-list` renders with the correct subset of `NOTES_DB` matching the keyword
|
||||
- Agent's follow-up reply demonstrably uses the resolved notes array (round-trip verified)
|
||||
- Zero-match prompts render the empty-state branch, not a broken card
|
||||
- No uncaught console errors; handler Promise always resolves; no layout breaks
|
||||
@@ -0,0 +1,59 @@
|
||||
# QA: Frontend Tools (In-App Actions) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/frontend-tools` on the dashboard host
|
||||
- Agent backend is healthy (`/api/copilotkit` GET returns `langgraph_status: "reachable"`); `OPENAI_API_KEY` is set; `LANGGRAPH_DEPLOYMENT_URL` points at a deployment exposing the `frontend_tools` graph (registered under agent name `frontend_tools`)
|
||||
- The backend `frontend_tools.py` registers NO server-side tools — the agent forwards the frontend tool schema at runtime; the browser owns the handler
|
||||
- Frontend registers exactly ONE tool via `useFrontendTool`: **`change_background`** (parameter: `background: string` — a CSS background value, colors or gradients). Handler sets local React state and returns `{ status: "success", message: "Background changed to …" }`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/frontend-tools`; verify the page loads within 3s
|
||||
- [ ] Verify the background container (`data-testid="background-container"`) is visible, full-screen, flex-centered
|
||||
- [ ] Verify the initial background value is `var(--copilot-kit-background-color)` (the theme default) — inspect the container's inline `style.background`
|
||||
- [ ] Verify the `CopilotChat` panel renders centered (max width 4xl, rounded-2xl corners) inside the background container
|
||||
- [ ] Send "Hello"; verify the agent responds with plain text within 10s and does NOT call `change_background`
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Suggestion Pills
|
||||
|
||||
- [ ] Verify the "Change background" suggestion pill is visible above the input
|
||||
- [ ] Verify the "Sunset theme" suggestion pill is visible
|
||||
- [ ] Click the "Change background" pill; verify it sends the verbatim prompt "Change the background to a blue-to-purple gradient."
|
||||
|
||||
#### `change_background` — Successful Invocation
|
||||
|
||||
- [ ] After clicking the "Change background" pill (or typing "Change the background to a linear gradient from indigo to pink"), verify within 15s that the agent invokes the `change_background` tool
|
||||
- [ ] Verify the `data-testid="background-container"` element's inline `style.background` mutates from `var(--copilot-kit-background-color)` to a CSS gradient string containing `linear-gradient` (or `radial-gradient`)
|
||||
- [ ] Verify the change is visually reflected (color/gradient swap is instant, no flash)
|
||||
|
||||
#### Round-Trip — Agent References Tool Return Value
|
||||
|
||||
- [ ] Immediately after the background changes, verify the agent emits a follow-up assistant message that acknowledges the change
|
||||
- [ ] Verify the follow-up message text references the new background value OR echoes the `"Background changed to <value>"` success message (confirming the agent consumed the handler's return payload, not just triggered the tool blindly)
|
||||
- [ ] Ask "What color is the background right now?"; verify the agent replies with a description consistent with the last `change_background` call's argument (demonstrates the agent received the handler's return string into context)
|
||||
|
||||
#### Second Invocation — State Persistence
|
||||
|
||||
- [ ] Ask "Change it to a sunset gradient with orange and pink"; verify the background mutates again to a new gradient
|
||||
- [ ] Verify the previous gradient is fully replaced (not layered)
|
||||
- [ ] Ask "Now make it solid dark blue"; verify `style.background` becomes a solid color value (e.g. `#00008b`, `darkblue`, or `rgb(...)`) — no gradient keyword
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send "Change the background to banana"; verify the agent either (a) calls `change_background` with a valid CSS fallback (e.g. `yellow`, `#FFE135`) and the container updates, or (b) declines politely without calling the tool — either is acceptable, but the UI must not crash
|
||||
- [ ] Send an empty message; verify it is a no-op
|
||||
- [ ] Send a ~500-character prompt unrelated to backgrounds; verify the agent responds normally without invoking `change_background`
|
||||
- [ ] Open DevTools → Console; verify no uncaught errors, no Zod parse failures, no `useFrontendTool` warnings
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds; first `change_background` invocation completes within 15 seconds of prompt
|
||||
- The `[data-testid="background-container"]` inline `style.background` updates in real time as the tool is invoked
|
||||
- Agent's follow-up reply demonstrably uses the handler's returned `message` field (round-trip verified)
|
||||
- Multiple sequential invocations replace the background cleanly with no visual artifacts
|
||||
- No uncaught console errors; no broken layouts
|
||||
@@ -0,0 +1,62 @@
|
||||
# QA: Agentic Generative UI — LangGraph (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,75 @@
|
||||
# QA: In-Chat HITL via useInterrupt — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/gen-ui-interrupt` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `interrupt_agent` graph
|
||||
- Note: The picker card is rendered INLINE inside the chat transcript via `useInterrupt({ renderInChat: true })`, wired to langgraph's `interrupt()` primitive from the backend `schedule_meeting` tool.
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/gen-ui-interrupt`; verify the page renders within 3s with the `CopilotChat` centered in a `max-w-4xl` container filling full viewport height, with rounded (`rounded-2xl`) styling
|
||||
- [ ] Verify the `CopilotChat` input placeholder is visible and the transcript is empty on first load
|
||||
- [ ] Send "Hello" and verify the agent responds with a text-only reply (no time picker rendered — the agent only calls `schedule_meeting` when explicitly asked to book/schedule)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify both suggestion pills are visible with verbatim titles:
|
||||
- "Book a call with sales"
|
||||
- "Schedule a 1:1 with Alice"
|
||||
|
||||
#### Interrupt Trigger + Inline Render (useInterrupt low-level primitive)
|
||||
|
||||
- [ ] Click the "Book a call with sales" suggestion (or prompt equivalently)
|
||||
- [ ] Within 20s verify the agent invokes `schedule_meeting`, the backend hits `interrupt({topic, attendee})`, and a time-picker card renders INLINE inside the chat transcript with `data-testid="time-picker-card"`
|
||||
- [ ] Confirm the card IS a descendant of the chat transcript container (NOT portaled to `<body>`, unlike the `hitl-in-app` modal) — inspect in DevTools to verify the card sits between chat message bubbles
|
||||
- [ ] Verify the card header shows eyebrow "Book a call" and a topic heading reflecting the agent-supplied topic (e.g. contains "sales" / "pricing")
|
||||
- [ ] Verify the "Pick a time:" subheading and a 2x2 grid of exactly 4 slot buttons with the default labels:
|
||||
- "Tomorrow 10:00 AM"
|
||||
- "Tomorrow 2:00 PM"
|
||||
- "Monday 9:00 AM"
|
||||
- "Monday 3:30 PM"
|
||||
- [ ] Verify a "None of these work" ghost button is rendered below the slot grid
|
||||
|
||||
#### Pick-a-Slot Resume Path
|
||||
|
||||
- [ ] Click one of the four time slot buttons (e.g. "Monday 9:00 AM")
|
||||
- [ ] Verify the card immediately switches to the confirmed state: `data-testid="time-picker-picked"`, green-tinted border/background, and text "Booked for <chosen label>" with the label in bold
|
||||
- [ ] Verify all slot buttons disable (opacity reduced) and cannot be re-clicked
|
||||
- [ ] Verify the agent resumes within 10s and produces a chat reply confirming the meeting was scheduled for the chosen label (backend returns `"Meeting scheduled for {chosen_label}: {topic}"`)
|
||||
|
||||
#### Cancel Path
|
||||
|
||||
- [ ] Send a second prompt "Schedule a 1:1 with Alice next week to review Q2 goals."
|
||||
- [ ] Verify a fresh time-picker card renders inline (`data-testid="time-picker-card"`) — attendee line "With Alice" should appear under the topic
|
||||
- [ ] Click the "None of these work" button
|
||||
- [ ] Verify the card switches to `data-testid="time-picker-cancelled"` with the text "Cancelled — no time picked."
|
||||
- [ ] Verify the agent resumes and replies that the meeting was NOT scheduled / the user cancelled
|
||||
|
||||
#### Multi-Turn
|
||||
|
||||
- [ ] After completing either the pick or cancel path, send one more prompt "Book another call tomorrow morning"
|
||||
- [ ] Verify a new, independent time-picker card renders inline (previous card stays in its resolved state), the interrupt lifecycle repeats cleanly, and a second resume works end-to-end
|
||||
|
||||
#### Contract Check — Interrupt Is Low-Level
|
||||
|
||||
- [ ] Confirm only the tool-triggered path renders the picker: sending a plain conversational message ("What's the weather?") should NOT render a picker
|
||||
- [ ] Confirm no approval-dialog-style modal appears at any point (this demo is inline, not modal)
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message; verify it is a no-op
|
||||
- [ ] Attempt to double-click a slot button rapidly; verify only one selection is committed (button disables on first click)
|
||||
- [ ] Verify no uncaught console errors across any pick / cancel / multi-turn flow above
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds; plain-text response within 10 seconds
|
||||
- Time picker card renders inline in the chat within 20 seconds of a schedule/booking prompt
|
||||
- Picker resolves via either a slot button (emits `{chosen_time, chosen_label}`) or the "None of these work" button (emits `{cancelled: true}`); post-resolution the card is read-only
|
||||
- Agent resume produces a confirmation message that references the chosen slot label or the cancellation
|
||||
- No UI layout breaks, no uncaught console errors, no duplicate pickers from a single interrupt
|
||||
@@ -0,0 +1,56 @@
|
||||
# QA: Tool-Based Generative UI — LangGraph (Python)
|
||||
|
||||
The demo registers two `useComponent` renderers (`render_bar_chart`,
|
||||
`render_pie_chart`) on a centered `<CopilotChat>`. The agent emits
|
||||
chart-shaped tool calls and the frontend materializes them as Recharts
|
||||
SVG inside the assistant message bubble.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/gen-ui-tool-based`
|
||||
- Agent backend is healthy (`/api/health`)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Initial render
|
||||
|
||||
- [ ] Navigate to `/demos/gen-ui-tool-based`
|
||||
- [ ] Verify the chat composer mounts (`textarea` with placeholder
|
||||
matching `/message/i`)
|
||||
- [ ] Verify all three suggestion pills are visible
|
||||
(`data-testid="copilot-suggestion"`):
|
||||
- "Sales bar chart"
|
||||
- "Traffic pie chart"
|
||||
- "Market share"
|
||||
|
||||
### 2. Bar chart flow
|
||||
|
||||
- [ ] Click the "Sales bar chart" suggestion (or type "Show me a bar
|
||||
chart of monthly expenses")
|
||||
- [ ] Verify the assistant message bubble
|
||||
(`[data-testid="copilot-assistant-message"]`) renders, and its inner
|
||||
Recharts SVG is visible
|
||||
|
||||
### 3. Pie chart flow
|
||||
|
||||
- [ ] Click the "Traffic pie chart" or "Market share" suggestion
|
||||
- [ ] Verify the assistant message bubble renders an SVG (the donut
|
||||
pie) inside
|
||||
|
||||
### 4. Free-form chat
|
||||
|
||||
- [ ] Type "Hello" and press Enter
|
||||
- [ ] Verify the assistant streams back a text response in a fresh
|
||||
`[data-testid="copilot-assistant-message"]` bubble
|
||||
|
||||
### 5. Hygiene
|
||||
|
||||
- [ ] No console errors during normal usage
|
||||
- [ ] No layout breakage with a very long input
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat composer mounts within ~3 seconds
|
||||
- Suggestion pills render alongside an empty chat
|
||||
- Tool-driven chart bubbles materialize within ~30 seconds for short
|
||||
prompts; the SVG renders progressively as Recharts mounts
|
||||
@@ -0,0 +1,82 @@
|
||||
# QA: Headless Chat (Complete) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/headless-complete` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `headless_complete` graph (backend tools: `get_weather`, `get_stock_price`)
|
||||
- The demo wires `agent="headless-complete"` at `/api/copilotkit-mcp-apps` (shared with the mcp-apps cell) so the Excalidraw MCP server at `MCP_SERVER_URL || https://mcp.excalidraw.com` is available
|
||||
- Note: the only `data-testid` in the source is `headless-complete-messages` on the scrollable messages container in `message-list.tsx`. Other checks rely on verbatim text, role selectors, and Tailwind utility classes
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/headless-complete`; verify the page renders within 3s with a centered card (max-width 3xl, full-height) on a `bg-gray-50` background
|
||||
- [ ] Verify the custom header renders with `<h1>` text "Headless Chat (Complete)" and subtext "Built from scratch on useAgent — no CopilotChat."
|
||||
- [ ] Verify the scrollable messages container (`[data-testid="headless-complete-messages"]`) is present and shows the empty-state hint "Try weather, a stock, a highlighted note, or an Excalidraw sketch."
|
||||
- [ ] Verify the custom composer renders at the bottom: a `<textarea>` with placeholder "Type a message..." and a `<button type="submit">Send</button>` (disabled while textarea is empty)
|
||||
- [ ] Confirm there is no `.copilotKitChat`, `.copilotKitMessages`, or `.copilotKitMessage` element in the DOM — the cell is truly headless and does NOT render `<CopilotChatMessageView>` or `<CopilotChatAssistantMessage>`
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Custom Composer + Send/Stop Toggle
|
||||
|
||||
- [ ] Type "Hello"; verify the Send button enables (goes from `bg-[#DBDBE5]` disabled to `bg-[#010507]` active)
|
||||
- [ ] Press `Enter`; verify the message submits and the textarea clears; press `Shift+Enter` in a follow-up message and verify a newline is inserted without submitting
|
||||
- [ ] While the agent is running, verify the textarea becomes disabled (`bg-[#FAFAFC]` muted), its placeholder switches to "Agent is working...", and the right-hand button swaps from "Send" to a red `bg-[#FA5F67]` "Stop" button
|
||||
- [ ] Click "Stop" mid-stream; verify `copilotkit.stopAgent({ agent })` fires and the button reverts to "Send" once `isRunning` returns false
|
||||
|
||||
#### Message List + Bubbles (pure chrome)
|
||||
|
||||
- [ ] Send "Hello"; within 10s verify:
|
||||
- [ ] A user bubble renders right-aligned (`flex justify-end`), rounded with `rounded-2xl rounded-br-sm`, `bg-[#010507] text-white` at `max-w-[75%]`, text "Hello"
|
||||
- [ ] A typing indicator (small pulsing gray dot in a `bg-[#F0F0F4]` rounded bubble) appears while `isRunning` is true and BEFORE any assistant content has streamed
|
||||
- [ ] The assistant bubble renders left-aligned (`flex justify-start`), `rounded-2xl rounded-bl-sm`, `bg-[#F0F0F4] text-[#010507]` at `max-w-[85%]`, with the assistant's plain-text response inside a `whitespace-pre-wrap break-words` div
|
||||
- [ ] Verify the messages container auto-scrolls to the bottom on each content-length change (send a long prompt whose response exceeds the viewport — scroll position should track the last line)
|
||||
- [ ] Verify empty assistant messages (mid-stream before any text/tool call) do NOT flash an empty `bg-[#F0F0F4]` box — `AssistantBubble`'s `isEmpty` check suppresses them
|
||||
|
||||
#### Multi-Turn Conversation
|
||||
|
||||
- [ ] Send a second message ("What else can you do?"); verify the prior user+assistant pair remain in the transcript in chronological order and the new pair is appended below
|
||||
- [ ] Verify each assistant bubble is independently sized (does not collapse neighbors) and the auto-scroll follows the newest content
|
||||
|
||||
#### Tool Rendering — WeatherCard (`useRenderTool` + backend `get_weather`)
|
||||
|
||||
- [ ] Send "What's the weather in Tokyo?"; within 15s verify a WeatherCard in the assistant bubble with eyebrow "FETCHING WEATHER" (loading) → "WEATHER" (complete), location "Tokyo" (`text-sm font-semibold capitalize`), temperature "68°", conditions "Sunny", wrapper `bg-[#EDEDF5] border-[#DBDBE5] rounded-xl max-w-xs`
|
||||
|
||||
#### Tool Rendering — StockCard (`useRenderTool` + backend `get_stock_price`)
|
||||
|
||||
- [ ] Send "What's AAPL trading at right now?"; within 15s verify a StockCard with eyebrow "LOADING" → "STOCK", ticker "AAPL" (`font-mono font-semibold`), price "$189.42", change "▲ 1.27%" in green `text-[#189370]`
|
||||
|
||||
#### Frontend Tool Rendering — HighlightNote (`useComponent` / `highlight_note`)
|
||||
|
||||
- [ ] Send "Highlight 'meeting at 3pm' in yellow."; within 15s verify a HighlightNote with eyebrow "NOTE", verbatim text "meeting at 3pm", and yellow variant classes `bg-[#FFF388]/30 border-[#FFF388]`
|
||||
- [ ] Optionally request pink/green/blue and verify corresponding `COLOR_CLASSES` are applied
|
||||
|
||||
#### Wildcard Catch-all + MCP Apps Activity (`useDefaultRenderTool` + `useRenderActivityMessage`)
|
||||
|
||||
- [ ] Send "Use Excalidraw to sketch a simple system diagram."; within 30s verify:
|
||||
- [ ] The activity message renders inline as a sandboxed Excalidraw iframe (built-in `MCPAppsActivityRenderer`), proving the hand-rolled `useRenderActivityMessage` path in `use-rendered-messages.tsx`
|
||||
- [ ] Any ancillary tool-call (not `get_weather` / `get_stock_price` / `highlight_note`) gets a visible default card via `useDefaultRenderTool` — not silently dropped
|
||||
- [ ] DevTools → Console shows no errors referencing the MCP server URL
|
||||
|
||||
#### Reasoning + Suggestions
|
||||
|
||||
- [ ] If the agent emits any `role: "reasoning"` messages, verify each renders via the imported `CopilotChatReasoningMessage` leaf inside an assistant bubble (the only chat primitive imported in `use-rendered-messages.tsx`)
|
||||
- [ ] Four suggestion strings are registered via `useConfigureSuggestions` with `available: "always"` ("Weather in Tokyo", "AAPL stock price", "Highlight a note", "Sketch a diagram") — exercised by manually sending the matching prompts above
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Attempt to submit an empty textarea; verify the Send button is disabled and Enter is a no-op (no user bubble, no run)
|
||||
- [ ] While `isRunning` is true, verify additional keystrokes cannot trigger a second run (`handleSubmit`'s `if (!text || isRunning) return;` guard)
|
||||
- [ ] Send a ~500-character message; verify the user bubble wraps within its 75% max-width via `break-words` without horizontal scroll
|
||||
- [ ] Navigate away mid-run; verify the unmount cleanup (`ac.abort()` + `agent.detachActiveRun()`) does not produce an uncaught rejection in DevTools → Console (connect/run rejections are swallowed by design)
|
||||
- [ ] With the backend stopped, send a message; verify `console.error("headless-complete: runAgent failed", err)` is emitted but no uncaught exception leaks, and the Send/Stop UI recovers to the idle state
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page loads within 3 seconds; first plain-text response within 10 seconds
|
||||
- Tool renders (WeatherCard, StockCard, HighlightNote) surface within 15 seconds of the triggering prompt
|
||||
- Excalidraw MCP activity surface renders within 30 seconds
|
||||
- Full generative-UI weave is reconstructed without `<CopilotChatMessageView>` / `<CopilotChatAssistantMessage>`: assistant text + tool-call renders (per-tool + catch-all) + reasoning + activity messages all appear through the hand-rolled `useRenderedMessages` composition
|
||||
- No flash of empty assistant bubbles while streaming; no uncaught console errors during any flow above
|
||||
@@ -0,0 +1,67 @@
|
||||
# QA: Headless Chat (Simple) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/headless-simple` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the neutral `sample_agent` graph
|
||||
- The demo wires `agent="headless-simple"` at `/api/copilotkit` (neutral assistant cell)
|
||||
- Note: the demo source contains no `data-testid` attributes. Checks below rely on verbatim visible text, role/button selectors, and Tailwind utility-class structure
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/headless-simple`; verify the page renders within 3s with a centered card (max-width 4xl) on a `bg-gray-50` background
|
||||
- [ ] Verify the custom heading "Headless Chat (Simple)" is visible (an `<h1>` — NOT a `<CopilotChat />` primitive)
|
||||
- [ ] Verify the empty-state text "No messages yet. Say hi!" is rendered inside the message panel
|
||||
- [ ] Verify the custom composer renders below the message panel: a `<textarea>` with placeholder "Type a message. Ask me to 'show a card about cats'." and a disabled `<button>` labeled "Send"
|
||||
- [ ] Verify the Send button is disabled while the textarea is empty (attribute `disabled` present, visual `opacity-50`)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Custom Composer (not default CopilotChat)
|
||||
|
||||
- [ ] Confirm there is no `.copilotKitChat`, `.copilotKitInput`, or `.copilotKitMessages` element in the DOM — the demo is built directly on `useAgent` and does not render `<CopilotChat />`
|
||||
- [ ] Type "Hello" in the textarea; verify the Send button becomes enabled (no longer `disabled`) and its background is the solid blue utility `bg-blue-600`
|
||||
- [ ] Press `Enter` (without Shift); verify the message submits, the textarea clears, and the user bubble appears right-aligned with `bg-blue-600 text-white` rounded styling and max-width 80%
|
||||
- [ ] Type another message and press `Shift+Enter`; verify a newline is inserted inside the textarea and the message is NOT submitted
|
||||
|
||||
#### Send → Receive → Render (minimal round-trip)
|
||||
|
||||
- [ ] Send "Hi"; verify within 10s:
|
||||
- [ ] A user bubble appears (right-aligned, blue, verbatim "Hi")
|
||||
- [ ] A transient "Agent is thinking..." text indicator appears while `agent.isRunning` is true
|
||||
- [ ] An assistant bubble appears left-aligned with `bg-gray-100 text-gray-900` rounded styling, max-width 90%, containing the assistant's text response
|
||||
- [ ] After the response settles, verify the "Agent is thinking..." indicator is gone (`agent.isRunning` flipped false) and the Send button re-enables when the textarea has content
|
||||
|
||||
#### Frontend Tool Rendering via `useComponent` (`show_card`)
|
||||
|
||||
- [ ] Send "Show a card about cats" (or "show a card titled Cats with a body about cats")
|
||||
- [ ] Within 15s verify the assistant invokes the `show_card` frontend tool and a `ShowCard` renders inline inside the assistant bubble area with:
|
||||
- [ ] A bold title (e.g. "Cats") in `font-semibold text-gray-900`
|
||||
- [ ] A body paragraph in `text-sm text-gray-700` with `whitespace-pre-wrap`
|
||||
- [ ] The card is wrapped in a white rounded container with `border border-gray-300` and a small shadow
|
||||
- [ ] Verify the tool call was routed through `copilotkit.runAgent({ agent })` (frontend tools registered via `useComponent` reach the agent) — confirm by the card rendering at all; if the route had used `agent.runAgent()` directly the card would not appear
|
||||
|
||||
#### Multi-Turn
|
||||
|
||||
- [ ] Send a second message (e.g. "Thanks!"); verify the user bubble is appended below the prior assistant content and a second assistant bubble appears without clearing or rearranging prior messages
|
||||
- [ ] Verify the message list preserves chronological order (oldest at top, newest at bottom) and the empty-state "No messages yet. Say hi!" text is gone after the first send
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] With the textarea empty or whitespace-only, confirm Enter is a no-op — no user bubble appears, no "Agent is thinking..." indicator
|
||||
- [ ] While `agent.isRunning` is true (long response streaming), verify:
|
||||
- [ ] The Send button is `disabled`
|
||||
- [ ] Pressing Enter in the textarea is a no-op (the `send()` guard `if (!text || agent.isRunning) return;` blocks concurrent sends)
|
||||
- [ ] Send a ~500-character message; verify the user bubble wraps within its 80% max-width without horizontal scroll and the layout does not break
|
||||
- [ ] With the backend stopped, send a message; verify the promise rejection is swallowed silently (`.catch(() => {})`) and no uncaught error is surfaced in DevTools → Console, but the user bubble remains and `agent.isRunning` eventually returns false
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page loads within 3 seconds; first assistant response within 10 seconds; `show_card` renders within 15 seconds of the triggering prompt
|
||||
- Custom composer (textarea + Send button) renders — no default CopilotChat DOM surfaces
|
||||
- User bubbles: right-aligned, `bg-blue-600 text-white`, max-width 80%
|
||||
- Assistant bubbles: left-aligned, `bg-gray-100 text-gray-900`, max-width 90%
|
||||
- Frontend `useComponent` tool (`show_card`) renders inline via `useRenderToolCall` inside the assistant message
|
||||
- No uncaught console errors during any flow above
|
||||
@@ -0,0 +1,73 @@
|
||||
# QA: In-App Human in the Loop — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/hitl-in-app` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `hitl_in_app` graph
|
||||
- Note: Unlike the in-chat HITL demo, the approval UI here is an app-level modal portal'd to `document.body` and is NOT a child of the chat transcript.
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/hitl-in-app`; verify the page renders within 3s with the "Support Inbox" panel filling the viewport and the `CopilotPopup` open by default in the bottom-right corner
|
||||
- [ ] Verify the left header shows the eyebrow "Support Inbox", heading "Open tickets", and the instruction text mentioning "approval dialog here in the app — outside the chat"
|
||||
- [ ] Verify exactly 3 ticket cards render with test ids `ticket-12345`, `ticket-12346`, `ticket-12347`, each showing the customer name (Jordan Rivera / Priya Shah / Morgan Lee), subject line, and status pill ("Open" green, "Escalating" amber)
|
||||
- [ ] Verify ticket #12345 displays "Disputed amount: $50.00"
|
||||
- [ ] Verify the `CopilotPopup` chat input placeholder is visible (popup opens by default) and no approval dialog is rendered on initial load
|
||||
- [ ] Send "Hello" and verify the agent responds within 10s
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify all 3 suggestion pills are visible with verbatim titles:
|
||||
- "Approve refund for #12345"
|
||||
- "Downgrade plan for #12346"
|
||||
- "Escalate ticket #12347"
|
||||
|
||||
#### Approval Flow — Approve Path (useFrontendTool async handler)
|
||||
|
||||
- [ ] Click the "Approve refund for #12345" suggestion (or type the equivalent prompt)
|
||||
- [ ] Within 15s verify an approval modal appears with `data-testid="approval-dialog-overlay"` (fullscreen fixed backdrop with `backdrop-blur-sm`) and `data-testid="approval-dialog"` (centered card with `role="dialog"` and `aria-modal="true"`)
|
||||
- [ ] Verify the modal is rendered at the document root (portaled via `createPortal(content, document.body)`) — confirm in DevTools that `approval-dialog-overlay` is a direct descendant of `<body>`, NOT nested inside the `CopilotPopup` container
|
||||
- [ ] Verify the modal shows the eyebrow "Action requires your approval", a heading containing the action summary (with concrete numbers such as "$50" and "#12345"), and optional context block below
|
||||
- [ ] Verify the textarea `data-testid="approval-dialog-reason"` is present with placeholder "Add a short note the assistant will see…"
|
||||
- [ ] Type a short note (e.g. "Verified duplicate charge") into the reason textarea
|
||||
- [ ] Click the `data-testid="approval-dialog-approve"` button (labeled "Approve")
|
||||
- [ ] Verify the modal closes immediately and does NOT re-open
|
||||
- [ ] Verify the agent resumes within 10s and replies in the chat with a one/two-sentence confirmation acknowledging the action is being processed (should reference the refund / #12345)
|
||||
|
||||
#### Approval Flow — Reject Path
|
||||
|
||||
- [ ] In a fresh conversation, click "Downgrade plan for #12346"
|
||||
- [ ] Verify the approval modal re-opens (same testids as above) with a heading referencing Priya Shah / #12346 / Starter plan
|
||||
- [ ] Type a rejection reason (e.g. "Customer must confirm in writing first")
|
||||
- [ ] Click `data-testid="approval-dialog-reject"` (labeled "Reject")
|
||||
- [ ] Verify the modal closes immediately
|
||||
- [ ] Verify the agent's reply acknowledges the rejection in one or two sentences, reflects the rejection reason back, and does NOT claim the downgrade was performed
|
||||
|
||||
#### Empty-Reason Path
|
||||
|
||||
- [ ] Trigger the approval flow a third time via "Escalate ticket #12347"
|
||||
- [ ] Leave the reason textarea empty and click Approve
|
||||
- [ ] Verify the modal closes and the agent still proceeds with the action (reason is optional — omitted when empty)
|
||||
|
||||
#### Modal Is Outside the Chat (Contract Check)
|
||||
|
||||
- [ ] While the modal is open, verify the chat transcript inside the popup is still scrollable / visible and does NOT contain an inline copy of the approval UI
|
||||
- [ ] Confirm that closing the modal via Approve/Reject is the only resolution path — there is no inline "approve" button rendered inside a chat bubble
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message; verify it is a no-op (no user bubble, no assistant response)
|
||||
- [ ] Trigger the approval flow and dismiss via Approve without touching the reason field — verify no console errors and the promise resolves
|
||||
- [ ] Verify no uncaught console errors during any approve / reject cycle above
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds; initial agent response within 10 seconds
|
||||
- Approval modal appears within 15 seconds of the triggering prompt
|
||||
- Modal is portaled to `<body>` (NOT nested in the chat) and closes on Approve/Reject
|
||||
- Approve path: agent acknowledges and proceeds; Reject path: agent acknowledges and stops, reflecting the reason when provided
|
||||
- No UI layout breaks, no uncaught console errors, no stuck-open modal
|
||||
@@ -0,0 +1,57 @@
|
||||
# QA: Human in the Loop — LangGraph (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,21 @@
|
||||
# QA: Interrupt (Headless) — LangGraph (Python)
|
||||
|
||||
> Stub — authored for column completeness. This is a testing-kind demo
|
||||
> (see `kind: "testing"` in feature-registry.json) and does not warrant a
|
||||
> full manual checklist.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/interrupt-headless
|
||||
- [ ] Send a scheduling prompt (e.g. "Book an intro call with sales") and verify a time-slot picker popup appears in the left app surface (not in the chat)
|
||||
- [ ] Click one of the time-slot buttons and verify the popup disappears and the agent confirms the booking back in the chat
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page loads without errors
|
||||
- Interrupt resolves via the plain button grid (no `useInterrupt` render prop, no in-chat picker) and the agent continues the run with the picked slot
|
||||
@@ -0,0 +1,62 @@
|
||||
# QA: MCP Apps — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/mcp-apps` on the dashboard host
|
||||
- Agent backend is healthy; `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `mcp_apps` graph (registered as agent name `mcp-apps` — see `src/app/api/copilotkit-mcp-apps/route.ts`)
|
||||
- MCP server target: the public Excalidraw MCP app at `https://mcp.excalidraw.com` (override via `MCP_SERVER_URL`). Pinned `serverId: "excalidraw"` so URL changes don't silently break persisted activities
|
||||
- Note: the demo source contains no `data-testid` attributes and registers no custom activity renderer — CopilotKit's built-in `MCPAppsActivityRenderer` handles the sandboxed iframe automatically. Checks below rely on verbatim visible text, network traffic, and the iframe DOM
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/mcp-apps`; verify the page renders within 3s and a single `CopilotChat` pane is centered (max-width ~896px, rounded-2xl, full-height)
|
||||
- [ ] Verify the chat is wired to `runtimeUrl="/api/copilotkit-mcp-apps"` and `agent="mcp-apps"` (DevTools → Network: sending a message hits that endpoint)
|
||||
- [ ] Verify both suggestion pills are visible with verbatim titles:
|
||||
- "Draw a flowchart"
|
||||
- "Sketch a system diagram"
|
||||
- [ ] Send "Hello" and verify an assistant text response appears within 10s (no MCP activity iframe for plain text)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### MCP Server Connection (runtime `mcpApps.servers`)
|
||||
|
||||
- [ ] Send the first flow-chart prompt; in DevTools → Network, verify the POST to `/api/copilotkit-mcp-apps` succeeds (status 200) and the server-side runtime resolves tools from `https://mcp.excalidraw.com` (watch the server logs for the MCP Apps middleware attaching the Excalidraw tool set — notably `create_view`)
|
||||
- [ ] Verify no console errors mentioning the MCP server URL, auth, or tool-schema parse failures
|
||||
|
||||
#### MCP Tool Invocation (`create_view`)
|
||||
|
||||
- [ ] Click "Draw a flowchart"; within 60s verify the agent calls the `create_view` MCP tool exactly ONCE (per `SYSTEM_PROMPT` in `src/agents/mcp_apps_agent.py`: "Call `create_view` ONCE with 3-5 elements total") — confirm via DevTools → Network stream or backend logs
|
||||
- [ ] Verify the tool payload contains 3-5 Excalidraw elements (shapes + arrows + optional title text), each with a unique string `id`, and ends with ONE `cameraUpdate` sized `600x450` or `800x600`
|
||||
|
||||
#### Activity Renderer (built-in `MCPAppsActivityRenderer`)
|
||||
|
||||
- [ ] Within 60s of the tool call, verify a sandboxed `<iframe>` renders inline in the chat transcript (activity-message slot) pointed at the Excalidraw MCP UI resource
|
||||
- [ ] Verify the iframe has a `sandbox` attribute (CopilotKit's built-in renderer always sandboxes MCP UI resources)
|
||||
- [ ] Verify the iframe paints a flow-chart-shaped diagram: at least 3 shape nodes (rectangles, ellipses, or diamonds with text labels) connected by arrows, framed within the viewport (camera-update step from the system prompt)
|
||||
- [ ] Verify the assistant text below the iframe is a single short sentence describing what was drawn (per system prompt)
|
||||
|
||||
#### Server-Driven UI Update (second prompt, same thread)
|
||||
|
||||
- [ ] Without reloading, send the second suggestion "Sketch a system diagram"; within 60s verify a new activity iframe renders in-transcript containing a client → server → database layout (3 labeled shapes + 2 arrows)
|
||||
- [ ] Verify the previous flow-chart iframe is still present and un-stale in the scrollback (activity messages persist, matching the rationale for the pinned `serverId: "excalidraw"` in the runtime config)
|
||||
|
||||
#### End-to-End MCP Interaction (concrete, single-case)
|
||||
|
||||
- [ ] Send an explicit prompt: `"Use Excalidraw to draw exactly 2 rectangles labelled 'A' and 'B' connected by one arrow from A to B."`
|
||||
- [ ] Within 60s verify: (1) `create_view` is called ONCE with exactly 3 elements (2 rectangles + 1 arrow) plus the trailing `cameraUpdate`; (2) an iframe renders showing two labelled rectangles with a connecting arrow; (3) the assistant reply is one short sentence; (4) no duplicate `create_view` invocations or retries appear in network / logs
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message; verify it is a no-op (no user bubble, no assistant response)
|
||||
- [ ] Send "What is 2+2?"; verify the agent replies in plain text without invoking `create_view` (no iframe, no MCP activity in the stream)
|
||||
- [ ] DevTools → Console: walk through all flows above; verify no uncaught errors, no CORS failures referencing `mcp.excalidraw.com`, and no "sandbox" / iframe-permission warnings
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3s; plain-text response within 10s; MCP-backed iframe renders within 60s of prompt (bias is "correct-enough diagram fast" per system prompt, one `create_view` call)
|
||||
- MCP server connection to `https://mcp.excalidraw.com` succeeds and the Excalidraw tool set (including `create_view`) is advertised to the agent at request time
|
||||
- At least one concrete end-to-end MCP interaction completes: user prompt → `create_view` tool call → activity event → sandboxed iframe painting the requested diagram
|
||||
- The built-in `MCPAppsActivityRenderer` is used (no app-side `useRenderActivityMessage` / `renderActivityMessages` registration exists in `page.tsx` — per the `@region[no-frontend-renderer-needed]` contract)
|
||||
- No UI layout breaks, no uncaught console errors, no duplicate `create_view` invocations within a single prompt turn
|
||||
@@ -0,0 +1,92 @@
|
||||
# QA: Multimodal Attachments — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed and accessible at `/demos/multimodal`
|
||||
- Railway service `showcase-langgraph-python` is healthy
|
||||
- `OPENAI_API_KEY` set on the Railway service (vision calls require it)
|
||||
- Agent is using a vision-capable model (`gpt-4o` or equivalent) — verifiable by
|
||||
inspecting the Railway logs for `src/agents/multimodal_agent.py` or by
|
||||
running one image round-trip
|
||||
- Sample files are bundled under `public/demo-files/`:
|
||||
- `sample.png` — a small PNG the vision model can describe
|
||||
- `sample.pdf` — a small one-page PDF mentioning "CopilotKit"
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/multimodal`
|
||||
- [ ] Verify the page header "Multimodal attachments" is visible
|
||||
- [ ] Verify the description text mentions image and PDF attachments
|
||||
- [ ] Verify the sample-row (`[data-testid="multimodal-sample-row"]`) is visible
|
||||
- [ ] Verify the "Try with sample image" and "Try with sample PDF" buttons are
|
||||
present and enabled
|
||||
- [ ] Verify `<CopilotChat />` renders a message composer with a paperclip /
|
||||
"Add attachments" button
|
||||
|
||||
### 2. Sample image path
|
||||
|
||||
- [ ] Click "Try with sample image"
|
||||
- [ ] Button label briefly flips to "Loading…"
|
||||
- [ ] Within 5 seconds an image attachment chip appears in the composer with a
|
||||
thumbnail preview
|
||||
- [ ] Type "Describe this image"
|
||||
- [ ] Click send
|
||||
- [ ] Within 60 seconds the agent responds with a description referring to the
|
||||
image content (e.g. mentions a logo, colors, or brand mark)
|
||||
|
||||
### 3. Sample PDF path
|
||||
|
||||
- [ ] Click "Try with sample PDF"
|
||||
- [ ] A document attachment chip appears in the composer (filename
|
||||
"sample.pdf" visible, document icon)
|
||||
- [ ] Type "Summarize this document"
|
||||
- [ ] Click send
|
||||
- [ ] Within 60 seconds the agent responds with text that mentions "CopilotKit"
|
||||
(the sample PDF contains the word multiple times)
|
||||
|
||||
### 4. Paperclip / real upload path (manual)
|
||||
|
||||
- [ ] Click the paperclip / "Add attachments" button
|
||||
- [ ] File picker opens filtered to `image/*` and `application/pdf`
|
||||
- [ ] Select a local image under 10 MB
|
||||
- [ ] Attachment chip renders in the composer within 2 seconds
|
||||
- [ ] Type "What's in this image?" and send
|
||||
- [ ] Agent responds with a description of the image content
|
||||
|
||||
### 5. Drag-and-drop
|
||||
|
||||
- [ ] Drag a local image onto the chat container
|
||||
- [ ] Chat surface shows a drop affordance
|
||||
- [ ] On drop, an attachment chip appears
|
||||
- [ ] Sending a prompt works the same as the paperclip path
|
||||
|
||||
### 6. Multi-attachment
|
||||
|
||||
- [ ] Inject the sample image AND the sample PDF (click both buttons sequentially)
|
||||
- [ ] Both chips are visible in the composer
|
||||
- [ ] Type "What do these two attachments have in common?"
|
||||
- [ ] Click send
|
||||
- [ ] Agent response acknowledges both attachments
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
- [ ] Try to attach a file over 10 MB via the paperclip
|
||||
- [ ] Verify `onUploadFailed` fires (console warning from the page) and the
|
||||
file is rejected without corrupting the composer
|
||||
- [ ] Try to attach an unsupported type (e.g. `.exe`) — the file picker filter
|
||||
excludes it or `onUploadFailed` fires with `reason: "invalid-type"`
|
||||
- [ ] Block `/demo-files/sample.png` in DevTools Network; click "Try with
|
||||
sample image"; the `multimodal-sample-error` span shows a helpful error
|
||||
without crashing the page
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Attachment chip appears within 2 seconds after upload or sample injection
|
||||
- Thumbnail renders for image attachments; document icon for PDFs
|
||||
- Agent response arrives within 60 seconds for single-attachment prompts
|
||||
(multimodal tokens are heavier than text-only)
|
||||
- No console errors during successful flows (warnings from `onUploadFailed`
|
||||
during intentional error cases are acceptable)
|
||||
- Error states are visible and recoverable — the user can retry
|
||||
@@ -0,0 +1,80 @@
|
||||
# QA: Open-Ended Generative UI (Advanced) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
- Graph `open_gen_ui_advanced` is registered in the OGUI runtime (`api/copilotkit-ogui/route.ts`) with `openGenerativeUI.agents` including `"open-gen-ui-advanced"`
|
||||
- Sandbox-function handlers in `sandbox-functions.ts` are exported: `evaluateExpression` and `notifyHost`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the open-gen-ui-advanced demo page
|
||||
- [ ] Verify the `<CopilotChat>` renders full-height within the centered max-w-4xl container
|
||||
- [ ] Verify the input composer is visible
|
||||
- [ ] Send a basic message (e.g. "Hi")
|
||||
- [ ] Verify the agent calls `generateSandboxedUi` and a sandboxed iframe mounts in the assistant turn
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify "Calculator (calls evaluateExpression)" suggestion is visible
|
||||
- [ ] Verify "Ping the host (calls notifyHost)" suggestion is visible
|
||||
- [ ] Verify "Inline expression evaluator" suggestion is visible
|
||||
|
||||
#### Sandbox-to-Host: evaluateExpression (Calculator)
|
||||
|
||||
- [ ] Click the "Calculator (calls evaluateExpression)" suggestion
|
||||
- [ ] Verify a sandboxed iframe renders a calculator UI with digit buttons, operator buttons, a display, and an "=" button
|
||||
- [ ] Verify all buttons use `type="button"` — no `<form>` element is present inside the iframe
|
||||
- [ ] Enter an expression like `12 * (3 + 4.5)` via the calculator buttons (or the generated input)
|
||||
- [ ] Open browser devtools console BEFORE pressing "="
|
||||
- [ ] Press "="
|
||||
- [ ] Verify the host console logs `[open-gen-ui/advanced] evaluateExpression 12 * (3 + 4.5) = 90`
|
||||
- [ ] Verify the display updates to show `90` (the `res.value` returned by the host)
|
||||
- [ ] Verify a computed-history entry appears below the display
|
||||
- [ ] In a follow-up user turn, type "What was the result of my last calculation?" and verify the agent's text response references the computed value (round-trip: sandbox call -> host handler -> visible result -> agent awareness via subsequent turn context)
|
||||
|
||||
#### Sandbox-to-Host: notifyHost (Ping)
|
||||
|
||||
- [ ] In a new turn, click the "Ping the host (calls notifyHost)" suggestion
|
||||
- [ ] Verify a sandboxed iframe renders a card with a single "Say hi to the host" button
|
||||
- [ ] Open the browser devtools console
|
||||
- [ ] Click the button
|
||||
- [ ] Verify the host console logs `[open-gen-ui/advanced] notifyHost: Hi from the sandbox!`
|
||||
- [ ] Verify the card updates to display the returned confirmation object, including a `receivedAt` ISO-8601 timestamp and the echoed `message` field
|
||||
|
||||
#### Sandbox-to-Host: Inline Expression Evaluator
|
||||
|
||||
- [ ] In a new turn, click the "Inline expression evaluator" suggestion
|
||||
- [ ] Verify a sandboxed iframe renders a text input + "Evaluate" button (no `<form>`, button `type="button"`)
|
||||
- [ ] Enter `2 + 2` and click "Evaluate"
|
||||
- [ ] Verify the output area renders `4` (from `res.value`)
|
||||
- [ ] Enter an invalid expression `abc + 1` and click "Evaluate"
|
||||
- [ ] Verify the output area renders the error string from `res.error` (e.g. "Unsupported characters in expression.")
|
||||
|
||||
#### Sandbox Constraints
|
||||
|
||||
- [ ] Verify the iframe sandbox attribute is `sandbox="allow-scripts"` only (no `allow-forms`, no `allow-same-origin`)
|
||||
- [ ] Verify no network requests originate from the iframe (check DevTools Network filtered by iframe frame)
|
||||
- [ ] Verify the agent keeps its own chat message brief (1 sentence) — the rendered UI is the real output
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Enter an expression with unsupported characters (e.g. `alert(1)`) into the calculator/evaluator and confirm `res.ok === false` with error "Unsupported characters in expression."
|
||||
- [ ] Enter a divide-by-zero (`1/0`) — verify the handler returns `{ ok: false, error: "Not a finite number." }` and the UI renders the error path
|
||||
- [ ] Refresh the page mid-stream — verify no broken UI persists
|
||||
- [ ] Send an empty message — input should be rejected without error
|
||||
- [ ] Verify no console errors beyond the two intentional `console.log` statements from the sandbox-function handlers
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- First interactive sandboxed UI mounts within ~15 seconds of prompt submission
|
||||
- Sandbox -> host round-trip (button click -> `Websandbox.connection.remote.<fn>` -> visible result) completes without page reload
|
||||
- `evaluateExpression` returns `{ ok, value }` on valid input and `{ ok: false, error }` on rejected input
|
||||
- `notifyHost` returns `{ ok: true, receivedAt, message }` with a valid ISO-8601 timestamp
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,67 @@
|
||||
# QA: Open-Ended Generative UI (Minimal) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
- Graph `open_gen_ui` is registered in the OGUI runtime (`api/copilotkit-ogui/route.ts`) with `openGenerativeUI.agents` including `"open-gen-ui"`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the open-gen-ui demo page
|
||||
- [ ] Verify the `<CopilotChat>` renders full-height within the centered max-w-4xl container
|
||||
- [ ] Verify the input composer is visible
|
||||
- [ ] Send a basic message (e.g. "Hi")
|
||||
- [ ] Verify the agent calls `generateSandboxedUi` and a sandboxed iframe is mounted inside the chat transcript
|
||||
- [ ] Verify the streaming placeholder message(s) appear before the final UI renders
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify "3D axis visualization (model airplane)" suggestion is visible
|
||||
- [ ] Verify "How a neural network works" suggestion is visible
|
||||
- [ ] Verify "Quicksort visualization" suggestion is visible
|
||||
- [ ] Verify "Fourier: square wave from sines" suggestion is visible
|
||||
|
||||
#### Neural Network Prompt (concrete render)
|
||||
|
||||
- [ ] Click the "How a neural network works" suggestion
|
||||
- [ ] Verify short placeholder lines show while streaming (e.g. "Sketching the scene…", "Labelling axes…", "Wiring up the animation…")
|
||||
- [ ] Verify a sandboxed iframe activity is mounted in the assistant turn
|
||||
- [ ] Inside the iframe, verify an inline `<svg>` element renders (not stacked `<div>`s)
|
||||
- [ ] Verify three layers of nodes are visible (input ~4, hidden ~5, output ~2) with connecting lines
|
||||
- [ ] Verify layer labels are present (e.g. "Input", "Hidden", "Output") and a "Forward pass" caption
|
||||
- [ ] Verify activations animate (pulse forward from input to output in a loop) — indigo active, slate quiescent
|
||||
- [ ] Verify the visualization loops continuously without user interaction
|
||||
|
||||
#### Quicksort Prompt (secondary render)
|
||||
|
||||
- [ ] In a fresh turn, click the "Quicksort visualization" suggestion
|
||||
- [ ] Verify a new sandboxed iframe is rendered with ~10 SVG rect bars
|
||||
- [ ] Verify a caption text element updates as the sort progresses (e.g. "Partition around pivot", "Swap", "Recurse left")
|
||||
- [ ] Verify the pivot highlight uses amber (#f59e0b) and compared elements use indigo (#6366f1)
|
||||
|
||||
#### Design Skill Injection
|
||||
|
||||
- [ ] In any rendered output, verify the visualization respects the palette: indigo accent, emerald success, amber warning, slate neutrals
|
||||
- [ ] Verify the outer container has a white background with rounded corners and padded content (no bleed to viewport edges)
|
||||
- [ ] Verify the visualization includes a title line and subtitle line at the top
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send a trivially off-topic prompt (e.g. "banana banana banana"); verify the agent still renders a sandboxed UI or a graceful text response — no unhandled exception
|
||||
- [ ] Verify no console errors originating from the host page during rendering (sandbox-internal warnings may be present)
|
||||
- [ ] Verify the sandbox iframe does NOT attempt fetch / XHR / localStorage (network tab: no origin-leaked requests from the iframe)
|
||||
- [ ] Send an empty message — input should be rejected without error
|
||||
- [ ] Refresh the page mid-stream — verify no broken UI persists
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- First visualization mounts within ~15 seconds of prompt submission
|
||||
- Sandboxed iframe renders SVG-based content with labeled axes / layers / legend
|
||||
- Animations loop smoothly with CSS keyframes (no jank from setInterval)
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,55 @@
|
||||
# QA: Pre-Built Popup — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/prebuilt-popup` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health` or `/api/copilotkit` GET); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the neutral `sample_agent` graph (registered to the `prebuilt-popup` agent name)
|
||||
- Note: the demo source contains no `data-testid` attributes. Checks below rely on verbatim visible text, role-based selectors, and DOM structure. The underlying agent is the neutral "helpful, concise assistant" (no frontend tools, no agent tools).
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/prebuilt-popup`; verify the main content renders with heading (h1, text: "Popup demo — look for the floating launcher") and paragraph body mentioning `<CopilotPopup />`
|
||||
- [ ] Verify a floating launcher button is visible in a corner of the viewport (typically bottom-right)
|
||||
- [ ] Verify the `<CopilotPopup />` window is OPEN by default (`defaultOpen={true}` in source) — a chat overlay sits on top of the page content with an input at the bottom
|
||||
- [ ] Verify the chat input placeholder is verbatim "Ask the popup anything..."
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Popup Open / Close
|
||||
|
||||
- [ ] With the popup open, click the popup's close control (X button in the popup header); verify the popup window collapses and only the floating launcher remains
|
||||
- [ ] Click the floating launcher; verify the popup re-opens showing the prior transcript (if any)
|
||||
- [ ] Verify opening/closing does not trigger a full page reload (URL remains `/demos/prebuilt-popup`, main content stays mounted)
|
||||
- [ ] Verify the popup overlays the page content rather than pushing layout (unlike the docked sidebar form factor)
|
||||
|
||||
#### Suggestions (`useConfigureSuggestions`)
|
||||
|
||||
- [ ] With the popup open, verify a suggestion pill titled "Say hi" is rendered in the chat surface (configured with `available: "always"`)
|
||||
- [ ] Click the "Say hi" pill; verify it sends the message "Say hi from the popup!" and an assistant text response appears within 10s
|
||||
|
||||
#### Chat Round-Trip
|
||||
|
||||
- [ ] Type "Hello" into the popup chat input ("Ask the popup anything..." placeholder) and submit; verify the user bubble appears, followed within 10s by an assistant text response
|
||||
- [ ] Send a second message; verify a second valid response appears and the transcript auto-scrolls to the latest turn
|
||||
- [ ] Close the popup and re-open it; verify the existing transcript persists within the open session
|
||||
|
||||
#### Agent Wiring
|
||||
|
||||
- [ ] Confirm (via DevTools → Network) that chat submissions POST to `/api/copilotkit` with agent name `prebuilt-popup` in the payload; response streams back as SSE with no 4xx/5xx status
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no network request)
|
||||
- [ ] Resize the viewport to ~375px wide (mobile); verify the popup adapts (full-width or close to it) without clipping the input, header, or close button
|
||||
- [ ] Stop the backend, send a message; verify the UI surfaces a visible error path rather than hanging silently; DevTools → Console shows no uncaught errors during any flow above
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page + popup render within 3 seconds
|
||||
- Assistant text response within 10 seconds
|
||||
- Popup open/close animates smoothly with no layout jank
|
||||
- Floating launcher remains accessible (not clipped) at all tested viewport sizes
|
||||
- No UI layout breaks, no uncaught console errors
|
||||
- The neutral agent (no tools) simply chats — no frontend tool registrations, no tool-call UI expected in this demo
|
||||
@@ -0,0 +1,53 @@
|
||||
# QA: Pre-Built Sidebar — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/prebuilt-sidebar` on the dashboard host
|
||||
- Agent backend is healthy (`/api/health` or `/api/copilotkit` GET); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the neutral `sample_agent` graph (registered to the `prebuilt-sidebar` agent name)
|
||||
- Note: the demo source contains no `data-testid` attributes. Checks below rely on verbatim visible text, role-based selectors, and DOM structure. The underlying agent is the neutral "helpful, concise assistant" (no frontend tools, no agent tools).
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/prebuilt-sidebar`; verify the main content renders with heading (h1, text: "Sidebar demo — click the launcher") and paragraph body mentioning `<CopilotSidebar />`
|
||||
- [ ] Verify the `<CopilotSidebar />` is rendered docked to one edge of the viewport (typically the right edge) and is OPEN by default (`defaultOpen={true}` in source)
|
||||
- [ ] Verify the sidebar contains a chat input and its own launcher/toggle button
|
||||
- [ ] Verify the page body (main content) remains visible alongside the sidebar (sidebar does not fully overlay the page — docked form factor, not modal)
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Sidebar Toggle
|
||||
|
||||
- [ ] Click the sidebar launcher/close button; verify the sidebar collapses and the main content expands to fill the freed width
|
||||
- [ ] Click the launcher again; verify the sidebar re-opens to its previous width
|
||||
- [ ] Verify toggling does not trigger a full page reload (URL remains `/demos/prebuilt-sidebar`, no flash of unstyled content)
|
||||
|
||||
#### Suggestions (`useConfigureSuggestions`)
|
||||
|
||||
- [ ] With the sidebar open, verify a suggestion pill titled "Say hi" is rendered in the chat surface (configured via `useConfigureSuggestions` with `available: "always"`)
|
||||
- [ ] Click the "Say hi" pill; verify it sends the message "Say hi!" and an assistant text response appears within 10s
|
||||
|
||||
#### Chat Round-Trip
|
||||
|
||||
- [ ] Type "Hello" into the sidebar chat input and submit; verify the user bubble appears, followed within 10s by an assistant text response
|
||||
- [ ] Send a follow-up ("What can you help with?"); verify a coherent second response referencing prior turn is NOT required (agent has no persistent memory beyond the session thread) but a valid response appears
|
||||
- [ ] Verify the transcript scrolls to the latest message automatically
|
||||
|
||||
#### Agent Wiring
|
||||
|
||||
- [ ] Confirm (via DevTools → Network) that chat submissions POST to `/api/copilotkit` with agent name `prebuilt-sidebar` in the payload; response streams back as SSE with no 4xx/5xx status
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no network request, no assistant response)
|
||||
- [ ] Resize the viewport to ~375px wide (mobile); verify the sidebar adapts (overlays content or stacks) without clipping the input or launcher
|
||||
- [ ] Stop the backend, send a message; verify the UI surfaces a visible error path rather than hanging silently; DevTools → Console shows no uncaught errors during any flow above
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page + sidebar render within 3 seconds
|
||||
- Assistant text response within 10 seconds
|
||||
- Sidebar toggle is instant (<200ms) with no layout jank
|
||||
- No UI layout breaks, no uncaught console errors
|
||||
- The neutral agent (no tools) simply chats — no frontend tool registrations, no tool-call UI expected in this demo
|
||||
@@ -0,0 +1,73 @@
|
||||
# QA: Read-Only Agent Context — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
- Graph `readonly_state_agent_context` is registered in the runtime (see `api/copilotkit/route.ts`)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the readonly-state-agent-context demo page
|
||||
- [ ] Verify the "Agent Context" card is visible (`data-testid="context-card"`)
|
||||
- [ ] Verify the description text references `useAgentContext`: "Read-only context provided to the agent via `useAgentContext`. The agent cannot modify these."
|
||||
- [ ] Verify the chat panel renders on the right with placeholder "Ask about your context..."
|
||||
- [ ] Send a basic message (e.g. "Hello") via the chat
|
||||
- [ ] Verify the agent responds with a text message
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
#### Initial Context State
|
||||
|
||||
- [ ] Verify the Name input (`data-testid="ctx-name"`) defaults to "Atai"
|
||||
- [ ] Verify the Timezone dropdown (`data-testid="ctx-timezone"`) defaults to "America/Los_Angeles"
|
||||
- [ ] Verify the Timezone dropdown offers: America/Los_Angeles, America/New_York, Europe/London, Europe/Berlin, Asia/Tokyo, Australia/Sydney
|
||||
- [ ] Verify the Recent Activity checkboxes list: "Viewed the pricing page", "Added 'Pro Plan' to cart", "Watched the product demo video", "Started the 14-day free trial", "Invited a teammate"
|
||||
- [ ] Verify two activities are checked by default: "Viewed the pricing page" and "Watched the product demo video"
|
||||
- [ ] Verify the Published Context JSON preview (`data-testid="ctx-state-json"`) shows `{ "name": "Atai", "timezone": "America/Los_Angeles", "recentActivity": [...] }`
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify "Who am I?" suggestion is visible
|
||||
- [ ] Verify "Suggest next steps" suggestion is visible
|
||||
- [ ] Verify "Plan my morning" suggestion is visible
|
||||
|
||||
#### Agent Reads User Name (useAgentContext)
|
||||
|
||||
- [ ] Click the "Who am I?" suggestion (or ask "What is my name?")
|
||||
- [ ] Verify the agent response addresses the user as "Atai"
|
||||
- [ ] Edit the Name input (`data-testid="ctx-name"`) to "Jamie"
|
||||
- [ ] Verify the Published Context JSON updates to show `"name": "Jamie"`
|
||||
- [ ] Ask "What is my name?" again
|
||||
- [ ] Verify the agent now responds with "Jamie" (not "Atai")
|
||||
|
||||
#### Agent Reads Timezone
|
||||
|
||||
- [ ] Change the Timezone dropdown (`data-testid="ctx-timezone"`) to "Asia/Tokyo"
|
||||
- [ ] Verify the Published Context JSON updates to `"timezone": "Asia/Tokyo"`
|
||||
- [ ] Click the "Plan my morning" suggestion
|
||||
- [ ] Verify the agent's response references Tokyo / JST / Asia/Tokyo when discussing the time
|
||||
|
||||
#### Agent Reads Recent Activity
|
||||
|
||||
- [ ] Uncheck all default activities, then check only "Started the 14-day free trial" and "Invited a teammate"
|
||||
- [ ] Verify the Published Context JSON shows the new recentActivity array
|
||||
- [ ] Click the "Suggest next steps" suggestion
|
||||
- [ ] Verify the agent's response references the trial and/or invited-teammate activities (not the pricing page or demo video)
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Clear the Name input to an empty string and ask "What is my name?" — agent should handle gracefully (no crash)
|
||||
- [ ] Send an empty chat message — input should be rejected without error
|
||||
- [ ] Verify no console errors during normal usage
|
||||
- [ ] Verify the agent cannot modify context values (Name / Timezone / Activity checkboxes stay user-controlled)
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Context card and chat load within 3 seconds
|
||||
- Agent responds within 10 seconds
|
||||
- Every change to Name / Timezone / Recent Activity reflects in the Published Context JSON immediately
|
||||
- Agent responses reflect the CURRENT context values on every turn (no stale context)
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,21 @@
|
||||
# QA: Reasoning (Default) — LangGraph (Python)
|
||||
|
||||
> Stub — authored for column completeness. This demo verifies the
|
||||
> built-in `CopilotChatReasoningMessage` renders without a custom slot
|
||||
> and does not warrant a full manual checklist.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/reasoning-default
|
||||
- [ ] Send any prompt that elicits a reasoning response and verify the built-in `CopilotChatReasoningMessage` collapsible card renders the reasoning tokens
|
||||
- [ ] Verify no custom reasoning slot is wired (default styling only — no `ReasoningBlock` or bespoke container)
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page loads without errors
|
||||
- Reasoning renders via CopilotKit's default `CopilotChatReasoningMessage` component with zero frontend configuration
|
||||
@@ -0,0 +1,61 @@
|
||||
# QA: Shared State (Read + Write) — LangGraph (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; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `shared-state-read-write` graph
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/shared-state-read-write`; verify the page renders within 3s with the preferences + notes cards in the main column and the `CopilotSidebar` open by default on the right (cards stack vertically once the viewport drops below `xl` / 1280px)
|
||||
- [ ] 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 `PreferencesInjectorMiddleware` injects these into the system prompt each 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
|
||||
|
||||
#### 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 (middleware skips injection when `preferences` 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) — LangGraph (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 (175°C)"
|
||||
|
||||
#### 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 — LangGraph (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: Sub-Agents — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the subagents demo page
|
||||
- [ ] Verify the chat interface loads with title "Sub-Agents"
|
||||
- [ ] 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,31 @@
|
||||
# Thread ID Frontend Tool Round Trip
|
||||
|
||||
## Scope
|
||||
|
||||
Regression checklist for ENT-658: a `CopilotChat` wrapped by
|
||||
`CopilotChatConfigurationProvider` must keep its SDK-generated non-explicit
|
||||
thread active across a frontend tool call and follow-up run.
|
||||
|
||||
## Manual QA
|
||||
|
||||
- [ ] Navigate to `/demos/threadid-frontend-tool-roundtrip`.
|
||||
- [ ] Verify the chat input is visible and `Explicit threadId` is unchecked.
|
||||
- [ ] Send `invoke testFrontendToolCalling with label X`.
|
||||
- [ ] Verify the user message remains visible.
|
||||
- [ ] Verify the `testFrontendToolCalling` card remains visible and shows
|
||||
`label: X` and `result: handled X`.
|
||||
- [ ] Verify the assistant reply `Frontend tool finished for X.` appears.
|
||||
- [ ] Verify the chat does not return to the empty state.
|
||||
- [ ] Refresh the page or open a new tab, check `Explicit threadId` before
|
||||
sending any message, send the same prompt again, and verify the same
|
||||
message/tool/reply persistence behavior.
|
||||
- [ ] Optionally toggle `Explicit threadId` after a generated-thread
|
||||
conversation and verify the chat switches to the explicit thread's
|
||||
history. An empty explicit thread on first use is expected.
|
||||
|
||||
## Automated Coverage
|
||||
|
||||
- `tests/e2e/threadid-frontend-tool-roundtrip.spec.ts` covers the demo route,
|
||||
generated-thread default state, and explicit-thread toggle.
|
||||
- `packages/react-core/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx`
|
||||
covers the SDK-generated thread handoff at the component level.
|
||||
@@ -0,0 +1,86 @@
|
||||
# QA: Tool Rendering (Custom Catch-all) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
- Agent slug `tool-rendering-custom-catchall` is registered at `/api/copilotkit`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the `tool-rendering-custom-catchall` demo page
|
||||
- [ ] Verify the chat interface loads in a centered full-height layout (max-width 4xl, `rounded-2xl`)
|
||||
- [ ] Verify the chat input placeholder "Type a message" is visible
|
||||
- [ ] Send a basic message (e.g. "Hi")
|
||||
- [ ] Verify the agent responds with a text message
|
||||
|
||||
### 2. Feature-Specific Checks — Branded Wildcard Renderer
|
||||
|
||||
The frontend calls `useDefaultRenderTool({ render: ... })` with a SINGLE branded wildcard component (`CustomCatchallRenderer`). There are ZERO per-tool named renderers. Every tool call must paint via this one branded card regardless of tool identity.
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify "Weather in SF" suggestion pill is visible
|
||||
- [ ] Verify "Find flights" suggestion pill is visible
|
||||
- [ ] Verify "Roll a d20" suggestion pill is visible
|
||||
- [ ] Click a suggestion and verify it either populates the input or sends the message
|
||||
|
||||
#### `get_weather` renders via the branded catch-all card
|
||||
|
||||
- [ ] Click the "Weather in SF" suggestion (or type "What's the weather in San Francisco?")
|
||||
- [ ] Verify the branded card renders (`data-testid="custom-catchall-card"`) with:
|
||||
- [ ] An uppercase label "Tool" followed by the tool name `get_weather` (`data-testid="custom-catchall-tool-name"`, monospaced)
|
||||
- [ ] The card root carries `data-tool-name="get_weather"`
|
||||
- [ ] A status badge (`data-testid="custom-catchall-status"`) that transitions through `streaming` (amber), `running` (lavender/indigo), and finally `done` (green `#189370`)
|
||||
- [ ] An "Arguments" section with a monospaced pre block (`data-testid="custom-catchall-args"`) showing pretty-printed JSON of the parameters
|
||||
- [ ] A "Result" section showing "waiting for tool to finish…" while pending and a green-tinted pre block (`data-testid="custom-catchall-result"`) once `status === "complete"`
|
||||
- [ ] The completed Result pre shows the mock weather payload (`city`, `temperature: 68`, `humidity: 55`, `wind_speed: 10`, `conditions: "Sunny"`)
|
||||
- [ ] Verify the card uses the branded styling: white background, rounded `2xl` corners, `#DBDBE5` border, `#FAFAFC` header strip, subtle shadow
|
||||
|
||||
#### `search_flights` renders via the SAME branded catch-all card
|
||||
|
||||
- [ ] Click the "Find flights" suggestion (or type "Find flights from SFO to JFK")
|
||||
- [ ] Verify a second `data-testid="custom-catchall-card"` renders
|
||||
- [ ] Verify `data-tool-name="search_flights"` on the card root and the name label reads `search_flights`
|
||||
- [ ] Verify the visual style is IDENTICAL to the `get_weather` card — same header strip, same status badge treatment, same Arguments/Result sections
|
||||
- [ ] Verify the Result pre shows the mock flights array (United UA231, Delta DL412, JetBlue B6722)
|
||||
|
||||
#### `roll_dice` renders via the SAME branded catch-all card
|
||||
|
||||
- [ ] Click the "Roll a d20" suggestion (or type "Roll a 20-sided die")
|
||||
- [ ] Verify another `custom-catchall-card` renders with `data-tool-name="roll_dice"`
|
||||
- [ ] Verify the Result pre shows `{ "sides": 20, "result": <1-20> }`
|
||||
|
||||
#### `get_stock_price` renders via the SAME branded catch-all card
|
||||
|
||||
- [ ] Type "How is AAPL doing?"
|
||||
- [ ] Verify a `custom-catchall-card` with `data-tool-name="get_stock_price"` renders
|
||||
- [ ] Verify the Result pre shows `ticker: "AAPL"`, a `price_usd`, and a `change_pct`
|
||||
|
||||
#### No built-in default UI appears
|
||||
|
||||
- [ ] Verify NO instance of CopilotKit's built-in `DefaultToolCallRenderer` appears alongside the branded card — the branded card replaces it
|
||||
- [ ] Verify every rendered tool call is a `data-testid="custom-catchall-card"` element
|
||||
|
||||
#### Chained tool calls all paint the same branded card
|
||||
|
||||
- [ ] Ask "What's the weather in Tokyo?" — the system prompt instructs the agent to chain tools
|
||||
- [ ] Verify at least TWO `custom-catchall-card` elements render in succession (e.g. one for `get_weather`, one for `search_flights`)
|
||||
- [ ] Verify each carries a distinct `data-tool-name` but the visual style is identical
|
||||
- [ ] While a tool is in flight, verify the status badge reads `streaming` or `running` and the Result section shows "waiting for tool to finish…"; after completion the badge reads `done` and the green Result pre appears
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message (should be handled gracefully)
|
||||
- [ ] Verify no console errors during normal usage
|
||||
- [ ] Verify the `done` badge text and the completed-result green tint (`#85ECCE`) remain stable across re-renders
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- Every tool invocation paints via the single branded `CustomCatchallRenderer` card — zero visual variance beyond `data-tool-name` and the payload
|
||||
- Status badge progresses: `streaming` → `running` → `done`
|
||||
- Branded styling details from `custom-catchall-renderer.tsx` are visible (uppercase "Tool" label, monospaced tool-name, rounded-2xl card, header strip, tone-coded status badge, green result pre on completion)
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,76 @@
|
||||
# QA: Tool Rendering (Default Catch-all) — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
- Agent slug `tool-rendering-default-catchall` is registered at `/api/copilotkit`
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the `tool-rendering-default-catchall` demo page
|
||||
- [ ] Verify the chat interface loads in a centered full-height layout (max-width 4xl, `rounded-2xl`)
|
||||
- [ ] Verify the chat input placeholder "Type a message" is visible
|
||||
- [ ] Send a basic message (e.g. "Hi")
|
||||
- [ ] Verify the agent responds with a text message
|
||||
|
||||
### 2. Feature-Specific Checks — Built-in Default Tool-Call UI
|
||||
|
||||
The frontend calls `useDefaultRenderTool()` with NO config — it registers CopilotKit's package-provided `DefaultToolCallRenderer` as the `*` wildcard. The frontend adds ZERO custom per-tool or custom wildcard renderers. Every tool call must paint via this one built-in card.
|
||||
|
||||
#### Suggestions
|
||||
|
||||
- [ ] Verify "Weather in SF" suggestion pill is visible
|
||||
- [ ] Verify "Find flights" suggestion pill is visible
|
||||
- [ ] Verify "Roll a d20" suggestion pill is visible
|
||||
- [ ] Click a suggestion and verify it either populates the input or sends the message
|
||||
|
||||
#### `get_weather` renders via the built-in default card
|
||||
|
||||
- [ ] Click the "Weather in SF" suggestion (or type "What's the weather in San Francisco?")
|
||||
- [ ] Verify a default tool-call card appears with the tool name `get_weather` visible
|
||||
- [ ] Verify a status pill transitions through `Running` and lands on `Done`
|
||||
- [ ] Expand the card's "Arguments" section and verify it shows `{ "location": "San Francisco" }` (or similar)
|
||||
- [ ] Expand the card's "Result" section and verify it shows the mock payload with `city`, `temperature: 68`, `humidity: 55`, `wind_speed: 10`, `conditions: "Sunny"`
|
||||
- [ ] Verify NO custom-branded card appears (no `data-testid="custom-catchall-card"`, no `data-testid="weather-card"`)
|
||||
|
||||
#### `search_flights` renders via the SAME built-in default card
|
||||
|
||||
- [ ] Click the "Find flights" suggestion (or type "Find flights from SFO to JFK")
|
||||
- [ ] Verify a tool-call card appears with tool name `search_flights`
|
||||
- [ ] Verify the card has the identical visual style/structure as the `get_weather` card — same header layout, same status pill, same Arguments/Result sections
|
||||
- [ ] Verify the Result section contains three mock flights (United UA231, Delta DL412, JetBlue B6722)
|
||||
|
||||
#### `roll_dice` renders via the SAME built-in default card
|
||||
|
||||
- [ ] Click the "Roll a d20" suggestion (or type "Roll a 20-sided die")
|
||||
- [ ] Verify a tool-call card for `roll_dice` appears with the same default visual style
|
||||
- [ ] Verify the Result section shows `{ "sides": 20, "result": <1-20> }`
|
||||
|
||||
#### `get_stock_price` renders via the SAME built-in default card
|
||||
|
||||
- [ ] Type "How is AAPL doing?"
|
||||
- [ ] Verify a `get_stock_price` tool-call card appears with the default built-in style
|
||||
- [ ] Verify the Result shows `ticker: "AAPL"`, a `price_usd`, and a `change_pct`
|
||||
|
||||
#### Chained tool calls
|
||||
|
||||
- [ ] Ask "What's the weather in Tokyo?" — the system prompt instructs the agent to chain tools
|
||||
- [ ] Verify at least TWO default tool-call cards render in succession (e.g. `get_weather` then `search_flights`)
|
||||
- [ ] Verify every card uses the identical default built-in UI — visually indistinguishable apart from the tool name and payload
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Send an empty message (should be handled gracefully)
|
||||
- [ ] Verify no console errors during normal usage
|
||||
- [ ] Verify no unhandled-promise warnings when a tool call streams
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- Every tool invocation paints via the built-in `DefaultToolCallRenderer` card (tool name + live status pill + Arguments + Result)
|
||||
- All four distinct tools (`get_weather`, `search_flights`, `get_stock_price`, `roll_dice`) render via the SAME default card — zero visual variance beyond payload
|
||||
- No custom-branded renderer appears anywhere
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,21 @@
|
||||
# QA: Tool Rendering (Reasoning Chain) — LangGraph (Python)
|
||||
|
||||
> Stub — authored for column completeness. This is a testing-kind demo
|
||||
> (see `kind: "testing"` in feature-registry.json) and does not warrant a
|
||||
> full manual checklist.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/tool-rendering-reasoning-chain
|
||||
- [ ] Send a multi-tool prompt (e.g. "What's the weather in Tokyo?") and verify reasoning blocks interleave with sequential tool cards (`WeatherCard`, `FlightListCard`, or the custom catchall)
|
||||
- [ ] Verify reasoning tokens stream into the custom `ReasoningBlock` slot alongside the tool cards in the same message view
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page loads without errors
|
||||
- Reasoning tokens and tool-call cards render side-by-side in a single sequential chain, each tool matched to its typed renderer
|
||||
@@ -0,0 +1,61 @@
|
||||
# QA: Tool Rendering — LangGraph (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,51 @@
|
||||
# QA: Voice Input — LangGraph (Python)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible at `/demos/voice`
|
||||
- Railway service `showcase-langgraph-python` is healthy (`/api/health` returns 200)
|
||||
- `OPENAI_API_KEY` is set on the Railway service (shared with other demos)
|
||||
- A modern browser that supports `MediaRecorder` (Chromium, Firefox, Safari 14+)
|
||||
- Microphone hardware available (required only for the mic path in section 3)
|
||||
- A bundled `public/demo-audio/sample.wav` is present (used for screenshot/preview generation; the in-app sample button no longer fetches it)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/voice`
|
||||
- [ ] Verify the page header "Voice input" is visible
|
||||
- [ ] Verify the sample-audio row (`data-testid="voice-sample-audio"`) is visible
|
||||
- [ ] Verify the caption reads `Sample: "What is the weather in Tokyo?"`
|
||||
- [ ] Verify the "Play sample" button (`data-testid="voice-sample-audio-button"`) is enabled
|
||||
- [ ] Verify `<CopilotChat />` renders a message composer (`data-testid="copilot-chat-input"`)
|
||||
- [ ] Verify the composer shows a microphone button (`data-testid="copilot-start-transcribe-button"`) — this is the authoritative signal that `transcriptionService` is mounted on `/api/copilotkit-voice`
|
||||
|
||||
### 2. Sample-audio path (no mic permission required)
|
||||
|
||||
- [ ] Click the "Play sample" button
|
||||
- [ ] Immediately, the chat textarea (`data-testid="copilot-chat-textarea"`) contains the canned phrase "What is the weather in Tokyo?" (no async round-trip; no "Transcribing…" state)
|
||||
- [ ] The button stays enabled
|
||||
- [ ] Click send (`data-testid="copilot-send-button"`)
|
||||
- [ ] Within 10 seconds, the agent responds with a weather-related tool render (WeatherCard, custom-catchall card, or default tool card — depending on which tool-rendering mode is active on this page)
|
||||
|
||||
### 3. Mic path (manual)
|
||||
|
||||
- [ ] Click the microphone button (`data-testid="copilot-start-transcribe-button"`) in the composer
|
||||
- [ ] Grant microphone permission at the browser prompt
|
||||
- [ ] Speak "Hello" clearly, then click the mic button again (now `data-testid="copilot-finish-transcribe-button"`) to stop recording
|
||||
- [ ] Within 5 seconds, the textarea contains text matching "hello" (case-insensitive)
|
||||
- [ ] Click send
|
||||
- [ ] Agent responds within 10 seconds
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
- [ ] Deny microphone permission, click the mic button
|
||||
- [ ] Verify the UI handles permission denial gracefully (no crash, mic button remains visible)
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Sample button click populates the textarea synchronously (no perceptible delay)
|
||||
- Weather-related tool response renders within 10 seconds of send
|
||||
- No console errors during the successful paths
|
||||
- Whisper transcription via the mic path returns text resembling what was spoken (deployment must have `OPENAI_API_KEY` configured)
|
||||
@@ -0,0 +1,16 @@
|
||||
copilotkit==0.1.94
|
||||
ag-ui-protocol==0.1.18
|
||||
# Backend-owned A2UI tool factory (get_a2ui_tools) used by the a2ui-recovery
|
||||
# demo to run the validate->retry recovery loop in-graph. Same pin as
|
||||
# langgraph-fastapi.
|
||||
ag-ui-langgraph==0.0.41
|
||||
langchain==1.2.15
|
||||
langchain-openai==1.1.9
|
||||
langchain-anthropic==1.4.1
|
||||
langgraph==1.1.6
|
||||
langgraph-cli[inmem]==0.4.21
|
||||
langgraph-api==0.7.101
|
||||
langsmith==0.7.33
|
||||
openai==1.109.1
|
||||
deepagents>=0.5.3,<0.6.0
|
||||
pypdf>=4.0.0,<7.0.0
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Shared validators for A2UI dynamic-schema agents.
|
||||
|
||||
The dynamic-schema flow has a secondary LLM produce a flat array of
|
||||
components. The renderer rejects entries missing `id` or `component`
|
||||
("Cannot create component root without a type" infinite-loop), so every
|
||||
agent that builds an A2UI surface dynamically needs to sanitize the
|
||||
LLM's output before forwarding it. These helpers are factored out so
|
||||
each agent's tool body stays focused on the demo-specific bits
|
||||
(catalog id, system prompt, data shape).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def sanitize_a2ui_components(raw: list) -> list[dict]:
|
||||
"""Drop entries that aren't dicts or are missing `id`/`component`."""
|
||||
return [
|
||||
c for c in raw if isinstance(c, dict) and c.get("id") and c.get("component")
|
||||
]
|
||||
|
||||
|
||||
def has_root_component(components: list[dict]) -> bool:
|
||||
"""True iff `components` contains an entry with `id == "root"`."""
|
||||
return any(c.get("id") == "root" for c in components)
|
||||
@@ -0,0 +1,430 @@
|
||||
"""_cvdiag_backend.py — schema-v1 backend CVDIAG emitter for langgraph-python.
|
||||
|
||||
This is the LGP (langgraph-python) realization of the §3 backend layer: it wires
|
||||
the 11 backend boundaries through the shared ``_shared.cvdiag_bootstrap.emit_cvdiag``
|
||||
single-source emitter. It runs ALONGSIDE the legacy free-form ``_cvdiag()`` log
|
||||
lines in ``_header_forwarding_middleware.py`` (dual-emit during the transition):
|
||||
|
||||
- legacy ``_cvdiag()`` keeps writing the human-grep ``CVDIAG component=...`` line,
|
||||
- this module writes the structured schema-v1 ``CVDIAG {json}`` envelope.
|
||||
|
||||
Guard: every emit here is gated on ``CVDIAG_BACKEND_EMITTER=1`` (default OFF). With
|
||||
the guard off this module is a pure no-op — it never validates, never writes, never
|
||||
throws into the observed boundary.
|
||||
|
||||
The 11 backend boundaries (spec §3 / §5):
|
||||
backend.request.ingress, backend.agent.enter, backend.llm.call.start,
|
||||
backend.llm.call.heartbeat (VERBOSE tier — periodic ~10s asyncio task),
|
||||
backend.llm.call.response, backend.sse.first_byte, backend.sse.event (DEBUG tier),
|
||||
backend.sse.aborted, backend.agent.exit, backend.response.complete,
|
||||
backend.error.caught.
|
||||
|
||||
Pure instrumentation: like the shared emitter, nothing here raises into the caller;
|
||||
the one place we ``await`` (the heartbeat task) is cancelled cleanly in a finally.
|
||||
|
||||
Plan unit: L1-I.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from _shared.cvdiag_bootstrap import _resolve_tier, current_tier, emit_cvdiag
|
||||
|
||||
# ── Tier gating ───────────────────────────────────────────────────────────────
|
||||
# The shared bootstrap already resolved the tier (default | verbose | debug) and
|
||||
# applied the §6 fail-closed DEBUG guard. We mirror the §6 tier matrix locally so
|
||||
# VERBOSE-only (heartbeat) and DEBUG-only (sse.event) boundaries are suppressed at
|
||||
# the wrong tier rather than relying on the consumer to filter.
|
||||
_VERBOSE_TIERS = frozenset({"verbose", "debug"})
|
||||
_DEBUG_TIERS = frozenset({"debug"})
|
||||
|
||||
_SLUG = "langgraph-python"
|
||||
_HEARTBEAT_INTERVAL_S = 10.0
|
||||
|
||||
|
||||
def emitter_enabled() -> bool:
|
||||
"""True when the schema-v1 backend emitter is armed (``CVDIAG_BACKEND_EMITTER=1``).
|
||||
|
||||
Default OFF: a missing/any-other value disables every emit in this module.
|
||||
"""
|
||||
return os.environ.get("CVDIAG_BACKEND_EMITTER") == "1"
|
||||
|
||||
|
||||
def _active_tier() -> str:
|
||||
"""Resolve the verbosity tier from a LIVE env read.
|
||||
|
||||
``emitter_enabled()`` reads ``CVDIAG_BACKEND_EMITTER`` live, so the tier MUST
|
||||
be read from the same live source — otherwise flipping ``CVDIAG_VERBOSE`` /
|
||||
``CVDIAG_DEBUG`` AFTER import arms the emitter but the tier stays frozen at
|
||||
the import-time ``setup()`` value, silently no-op'ing every verbose/debug-
|
||||
gated boundary (heartbeat, sse.event). We reuse the bootstrap's
|
||||
``_resolve_tier`` so the §6 fail-closed DEBUG guard still applies (a
|
||||
production / unresolved DEBUG request raises → degrade to the frozen tier).
|
||||
"""
|
||||
try:
|
||||
return _resolve_tier(dict(os.environ))
|
||||
except RuntimeError:
|
||||
# Fail-closed DEBUG refusal: fall back to the import-time resolved tier
|
||||
# (never silently escalate to debug in production).
|
||||
return current_tier()
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _mono_ns() -> int:
|
||||
return time.monotonic_ns()
|
||||
|
||||
|
||||
def _span_id() -> str:
|
||||
"""16 hex chars (8 random bytes) — matches SPAN_ID_PATTERN."""
|
||||
return uuid.uuid4().hex[:16]
|
||||
|
||||
|
||||
def _coerce_test_id(raw: Optional[str]) -> str:
|
||||
"""Return a schema-valid UUIDv7 ``test_id``.
|
||||
|
||||
When the inbound header carries a valid UUIDv7 we keep it (this is the
|
||||
propagation we are measuring). When it is absent/malformed we synthesize a
|
||||
deterministic-shape UUIDv7 so the envelope still validates — but the
|
||||
propagation gate measures the *inbound* presence, not this fallback.
|
||||
"""
|
||||
from _shared.cvdiag_schema import (
|
||||
TEST_ID_PATTERN,
|
||||
) # local import: cheap, avoids cycle
|
||||
import re
|
||||
|
||||
if isinstance(raw, str) and re.match(TEST_ID_PATTERN, raw):
|
||||
return raw
|
||||
# Synthesize a UUIDv7-shaped value (version nibble 7, variant 8..b).
|
||||
hexs = uuid.uuid4().hex
|
||||
return f"{hexs[0:8]}-{hexs[8:12]}-7{hexs[13:16]}-8{hexs[17:20]}-{hexs[20:32]}"
|
||||
|
||||
|
||||
def extract_test_id(headers: Dict[str, str]) -> Optional[str]:
|
||||
"""Return the inbound ``x-test-id`` header value, or ``None`` when absent.
|
||||
|
||||
This is the raw inbound value used by the propagation-reliability gate — it
|
||||
is NOT coerced/synthesized here so the gate can measure true propagation.
|
||||
"""
|
||||
raw = headers.get("x-test-id")
|
||||
return raw if isinstance(raw, str) and raw else None
|
||||
|
||||
|
||||
def _empty_edge_headers() -> Dict[str, Any]:
|
||||
return {
|
||||
"cf-ray": None,
|
||||
"cf-mitigated": None,
|
||||
"cf-cache-status": None,
|
||||
"x-railway-edge": None,
|
||||
"x-railway-request-id": None,
|
||||
"x-hikari-trace": None,
|
||||
"retry-after": None,
|
||||
"via": None,
|
||||
"server": None,
|
||||
}
|
||||
|
||||
|
||||
def _edge_headers_from(headers: Dict[str, str]) -> Dict[str, Any]:
|
||||
"""Project the inbound header bag onto the closed 9-key edge-header shape.
|
||||
|
||||
Only the 9 allow-listed keys are carried; everything else is dropped (the
|
||||
envelope's per-boundary model + EdgeHeaders ``extra=forbid`` enforce this).
|
||||
"""
|
||||
allow = _empty_edge_headers()
|
||||
for key in list(allow.keys()):
|
||||
val = headers.get(key)
|
||||
if isinstance(val, str) and val:
|
||||
allow[key] = val
|
||||
return allow
|
||||
|
||||
|
||||
def _emit(
|
||||
boundary: str,
|
||||
*,
|
||||
headers: Dict[str, str],
|
||||
trace_id: str,
|
||||
outcome: str = "ok",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
demo: str = "chat",
|
||||
tier_gate: Optional[frozenset] = None,
|
||||
) -> None:
|
||||
"""Build + emit one schema-v1 envelope, guarded + tier-filtered.
|
||||
|
||||
No-op when the emitter is disabled OR the boundary's tier gate excludes the
|
||||
resolved tier. Never raises (delegates to the shared emitter's safety).
|
||||
"""
|
||||
if not emitter_enabled():
|
||||
return
|
||||
if tier_gate is not None and _active_tier() not in tier_gate:
|
||||
return
|
||||
envelope = {
|
||||
"schema_version": 1,
|
||||
"test_id": _coerce_test_id(headers.get("x-test-id")),
|
||||
"trace_id": trace_id,
|
||||
"span_id": _span_id(),
|
||||
"parent_span_id": None,
|
||||
"layer": "backend",
|
||||
"boundary": boundary,
|
||||
"slug": _SLUG,
|
||||
"demo": demo,
|
||||
"ts": _now_iso(),
|
||||
"mono_ns": _mono_ns(),
|
||||
"duration_ms": duration_ms,
|
||||
"outcome": outcome,
|
||||
"edge_headers": _edge_headers_from(headers),
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
emit_cvdiag(envelope)
|
||||
|
||||
|
||||
class CvdiagBackendRun:
|
||||
"""Per-model-call CVDIAG run context for the LGP middleware.
|
||||
|
||||
Constructed inside ``awrap_model_call`` (and the sync ``wrap_model_call``);
|
||||
owns the trace correlation id, the ingress monotonic anchor, and the
|
||||
heartbeat asyncio task. All methods are no-ops when the emitter is disabled.
|
||||
"""
|
||||
|
||||
def __init__(self, headers: Dict[str, str]) -> None:
|
||||
self._headers = dict(headers)
|
||||
# Correlate every boundary in this run under one trace_id. Prefer the
|
||||
# inbound x-diag-run-id breadcrumb so probe/backend rows join; fall back
|
||||
# to a synthesized id.
|
||||
self._trace_id = (
|
||||
headers.get("x-diag-run-id") or headers.get("x-test-id") or uuid.uuid4().hex
|
||||
)
|
||||
self._ingress_mono = _mono_ns()
|
||||
self._first_byte_emitted = False
|
||||
self._sse_seq = 0
|
||||
self._heartbeat_task: Optional[asyncio.Task] = None
|
||||
|
||||
# ── Lifecycle boundaries ──────────────────────────────────────────────
|
||||
def request_ingress(self) -> None:
|
||||
_emit(
|
||||
"backend.request.ingress",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
metadata={"method": "POST", "path": "/threads", "content_length": None},
|
||||
tier_gate=_VERBOSE_TIERS,
|
||||
)
|
||||
|
||||
def agent_enter(
|
||||
self, agent_name: Optional[str] = None, model_id: Optional[str] = None
|
||||
) -> None:
|
||||
_emit(
|
||||
"backend.agent.enter",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
metadata={"agent_name": agent_name, "model_id": model_id},
|
||||
)
|
||||
|
||||
def llm_call_start(
|
||||
self, provider: Optional[str] = None, model: Optional[str] = None
|
||||
) -> None:
|
||||
_emit(
|
||||
"backend.llm.call.start",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
metadata={
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"prompt_token_count_estimate": None,
|
||||
},
|
||||
tier_gate=_VERBOSE_TIERS,
|
||||
)
|
||||
|
||||
def llm_call_response(
|
||||
self,
|
||||
provider: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
latency_ms: Optional[int] = None,
|
||||
error_class: Optional[str] = None,
|
||||
) -> None:
|
||||
_emit(
|
||||
"backend.llm.call.response",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
outcome="err" if error_class else "ok",
|
||||
metadata={
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"response_token_count": None,
|
||||
"latency_ms": latency_ms,
|
||||
"error_class": error_class,
|
||||
},
|
||||
tier_gate=_VERBOSE_TIERS,
|
||||
)
|
||||
|
||||
def sse_first_byte(self) -> None:
|
||||
"""Emit ``backend.sse.first_byte`` once, with the ingress→first-byte delta."""
|
||||
if self._first_byte_emitted:
|
||||
return
|
||||
self._first_byte_emitted = True
|
||||
delta_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
|
||||
_emit(
|
||||
"backend.sse.first_byte",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
metadata={"delta_ms_from_ingress": delta_ms},
|
||||
tier_gate=_VERBOSE_TIERS,
|
||||
)
|
||||
|
||||
def sse_event(
|
||||
self, event_type: Optional[str] = None, payload_size_bytes: Optional[int] = None
|
||||
) -> None:
|
||||
"""Emit ``backend.sse.event`` (DEBUG tier — suppressed below debug)."""
|
||||
self._sse_seq += 1
|
||||
_emit(
|
||||
"backend.sse.event",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
metadata={
|
||||
"event_type": event_type,
|
||||
"payload_size_bytes": payload_size_bytes,
|
||||
"sequence_num": self._sse_seq,
|
||||
},
|
||||
tier_gate=_DEBUG_TIERS,
|
||||
)
|
||||
|
||||
def sse_aborted(
|
||||
self,
|
||||
termination_kind: Optional[str] = None,
|
||||
bytes_before_abort: Optional[int] = None,
|
||||
) -> None:
|
||||
_emit(
|
||||
"backend.sse.aborted",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
outcome="err",
|
||||
metadata={
|
||||
"termination_kind": termination_kind,
|
||||
"bytes_before_abort": bytes_before_abort,
|
||||
},
|
||||
)
|
||||
|
||||
def agent_exit(self, terminal_outcome: str = "ok") -> None:
|
||||
total_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
|
||||
_emit(
|
||||
"backend.agent.exit",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
outcome="err" if terminal_outcome == "err" else "ok",
|
||||
metadata={
|
||||
"terminal_outcome": terminal_outcome,
|
||||
"total_duration_ms": total_ms,
|
||||
},
|
||||
)
|
||||
|
||||
def response_complete(
|
||||
self,
|
||||
http_status: Optional[int] = 200,
|
||||
sse_event_count: Optional[int] = None,
|
||||
) -> None:
|
||||
total_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
|
||||
_emit(
|
||||
"backend.response.complete",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
metadata={
|
||||
"http_status": http_status,
|
||||
"content_length": None,
|
||||
"total_duration_ms": total_ms,
|
||||
"sse_event_count": sse_event_count
|
||||
if sse_event_count is not None
|
||||
else self._sse_seq,
|
||||
},
|
||||
)
|
||||
|
||||
def error_caught(self, exc: BaseException) -> None:
|
||||
_emit(
|
||||
"backend.error.caught",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
outcome="err",
|
||||
metadata={
|
||||
"exception_type": type(exc).__name__,
|
||||
"message_scrubbed": "<scrubbed>",
|
||||
"stack_brief": None,
|
||||
"truncated": False,
|
||||
},
|
||||
)
|
||||
|
||||
# ── Heartbeat (VERBOSE tier — periodic asyncio task) ──────────────────
|
||||
async def _heartbeat_loop(self) -> None:
|
||||
"""Emit ``backend.llm.call.heartbeat`` every ~10s while the LLM call runs."""
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(_HEARTBEAT_INTERVAL_S)
|
||||
elapsed_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
|
||||
_emit(
|
||||
"backend.llm.call.heartbeat",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
outcome="info",
|
||||
metadata={"elapsed_ms_since_start": elapsed_ms},
|
||||
tier_gate=_VERBOSE_TIERS,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
# Clean cancellation when the LLM call finishes — swallow.
|
||||
return
|
||||
|
||||
def start_heartbeat(self) -> None:
|
||||
"""Arm the heartbeat task (no-op when disabled or below VERBOSE tier)."""
|
||||
if not emitter_enabled() or _active_tier() not in _VERBOSE_TIERS:
|
||||
return
|
||||
if self._heartbeat_task is not None:
|
||||
return
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return
|
||||
self._heartbeat_task = loop.create_task(self._heartbeat_loop())
|
||||
|
||||
async def stop_heartbeat(self) -> None:
|
||||
"""Cancel + await the heartbeat task. Safe to call when never started.
|
||||
|
||||
Cooperative cancellation: the legacy ``except (CancelledError,
|
||||
Exception)`` swallowed the CALLER's CancelledError, breaking cooperative
|
||||
cancellation (a client-disconnect / request-cancel that arrives while we
|
||||
await the heartbeat task would be lost). We suppress ONLY the heartbeat
|
||||
task's OWN cancellation — the one we just requested — and re-raise when
|
||||
THIS task is being cancelled by the caller (a pending cancellation
|
||||
request, ``current_task().cancelling() > 0``). ``Task.cancelling()`` is
|
||||
3.11+ (production runs 3.12); on older runtimes the attribute is absent
|
||||
and we degrade to suppressing (the legacy behavior).
|
||||
"""
|
||||
task = self._heartbeat_task
|
||||
if task is None:
|
||||
return
|
||||
self._heartbeat_task = None
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
current = asyncio.current_task()
|
||||
cancelling = getattr(current, "cancelling", None)
|
||||
if current is not None and cancelling is not None and cancelling() > 0:
|
||||
raise
|
||||
except Exception: # noqa: BLE001 - heartbeat body must never throw out
|
||||
return
|
||||
|
||||
def emit_heartbeat_once(self) -> None:
|
||||
"""Synchronous single heartbeat emit (used by the sync wrap path + tests)."""
|
||||
elapsed_ms = int((_mono_ns() - self._ingress_mono) / 1_000_000)
|
||||
_emit(
|
||||
"backend.llm.call.heartbeat",
|
||||
headers=self._headers,
|
||||
trace_id=self._trace_id,
|
||||
outcome="info",
|
||||
metadata={"elapsed_ms_since_start": elapsed_ms},
|
||||
tier_gate=_VERBOSE_TIERS,
|
||||
)
|
||||
@@ -0,0 +1,272 @@
|
||||
"""Minimal header-forwarding-only AgentMiddleware.
|
||||
|
||||
Some showcase demos (reasoning, tool-rendering-reasoning-chain, the
|
||||
sub-agents in `subagents.py`) intentionally avoid the full
|
||||
`CopilotKitMiddleware` because they don't need its frontend-tool
|
||||
injection, App-Context surfacing, or state-note features — they're
|
||||
minimal demos of LangGraph capabilities.
|
||||
|
||||
But every showcase request goes through aimock (the locally-served
|
||||
LLM mock), and aimock requires the ``x-aimock-context`` header (and
|
||||
friends) on every ``/v1/responses`` and ``/v1/chat/completions``
|
||||
request to match the right fixture. Without middleware to populate
|
||||
the header-forwarding ContextVar from the LangGraph RunnableConfig
|
||||
``configurable``, those requests go out without the header and aimock
|
||||
returns 404, breaking the demo.
|
||||
|
||||
This middleware does ONLY that header propagation — nothing else.
|
||||
It reuses copilotkit's own primitives (kept private but exported by
|
||||
the installed package at the module level) so the propagation logic
|
||||
is identical to the full middleware. No App-Context injection, no
|
||||
tool-merging, no state-to-prompt surfacing, no Bedrock message
|
||||
fix-up.
|
||||
|
||||
CVDIAG instrumentation (diagnostic only — DOES NOT change WHERE
|
||||
headers come from): after the existing
|
||||
``_extract_forwarded_headers_from_config()`` populates copilotkit's
|
||||
forwarded-headers ContextVar, we read it back via
|
||||
``get_forwarded_headers()`` and emit a structured ``CVDIAG`` log line
|
||||
at the configurable-read boundary recording whether
|
||||
``x-aimock-context`` actually arrived on the LangGraph configurable
|
||||
channel (``header_present=false`` is the alarm we are hunting). We
|
||||
also append this layer's hop tag to ``x-diag-hops`` on the SAME
|
||||
ContextVar the httpx hook already forwards from — so the breadcrumb
|
||||
and correlation headers (``x-diag-run-id``, ``x-diag-hops``) ride
|
||||
along on the outbound LLM call exactly the way ``x-aimock-context``
|
||||
does, without introducing any new forwarding source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Awaitable, Callable, Dict
|
||||
|
||||
from langchain.agents.middleware import (
|
||||
AgentMiddleware,
|
||||
AgentState,
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
)
|
||||
|
||||
# Reuse the installed copilotkit's existing header-forwarding helpers so
|
||||
# the behaviour stays bit-identical to the full CopilotKitMiddleware's
|
||||
# header-propagation step. These are module-level functions in
|
||||
# copilotkit 0.1.94's copilotkit_lg_middleware module.
|
||||
from copilotkit.copilotkit_lg_middleware import (
|
||||
_extract_forwarded_headers_from_config,
|
||||
_ensure_httpx_hook,
|
||||
)
|
||||
|
||||
# CVDIAG-only: read/append the forwarded-header ContextVar copilotkit
|
||||
# already populates. set_forwarded_headers is used SOLELY to append the
|
||||
# diagnostic hop breadcrumb onto the SAME channel x-aimock-context rides;
|
||||
# it does not introduce a new forwarding source.
|
||||
from copilotkit.header_propagation import (
|
||||
get_forwarded_headers,
|
||||
set_forwarded_headers,
|
||||
)
|
||||
|
||||
# CVDIAG schema-v1 backend emitter (L1-I). Dual-emit: this rides ALONGSIDE the
|
||||
# legacy free-form _cvdiag() log lines below — it writes the structured
|
||||
# schema-v1 CVDIAG envelope through the shared single-source emitter, guarded by
|
||||
# CVDIAG_BACKEND_EMITTER (default OFF). With the guard off it is a pure no-op.
|
||||
from src.agents._cvdiag_backend import CvdiagBackendRun
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CVDIAG_COMPONENT = "backend-langgraph-py"
|
||||
_CVDIAG_HOP_TAG = "backend-langgraph-py"
|
||||
|
||||
|
||||
def _cvdiag(
|
||||
boundary: str,
|
||||
headers: Dict[str, str],
|
||||
status: str,
|
||||
*,
|
||||
hop: Any = "-",
|
||||
error: str = "",
|
||||
) -> None:
|
||||
"""Emit a single CVDIAG log line in the shared cross-language convention.
|
||||
|
||||
Never logs full header values — only a 12-char prefix of
|
||||
``x-aimock-context``.
|
||||
"""
|
||||
slug = headers.get("x-aimock-context")
|
||||
header_present = isinstance(slug, str) and len(slug) > 0
|
||||
run_id = headers.get("x-diag-run-id", "none")
|
||||
test_id = headers.get("x-test-id", "none")
|
||||
prefix = slug[:12] if header_present else ""
|
||||
logger.info(
|
||||
"CVDIAG component=%s boundary=%s run_id=%s slug=%s "
|
||||
"header_present=%s header_value_prefix=%s hop=%s status=%s "
|
||||
"test_id=%s error=%s",
|
||||
_CVDIAG_COMPONENT,
|
||||
boundary,
|
||||
run_id,
|
||||
slug if header_present else "MISSING",
|
||||
str(header_present).lower(),
|
||||
prefix,
|
||||
hop,
|
||||
status,
|
||||
test_id,
|
||||
error,
|
||||
)
|
||||
|
||||
|
||||
def _instrument_and_breadcrumb() -> None:
|
||||
"""Read the configurable-read result, log it, and append the diag hop.
|
||||
|
||||
Called immediately AFTER
|
||||
``_extract_forwarded_headers_from_config()`` has populated the
|
||||
ContextVar. Reads the headers back, emits the configurable-read
|
||||
CVDIAG line (wrapping the previously-silent "no x-aimock-context in
|
||||
configurable" case as an alarm), then — only when x-aimock-context
|
||||
is present — appends this layer's hop tag to ``x-diag-hops`` on the
|
||||
SAME ContextVar so the breadcrumb rides the existing forwarding path.
|
||||
"""
|
||||
headers = dict(get_forwarded_headers())
|
||||
has_context = (
|
||||
isinstance(headers.get("x-aimock-context"), str)
|
||||
and len(headers.get("x-aimock-context", "")) > 0
|
||||
)
|
||||
|
||||
if has_context:
|
||||
_cvdiag("configurable-read", headers, "ok")
|
||||
else:
|
||||
# The alarm we are hunting: the configurable channel reached this
|
||||
# middleware without x-aimock-context. Surface it instead of the
|
||||
# previous silent no-op.
|
||||
_cvdiag(
|
||||
"configurable-read",
|
||||
headers,
|
||||
"miss" if headers else "error",
|
||||
error="x-aimock-context-absent-in-configurable"
|
||||
if headers
|
||||
else "no-forwarded-headers-in-configurable",
|
||||
)
|
||||
# Nothing to breadcrumb onto — do not invent a forwarding source.
|
||||
return
|
||||
|
||||
# Append this layer's hop tag to x-diag-hops on the SAME ContextVar the
|
||||
# httpx hook forwards from. This rides the existing path; no new source.
|
||||
existing_hops = headers.get("x-diag-hops", "")
|
||||
headers["x-diag-hops"] = (
|
||||
f"{existing_hops},{_CVDIAG_HOP_TAG}"
|
||||
if isinstance(existing_hops, str) and existing_hops
|
||||
else _CVDIAG_HOP_TAG
|
||||
)
|
||||
set_forwarded_headers(headers)
|
||||
|
||||
hop = len([h for h in headers["x-diag-hops"].split(",") if h])
|
||||
_cvdiag("outbound-llm", headers, "ok", hop=hop)
|
||||
|
||||
|
||||
class HeaderForwardingMiddleware(AgentMiddleware[AgentState, Any]):
|
||||
"""AgentMiddleware that only forwards inbound x-* headers.
|
||||
|
||||
Behaviourally a no-op except for two calls inside both
|
||||
``wrap_model_call`` and ``awrap_model_call``:
|
||||
|
||||
1. ``_extract_forwarded_headers_from_config()`` — read the
|
||||
``x-*`` keys from the active LangGraph RunnableConfig
|
||||
(``context`` and ``configurable``) and populate the
|
||||
header-forwarding ContextVar.
|
||||
2. ``_ensure_httpx_hook(request.model)`` — install copilotkit's
|
||||
httpx event hook on the model's underlying HTTP client(s)
|
||||
so the next outgoing LLM request picks the headers up.
|
||||
|
||||
No App-Context injection, no tool-merging, no state-surfacing,
|
||||
no Bedrock message fix-up — strictly header propagation.
|
||||
|
||||
CVDIAG: ``_instrument_and_breadcrumb()`` is inserted between the
|
||||
two steps purely to OBSERVE the configurable-read boundary and tag
|
||||
the existing breadcrumb. It does not change where headers come from.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "HeaderForwardingMiddleware"
|
||||
|
||||
def wrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], ModelResponse],
|
||||
) -> ModelResponse:
|
||||
_extract_forwarded_headers_from_config()
|
||||
_instrument_and_breadcrumb()
|
||||
_ensure_httpx_hook(request.model)
|
||||
|
||||
# CVDIAG schema-v1 dual-emit (L1-I). No-op when CVDIAG_BACKEND_EMITTER off.
|
||||
headers = dict(get_forwarded_headers())
|
||||
run = CvdiagBackendRun(headers)
|
||||
model_name = _model_name(request)
|
||||
run.request_ingress()
|
||||
run.agent_enter(agent_name=self.name, model_id=model_name)
|
||||
run.llm_call_start(provider="langchain", model=model_name)
|
||||
run.emit_heartbeat_once()
|
||||
start_ns = time.monotonic_ns()
|
||||
try:
|
||||
response = handler(request)
|
||||
except BaseException as exc: # noqa: BLE001 - re-raised after observing
|
||||
run.error_caught(exc)
|
||||
run.agent_exit(terminal_outcome="err")
|
||||
raise
|
||||
latency_ms = int((time.monotonic_ns() - start_ns) / 1_000_000)
|
||||
run.llm_call_response(
|
||||
provider="langchain", model=model_name, latency_ms=latency_ms
|
||||
)
|
||||
run.sse_first_byte()
|
||||
run.sse_event(event_type="response", payload_size_bytes=None)
|
||||
run.agent_exit(terminal_outcome="ok")
|
||||
run.response_complete(http_status=200)
|
||||
return response
|
||||
|
||||
async def awrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
||||
) -> ModelResponse:
|
||||
_extract_forwarded_headers_from_config()
|
||||
_instrument_and_breadcrumb()
|
||||
_ensure_httpx_hook(request.model)
|
||||
|
||||
# CVDIAG schema-v1 dual-emit (L1-I). No-op when CVDIAG_BACKEND_EMITTER off.
|
||||
headers = dict(get_forwarded_headers())
|
||||
run = CvdiagBackendRun(headers)
|
||||
model_name = _model_name(request)
|
||||
run.request_ingress()
|
||||
run.agent_enter(agent_name=self.name, model_id=model_name)
|
||||
run.llm_call_start(provider="langchain", model=model_name)
|
||||
run.start_heartbeat()
|
||||
start_ns = time.monotonic_ns()
|
||||
try:
|
||||
response = await handler(request)
|
||||
except BaseException as exc: # noqa: BLE001 - re-raised after observing
|
||||
await run.stop_heartbeat()
|
||||
run.error_caught(exc)
|
||||
run.agent_exit(terminal_outcome="err")
|
||||
raise
|
||||
await run.stop_heartbeat()
|
||||
latency_ms = int((time.monotonic_ns() - start_ns) / 1_000_000)
|
||||
run.llm_call_response(
|
||||
provider="langchain", model=model_name, latency_ms=latency_ms
|
||||
)
|
||||
run.sse_first_byte()
|
||||
run.sse_event(event_type="response", payload_size_bytes=None)
|
||||
run.agent_exit(terminal_outcome="ok")
|
||||
run.response_complete(http_status=200)
|
||||
return response
|
||||
|
||||
|
||||
def _model_name(request: ModelRequest) -> str:
|
||||
"""Best-effort model identifier off the ModelRequest (never raises)."""
|
||||
try:
|
||||
model = getattr(request, "model", None)
|
||||
for attr in ("model_name", "model", "model_id"):
|
||||
val = getattr(model, attr, None)
|
||||
if isinstance(val, str) and val:
|
||||
return val
|
||||
except Exception: # noqa: BLE001 - instrumentation must not throw
|
||||
pass
|
||||
return "unknown"
|
||||
@@ -0,0 +1,80 @@
|
||||
"""LangGraph agent for the Declarative Generative UI (A2UI — Dynamic Schema) demo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
from langchain.agents import create_agent
|
||||
from langchain.tools import tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
|
||||
# Cross-reference: showcase/integrations/google-adk/src/agents/declarative_gen_ui_agent.py
|
||||
# Both integrations register the same a2ui catalog (Card / Row / Column /
|
||||
# Text / Metric / PieChart / BarChart / DataTable / StatusBadge / InfoRow /
|
||||
# PrimaryButton — see each integration's
|
||||
# src/app/demos/declarative-gen-ui/a2ui/definitions.ts, which are
|
||||
# byte-identical across LP and ADK).
|
||||
#
|
||||
# The fictional sales dataset and the per-question composition rules
|
||||
# are injected via App Context from
|
||||
# showcase/integrations/langgraph-python/src/app/demos/declarative-gen-ui/sales-context.ts
|
||||
# (a frontend file shared byte-for-byte with the ADK integration — see
|
||||
# its DUPLICATION NOTICE).
|
||||
#
|
||||
# Keep this SYSTEM_PROMPT and the ADK `_INSTRUCTION` aligned in spirit.
|
||||
# Minor wording differences are tolerated (e.g. this prompt uses shape
|
||||
# words — "table"/"pie"/"bar" — as question-category descriptors, while
|
||||
# ADK names the rendered components — "DataTable"/"PieChart"/"BarChart"
|
||||
# — in the analogous slot), but the structural rules and the component
|
||||
# name set must match the catalog above.
|
||||
SYSTEM_PROMPT = (
|
||||
"You are the embedded sales analyst for Vantage Threads, the fictional "
|
||||
"B2B apparel company described in your App Context. Answer every "
|
||||
"business question by calling `generate_a2ui` to draw a rich visual "
|
||||
"surface, and keep the chat reply to one short sentence.\n"
|
||||
"\n"
|
||||
"Ground every number in the sales dataset from App Context — never "
|
||||
"invent figures that contradict it. Follow the dashboard composition "
|
||||
"rules from App Context when choosing components: pick the component "
|
||||
"by the shape of the question (snapshot → composed KPI dashboard with "
|
||||
"charts; team performance → table; risk → status badges; single "
|
||||
"account → info rows; part-of-whole → pie; trend/comparison → bar). "
|
||||
"Never ask the user which chart they want. `generate_a2ui` takes no "
|
||||
"arguments and handles the rendering automatically. Compose "
|
||||
"generously — a dashboard should feel like a real analytics product, "
|
||||
"not a single widget."
|
||||
)
|
||||
|
||||
|
||||
@tool
|
||||
def generate_a2ui() -> dict:
|
||||
"""Generate a dynamic A2UI dashboard surface from the current conversation.
|
||||
|
||||
Takes no arguments. The CopilotKit runtime middleware
|
||||
(`a2ui.injectA2UITool: true`) intercepts the call and drives a
|
||||
secondary-LLM `render_a2ui` planner to emit the surface ops; this
|
||||
Python body should NEVER execute in normal operation. It exists only
|
||||
so the LP agent's declared `tools=` list mirrors the ADK sibling
|
||||
(`declarative_gen_ui_agent.py`) and the SYSTEM_PROMPT's
|
||||
`generate_a2ui` reference resolves to a registered tool name.
|
||||
|
||||
If this body actually runs, the CopilotKit a2ui middleware is
|
||||
misconfigured and silently returning an empty surface would hide the
|
||||
real bug — fail loud per `fail-loud-discipline`.
|
||||
"""
|
||||
raise RuntimeError(
|
||||
"generate_a2ui called directly — CopilotKit a2ui.injectA2UITool "
|
||||
"middleware should intercept this call before it reaches the "
|
||||
"agent. Check the route configuration at "
|
||||
"app/api/copilotkit-declarative-gen-ui/route.ts."
|
||||
)
|
||||
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model=os.getenv("OPENAI_MODEL", "gpt-4o")),
|
||||
tools=[generate_a2ui],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
LangGraph agent for the Declarative Generative UI (A2UI — Fixed Schema) demo.
|
||||
|
||||
Fixed-schema A2UI: the component tree (schema) is authored ahead of time as
|
||||
JSON and loaded at startup via `a2ui.load_schema(...)`. The agent only
|
||||
streams *data* into the data model at runtime. The frontend registers a
|
||||
matching catalog (see `src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts`)
|
||||
that pins the schema's component names to real React implementations.
|
||||
|
||||
Reference:
|
||||
examples/integrations/langgraph-python/agent/src/a2ui_fixed_schema.py
|
||||
"""
|
||||
|
||||
# @region[backend-render-operations]
|
||||
# @region[backend-schema-json-load]
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
from copilotkit import CopilotKitMiddleware, a2ui
|
||||
from langchain.agents import create_agent
|
||||
from langchain.tools import tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
CATALOG_ID = "copilotkit://flight-fixed-catalog"
|
||||
SURFACE_ID = "flight-fixed-schema"
|
||||
|
||||
_SCHEMAS_DIR = Path(__file__).parent / "a2ui_schemas"
|
||||
|
||||
# The schema is JSON so it can be authored and reviewed independently of the
|
||||
# Python code. `a2ui.load_schema` is just a thin `json.load` wrapper.
|
||||
FLIGHT_SCHEMA = a2ui.load_schema(_SCHEMAS_DIR / "flight_schema.json")
|
||||
# @endregion[backend-schema-json-load]
|
||||
|
||||
|
||||
class Flight(TypedDict):
|
||||
"""Shape the LLM should fill in when calling `display_flight`.
|
||||
|
||||
LangGraph serializes this TypedDict into the tool's JSON schema, so
|
||||
defining it narrowly is how we steer the LLM to produce data that fits
|
||||
the frontend `FlightCard` component's props.
|
||||
"""
|
||||
|
||||
origin: str
|
||||
destination: str
|
||||
airline: str
|
||||
price: str
|
||||
|
||||
|
||||
@tool
|
||||
def display_flight(origin: str, destination: str, airline: str, price: str) -> str:
|
||||
"""Show a flight card for the given trip.
|
||||
|
||||
Use short airport codes (e.g. "SFO", "JFK") for origin/destination and a
|
||||
price string like "$289".
|
||||
|
||||
After this tool returns, the flight card is already rendered to the user
|
||||
via the A2UI surface — the JSON returned here is the surface descriptor
|
||||
the renderer consumes, NOT a status code. Do NOT call this tool again
|
||||
for the same flight (the user already sees the card). Reply with one
|
||||
short confirmation sentence and stop.
|
||||
"""
|
||||
# The A2UI middleware detects the `a2ui_operations` container in this
|
||||
# tool result and forwards the ops to the frontend renderer. The frontend
|
||||
# catalog resolves component names to the local React components.
|
||||
#
|
||||
# Note: schema-swap-on-action (e.g. swapping to a "booked" schema when
|
||||
# the card's button is clicked) will be added once the Python SDK
|
||||
# exposes `action_handlers=` on `a2ui.render`.
|
||||
return a2ui.render(
|
||||
operations=[
|
||||
a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID),
|
||||
a2ui.update_components(SURFACE_ID, FLIGHT_SCHEMA),
|
||||
a2ui.update_data_model(
|
||||
SURFACE_ID,
|
||||
{
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
"airline": airline,
|
||||
"price": price,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
# @endregion[backend-render-operations]
|
||||
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[display_flight],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=(
|
||||
"You help users find flights. When asked about a flight, call "
|
||||
"`display_flight` exactly ONCE with origin, destination, airline, "
|
||||
"and price. The tool's JSON return value is an A2UI surface "
|
||||
"descriptor — the flight card is already rendered to the user; do "
|
||||
"NOT call `display_flight` again for the same trip. After the tool "
|
||||
"returns, reply with one short confirmation sentence and stop."
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
[
|
||||
{
|
||||
"id": "root",
|
||||
"component": "Card",
|
||||
"child": "content"
|
||||
},
|
||||
{
|
||||
"id": "content",
|
||||
"component": "Column",
|
||||
"children": ["title", "route", "meta", "bookButton"]
|
||||
},
|
||||
{
|
||||
"id": "title",
|
||||
"component": "Title",
|
||||
"text": "Flight Details"
|
||||
},
|
||||
{
|
||||
"id": "route",
|
||||
"component": "Row",
|
||||
"justify": "spaceBetween",
|
||||
"align": "center",
|
||||
"children": ["from", "arrow", "to"]
|
||||
},
|
||||
{
|
||||
"id": "from",
|
||||
"component": "Airport",
|
||||
"code": { "path": "/origin" }
|
||||
},
|
||||
{
|
||||
"id": "arrow",
|
||||
"component": "Arrow"
|
||||
},
|
||||
{
|
||||
"id": "to",
|
||||
"component": "Airport",
|
||||
"code": { "path": "/destination" }
|
||||
},
|
||||
{
|
||||
"id": "meta",
|
||||
"component": "Row",
|
||||
"justify": "spaceBetween",
|
||||
"align": "center",
|
||||
"children": ["airline", "price"]
|
||||
},
|
||||
{
|
||||
"id": "airline",
|
||||
"component": "AirlineBadge",
|
||||
"name": { "path": "/airline" }
|
||||
},
|
||||
{
|
||||
"id": "price",
|
||||
"component": "PriceTag",
|
||||
"amount": { "path": "/price" }
|
||||
},
|
||||
{
|
||||
"id": "bookButton",
|
||||
"component": "Button",
|
||||
"variant": "primary",
|
||||
"child": "bookButtonLabel",
|
||||
"action": {
|
||||
"event": {
|
||||
"name": "book_flight",
|
||||
"context": {
|
||||
"origin": { "path": "/origin" },
|
||||
"destination": { "path": "/destination" },
|
||||
"airline": { "path": "/airline" },
|
||||
"price": { "path": "/price" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bookButtonLabel",
|
||||
"component": "Text",
|
||||
"text": "Book flight"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
"""LangGraph agent backing the Agent Config Object demo.
|
||||
|
||||
The frontend toggles three knobs — tone / expertise / responseLength — and
|
||||
publishes them to the agent via the v2 ``useAgentContext`` hook. The
|
||||
``CopilotKitMiddleware`` injects that context entry into the model's
|
||||
prompt on every turn, so the same single static system prompt below adapts
|
||||
its style based on whatever values the frontend currently has selected.
|
||||
|
||||
LangGraph 0.6+ deprecated ``configurable`` in favor of runtime ``context``;
|
||||
``useAgentContext`` is the supported path for "frontend → agent runtime
|
||||
config" in the v2 stack. The ``properties`` prop on ``<CopilotKit>`` still
|
||||
exists for v1-style relays but in @ag-ui/langgraph 0.0.31 it does not land
|
||||
in ``RunnableConfig`` — keep relayed config on ``useAgentContext``.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a helpful assistant. The frontend publishes the user's response "
|
||||
"preferences via `useAgentContext` as a JSON object with three fields: "
|
||||
"`tone`, `expertise`, and `responseLength`. Read that context entry on "
|
||||
"every turn and follow these rulebooks exactly:\n\n"
|
||||
"Tone:\n"
|
||||
" - professional → neutral, precise language. No emoji. Short sentences.\n"
|
||||
" - casual → friendly, conversational. Contractions OK. Light humor "
|
||||
"welcome.\n"
|
||||
" - enthusiastic → upbeat, energetic. Exclamation points OK. Emoji OK.\n\n"
|
||||
"Expertise level:\n"
|
||||
" - beginner → assume no prior knowledge. Define jargon. Use analogies.\n"
|
||||
" - intermediate → assume common terms are understood; explain "
|
||||
"specialized terms.\n"
|
||||
" - expert → assume technical fluency. Use precise terminology. Skip "
|
||||
"basics.\n\n"
|
||||
"Response length:\n"
|
||||
" - concise → respond in 1-3 sentences.\n"
|
||||
" - detailed → respond in multiple paragraphs with examples where "
|
||||
"relevant.\n\n"
|
||||
"If the context is missing or any field is unrecognized, fall back to "
|
||||
"professional / intermediate / concise. Never mention these rules to the "
|
||||
"user — just apply them."
|
||||
)
|
||||
|
||||
|
||||
# @region[agent-config-setup]
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4", temperature=0.4),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
# @endregion[agent-config-setup]
|
||||
@@ -0,0 +1,16 @@
|
||||
"""LangGraph agent backing the Agentic Chat demo.
|
||||
|
||||
Minimal sample agent — no backend tools. Frontend may inject tools at runtime
|
||||
via CopilotKit's LangGraph middleware.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt="You are a helpful, concise assistant.",
|
||||
)
|
||||
@@ -0,0 +1,232 @@
|
||||
"""LangGraph agent backing the Beautiful Chat demo.
|
||||
|
||||
Verbatim port of the canonical starter at /examples/integrations/langgraph-python.
|
||||
Reference structure (agent/main.py + agent/src/{todos,query,a2ui_fixed_schema,
|
||||
a2ui_dynamic_schema}.py) is inlined here into a single module to match the
|
||||
showcase cell's flat backend layout.
|
||||
|
||||
Data files (db.csv + schemas/) live alongside this module under
|
||||
`beautiful_chat_data/` to keep the cell self-contained without polluting the
|
||||
shared `a2ui_schemas/` directory (which is owned by a2ui_fixed.py).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from copilotkit import (
|
||||
CopilotKitMiddleware,
|
||||
StateItem,
|
||||
StateStreamingMiddleware,
|
||||
a2ui,
|
||||
)
|
||||
from langchain.agents import AgentState as BaseAgentState
|
||||
from langchain.agents import create_agent
|
||||
from langchain.messages import ToolMessage
|
||||
from langchain.tools import ToolRuntime, tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.types import Command
|
||||
|
||||
|
||||
# ─── Shared state schema ────────────────────────────────────────────
|
||||
|
||||
|
||||
class Todo(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
description: str
|
||||
emoji: str
|
||||
status: Literal["pending", "completed"]
|
||||
|
||||
|
||||
class AgentState(BaseAgentState):
|
||||
todos: list[Todo]
|
||||
|
||||
|
||||
# ─── Todo tools ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@tool
|
||||
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
|
||||
"""
|
||||
Manage the current todos.
|
||||
"""
|
||||
# Ensure all todos have IDs that are unique
|
||||
for todo in todos:
|
||||
if "id" not in todo or not todo["id"]:
|
||||
todo["id"] = str(uuid.uuid4())
|
||||
|
||||
# Update the state
|
||||
return Command(
|
||||
update={
|
||||
"todos": todos,
|
||||
"messages": [
|
||||
ToolMessage(
|
||||
content="Successfully updated todos",
|
||||
name="manage_todos",
|
||||
id=str(uuid.uuid4()),
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@tool
|
||||
def get_todos(runtime: ToolRuntime):
|
||||
"""
|
||||
Get the current todos.
|
||||
"""
|
||||
return runtime.state.get("todos", [])
|
||||
|
||||
|
||||
todo_tools = [
|
||||
manage_todos,
|
||||
get_todos,
|
||||
]
|
||||
|
||||
|
||||
# ─── Data query tool ────────────────────────────────────────────────
|
||||
|
||||
# Read data at module load time to avoid file I/O issues in
|
||||
# LangGraph Cloud's sandboxed tool execution environment.
|
||||
_DATA_DIR = Path(__file__).parent / "beautiful_chat_data"
|
||||
_csv_path = _DATA_DIR / "db.csv"
|
||||
with open(_csv_path) as _f:
|
||||
_cached_data = list(csv.DictReader(_f))
|
||||
|
||||
|
||||
@tool
|
||||
def query_data(query: str):
|
||||
"""
|
||||
Query the database, takes natural language. Always call before showing a chart or graph.
|
||||
"""
|
||||
return _cached_data
|
||||
|
||||
|
||||
# ─── A2UI fixed-schema tool: flight search ──────────────────────────
|
||||
|
||||
CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
SURFACE_ID = "flight-search-results"
|
||||
|
||||
|
||||
class Flight(TypedDict, total=False):
|
||||
# All fields marked optional (`total=False`) so the LLM (or aimock fixture)
|
||||
# can omit auxiliary fields like `id` / `statusIcon` without tripping
|
||||
# langchain's tool-arg validation. Previously these were required and any
|
||||
# missing field surfaced as `Error invoking tool 'search_flights' with
|
||||
# kwargs ... flights.N.id: Field required` — the agent treated the error
|
||||
# string as the tool result and the surface never rendered.
|
||||
airline: str
|
||||
airlineLogo: str
|
||||
flightNumber: str
|
||||
origin: str
|
||||
destination: str
|
||||
date: str
|
||||
departureTime: str
|
||||
arrivalTime: str
|
||||
duration: str
|
||||
status: str
|
||||
price: str
|
||||
|
||||
|
||||
def _build_flight_components(flights: list[dict]) -> list[dict]:
|
||||
"""Build a flat A2UI component tree with one literal FlightCard per flight.
|
||||
|
||||
Avoids the structural-children template form (Row.children = { componentId,
|
||||
path }), which the GenericBinder only expands correctly for components whose
|
||||
schema declares STRUCTURAL children — sibling demos work because their
|
||||
schemas use literal-string-array children. Inlining the values per-flight
|
||||
sidesteps the template path entirely and renders identically.
|
||||
"""
|
||||
flight_card_ids: list[str] = []
|
||||
components: list[dict] = []
|
||||
for index, flight in enumerate(flights):
|
||||
card_id = f"flight-card-{index}"
|
||||
flight_card_ids.append(card_id)
|
||||
components.append(
|
||||
{
|
||||
"id": card_id,
|
||||
"component": "FlightCard",
|
||||
"airline": flight.get("airline", ""),
|
||||
"airlineLogo": flight.get("airlineLogo", ""),
|
||||
"flightNumber": flight.get("flightNumber", ""),
|
||||
"origin": flight.get("origin", ""),
|
||||
"destination": flight.get("destination", ""),
|
||||
"date": flight.get("date", ""),
|
||||
"departureTime": flight.get("departureTime", ""),
|
||||
"arrivalTime": flight.get("arrivalTime", ""),
|
||||
"duration": flight.get("duration", ""),
|
||||
"status": flight.get("status", ""),
|
||||
"price": flight.get("price", ""),
|
||||
}
|
||||
)
|
||||
root: dict = {
|
||||
"id": "root",
|
||||
"component": "Row",
|
||||
"children": flight_card_ids,
|
||||
"gap": 16,
|
||||
}
|
||||
return [root, *components]
|
||||
|
||||
|
||||
@tool
|
||||
def search_flights(flights: list[Flight]) -> str:
|
||||
"""Search for flights and display the results as rich cards. Return exactly 2 flights.
|
||||
|
||||
Each flight must have: airline (e.g. "United Airlines"),
|
||||
airlineLogo (use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128
|
||||
e.g. "https://www.google.com/s2/favicons?domain=united.com&sz=128" for United,
|
||||
"https://www.google.com/s2/favicons?domain=delta.com&sz=128" for Delta,
|
||||
"https://www.google.com/s2/favicons?domain=aa.com&sz=128" for American,
|
||||
"https://www.google.com/s2/favicons?domain=alaskaair.com&sz=128" for Alaska),
|
||||
flightNumber, origin, destination,
|
||||
date (short readable format like "Tue, Mar 18" — use near-future dates),
|
||||
departureTime, arrivalTime,
|
||||
duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"),
|
||||
and price (e.g. "$289").
|
||||
"""
|
||||
return a2ui.render(
|
||||
operations=[
|
||||
a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID),
|
||||
a2ui.update_components(SURFACE_ID, _build_flight_components(flights)),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ─── A2UI dynamic-schema tool: LLM-generated UI ─────────────────────
|
||||
|
||||
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
|
||||
# ─── Graph ──────────────────────────────────────────────────────────
|
||||
|
||||
model = ChatOpenAI(model="gpt-5.4", model_kwargs={"parallel_tool_calls": False})
|
||||
|
||||
agent = create_agent(
|
||||
model=model,
|
||||
tools=[query_data, *todo_tools, search_flights],
|
||||
middleware=[
|
||||
CopilotKitMiddleware(),
|
||||
StateStreamingMiddleware(
|
||||
StateItem(state_key="todos", tool="manage_todos", tool_argument="todos")
|
||||
),
|
||||
],
|
||||
state_schema=AgentState,
|
||||
system_prompt="""
|
||||
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
|
||||
|
||||
Tool guidance:
|
||||
- Flights: call search_flights to show flight cards with a pre-built schema.
|
||||
- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,
|
||||
charts, tables, and cards. It handles rendering automatically.
|
||||
- Charts: call query_data first, then render with the chart component.
|
||||
- Todos: enable app mode first, then manage todos.
|
||||
- A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"),
|
||||
respond with a brief confirmation. The UI already updated on the frontend.
|
||||
""",
|
||||
)
|
||||
|
||||
graph = agent
|
||||
@@ -0,0 +1,41 @@
|
||||
date,category,subcategory,amount,type,notes
|
||||
2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
|
||||
2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
|
||||
2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
|
||||
2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
|
||||
2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
|
||||
2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
|
||||
2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
|
||||
2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
|
||||
2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
|
||||
2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
|
||||
2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
|
||||
2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
|
||||
2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
|
||||
2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
|
||||
2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
|
||||
2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
|
||||
2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
|
||||
2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
|
||||
2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
|
||||
2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
|
||||
2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
|
||||
2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
|
||||
2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
|
||||
2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
|
||||
2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
|
||||
2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
|
||||
2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
|
||||
2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
|
||||
2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
|
||||
2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
|
||||
2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
|
||||
2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
|
||||
2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
|
||||
2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
|
||||
2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
|
||||
2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
|
||||
2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
|
||||
2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
|
||||
2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
|
||||
2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
|
||||
|
+37
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"id": "root",
|
||||
"component": "Row",
|
||||
"children": {
|
||||
"componentId": "flight-card",
|
||||
"path": "/flights"
|
||||
},
|
||||
"gap": 16
|
||||
},
|
||||
{
|
||||
"id": "flight-card",
|
||||
"component": "FlightCard",
|
||||
"airline": { "path": "airline" },
|
||||
"airlineLogo": { "path": "airlineLogo" },
|
||||
"flightNumber": { "path": "flightNumber" },
|
||||
"origin": { "path": "origin" },
|
||||
"destination": { "path": "destination" },
|
||||
"date": { "path": "date" },
|
||||
"departureTime": { "path": "departureTime" },
|
||||
"arrivalTime": { "path": "arrivalTime" },
|
||||
"duration": { "path": "duration" },
|
||||
"status": { "path": "status" },
|
||||
"price": { "path": "price" },
|
||||
"action": {
|
||||
"event": {
|
||||
"name": "book_flight",
|
||||
"context": {
|
||||
"flightNumber": { "path": "flightNumber" },
|
||||
"origin": { "path": "origin" },
|
||||
"destination": { "path": "destination" },
|
||||
"price": { "path": "price" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""LangGraph agent backing the declarative-hashbrown demo.
|
||||
|
||||
Emits hashbrown-shaped structured output that the ported HashBrownDashboard
|
||||
renderer (`src/app/demos/declarative-hashbrown/hashbrown-renderer.tsx`) progressively
|
||||
parses via `@hashbrownai/react`'s `useJsonParser` + `useUiKit`.
|
||||
|
||||
Wire format
|
||||
-----------
|
||||
`@hashbrownai/react`'s `useJsonParser(content, kit.schema)` expects the agent
|
||||
to stream a JSON object literal matching `kit.schema` — NOT the `<ui>...</ui>`
|
||||
XML-style examples shown inside `useUiKit({ examples })`. Those XML examples
|
||||
are the hashbrown prompt DSL that hashbrown compiles into a schema description
|
||||
when driving the LLM directly (e.g. `useUiChat`/`useUiCompletion`). Because
|
||||
this demo drives the LLM via langgraph instead, we must mirror what
|
||||
hashbrown's own schema wire format looks like:
|
||||
|
||||
{
|
||||
"ui": [
|
||||
{ "metric": { "props": { "label": "...", "value": "..." } } },
|
||||
{ "pieChart": { "props": { "title": "...", "data": "[{...}]" } } },
|
||||
{ "barChart": { "props": { "title": "...", "data": "[{...}]" } } },
|
||||
{ "dealCard": { "props": { "title": "...", "stage": "prospect", "value": 100000 } } },
|
||||
{ "Markdown": { "props": { "children": "## heading\\nbody" } } }
|
||||
]
|
||||
}
|
||||
|
||||
Every node is a single-key object `{tagName: {props: {...}}}`. The tag names
|
||||
and prop schemas match `useSalesDashboardKit()` in
|
||||
`hashbrown-renderer.tsx`. `pieChart` and `barChart` receive `data` as a
|
||||
JSON-encoded string (this was intentional in PR #4252 to keep the schema
|
||||
stable under partial streaming).
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
from src.agents.byoc_hashbrown_prompt import BYOC_HASHBROWN_SYSTEM_PROMPT
|
||||
|
||||
# Force JSON-object output mode. The frontend's `useJsonParser` bails to
|
||||
# `null` on any non-JSON prefix (code fences, prose preamble, etc.), so
|
||||
# leaving the model free to wander out of JSON is what left the renderer
|
||||
# empty in practice. `response_format={"type": "json_object"}` tells
|
||||
# OpenAI to refuse to emit anything but a single JSON object, which
|
||||
# aligns the wire-level contract with what the parser accepts.
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(
|
||||
model="gpt-5.4",
|
||||
model_kwargs={"response_format": {"type": "json_object"}},
|
||||
),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=BYOC_HASHBROWN_SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""System prompt for byoc_hashbrown_agent.
|
||||
|
||||
Lives alongside the agent module so the agent file stays focused on
|
||||
LangGraph setup. The prompt is long because it documents the
|
||||
component-by-component wire format that `@hashbrownai/react`'s streaming
|
||||
JSON parser expects on the frontend — see `byoc_hashbrown_agent.py` for
|
||||
the why.
|
||||
"""
|
||||
|
||||
BYOC_HASHBROWN_SYSTEM_PROMPT = """\
|
||||
You are a sales analytics assistant that replies by emitting a single JSON
|
||||
object consumed by a streaming JSON parser on the frontend.
|
||||
|
||||
ALWAYS respond with a single JSON object of the form:
|
||||
|
||||
{
|
||||
"ui": [
|
||||
{ <componentName>: { "props": { ... } } },
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Do NOT wrap the response in code fences. Do NOT include any preface or
|
||||
explanation outside the JSON object. The response MUST be valid JSON.
|
||||
|
||||
Available components and their prop schemas:
|
||||
|
||||
- "metric": { "props": { "label": string, "value": string } }
|
||||
A KPI card. `value` is a pre-formatted string like "$1.2M" or "248".
|
||||
|
||||
- "pieChart": { "props": { "title": string, "data": string } }
|
||||
A donut chart. `data` is a JSON-encoded STRING (embedded JSON) of an
|
||||
array of {label, value} objects with at least 3 segments, e.g.
|
||||
"data": "[{\\"label\\":\\"Enterprise\\",\\"value\\":600000}]".
|
||||
|
||||
- "barChart": { "props": { "title": string, "data": string } }
|
||||
A vertical bar chart. `data` is a JSON-encoded STRING of an array of
|
||||
{label, value} objects with at least 3 bars, typically time-ordered.
|
||||
|
||||
- "dealCard": { "props": { "title": string, "stage": string, "value": number } }
|
||||
A single sales deal. `stage` MUST be one of: "prospect", "qualified",
|
||||
"proposal", "negotiation", "closed-won", "closed-lost". `value` is a
|
||||
raw number (no currency symbol or comma).
|
||||
|
||||
- "Markdown": { "props": { "children": string } }
|
||||
Short explanatory text. Use for section headings and brief summaries.
|
||||
Standard markdown is supported in `children`.
|
||||
|
||||
Rules:
|
||||
- Always produce plausible sample data when the user asks for a dashboard or
|
||||
chart — do not refuse for lack of data.
|
||||
- Prefer 3-6 rows of data in charts; keep labels short.
|
||||
- Use "Markdown" for short headings or linking sentences between visual
|
||||
components. Do not emit long prose.
|
||||
- Do not emit components that are not listed above.
|
||||
- `data` props on charts MUST be a JSON STRING — escape inner quotes.
|
||||
|
||||
Example response (sales dashboard):
|
||||
{"ui":[{"Markdown":{"props":{"children":"## Q4 Sales Summary"}}},{"metric":{"props":{"label":"Total Revenue","value":"$1.2M"}}},{"metric":{"props":{"label":"New Customers","value":"248"}}},{"pieChart":{"props":{"title":"Revenue by Segment","data":"[{\\"label\\":\\"Enterprise\\",\\"value\\":600000},{\\"label\\":\\"SMB\\",\\"value\\":400000},{\\"label\\":\\"Startup\\",\\"value\\":200000}]"}}},{"barChart":{"props":{"title":"Monthly Revenue","data":"[{\\"label\\":\\"Oct\\",\\"value\\":350000},{\\"label\\":\\"Nov\\",\\"value\\":400000},{\\"label\\":\\"Dec\\",\\"value\\":450000}]"}}}]}
|
||||
"""
|
||||
@@ -0,0 +1,160 @@
|
||||
"""LangGraph agent backing the BYOC json-render demo.
|
||||
|
||||
Emits a single JSON object shaped like `@json-render/react`'s flat spec
|
||||
format (`{ root, elements }`) so the frontend can feed it directly into
|
||||
`<Renderer />` against a Zod-validated catalog of three components —
|
||||
MetricCard, BarChart, PieChart.
|
||||
|
||||
The scenario mirrors the declarative-hashbrown demo so the two BYOC rows on the
|
||||
dashboard are directly comparable. The only difference is the rendering
|
||||
technology; the catalog shape and suggestion prompts are identical.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """
|
||||
You are a sales-dashboard UI generator for a BYOC json-render demo.
|
||||
|
||||
When the user asks for a UI, respond with **exactly one JSON object** and
|
||||
nothing else — no prose, no markdown fences, no leading explanation. The
|
||||
object must match this schema (the "flat element map" format consumed by
|
||||
`@json-render/react`):
|
||||
|
||||
{
|
||||
"root": "<id of the root element>",
|
||||
"elements": {
|
||||
"<id>": {
|
||||
"type": "<component name>",
|
||||
"props": { ... component-specific props ... },
|
||||
"children": [ "<id>", ... ]
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Available components (use each name verbatim as "type"):
|
||||
|
||||
- MetricCard
|
||||
props: { "label": string, "value": string, "trend": string | null }
|
||||
Example trend strings: "+12% vs last quarter", "-3% vs last month", null.
|
||||
|
||||
- BarChart
|
||||
props: {
|
||||
"title": string,
|
||||
"description": string | null,
|
||||
"data": [ { "label": string, "value": number }, ... ]
|
||||
}
|
||||
|
||||
- PieChart
|
||||
props: {
|
||||
"title": string,
|
||||
"description": string | null,
|
||||
"data": [ { "label": string, "value": number }, ... ]
|
||||
}
|
||||
|
||||
Rules:
|
||||
|
||||
1. Output **only** valid JSON. No markdown code fences. No text outside
|
||||
the object.
|
||||
2. Every id referenced in `root` or any `children` array must be a key
|
||||
in `elements`.
|
||||
3. For a multi-component dashboard, use a root MetricCard and list the
|
||||
charts in its `children` array, OR pick any element as root and list
|
||||
the others as its children. Do not emit orphan elements.
|
||||
4. Use realistic sales-domain values (revenue, pipeline, conversion,
|
||||
categories, months) — the demo is a sales dashboard.
|
||||
5. `children` is optional but when present must be an array of strings.
|
||||
6. Never invent component types outside the three listed above.
|
||||
|
||||
### Worked example — "Show me the sales dashboard with metrics and a revenue chart"
|
||||
|
||||
{
|
||||
"root": "revenue-metric",
|
||||
"elements": {
|
||||
"revenue-metric": {
|
||||
"type": "MetricCard",
|
||||
"props": {
|
||||
"label": "Revenue (Q3)",
|
||||
"value": "$1.24M",
|
||||
"trend": "+18% vs Q2"
|
||||
},
|
||||
"children": ["revenue-bar"]
|
||||
},
|
||||
"revenue-bar": {
|
||||
"type": "BarChart",
|
||||
"props": {
|
||||
"title": "Monthly revenue",
|
||||
"description": "Revenue by month across Q3",
|
||||
"data": [
|
||||
{ "label": "Jul", "value": 380000 },
|
||||
{ "label": "Aug", "value": 410000 },
|
||||
{ "label": "Sep", "value": 450000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### Worked example — "Break down revenue by category as a pie chart"
|
||||
|
||||
{
|
||||
"root": "category-pie",
|
||||
"elements": {
|
||||
"category-pie": {
|
||||
"type": "PieChart",
|
||||
"props": {
|
||||
"title": "Revenue by category",
|
||||
"description": "Share of total revenue by product category",
|
||||
"data": [
|
||||
{ "label": "Enterprise", "value": 540000 },
|
||||
{ "label": "SMB", "value": 310000 },
|
||||
{ "label": "Self-serve", "value": 220000 },
|
||||
{ "label": "Partner", "value": 170000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### Worked example — "Show me monthly expenses as a bar chart"
|
||||
|
||||
{
|
||||
"root": "expense-bar",
|
||||
"elements": {
|
||||
"expense-bar": {
|
||||
"type": "BarChart",
|
||||
"props": {
|
||||
"title": "Monthly expenses",
|
||||
"description": "Operating expenses by month",
|
||||
"data": [
|
||||
{ "label": "Jul", "value": 210000 },
|
||||
{ "label": "Aug", "value": 225000 },
|
||||
{ "label": "Sep", "value": 240000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Respond with the JSON object only.
|
||||
"""
|
||||
|
||||
|
||||
# Force JSON-object output mode. The frontend's `parseSpec` already
|
||||
# tolerates code fences and prose preamble via `extractJsonObject`, but
|
||||
# locking the model to JSON at the API layer removes the ambiguity
|
||||
# entirely — the only thing the LLM can emit is a single JSON object,
|
||||
# which is exactly what `<Renderer />` needs.
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(
|
||||
model="gpt-5.4",
|
||||
temperature=0.2,
|
||||
model_kwargs={"response_format": {"type": "json_object"}},
|
||||
),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT.strip(),
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""LangGraph agent backing the Frontend Tools demo.
|
||||
|
||||
This cell demonstrates `useFrontendTool` with a synchronous handler.
|
||||
The backend graph registers no tools of its own — CopilotKit forwards
|
||||
the frontend tool schema(s) to the agent at runtime, and the handler
|
||||
executes in the browser. CopilotKitMiddleware is attached so frontend
|
||||
tools, shared state, and agent context flow into every turn.
|
||||
|
||||
Like the sibling `frontend_tools_async` cell, the agent has no custom
|
||||
behavior beyond a permissive system prompt — the demo's value is in
|
||||
showing the wiring contract, not the agent logic.
|
||||
"""
|
||||
|
||||
# region: middleware
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt="You are a helpful, concise assistant.",
|
||||
)
|
||||
# endregion
|
||||
@@ -0,0 +1,32 @@
|
||||
"""LangGraph agent backing the Frontend Tools (Async) demo.
|
||||
|
||||
This cell demonstrates `useFrontendTool` with an ASYNC handler. The
|
||||
frontend registers a `query_notes` tool whose handler awaits a simulated
|
||||
client-side DB query (500ms latency) and returns matching notes. The
|
||||
agent uses the returned result to summarize what it found.
|
||||
|
||||
Like the sibling `frontend_tools` cell, the backend graph registers no
|
||||
tools of its own — CopilotKit forwards the frontend tool schema(s) to
|
||||
the agent at runtime, and the handler executes in the browser.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a helpful assistant that can search the user's personal notes. "
|
||||
"When the user asks about their notes, call the `query_notes` tool with "
|
||||
"a concise keyword extracted from their request. The tool is provided "
|
||||
"by the frontend at runtime and runs entirely in the user's browser — "
|
||||
"you do not need to implement it yourself. After the tool returns, "
|
||||
"summarize the matching notes clearly and concisely. If no notes match, "
|
||||
"say so plainly and offer to try a different keyword."
|
||||
)
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""gen-ui-agent — minimal agent with explicit state + state-editing tool.
|
||||
|
||||
The agent plans a task as 3 steps and walks each pending -> in_progress
|
||||
-> completed, calling `set_steps` after every transition. The frontend
|
||||
subscribes to `state.steps` via `useAgent` and renders a live progress
|
||||
card.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
from langchain.agents import create_agent
|
||||
from langchain.agents.middleware.types import AgentState, OmitFromInput
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langchain_core.tools import InjectedToolCallId, tool
|
||||
from langgraph.types import Command
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
|
||||
class Step(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
status: Literal["pending", "in_progress", "completed"]
|
||||
|
||||
|
||||
def _last_steps(_prev: list[Step] | None, new: list[Step] | None) -> list[Step]:
|
||||
"""Reducer: last write wins (accepts parallel tool calls in one superstep)."""
|
||||
return new if new is not None else (_prev or [])
|
||||
|
||||
|
||||
class GenUiAgentState(AgentState):
|
||||
"""Extends the base agent state with a typed `steps` field."""
|
||||
|
||||
steps: Annotated[NotRequired[list[Step]], _last_steps, OmitFromInput]
|
||||
|
||||
|
||||
@tool
|
||||
def set_steps(
|
||||
steps: list[Step], tool_call_id: Annotated[str, InjectedToolCallId]
|
||||
) -> Command[Any]:
|
||||
"""Publish the current plan + step statuses. Call this every time a step
|
||||
transitions (including the first enumeration of steps)."""
|
||||
return Command(
|
||||
update={
|
||||
"steps": steps,
|
||||
"messages": [
|
||||
ToolMessage(
|
||||
f"Published {len(steps)} step(s).",
|
||||
name="set_steps",
|
||||
id=str(uuid.uuid4()),
|
||||
tool_call_id=tool_call_id,
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are an agentic planner. For each user request, follow this exact "
|
||||
"sequence:\n"
|
||||
"1. Plan exactly 3 concrete steps and call `set_steps` ONCE with all "
|
||||
'three steps at status="pending".\n'
|
||||
'2. Step 1: call `set_steps` with step 1 at status="in_progress", '
|
||||
'then call `set_steps` again with step 1 at status="completed".\n'
|
||||
'3. Step 2: call `set_steps` with step 2 at status="in_progress", '
|
||||
'then call `set_steps` again with step 2 at status="completed".\n'
|
||||
'4. Step 3: call `set_steps` with step 3 at status="in_progress", '
|
||||
'then call `set_steps` again with step 3 at status="completed".\n'
|
||||
"5. Send ONE final conversational assistant message summarizing the "
|
||||
"plan, then stop. Do not call any more tools after step 3 is "
|
||||
"completed.\n"
|
||||
"\n"
|
||||
"Rules: never call set_steps in parallel — always wait for one call to "
|
||||
"return before the next. After all three steps are completed you MUST "
|
||||
"send a final assistant message and terminate."
|
||||
)
|
||||
|
||||
|
||||
# Uses `langchain.agents.create_agent` (not `deepagents.create_deep_agent`)
|
||||
# because `create_deep_agent`'s planner+sub-agent middleware ate enough
|
||||
# supersteps on this single-tool ReAct loop to repeatedly trip
|
||||
# LangGraph's default recursion limit. The ReAct loop here is two
|
||||
# supersteps per LLM/tool cycle (model node + tool node); the prompt
|
||||
# drives ~7 set_steps cycles + 1 final model turn, so nominal cost is
|
||||
# ~15 supersteps. `recursion_limit=50` gives ~3× headroom for retries
|
||||
# inside the LLM loop.
|
||||
graph = create_agent(
|
||||
model=init_chat_model("openai:gpt-4o-mini", temperature=0, use_responses_api=False),
|
||||
tools=[set_steps],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
state_schema=GenUiAgentState,
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
).with_config({"recursion_limit": 50})
|
||||
@@ -0,0 +1,34 @@
|
||||
"""LangGraph agent backing the Tool-Based Generative UI demo.
|
||||
|
||||
The frontend registers `render_bar_chart` and `render_pie_chart` tools via
|
||||
`useComponent`. CopilotKit's LangGraph middleware injects those tools into
|
||||
the model request at runtime so the agent can call them.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
SYSTEM_PROMPT = """You are a data visualization assistant.
|
||||
|
||||
When the user asks for a chart, call `render_bar_chart` or `render_pie_chart`
|
||||
with a concise title, short description, and a `data` array of
|
||||
`{label, value}` items. Pick bar for comparisons over a small set of
|
||||
categories; pick pie for composition / share-of-whole.
|
||||
|
||||
If the user names a chart subject but does NOT supply concrete numbers
|
||||
(e.g. "show me a pie chart of website traffic by source"), do NOT ask
|
||||
them for data. Invent plausible illustrative sample values yourself,
|
||||
call the appropriate `render_*` tool immediately, and briefly note in
|
||||
the follow-up that the values are illustrative samples. Always render
|
||||
the chart on the first turn -- never reply with a clarifying question
|
||||
asking for the data.
|
||||
|
||||
Keep chat responses brief -- let the chart do the talking."""
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
"""LangGraph agent backing the Headless Chat (Complete) demo.
|
||||
|
||||
The cell exists to prove that every CopilotKit rendering surface works
|
||||
when the chat UI is composed manually (no <CopilotChatMessageView /> or
|
||||
<CopilotChatAssistantMessage />). To exercise those surfaces we give
|
||||
this agent:
|
||||
|
||||
- two mock backend tools (get_weather, get_stock_price) — render via
|
||||
app-registered `useRenderTool` renderers on the frontend,
|
||||
- access to a frontend-registered `useComponent` tool
|
||||
(`highlight_note`) — the agent "calls" it and the UI flows through
|
||||
the same `useRenderToolCall` path,
|
||||
- MCP Apps wired through the runtime — the agent can invoke Excalidraw
|
||||
MCP tools and the middleware emits activity events that
|
||||
`useRenderActivityMessage` picks up.
|
||||
|
||||
The system prompt nudges the model toward the right surface per user
|
||||
question and falls back to plain text otherwise.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain.tools import tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a helpful, concise assistant wired into a headless chat "
|
||||
"surface that demonstrates CopilotKit's full rendering stack. Pick the "
|
||||
"right surface for each user question and fall back to plain text when "
|
||||
"none of the tools fit.\n\n"
|
||||
"Routing rules:\n"
|
||||
" - If the user asks about weather for a place, call `get_weather` "
|
||||
"with the location.\n"
|
||||
" - If the user asks about a stock or ticker (AAPL, TSLA, MSFT, ...), "
|
||||
"call `get_stock_price` with the ticker.\n"
|
||||
" - If the user asks for a chart, graph, or visualization of revenue, "
|
||||
"sales, or other metrics over time, call `get_revenue_chart`.\n"
|
||||
" - If the user asks you to highlight, flag, or mark a short note or "
|
||||
"phrase, call the frontend `highlight_note` tool with the text and a "
|
||||
"color (yellow, pink, green, or blue). Do NOT ask the user for the "
|
||||
"color — pick a sensible one if they didn't say.\n"
|
||||
" - If the user asks to draw, sketch, or diagram something, use the "
|
||||
"Excalidraw MCP tools that are available to you.\n"
|
||||
" - Otherwise, reply in plain text.\n\n"
|
||||
"After a tool returns, write one short sentence summarizing the "
|
||||
"result. Never fabricate data a tool could provide."
|
||||
)
|
||||
|
||||
|
||||
@tool
|
||||
def get_weather(location: str) -> dict:
|
||||
"""Get the current weather for a given location.
|
||||
|
||||
Returns a mock payload with city, temperature in Fahrenheit, humidity,
|
||||
wind speed, and conditions. Use this whenever the user asks about
|
||||
weather anywhere.
|
||||
"""
|
||||
return {
|
||||
"city": location,
|
||||
"temperature": 68,
|
||||
"humidity": 55,
|
||||
"wind_speed": 10,
|
||||
"conditions": "Sunny",
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def get_stock_price(ticker: str) -> dict:
|
||||
"""Get a mock current price for a stock ticker.
|
||||
|
||||
Returns a payload with the ticker symbol (uppercased), price in USD,
|
||||
and percentage change for the day. Use this whenever the user asks
|
||||
about a stock price.
|
||||
"""
|
||||
return {
|
||||
"ticker": ticker.upper(),
|
||||
"price_usd": 189.42,
|
||||
"change_pct": 1.27,
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def get_revenue_chart() -> dict:
|
||||
"""Get a mock six-month revenue series for a chart visualization.
|
||||
|
||||
Returns a title, subtitle, and an array of {label, value} points. Use
|
||||
this whenever the user asks for a chart, graph, or visualization of
|
||||
revenue, sales, or other quarterly/monthly metrics.
|
||||
"""
|
||||
return {
|
||||
"title": "Quarterly revenue",
|
||||
"subtitle": "Last six months · USD thousands",
|
||||
"data": [
|
||||
{"label": "Jan", "value": 38},
|
||||
{"label": "Feb", "value": 47},
|
||||
{"label": "Mar", "value": 52},
|
||||
{"label": "Apr", "value": 49},
|
||||
{"label": "May", "value": 63},
|
||||
{"label": "Jun", "value": 71},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[get_weather, get_stock_price, get_revenue_chart],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""LangGraph agent backing the In-App HITL (frontend-tool + popup) demo.
|
||||
|
||||
The agent is a support assistant that processes customer-care requests
|
||||
(refunds, account changes, escalations). Any action that materially
|
||||
affects a customer MUST be confirmed by the human operator via the
|
||||
frontend-provided `request_user_approval` tool.
|
||||
|
||||
The tool is defined on the frontend via `useFrontendTool` with an async
|
||||
handler that opens a modal dialog OUTSIDE the chat surface. The handler
|
||||
awaits the user's decision and resolves with
|
||||
`{"approved": bool, "reason": str}`. This agent treats that result as
|
||||
authoritative: if `approved` is `True`, continue; otherwise, stop and
|
||||
explain the decision back to the user.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a support operations copilot working alongside a human operator "
|
||||
"inside an internal support console. The operator can see a list of open "
|
||||
"support tickets on the left side of their screen and is chatting with "
|
||||
"you on the right.\n"
|
||||
"\n"
|
||||
"Whenever the operator asks you to take an action that affects a "
|
||||
"customer — for example: issuing a refund, updating a customer's plan, "
|
||||
"cancelling a subscription, escalating a ticket, or sending an apology "
|
||||
"credit — you MUST first call the frontend-provided "
|
||||
"`request_user_approval` tool to obtain the operator's explicit consent.\n"
|
||||
"\n"
|
||||
"How to use `request_user_approval`:\n"
|
||||
"- `message`: a short, plain-English summary of the exact action you "
|
||||
" are about to take, including concrete numbers (e.g. '$50 refund to "
|
||||
" customer #12345').\n"
|
||||
"- `context`: optional extra context the operator might want to review "
|
||||
" (the ticket ID, the policy rule you're applying, etc.). Keep it to "
|
||||
" one or two short sentences.\n"
|
||||
"\n"
|
||||
"The tool returns an object of the shape "
|
||||
'`{"approved": boolean, "reason": string | null}`.\n'
|
||||
"- If `approved` is `true`: confirm in one short sentence that you are "
|
||||
" processing the action. You do not actually need to call any other "
|
||||
" tool — this is a demo. Just acknowledge.\n"
|
||||
"- If `approved` is `false`: acknowledge the rejection in one short "
|
||||
" sentence and, if `reason` is non-empty, reflect the operator's "
|
||||
" reason back to them. Do NOT retry the action.\n"
|
||||
"\n"
|
||||
"Keep all chat replies to one or two short sentences. Never make up "
|
||||
"customer data — always use whatever the operator told you in the "
|
||||
"prompt."
|
||||
)
|
||||
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""LangGraph agent backing the In-Chat HITL (useHumanInTheLoop) demo.
|
||||
|
||||
The `book_call` tool is defined on the frontend via `useHumanInTheLoop`,
|
||||
so there is no backend tool here. CopilotKitMiddleware is attached so the
|
||||
frontend suggestions and the time-picker render hook are picked up.
|
||||
"""
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=(
|
||||
"You help users book an onboarding call with the sales team. "
|
||||
"When they ask to book a call, call the frontend-provided "
|
||||
"`book_call` tool with a short topic and the user's name. "
|
||||
"Keep any chat reply to one short sentence."
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""LangGraph agent for the Human-in-the-Loop (Interrupt-based) booking demo.
|
||||
|
||||
Defines a backend tool `schedule_meeting(topic, attendee)` that uses
|
||||
LangGraph's `interrupt()` primitive to pause the run and surface a
|
||||
structured booking payload to the frontend. The frontend `useInterrupt`
|
||||
renderer shows a time picker inline in the chat and resolves with
|
||||
`{chosen_time, chosen_label}` or `{cancelled: true}`, which this tool
|
||||
turns into a human-readable result the agent uses to confirm the booking.
|
||||
"""
|
||||
|
||||
# @region[backend-interrupt-tool]
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Any, List, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_core.tools import tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.types import interrupt
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a scheduling assistant. Whenever the user asks you to book a "
|
||||
"call / schedule a meeting, you MUST call the `schedule_meeting` tool. "
|
||||
"Pass a short `topic` describing the purpose and `attendee` describing "
|
||||
"who the meeting is with. After the tool returns, confirm briefly "
|
||||
"whether the meeting was scheduled and at what time, or that the user "
|
||||
"cancelled."
|
||||
)
|
||||
|
||||
# Demo-only fixed timezone. A real app would use the user's calendar +
|
||||
# locale (e.g. zoneinfo.ZoneInfo(user.timezone) and Google Calendar /
|
||||
# Outlook availability); we hardcode Pacific so screenshots are stable.
|
||||
_DEMO_TZ = ZoneInfo("America/Los_Angeles")
|
||||
|
||||
|
||||
def _candidate_slots() -> List[dict]:
|
||||
"""Upcoming candidate slots, relative to "now" so the picker never
|
||||
shows stale dates."""
|
||||
now = datetime.now(_DEMO_TZ)
|
||||
tomorrow = (now + timedelta(days=1)).date()
|
||||
# Skip a week when the result would collide with `tomorrow` — i.e.
|
||||
# today is Mon (0 days away, picker would show two slots both
|
||||
# labelled "Monday") or Sun (1 day away, picker would show
|
||||
# "Tomorrow" and "Monday" both pointing at the same date).
|
||||
days_to_monday = (7 - now.weekday()) % 7
|
||||
if days_to_monday <= 1:
|
||||
days_to_monday += 7
|
||||
next_monday = (now + timedelta(days=days_to_monday)).date()
|
||||
candidates = [
|
||||
("Tomorrow 10:00 AM", tomorrow, time(10, 0)),
|
||||
("Tomorrow 2:00 PM", tomorrow, time(14, 0)),
|
||||
("Monday 9:00 AM", next_monday, time(9, 0)),
|
||||
("Monday 3:30 PM", next_monday, time(15, 30)),
|
||||
]
|
||||
return [
|
||||
{"label": label, "iso": datetime.combine(d, t, _DEMO_TZ).isoformat()}
|
||||
for label, d, t in candidates
|
||||
]
|
||||
|
||||
|
||||
@tool
|
||||
def schedule_meeting(topic: str, attendee: Optional[str] = None) -> str:
|
||||
"""Ask the user to pick a time slot for a call, via an in-chat picker.
|
||||
|
||||
Args:
|
||||
topic: Short human-readable description of the call's purpose.
|
||||
attendee: Who the call is with (optional).
|
||||
|
||||
Returns:
|
||||
Human-readable result string describing the chosen slot or
|
||||
indicating the user cancelled.
|
||||
"""
|
||||
# `interrupt()` pauses the LangGraph run and forwards a structured
|
||||
# payload to the client. The frontend v2 `useInterrupt` hook renders
|
||||
# the picker inline in the chat, then calls `resolve(...)` with the
|
||||
# user's selection — that value comes back here as `response`.
|
||||
response: Any = interrupt(
|
||||
{
|
||||
"topic": topic,
|
||||
"attendee": attendee,
|
||||
"slots": _candidate_slots(),
|
||||
}
|
||||
)
|
||||
|
||||
if isinstance(response, dict):
|
||||
if response.get("cancelled"):
|
||||
return f"User cancelled. Meeting NOT scheduled: {topic}"
|
||||
chosen_label = response.get("chosen_label") or response.get("chosen_time")
|
||||
if chosen_label:
|
||||
return f"Meeting scheduled for {chosen_label}: {topic}"
|
||||
|
||||
return f"User did not pick a time. Meeting NOT scheduled: {topic}"
|
||||
|
||||
|
||||
# @endregion[backend-interrupt-tool]
|
||||
|
||||
|
||||
model = ChatOpenAI(model="gpt-5.4")
|
||||
|
||||
graph = create_agent(
|
||||
model=model,
|
||||
tools=[schedule_meeting],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Default LangGraph agent — neutral "helpful, concise assistant".
|
||||
|
||||
This is the fallthrough graph for demos that don't require anything more
|
||||
specialized. Cells that need tailored behavior (chart viz, weather-only,
|
||||
etc.) should have their own dedicated graph under `src/agents/` and
|
||||
explicit wiring in the CopilotKit route.
|
||||
"""
|
||||
|
||||
# CVDIAG runtime bootstrap (L1-H, folded into L1-I for LGP). MUST be the first
|
||||
# non-stdlib import: importing this module configures the root logger so the
|
||||
# agents._* CVDIAG loggers actually emit, resolves the verbosity tier (§6
|
||||
# fail-closed DEBUG guard), and builds the threaded PocketBase writer — once, at
|
||||
# process start. main.py is langgraph's default graph entrypoint (sample_agent)
|
||||
# and is verified present by entrypoint.sh, so it is the reliable single
|
||||
# bootstrap chokepoint for the LGP process.
|
||||
import _shared.cvdiag_bootstrap # noqa: F401 (import side effects = the bootstrap)
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from copilotkit import CopilotKitMiddleware
|
||||
|
||||
|
||||
graph = create_agent(
|
||||
model=ChatOpenAI(model="gpt-5.4"),
|
||||
tools=[],
|
||||
middleware=[CopilotKitMiddleware()],
|
||||
system_prompt="You are a helpful, concise assistant.",
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user