How to Stream Scene Outlines During Generation in OpenMAIC
OpenMAIC streams scene outlines during course generation via a Server-Sent Events (SSE) endpoint at /api/generate/scene-outlines-stream that incrementally parses LLM output and dispatches real-time 'outline' events to the client.
OpenMAIC is an open-source AI course generation platform developed by THU-MAIC that produces educational content through multi-stage pipelines. When generating a course, the system creates scene outlines as its first stage, and you can stream these outlines to the frontend as they are produced rather than waiting for the entire generation to complete. This guide explains the architecture and implementation for consuming live scene outline streams using the OpenMAIC source code.
Architecture Overview
The streaming pipeline uses Server-Sent Events to push data from the Next.js backend to the React frontend. When a generation session starts, the backend opens an SSE connection and begins calling the LLM through the outline generator. Instead of buffering the entire response, the server parses incoming chunks incrementally and emits events for each newly available outline object.
The flow involves three core components: the SSE route handler in app/api/generate/scene-outlines-stream/route.ts, the outline generation logic in packages/@openmaic/generation/src/outline-generator.ts, and the client-side session management hooks in the workbench library.
Server-Side SSE Implementation
The Route Handler
The streaming endpoint is implemented as a Next.js Route Handler in app/api/generate/scene-outlines-stream/route.ts. This file exports a GET function that establishes an SSE connection with the client and manages the lifecycle of the generation process.
The handler initializes an EventSource instance and invokes generateSceneOutlinesFromRequirements to begin the LLM call. As the language model produces output, the handler buffers the incoming text and searches for parseable outline objects.
// app/api/generate/scene-outlines-stream/route.ts
export async function GET(req: Request) {
const { sessionId } = req.query;
const source = new EventSource(`/api/agent/owner-events?session=${sessionId}`);
// Call the outline generator (uses the LLM)
const { outlines, languageDirective, courseTitle } = await generateSceneOutlinesFromRequirements(
requirements,
pdfText,
pdfImages,
aiCall,
{ logger }
);
// Stream each outline as soon as it is parsed
for (const outline of outlines) {
source.dispatchEvent(new MessageEvent('outline', { data: JSON.stringify(outline) }));
}
// Final event signalling completion
source.dispatchEvent(
new MessageEvent('done', {
data: JSON.stringify({ outlines, languageDirective, courseTitle }),
})
);
return new Response(source, { headers: { 'Content-Type': 'text/event-stream' } });
}
Incremental Parsing with extractNewOutlines
Because the LLM streams a long JSON payload, the server cannot wait for the entire response to parse it. The extractNewOutlines function (implemented around lines 120-180 of the route file) buffers incoming chunks and repeatedly attempts to extract newly available outline objects from the growing text buffer.
This function looks for the "outlines" array key in the partial JSON and returns any fresh items that have been fully received since the last extraction. The implementation handles incomplete JSON segments gracefully, ensuring that the parser only emits valid outline objects.
// Simplified excerpt from app/api/generate/scene-outlines-stream/route.ts
function extractNewOutlines(buffer: string, lastIndex: number): { outlines: Outline[], newIndex: number } {
// Look for the "outlines" array in the partial JSON
const arrayMatch = buffer.match(/"outlines"\s*:\s*(\[.*?\])(?!.*\])/s);
if (!arrayMatch) return { outlines: [], newIndex: lastIndex };
// Parse only up to the last complete object
const parseableSection = arrayMatch[1].slice(0, arrayMatch[1].lastIndexOf('}') + 1);
const outlines = JSON.parse(`[${parseableSection}]`);
return { outlines: outlines.slice(lastIndex), newIndex: outlines.length };
}
LLM Integration and Error Handling
The core generation logic resides in packages/@openmaic/generation/src/outline-generator.ts, specifically the generateSceneOutlinesFromRequirements function. This function builds the prompt using buildOutlinePrompt and manages the streaming call to the AI provider via aiCall.
If the parser fails to extract outlines from the stream, the route implements retry logic. It attempts the generation up to MAX_STREAM_RETRIES times (as seen around lines 450-670) before sending an error event to the client. This resilience ensures that transient LLM failures do not break the user experience.
// Error handling and retry logic excerpt
const MAX_STREAM_RETRIES = 3;
let attempts = 0;
while (attempts < MAX_STREAM_RETRIES) {
try {
const result = await generateSceneOutlinesFromRequirements(...);
if (result.outlines.length > 0) break;
} catch (err) {
attempts++;
if (attempts === MAX_STREAM_RETRIES) {
source.dispatchEvent(new MessageEvent('error', { data: 'Generation failed' }));
return;
}
}
}
Client-Side Consumption
React Hook Implementation
The frontend consumes the SSE stream through a custom React hook. While OpenMAIC uses useWorkbenchSession for general session management, you can implement a specific hook for scene outlines that wraps the native EventSource API.
// lib/workbench/use-workbench-session.ts (excerpt around lines 145-165)
export function useSceneOutlineStream(sessionId: string) {
const [outlines, setOutlines] = useState<SceneOutline[]>([]);
const [done, setDone] = useState(false);
useEffect(() => {
const es = new EventSource(`/api/generate/scene-outlines-stream?sessionId=${sessionId}`);
es.addEventListener('outline', (ev) => {
const outline = JSON.parse(ev.data) as SceneOutline;
setOutlines((prev) => [...prev, outline]);
});
es.addEventListener('done', (ev) => {
const payload = JSON.parse(ev.data);
setOutlines(payload.outlines);
setDone(true);
es.close();
});
es.addEventListener('error', () => {
console.error('Outline stream error');
es.close();
});
return () => es.close();
}, [sessionId]);
return { outlines, done };
}
Event Types and UI Integration
The stream emits three event types that your client must handle:
outline: Contains a singleSceneOutlineobject. Append this to your UI list immediately for a live-preview effect.done: Signals completion and contains the finaloutlinesarray,languageDirective, andcourseTitle. Use this to finalize the UI state (implemented around lines 660-680 of the route).error: Indicates that generation failed after all retry attempts. Display an error message to the user.
// Component usage example
const { outlines, done } = useSceneOutlineStream(currentSessionId);
return (
<div className="outline-list">
{outlines.map((outline) => (
<OutlineCard key={outline.id} outline={outline} />
))}
{done && <p>All outlines generated.</p>}
</div>
);
Key Files and Functions
Understanding the streaming implementation requires familiarity with these specific source files:
app/api/generate/scene-outlines-stream/route.ts: The SSE endpoint that handles client connections, manages LLM streaming, and dispatches events. ContainsextractNewOutlinesfor incremental parsing.packages/@openmaic/generation/src/outline-generator.ts: Core generation logic implementinggenerateSceneOutlinesFromRequirementsand prompt construction viabuildOutlinePrompt.lib/workbench/use-workbench-session.ts: Generic session hook (lines 145-165) that manages EventSource connections; reference implementation for streaming consumption.lib/workbench/owner-session-client.ts: Low-level EventSource wrapper handling reconnection logic and frame routing.lib/server/classroom-generation.ts(lines 470-500): Higher-level orchestration that logs generation start times and formats final messages before they reach the SSE route.
Summary
- Use the SSE endpoint
/api/generate/scene-outlines-streamto receive real-time scene outline updates during course generation. - Incremental parsing via
extractNewOutlinesin the route handler allows the server to emit valid JSON objects before the LLM finishes generating the complete response. - Event types include
outline(new data),done(completion with metadata), anderror(failure after retries). - Retry logic with
MAX_STREAM_RETRIESensures resilience against transient LLM failures. - Client implementation uses the native
EventSourceAPI or wrapper hooks likeuseWorkbenchSessionto update React state as outlines arrive.
Frequently Asked Questions
What is the exact endpoint URL for streaming scene outlines?
The endpoint is GET /api/generate/scene-outlines-stream. You must provide a sessionId query parameter to identify the generation session. The route is implemented in app/api/generate/scene-outlines-stream/route.ts and returns a text/event-stream Content-Type response.
How does OpenMAIC parse partial LLM responses during streaming?
The server uses a helper function called extractNewOutlines located in the SSE route file around lines 120-180. This function maintains a text buffer of incoming chunks and searches for the "outlines" JSON array key. It extracts only complete objects that have been fully received, returning them to the caller while leaving partial data in the buffer for the next chunk.
What events does the scene outline stream emit?
The stream emits three primary event types: outline (containing individual scene outline objects as they become available), done (signaling completion with the full outline list, language directive, and course title), and error (indicating that generation failed after exhausting all MAX_STREAM_RETRIES attempts).
How do I implement the client-side listener for this stream?
Create a React hook that instantiates new EventSource('/api/generate/scene-outlines-stream?sessionId=' + id) and attaches event listeners for outline, done, and error events. Update your component state within these listeners to render outlines in real-time, as demonstrated in lib/workbench/use-workbench-session.ts. Always close the connection in the cleanup function to prevent memory leaks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →