import { readDataStream } from './read-data-stream'; import type { FunctionCall, JSONValue, Message, ToolCall } from './types'; import { generateId as generateIdFunction } from './generate-id'; type PrefixMap = { text?: Message; function_call?: Message & { role: 'assistant'; function_call: FunctionCall; }; tool_calls?: Message & { role: 'assistant'; tool_calls: ToolCall[]; }; data: JSONValue[]; }; function assignAnnotationsToMessage( message: T, annotations: JSONValue[] | undefined, ): T { if (!message || !annotations || !annotations.length) return message; return { ...message, annotations: [...annotations] } as T; } export async function parseComplexResponse({ reader, abortControllerRef, update, onFinish, generateId = generateIdFunction, getCurrentDate = () => new Date(), }: { reader: ReadableStreamDefaultReader; abortControllerRef?: { current: AbortController | null; }; update: (merged: Message[], data: JSONValue[] | undefined) => void; onFinish?: (prefixMap: PrefixMap) => void; generateId?: () => string; getCurrentDate?: () => Date; }) { const createdAt = getCurrentDate(); const prefixMap: PrefixMap = { data: [], }; // keep list of current message annotations for message let message_annotations: JSONValue[] | undefined = undefined; // we create a map of each prefix, and for each prefixed message we push to the map for await (const { type, value } of readDataStream(reader, { isAborted: () => abortControllerRef?.current === null, })) { if (type === 'text') { if (prefixMap['text']) { prefixMap['text'] = { ...prefixMap['text'], content: (prefixMap['text'].content || '') + value, }; } else { prefixMap['text'] = { id: generateId(), role: 'assistant', content: value, createdAt, }; } } let functionCallMessage: Message | null | undefined = null; if (type === 'function_call') { prefixMap['function_call'] = { id: generateId(), role: 'assistant', content: '', function_call: value.function_call, name: value.function_call.name, createdAt, }; functionCallMessage = prefixMap['function_call']; } let toolCallMessage: Message | null | undefined = null; if (type === 'tool_calls') { prefixMap['tool_calls'] = { id: generateId(), role: 'assistant', content: '', tool_calls: value.tool_calls, createdAt, }; toolCallMessage = prefixMap['tool_calls']; } if (type === 'data') { prefixMap['data'].push(...value); } let responseMessage = prefixMap['text']; if (type === 'message_annotations') { if (!message_annotations) { message_annotations = [...value]; } else { message_annotations.push(...value); } // Update any existing message with the latest annotations functionCallMessage = assignAnnotationsToMessage( prefixMap['function_call'], message_annotations, ); toolCallMessage = assignAnnotationsToMessage( prefixMap['tool_calls'], message_annotations, ); responseMessage = assignAnnotationsToMessage( prefixMap['text'], message_annotations, ); } // keeps the prefixMap up to date with the latest annotations, even if annotations preceded the message if (message_annotations?.length) { const messagePrefixKeys: (keyof PrefixMap)[] = [ 'text', 'function_call', 'tool_calls', ]; messagePrefixKeys.forEach(key => { if (prefixMap[key]) { (prefixMap[key] as Message).annotations = [...message_annotations!]; } }); } // We add function & tool calls and response messages to the messages[], but data is its own thing const merged = [functionCallMessage, toolCallMessage, responseMessage] .filter(Boolean) .map(message => ({ ...assignAnnotationsToMessage(message, message_annotations), })) as Message[]; update(merged, [...prefixMap['data']]); // make a copy of the data array } onFinish?.(prefixMap); return { messages: [ prefixMap.text, prefixMap.function_call, prefixMap.tool_calls, ].filter(Boolean) as Message[], data: prefixMap.data, }; }