```
refactor: stream text segments for TTS preparation Refactor LLM streaming to emit complete text segments before tool calls for future TTS integration. Moves from step-based callbacks to chunk-based streaming with text buffering and segment flushing at tool boundaries. ```
This commit is contained in:
@@ -131,6 +131,7 @@ export interface Message {
|
|||||||
export interface StreamLLMParams {
|
export interface StreamLLMParams {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
onChunk?: (chunk: string) => void;
|
onChunk?: (chunk: string) => void;
|
||||||
|
onTextSegment?: (segment: string) => void;
|
||||||
onToolCall?: (toolName: string, args: any) => void;
|
onToolCall?: (toolName: string, args: any) => void;
|
||||||
onToolResult?: (toolName: string, result: any) => void;
|
onToolResult?: (toolName: string, result: any) => void;
|
||||||
onFinish?: (fullText: string) => void;
|
onFinish?: (fullText: string) => void;
|
||||||
@@ -150,39 +151,54 @@ export async function streamLLM(params: StreamLLMParams): Promise<string> {
|
|||||||
model: openrouter("anthropic/claude-haiku-4.5"),
|
model: openrouter("anthropic/claude-haiku-4.5"),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
tools: terminalTools,
|
tools: terminalTools,
|
||||||
stopWhen: stepCountIs(10),
|
onChunk: (chuk) => {
|
||||||
onStepFinish: async (event) => {
|
// if (params.onChunk) {
|
||||||
// Called after each step (tool call or text generation)
|
// params.onChunk(chunk);
|
||||||
if (event.toolCalls && event.toolCalls.length > 0) {
|
// }
|
||||||
for (const toolCall of event.toolCalls) {
|
|
||||||
if (params.onToolCall) {
|
|
||||||
params.onToolCall(toolCall.toolName, toolCall.input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.toolResults && event.toolResults.length > 0) {
|
|
||||||
for (const toolResult of event.toolResults) {
|
|
||||||
if (params.onToolResult) {
|
|
||||||
params.onToolResult(toolResult.toolName, toolResult.output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
stopWhen: stepCountIs(10),
|
||||||
});
|
});
|
||||||
|
|
||||||
let fullText = "";
|
let fullText = "";
|
||||||
|
let textBuffer = "";
|
||||||
|
|
||||||
result.fullStream;
|
function flushTextBuffer() {
|
||||||
|
if (textBuffer.length > 0 && params.onTextSegment) {
|
||||||
|
params.onTextSegment(textBuffer);
|
||||||
|
}
|
||||||
|
textBuffer = "";
|
||||||
|
}
|
||||||
|
|
||||||
// Stream text chunks to the caller
|
// Stream all events from fullStream
|
||||||
for await (const chunk of result.textStream) {
|
for await (const part of result.fullStream) {
|
||||||
fullText += chunk;
|
if (part.type === "text-delta") {
|
||||||
if (params.onChunk) {
|
// Accumulate text in buffer
|
||||||
params.onChunk(chunk);
|
textBuffer += part.text;
|
||||||
|
fullText += part.text;
|
||||||
|
|
||||||
|
// Stream individual deltas to caller
|
||||||
|
if (params.onChunk) {
|
||||||
|
params.onChunk(part.text);
|
||||||
|
}
|
||||||
|
} else if (part.type === "tool-call") {
|
||||||
|
// Flush accumulated text as a segment before tool call
|
||||||
|
flushTextBuffer();
|
||||||
|
|
||||||
|
// Emit tool call event
|
||||||
|
if (params.onToolCall) {
|
||||||
|
params.onToolCall(part.toolName, part.input);
|
||||||
|
}
|
||||||
|
} else if (part.type === "tool-result") {
|
||||||
|
// Emit tool result event
|
||||||
|
if (params.onToolResult) {
|
||||||
|
params.onToolResult(part.toolName, part.output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush any remaining text at the end
|
||||||
|
flushTextBuffer();
|
||||||
|
|
||||||
if (params.onFinish) {
|
if (params.onFinish) {
|
||||||
params.onFinish(fullText);
|
params.onFinish(fullText);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ export async function processUserMessage(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onTextSegment: (segment) => {
|
||||||
|
// Broadcast complete text segments (for future TTS integration)
|
||||||
|
if (params.wsServer) {
|
||||||
|
params.wsServer.broadcastActivityLog({
|
||||||
|
id: uuidv4(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: "system",
|
||||||
|
content: `Text segment: ${segment}`,
|
||||||
|
metadata: { segment, readyForTTS: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
onToolCall: (toolName, args) => {
|
onToolCall: (toolName, args) => {
|
||||||
// Broadcast tool call to WebSocket
|
// Broadcast tool call to WebSocket
|
||||||
if (params.wsServer) {
|
if (params.wsServer) {
|
||||||
@@ -126,7 +138,7 @@ export async function processUserMessage(params: {
|
|||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: "error",
|
type: "error",
|
||||||
content: `Error: ${error.message}`,
|
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user