Skip to content

Commit 947fa54

Browse files
AlemTuzlakjpr5
authored andcommitted
fix(showcase/langgraph-python): propagate runtime fixes from PR CopilotKit#4271
1 parent fcc610c commit 947fa54

14 files changed

Lines changed: 343 additions & 248 deletions

File tree

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
# Audio clips bundled with demos (e.g. the voice demo's sample.wav) are
2-
# binary — prevent autocrlf / line-ending rewrites on Windows checkouts.
3-
*.wav binary
1+
# Sample multimodal demo binaries (PNG + PDF) must stay as regular
2+
# binaries — not LFS pointers — so deploy environments without `git lfs pull`
3+
# (Railway, in particular) serve the actual files. LFS tracking for other
4+
# PNG/PDFs under this package inherits from the repo-root .gitattributes.
5+
public/demo-files/sample.png -filter -diff -merge binary
6+
public/demo-files/sample.pdf -filter -diff -merge binary
7+
# Voice demo sample audio follows the same convention.
8+
public/demo-audio/sample.wav -filter -diff -merge binary

showcase/packages/langgraph-python/manifest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ demos:
474474
- src/app/demos/auth/auth-banner.tsx
475475
- src/app/demos/auth/use-demo-auth.ts
476476
- src/app/demos/auth/demo-token.ts
477-
- src/app/api/copilotkit-auth/route.ts
477+
- src/app/api/copilotkit-auth/[[...slug]]/route.ts
478478
- id: byoc-hashbrown
479479
name: BYOC Hashbrown
480480
description: Streaming structured output via @hashbrownai/react, rendering a sales dashboard catalog (MetricCard + PieChart + BarChart)
@@ -527,7 +527,7 @@ demos:
527527
- src/agents/main.py
528528
- src/app/demos/voice/page.tsx
529529
- src/app/demos/voice/sample-audio-button.tsx
530-
- src/app/api/copilotkit-voice/route.ts
530+
- src/app/api/copilotkit-voice/[[...slug]]/route.ts
531531
- id: agent-config
532532
name: Agent Config Object
533533
description: Forward a typed config object (tone / expertise / response length) from the provider to the agent's dynamic system prompt
84.9 KB
Binary file not shown.
2.3 KB
Binary file not shown.
9.72 KB
Loading

showcase/packages/langgraph-python/src/agents/byoc_hashbrown_agent.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,17 @@
8787
{"ui":[{"Markdown":{"props":{"children":"## Q4 Sales Summary"}}},{"metric":{"props":{"label":"Total Revenue","value":"$1.2M"}}},{"metric":{"props":{"label":"New Customers","value":"248"}}},{"pieChart":{"props":{"title":"Revenue by Segment","data":"[{\\"label\\":\\"Enterprise\\",\\"value\\":600000},{\\"label\\":\\"SMB\\",\\"value\\":400000},{\\"label\\":\\"Startup\\",\\"value\\":200000}]"}}},{"barChart":{"props":{"title":"Monthly Revenue","data":"[{\\"label\\":\\"Oct\\",\\"value\\":350000},{\\"label\\":\\"Nov\\",\\"value\\":400000},{\\"label\\":\\"Dec\\",\\"value\\":450000}]"}}}]}
8888
"""
8989

90+
# Force JSON-object output mode. The frontend's `useJsonParser` bails to
91+
# `null` on any non-JSON prefix (code fences, prose preamble, etc.), so
92+
# leaving the model free to wander out of JSON is what left the renderer
93+
# empty in practice. `response_format={"type": "json_object"}` tells
94+
# OpenAI to refuse to emit anything but a single JSON object, which
95+
# aligns the wire-level contract with what the parser accepts.
9096
graph = create_agent(
91-
model=ChatOpenAI(model="gpt-4o-mini"),
97+
model=ChatOpenAI(
98+
model="gpt-4o-mini",
99+
model_kwargs={"response_format": {"type": "json_object"}},
100+
),
92101
tools=[],
93102
middleware=[CopilotKitMiddleware()],
94103
system_prompt=BYOC_HASHBROWN_SYSTEM_PROMPT,

showcase/packages/langgraph-python/src/agents/byoc_json_render_agent.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,17 @@
143143
"""
144144

145145

