import { IChatMessage, IMessageItem, IStreamDelta, IStreamData, PatternExtractor } from '@opentiny/genui-sdk-core';
import { ThinkTagWrapPattern } from './think-tag-wrap-pattern';
import { reactive, readonly, watch } from 'vue';
import { v4 as uuidv4 } from 'uuid';
import { emitter } from './event-emitter';
import { useI18n } from './i18n';
export interface IResponseHandler<T> {
name: string;
match: (data: T, context: any) => boolean;
handler: (data: T, context: any) => boolean;
notMatchHandler?: (data: T, context: any) => boolean;
start?: (
context: any,
handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void },
) => void;
end?: (context: any) => void;
}
const getStreamDelta = (data: IStreamData): IStreamDelta => {
return data.choices?.[0]?.delta ?? {};
};
function onToolResult(
toolCallsResult: any[],
delta: IStreamDelta,
toolCallIdMap: Record<string, IMessageItem & { type: 'tool' }>,
chatMessage: IChatMessage,
addToolCallContext: boolean,
) {
const {
id,
function: { arguments: args, result },
} = toolCallsResult[0];
const toolCallItem = toolCallIdMap[id];
if (toolCallItem) {
toolCallItem.status = 'success';
toolCallItem.content = JSON.stringify({ arguments: args, result }, null, 2);
emitter.emit('notification', {
type: 'tool',
delta,
toolCallData: readonly(toolCallItem),
chatMessage: readonly(chatMessage),
});
if (addToolCallContext) {
const { t } = useI18n();
chatMessage.content +=
t('toolCall.context', {
toolName: toolCallItem.name,
toolParams: JSON.stringify(args),
toolResult: JSON.stringify(result),
}) + '\n\n';
}
}
}
function onToolCall(
toolCalls: any[],
delta: IStreamDelta,
toolCallIdMap: Record<string, IMessageItem & { type: 'tool' }>,
chatMessage: IChatMessage,
toolCallStatus: { inProcessToolCallId: string | null },
) {
toolCalls.forEach((toolCall) => {
const {
id,
function: { name, arguments: argsDelta },
} = toolCall;
let toolCallItem: IMessageItem & { type: 'tool' };
if (id) {
toolCallStatus.inProcessToolCallId = id;
toolCallItem = reactive({
type: 'tool',
name: name,
formatPretty: true,
status: 'running',
content: JSON.stringify({ arguments: argsDelta || '' }, null, 2),
id,
});
toolCallIdMap[id] = toolCallItem;
chatMessage.messages.push(toolCallItem);
} else {
toolCallItem = toolCallIdMap[toolCallStatus.inProcessToolCallId];
const prevArgs = JSON.parse(toolCallItem.content).arguments;
const nextArgs = prevArgs + (argsDelta || '');
toolCallItem.content = JSON.stringify({ arguments: nextArgs }, null, 2);
}
emitter.emit('notification', {
type: 'tool',
delta,
toolCallData: readonly(toolCallItem),
chatMessage: readonly(chatMessage),
});
});
}
function onReasoningContent(reasoningContent: string, delta: IStreamDelta, chatMessage: IChatMessage) {
const lastMessage = chatMessage.messages[chatMessage.messages.length - 1];
let reasoningMessage = lastMessage;
if (reasoningMessage?.type === 'reasoning') {
reasoningMessage.content += reasoningContent;
} else {
reasoningMessage = {
type: 'reasoning',
content: reasoningContent,
thinking: true,
};
chatMessage.messages.push(reasoningMessage);
}
emitNotification(delta, chatMessage);
return reasoningMessage;
}
function onReasoningEnd(reasoningMessage: IMessageItem) {
if (reasoningMessage?.type === 'reasoning') reasoningMessage.thinking = false;
}
function emitNotification(delta: IStreamDelta, chatMessage: IChatMessage) {
const lastMessage = chatMessage.messages[chatMessage.messages.length - 1];
if (lastMessage) {
emitter.emit('notification', {
type: lastMessage.type as 'markdown' | 'schema-card',
delta,
chatMessage: readonly(chatMessage),
});
}
}
function onMarkdown(content: string, delta: IStreamDelta, chatMessage: IChatMessage) {
if (chatMessage.messages.length > 0 && chatMessage.messages[chatMessage.messages.length - 1].type === 'markdown') {
chatMessage.messages[chatMessage.messages.length - 1].content += content;
} else {
chatMessage.messages.push({
type: 'markdown',
content: content,
});
}
emitNotification(delta, chatMessage);
}
function onSchemaJSON(content: string, delta: IStreamDelta, chatMessage: IChatMessage) {
if (chatMessage.messages.length > 0 && chatMessage.messages[chatMessage.messages.length - 1].type === 'schema-card') {
chatMessage.messages[chatMessage.messages.length - 1].content += content;
} else {
chatMessage.messages.push({
type: 'schema-card',
content: content,
id: uuidv4(),
});
}
emitNotification(delta, chatMessage);
}
function watchReasoningEnd(context: any) {
context.unWatchReasoning = watch(
() => [...context.chatMessage.messages],
(newVal) => {
if (context.handleReasoning && newVal[newVal.length - 1]?.type !== 'reasoning') {
context.handleReasoning = false;
onReasoningEnd(context.reasoningMessage);
context.unWatchReasoning?.();
}
},
{ flush: 'sync' },
);
}
export const defaultResponseHandlers: IResponseHandler<IStreamData>[] = [
{
name: 'init',
match: (data: IStreamData, context: any) => false,
handler: (data: IStreamData, context: any) => false,
start: (
context: any,
handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void },
) => {
const chatMessage = reactive<IChatMessage>({
role: 'assistant',
content: '',
messages: [],
});
context.chatMessage = chatMessage;
context.handlers = handlers;
handlers.onData(chatMessage);
return false;
},
end: (context: any) => {
context.handlers.onDone();
emitter.emit('notification', {
type: 'done',
delta: {},
chatMessage: readonly(context.chatMessage),
});
},
},
{
name: 'finish-info',
match: (data: IStreamData, context: any) => {
const { choices, usage } = data;
return choices?.[0]?.finish_reason && Boolean(usage);
},
handler: (data: IStreamData, context: any) => {
context.chatMessage.finishInfo = data;
return true;
},
},
{
name: 'reasoning',
match: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
return Boolean(delta.reasoning_content);
},
handler: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
context.reasoningMessage = onReasoningContent(delta.reasoning_content, delta, context.chatMessage);
if (!context.handleReasoning) {
watchReasoningEnd(context);
context.handleReasoning = true;
}
return true;
},
start: (
context: any,
handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void },
) => {
context.handleReasoning = false;
},
end: (context: any) => {
context.unWatchReasoning?.();
if (context.handleReasoning) {
context.handleReasoning = false;
onReasoningEnd(context.reasoningMessage);
}
},
},
{
name: 'toolCall',
match: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
return delta.tool_calls?.length > 0;
},
handler: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
onToolCall(delta.tool_calls, delta, context.toolCallIdMap, context.chatMessage, context.toolCallStatus);
return true;
},
start: (
context: any,
handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void },
) => {
context.toolCallIdMap = {};
context.toolCallStatus = { inProcessToolCallId: null };
},
},
{
name: 'toolResult',
match: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
return delta.tool_calls_result?.length > 0;
},
handler: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
onToolResult(
delta.tool_calls_result,
delta,
context.toolCallIdMap,
context.chatMessage,
context.chatConfig.addToolCallContext,
);
return true;
},
},
{
name: 'content',
match: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
return Boolean(delta.content);
},
handler: (data: IStreamData, context: any) => {
const delta = getStreamDelta(data);
context.delta = delta;
context.patternExtractor.handleContent(delta.content);
context.chatMessage.content += delta.content;
return true;
},
start: (
context: any,
handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void },
) => {
const thinkPatternExtractor = new PatternExtractor({
onNormalWrite: (value) => onMarkdown(value, context.delta, context.chatMessage),
onHandledWrite: (value) => {
context.reasoningMessage = onReasoningContent(value, context.delta, context.chatMessage);
if (!context.handleReasoning) {
watchReasoningEnd(context);
context.handleReasoning = true;
}
},
regExpMap: new ThinkTagWrapPattern().regExpMap,
});
context.patternExtractor = new PatternExtractor({
onNormalWrite: (value) => thinkPatternExtractor.handleContent(value),
onHandledWrite: (value) => onSchemaJSON(value, context.delta, context.chatMessage),
});
},
},
];