import { createToolCall, type ToolCall } from "../../domain/value-objects/ToolCall.js" /** * Parsed response from LLM. */ export interface ParsedResponse { /** Text content (excluding tool calls) */ content: string /** Extracted tool calls */ toolCalls: ToolCall[] /** Whether parsing encountered issues */ hasParseErrors: boolean /** Parse error messages */ parseErrors: string[] } /** * XML tool call tag pattern. * Matches: ... */ const TOOL_CALL_REGEX = /([\s\S]*?)<\/tool_call>/gi /** * XML parameter tag pattern. * Matches: value or value */ const PARAM_REGEX_NAMED = /([\s\S]*?)<\/param>/gi const PARAM_REGEX_ELEMENT = /<([a-z_][a-z0-9_]*)>([\s\S]*?)<\/\1>/gi /** * Parse tool calls from LLM response text. * Supports XML format: src/index.ts */ export function parseToolCalls(response: string): ParsedResponse { const toolCalls: ToolCall[] = [] const parseErrors: string[] = [] let content = response const matches = [...response.matchAll(TOOL_CALL_REGEX)] for (const match of matches) { const [fullMatch, toolName, paramsXml] = match try { const params = parseParameters(paramsXml) const toolCall = createToolCall( `xml_${String(Date.now())}_${String(toolCalls.length)}`, toolName, params, ) toolCalls.push(toolCall) content = content.replace(fullMatch, "") } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) parseErrors.push(`Failed to parse tool call "${toolName}": ${errorMsg}`) } } content = content.trim() return { content, toolCalls, hasParseErrors: parseErrors.length > 0, parseErrors, } } /** * Parse parameters from XML content. */ function parseParameters(xml: string): Record { const params: Record = {} const namedMatches = [...xml.matchAll(PARAM_REGEX_NAMED)] for (const match of namedMatches) { const [, name, value] = match params[name] = parseValue(value) } if (namedMatches.length === 0) { const elementMatches = [...xml.matchAll(PARAM_REGEX_ELEMENT)] for (const match of elementMatches) { const [, name, value] = match params[name] = parseValue(value) } } return params } /** * Parse a value string to appropriate type. */ function parseValue(value: string): unknown { const trimmed = value.trim() if (trimmed === "true") { return true } if (trimmed === "false") { return false } if (trimmed === "null") { return null } const num = Number(trimmed) if (!isNaN(num) && trimmed !== "") { return num } if ( (trimmed.startsWith("[") && trimmed.endsWith("]")) || (trimmed.startsWith("{") && trimmed.endsWith("}")) ) { try { return JSON.parse(trimmed) } catch { return trimmed } } return trimmed } /** * Format tool calls to XML for prompt injection. * Useful when you need to show the LLM the expected format. */ export function formatToolCallsAsXml(toolCalls: ToolCall[]): string { return toolCalls .map((tc) => { const params = Object.entries(tc.params) .map(([key, value]) => ` <${key}>${formatValueForXml(value)}`) .join("\n") return `\n${params}\n` }) .join("\n\n") } /** * Format a value for XML output. */ function formatValueForXml(value: unknown): string { if (value === null || value === undefined) { return "" } if (typeof value === "object") { return JSON.stringify(value) } if (typeof value === "string") { return value } if (typeof value === "number" || typeof value === "boolean") { return String(value) } return JSON.stringify(value) } /** * Extract thinking/reasoning from response. * Matches content between ... tags. */ export function extractThinking(response: string): { thinking: string; content: string } { const thinkingRegex = /([\s\S]*?)<\/thinking>/gi const matches = [...response.matchAll(thinkingRegex)] if (matches.length === 0) { return { thinking: "", content: response } } let content = response const thoughts: string[] = [] for (const match of matches) { thoughts.push(match[1].trim()) content = content.replace(match[0], "") } return { thinking: thoughts.join("\n\n"), content: content.trim(), } } /** * Check if response contains tool calls. */ export function hasToolCalls(response: string): boolean { return TOOL_CALL_REGEX.test(response) } /** * Validate tool call parameters against expected schema. */ export function validateToolCallParams( toolName: string, params: Record, requiredParams: string[], ): { valid: boolean; errors: string[] } { const errors: string[] = [] for (const param of requiredParams) { if (!(param in params) || params[param] === undefined || params[param] === null) { errors.push(`Missing required parameter: ${param}`) } } return { valid: errors.length === 0, errors, } }