chore: import upstream snapshot with attribution
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
// Dedicated runtime for the A2UI — Fixed Schema cell. Splitting into its own
|
||||
// endpoint (mirroring beautiful-chat) lets us set
|
||||
// `a2ui.injectA2UITool: false` — the backend agent owns the `display_flight`
|
||||
// tool which emits its own `a2ui_operations` container via `a2ui.render(...)`.
|
||||
//
|
||||
// Reference:
|
||||
// - src/app/api/copilotkit/route.ts (LF main runtime)
|
||||
// - src/agents/src/a2ui_fixed.py (the backend graph)
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
const a2uiFixedSchemaAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId: "a2ui_fixed",
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents: { "a2ui-fixed-schema": a2uiFixedSchemaAgent },
|
||||
a2ui: {
|
||||
// The backend emits its own `a2ui_operations` container via
|
||||
// `a2ui.render(...)` inside `display_flight` (see src/agents/src/a2ui_fixed.py).
|
||||
// We still run the A2UI middleware so it detects the container in tool
|
||||
// results and forwards surfaces to the frontend — but we do NOT inject a
|
||||
// runtime `render_a2ui` tool on top of the agent's existing tools.
|
||||
injectA2UITool: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-a2ui-fixed-schema",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
const e = error as { message?: string; stack?: string };
|
||||
return NextResponse.json(
|
||||
{ error: e.message, stack: e.stack },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
// Dedicated runtime for the A2UI Error Recovery demo.
|
||||
// `a2ui.injectA2UITool: false` — the backend LangGraph agent OWNS
|
||||
// `generate_a2ui` via `ag_ui_langgraph.get_a2ui_tools` (see
|
||||
// src/agents/src/recovery_agent.py), whose body runs the `render_a2ui`
|
||||
// sub-agent + the toolkit validate->retry recovery loop + the
|
||||
// recovery-exhausted hard-fail envelope IN-GRAPH (OSS-158 / OSS-375). The
|
||||
// runtime must NOT inject a second copy (double-bind); this `false` is
|
||||
// load-bearing post CopilotKit#5611 (the provider catalog otherwise defaults
|
||||
// injectA2UITool to true). The middleware still renders the building ->
|
||||
// retrying (N/M) -> painted / failed lifecycle.
|
||||
//
|
||||
// The demo reuses the declarative-gen-ui catalog. The aimock fixtures force the
|
||||
// inner render_a2ui sub-agent to emit free-form/sloppy args the middleware heals
|
||||
// (heal pill) or a structurally-invalid surface on every attempt (exhaust pill).
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
const recoveryAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId: "a2ui_recovery",
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents: { "a2ui-recovery": recoveryAgent },
|
||||
a2ui: {
|
||||
injectA2UITool: false,
|
||||
// Reuse the catalog the page registers (shared with declarative-gen-ui).
|
||||
defaultCatalogId: "declarative-gen-ui-catalog",
|
||||
},
|
||||
});
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-a2ui-recovery",
|
||||
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,92 @@
|
||||
// Dedicated runtime for the Agent Config Object demo.
|
||||
//
|
||||
// This runtime hosts a single LangGraph agent (`agent_config_agent`).
|
||||
// The Python graph reads three properties — tone / expertise / responseLength
|
||||
// — from `RunnableConfig["configurable"]["properties"]` to build its system
|
||||
// prompt dynamically per turn (see `src/agents/agent_config_agent.py`).
|
||||
//
|
||||
// ── Property-forwarding regression note ────────────────────────────
|
||||
// Previously this route used a custom `AgentConfigLangGraphAgent` subclass
|
||||
// that repacked the CopilotKit provider's `properties` into
|
||||
// `forwardedProps.config.configurable.properties` so the Python graph could
|
||||
// read them. That stopped working with `@ag-ui/langgraph@0.0.31`, which
|
||||
// builds the LangGraph SDK request as
|
||||
// `{ ..., config, context: { ...input.context, ...config.configurable } }`
|
||||
// — i.e. it merges `configurable` INTO `context`. LangGraph 0.6.0+ rejects
|
||||
// any request that sets both `configurable` and `context`:
|
||||
//
|
||||
// HTTP 400: "Cannot specify both configurable and context. Prefer setting
|
||||
// context alone. Context was introduced in LangGraph 0.6.0 and is the long
|
||||
// term planned replacement for configurable."
|
||||
//
|
||||
// Net effect: any forwardedProps that landed in `configurable.<key>` made
|
||||
// the chat round-trip 400 unconditionally — the user message rendered, but
|
||||
// no assistant reply ever came back.
|
||||
//
|
||||
// To unbreak the chat round-trip, this route now uses the plain
|
||||
// `LangGraphAgent` and stops repacking properties into `configurable`. The
|
||||
// Python graph falls back to its `DEFAULT_*` constants, so the demo's
|
||||
// frontend toggles no longer affect the agent's response style. The
|
||||
// property-forwarding feature is tracked as a known regression pending an
|
||||
// `@ag-ui/langgraph` fix that decouples `context` from `configurable`.
|
||||
//
|
||||
// References:
|
||||
// - src/agents/agent_config_agent.py — the graph (still reads
|
||||
// configurable.properties; falls back to DEFAULT_* when missing)
|
||||
// - src/app/demos/agent-config/page.tsx — the provider config
|
||||
// - node_modules/.pnpm/@ag-ui+langgraph@0.0.31_*/dist/index.js — the
|
||||
// prepareStream merge that introduces the conflict
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const LANGGRAPH_URL =
|
||||
process.env.AGENT_URL ||
|
||||
process.env.LANGGRAPH_DEPLOYMENT_URL ||
|
||||
"http://localhost:8123";
|
||||
|
||||
const agentConfigAgent = new LangGraphAgent({
|
||||
deploymentUrl: LANGGRAPH_URL,
|
||||
graphId: "agent_config_agent",
|
||||
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
|
||||
});
|
||||
|
||||
const agents: Record<string, LangGraphAgent> = {
|
||||
// The page's <CopilotKitProvider agent="agent-config-demo"> resolves here.
|
||||
"agent-config-demo": agentConfigAgent,
|
||||
// Internal components (headless-chat, example-canvas) call `useAgent()` with
|
||||
// no args, which defaults to agentId "default". Alias to the same graph so
|
||||
// those component hooks resolve instead of throwing "Agent 'default' not
|
||||
// found".
|
||||
default: agentConfigAgent,
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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 },
|
||||
);
|
||||
}
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Dedicated runtime for the /demos/auth cell.
|
||||
//
|
||||
// Demonstrates framework-native request authentication via the V2 runtime's
|
||||
// `onRequest` hook, which runs before routing and can short-circuit the
|
||||
// request by throwing a Response. We validate a static `Authorization: Bearer
|
||||
// <DEMO_TOKEN>` header; mismatch throws 401 before the request reaches the
|
||||
// agent.
|
||||
//
|
||||
// Implementation note: the V1 Next.js adapter
|
||||
// (`copilotRuntimeNextJSAppRouterEndpoint`) does NOT forward the `hooks`
|
||||
// option to the V2 fetch handler. To get `onRequest` wired, this route uses
|
||||
// `createCopilotRuntimeHandler` from `@copilotkit/runtime/v2` directly — the
|
||||
// framework-agnostic fetch handler that returns a plain
|
||||
// `(Request) => Promise<Response>`, which composes cleanly with a Next.js
|
||||
// App Router route export.
|
||||
//
|
||||
// References:
|
||||
// - packages/runtime/src/v2/runtime/core/hooks.ts (onRequest semantics)
|
||||
// - packages/runtime/src/v2/runtime/__tests__/hooks.test.ts (throw Response pattern)
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
createCopilotRuntimeHandler,
|
||||
} from "@copilotkit/runtime/v2";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
import { DEMO_AUTH_HEADER } from "@/app/demos/auth/demo-token";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
// Reuse the neutral `sample_agent` graph for the authenticated path. The
|
||||
// point of this demo is the gate mechanism, not per-user agent branching —
|
||||
// authenticated users get the same behavior as any other neutral demo.
|
||||
const authDemoAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId: "sample_agent",
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
agents: {
|
||||
"auth-demo": authDemoAgent,
|
||||
// Fallback: useAgent() with no args resolves "default" — alias to the
|
||||
// same agent so hooks in the demo page resolve cleanly.
|
||||
default: authDemoAgent,
|
||||
},
|
||||
});
|
||||
|
||||
const BASE_PATH = "/api/copilotkit-auth";
|
||||
|
||||
// Framework-agnostic fetch handler with the auth gate wired up.
|
||||
const handler = createCopilotRuntimeHandler({
|
||||
runtime,
|
||||
basePath: BASE_PATH,
|
||||
hooks: {
|
||||
onRequest: ({ request }) => {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader !== DEMO_AUTH_HEADER) {
|
||||
// Throwing a Response short-circuits the pipeline. The runtime maps
|
||||
// thrown Responses to the HTTP response verbatim (status + body).
|
||||
throw new Response(
|
||||
JSON.stringify({
|
||||
error: "unauthorized",
|
||||
message:
|
||||
"Missing or invalid Authorization header. Click Authenticate above to send messages.",
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Next.js App Router bindings. The handler is framework-agnostic — it takes
|
||||
// a web Request and returns a web Response — so it drops straight into the
|
||||
// POST/GET exports without any adapter shim.
|
||||
export const POST = (req: NextRequest) => handler(req);
|
||||
export const GET = (req: NextRequest) => handler(req);
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Dedicated runtime for the Beautiful Chat flagship showcase cell.
|
||||
//
|
||||
// Beautiful Chat simultaneously exercises A2UI (dynamic + fixed schema),
|
||||
// Open Generative UI, and MCP Apps. The canonical reference
|
||||
// (examples/integrations/langgraph-python) ships all three flags on a single
|
||||
// runtime, but the 4085 showcase splits those concerns into per-feature
|
||||
// endpoints so non-flagship cells keep their per-demo `useFrontendTool` /
|
||||
// `useComponent` registrations isolated. This route restores the canonical's
|
||||
// combined runtime for just the one cell that needs it.
|
||||
//
|
||||
// References:
|
||||
// - examples/integrations/langgraph-python/src/app/api/copilotkit/[[...slug]]/route.ts
|
||||
// - src/app/api/copilotkit-ogui/route.ts (scoping pattern)
|
||||
// - src/app/api/copilotkit-mcp-apps/route.ts (mcpApps config pattern)
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const LANGGRAPH_URL =
|
||||
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
|
||||
|
||||
const beautifulChatAgent = new LangGraphAgent({
|
||||
deploymentUrl: LANGGRAPH_URL,
|
||||
graphId: "beautiful_chat",
|
||||
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
|
||||
});
|
||||
|
||||
const agents: Record<string, LangGraphAgent> = {
|
||||
// The page's <CopilotKit agent="beautiful-chat"> resolves here.
|
||||
"beautiful-chat": beautifulChatAgent,
|
||||
// Internal components (headless-chat, example-canvas) call `useAgent()`
|
||||
// with no args, which defaults to agentId "default". Alias to the same
|
||||
// graph so those component hooks resolve instead of throwing
|
||||
// "Agent 'default' not found". This matches the canonical's
|
||||
// `agents: { default: defaultAgent }` shape.
|
||||
default: beautifulChatAgent,
|
||||
};
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents,
|
||||
// Canonical: openGenerativeUI: true, a2ui.injectA2UITool: false, mcpApps.
|
||||
openGenerativeUI: true,
|
||||
a2ui: {
|
||||
// The backend graph has its own `generate_a2ui` tool, so we must NOT
|
||||
// inject the runtime's default A2UI tool on top (that would double-bind
|
||||
// the tool slot and confuse the LLM).
|
||||
injectA2UITool: false,
|
||||
// Models follow the tool-usage guide and omit `catalogId`, and the
|
||||
// middleware then falls back to the unregistered spec basic catalog
|
||||
// ("Catalog not found" render error). Pin the catalog the page registers.
|
||||
defaultCatalogId: "copilotkit://app-dashboard-catalog",
|
||||
},
|
||||
mcpApps: {
|
||||
servers: [
|
||||
{
|
||||
type: "http",
|
||||
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
|
||||
// Stable serverId so persisted threads keep restoring the same MCP
|
||||
// server across URL changes.
|
||||
serverId: "beautiful_chat_mcp",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-beautiful-chat",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
const e = error as { message?: string; stack?: string };
|
||||
return NextResponse.json(
|
||||
{ error: e.message, stack: e.stack },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Dedicated runtime for the byoc-hashbrown demo (Wave 4a).
|
||||
//
|
||||
// The demo page (`src/app/demos/byoc-hashbrown/page.tsx`) wraps CopilotChat
|
||||
// in the HashBrownDashboard provider and overrides the assistant message
|
||||
// slot with a renderer that consumes hashbrown-shaped structured output via
|
||||
// `@hashbrownai/react`'s `useUiKit` + `useJsonParser`. The agent behind this
|
||||
// endpoint (`byoc_hashbrown_agent`) has a system prompt tuned to emit that
|
||||
// shape — see `src/agents/byoc_hashbrown_agent.py`.
|
||||
//
|
||||
// Reference:
|
||||
// - src/app/api/copilotkit-a2ui-fixed-schema/route.ts (topology this mirrors)
|
||||
// - src/agents/byoc_hashbrown_agent.py (the backend graph)
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const LANGGRAPH_URL =
|
||||
process.env.AGENT_URL ||
|
||||
process.env.LANGGRAPH_DEPLOYMENT_URL ||
|
||||
"http://localhost:8123";
|
||||
|
||||
const byocHashbrownAgent = new LangGraphAgent({
|
||||
deploymentUrl: LANGGRAPH_URL,
|
||||
graphId: "byoc_hashbrown",
|
||||
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
|
||||
});
|
||||
|
||||
const agents: Record<string, LangGraphAgent> = {
|
||||
"byoc-hashbrown-demo": byocHashbrownAgent,
|
||||
// Internal components (headless-chat, example-canvas) call `useAgent()` with
|
||||
// no args, which defaults to agentId "default". Alias to the same graph so
|
||||
// those component hooks resolve instead of throwing "Agent 'default' not
|
||||
// found".
|
||||
default: byocHashbrownAgent,
|
||||
};
|
||||
|
||||
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-hashbrown",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
const e = error as { message?: string; stack?: string };
|
||||
return NextResponse.json(
|
||||
{ error: e.message, stack: e.stack },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Dedicated runtime for the BYOC json-render demo.
|
||||
*
|
||||
* Splitting into its own endpoint (mirroring beautiful-chat +
|
||||
* declarative-gen-ui) keeps the `byoc_json_render` agent isolated from the
|
||||
* default multi-agent `/api/copilotkit` runtime. The frontend's demo page
|
||||
* (src/app/demos/byoc-json-render/page.tsx) points `<CopilotKit
|
||||
* runtimeUrl>` here.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const LANGGRAPH_URL =
|
||||
process.env.AGENT_URL ||
|
||||
process.env.LANGGRAPH_DEPLOYMENT_URL ||
|
||||
"http://localhost:8123";
|
||||
|
||||
const byocJsonRenderAgent = new LangGraphAgent({
|
||||
deploymentUrl: LANGGRAPH_URL,
|
||||
graphId: "byoc_json_render",
|
||||
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- same-shape mismatch as the other dedicated routes in this
|
||||
// package; the LangGraphAgent satisfies the runtime's agent interface at
|
||||
// runtime but the generics don't line up across the v1/v2 boundary.
|
||||
agents: { byoc_json_render: byocJsonRenderAgent },
|
||||
});
|
||||
|
||||
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 },
|
||||
);
|
||||
}
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Dedicated runtime for the Declarative Generative UI (A2UI — Dynamic Schema) cell.
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
const declarativeGenUiAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId: "a2ui_dynamic",
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents: { "declarative-gen-ui": declarativeGenUiAgent },
|
||||
// No runtime `a2ui` config: the page passes a catalog to the provider
|
||||
// (`<CopilotKit a2ui={{ catalog }}>`), which auto-enables A2UI and defaults
|
||||
// tool injection on (CopilotKit >= 1.61.2, PR #5611).
|
||||
});
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-declarative-gen-ui",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
const e = error as { message?: string; stack?: string };
|
||||
return NextResponse.json(
|
||||
{ error: e.message, stack: e.stack },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
// CopilotKit runtime for the MCP Apps cell — shared by two demos:
|
||||
// - headless-complete (frontend wires runtimeUrl="/api/copilotkit-mcp-apps"
|
||||
// and renders activity events via a hand-rolled useRenderActivityMessage
|
||||
// in use-rendered-messages.tsx)
|
||||
// - mcp-apps (frontend relies on CopilotKit's built-in MCPAppsActivityRenderer)
|
||||
//
|
||||
// The runtime's `mcpApps` config auto-applies the MCP Apps middleware to the
|
||||
// agent: when the agent calls a tool backed by an MCP UI resource, the
|
||||
// middleware fetches the resource and emits the activity event that the
|
||||
// built-in `MCPAppsActivityRenderer` (registered by CopilotKit internally)
|
||||
// renders in the chat as a sandboxed iframe.
|
||||
//
|
||||
// Reference:
|
||||
// https://docs.copilotkit.ai/integrations/langgraph/generative-ui/mcp-apps
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
const headlessCompleteAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId: "headless_complete",
|
||||
});
|
||||
|
||||
const mcpAppsAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId: "mcp_apps",
|
||||
});
|
||||
|
||||
// @region[runtime-mcpapps-config]
|
||||
// The `mcpApps.servers` config is all you need server-side. The runtime
|
||||
// auto-applies the MCP Apps middleware to every registered agent: on each
|
||||
// MCP tool call it fetches the associated UI resource and emits an
|
||||
// `activity` event that the built-in `MCPAppsActivityRenderer` renders
|
||||
// inline in the chat.
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore
|
||||
agents: {
|
||||
"headless-complete": headlessCompleteAgent,
|
||||
"mcp-apps": mcpAppsAgent,
|
||||
},
|
||||
mcpApps: {
|
||||
servers: [
|
||||
{
|
||||
type: "http",
|
||||
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
|
||||
// Always pin a stable `serverId`. Without it CopilotKit hashes the
|
||||
// URL, and a URL change silently breaks restoration of persisted
|
||||
// MCP Apps in prior conversation threads.
|
||||
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,61 @@
|
||||
// Dedicated runtime for the Multimodal Attachments demo (Wave 2b).
|
||||
//
|
||||
// Why its own route? The backing graph (`multimodal`, from
|
||||
// src/agents/multimodal_agent.py) runs a vision-capable model (gpt-4o). Every
|
||||
// other cell in the showcase uses a text-only, cheaper model. Registering
|
||||
// `multimodal` under the shared `/api/copilotkit` runtime would silently upgrade
|
||||
// *all* cells that share that runtime to a vision model whenever the browser
|
||||
// routed to this one — wasting tokens and blurring the per-demo cost boundary.
|
||||
// A dedicated route keeps the vision capability — and its cost — scoped to
|
||||
// exactly the cell that exercises it, matching the pattern used by
|
||||
// `/api/copilotkit-beautiful-chat`.
|
||||
//
|
||||
// The page at src/app/demos/multimodal/page.tsx points its `runtimeUrl` at
|
||||
// this endpoint and sets `agent="multimodal-demo"` (the slug registered below).
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
const multimodalAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
// graphId references the key in langgraph.json — must match the
|
||||
// "multimodal" entry that resolves to src/agents/src/multimodal_agent.py:graph.
|
||||
graphId: "multimodal",
|
||||
});
|
||||
|
||||
const agents: Record<string, LangGraphAgent> = {
|
||||
// The page's <CopilotKit agent="multimodal-demo"> resolves here.
|
||||
"multimodal-demo": multimodalAgent,
|
||||
// Alias for any internal component that calls `useAgent()` without args
|
||||
// (matches the beautiful-chat route's "default" alias pattern).
|
||||
default: multimodalAgent,
|
||||
};
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-multimodal",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime: new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts; published CopilotRuntime's `agents`
|
||||
// type wraps Record in MaybePromise<NonEmptyRecord<...>> which rejects
|
||||
// plain Records. Fixed in source, pending release.
|
||||
agents,
|
||||
}),
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
const e = error as { message?: string; stack?: string };
|
||||
return NextResponse.json(
|
||||
{ error: e.message, stack: e.stack },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
|
||||
// Dedicated runtime for the Open Generative UI demos.
|
||||
// Isolated here because the `openGenerativeUI` runtime flag sets
|
||||
// `openGenerativeUIEnabled: true` globally on the probe response, which
|
||||
// causes the CopilotKit provider's setTools effect to wipe per-demo
|
||||
// `useFrontendTool`/`useComponent` registrations in the default runtime.
|
||||
|
||||
const LANGGRAPH_URL =
|
||||
process.env.AGENT_URL ||
|
||||
process.env.LANGGRAPH_DEPLOYMENT_URL ||
|
||||
"http://localhost:8123";
|
||||
|
||||
const openGenUiAgent = new LangGraphAgent({
|
||||
deploymentUrl: LANGGRAPH_URL,
|
||||
graphId: "open_gen_ui",
|
||||
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
|
||||
});
|
||||
const openGenUiAdvancedAgent = new LangGraphAgent({
|
||||
deploymentUrl: LANGGRAPH_URL,
|
||||
graphId: "open_gen_ui_advanced",
|
||||
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
|
||||
});
|
||||
|
||||
const agents: Record<string, LangGraphAgent> = {
|
||||
"open-gen-ui": openGenUiAgent,
|
||||
"open-gen-ui-advanced": openGenUiAdvancedAgent,
|
||||
// Internal components (headless-chat, example-canvas) call `useAgent()` with
|
||||
// no args, which defaults to agentId "default". Alias so those hooks resolve
|
||||
// instead of throwing "Agent 'default' not found".
|
||||
default: openGenUiAgent,
|
||||
};
|
||||
|
||||
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: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message, stack: error.stack },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Dedicated runtime for the /demos/voice cell.
|
||||
//
|
||||
// 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`), so recorded
|
||||
// audio is transcribed and the transcript auto-sends.
|
||||
// 3. Return a deterministic 4xx when `OPENAI_API_KEY` is not configured,
|
||||
// instead of an opaque 5xx. The V2 runtime's `handleTranscribe` maps
|
||||
// error messages containing "api key" or "unauthorized" to
|
||||
// `AUTH_FAILED → HTTP 401`, so throwing with that message funnels the
|
||||
// missing-key case into the intended 4xx path.
|
||||
//
|
||||
// Implementation
|
||||
// --------------
|
||||
// Wires the **V2** `CopilotRuntime` directly (from `@copilotkit/runtime/v2`)
|
||||
// because the V1 wrapper in `@copilotkit/runtime` drops the
|
||||
// `transcriptionService` option on the floor (see the TODO on the V1
|
||||
// constructor). V2 URL-routes on `/info`, `/agent/:id/run`, `/transcribe`,
|
||||
// etc., so the route file lives at `[[...slug]]/route.ts` to catch all
|
||||
// sub-paths under `/api/copilotkit-voice`.
|
||||
|
||||
// @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 { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
import { TranscriptionServiceOpenAI } from "@copilotkit/voice";
|
||||
import OpenAI from "openai";
|
||||
|
||||
const LANGGRAPH_URL =
|
||||
process.env.AGENT_URL ||
|
||||
process.env.LANGGRAPH_DEPLOYMENT_URL ||
|
||||
"http://localhost:8123";
|
||||
|
||||
const voiceDemoAgent = new LangGraphAgent({
|
||||
deploymentUrl: `${LANGGRAPH_URL}/`,
|
||||
graphId: "sample_agent",
|
||||
});
|
||||
|
||||
/**
|
||||
* Transcription service wrapper that reports a clean, typed auth error when
|
||||
* OPENAI_API_KEY is not configured. When the key is present we delegate to
|
||||
* the real OpenAI-backed service; any upstream Whisper error keeps its
|
||||
* natural categorization.
|
||||
*/
|
||||
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) {
|
||||
// "api key" substring → handleTranscribe maps to AUTH_FAILED → 401.
|
||||
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]
|
||||
|
||||
// Cache the runtime + handler across invocations so the transcription service
|
||||
// is constructed once per Node process instead of per request. The guarded
|
||||
// service reads OPENAI_API_KEY lazily in its transcribeFile call path, so
|
||||
// deferring construction past module load is not required for cold-start
|
||||
// safety under missing-key conditions.
|
||||
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: {
|
||||
// The page mounts <CopilotKit agent="voice-demo">; resolve that to
|
||||
// the neutral sample_agent graph.
|
||||
"voice-demo": voiceDemoAgent,
|
||||
// useAgent() with no args defaults to "default"; alias so any internal
|
||||
// default-agent lookups resolve against the same graph.
|
||||
default: voiceDemoAgent,
|
||||
},
|
||||
transcriptionService: new GuardedOpenAITranscriptionService(),
|
||||
});
|
||||
|
||||
cachedHandler = createCopilotRuntimeHandler({
|
||||
runtime,
|
||||
basePath: "/api/copilotkit-voice",
|
||||
});
|
||||
return cachedHandler;
|
||||
}
|
||||
|
||||
// Next.js App Router bindings. This file lives at
|
||||
// `src/app/api/copilotkit-voice/[[...slug]]/route.ts` — the catchall slug
|
||||
// pattern forwards every sub-path (`/info`, `/agent/:id/run`,
|
||||
// `/transcribe`, ...) to the V2 handler so its URL router can dispatch.
|
||||
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,169 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
// Emit a structured server-side error log with a correlation id so the
|
||||
// 500 we return to the client carries no stack/message details (which
|
||||
// can leak internal config, prompts, or upstream URLs) while operators
|
||||
// can still grep logs for the same `errorId` to find the full failure.
|
||||
function logRouteError(err: unknown): string {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
const errorId = crypto.randomUUID();
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
at: new Date().toISOString(),
|
||||
level: "error",
|
||||
phase: "setup",
|
||||
errorId,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}),
|
||||
);
|
||||
return errorId;
|
||||
}
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123";
|
||||
|
||||
console.log("[copilotkit/route] Initializing CopilotKit runtime");
|
||||
console.log(`[copilotkit/route] AGENT_URL: ${AGENT_URL}`);
|
||||
|
||||
// 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";
|
||||
|
||||
function createAgent(graphId: string = "sample_agent") {
|
||||
return new LangGraphAgent({
|
||||
deploymentUrl: `${AGENT_URL}/`,
|
||||
graphId,
|
||||
});
|
||||
}
|
||||
|
||||
const agentNames = [
|
||||
"agentic_chat",
|
||||
"human_in_the_loop",
|
||||
"gen-ui-tool-based",
|
||||
"gen-ui-agent",
|
||||
"shared-state-read",
|
||||
"shared-state-write",
|
||||
"shared-state-streaming",
|
||||
"prebuilt-sidebar",
|
||||
"prebuilt-popup",
|
||||
"chat-slots",
|
||||
"chat-customization-css",
|
||||
"headless-simple",
|
||||
];
|
||||
|
||||
const agents: Record<string, LangGraphAgent> = {};
|
||||
for (const name of agentNames) {
|
||||
agents[name] = createAgent();
|
||||
}
|
||||
|
||||
// Dedicated-graph agents for tool-rendering + reasoning demos.
|
||||
agents["tool-rendering"] = createAgent("tool_rendering");
|
||||
agents["tool-rendering-default-catchall"] = createAgent("tool_rendering");
|
||||
agents["tool-rendering-custom-catchall"] = createAgent("tool_rendering");
|
||||
agents["tool-rendering-reasoning-chain"] = createAgent(
|
||||
"tool_rendering_reasoning_chain",
|
||||
);
|
||||
agents["agentic-chat-reasoning"] = createAgent("reasoning_agent");
|
||||
agents["reasoning-default-render"] = createAgent("reasoning_agent");
|
||||
|
||||
// Interrupt variants — share the dedicated `interrupt_agent` graph that uses
|
||||
// langgraph's `interrupt()` primitive inside `schedule_meeting`.
|
||||
agents["gen-ui-interrupt"] = createAgent("interrupt_agent");
|
||||
agents["interrupt-headless"] = createAgent("interrupt_agent");
|
||||
|
||||
// Dedicated-graph agents — each cell has its own LangGraph graph with a
|
||||
// tailored system prompt (tools=[], CopilotKitMiddleware attached).
|
||||
agents["frontend_tools"] = createAgent("frontend_tools");
|
||||
agents["frontend-tools-async"] = createAgent("frontend_tools_async");
|
||||
agents["hitl-in-app"] = createAgent("hitl_in_app");
|
||||
// In-Chat HITL via the high-level `useHumanInTheLoop` hook — backend
|
||||
// agent has zero tools; the frontend-registered `book_call` tool is
|
||||
// injected into the LLM's tool list by `CopilotKitMiddleware`. Both
|
||||
// the canonical `hitl-in-chat` demo and the `hitl-in-chat-booking`
|
||||
// alias share the same backend graph.
|
||||
agents["hitl-in-chat"] = createAgent("hitl_in_chat");
|
||||
agents["hitl-in-chat-booking"] = createAgent("hitl_in_chat");
|
||||
// HITL step-selection: dedicated graph with tools=[] + CopilotKitMiddleware.
|
||||
// The `human_in_the_loop` alias in the neutral-assistant loop above maps to
|
||||
// `sample_agent` which has 7+ backend tools and a custom AgentState — the
|
||||
// frontend-only `generate_task_steps` tool (from useHumanInTheLoop) is the
|
||||
// ONLY tool this demo needs, so a minimal graph avoids state/tool contention.
|
||||
agents["human_in_the_loop"] = createAgent("hitl_steps");
|
||||
agents["readonly-state-agent-context"] = createAgent(
|
||||
"readonly_state_agent_context",
|
||||
);
|
||||
|
||||
// Shared State (Read + Write) — bidirectional shared state between UI and
|
||||
// agent. UI writes `preferences` via agent.setState; middleware reads them
|
||||
// into the system prompt; agent writes `notes` back via the `set_notes` tool.
|
||||
agents["shared-state-read-write"] = createAgent("shared_state_read_write");
|
||||
|
||||
// Sub-Agents — supervisor delegates to research_agent / writing_agent /
|
||||
// critique_agent (each a full create_agent under the hood). Every delegation
|
||||
// is appended to `state.delegations` for live UI rendering.
|
||||
agents["subagents"] = createAgent("subagents");
|
||||
|
||||
agents["default"] = createAgent();
|
||||
|
||||
console.log(
|
||||
`[copilotkit/route] Registered ${Object.keys(agents).length} agent names: ${Object.keys(agents).join(", ")}`,
|
||||
);
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime: new CopilotRuntime({
|
||||
// @ts-ignore
|
||||
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 message + stack server-side under a correlation id; return
|
||||
// only the id to the client so we don't leak internal details (upstream
|
||||
// URLs, env-driven config, prompts, etc.) into HTTP responses.
|
||||
const errorId = logRouteError(error);
|
||||
return NextResponse.json(
|
||||
{ error: "internal runtime error", errorId },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const GET = async () => {
|
||||
let agentStatus = "unknown";
|
||||
try {
|
||||
const res = await fetch(`${AGENT_URL}/ok`, {
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
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 || process.env.LANGGRAPH_DEPLOYMENT_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: "langgraph-fastapi",
|
||||
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: "langgraph-fastapi",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const INTEGRATION_SLUG = "langgraph-fastapi";
|
||||
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user