添加工具调用解析和聊天工具操作处理
This commit is contained in:
@@ -4,6 +4,30 @@ export type AssistantMessageSections = {
|
||||
thoughtComplete: boolean;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Tool-call types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export type ToolCall = {
|
||||
id: string;
|
||||
tool: string;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ContentSegment =
|
||||
| { type: "text"; content: string }
|
||||
| { type: "tool_call"; toolCall: ToolCall }
|
||||
| { type: "tool_call_pending" };
|
||||
|
||||
export type ParsedToolContent = {
|
||||
segments: ContentSegment[];
|
||||
toolCalls: ToolCall[];
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Think-block parsing */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const THINK_BLOCK_PATTERN = /<think>([\s\S]*?)<\/think>/gi;
|
||||
const THINK_OPEN_TAG = "<think>";
|
||||
const THINK_CLOSE_TAG = "</think>";
|
||||
@@ -53,3 +77,90 @@ export const parseAssistantMessageSections = (
|
||||
thoughtComplete: Boolean(normalizedThought) && !hasUnclosedThought,
|
||||
};
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Tool-call parsing */
|
||||
/* */
|
||||
/* AI responses may embed tool calls using: */
|
||||
/* <tool_call>{"tool":"locate_pipes","params":{...}}</tool_call> */
|
||||
/* */
|
||||
/* Returns ordered segments (text + tool_call interleaved) so the */
|
||||
/* UI can render them inline where the AI placed them. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const TOOL_CALL_BLOCK_PATTERN = /<tool_call>([\s\S]*?)<\/tool_call>/gi;
|
||||
const TOOL_CALL_OPEN_TAG = "<tool_call>";
|
||||
|
||||
/** Regex to strip partial opening tag at the end of text during streaming. */
|
||||
const PARTIAL_TOOL_TAG_TAIL = /<(?:t(?:o(?:o(?:l(?:_(?:c(?:a(?:l(?:l)?)?)?)?)?)?)?)?)?$/;
|
||||
|
||||
export const parseContentWithToolCalls = (
|
||||
content: string,
|
||||
): ParsedToolContent => {
|
||||
if (!content) {
|
||||
return { segments: [], toolCalls: [] };
|
||||
}
|
||||
|
||||
const segments: ContentSegment[] = [];
|
||||
const toolCalls: ToolCall[] = [];
|
||||
let lastIndex = 0;
|
||||
let tcIndex = 0;
|
||||
|
||||
// Find all complete <tool_call>...</tool_call> blocks
|
||||
const regex = /<tool_call>([\s\S]*?)<\/tool_call>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
// Text before this tool call
|
||||
const textBefore = content.slice(lastIndex, match.index);
|
||||
if (textBefore.trim()) {
|
||||
segments.push({ type: "text", content: textBefore.trim() });
|
||||
}
|
||||
|
||||
// Parse the tool call JSON
|
||||
try {
|
||||
const parsed = JSON.parse(match[1].trim()) as {
|
||||
tool?: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
const toolCall: ToolCall = {
|
||||
id: `tc-${tcIndex++}`,
|
||||
tool: parsed.tool ?? "unknown",
|
||||
params: parsed.params ?? {},
|
||||
};
|
||||
segments.push({ type: "tool_call", toolCall });
|
||||
toolCalls.push(toolCall);
|
||||
} catch {
|
||||
// Malformed JSON – treat as plain text
|
||||
segments.push({ type: "text", content: match[0] });
|
||||
}
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Handle remaining text after the last match
|
||||
const remaining = content.slice(lastIndex);
|
||||
|
||||
// Check for an unclosed <tool_call> tag (still streaming)
|
||||
const unclosedIdx = remaining.lastIndexOf(TOOL_CALL_OPEN_TAG);
|
||||
if (unclosedIdx !== -1) {
|
||||
const textBefore = remaining.slice(0, unclosedIdx);
|
||||
if (textBefore.trim()) {
|
||||
segments.push({ type: "text", content: textBefore.trim() });
|
||||
}
|
||||
segments.push({ type: "tool_call_pending" });
|
||||
} else {
|
||||
// Strip partial opening tags at the end (e.g. "<tool_c" while streaming)
|
||||
const cleaned = remaining.replace(PARTIAL_TOOL_TAG_TAIL, "").trim();
|
||||
if (cleaned) {
|
||||
segments.push({ type: "text", content: cleaned });
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing was parsed, return the original content as a single text segment
|
||||
if (segments.length === 0) {
|
||||
segments.push({ type: "text", content });
|
||||
}
|
||||
|
||||
return { segments, toolCalls };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user