fix: parse production learning agent streams
This commit is contained in:
@@ -38,6 +38,63 @@ function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function createSseDataParser() {
|
||||
let line = '';
|
||||
let pendingCarriageReturn = false;
|
||||
let dataLines: string[] = [];
|
||||
|
||||
const commitLine = (events: string[]) => {
|
||||
if (line === '') {
|
||||
if (dataLines.length > 0) events.push(dataLines.join('\n'));
|
||||
dataLines = [];
|
||||
return;
|
||||
}
|
||||
if (!line.startsWith(':')) {
|
||||
const separator = line.indexOf(':');
|
||||
const field = separator === -1 ? line : line.slice(0, separator);
|
||||
let value = separator === -1 ? '' : line.slice(separator + 1);
|
||||
if (value.startsWith(' ')) value = value.slice(1);
|
||||
if (field === 'data') dataLines.push(value);
|
||||
}
|
||||
line = '';
|
||||
};
|
||||
|
||||
const push = (chunk: string): string[] => {
|
||||
const events: string[] = [];
|
||||
for (const character of chunk) {
|
||||
if (pendingCarriageReturn) {
|
||||
pendingCarriageReturn = false;
|
||||
commitLine(events);
|
||||
if (character === '\n') continue;
|
||||
}
|
||||
if (character === '\r') {
|
||||
pendingCarriageReturn = true;
|
||||
} else if (character === '\n') {
|
||||
commitLine(events);
|
||||
} else {
|
||||
line += character;
|
||||
}
|
||||
}
|
||||
return events;
|
||||
};
|
||||
|
||||
const finish = (): string[] => {
|
||||
const events: string[] = [];
|
||||
if (pendingCarriageReturn) {
|
||||
pendingCarriageReturn = false;
|
||||
commitLine(events);
|
||||
}
|
||||
if (line !== '') commitLine(events);
|
||||
if (dataLines.length > 0) {
|
||||
events.push(dataLines.join('\n'));
|
||||
dataLines = [];
|
||||
}
|
||||
return events;
|
||||
};
|
||||
|
||||
return { push, finish };
|
||||
}
|
||||
|
||||
export function createLearningAgentClient(dependencies: Dependencies = {}) {
|
||||
const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch;
|
||||
const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken;
|
||||
@@ -129,28 +186,38 @@ export function createLearningAgentClient(dependencies: Dependencies = {}) {
|
||||
}
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const parser = createSseDataParser();
|
||||
let text = '';
|
||||
const consume = (data: string): { text?: string; completed?: true } => {
|
||||
if (!data) return {};
|
||||
const envelope = record(JSON.parse(data));
|
||||
if (envelope.run_id !== runId) return {};
|
||||
const payload = record(envelope.payload);
|
||||
if (envelope.type === 'learning.assistant.delta' && typeof payload.delta === 'string') {
|
||||
text += payload.delta;
|
||||
}
|
||||
if (envelope.type === 'learning.assistant.failed') {
|
||||
throw new Error(typeof payload.message === 'string' ? payload.message : '助教回答失败');
|
||||
}
|
||||
if (envelope.type === 'learning.assistant.completed') {
|
||||
return { text: text.trim(), completed: true };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
try {
|
||||
while (true) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
buffer += decoder.decode(chunk.value, { stream: true });
|
||||
const frames = buffer.split('\n\n');
|
||||
buffer = frames.pop() || '';
|
||||
for (const frame of frames) {
|
||||
const data = frame.split('\n').find((line) => line.startsWith('data:'))?.slice(5).trim();
|
||||
if (!data) continue;
|
||||
const envelope = record(JSON.parse(data));
|
||||
if (envelope.run_id !== runId) continue;
|
||||
const payload = record(envelope.payload);
|
||||
if (envelope.type === 'learning.assistant.delta' && typeof payload.delta === 'string') text += payload.delta;
|
||||
if (envelope.type === 'learning.assistant.failed') throw new Error(typeof payload.message === 'string' ? payload.message : '助教回答失败');
|
||||
if (envelope.type === 'learning.assistant.completed') {
|
||||
const events = chunk.done
|
||||
? [...parser.push(decoder.decode()), ...parser.finish()]
|
||||
: parser.push(decoder.decode(chunk.value, { stream: true }));
|
||||
for (const data of events) {
|
||||
const result = consume(data);
|
||||
if (result.completed) {
|
||||
controller.abort();
|
||||
return { text: text.trim() };
|
||||
return { text: result.text ?? '' };
|
||||
}
|
||||
}
|
||||
if (chunk.done) break;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
|
||||
Reference in New Issue
Block a user