chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1 @@
f0cba3cf6d2173dd849638fd8edcf3d6344db491
@@ -0,0 +1,11 @@
**/node_modules
.next
__pycache__
*.pyc
.git
**/vitest.config.ts
**/test-setup.ts
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
@@ -0,0 +1,6 @@
# API Keys (shared across integrations)
OPENAI_API_KEY=replace-with-your-key
ANTHROPIC_API_KEY=replace-with-your-key
# Showcase
NEXT_PUBLIC_BASE_URL=http://localhost:3000
@@ -0,0 +1,13 @@
node_modules/
.next/
.env.local
.env
*.pyc
__pycache__/
.venv/
dist/
playwright-report/
test-results/
# Shared modules (copied by CI)
shared_frontend/
@@ -0,0 +1,97 @@
# Stage 1: Build AG-UI Java SDK + Spring Boot agent backend
# Align on Temurin 21 (tracks the starter Dockerfile) — matches the
# spring-ai community artifacts + SDK source in ag-ui-protocol/ag-ui.
FROM maven:3-eclipse-temurin-21 AS java-builder
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
# AG-UI community artifacts aren't on Maven Central yet — build from source
WORKDIR /build
COPY pom.xml ./
# Single source of truth for the AG-UI commit (shared with the
# `.github/workflows/test_unit-spring-ai.yml` workflow). Bump by editing
# the file; both CI and this image rebuild against the new SHA.
COPY .ag-ui-sha /tmp/.ag-ui-sha
ENV GIT_LFS_SKIP_SMUDGE=1
RUN AG_UI_SHA="$(tr -d '[:space:]' < /tmp/.ag-ui-sha)" && \
if [ -z "${AG_UI_SHA}" ]; then echo "ERROR: .ag-ui-sha is empty" >&2; exit 1; fi && \
git clone --no-checkout https://github.com/ag-ui-protocol/ag-ui.git /ag-ui && \
# `git clone` only fetches the default branch by default — if AG_UI_SHA
# points at a commit on a PR branch or older history not reachable from
# HEAD, the subsequent `checkout` fails with a cryptic "reference is not
# a tree". Explicitly fetch the SHA first so checkout is always against
# a known-local commit (R7-A2). `--depth 1` keeps the image lean.
git -C /ag-ui fetch --depth 1 origin "${AG_UI_SHA}" && \
git -C /ag-ui checkout "${AG_UI_SHA}" && \
cd /ag-ui/sdks/community/java && \
mvn install \
-pl servers/spring,integrations/spring-ai -am \
-DskipTests -Dgpg.skip=true \
-Dmaven.javadoc.skip=true -Djavadoc.skip=true \
-Dmaven.source.skip=true -Dcheckstyle.skip=true \
-Dmaven.site.skip=true -Dreporting.skip=true \
-Dassembly.skipAssembly=true \
-B && \
cd /build && \
mvn dependency:go-offline -B
COPY src/main/java/ src/main/java/
COPY src/main/resources/ src/main/resources/
RUN mvn package -DskipTests -B
# Stage 2: 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 next.config.ts tsconfig.json postcss.config.mjs manifest.yaml ./
COPY src/app/ src/app/
COPY src/components/ src/components/
COPY src/lib/ src/lib/
COPY public/ public/
RUN npm run build
# Stage 3: Production runtime (JRE + Node, no JDK, no Maven)
FROM eclipse-temurin:21-jre AS runner
# Install Node.js 22 runtime (for Next.js). No build tools.
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).
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
# Spring Boot fat JAR (from java-builder, not the full Maven workspace)
COPY --chown=app:app --from=java-builder /build/target/agent.jar ./agent.jar
# Next.js build artifacts (from frontend stage — no npm install at runtime)
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
# Entrypoint
COPY --chown=app:app entrypoint.sh ./
RUN chmod +x entrypoint.sh
# Ensure WORKDIR itself is owned by `app` — `WORKDIR /app` at the top of the
# stage creates /app as root, and `COPY --chown=app:app` only reassigns the
# copied files, NOT the parent dir. Without this, any subprocess that tries
# to mkdir under /app at runtime (Next.js build caches, JVM tmp, etc.) hits
# EACCES under the unprivileged user and crashes the container.
RUN chown app:app /app
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 the Java agent and
# any shell subprocesses. entrypoint.sh scopes NODE_ENV=production to the
# Next.js invocation only (see `env NODE_ENV=production npx next start`).
ENV PORT=10000
CMD ["./entrypoint.sh"]
@@ -0,0 +1,165 @@
# Spring AI Showcase — Parity Notes
This document tracks demos from the canonical `langgraph-python` showcase
manifest that are **not ported** to the Spring AI showcase, along with the
specific Spring AI / `ag-ui:spring-ai` primitive that is missing.
Spring AI is a Java framework with a narrower primitive set than LangGraph
for a handful of specific use-cases — especially streaming structured
output, multi-agent orchestration, and graph-level interrupts. The demos
below are the ones where those primitives are genuinely unavailable.
## Skipped demos
### LangGraph graph-control primitives (no Spring AI equivalent)
- **subagents** — Ported using the tool-composition pattern (each
sub-agent is a separate `ChatClient` call wired as a supervisor tool;
see `SubagentsController`). This deviates from LangGraph's
graph-as-node construct: there is no per-sub-agent interrupt point, and
step-started/step-finished events are not emitted. The user-visible
semantics — supervisor delegates work, each delegation is logged in
shared state, the UI renders a live timeline — match the canonical
demo. STATE_SNAPSHOT is emitted after every delegation so the
delegation log updates incrementally.
### `ag-ui:spring-ai` adapter gaps
- **shared-state-streaming** — Spring AI's `ChatClient.stream()` emits
token deltas, but the `ag-ui:spring-ai` adapter does not expose a
mid-stream state-delta emission API comparable to LangGraph's
`copilotkit_emit_state`. Per-token state patches cannot be forwarded
through the AG-UI channel with the current integration. The demo cell
is shipped as a stub frontend (`src/app/demos/shared-state-streaming/`)
so the UI lights up when the adapter exposes mid-stream emission.
- **byoc-json-render** — Relies on a streaming structured-output primitive
(LangGraph's `with_structured_output` + incremental JSON streaming that
yields partial objects matching a Zod schema across the stream). Spring
AI has `BeanOutputConverter` / `ParameterizedTypeReference` structured
output, but it resolves on the FINAL response only — it does not emit
partial schema-conformant objects during the stream. The BYOC renderer
needs per-token JSON to progressively paint the UI. Additionally,
`@json-render/core` and `@json-render/react` are not currently
dependencies of the Spring AI showcase package.
## Ported with caveats
- **gen-ui-interrupt** — Ported using **Strategy B** (the same approach
used by MS Agent Python). Spring AI has no `interrupt()` primitive, so
the backend agent (`InterruptAgentController`) provides a scheduling
system prompt with NO backend tool callbacks. The `schedule_meeting`
tool is registered entirely on the frontend via `useFrontendTool` with
an async handler that renders a `TimePickerCard` and blocks until the
user picks a slot or cancels. The UX is identical to the LangGraph
version.
- **interrupt-headless** — Same Strategy B adaptation as
`gen-ui-interrupt`, but the time-picker popup renders in the app
surface (outside the chat) instead of inline. Both demos share the
same backend agent (`InterruptAgentController`).
- **byoc-hashbrown** — Ported. The hashbrown UI kit
(`@hashbrownai/react@0.5.0-beta.4`) consumes streaming text and uses
`useJsonParser` to progressively assemble UI from partial JSON. Spring
AI's `ChatClient.stream()` streams text tokens, so the hashbrown
parser tolerates the per-token feed. Final-shape correctness depends on
the model following the example prompt — there is no guarantee like
LangGraph's `with_structured_output`.
- **gen-ui-tool-based** — Ported using `useComponent` per-tool renderers
bound to `render_bar_chart` / `render_pie_chart` tools. Args stream as
partial JSON; the Zod schemas accept partials so the chart components
can render once enough fields are present.
- **reasoning-custom**, **reasoning-default**,
**tool-rendering-reasoning-chain** — frontend code is wired for
`REASONING_MESSAGE_*` events, but the Spring AI handler CANNOT emit
them. This is a genuine SDK limitation in Spring AI 1.0.1, not an
adapter or wiring gap. Details below.
**What the demo needs.** The reasoning UI mounts only when the backend
emits AG-UI `REASONING_MESSAGE_START` / `_CONTENT` / `_END` events
(role `"reasoning"`). The canonical `langgraph-python` agent produces
these by routing the OpenAI model's reasoning summary through the
**OpenAI Responses API** (`reasoning={"effort": "medium", "summary":
"detailed"}`). The aimock fixtures for these spring-ai cells
(`d6/spring-ai/reasoning.json`,
`d6/spring-ai/tool-rendering-reasoning-chain.json`, copied from
langgraph-python) carry the reasoning text in a dedicated
`response.reasoning` field, which aimock renders over the OpenAI
**chat-completions** wire as streaming `delta.reasoning_content`
chunks (see `@copilotkit/aimock` `buildTextChunks`
`delta: { reasoning_content: slice }`).
**Why Spring AI 1.0.1 cannot surface it.** The spring-ai integration
speaks OpenAI chat-completions (`spring-ai-starter-model-openai`,
`/v1/chat/completions`). In `spring-ai-openai:1.0.1` the streaming
delta is bound to the record `OpenAiApi.ChatCompletionMessage`, whose
components are exactly `rawContent, role, name, toolCallId, toolCalls,
refusal, audioOutput, annotations` — there is **no `reasoning_content`
/ `reasoning` field**, no metadata map, and no `@JsonAnySetter`
catch-all. The record is annotated `@JsonIgnoreProperties`, so the
inbound `reasoning_content` JSON property is **silently discarded at
deserialization**. It never reaches `ChatResponse` /
`Generation.getOutput()`, so the Java handler has no API to read it.
The reasoning-summary channel of the OpenAI **Responses API** is also
unavailable: `spring-ai-openai:1.0.1` ships no Responses-API client
(only `OpenAiApi` chat-completions classes exist), so the
langgraph-python parity path cannot be reproduced either.
**Why the inline-`<reasoning>`-tag workaround does not apply.** The
proven `claude-sdk-python` agent PRIMARILY maps Anthropic's native
extended-thinking channel: it enables `thinking={"type": "enabled", ...}`
on the Messages API, receives `thinking_delta` blocks, and re-routes
them to `REASONING_MESSAGE_*`. Only when no native thinking channel is
present does it FALL BACK to prompting the model to wrap its plan in
literal `<reasoning>...</reasoning>` text tags inside normal output and
parsing those tags out of the text stream. The inline-tag fallback IS
expressible in Spring AI (the handler already streams
`getOutput().getText()`). But neither claude-sdk path fits these cells:
the spring-ai aimock fixtures emit reasoning through the dedicated
`reasoning` field (→ `reasoning_content`), NOT via an Anthropic native
thinking channel and NOT as inline `<reasoning>` tags in `content`.
Rewriting the fixtures to embed inline tags — or hand-fabricating a
reasoning block in the handler — would be a demo-weakening fixture hack
that misrepresents the integration's real capability, so it is
deliberately not done.
**What a real fix requires (upstream / out of scope here).** Either
(a) Spring AI adds a `reasoning_content` (or reasoning-summary) field
to its chat-completions delta record and exposes it on
`Generation`/output metadata; or (b) Spring AI ships an OpenAI
Responses-API client that surfaces the reasoning summary; or (c) a
custom `WebClient`-level interceptor parses the raw chat-completions
SSE for `delta.reasoning_content` BEFORE Spring AI's binding drops it,
bypassing `ChatClient` entirely (a substantial custom-parser effort
that re-implements the streaming pipeline). None of these is a
showcase-side change. Until one lands, these cells ship as frontend
code (so the pattern is documented end-to-end) and the chat behaves as
a regular chat with no reasoning block.
- **multimodal** — the frontend sends image + PDF attachments through
CopilotChat's `AttachmentsConfig`. Whether the adapter forwards them
into Spring AI's `UserMessage.media()` surface is
integration-dependent; the Spring-AI model (`gpt-4.1`) is vision-capable
on the provider side.
- **mcp-apps** — the runtime wires the MCP Apps middleware with the public
Excalidraw MCP server. The middleware injects MCP tools into the AG-UI
request so the Spring-AI ChatClient sees them, and intercepts tool calls
to emit activity events. Whether the `ag-ui:spring-ai` adapter forwards
runtime-injected tools into Spring AI's tool-calling surface is
integration-dependent; the demo wiring is in place so the cell lights up
when the adapter supports it.
## Ported demos
The full ported list lives in `manifest.yaml`. Highlights include:
agentic-chat, tool-rendering (default + custom + catchall), frontend-tools
(+ async), hitl-in-chat (+ booking variant), hitl-in-app, prebuilt-sidebar
/ popup, chat-slots, chat-customization-css, headless-simple,
headless-complete, beautiful-chat, auth, readonly-state-agent-context,
open-gen-ui (+ advanced), voice, agent-config, a2ui-fixed-schema,
declarative-gen-ui, multimodal, gen-ui-tool-based, mcp-apps,
byoc-hashbrown, and the three reasoning variants.
@@ -0,0 +1,43 @@
{
"framework": "spring-ai",
"features": {
"agentic-chat": {
"og_docs_url": "https://docs.copilotkit.ai/agentic-chat-ui",
"shell_docs_path": "/prebuilt-components"
},
"hitl-in-chat": {
"og_docs_url": "https://docs.copilotkit.ai/human-in-the-loop",
"shell_docs_path": "/generative-ui/your-components/interactive"
},
"tool-rendering": {
"og_docs_url": "https://docs.copilotkit.ai/generative-ui/tool-rendering",
"shell_docs_path": "/generative-ui/tool-rendering"
},
"gen-ui-tool-based": {
"og_docs_url": "https://docs.copilotkit.ai/generative-ui/your-components/display-only",
"shell_docs_path": "/generative-ui/your-components/display-only"
},
"gen-ui-agent": {
"og_docs_url": "https://docs.copilotkit.ai/generative-ui/state-rendering",
"shell_docs_path": "/generative-ui/state-rendering"
},
"shared-state-read-write": {
"og_docs_url": "https://docs.copilotkit.ai/shared-state",
"shell_docs_path": "/shared-state"
},
"shared-state-streaming": {
"og_docs_url": "https://docs.copilotkit.ai/shared-state",
"shell_docs_path": "/shared-state"
},
"subagents": {
"og_docs_url": null,
"shell_docs_path": "/multi-agent/subagents"
}
},
"missing": [
{
"feature": "subagents",
"reason": "No spring-ai-scoped multi-agent/subagent docs exist; the spring-ai framework root is also a SPA fallback. OG left null rather than pointing at a soft-404; shell points at the unified /multi-agent/subagents content."
}
]
}
@@ -0,0 +1,177 @@
#!/bin/bash
set -e
# Derive SPRING_AI_OPENAI_BASE_URL from the showcase-wide OPENAI_BASE_URL if
# not already set. The showcase convention is that OPENAI_BASE_URL includes
# "/v1" (e.g. https://aimock.example.com/v1), but Spring AI appends
# "/v1/chat/completions" itself, so we must strip the trailing "/v1" to avoid
# a doubled path segment.
if [ -z "${SPRING_AI_OPENAI_BASE_URL:-}" ] && [ -n "${OPENAI_BASE_URL:-}" ]; then
export SPRING_AI_OPENAI_BASE_URL="${OPENAI_BASE_URL%/v1}"
echo "[entrypoint] Derived SPRING_AI_OPENAI_BASE_URL=${SPRING_AI_OPENAI_BASE_URL} from OPENAI_BASE_URL=${OPENAI_BASE_URL}"
fi
echo "[entrypoint] Starting Spring Boot agent backend..."
# jdk.httpclient.keepalive.timeout=0 disables JDK HttpClient connection pooling.
# Required because Spring-AI streams via WebClient + JdkClientHttpConnector and a
# pooled connection can be half-closed by some upstreams (aimock/Prism) between
# SSE responses, which trips `Connection reset` on the follow-up tool-result
# request. Setting this as a JVM arg guarantees it lands before any
# java.net.http.HttpClient is constructed. This is the authoritative path;
# WebClientConfig's static initializer is a defensive fallback only.
#
# copilotkit.tool.max-iterations: override the BoundedToolCallingManager's
# cap via a JVM property so the pre-built jar picks it up without a
# rebuild. The application.properties inside the jar defaults to 5 via
# ${COPILOTKIT_TOOL_MAX_ITERATIONS:5}, but passing it as -D here ensures
# it takes effect even on images built before that property was added.
# D5 fixtures need at least 3 (subagents: research -> writing -> critique);
# 5 gives headroom for future multi-tool demos.
TOOL_MAX_ITER="${COPILOTKIT_TOOL_MAX_ITERATIONS:-5}"
echo "[entrypoint] copilotkit.tool.max-iterations=${TOOL_MAX_ITER}"
java -Djdk.httpclient.keepalive.timeout=0 \
-Dcopilotkit.tool.max-iterations="${TOOL_MAX_ITER}" \
-jar /app/agent.jar &
JAVA_PID=$!
# Wait for Spring Boot to be ready (up to 60 seconds). Cold-start JVM warmup
# plus Spring context refresh can legitimately exceed 30s under load — we
# also probe the Java PID each tick as a liveness fallback, so a crashing
# boot fails fast regardless of the cap.
STARTUP_TIMEOUT=60
echo "[entrypoint] Waiting for Spring Boot health check (timeout=${STARTUP_TIMEOUT}s)..."
SPRING_READY=0
for i in $(seq 1 "$STARTUP_TIMEOUT"); do
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
echo "[entrypoint] Spring Boot ready after ${i}s"
SPRING_READY=1
break
fi
if ! kill -0 "$JAVA_PID" 2>/dev/null; then
echo "[entrypoint] Spring Boot process (pid=${JAVA_PID}) died during startup"
exit 1
fi
sleep 1
done
if [ "$SPRING_READY" -ne 1 ]; then
# Differentiate "slow" from "dead" so operators know whether to raise
# the timeout or debug a crash loop.
if kill -0 "$JAVA_PID" 2>/dev/null; then
echo "[entrypoint] Spring Boot still alive (pid=${JAVA_PID}) but /health did not return 2xx within ${STARTUP_TIMEOUT}s"
else
echo "[entrypoint] Spring Boot process (pid=${JAVA_PID}) exited before reporting healthy"
fi
exit 1
fi
echo "[entrypoint] Starting Next.js frontend on port ${PORT:-10000}..."
# Scope NODE_ENV=production to the Next.js invocation ONLY so it doesn't
# leak into the Java agent process. See Dockerfile comment for rationale.
env NODE_ENV=production npx next start --port ${PORT:-10000} &
NODE_PID=$!
# Watchdog: Railway deploys of showcase packages have been observed to hit a
# silent agent hang — the Spring Boot process stays alive (so `wait -n`
# never fires and the container never restarts) but stops responding on
# :8000. Poll Spring Boot's /health endpoint every 30s; after 3 consecutive
# failures (~90s of unreachable agent), kill the java process so `wait -n`
# returns and Railway restarts the container. The startup probe above
# already gates the initial readiness window; this watchdog takes over for
# steady-state monitoring. Generalized from
# showcase/integrations/crewai-crews/entrypoint.sh (PRs #4114 + #4115).
(
FAILS=0
while sleep 30; do
if ! kill -0 "$JAVA_PID" 2>/dev/null; then
break
fi
if curl -fsS --max-time 5 http://127.0.0.1:8000/health > /dev/null 2>&1; then
FAILS=0
else
FAILS=$((FAILS + 1))
echo "[watchdog] Agent health probe failed (count=$FAILS)"
if [ $FAILS -ge 3 ]; then
echo "[watchdog] Agent unresponsive for ~90s — killing PID $JAVA_PID to trigger container restart"
kill -9 "$JAVA_PID" 2>/dev/null || true
break
fi
fi
done
) &
WATCHDOG_PID=$!
echo "[entrypoint] Watchdog started (PID: $WATCHDOG_PID, probing http://127.0.0.1:8000/health)"
# Wait for either process to exit. `wait -n` without PID args works on all
# bash >= 4.3 (align with other showcase entrypoints such as google-adk);
# the PID-args form requires bash 5.1+ which isn't guaranteed in minimal
# container images.
#
# Disable errexit for the wait + post-mortem block. With `set -e` still active,
# a non-zero child-exit code from `wait -n` would terminate the shell BEFORE we
# get a chance to run the diagnostic `kill -0` probes below — meaning the
# container log would never carry the "which died" line that operators rely on.
# We capture the exit code explicitly into EXIT_CODE and the final
# `exit "$EXIT_CODE"` propagates the dying child's status, so skipping errexit
# here doesn't change the container exit semantics. Restoration of `set -e` is
# intentionally omitted (mirrors google-adk's entrypoint).
set +e
wait -n
EXIT_CODE=$?
# Identify which process exited AND kill the surviving sibling so it doesn't
# get orphan-reparented to PID 1 when the container exits. Without this
# explicit cleanup, a Java crash would leave Next.js alive (and vice versa)
# consuming resources until the container runtime tears down the whole
# process tree.
SURVIVOR_PID=""
if ! kill -0 "$JAVA_PID" 2>/dev/null; then
echo "[entrypoint] Java process (pid=${JAVA_PID}) exited (code=${EXIT_CODE})"
if kill -0 "$NODE_PID" 2>/dev/null; then
SURVIVOR_PID="$NODE_PID"
fi
elif ! kill -0 "$NODE_PID" 2>/dev/null; then
echo "[entrypoint] Node.js process (pid=${NODE_PID}) exited (code=${EXIT_CODE})"
if kill -0 "$JAVA_PID" 2>/dev/null; then
SURVIVOR_PID="$JAVA_PID"
fi
else
echo "[entrypoint] A child exited (code=${EXIT_CODE}); both PIDs still resolve — race between wait and kill -0"
fi
if [ -n "$SURVIVOR_PID" ]; then
# Bounded grace window. A plain `wait` on the survivor could hang
# indefinitely (e.g. Node.js stuck flushing a response, Java caught in a
# finalizer) — which would push us past the platform's SIGKILL grace
# period (typically 10s on Railway/ECS) and cause the runtime to reap
# us mid-log-write, losing the structured "who died" line we just
# emitted. SIGTERM first, poll `kill -0` for up to SURVIVOR_GRACE_SECS,
# then SIGKILL as last resort. Mirrors what the comment above this
# block already promised.
SURVIVOR_GRACE_SECS=10
echo "[entrypoint] Terminating surviving sibling (pid=${SURVIVOR_PID}) to avoid orphan-reparent (grace=${SURVIVOR_GRACE_SECS}s)"
kill -TERM "$SURVIVOR_PID" 2>/dev/null
for _ in $(seq 1 "$SURVIVOR_GRACE_SECS"); do
if ! kill -0 "$SURVIVOR_PID" 2>/dev/null; then
break
fi
sleep 1
done
if kill -0 "$SURVIVOR_PID" 2>/dev/null; then
echo "[entrypoint] Survivor (pid=${SURVIVOR_PID}) did not exit within ${SURVIVOR_GRACE_SECS}s; sending SIGKILL"
kill -KILL "$SURVIVOR_PID" 2>/dev/null || true
fi
# Reap the (now-dead) child so it doesn't become a zombie. wait may
# return non-zero; we don't care — we've already captured EXIT_CODE
# from the first-to-die child.
wait "$SURVIVOR_PID" 2>/dev/null || true
fi
# Clean up the watchdog if it's still running (e.g. Next.js exited, not Java).
# Without this the backgrounded watchdog would continue polling /health on a
# dying container until the platform SIGKILLs the process tree.
if [ -n "${WATCHDOG_PID:-}" ] && kill -0 "$WATCHDOG_PID" 2>/dev/null; then
kill "$WATCHDOG_PID" 2>/dev/null || true
fi
exit "$EXIT_CODE"
@@ -0,0 +1,592 @@
name: Spring AI
slug: spring-ai
category: enterprise-platform
language: java
logo: /logos/spring-ai.svg
description: >-
CopilotKit integration with Spring AI. Java-based agent backend with full
feature coverage including frontend tools, in-chat and in-app human-in-the-
loop, generative UI, per-tool renderers, agent context, interrupt-adapted
scheduling (Strategy B), per-token state streaming, and prebuilt chat surfaces
(sidebar / popup / slots / headless). Demos that depend on LangGraph-specific
primitives (interrupt, multi-agent orchestration, MCP-driven UI) are
documented as skipped in PARITY_NOTES.md.
partner_docs: null
repo: https://github.com/CopilotKit/CopilotKit/tree/main/showcase/integrations/spring-ai
copilotkit_version: 2.0.0
deployed: true
docs_mode: hidden
sort_order: 170
generative_ui:
- constrained-explicit
- a2ui-dynamic-schema
interaction_modalities:
- sidebar
- embedded
- chat
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.
- gen-ui-interrupt
- interrupt-headless
# Reasoning-family NSF: while spring-ai DOES emit REASONING_MESSAGE_* events
# for the `reasoning-custom` / `reasoning-default` demos (via the dedicated
# ReasoningController at /reasoning/, which writes raw-JSON
# REASONING_MESSAGE_START/CONTENT/END frames whose `type` matches the
# @ag-ui/core zod schema — workaround for the AG-UI Java SDK gap where
# Spring AI 1.0.1's SDK has no REASONING_MESSAGE_* subtypes), the following
# canonical reasoning cell-ids are NOT covered by that workaround and
# cannot mount here:
# * `reasoning-default-render` — alternate render-path for default
# reasoning that this Spring backend does not parallel.
# * `agentic-chat-reasoning` — agentic-chat composed with reasoning
# frames; spring-ai's default agent at `/` does not emit reasoning.
# * `tool-rendering-reasoning-chain` — registered against the default
# Spring agent at the root path (no reasoning emission), and spring-ai
# has no parallel multi-tool-call streaming surface to interleave with
# reasoning frames, so the reasoning-chain composition cannot mount.
# Skipped-incapable, not regressions. NSF entries are canonical cell-ids
# matching the dashboard cell-schema.
- reasoning-default-render
- agentic-chat-reasoning
- tool-rendering-reasoning-chain
features:
- cli-start
- agentic-chat
- tool-rendering-default-catchall
- tool-rendering-custom-catchall
- chat-customization-css
- frontend-tools
- frontend-tools-async
- gen-ui-agent
- gen-ui-tool-based
- open-gen-ui
- open-gen-ui-advanced
- declarative-gen-ui
- a2ui-fixed-schema
- prebuilt-sidebar
- prebuilt-popup
- chat-slots
- headless-simple
- headless-complete
- beautiful-chat
- readonly-state-agent-context
- shared-state-read-write
- shared-state-streaming
- subagents
- auth
- voice
- multimodal
- agent-config
- hitl-in-chat
- hitl-in-chat-booking
- hitl-in-app
- hitl
- tool-rendering
- mcp-apps
- declarative-hashbrown
- declarative-json-render
a2ui_pattern: schema-inline
agent_config_pattern: shared-state
auth_pattern: runtime-onrequest
demos:
- id: agentic-chat
name: Agentic Chat
description: Natural conversation with frontend tool execution
tags:
- chat-ui
route: /demos/agentic-chat
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/AgentConfig.java
- src/main/java/com/copilotkit/showcase/springai/AgentController.java
- 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 scoped CSS overrides
tags:
- chat-ui
route: /demos/chat-customization-css
animated_preview_url:
highlight:
- src/app/demos/chat-customization-css/page.tsx
- src/app/demos/chat-customization-css/theme.css
- 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/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 and uses the returned result
tags:
- interactivity
route: /demos/frontend-tools-async
animated_preview_url:
highlight:
- src/app/demos/frontend-tools-async/page.tsx
- src/app/demos/frontend-tools-async/notes-card.tsx
- src/app/api/copilotkit/route.ts
- id: gen-ui-agent
name: Agentic Generative UI
description: Long-running agent tasks with generated UI
tags:
- generative-ui
route: /demos/gen-ui-agent
animated_preview_url:
highlight:
- src/app/demos/gen-ui-agent/page.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/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/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/app/demos/chat-slots/page.tsx
- src/app/api/copilotkit/route.ts
- src/app/demos/chat-slots/slot-wrappers.tsx
- id: headless-simple
name: Headless Chat (Simple)
description: Minimal custom chat surface built on useAgent
tags:
- chat-ui
route: /demos/headless-simple
animated_preview_url:
highlight:
- src/app/demos/headless-simple/page.tsx
- src/app/api/copilotkit/route.ts
- id: readonly-state-agent-context
name: Readonly State (Agent Context)
description: Frontend provides read-only context to the agent via useAgentContext
tags:
- agent-state
route: /demos/readonly-state-agent-context
animated_preview_url:
highlight:
- src/app/demos/readonly-state-agent-context/page.tsx
- src/app/api/copilotkit/route.ts
- id: auth
name: Authentication
description:
Bearer-token gate via runtime onRequest hook with unauthenticated /
authenticated states
tags:
- chat-ui
route: /demos/auth
animated_preview_url:
highlight:
- src/app/demos/auth/page.tsx
- src/app/demos/auth/auth-banner.tsx
- src/app/demos/auth/use-demo-auth.ts
- src/app/demos/auth/demo-token.ts
- src/app/api/copilotkit-auth/[[...slug]]/route.ts
- id: open-gen-ui
name: Open Generative UI (Minimal)
description:
Agent streams HTML + CSS into a sandboxed iframe via the built-in
OpenGenerativeUIActivityRenderer
tags:
- generative-ui
route: /demos/open-gen-ui
animated_preview_url:
highlight:
- src/app/demos/open-gen-ui/page.tsx
- src/app/api/copilotkit-ogui/route.ts
- id: open-gen-ui-advanced
name: Open Generative UI (Advanced)
description: Sandboxed UI can call host-side functions (evaluateExpression,
notifyHost) via the provider's sandboxFunctions registration
tags:
- generative-ui
route: /demos/open-gen-ui-advanced
animated_preview_url:
highlight:
- 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: CopilotChat mic button via a runtime mounted with
TranscriptionServiceOpenAI; transcribed text is forwarded to the Spring AI
backend
tags:
- interactivity
route: /demos/voice
animated_preview_url:
highlight:
- 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:
Frontend forwards typed config (tone, expertise, responseLength) to
Spring, which rebuilds the system prompt per request
tags:
- agent-capabilities
route: /demos/agent-config
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/AgentConfigController.java
- 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
- id: a2ui-fixed-schema
name: A2UI Fixed Schema
description:
Dedicated Spring tool emits a fixed flight-card component tree plus
a data model; frontend catalog renders each component
tags:
- generative-ui
route: /demos/a2ui-fixed-schema
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/A2uiFixedSchemaController.java
- src/main/java/com/copilotkit/showcase/springai/tools/DisplayFlightTool.java
- src/app/demos/a2ui-fixed-schema/page.tsx
- src/app/demos/a2ui-fixed-schema/a2ui/definitions.ts
- src/app/demos/a2ui-fixed-schema/a2ui/renderers.tsx
- src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts
- src/app/api/copilotkit-a2ui-fixed-schema/route.ts
- id: headless-complete
name: Headless Chat (Complete)
description: Full chat built from scratch on useAgent plus low-level render
hooks (useRenderToolCall, useRenderActivityMessage,
useRenderCustomMessages)
tags:
- chat-ui
route: /demos/headless-complete
animated_preview_url:
highlight:
- src/app/demos/headless-complete/page.tsx
- src/app/api/copilotkit/route.ts
- 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: beautiful-chat
name: Beautiful Chat
description:
Polished brand-themed chat surface over the Spring AI agent with
seeded suggestion pills
tags:
- chat-ui
route: /demos/beautiful-chat
animated_preview_url:
highlight:
- src/app/demos/beautiful-chat/page.tsx
- src/app/api/copilotkit/route.ts
- id: tool-rendering-reasoning-chain
name: Tool Rendering (Reasoning Chain)
description: Per-tool renderers (WeatherCard, FlightListCard) plus custom
reasoningMessage slot composed in a single chat
tags:
- generative-ui
route: /demos/tool-rendering-reasoning-chain
animated_preview_url:
highlight:
- 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: declarative-gen-ui
name: Declarative Generative UI
description: A2UI dynamic-schema against a branded catalog; backend owns the
`generate_a2ui` tool
tags:
- generative-ui
route: /demos/declarative-gen-ui
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/tools/GenerateA2uiTool.java
- src/app/demos/declarative-gen-ui/page.tsx
- src/app/demos/declarative-gen-ui/a2ui/definitions.ts
- src/app/demos/declarative-gen-ui/a2ui/renderers.tsx
- src/app/demos/declarative-gen-ui/a2ui/catalog.ts
- src/app/api/copilotkit-declarative-gen-ui/route.ts
- id: shared-state-read-write
name: Shared State (Read + Write)
description: Bidirectional shared state — UI writes preferences into agent
state; the agent's set_notes tool mutates state.notes and emits a
STATE_SNAPSHOT back to the UI
tags:
- agent-state
route: /demos/shared-state-read-write
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/SharedStateReadWriteController.java
- 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-shared-state-read-write/route.ts
- id: shared-state-streaming
name: Shared State Streaming
description:
Per-token state streaming — the agent's write_document tool streams
content into state.document via STATE_SNAPSHOT events; the frontend
renders the growing document live
tags:
- agent-state
route: /demos/shared-state-streaming
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/SharedStateStreamingController.java
- src/app/demos/shared-state-streaming/page.tsx
- src/app/demos/shared-state-streaming/document-view.tsx
- src/app/api/copilotkit-shared-state-streaming/route.ts
- id: subagents
name: Sub-Agents
description:
Supervisor delegates to research / writing / critique sub-agents
(each its own ChatClient call). Every delegation appends a Delegation
entry to state.delegations and emits a STATE_SNAPSHOT for a live UI log
tags:
- agent-capabilities
route: /demos/subagents
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/SubagentsController.java
- src/app/demos/subagents/page.tsx
- src/app/demos/subagents/delegation-log.tsx
- src/app/api/copilotkit-subagents/route.ts
- id: multimodal
name: Multimodal Attachments
description: CopilotChat AttachmentsConfig for images + PDFs; sample files
inject through the same pipeline as the paperclip upload
tags:
- interactivity
route: /demos/multimodal
animated_preview_url:
highlight:
- src/app/demos/multimodal/page.tsx
- src/app/demos/multimodal/sample-attachment-buttons.tsx
- src/app/api/copilotkit-multimodal/route.ts
- id: hitl-in-chat
name: In-Chat Human in the Loop
description: User approves agent actions before execution via text input inside the chat
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/app/api/copilotkit/route.ts
- id: hitl-in-app
name: In-App Human in the Loop
description:
Agent requests approval via useFrontendTool; the approval UI pops
up as an app-level modal OUTSIDE the chat
tags:
- interactivity
route: /demos/hitl-in-app
animated_preview_url:
highlight:
- src/app/demos/hitl-in-app/page.tsx
- src/app/demos/hitl-in-app/approval-dialog.tsx
- src/app/api/copilotkit/route.ts
- id: hitl
name: Human in the Loop (Steps)
description:
Multi-step HITL flow where the agent pauses for user confirmation
at each step
tags:
- interactivity
route: /demos/hitl
animated_preview_url:
highlight:
- src/app/demos/hitl/page.tsx
- src/app/api/copilotkit/route.ts
- id: tool-rendering
name: Tool Rendering
description: Backend agent tools rendered as UI components
tags:
- agent-capabilities
route: /demos/tool-rendering
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/tools/WeatherTool.java
- src/app/demos/tool-rendering/page.tsx
- src/app/api/copilotkit/route.ts
- id: cli-start
name: CLI Start Command
description: Copy-paste command to clone the canonical Spring AI starter
tags:
- chat-ui
command: "npx copilotkit@latest init --framework spring-ai"
- id: gen-ui-tool-based
name: Tool-Based Generative UI
description:
Frontend registers per-tool component renderers via useComponent;
the Spring agent calls render tools that paint chart components inline in
the chat
tags:
- generative-ui
route: /demos/gen-ui-tool-based
animated_preview_url:
highlight:
- src/app/demos/gen-ui-tool-based/page.tsx
- src/app/demos/gen-ui-tool-based/bar-chart.tsx
- src/app/demos/gen-ui-tool-based/pie-chart.tsx
- src/app/api/copilotkit/route.ts
- id: tool-rendering-default-catchall
name: Tool Rendering (Default Catch-all)
description:
Out-of-the-box tool rendering — the Spring backend defines 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/main/java/com/copilotkit/showcase/springai/AgentConfig.java
- src/app/api/copilotkit/route.ts
- id: tool-rendering-custom-catchall
name: Tool Rendering (Custom Catch-all)
description: Single branded wildcard renderer via useDefaultRenderTool — the
same app-designed card paints every Spring 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/main/java/com/copilotkit/showcase/springai/AgentConfig.java
- src/app/api/copilotkit/route.ts
- id: hitl-in-chat-booking
name: In-Chat HITL (Booking)
description: Time-picker card rendered inline via useHumanInTheLoop for a
booking flow against the Spring AI 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/app/api/copilotkit/route.ts
- id: mcp-apps
name: MCP Apps
description:
MCP server-driven UI via the runtime mcpApps middleware; sandboxed
iframe rendered via the built-in MCPAppsActivityRenderer
tags:
- generative-ui
route: /demos/mcp-apps
animated_preview_url:
highlight:
- src/app/demos/mcp-apps/page.tsx
- src/app/api/copilotkit-mcp-apps/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/main/java/com/copilotkit/showcase/springai/ByocHashbrownController.java
- 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: declarative-json-render
name: "Declarative UI: json-render"
description: Streaming structured output via @json-render/react, rendering a sales dashboard catalog (MetricCard + PieChart + BarChart)
tags:
- generative-ui
route: /demos/declarative-json-render
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/ByocJsonRenderController.java
- src/app/demos/declarative-json-render/page.tsx
- src/app/demos/declarative-json-render/json-render-renderer.tsx
- 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: gen-ui-interrupt
name: Generative UI (Interrupt)
description:
Time-picker card rendered inline via useFrontendTool with an async
handler that blocks until the user picks a slot — Strategy B adaptation of
LangGraph's interrupt() primitive
tags:
- interactivity
route: /demos/gen-ui-interrupt
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/InterruptAgentController.java
- 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: Headless Interrupt
description:
Time-picker popup rendered in the app surface (outside the chat)
via useFrontendTool with an async handler — headless variant of the
interrupt-adapted pattern
tags:
- interactivity
route: /demos/interrupt-headless
animated_preview_url:
highlight:
- src/main/java/com/copilotkit/showcase/springai/InterruptAgentController.java
- src/app/demos/interrupt-headless/page.tsx
- src/app/api/copilotkit/route.ts
@@ -0,0 +1,25 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Allow iframe embedding from the showcase shell
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "ALLOWALL",
},
{
key: "Content-Security-Policy",
value: "frame-ancestors *;",
},
],
},
];
},
typescript: { ignoreBuildErrors: true },
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
{
"name": "@copilotkit/showcase-spring-ai",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test:e2e": "playwright test"
},
"dependencies": {
"@ag-ui/client": "0.0.57",
"@copilotkit/a2ui-renderer": "1.61.2",
"@copilotkit/react-core": "1.61.2",
"@copilotkit/runtime": "1.61.2",
"@copilotkit/shared": "1.61.2",
"@copilotkit/voice": "1.61.2",
"@hashbrownai/core": "0.5.0-beta.4",
"@hashbrownai/react": "0.5.0-beta.4",
"@json-render/core": "0.18.0",
"@json-render/react": "0.18.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-separator": "^1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"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",
"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,35 @@
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": "spring-ai",
},
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: process.env.CI
? undefined
: {
command: "pnpm dev",
url: "http://localhost:3000",
reuseExistingServer: true,
env: {
...process.env,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || "",
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
},
},
});
+96
View File
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>
<groupId>com.copilotkit.showcase</groupId>
<artifactId>spring-ai-agent</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.1</spring-ai.version>
<ag-ui.version>0.0.1</ag-ui.version>
<ag-ui.spring-ai.version>1.0.1</ag-ui.spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI (OpenAI) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- AG-UI Java SDK (installed from source in Dockerfile) -->
<dependency>
<groupId>com.ag-ui.community</groupId>
<artifactId>spring-ai</artifactId>
<version>${ag-ui.spring-ai.version}</version>
</dependency>
<dependency>
<groupId>com.ag-ui.community</groupId>
<artifactId>java-server</artifactId>
<version>${ag-ui.version}</version>
</dependency>
<dependency>
<groupId>com.ag-ui.community</groupId>
<artifactId>spring</artifactId>
<version>${ag-ui.version}</version>
</dependency>
<!-- Caffeine: bounded, time-expiring cache backing BoundedToolCallingManager's
per-ChatOptions iteration counter. Prevents the map from growing unbounded
on the happy-path (conversation turn completes naturally without hitting the
cap) since Spring-AI's outer loop never signals "turn finished" back to the
ToolCallingManager. expireAfterAccess(5m) bounds worst-case residency. -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Test scope: JUnit 5 + Mockito via Spring Boot starter. Kept test-only
so the production JAR stays lean. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>agent</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
@@ -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.
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd4aa7b049f1c3e324dfd15af4068d7f8fbf2eae1dd044df270dddc5f38a5c57
size 87078
@@ -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,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3da2afae36a1a81fd2c02f15e54bfc38b6c22e41655c31a5b54ff1e0e3daab41
size 2486
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01aa5681de99461247543e9215c1e4da3242e26b2bee11593fcdbe209672d973
size 10083
@@ -0,0 +1,19 @@
# QA: A2UI Fixed Schema — Spring AI
## Prerequisites
- Spring AI backend is up with A2uiFixedSchemaController mounted at `/a2ui-fixed-schema/run`
## Test Steps
- [ ] Navigate to `/demos/a2ui-fixed-schema`
- [ ] Click the "Find SFO → JFK" suggestion
- [ ] Verify a Flight Details card appears with SFO, arrow, JFK, UNITED badge, and a $289 price tag
- [ ] Click "Book flight"
- [ ] Verify the button transitions to "Booked ✓" (stateful ActionButton)
## Expected Results
- Spring tool `display_flight` returns the fixed schema + data model
- A2UI middleware on runtime forwards it as ACTIVITY_SNAPSHOT events
- Catalog pins Title/Airport/Arrow/AirlineBadge/PriceTag/Button to React renderers
@@ -0,0 +1,19 @@
# QA: Agent Config — Spring AI
## Prerequisites
- Spring AI backend is up with the AgentConfigController mounted at `/agent-config/run`
## Test Steps
- [ ] Navigate to `/demos/agent-config`
- [ ] Change Tone to "enthusiastic", Expertise to "beginner", Response length to "detailed"
- [ ] Ask "Explain how a database index works"
- [ ] Verify the response is enthusiastic, beginner-friendly, and multi-paragraph
- [ ] Switch Tone to "professional" and Response length to "concise"
- [ ] Ask the same question
- [ ] Verify the response is neutral, precise, and 1-3 sentences
## Expected Results
- The forwarded `tone` / `expertise` / `responseLength` props drive the system prompt composition on the Spring side
@@ -0,0 +1,17 @@
# QA: Agentic Chat (Reasoning) — Spring AI
## Prerequisites
- Spring AI backend is up
## Test Steps
- [ ] Navigate to `/demos/agentic-chat-reasoning`
- [ ] Send a message
- [ ] Verify the page loads and chat responds normally
## Expected Results
- Page demonstrates the `messageView.reasoningMessage` slot pattern
- If the `ag-ui:spring-ai` adapter emits REASONING*MESSAGE*\* events, the custom amber-banner ReasoningBlock renders
- If not, the chat behaves as a normal chat (adapter-level limitation documented in PARITY_NOTES.md)
@@ -0,0 +1,62 @@
# QA: Agentic Chat — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the agentic-chat demo page
- [ ] Verify the chat interface loads with a text input placeholder "Type a message"
- [ ] Verify the background container (`data-testid="background-container"`) is visible
- [ ] Verify the default background color is the theme default (rgb(250, 250, 249))
- [ ] Send a basic message (e.g. "Hello")
- [ ] Verify the agent responds with a text message
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Change background" suggestion button is visible
- [ ] Verify "Generate sonnet" suggestion button is visible
- [ ] Click the "Change background" suggestion
- [ ] Verify the suggestion either populates the input or sends the message
#### Background Change (useFrontendTool)
- [ ] Ask "Change the background to a sunset gradient"
- [ ] Verify the background container style changes from the default
- [ ] Verify the change_background tool returns a success status
#### Weather Render Tool (useRenderTool)
- [ ] Type "What's the weather in Tokyo?"
- [ ] Verify loading state shows "Loading weather..." (`data-testid="weather-info-loading"`)
- [ ] Verify WeatherCard renders (`data-testid="weather-info"`) with:
- [ ] City name displayed
- [ ] Temperature in degrees C
- [ ] Humidity percentage
- [ ] Wind speed in mph
- [ ] Conditions text
#### Agent Context
- [ ] Verify the agent knows the user's name is "Bob" (provided via useAgentContext)
- [ ] Ask "What is my name?" and verify the agent responds with "Bob"
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
- [ ] Send a very long message and verify no UI breakage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Background changes are instant after tool execution
- Weather card renders with all data fields populated
- No UI errors or broken layouts
@@ -0,0 +1,68 @@
# QA: Authentication — Spring AI
## Prerequisites
- Demo deployed and accessible at /demos/auth
- Spring AI showcase service healthy
- OPENAI_API_KEY set on the Spring AI deployment
## 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,17 @@
# QA: Beautiful Chat — Spring AI
## Prerequisites
- Spring AI backend is up
## Test Steps
- [ ] Navigate to `/demos/beautiful-chat`
- [ ] Verify the polished brand surface (gradient background, rounded card) renders
- [ ] Click a suggestion pill (e.g. "Plan a 3-day Tokyo trip")
- [ ] Verify the agent responds in the themed chat surface
## Expected Results
- Chat runs against the shared Spring AI ChatClient backend
- Brand-themed layout, suggestions, and chat shell all render correctly
@@ -0,0 +1,19 @@
# QA: Chat Customization (CSS) — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
- [ ] Navigate to `/demos/chat-customization-css`
- [ ] Verify the chat renders inside the scoped `.chat-css-demo-scope` wrapper
- [ ] Verify user messages have the hot-pink gradient bubble style
- [ ] Verify assistant messages have the amber boxy monospace style
- [ ] Send a message and verify both bubble styles render correctly
## Expected Results
- Chat loads within 3 seconds
- Themed styles only apply inside the demo scope (no global leak)
@@ -0,0 +1,17 @@
# QA: Chat Slots — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/chat-slots`
- [ ] Verify the custom welcome screen renders (`data-testid="custom-welcome-screen"`)
- [ ] Verify the custom disclaimer renders (`data-testid="custom-disclaimer"`)
- [ ] Send a message
- [ ] Verify assistant messages are wrapped in the indigo "slot" card
## Expected Results
- All three slot overrides are active: welcome screen, disclaimer, assistantMessage
@@ -0,0 +1,20 @@
# QA: Declarative Generative UI — Spring AI
## Prerequisites
- Spring AI backend is up with the `generate_a2ui` tool registered
- OPENAI_API_KEY is set (the dynamic-A2UI tool makes a secondary LLM call)
## Test Steps
- [ ] Navigate to `/demos/declarative-gen-ui`
- [ ] Click the "Show a KPI dashboard" suggestion
- [ ] Verify a dashboard with 3-4 metric cards renders inline
- [ ] Click the "What's the weather like?" suggestion (or similar)
- [ ] Verify the agent generates a branded UI card using the catalog components
## Expected Results
- `a2ui.injectA2UITool: false` keeps the tool owned by the Spring backend
- Catalog schema is serialized into `copilotkit.context` so the Spring tool's LLM knows available components
- Rendered components match the catalog definitions in `./a2ui/renderers.tsx`
@@ -0,0 +1,17 @@
# QA: Frontend Tools (Async) — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/frontend-tools-async`
- [ ] Ask "Find my notes about project planning"
- [ ] Verify the NotesCard shows a loading state briefly (simulated 500ms latency)
- [ ] Verify the NotesCard then renders with matching notes
- [ ] Verify the agent's text summary references the found notes
## Expected Results
- Async frontend tool resolves correctly and the agent receives the result
@@ -0,0 +1,16 @@
# QA: Frontend Tools — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/frontend-tools`
- [ ] Ask "Change the background to a blue-to-purple gradient"
- [ ] Verify the background-container background changes
- [ ] Verify the change_background tool returns a success status
## Expected Results
- Frontend tool fires and updates the UI state
@@ -0,0 +1,62 @@
# QA: Agentic Generative UI — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the gen-ui-agent demo page
- [ ] Verify the chat interface loads in a centered full-height layout
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Simple plan" suggestion button is visible (plan to go to Mars in 5 steps)
- [ ] Verify "Complex plan" suggestion button is visible (plan to make pizza in 10 steps)
#### Task Progress Tracker (useAgent with state streaming)
- [ ] Click "Simple plan" suggestion or type "Build a plan to go to Mars in 5 steps"
- [ ] Verify the TaskProgress component renders (`data-testid="task-progress"`)
- [ ] Verify the progress bar appears with a gradient fill
- [ ] Verify step items appear with descriptions (`data-testid="task-step-text"`)
- [ ] Verify the "N/N Complete" counter updates as steps complete
- [ ] Verify completed steps show:
- Green background gradient
- Check icon
- Green text color
- [ ] Verify the current pending step shows:
- Blue/purple background gradient
- Spinner icon with "Processing..." text
- Pulsing animation
- [ ] Verify future pending steps show:
- Gray background
- Clock icon
- Muted text color
#### Complex Plan
- [ ] Type "Plan to make pizza in 10 steps"
- [ ] Verify 10 steps appear in the progress tracker
- [ ] Verify progress bar width increases as steps complete
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Task progress tracker shows live step completion
- Progress bar animates smoothly
- No UI errors or broken layouts
@@ -0,0 +1,59 @@
# QA: Tool-Based Generative UI — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the gen-ui-tool-based demo page
- [ ] Verify the CopilotSidebar opens by default with title "Haiku Generator"
- [ ] Verify a placeholder haiku card is displayed on the main area
- [ ] Send a basic message via the sidebar
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Nature Haiku" suggestion button is visible
- [ ] Verify "Ocean Haiku" suggestion button is visible
- [ ] Verify "Spring Haiku" suggestion button is visible
#### Haiku Generation (useFrontendTool)
- [ ] Click the "Nature Haiku" suggestion or type "Write me a haiku about nature"
- [ ] Verify a HaikuCard renders (`data-testid="haiku-card"`) with:
- [ ] Three Japanese text lines (`data-testid="haiku-japanese-line"`)
- [ ] Three English translation lines (`data-testid="haiku-english-line"`)
- [ ] A background gradient style applied to the card
- [ ] Verify the Japanese text contains actual Japanese characters (not Latin)
- [ ] Verify the English lines are a readable English translation
#### Image Display
- [ ] After haiku generation, verify an image renders (`data-testid="haiku-image"`) if the agent provides an image_name
- [ ] Verify the image src points to /images/ with a valid filename from the predefined list
#### Multiple Haikus
- [ ] Generate a second haiku (e.g. "Ocean Haiku")
- [ ] Verify the new haiku card appears at the top
- [ ] Verify the previous haiku card is still visible below
- [ ] Verify the initial placeholder haiku is removed
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Sidebar loads within 3 seconds
- Agent responds and generates haiku within 10 seconds
- Haiku cards display with Japanese and English text
- Generated haikus stack with newest on top
- No UI errors or broken layouts
@@ -0,0 +1,19 @@
# QA: Headless Chat (Complete) — Spring AI
## Prerequisites
- Spring AI backend is up
## Test Steps
- [ ] Navigate to `/demos/headless-complete`
- [ ] Send "Hi"
- [ ] Verify distinct user/assistant bubbles render (hand-rolled, not CopilotChat)
- [ ] Ask "What's the weather in Tokyo?"
- [ ] Verify the WeatherCard renders via the hand-rolled tool-call renderer
- [ ] Press the stop button during a long reply — verify the agent halts
## Expected Results
- Chat is built entirely on `useAgent` + low-level render hooks
- MCP-driven activity rendering is a no-op on Spring; tool + A2UI renderers still compose
@@ -0,0 +1,17 @@
# QA: Headless Simple — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/headless-simple`
- [ ] Send "Hi"
- [ ] Verify a user bubble appears and the agent responds
- [ ] Ask "Show a card about cats"
- [ ] Verify a ShowCard component renders via useComponent
## Expected Results
- Headless surface renders messages and tool renderings via useAgent + useComponent
@@ -0,0 +1,19 @@
# QA: In-App Human in the Loop — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/hitl-in-app`
- [ ] Verify the tickets panel renders three mock tickets
- [ ] Ask "Approve a $50 refund to Jordan Rivera on ticket #12345"
- [ ] Verify an ApprovalDialog modal appears outside the chat
- [ ] Click Approve with an optional note
- [ ] Verify the agent receives the approval and confirms the action
## Expected Results
- Modal appears outside the chat surface
- Approve/Reject resolves the pending tool Promise
@@ -0,0 +1,17 @@
# QA: In-Chat HITL (hitl-in-chat) — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/hitl-in-chat`
- [ ] Ask "Please book an intro call with the sales team"
- [ ] Verify a TimePickerCard renders inline in the chat (`data-testid="time-picker-card"`)
- [ ] Click a time slot
- [ ] Verify the agent acknowledges the booking with the chosen time
## Expected Results
- The useHumanInTheLoop card renders inside the chat and resolves back to the agent
@@ -0,0 +1,22 @@
# QA: MCP Apps — Spring AI
## Prerequisites
- Spring AI backend is up
- NextJS runtime can reach `https://mcp.excalidraw.com` (or configured MCP_SERVER_URL)
## Test Steps
- [ ] Navigate to `/demos/mcp-apps`
- [ ] Click "Draw a flowchart"
- [ ] Verify the agent invokes an MCP tool and the Excalidraw UI resource appears inline as a sandboxed iframe
## Expected Results
- `mcpApps.servers` on the runtime auto-adds MCP tools to the forwarded tool list
- Agent calls an MCP tool; middleware emits the activity event with the fetched UI resource
- Built-in `MCPAppsActivityRenderer` renders the iframe
## Known gaps
- If the `ag-ui:spring-ai` adapter does not forward the runtime-injected MCP tools through to the Spring ChatClient's tool-calling surface, the agent will not invoke them. In that case the activity event is never emitted and the iframe does not render; see PARITY_NOTES.md for context.
@@ -0,0 +1,19 @@
# QA: Multimodal Attachments — Spring AI
## Prerequisites
- Spring AI backend is up with a vision-capable OpenAI model (e.g. gpt-4.1 / gpt-4o)
## Test Steps
- [ ] Navigate to `/demos/multimodal`
- [ ] Click "Try sample image" to inject the bundled PNG
- [ ] Verify the paperclip shows the attachment
- [ ] Ask "What do you see in this image?"
- [ ] If the `ag-ui:spring-ai` adapter forwards media to `UserMessage.media`, verify the agent describes the image contents
- [ ] Try the same with the sample PDF
## Expected Results
- Attachment UX (paperclip + sample buttons + drag-drop + clipboard paste) works
- Adapter-level image forwarding behavior is integration-dependent; see PARITY_NOTES.md
@@ -0,0 +1,20 @@
# QA: Open-Ended Generative UI (Advanced) — Spring AI
## Prerequisites
- Demo is deployed and accessible
- OPENAI_API_KEY is set on the Spring backend
## Test Steps
- [ ] Navigate to `/demos/open-gen-ui-advanced`
- [ ] Click the "Calculator (calls evaluateExpression)" suggestion
- [ ] Verify a calculator UI appears inside a sandboxed iframe
- [ ] Type `12 * (3 + 4.5)`, press `=`
- [ ] Verify the host-side `evaluateExpression` runs and the sandbox displays `94.5`
- [ ] Check browser console for `[open-gen-ui/advanced] evaluateExpression`
## Expected Results
- Sandbox function round-trips complete end-to-end
- Console logs confirm host invocation
@@ -0,0 +1,18 @@
# QA: Open-Ended Generative UI — Spring AI
## Prerequisites
- Demo is deployed and accessible
- OPENAI_API_KEY is set on the Spring backend
## Test Steps
- [ ] Navigate to `/demos/open-gen-ui`
- [ ] Click a suggestion (e.g. "3D axis visualization")
- [ ] Verify a sandboxed iframe renders an educational visualisation inside the chat stream
- [ ] Confirm the visualisation is self-running (no host-side functions required)
## Expected Results
- Agent streams HTML + CSS into the built-in OpenGenerativeUIActivityRenderer
- The iframe mounts and animates without user interaction
@@ -0,0 +1,15 @@
# QA: Pre-Built Popup — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/prebuilt-popup`
- [ ] Verify the popup launcher is visible and the popup opens by default
- [ ] Send a message via the popup and verify the agent responds
## Expected Results
- Popup chat surface works with the Spring AI agent
@@ -0,0 +1,16 @@
# QA: Pre-Built Sidebar — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/prebuilt-sidebar`
- [ ] Verify the sidebar is open by default
- [ ] Toggle the sidebar via its launcher button
- [ ] Send a message and verify the agent responds inside the sidebar
## Expected Results
- Sidebar chat surface works with the Spring AI agent
@@ -0,0 +1,18 @@
# QA: Readonly State / Agent Context — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/readonly-state-agent-context`
- [ ] Verify the context card renders with Name, Timezone, Recent Activity
- [ ] Ask "What do you know about me?"
- [ ] Verify the agent reflects the context values (name, timezone, activities)
- [ ] Toggle an activity checkbox
- [ ] Ask again and verify the agent sees the updated context
## Expected Results
- The agent receives the latest frontend context via useAgentContext
@@ -0,0 +1,17 @@
# QA: Reasoning (Default Render) — Spring AI
## Prerequisites
- Spring AI backend is up
## Test Steps
- [ ] Navigate to `/demos/reasoning-default-render`
- [ ] Send a message
- [ ] Verify the page loads and chat responds normally
## Expected Results
- Zero-config reasoning: no `reasoningMessage` slot is registered
- If REASONING*MESSAGE*\* events are emitted, the built-in CopilotChatReasoningMessage renders a collapsible card
- If not (current Spring AI adapter default), the chat behaves as a normal chat
@@ -0,0 +1,78 @@
# QA: Shared State (Read + Write) — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check `/api/health`)
- `OPENAI_API_KEY` is set on the Spring backend
## Architecture under test
- The UI owns a `preferences = { name, tone, language, interests }` object
and writes it into agent state via `agent.setState({ preferences, notes })`.
- Spring's `SharedStateReadWriteController` runs a per-request `LocalAgent`
subclass that reads `preferences` off the AG-UI envelope and injects them
into the system prompt every turn.
- The agent has a single tool, `set_notes(notes: List<String>)`, that
mutates `state.notes` and emits a `STATE_SNAPSHOT` event so the
`useAgent({ updates: [OnStateChanged] })` hook re-renders the page.
## Test Steps
### 1. Page load
- [ ] Navigate to `/demos/shared-state-read-write`.
- [ ] Verify both sidebar cards render: `data-testid="preferences-card"` and
`data-testid="notes-card"`.
- [ ] Verify the chat input is visible on the right.
- [ ] Confirm the preferences JSON pre block (`data-testid="pref-state-json"`)
shows the seeded defaults: `{ name: "", tone: "casual", language:
"English", interests: [] }`.
### 2. Write side (UI -> agent)
- [ ] Type "Atai" into `data-testid="pref-name"`.
- [ ] Switch tone to "playful" via `data-testid="pref-tone"`.
- [ ] Switch language to "Spanish" via `data-testid="pref-language"`.
- [ ] Click two interest chips (e.g. "Cooking", "Music").
- [ ] Confirm the JSON pre block updates live to reflect every edit.
- [ ] Send the message "Greet me." and verify the assistant's reply:
- addresses the user as "Atai",
- is in Spanish,
- has a playful tone (exclamations / light humor OK),
- and references at least one selected interest if natural.
### 3. Read side (agent -> UI)
- [ ] Send: "Remember that I prefer morning meetings and that I don't eat
dairy."
- [ ] Verify the agent calls `set_notes` (visible briefly in the chat
stream) and that the Notes card shows two list items shortly after.
- [ ] Verify `data-testid="notes-list"` is present and contains two
`data-testid="note-item"` entries.
- [ ] Send another remember-style message: "Also remember I'm vegetarian."
- [ ] Verify the notes list grows (the agent should pass the FULL list,
not a diff), now showing three items.
### 4. Bidirectional clear
- [ ] Click `data-testid="notes-clear-button"` in the Notes card.
- [ ] Verify the list is cleared and `data-testid="notes-empty"` appears.
- [ ] Send "What did I tell you to remember?" — agent should not hallucinate
removed notes (memory persists in chat history but the notes panel
is canonical).
### 5. Error handling
- [ ] Send an empty message — UI should handle gracefully.
- [ ] Disconnect briefly: verify reconnect does not corrupt state.
- [ ] No console errors during normal usage.
## Expected Results
- Page loads within 3 seconds.
- Preferences edits show in the JSON pre block instantly (local state).
- Agent replies adapt to preferences within 10 seconds.
- Notes panel reflects `set_notes` calls within 1 second after the tool
result event lands (driven by the STATE_SNAPSHOT emission).
- No UI errors, no orphaned state across renders.
@@ -0,0 +1,75 @@
# QA: Shared State (Reading) — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-read demo page
- [ ] Verify the recipe card form loads (`data-testid="recipe-card"`)
- [ ] Verify the CopilotSidebar opens by default with title "AI Recipe Assistant"
- [ ] Send a message via the sidebar
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Initial Recipe State
- [ ] Verify the recipe title input shows "Make Your Recipe"
- [ ] Verify the cooking time dropdown defaults to "45 min"
- [ ] Verify the skill level dropdown defaults to "Intermediate"
- [ ] Verify the default ingredients are displayed:
- [ ] Carrots (3 large, grated) with carrot emoji
- [ ] All-Purpose Flour (2 cups) with wheat emoji
- [ ] Verify the default instruction is displayed: "Preheat oven to 350 F"
#### Suggestions
- [ ] Verify "Create Italian recipe" suggestion is visible
- [ ] Verify "Make it healthier" suggestion is visible
- [ ] Verify "Suggest variations" suggestion is visible
#### Recipe Editing (Local State)
- [ ] Edit the recipe title and verify it updates
- [ ] Change the skill level dropdown and verify it updates
- [ ] Change the cooking time dropdown and verify it updates
- [ ] Toggle a dietary preference checkbox (e.g. "Vegetarian") and verify it's checked
- [ ] Click "+ Add Ingredient" (`data-testid="add-ingredient-button"`) and verify a new empty row appears
- [ ] Edit an ingredient name and amount
- [ ] Remove an ingredient by clicking the "x" button
- [ ] Click "+ Add Step" and verify a new instruction row appears
- [ ] Edit an instruction and verify it saves
- [ ] Remove an instruction by clicking the "x" button
#### AI-Powered Recipe Updates (useAgent with shared state)
- [ ] Click "Create Italian recipe" suggestion
- [ ] Verify the agent updates the recipe title, ingredients, and instructions
- [ ] Verify the ping indicator appears on changed sections
- [ ] Verify the "Improve with AI" button (`data-testid="improve-button"`) changes to "Please Wait..." while loading
- [ ] Click "Improve with AI" and verify the recipe is enhanced
#### Agent Reads Frontend State
- [ ] Edit the recipe (change title, add ingredients)
- [ ] Ask the agent "What recipe am I making?"
- [ ] Verify the agent's response references the current recipe state
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
- [ ] Verify the "Improve with AI" button is disabled while loading
## Expected Results
- Recipe card and sidebar load within 3 seconds
- Agent responds within 10 seconds
- Recipe state syncs bidirectionally between UI and agent
- Ping indicators highlight changed sections
- No UI errors or broken layouts
@@ -0,0 +1,41 @@
# QA: State Streaming — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-streaming demo page
- [ ] Verify the chat interface loads with title "State Streaming"
- [ ] Verify the chat input placeholder "Type a message..." is visible
- [ ] Send a basic message (e.g. "Hello! What can you do?")
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Get started" suggestion button is visible
#### Note: Stub Demo
> **Status: Stub** — This demo is currently a stub (TODO: implement)
- [ ] Verify the basic CopilotChat loads and accepts messages
- [ ] Verify the agent responds to messages
- [ ] No custom UI components are expected beyond the chat interface
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- No UI errors or broken layouts
@@ -0,0 +1,41 @@
# QA: Shared State (Writing) — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-write demo page
- [ ] Verify the chat interface loads with title "Shared State (Writing)"
- [ ] Verify the chat input placeholder "Type a message..." is visible
- [ ] Send a basic message (e.g. "Hello! What can you do?")
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Get started" suggestion button is visible
#### Note: Stub Demo
> **Status: Stub** — This demo is currently a stub (TODO: implement)
- [ ] Verify the basic CopilotChat loads and accepts messages
- [ ] Verify the agent responds to messages
- [ ] No custom UI components are expected beyond the chat interface
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- No UI errors or broken layouts
@@ -0,0 +1,89 @@
# QA: Sub-Agents — Spring AI
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check `/api/health`)
- `OPENAI_API_KEY` is set on the Spring backend
## Architecture under test
- Spring's `SubagentsController` runs a per-request supervisor agent that
exposes three tools — `research_agent`, `writing_agent`, `critique_agent`
— each backed by its own `ChatClient` call with a dedicated system
prompt (research vs. writing vs. critique).
- Every tool invocation appends a `Delegation` entry
`{ id, sub_agent, task, status, result }` to `state.delegations` and
emits a `STATE_SNAPSHOT` event, so the UI's live "delegation log"
panel grows entry-by-entry while the supervisor works.
## Test Steps
### 1. Page load
- [ ] Navigate to `/demos/subagents`.
- [ ] Verify the delegation log card renders
(`data-testid="delegation-log"`).
- [ ] Verify it shows the empty-state message
("Ask the supervisor to complete a task...").
- [ ] Verify `data-testid="delegation-count"` reads "0 calls".
- [ ] Verify the chat panel is visible on the right with the
"Give the supervisor a task..." placeholder.
### 2. Single delegation
- [ ] Send: "Research the basics of magnesium supplementation. Just one
delegation, no writing or critique."
- [ ] While the supervisor is running, verify
`data-testid="supervisor-running"` is visible.
- [ ] Verify exactly one `data-testid="delegation-entry"` appears with
sub-agent label "Research" and status "completed".
- [ ] Verify `data-testid="delegation-count"` reads "1 calls".
### 3. Full pipeline
- [ ] Click the "Write a blog post" suggestion (or send the equivalent
message manually).
- [ ] Watch the delegation log grow in real time:
- [ ] First entry: Research (sub_agent = `research_agent`)
- [ ] Second entry: Writing (sub_agent = `writing_agent`) — should
contain a polished one-paragraph draft.
- [ ] Third entry: Critique (sub_agent = `critique_agent`) — should
contain 23 actionable critiques.
- [ ] Verify each entry's "Task" line shows what the supervisor passed
to that sub-agent.
- [ ] Verify final supervisor message is concise (a short summary, not a
replay of all three sub-agent outputs).
### 4. Live snapshot ordering
- [ ] Verify the entries appear incrementally — the writing entry should
not show up at the same time as the research entry; if it does, the
backend is batching state snapshots.
### 5. Multi-turn
- [ ] After the pipeline completes, send: "Now do the same for the
benefits of cold exposure training."
- [ ] Verify three more entries are appended (count = 6).
- [ ] Verify older entries remain visible (delegation log is append-only
within a thread).
### 6. Error handling
- [ ] Send an empty message — UI should handle gracefully.
- [ ] If a sub-agent fails (transient OpenAI error), verify the entry's
`status` field reads "failed" and its result starts with
"Sub-agent failed:".
- [ ] No console errors during normal usage.
## Expected Results
- Page loads within 3 seconds.
- First delegation entry appears within 10 seconds of sending the
message; subsequent entries appear as each sub-agent ChatClient call
completes.
- "Supervisor running" indicator clears as soon as the run finishes.
- Final delegation count matches the number of sub-agent calls the
supervisor made (typically 1 or 3).
- No UI errors, no broken layout, no overlap between log and chat panes.
@@ -0,0 +1,17 @@
# QA: Tool Rendering (Custom Catch-all) — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/tool-rendering-custom-catchall`
- [ ] Ask "What's the weather in San Francisco?"
- [ ] Verify the branded custom catch-all card appears (`data-testid="custom-catchall-card"`)
- [ ] Verify the card includes tool name, status badge, Arguments and Result sections
- [ ] Try "Find flights from SFO to JFK" and verify the same branded card paints
## Expected Results
- All tool calls render via the custom catch-all component
@@ -0,0 +1,18 @@
# QA: Tool Rendering (Default Catch-all) — Spring AI
## Prerequisites
- Demo is deployed and accessible
## Test Steps
- [ ] Navigate to `/demos/tool-rendering-default-catchall`
- [ ] Ask "What's the weather in San Francisco?"
- [ ] Verify the default tool-call card appears with status Running -> Done
- [ ] Verify Arguments and Result sections populate
- [ ] Try "Find flights from SFO to JFK"
- [ ] Verify another tool card appears with the same default renderer
## Expected Results
- Every backend tool call is painted by the default renderer (no missing tool UI)
@@ -0,0 +1,19 @@
# QA: Tool Rendering (Reasoning Chain) — Spring AI
## Prerequisites
- Spring AI backend is up with `get_weather` and `search_flights` tools
## Test Steps
- [ ] Navigate to `/demos/tool-rendering-reasoning-chain`
- [ ] Send: "Check weather in Tokyo, then search for flights SFO to JFK"
- [ ] Verify WeatherCard renders for the weather tool call
- [ ] Verify FlightListCard renders for the search_flights tool call
- [ ] If the adapter emits reasoning events, verify the ReasoningBlock renders between tool calls
## Expected Results
- Per-tool custom renderers fire for `get_weather` and `search_flights`
- Catchall renderer handles any unexpected tools
- Reasoning slot renders when backend provides reasoning content
@@ -0,0 +1,61 @@
# QA: Tool Rendering — Spring AI
## 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,20 @@
# QA: Voice — Spring AI
## Prerequisites
- OPENAI_API_KEY is set on the NextJS runtime (voice transcription uses Whisper)
- Spring AI backend is up
## Test Steps
- [ ] Navigate to `/demos/voice`
- [ ] Verify the microphone button appears in the chat composer
- [ ] Click "Play sample" to inject the bundled audio
- [ ] Confirm the transcribed text appears in the composer textarea
- [ ] Press send and verify the agent responds to the transcribed text
## Expected Results
- `audioFileTranscriptionEnabled: true` is advertised by the runtime probe
- Sample audio transcribes to "What is the weather in Tokyo?"
- Agent responds with weather information from `get_weather` tool
@@ -0,0 +1,47 @@
// Dedicated runtime for the A2UI Fixed Schema demo.
//
// Routes to the Spring /a2ui-fixed-schema/run endpoint, which registers a
// single display_flight tool that emits the fixed flight-card schema and
// data model. The frontend catalog (see src/app/demos/a2ui-fixed-schema/a2ui/)
// pins the Title, Airport, Arrow, AirlineBadge, PriceTag, and Button
// components to React renderers.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/a2ui-fixed-schema/run` });
}
const agents: Record<string, AbstractAgent> = {
"a2ui-fixed-schema": createAgent(),
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-a2ui-fixed-schema",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,47 @@
// Dedicated runtime for the Agent Config Object demo.
//
// Routes to the Spring /agent-config/run endpoint which reads
// forwardedProps (tone, expertise, responseLength) off the AG-UI envelope
// and rebuilds the SpringAIAgent's system prompt per request.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/agent-config/run` });
}
const agentConfigAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
"agent-config-demo": agentConfigAgent,
default: agentConfigAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-agent-config",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,55 @@
// Dedicated runtime for the /demos/auth cell.
//
// Demonstrates framework-native request authentication via the V2 runtime's
// `onRequest` hook, which runs before routing and can short-circuit by
// throwing a Response. We validate a static `Authorization: Bearer
// <DEMO_TOKEN>` header; mismatch throws 401 before the request reaches the
// underlying Spring AI HttpAgent.
import type { NextRequest } from "next/server";
import {
CopilotRuntime,
createCopilotRuntimeHandler,
} from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { DEMO_AUTH_HEADER } from "@/app/demos/auth/demo-token";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const authDemoAgent = new HttpAgent({ url: `${AGENT_URL}/` });
const runtime = new CopilotRuntime({
// @ts-ignore - agents type shape as in main route
agents: {
"auth-demo": authDemoAgent,
default: authDemoAgent,
},
});
const BASE_PATH = "/api/copilotkit-auth";
const handler = createCopilotRuntimeHandler({
runtime,
basePath: BASE_PATH,
hooks: {
onRequest: ({ request }) => {
const authHeader = request.headers.get("authorization");
if (authHeader !== DEMO_AUTH_HEADER) {
throw new Response(
JSON.stringify({
error: "unauthorized",
message:
"Missing or invalid Authorization header. Click Authenticate above to send messages.",
}),
{
status: 401,
headers: { "content-type": "application/json" },
},
);
}
},
},
});
export const POST = (req: NextRequest) => handler(req);
export const GET = (req: NextRequest) => handler(req);
@@ -0,0 +1,74 @@
// Dedicated runtime for the Beautiful Chat flagship showcase cell (Spring AI).
//
// Beautiful Chat exercises A2UI (dynamic + fixed schema) and Open
// Generative UI. The Spring AI backend serves its main agent at "/" (see
// the `@PostMapping("/")` controller); there is no dedicated beautiful-chat
// controller, so the cell routes there and the flagship behavior comes from
// the runtime flags below plus the frontend's per-cell registrations.
//
// Isolated on its own endpoint (mirroring beautiful-chat in the canonical)
// because the `openGenerativeUI` / `a2ui` runtime flags set global state on
// the probe response that would otherwise leak into the other cells sharing
// the default `/api/copilotkit` runtime.
//
// Mirrors showcase/integrations/pydantic-ai/src/app/api/copilotkit-beautiful-chat/route.ts.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import type { AbstractAgent } from "@ag-ui/client";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// The beautiful-chat page resolves <CopilotKit agent="beautiful-chat">
// here; internal components (headless-chat, example-canvas) also call
// `useAgent()` with no args, which defaults to agentId "default". Alias
// default to the same backend so those hooks resolve.
const agents: Record<string, AbstractAgent> = {
"beautiful-chat": new HttpAgent({ url: `${AGENT_URL}/` }),
default: new HttpAgent({ url: `${AGENT_URL}/` }),
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
openGenerativeUI: true,
a2ui: {
// The targeted Spring AI agent ("/") already registers the
// `generate_a2ui` tool itself (GenerateA2uiTool in AgentConfig), so the
// runtime must NOT inject a second copy — that would double-bind the
// render tool. Matches pydantic-ai's beautiful-chat (this file's mirror
// source).
injectA2UITool: false,
// Models follow the tool-usage guide and omit `catalogId`, and the
// middleware then falls back to the unregistered spec basic catalog
// ("Catalog not found" render error). Pin the catalog the page registers.
defaultCatalogId: "copilotkit://app-dashboard-catalog",
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-beautiful-chat",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
console.error(
`[copilotkit-beautiful-chat/route] ERROR: ${e.message}`,
e.stack,
);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
};
@@ -0,0 +1,48 @@
// Dedicated runtime for the BYOC json-render demo (Spring AI).
//
// The demo page renders the agent's JSON output into a frontend-owned
// component catalog via @json-render/react. The Spring AI backend runs a
// dedicated controller at /byoc-json-render/run whose system prompt instructs
// the LLM to emit a flat-spec JSON object matching the catalog's schema.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/byoc-json-render/run` });
}
const byocJsonRenderAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
byoc_json_render: byocJsonRenderAgent,
default: byocJsonRenderAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-byoc-json-render",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,54 @@
// Dedicated runtime for the Declarative Generative UI (A2UI — Dynamic Schema) demo.
//
// Configured with `a2ui.injectA2UITool: false` — the Spring backend owns the
// `generate_a2ui` tool explicitly (see AgentConfig.java / GenerateA2uiTool.java).
// The A2UI middleware still serialises the registered catalog schema into
// `copilotkit.context` so the Spring-side secondary LLM inside
// `generate_a2ui` sees the catalog.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import type { AbstractAgent } from "@ag-ui/client";
import { HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/` });
}
const agents: Record<string, AbstractAgent> = {
"declarative-gen-ui": createAgent(),
};
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-gen-ui",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
a2ui: {
injectA2UITool: false,
// Models follow the tool-usage guide and omit `catalogId`, and the
// middleware then falls back to the unregistered spec basic catalog
// ("Catalog not found" render error). Pin the catalog the page registers.
defaultCatalogId: "declarative-gen-ui-catalog",
},
}),
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,54 @@
// Dedicated runtime for the declarative-hashbrown demo (Spring AI).
//
// The demo page wraps CopilotChat in HashBrownDashboard and overrides the
// assistant message slot with a renderer that consumes hashbrown-shaped
// structured output via `@hashbrownai/react`'s `useUiKit` + `useJsonParser`.
// The Spring AI backend runs a dedicated controller at /byoc-hashbrown/run
// (ByocHashbrownController) whose system prompt instructs the LLM to emit the
// hashbrown UI-kit envelope (`{ "ui": [ { "<component>": { "props": {...} } } ] }`);
// this route proxies to it rather than the generic "/" agent. The page
// mounts <CopilotKit agent="declarative-hashbrown-demo">.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent() {
return new HttpAgent({ url: `${AGENT_URL}/byoc-hashbrown/run` });
}
const declarativeHashbrownAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
"declarative-hashbrown-demo": declarativeHashbrownAgent,
default: declarativeHashbrownAgent,
};
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-hashbrown",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
// @ts-expect-error -- see main route.ts
agents,
}),
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
console.error(
`[copilotkit-declarative-hashbrown/route] ERROR: ${e.message}`,
e.stack,
);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
};
@@ -0,0 +1,55 @@
// Dedicated runtime for the declarative-json-render demo (Spring AI).
//
// The demo page renders the agent's JSON output into a frontend-owned
// component catalog via @json-render/react. The Spring AI backend runs a
// dedicated controller at /byoc-json-render/run whose system prompt instructs
// the LLM to emit a flat-spec JSON object matching the catalog's schema. The
// demo folder + route surface were renamed from `byoc-json-render` to the
// canonical `declarative-json-render`; the agent ID retains its legacy
// `byoc_json_render` name.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/byoc-json-render/run` });
}
const byocJsonRenderAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
byoc_json_render: byocJsonRenderAgent,
default: byocJsonRenderAgent,
};
const runtime = new CopilotRuntime({
// @ts-expect-error -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-json-render",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
console.error(
`[copilotkit-declarative-json-render/route] ERROR: ${e.message}`,
e.stack,
);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
};
@@ -0,0 +1,68 @@
// CopilotKit runtime for the MCP Apps cell.
//
// The MCP Apps middleware runs on the NextJS runtime layer — it adds MCP
// server tools to the AG-UI tools list forwarded to the agent, and when
// the agent calls an MCP tool the middleware fetches the associated UI
// resource and emits an `activity` event. The built-in
// `MCPAppsActivityRenderer` registered by CopilotKitProvider renders the
// sandboxed iframe.
//
// For the Spring AI backend, the Spring-AI ChatClient sees the extra MCP
// tools via the standard `tools` field in the AG-UI request; the MCP
// activity rendering is driven entirely by the runtime + frontend.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/` });
}
const agents: Record<string, AbstractAgent> = {
// headless-complete shares this runtime (its page wires
// runtimeUrl="/api/copilotkit-mcp-apps") but is backed by the shared
// Spring-AI ChatClient at "/" — the same backend the main route
// registers it against.
"headless-complete": createAgent(),
"mcp-apps": createAgent(),
};
// @region[runtime-mcpapps-config]
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
mcpApps: {
servers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
serverId: "excalidraw",
},
],
},
});
// @endregion[runtime-mcpapps-config]
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-mcp-apps",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,51 @@
// Dedicated runtime for the Multimodal Attachments demo.
//
// Scoped to its own route mostly so the Playwright spec can assert against
// exactly one URL. The runtime proxies to the same Spring-AI ChatClient
// backend (the existing bean already uses a vision-capable OpenAI model
// such as gpt-4.1). Whether the `ag-ui:spring-ai` adapter currently forwards
// multipart attachments from CopilotChat into Spring AI's
// `UserMessage.media` list is integration-dependent — see PARITY_NOTES.md
// for the current state.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/` });
}
const multimodalAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
"multimodal-demo": multimodalAgent,
default: multimodalAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-multimodal",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
// Dedicated runtime for Open Generative UI demos.
//
// Isolated from the shared `/api/copilotkit` route because the
// `openGenerativeUI` flag on the runtime sets `openGenerativeUIEnabled: true`
// on the probe response. Setting it globally would cause every cell's
// provider to re-mount tools with the OGUI middleware active. Scoping to
// per-demo keeps the OGUI behavior exactly where expected.
//
// The underlying agent is the SAME Spring-AI ChatClient the main runtime
// routes to — the OGUI behavior is driven entirely by the runtime flag
// + provider-side `designSkill` / `sandboxFunctions` props, which get
// injected as agent context on the LLM turn. No new Java endpoint needed;
// we just point multiple named agents at the same Spring controller URL.
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent() {
return new HttpAgent({ url: `${AGENT_URL}/` });
}
const agents: Record<string, AbstractAgent> = {
"open-gen-ui": createAgent(),
"open-gen-ui-advanced": createAgent(),
};
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-ogui",
serviceAdapter: new ExperimentalEmptyAdapter(),
// @region[minimal-runtime-flag]
// @region[advanced-runtime-config]
// Server-side config is identical for the minimal and advanced cells —
// the advanced behaviour (sandbox -> host function calls) is wired
// entirely on the frontend via `openGenerativeUI.sandboxFunctions` on
// the provider. The single `openGenerativeUI` flag below turns on
// Open Generative UI for the listed agent(s); the runtime middleware
// converts each agent's streamed `generateSandboxedUi` tool call into
// `open-generative-ui` activity events.
runtime: new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
openGenerativeUI: {
agents: ["open-gen-ui", "open-gen-ui-advanced"],
},
}),
// @endregion[advanced-runtime-config]
// @endregion[minimal-runtime-flag]
});
return await handleRequest(req);
} catch (error: unknown) {
const err = error as Error;
console.error(`[copilotkit-ogui/route] ERROR: ${err.message}`);
return NextResponse.json(
{ error: err.message, stack: err.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,48 @@
// Dedicated runtime for the Shared State (Read + Write) demo.
//
// Routes to the Spring `/shared-state-read-write/run` endpoint, which runs a
// per-request `LocalAgent` subclass that reads `preferences` off AG-UI state
// to compose its system prompt and exposes a `set_notes` tool that mutates
// the `notes` slot of state and emits a STATE_SNAPSHOT back to the frontend.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/shared-state-read-write/run` });
}
const sharedStateAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
"shared-state-read-write": sharedStateAgent,
default: sharedStateAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-shared-state-read-write",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,47 @@
// Dedicated runtime for the Shared State Streaming demo.
//
// Routes to the Spring `/shared-state-streaming/run` endpoint, which streams
// tool-call arguments for write_document and emits STATE_SNAPSHOT events with
// the growing document text so the frontend sees per-token updates.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/shared-state-streaming/run` });
}
const sharedStateStreamingAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
"shared-state-streaming": sharedStateStreamingAgent,
default: sharedStateStreamingAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-shared-state-streaming",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,48 @@
// Dedicated runtime for the Sub-Agents demo.
//
// Routes to the Spring `/subagents/run` endpoint, which runs a per-request
// supervisor agent that delegates to research / writing / critique
// sub-agents (each its own ChatClient call) and emits STATE_SNAPSHOT events
// after every delegation so the live "delegation log" panel updates.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
function createAgent(): AbstractAgent {
return new HttpAgent({ url: `${AGENT_URL}/subagents/run` });
}
const subagentsAgent = createAgent();
const agents: Record<string, AbstractAgent> = {
subagents: subagentsAgent,
default: subagentsAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-subagents",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,85 @@
// Dedicated runtime for the /demos/voice cell (Spring AI).
//
// Goals
// -----
// 1. Advertise `audioFileTranscriptionEnabled: true` on `/info` so the chat
// composer renders the mic button.
// 2. Handle `POST /transcribe` by invoking an OpenAI-backed
// `TranscriptionServiceOpenAI` (from `@copilotkit/voice`).
// 3. Return a deterministic 4xx when `OPENAI_API_KEY` is not configured.
//
// Voice is an INPUT MODALITY concern handled in the NextJS runtime -- not
// the Spring-AI Java backend. The transcribed text is sent as a plain message
// to the same Spring-AI ChatClient the other demos use, via HttpAgent.
//
// Wires the V2 `CopilotRuntime` directly because the V1 wrapper drops the
// `transcriptionService` option. V2 URL-routes on `/info`, `/agent/:id/run`,
// `/transcribe`, etc., so the route lives at `[[...slug]]/route.ts`.
// @region[voice-runtime]
// @region[transcription-service-guard]
import type { NextRequest } from "next/server";
import {
CopilotRuntime,
TranscriptionService,
createCopilotRuntimeHandler,
} from "@copilotkit/runtime/v2";
import type { TranscribeFileOptions } from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { TranscriptionServiceOpenAI } from "@copilotkit/voice";
import OpenAI from "openai";
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
const voiceDemoAgent = new HttpAgent({ url: `${AGENT_URL}/` });
class GuardedOpenAITranscriptionService extends TranscriptionService {
private delegate: TranscriptionServiceOpenAI | null;
constructor() {
super();
const apiKey = process.env.OPENAI_API_KEY;
this.delegate = apiKey
? new TranscriptionServiceOpenAI({ openai: new OpenAI({ apiKey }) })
: null;
}
async transcribeFile(options: TranscribeFileOptions): Promise<string> {
if (!this.delegate) {
throw new Error(
"OPENAI_API_KEY not configured for this deployment (api key missing). " +
"Set OPENAI_API_KEY to enable voice transcription.",
);
}
return this.delegate.transcribeFile(options);
}
}
// @endregion[transcription-service-guard]
let cachedHandler: ((req: Request) => Promise<Response>) | null = null;
function getHandler(): (req: Request) => Promise<Response> {
if (cachedHandler) return cachedHandler;
const runtime = new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in
// MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in
// source, pending release.
agents: {
"voice-demo": voiceDemoAgent,
default: voiceDemoAgent,
},
transcriptionService: new GuardedOpenAITranscriptionService(),
});
cachedHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit-voice",
});
return cachedHandler;
}
export const POST = (req: NextRequest) => getHandler()(req);
export const GET = (req: NextRequest) => getHandler()(req);
export const PUT = (req: NextRequest) => getHandler()(req);
export const DELETE = (req: NextRequest) => getHandler()(req);
// @endregion[voice-runtime]
@@ -0,0 +1,200 @@
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
// The Spring AI agent backend runs as a separate Java process.
// This runtime proxies CopilotKit requests to it via AG-UI protocol.
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
// Per-request request/response logging is gated behind this flag (default off).
// Under d6 probe fan-out, unconditional per-request logs flooded Railway's
// 500-logs/sec cap and killed the replica ("Messages dropped" → container stop).
// Set SHOWCASE_ROUTE_DEBUG=1 to re-enable verbose per-request tracing locally.
const ROUTE_DEBUG =
process.env.SHOWCASE_ROUTE_DEBUG === "1" ||
process.env.SHOWCASE_ROUTE_DEBUG === "true";
console.log("[copilotkit/route] Initializing CopilotKit runtime");
console.log(`[copilotkit/route] AGENT_URL: ${AGENT_URL}`);
function createAgent(path = "/") {
return new HttpAgent({ url: `${AGENT_URL}${path}` });
}
// Register the same Spring AI agent (backed by a single Spring-AI ChatClient
// on the Java side) under every name used by the demo pages in this package.
// Each entry proxies CopilotKit requests to the same Spring endpoint —
// per-demo behavior differences live on the frontend in the form of
// useFrontendTool / useRenderTool / useHumanInTheLoop / useAgentContext
// hooks registered on each page.
const agentNames = [
"agentic_chat",
"human_in_the_loop",
"tool-rendering",
"tool-rendering-default-catchall",
"tool-rendering-custom-catchall",
"gen-ui-tool-based",
"gen-ui-agent",
"shared-state-read",
"shared-state-write",
"shared-state-read-write",
"shared-state-streaming",
"subagents",
"chat-customization-css",
"frontend_tools",
"frontend-tools-async",
"hitl-in-chat",
"hitl-in-app",
"prebuilt-sidebar",
"prebuilt-popup",
"chat-slots",
"headless-simple",
"headless-complete",
"readonly-state-agent-context",
"auth",
"open-gen-ui",
"beautiful-chat",
// NOTE: `tool-rendering-reasoning-chain` is intentionally NOT registered
// here. It is declared NSF in manifest.yaml (no parallel multi-tool-call
// streaming surface to interleave with reasoning frames on spring-ai), and
// routing it to the default Spring agent at `/` silently bypassed the
// reasoning workaround and rendered a regular tool-rendering run, hiding
// the NSF. With no registration the page errors loudly when probed —
// appropriate for a not-supported feature.
"mcp-apps",
"byoc-hashbrown",
];
// Reasoning agent names — backed by the dedicated Spring-AI reasoning
// controller at /reasoning (ReasoningController). The AG-UI Java SDK at
// Spring AI 1.0.1 has no REASONING_MESSAGE_* subtypes in its typed event
// model (the SDK only knows THINKING_*, which @ag-ui/client silently drops),
// so ReasoningController takes over its own SseEmitter and writes raw-JSON
// REASONING_MESSAGE_START/CONTENT/END frames whose `type` literal matches
// the @ag-ui/core zod schema — the frontend then renders them via the
// `reasoningMessage` slot: CopilotKit's built-in card for
// `reasoning-default`, the custom amber ReasoningBlock for
// `reasoning-custom`. This is the Spring/Java reimplementation of ag2's
// reasoning_agent.py. The demo pages use the ids `reasoning-default` /
// `reasoning-custom`; both share the one reasoning backend.
// Mirrors ag2's route.ts `reasoningAgentNames`. NOTE:
// `tool-rendering-reasoning-chain`, `reasoning-default-render`, and
// `agentic-chat-reasoning` are NOT listed here — they are declared NSF in
// manifest.yaml (no parallel multi-tool-call streaming surface to interleave
// with reasoning frames on spring-ai for tool-rendering-reasoning-chain; no
// matching backend surface for the other two). With no registration the
// pages error loudly when probed — appropriate for not-supported features.
const reasoningAgentNames = ["reasoning-default", "reasoning-custom"];
// Agent names routed to the interrupt-adapted scheduling backend. Both
// gen-ui-interrupt and interrupt-headless share the same Spring AI scheduling
// agent; only the frontend UX differs (inline picker in chat vs. external
// popup driven by useFrontendTool).
const interruptAgentNames = ["gen-ui-interrupt", "interrupt-headless"];
const agents: Record<string, AbstractAgent> = {};
for (const name of agentNames) {
agents[name] = createAgent();
}
for (const name of interruptAgentNames) {
agents[name] = createAgent("/interrupt-adapted");
}
for (const name of reasoningAgentNames) {
agents[name] = createAgent("/reasoning/");
}
// gen-ui-agent has a dedicated Java controller (GenUiAgentController @ /gen-ui-agent/run)
// that drives the set_steps state-card chain; override the shared-root registration.
agents["gen-ui-agent"] = createAgent("/gen-ui-agent/run");
// subagents likewise has a dedicated Java controller (SubagentsController @
// /subagents/run) that emits the research/writing/critique sub-agent
// TOOL_CALL chains the subagent cards render from. The shared-root
// registration above routed it to the default StreamingToolAgent, which has
// no research_agent/writing_agent/critique_agent tools — the run produced a
// final summary but zero subagent cards. Override to the dedicated controller.
agents["subagents"] = createAgent("/subagents/run");
agents["default"] = createAgent();
console.log(
`[copilotkit/route] Registered ${Object.keys(agents).length} agent names: ${Object.keys(agents).join(", ")}`,
);
export const POST = async (req: NextRequest) => {
const url = req.url;
const contentType = req.headers.get("content-type");
if (ROUTE_DEBUG) {
console.log(
`[copilotkit/route] POST ${url} (content-type: ${contentType})`,
);
}
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime: new CopilotRuntime({
// @ts-expect-error -- see main route.ts; published CopilotRuntime's `agents`
// type wraps Record in MaybePromise<NonEmptyRecord<...>> which rejects
// plain Records. Fixed in source, pending release.
agents,
}),
});
const response = await handleRequest(req);
if (!response.ok) {
console.log(`[copilotkit/route] Response status: ${response.status}`);
} else if (ROUTE_DEBUG) {
console.log(`[copilotkit/route] Response status: ${response.status}`);
}
return response;
} catch (error: unknown) {
// Log full details server-side (operators grep `errorId` to correlate),
// but never echo `err.message` / `err.stack` back to the HTTP client —
// that leaks internal paths, dependency versions, and stack traces.
const err = error instanceof Error ? error : new Error(String(error));
const errorId = crypto.randomUUID();
console.error(
JSON.stringify({
at: new Date().toISOString(),
level: "error",
scope: "copilotkit/route",
errorId,
message: err.message,
stack: err.stack,
}),
);
return NextResponse.json(
{ error: "internal runtime error", errorId },
{ status: 500 },
);
}
};
export const GET = async () => {
if (ROUTE_DEBUG) {
console.log("[copilotkit/route] GET /api/copilotkit (health probe)");
}
let agentStatus = "unknown";
try {
const res = await fetch(`${AGENT_URL}/health`, {
signal: AbortSignal.timeout(3000),
});
agentStatus = res.ok ? "reachable" : `error (${res.status})`;
} catch (e: unknown) {
agentStatus = `unreachable (${(e as Error).message})`;
}
return NextResponse.json({
status: "ok",
agent_url: AGENT_URL,
agent_status: agentStatus,
env: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY ? "set" : "NOT SET",
NODE_ENV: process.env.NODE_ENV,
},
});
};
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
// Token-gated: SHOWCASE_DEBUG_TOKEN must be set in env and matched
const token =
req.headers.get("x-debug-token") || req.nextUrl.searchParams.get("token");
const expectedToken = process.env.SHOWCASE_DEBUG_TOKEN;
if (!expectedToken || !token || token !== expectedToken) {
return NextResponse.json({ error: "unauthorized" }, { status: 403 });
}
const AGENT_URL = process.env.AGENT_URL || "unknown";
// Agent connectivity
let agentStatus = "unknown";
let agentDetail = "";
try {
const res = await fetch(`${AGENT_URL}/health`, {
signal: AbortSignal.timeout(3000),
});
agentStatus = res.ok ? "ok" : "error";
agentDetail = `HTTP ${res.status}`;
} catch (e: unknown) {
agentStatus = "down";
agentDetail = (e as Error).message;
}
const uptime = process.uptime();
const mem = process.memoryUsage();
return NextResponse.json({
integration: "spring-ai",
uptime: `${Math.floor(uptime / 60)}m ${Math.floor(uptime % 60)}s`,
agent: { url: AGENT_URL, status: agentStatus, detail: agentDetail },
memory: {
rss: `${Math.round(mem.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(mem.heapUsed / 1024 / 1024)}MB`,
},
env: {
NODE_ENV: process.env.NODE_ENV,
OPENAI_API_KEY: process.env.OPENAI_API_KEY ? "set" : "NOT SET",
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ? "set" : "NOT SET",
LANGSMITH_API_KEY: process.env.LANGSMITH_API_KEY ? "set" : "NOT SET",
},
nodeVersion: process.version,
});
}
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
integration: "spring-ai",
timestamp: new Date().toISOString(),
});
}
@@ -0,0 +1,120 @@
import { NextResponse } from "next/server";
const INTEGRATION_SLUG = "spring-ai";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export async function GET() {
const start = Date.now();
const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL ||
`http://localhost:${process.env.PORT || 3000}`;
try {
const res = await fetch(`${baseUrl}/api/copilotkit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
method: "agent/run",
params: { agentId: "agentic_chat" },
body: {
threadId: `smoke-${Date.now()}`,
runId: `smoke-run-${Date.now()}`,
state: {},
messages: [
{
id: `smoke-msg-${Date.now()}`,
role: "user",
content: "Respond with exactly: OK",
},
],
tools: [],
context: [],
forwardedProps: {},
},
}),
signal: AbortSignal.timeout(45000),
});
const latency = Date.now() - start;
if (!res.ok) {
const errBody = await res.text().catch(() => "");
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage: "runtime_response",
error: `Runtime returned ${res.status}: ${errBody.slice(0, 200)}`,
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
// TTFB: read first chunk only to confirm SSE stream started, then cancel
const reader = res.body?.getReader();
if (!reader) {
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage: "response_empty",
error: "Runtime returned no readable body",
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
const { value, done } = await reader.read();
reader.cancel();
if (done || !value || value.length === 0) {
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage: "response_empty",
error: "Runtime returned empty response body",
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
return NextResponse.json({
status: "ok",
integration: INTEGRATION_SLUG,
latency_ms: latency,
timestamp: new Date().toISOString(),
});
} catch (e: unknown) {
const err = e instanceof Error ? e : new Error(String(e));
const latency = Date.now() - start;
let stage = "unknown";
if (err.name === "AbortError" || err.message.includes("timeout"))
stage = "timeout";
else if (
err.message.includes("fetch") ||
err.message.includes("ECONNREFUSED")
)
stage = "agent_unreachable";
else stage = "pipeline_error";
return NextResponse.json(
{
status: "error",
integration: INTEGRATION_SLUG,
stage,
error: err.message,
latency_ms: latency,
timestamp: new Date().toISOString(),
},
{ status: 502 },
);
}
}
@@ -0,0 +1,55 @@
// Shared fallback time-slot generator for the interrupt demos
// (`gen-ui-interrupt`, `interrupt-headless`). The interrupt backend
// (`src/agents/interrupt_agent.py`) supplies its own candidate slots
// inside the interrupt payload — these fallbacks only run if the
// payload arrives without them. Generating relative to `Date.now()`
// keeps the fallback from rotting, which previously had hardcoded
// dates that decayed within a week of being authored.
export interface TimeSlot {
label: string;
iso: string;
}
function atLocal(date: Date, hour: number, minute = 0): Date {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
hour,
minute,
0,
0,
);
}
function nextMonday(from: Date): Date {
// `getDay()` is 0=Sun, 1=Mon, ..., 6=Sat. We want the next Monday
// that's at LEAST 2 days away — otherwise "Monday" would collide
// with "Tomorrow" on Sunday (offset would be 1) or with itself on
// Monday (offset would be 0). Mirrors interrupt_agent.py.
const day = from.getDay();
let offset = (1 - day + 7) % 7;
if (offset <= 1) offset += 7;
const next = new Date(from);
next.setDate(from.getDate() + offset);
return next;
}
export function generateFallbackSlots(now: Date = new Date()): TimeSlot[] {
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
const monday = nextMonday(now);
const candidates: Array<[string, Date]> = [
["Tomorrow 10:00 AM", atLocal(tomorrow, 10)],
["Tomorrow 2:00 PM", atLocal(tomorrow, 14)],
["Monday 9:00 AM", atLocal(monday, 9)],
["Monday 3:30 PM", atLocal(monday, 15, 30)],
];
return candidates.map(([label, date]) => ({
label,
iso: date.toISOString(),
}));
}
@@ -0,0 +1,12 @@
// Coerces a tool-call result into a typed object. Tool results arrive
// as strings when the agent emits JSON or as already-parsed objects
// when the runtime decoded them upstream — this helper handles both
// shapes and returns `{}` if the result is missing or unparseable.
export function parseJsonResult<T>(result: unknown): T {
if (!result) return {} as T;
try {
return (typeof result === "string" ? JSON.parse(result) : result) as T;
} catch {
return {} as T;
}
}
@@ -0,0 +1,21 @@
// Helper for the CopilotChat slot overrides. The slot prop types in
// `@copilotkit/react-core` are nominally typed against the *exact*
// default component identity, but a custom wrapper that returns a
// structurally compatible ReactElement is functionally a drop-in. This
// helper names that fact and centralizes the type assertion in one
// place — readers see `makeSlotOverride` and know it's about the slot
// contract, not arbitrary type-system gymnastics. Once the slot prop
// types accept structural compatibility, this helper can be deleted
// and the casts will resolve automatically.
import type { ComponentType } from "react";
// `any` on the input is intentional: the helper's purpose is to accept
// any component shape and assert it as the slot's expected type. A
// stricter constraint would defeat the whole point.
export function makeSlotOverride<TDefault>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: ComponentType<any>,
): TDefault {
return component as unknown as TDefault;
}
@@ -0,0 +1,31 @@
import * as React from "react";
/**
* ShadCN-style Badge primitive (inline-cloned for this demo).
* Plain Tailwind classes, no `cn()`/`cva` helpers.
*/
type Variant = "default" | "secondary" | "outline" | "success";
const variantClasses: Record<Variant, string> = {
default: "border-transparent bg-neutral-900 text-neutral-50",
secondary: "border-transparent bg-neutral-100 text-neutral-900",
outline: "border-neutral-200 text-neutral-700 bg-white",
success: "border-transparent bg-emerald-100 text-emerald-700",
};
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: Variant;
}
export function Badge({
className = "",
variant = "default",
...props
}: BadgeProps) {
return (
<div
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium tracking-wide ${variantClasses[variant]} ${className}`}
{...props}
/>
);
}
@@ -0,0 +1,48 @@
import * as React from "react";
/**
* ShadCN-style Button primitive (inline-cloned for this demo).
* Plain Tailwind classes, no `cn()`/`cva` helpers.
*/
type Variant = "default" | "outline" | "secondary" | "ghost" | "success";
type Size = "default" | "sm" | "lg";
const baseClasses =
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60";
const variantClasses: Record<Variant, string> = {
default: "bg-neutral-900 text-neutral-50 shadow-sm hover:bg-neutral-800",
outline:
"border border-neutral-200 bg-white text-neutral-900 shadow-sm hover:bg-neutral-100",
secondary: "bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-200",
ghost: "hover:bg-neutral-100 hover:text-neutral-900",
success:
"bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm hover:bg-emerald-50",
};
const sizeClasses: Record<Size, string> = {
default: "h-10 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-11 rounded-md px-6",
};
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className = "", variant = "default", size = "default", ...props },
ref,
) => {
return (
<button
ref={ref}
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
/>
);
},
);
Button.displayName = "Button";
@@ -0,0 +1,61 @@
import * as React from "react";
/**
* ShadCN-style Card primitive (inline-cloned for this demo).
* Plain Tailwind classes, no `cn()`/`cva` helpers.
*/
export const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div
ref={ref}
className={`rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow-sm ${className}`}
{...props}
/>
));
Card.displayName = "Card";
export const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div
ref={ref}
className={`flex flex-col space-y-1.5 p-5 pb-3 ${className}`}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
export const CardTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className = "", ...props }, ref) => (
<h3
ref={ref}
className={`text-base font-semibold leading-none tracking-tight text-neutral-900 ${className}`}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
export const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div ref={ref} className={`p-5 pt-0 ${className}`} {...props} />
));
CardContent.displayName = "CardContent";
export const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className = "", ...props }, ref) => (
<div
ref={ref}
className={`flex items-center p-5 pt-0 ${className}`}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
@@ -0,0 +1,26 @@
import * as React from "react";
/**
* ShadCN-style Separator primitive (inline-cloned for this demo).
* Plain Tailwind classes; uses a div instead of Radix to keep dependencies minimal.
*/
export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: "horizontal" | "vertical";
}
export function Separator({
className = "",
orientation = "horizontal",
...props
}: SeparatorProps) {
const orientationClasses =
orientation === "horizontal" ? "h-px w-full" : "h-full w-px";
return (
<div
role="separator"
aria-orientation={orientation}
className={`shrink-0 bg-neutral-200 ${orientationClasses} ${className}`}
{...props}
/>
);
}
@@ -0,0 +1,23 @@
"use client";
/**
* Fixed A2UI catalog — wires definitions to renderers.
*
* `includeBasicCatalog: true` merges CopilotKit's built-in components
* (Card, Column, Row, Text, Button, Divider, …) into this catalog, so
* the agent's fixed schema (src/agents/a2ui_schemas/flight_schema.json) can
* compose custom and basic components interchangeably.
*/
// @region[catalog-creation]
import { createCatalog } from "@copilotkit/a2ui-renderer";
import { definitions } from "./definitions";
import { renderers } from "./renderers";
export const CATALOG_ID = "copilotkit://flight-fixed-catalog";
export const catalog = createCatalog(definitions, renderers, {
catalogId: CATALOG_ID,
includeBasicCatalog: true,
});
// @endregion[catalog-creation]
@@ -0,0 +1,107 @@
/**
* A2UI catalog DEFINITIONS — platform-agnostic.
*
* Each entry declares a component name + its Zod props schema. The basic
* catalog (Card, Column, Row, Text, Button, …) ships with CopilotKit and
* is mixed in via `createCatalog(..., { includeBasicCatalog: true })`, so
* we only declare the project-specific additions and the visual overrides
* here. (Custom entries with the same name as a basic component override
* the basic one — Catalog dedupes by `comp.name`, last-write-wins.)
*
* IMPORTANT — path bindings: fields that can be bound to a data-model path
* (e.g. `{ path: "/origin" }` in the fixed schema JSON) must declare their
* Zod type as a union of `z.string()` and `z.object({ path: z.string() })`.
* The A2UI `GenericBinder` uses this union to detect the field as dynamic
* and resolve the path against the current data model at render time. Using
* plain `z.string()` causes the raw `{ path }` object to reach the
* renderer, which React then throws on (error #31 "object with keys {path}").
* This matches the canonical catalog's `DynString` helper:
* examples/integrations/langgraph-python/src/app/declarative-generative-ui/definitions.ts
*/
// @region[definitions-types]
import { z } from "zod";
import type { CatalogDefinitions } from "@copilotkit/a2ui-renderer";
/**
* Dynamic string: literal OR a data-model path binding. The GenericBinder
* resolves path bindings to the actual value at render time.
*/
const DynString = z.union([z.string(), z.object({ path: z.string() })]);
export const definitions = {
/**
* Card override: gives the outer flight-card container a ShadCN look
* (rounded-xl, neutral-200 border, soft shadow). The basic catalog's
* Card uses inline styles; overriding here lets the demo's renderer
* adopt the demo's Tailwind aesthetic without touching the schema JSON.
*/
Card: {
description: "A container card with a single child.",
props: z.object({
child: z.string(),
}),
},
Title: {
description: "A prominent heading for the flight card.",
props: z.object({
text: DynString,
}),
},
Airport: {
description: "A 3-letter airport code, displayed large.",
props: z.object({
code: DynString,
}),
},
Arrow: {
description: "A right-pointing arrow used between airports.",
props: z.object({}),
},
AirlineBadge: {
description: "A pill-styled airline name tag.",
props: z.object({
name: DynString,
}),
},
PriceTag: {
description: "A stylized price display (e.g. '$289').",
props: z.object({
amount: DynString,
}),
},
/**
* Button override: swaps in an ActionButton renderer that tracks
* its own `done` state so clicking "Book flight" visually updates to
* a "Booked ✓" confirmation. The basic catalog's Button is stateless,
* so without this override the click fires the action but the button
* looks unchanged. Mirrors the pattern in beautiful-chat
* (src/app/demos/beautiful-chat/declarative-generative-ui/renderers.tsx).
*/
Button: {
description:
"An interactive button with an action event. Use 'child' with a Text component ID for the label. After click, the button shows a confirmation state.",
props: z.object({
child: z
.string()
.describe(
"The ID of the child component (e.g. a Text component for the label).",
),
variant: z.enum(["primary", "secondary", "ghost"]).optional(),
// Union with { event } so GenericBinder resolves this as ACTION → callable () => void.
action: z
.union([
z.object({
event: z.object({
name: z.string(),
context: z.record(z.any()).optional(),
}),
}),
z.null(),
])
.optional(),
}),
},
} satisfies CatalogDefinitions;
// @endregion[definitions-types]
export type Definitions = typeof definitions;
@@ -0,0 +1,110 @@
"use client";
/**
* A2UI catalog RENDERERS — React implementations for the custom components
* declared in `./definitions`. TypeScript enforces that the renderer map's
* keys and prop shapes match the definitions exactly.
*
* Visual style: ShadCN aesthetic (neutral palette, rounded-xl, subtle
* borders, clean typography). Tailwind utility classes only — no `cn()` /
* `cva` helpers, no shadcn CLI install. Inline-cloned primitives live in
* `../_components/`.
*/
import React from "react";
import type { CatalogRenderers } from "@copilotkit/a2ui-renderer";
import type { Definitions } from "./definitions";
import { Card } from "../_components/card";
import { Badge } from "../_components/badge";
import { Button as UIButton } from "../_components/button";
import { Separator } from "../_components/separator";
// `DynString` props are typed as `string | { path }` (see definitions.ts), but
// the A2UI binder resolves path bindings before render — renderers only ever
// see resolved strings. One shared helper keeps that narrowing in one place.
const s = (v: unknown): string => (typeof v === "string" ? v : "");
// @region[renderers-tsx]
export const renderers: CatalogRenderers<Definitions> = {
/**
* Card override: ShadCN-style outer container. The basic catalog's Card
* uses inline styles; overriding here keeps the demo's tailwind aesthetic.
* The flight schema renders Card > Column > [Title, Row, …]; the inner
* Column adds the vertical spacing.
*/
Card: ({ props, children }) => (
<Card className="w-full max-w-md p-5" data-testid="a2ui-fixed-card">
{props.child ? children(props.child) : null}
</Card>
),
Title: ({ props }) => (
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-[11px] font-medium uppercase tracking-[0.14em] text-neutral-500">
Itinerary
</p>
<h3 className="text-base font-semibold leading-none tracking-tight text-neutral-900">
{s(props.text)}
</h3>
</div>
<Badge variant="outline" className="font-mono">
1-stop · economy
</Badge>
</div>
),
Airport: ({ props }) => (
<div className="flex flex-col items-center">
<span className="font-mono text-2xl font-semibold tracking-wider text-neutral-900">
{s(props.code)}
</span>
</div>
),
Arrow: () => (
<div className="flex flex-1 items-center px-3">
<Separator className="flex-1 bg-neutral-200" />
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mx-1 text-neutral-400"
aria-hidden
>
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="12 5 19 12 12 19" />
</svg>
<Separator className="flex-1 bg-neutral-200" />
</div>
),
AirlineBadge: ({ props }) => (
<Badge variant="secondary" className="uppercase tracking-[0.08em]">
{s(props.name)}
</Badge>
),
PriceTag: ({ props }) => (
<div className="flex items-baseline gap-1">
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-neutral-500">
Total
</span>
<span className="font-mono text-base font-semibold text-neutral-900">
{s(props.amount)}
</span>
</div>
),
/**
* Button override: this is a pure-presentation demo, so the button just
* renders its label. The schema declares an `action` for visual fidelity,
* but the click handler is inert until the Python SDK exposes
* `action_handlers=` on `a2ui.render` (see `src/agents/a2ui_fixed.py`).
*/
Button: ({ props, children }) => (
<UIButton className="w-full">
{props.child ? children(props.child) : null}
</UIButton>
),
};
// @endregion[renderers-tsx]
@@ -0,0 +1,11 @@
"use client";
import { CopilotChat } from "@copilotkit/react-core/v2";
import { useA2UIFixedSchemaSuggestions } from "./suggestions";
export function Chat() {
useA2UIFixedSchemaSuggestions();
return (
<CopilotChat agentId="a2ui-fixed-schema" className="h-full rounded-2xl" />
);
}
@@ -0,0 +1,41 @@
"use client";
/**
* Declarative Generative UI — A2UI Fixed Schema demo.
*
* In the fixed-schema flavor of A2UI, the component tree (schema) lives on
* the frontend and the agent only streams *data* into the data model. The
* flight card is ASSEMBLED from small sub-components in
* `src/agents/a2ui_schemas/flight_schema.json` (Card > Column > [Title, Row, …]).
*
* - Definitions (zod schemas): `./a2ui/definitions.ts`
* - Renderers (React): `./a2ui/renderers.tsx`
* - Catalog wiring: `./a2ui/catalog.ts` (includes the basic catalog)
* - Agent: `src/agents/a2ui_fixed.py` (emits an `a2ui_operations` container)
*
* Reference:
* https://docs.copilotkit.ai/integrations/langgraph/generative-ui/a2ui/fixed-schema
*/
import React from "react";
import { CopilotKit } from "@copilotkit/react-core/v2";
import { catalog } from "./a2ui/catalog";
import { Chat } from "./chat";
export default function A2UIFixedSchemaDemo() {
return (
// `a2ui.catalog` wires the fixed catalog into the A2UI activity renderer.
<CopilotKit
runtimeUrl="/api/copilotkit-a2ui-fixed-schema"
agent="a2ui-fixed-schema"
a2ui={{ catalog: catalog }}
>
<div className="flex justify-center items-center h-screen w-full bg-neutral-50">
<div className="h-full w-full max-w-4xl border-x border-neutral-200 bg-white">
<Chat />
</div>
</div>
</CopilotKit>
);
}
@@ -0,0 +1,13 @@
import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
export function useA2UIFixedSchemaSuggestions() {
useConfigureSuggestions({
suggestions: [
{
title: "Find SFO → JFK",
message: "Find me a flight from SFO to JFK on United for $289.",
},
],
available: "always",
});
}
@@ -0,0 +1,91 @@
"use client";
import type { ChangeEvent } from "react";
import {
type AgentConfig,
EXPERTISE_OPTIONS,
type Expertise,
RESPONSE_LENGTH_OPTIONS,
type ResponseLength,
TONE_OPTIONS,
type Tone,
} from "./config-types";
interface ConfigCardProps {
config: AgentConfig;
onToneChange: (tone: Tone) => void;
onExpertiseChange: (expertise: Expertise) => void;
onResponseLengthChange: (length: ResponseLength) => void;
}
export function ConfigCard({
config,
onToneChange,
onExpertiseChange,
onResponseLengthChange,
}: ConfigCardProps) {
return (
<div
data-testid="agent-config-card"
className="flex flex-col gap-2 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-4 text-sm"
>
<h2 className="text-sm font-semibold">Agent Config</h2>
<p className="text-xs text-[var(--text-muted)]">
Change these and send a message to see the agent adapt.
</p>
<div className="flex flex-wrap gap-3">
<label className="flex flex-col gap-1">
<span className="text-xs font-medium">Tone</span>
<select
data-testid="agent-config-tone-select"
value={config.tone}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
onToneChange(e.target.value as Tone)
}
className="rounded border border-[var(--border)] bg-[var(--bg-muted)] px-2 py-1 text-sm"
>
{TONE_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs font-medium">Expertise</span>
<select
data-testid="agent-config-expertise-select"
value={config.expertise}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
onExpertiseChange(e.target.value as Expertise)
}
className="rounded border border-[var(--border)] bg-[var(--bg-muted)] px-2 py-1 text-sm"
>
{EXPERTISE_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs font-medium">Response length</span>
<select
data-testid="agent-config-length-select"
value={config.responseLength}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
onResponseLengthChange(e.target.value as ResponseLength)
}
className="rounded border border-[var(--border)] bg-[var(--bg-muted)] px-2 py-1 text-sm"
>
{RESPONSE_LENGTH_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
</div>
</div>
);
}
@@ -0,0 +1,25 @@
"use client";
/**
* Publishes the current agent-config toggles to the agent runtime via
* `useAgentContext`. Lives inside the `<CopilotKit>` provider so the
* context store is reachable. The middleware on the Python side reads
* this entry off the agent's runtime context on every turn and routes
* it into the model's prompt.
*/
import { useAgentContext } from "@copilotkit/react-core/v2";
import type { AgentConfig } from "./config-types";
export function ConfigContextRelay({ config }: { config: AgentConfig }) {
useAgentContext({
description:
"Agent response preferences. Apply tone, expertise level, and response length to every reply.",
value: {
tone: config.tone,
expertise: config.expertise,
responseLength: config.responseLength,
},
});
return null;
}
@@ -0,0 +1,26 @@
export type Tone = "professional" | "casual" | "enthusiastic";
export type Expertise = "beginner" | "intermediate" | "expert";
export type ResponseLength = "concise" | "detailed";
export interface AgentConfig {
tone: Tone;
expertise: Expertise;
responseLength: ResponseLength;
}
export const DEFAULT_AGENT_CONFIG: AgentConfig = {
tone: "professional",
expertise: "intermediate",
responseLength: "concise",
};
export const TONE_OPTIONS: Tone[] = ["professional", "casual", "enthusiastic"];
export const EXPERTISE_OPTIONS: Expertise[] = [
"beginner",
"intermediate",
"expert",
];
export const RESPONSE_LENGTH_OPTIONS: ResponseLength[] = [
"concise",
"detailed",
];
@@ -0,0 +1,38 @@
"use client";
import React from "react";
import { CopilotChat } from "@copilotkit/react-core/v2";
import { ConfigCard } from "./config-card";
import type { AgentConfig } from "./config-types";
interface DemoLayoutProps {
config: AgentConfig;
onToneChange: (tone: AgentConfig["tone"]) => void;
onExpertiseChange: (expertise: AgentConfig["expertise"]) => void;
onResponseLengthChange: (length: AgentConfig["responseLength"]) => void;
}
export function DemoLayout({
config,
onToneChange,
onExpertiseChange,
onResponseLengthChange,
}: DemoLayoutProps) {
return (
<div className="flex h-screen flex-col gap-3 p-6">
<ConfigCard
config={config}
onToneChange={onToneChange}
onExpertiseChange={onExpertiseChange}
onResponseLengthChange={onResponseLengthChange}
/>
<div className="flex-1 overflow-hidden rounded-md border border-[var(--border)]">
<CopilotChat
agentId="agent-config-demo"
className="h-full rounded-md"
/>
</div>
</div>
);
}
@@ -0,0 +1,44 @@
"use client";
/**
* Agent Config Object — typed config knobs (tone / expertise / responseLength)
* forwarded from the provider into the agent so its behavior changes per turn.
*
* Wiring: the toggles live in `useAgentConfig`. Each render the resolved
* config is published to the agent via `useAgentContext` — the v2 idiom
* for "frontend → agent runtime context" in LangGraph 0.6+. The Python
* graph picks it up through `CopilotKitMiddleware`, which routes the
* context entry into the model's prompt before each call.
*
* (LangGraph 0.6 deprecated `configurable` in favor of `context`; the
* `properties` prop on `<CopilotKit>` still works for v1-style relays
* but goes through `forwardedProps` and does not land in `RunnableConfig`
* in @ag-ui/langgraph 0.0.31. `useAgentContext` is the supported path.)
*/
import { CopilotKit } from "@copilotkit/react-core/v2";
import { DemoLayout } from "./demo-layout";
import { ConfigContextRelay } from "./config-context-relay";
import { useAgentConfig } from "./use-agent-config";
export default function AgentConfigDemoPage() {
const { config, setTone, setExpertise, setResponseLength } = useAgentConfig();
return (
// @region[provider-setup]
<CopilotKit
runtimeUrl="/api/copilotkit-agent-config"
agent="agent-config-demo"
>
<ConfigContextRelay config={config} />
<DemoLayout
config={config}
onToneChange={setTone}
onExpertiseChange={setExpertise}
onResponseLengthChange={setResponseLength}
/>
</CopilotKit>
// @endregion[provider-setup]
);
}
@@ -0,0 +1,39 @@
"use client";
import { useCallback, useState } from "react";
import {
type AgentConfig,
DEFAULT_AGENT_CONFIG,
type Expertise,
type ResponseLength,
type Tone,
} from "./config-types";
export interface UseAgentConfigHandle {
config: AgentConfig;
setTone: (tone: Tone) => void;
setExpertise: (expertise: Expertise) => void;
setResponseLength: (length: ResponseLength) => void;
reset: () => void;
}
export function useAgentConfig(): UseAgentConfigHandle {
const [config, setConfig] = useState<AgentConfig>(DEFAULT_AGENT_CONFIG);
const setTone = useCallback(
(tone: Tone) => setConfig((prev) => ({ ...prev, tone })),
[],
);
const setExpertise = useCallback(
(expertise: Expertise) => setConfig((prev) => ({ ...prev, expertise })),
[],
);
const setResponseLength = useCallback(
(responseLength: ResponseLength) =>
setConfig((prev) => ({ ...prev, responseLength })),
[],
);
const reset = useCallback(() => setConfig(DEFAULT_AGENT_CONFIG), []);
return { config, setTone, setExpertise, setResponseLength, reset };
}
@@ -0,0 +1,28 @@
# Agentic Chat
## What This Demo Shows
The simplest CopilotKit surface: a plain agentic chat backed by a LangGraph (Python) agent.
- **Natural Conversation**: Chat with your Copilot in a familiar chat interface
- **Streaming Responses**: Assistant messages stream in token-by-token via AG-UI
- **Suggestion Chips**: A starter suggestion is rendered as a quick-action chip
## How to Interact
Click the suggestion chip, or type your own prompt. For example:
- "Write a short sonnet about AI"
- "Explain the difference between an LLM and an agent"
- "Give me three ideas for a weekend project"
## Technical Details
**Provider**`CopilotKit` wires the page to the runtime:
- `runtimeUrl="/api/copilotkit"` points at the Next.js route that proxies to the agent
- `agent="agentic_chat"` selects the LangGraph agent defined in `langgraph.json`
**Chat surface**`CopilotChat` renders the full chat UI with input, message list, and streaming.
**Suggestions**`useConfigureSuggestions` registers a static suggestion that appears as a clickable chip below the chat input.
@@ -0,0 +1,24 @@
"use client";
import React from "react";
import { CopilotKit, CopilotChat } from "@copilotkit/react-core/v2";
import { useAgenticChatSuggestions } from "./suggestions";
export default function AgenticChatDemo() {
return (
// @region[provider-setup]
<CopilotKit runtimeUrl="/api/copilotkit" agent="agentic_chat">
<Chat />
</CopilotKit>
// @endregion[provider-setup]
);
}
// @region[chat-component]
function Chat() {
useAgenticChatSuggestions();
// @region[render-chat]
return <CopilotChat agentId="agentic_chat" />;
// @endregion[render-chat]
}
// @endregion[chat-component]
@@ -0,0 +1,22 @@
"use client";
import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
// @region[configure-suggestions]
export function useAgenticChatSuggestions() {
useConfigureSuggestions({
suggestions: [
{ title: "Write a sonnet", message: "Write a short sonnet about AI." },
{
title: "Tell me a joke",
message: "Tell me a one-line joke.",
},
{
title: "Is 17 prime?",
message: "Walk me through whether 17 is prime.",
},
],
available: "always",
});
}
// @endregion[configure-suggestions]
@@ -0,0 +1,63 @@
"use client";
import { Button } from "@/components/ui/button";
interface AuthBannerProps {
authenticated: boolean;
onSignOut: () => void;
onSignIn: () => void;
}
/**
* Status strip rendered above <CopilotChat /> in both authenticated and
* post-sign-out states. The post-sign-out (amber) variant exists so the demo
* actually showcases what its name promises — the runtime rejecting an
* unauthenticated request — instead of bouncing the user back to the gate
* page where the rejection never happens.
*
* Pure presentational — owns no state itself. Testids are stable contract
* for QA + Playwright specs.
*/
export function AuthBanner({
authenticated,
onSignOut,
onSignIn,
}: AuthBannerProps) {
const classes = authenticated
? "border-emerald-300 bg-emerald-50 text-emerald-900"
: "border-amber-300 bg-amber-50 text-amber-900";
return (
<div
data-testid="auth-banner"
data-authenticated={authenticated ? "true" : "false"}
className={`flex items-center justify-between gap-3 rounded-md border px-4 py-2 text-sm ${classes}`}
>
<span data-testid="auth-status" className="font-medium">
{authenticated
? "✓ Signed in as demo user"
: "⚠ Signed out — the agent will reject your messages until you sign in."}
</span>
{authenticated ? (
<Button
type="button"
data-testid="auth-sign-out-button"
size="sm"
variant="outline"
onClick={onSignOut}
>
Sign out
</Button>
) : (
<Button
type="button"
data-testid="auth-authenticate-button"
size="sm"
onClick={onSignIn}
>
Sign in
</Button>
)}
</div>
);
}
@@ -0,0 +1,11 @@
/**
* Shared demo-token constant imported by both the client
* (use-demo-auth.ts) and the server runtime route
* (api/copilotkit-auth/route.ts). Keeping the constant in one file
* prevents drift: changing the token in one place changes it everywhere.
*
* This is a DEMO token. Never use a hard-coded shared secret for real auth.
*/
export const DEMO_TOKEN = "demo-token-123";
export const DEMO_AUTH_HEADER = `Bearer ${DEMO_TOKEN}`;
@@ -0,0 +1,144 @@
"use client";
// Auth demo — framework-native request authentication via the V2 runtime's
// `onRequest` hook. The runtime route (/api/copilotkit-auth) rejects any
// request whose `Authorization: Bearer <demo-token>` header is missing or
// wrong.
//
// UX shape: the demo defaults to UNAUTHENTICATED on first paint so visitors
// land on a clear sign-in card. We don't render `<CopilotKit>` until the user
// has signed in at least once — that sidesteps the transport 401 that would
// otherwise crash `<CopilotChat>` during its initial `/info` handshake.
// After the user signs in once, `<CopilotKit>` stays mounted across the
// sign-out → sign-in cycle so the post-sign-out state can actually
// demonstrate the runtime rejecting unauthenticated requests in the chat
// surface (the whole point of the demo).
//
// Error surfacing: the post-sign-out 401 is captured via the AGENT-SCOPED
// `<CopilotChat onError>` channel, NOT the provider-level `<CopilotKit
// onError>` alone. Agent-run errors (`agent_run_failed`) are reliably
// delivered to the chat-scoped subscription, whereas the provider-level
// handler does not fire for them in this flow — so a demo that relies only
// on `<CopilotKit onError>` never renders the rejection banner. We register
// the same handler on BOTH channels: `<CopilotKit onError>` covers any
// provider-level errors (e.g. the initial `/info` handshake) and
// `<CopilotChat onError>` covers agent-run rejections, which is what the
// sign-out path produces.
import { useCallback, useEffect, useMemo, useState } from "react";
import { CopilotKit, CopilotChat } from "@copilotkit/react-core/v2";
import type { CopilotKitCoreErrorCode } from "@copilotkit/react-core/v2";
import { AuthBanner } from "./auth-banner";
import { SignInCard } from "./sign-in-card";
import { useDemoAuth } from "./use-demo-auth";
import { DEMO_TOKEN } from "./demo-token";
interface AuthDemoErrorState {
message: string;
code: CopilotKitCoreErrorCode | string;
}
interface AuthErrorEvent {
error?: { message?: string } | null;
code: CopilotKitCoreErrorCode;
}
export default function AuthDemoPage() {
const {
isAuthenticated,
authorizationHeader,
hasEverSignedIn,
signIn,
signOut,
} = useDemoAuth();
const headers = useMemo<Record<string, string>>(
() => (authorizationHeader ? { Authorization: authorizationHeader } : {}),
[authorizationHeader],
);
const [authError, setAuthError] = useState<AuthDemoErrorState | null>(null);
// Shared error handler wired to BOTH the provider-level and chat-level
// `onError` channels (see the file header for why both are needed).
const handleAuthError = useCallback((event: AuthErrorEvent) => {
setAuthError({
message:
(event.error?.message && event.error.message.trim()) ||
(event.code
? `Request rejected (${event.code})`
: "The request was rejected."),
code: event.code,
});
}, []);
// Clear stale errors as soon as the user re-authenticates. This is the
// ONLY thing that gates the amber error surface on auth state — the render
// condition below keys off `authError` alone. Coupling the render to a
// second `!isAuthenticated` slice (the obvious-but-wrong guard) created a
// post-sign-out race: the rejection's `onError` fires and calls
// `setAuthError`, but if that commit landed in a render where the auth
// state hadn't yet settled to false, `authError && !isAuthenticated`
// evaluated false and the banner never appeared. Driving the surface off
// `authError` and clearing it here on re-auth removes the cross-slice
// ordering dependency: a rejection always renders, and signing back in
// always wipes it.
useEffect(() => {
if (isAuthenticated) setAuthError(null);
}, [isAuthenticated]);
if (!hasEverSignedIn) {
return (
<div className="flex h-screen flex-col">
<SignInCard onSignIn={signIn} />
</div>
);
}
return (
// `useSingleEndpoint={false}` opts into the V2 multi-endpoint protocol
// (separate /info, /agents/<id>/run, etc.), which is what this demo's
// runtime route is wired up for.
<CopilotKit
runtimeUrl="/api/copilotkit-auth"
agent="auth-demo"
headers={headers}
useSingleEndpoint={false}
onError={handleAuthError}
>
<div className="flex h-screen flex-col gap-3 p-6">
<AuthBanner
authenticated={isAuthenticated}
onSignOut={signOut}
onSignIn={() => signIn(DEMO_TOKEN)}
/>
<header>
<h1 className="text-lg font-semibold">Authentication</h1>
</header>
{authError && (
<div
data-testid="auth-demo-error"
className="rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900"
>
<strong className="font-semibold">
Runtime rejected the request:
</strong>{" "}
<span data-testid="auth-demo-error-message">
{authError.message}
</span>{" "}
<code className="ml-1 rounded bg-amber-100 px-1 py-0.5 font-mono text-xs">
{authError.code}
</code>
</div>
)}
<div className="flex-1 overflow-hidden rounded-md border border-neutral-200">
<CopilotChat
agentId="auth-demo"
className="h-full"
onError={handleAuthError}
/>
</div>
</div>
</CopilotKit>
);
}

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