146+
# Force JSON-object output mode. The frontend's `parseSpec` already
147+
# tolerates code fences and prose preamble via `extractJsonObject`, but
148+
# locking the model to JSON at the API layer removes the ambiguity
149+
# entirely — the only thing the LLM can emit is a single JSON object,
150+
# which is exactly what `<Renderer />` needs.
146151
graph = create_agent(
147-
model=ChatOpenAI(model="gpt-4o-mini", temperature=0.2),
152+
model=ChatOpenAI(
153+
model="gpt-4o-mini",
154+
temperature=0.2,
155+
model_kwargs={"response_format": {"type": "json_object"}},
156+
),
148157
tools=[],
149158
middleware=[CopilotKitMiddleware()],
150159
system_prompt=SYSTEM_PROMPT.strip(),

showcase/packages/langgraph-python/src/app/api/copilotkit-auth/route.ts renamed to showcase/packages/langgraph-python/src/app/api/copilotkit-auth/[[...slug]]/route.ts

File renamed without changes.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Dedicated runtime for the /demos/voice cell.
2+
//
3+
// Goals
4+
// -----
5+
// 1. Advertise `audioFileTranscriptionEnabled: true` on `/info` so the chat
6+
// composer renders the mic button.
7+
// 2. Handle `POST /transcribe` by invoking an OpenAI-backed
8+
// `TranscriptionServiceOpenAI` (from `@copilotkit/voice`), so recorded
9+
// audio is transcribed and the transcript auto-sends.
10+
// 3. Return a deterministic 4xx when `OPENAI_API_KEY` is not configured,
11+
// instead of an opaque 5xx. The V2 runtime's `handleTranscribe` maps
12+
// error messages containing "api key" or "unauthorized" to
13+
// `AUTH_FAILED → HTTP 401`, so throwing with that message funnels the
14+
// missing-key case into the intended 4xx path.
15+
//
16+
// Implementation
17+
// --------------
18+
// Wires the **V2** `CopilotRuntime` directly (from `@copilotkit/runtime/v2`)
19+
// because the V1 wrapper in `@copilotkit/runtime` drops the
20+
// `transcriptionService` option on the floor (see the TODO on the V1
21+
// constructor). V2 URL-routes on `/info`, `/agent/:id/run`, `/transcribe`,
22+
// etc., so the route file lives at `[[...slug]]/route.ts` to catch all
23+
// sub-paths under `/api/copilotkit-voice`.
24+
25+
import type { NextRequest } from "next/server";
26+
import {
27+
CopilotRuntime,
28+
TranscriptionService,
29+
createCopilotRuntimeHandler,
30+
} from "@copilotkit/runtime/v2";
31+
import type { TranscribeFileOptions } from "@copilotkit/runtime/v2";
32+
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
33+
import { TranscriptionServiceOpenAI } from "@copilotkit/voice";
34+
import OpenAI from "openai";
35+
36+
const LANGGRAPH_URL =
37+
process.env.AGENT_URL ||
38+
process.env.LANGGRAPH_DEPLOYMENT_URL ||
39+
"http://localhost:8123";
40+
41+
const voiceDemoAgent = new LangGraphAgent({
42+
deploymentUrl: `${LANGGRAPH_URL}/`,
43+
graphId: "sample_agent",
44+
});
45+
46+
/**
47+
* Transcription service wrapper that reports a clean, typed auth error when
48+
* OPENAI_API_KEY is not configured. When the key is present we delegate to
49+
* the real OpenAI-backed service; any upstream Whisper error keeps its
50+
* natural categorization.
51+
*/
52+
class GuardedOpenAITranscriptionService extends TranscriptionService {
53+
private delegate: TranscriptionServiceOpenAI | null;
54+
55+
constructor() {
56+
super();
57+
const apiKey = process.env.OPENAI_API_KEY;
58+
this.delegate = apiKey
59+
? new TranscriptionServiceOpenAI({ openai: new OpenAI({ apiKey }) })
60+
: null;
61+
}
62+
63+
async transcribeFile(options: TranscribeFileOptions): Promise<string> {
64+
if (!this.delegate) {
65+
// "api key" substring → handleTranscribe maps to AUTH_FAILED → 401.
66+
throw new Error(
67+
"OPENAI_API_KEY not configured for this deployment (api key missing). " +
68+
"Set OPENAI_API_KEY to enable voice transcription.",
69+
);
70+
}
71+
return this.delegate.transcribeFile(options);
72+
}
73+
}
74+
75+
// Cache the runtime + handler across invocations so the transcription service
76+
// is constructed once per Node process instead of per request. The guarded
77+
// service reads OPENAI_API_KEY lazily in its transcribeFile call path, so
78+
// deferring construction past module load is not required for cold-start
79+
// safety under missing-key conditions.
80+
let cachedHandler: ((req: Request) => Promise<Response>) | null = null;
81+
function getHandler(): (req: Request) => Promise<Response> {
82+
if (cachedHandler) return cachedHandler;
83+
84+
const runtime = new CopilotRuntime({
85+
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in
86+
// MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in
87+
// source, pending release.
88+
agents: {
89+
// The page mounts <CopilotKit agent="voice-demo">; resolve that to
90+
// the neutral sample_agent graph.
91+
"voice-demo": voiceDemoAgent,
92+
// useAgent() with no args defaults to "default"; alias so any internal
93+
// default-agent lookups resolve against the same graph.
94+
default: voiceDemoAgent,
95+
},
96+
transcriptionService: new GuardedOpenAITranscriptionService(),
97+
});
98+
99+
cachedHandler = createCopilotRuntimeHandler({
100+
runtime,
101+
basePath: "/api/copilotkit-voice",
102+
});
103+
return cachedHandler;
104+
}
105+
106+
// Next.js App Router bindings. This file lives at
107+
// `src/app/api/copilotkit-voice/[[...slug]]/route.ts` — the catchall slug
108+
// pattern forwards every sub-path (`/info`, `/agent/:id/run`,
109+
// `/transcribe`, ...) to the V2 handler so its URL router can dispatch.
110+
export const POST = (req: NextRequest) => getHandler()(req);
111+
export const GET = (req: NextRequest) => getHandler()(req);
112+
export const PUT = (req: NextRequest) => getHandler()(req);
113+
export const DELETE = (req: NextRequest) => getHandler()(req);

showcase/packages/langgraph-python/src/app/api/copilotkit-voice/route.ts

Lines changed: 0 additions & 160 deletions
This file was deleted.

0 commit comments

Comments
 (0)