interface ParsedCall { name: string; positionalArgs: unknown[]; keywordArgs: Record; } interface Schema { name: string; description: string; parameters: { type: string; properties: Record< string, { type: string; description: string; default?: unknown; } >; required: string[]; }; } interface JSDocParam { type: string; description: string; isOptional: boolean; defaultValue?: string; } const parseArguments = (argsString: string): string[] => { const args: string[] = []; let current = ""; let inQuotes = false; let quoteChar = ""; let depth = 0; for (let i = 0; i < argsString.length; i++) { const char = argsString[i]; if (!inQuotes && (char === '"' || char === "'")) { inQuotes = true; quoteChar = char; current += char; } else if (inQuotes && char === quoteChar) { inQuotes = false; quoteChar = ""; current += char; } else if (!inQuotes && char === "(") { depth++; current += char; } else if (!inQuotes && char === ")") { depth--; current += char; } else if (!inQuotes && char === "," && depth === 0) { args.push(current.trim()); current = ""; } else { current += char; } } if (current.trim()) { args.push(current.trim()); } return args; }; export const extractPythonicCalls = (toolCallContent: string): string[] => { try { const cleanContent = toolCallContent.trim(); // Try to parse as Granite format (JSON object with name and arguments) try { const parsed = JSON.parse(cleanContent); if (parsed && typeof parsed === 'object' && parsed.name) { // Convert Granite JSON format to Pythonic format const args = parsed.arguments || {}; const argPairs = Object.entries(args).map(([key, value]) => `${key}=${JSON.stringify(value)}` ); return [`${parsed.name}(${argPairs.join(', ')})`]; } if (Array.isArray(parsed)) { return parsed; } } catch { // Fallback to manual parsing } if (cleanContent.startsWith("[") && cleanContent.endsWith("]")) { const inner = cleanContent.slice(1, -1).trim(); if (!inner) return []; return parseArguments(inner).map((call) => call.trim().replace(/^['"]|['"]$/g, ""), ); } return [cleanContent]; } catch (error) { console.error("Error parsing tool calls:", error); return []; } }; export const parsePythonicCalls = (command: string): ParsedCall | null => { const callMatch = command.match(/^([a-zA-Z0-9_]+)\((.*)\)$/); if (!callMatch) return null; const [, name, argsStr] = callMatch; const args = parseArguments(argsStr); const positionalArgs: unknown[] = []; const keywordArgs: Record = {}; for (const arg of args) { const kwargMatch = arg.match(/^([a-zA-Z0-9_]+)\s*=\s*(.*)$/); if (kwargMatch) { const [, key, value] = kwargMatch; try { keywordArgs[key] = JSON.parse(value); } catch { keywordArgs[key] = value; } } else { try { positionalArgs.push(JSON.parse(arg)); } catch { positionalArgs.push(arg); } } } return { name, positionalArgs, keywordArgs }; }; export const extractFunctionAndRenderer = ( code: string, ): { functionCode: string; rendererCode?: string } => { if (typeof code !== "string") { return { functionCode: code }; } const exportMatch = code.match(/export\s+default\s+/); if (!exportMatch) { return { functionCode: code }; } const exportIndex = exportMatch.index!; const functionCode = code.substring(0, exportIndex).trim(); const rendererCode = code.substring(exportIndex).trim(); return { functionCode, rendererCode }; }; /** * Helper function to extract JSDoc parameters from JSDoc comments. */ const extractJSDocParams = ( jsdoc: string, ): Record => { const jsdocParams: Record = {}; const lines = jsdoc .split("\n") .map((line) => line.trim().replace(/^\*\s?/, "")); const paramRegex = /@param\s+\{([^}]+)\}\s+(\[?[a-zA-Z0-9_]+(?:=[^\]]+)?\]?|\S+)\s*-?\s*(.*)?/; for (const line of lines) { const paramMatch = line.match(paramRegex); if (paramMatch) { const [, type, namePart] = paramMatch; const description = paramMatch[3] || ""; let isOptional = false; let name = namePart; let jsdocDefault: string | undefined = undefined; if (name.startsWith("[") && name.endsWith("]")) { isOptional = true; name = name.slice(1, -1); } if (name.includes("=")) { const [n, def] = name.split("="); name = n.trim(); jsdocDefault = def.trim().replace(/['"]/g, ""); } jsdocParams[name] = { type: type.toLowerCase(), description: description.trim(), isOptional, defaultValue: undefined, jsdocDefault, }; } } return jsdocParams; }; /** * Helper function to extract function signature information. */ const extractFunctionSignature = ( functionCode: string, ): { name: string; params: { name: string; defaultValue?: string }[]; } | null => { const functionSignatureMatch = functionCode.match( /function\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/, ); if (!functionSignatureMatch) { return null; } const functionName = functionSignatureMatch[1]; const params = functionSignatureMatch[2] .split(",") .map((p) => p.trim()) .filter(Boolean) .map((p) => { const [name, defaultValue] = p.split("=").map((s) => s.trim()); return { name, defaultValue }; }); return { name: functionName, params }; }; export const generateSchemaFromCode = (code: string): Schema => { const { functionCode } = extractFunctionAndRenderer(code); if (typeof functionCode !== "string") { return { name: "invalid_code", description: "Code is not a valid string.", parameters: { type: "object", properties: {}, required: [] }, }; } // 1. Extract function signature, name, and parameter names directly from the code const signatureInfo = extractFunctionSignature(functionCode); if (!signatureInfo) { return { name: "invalid_function", description: "Could not parse function signature.", parameters: { type: "object", properties: {}, required: [] }, }; } const { name: functionName, params: paramsFromSignature } = signatureInfo; const schema: Schema = { name: functionName, description: "", parameters: { type: "object", properties: {}, required: [], }, }; // 2. Parse JSDoc comments to get descriptions and types const jsdocMatch = functionCode.match(/\/\*\*([\s\S]*?)\*\//); let jsdocParams: Record = {}; if (jsdocMatch) { const jsdoc = jsdocMatch[1]; jsdocParams = extractJSDocParams(jsdoc); const descriptionLines = jsdoc .split("\n") .map((line) => line.trim().replace(/^\*\s?/, "")) .filter((line) => !line.startsWith("@") && line); schema.description = descriptionLines.join(" ").trim(); } // 3. Combine signature parameters with JSDoc info for (const param of paramsFromSignature) { const paramName = param.name; const jsdocInfo = jsdocParams[paramName]; schema.parameters.properties[paramName] = { type: jsdocInfo ? jsdocInfo.type : "any", description: jsdocInfo ? jsdocInfo.description : "", }; // Prefer default from signature, then from JSDoc if (param.defaultValue !== undefined) { // Try to parse as JSON, fallback to string try { schema.parameters.properties[paramName].default = JSON.parse( param.defaultValue.replace(/'/g, '"'), ); } catch { schema.parameters.properties[paramName].default = param.defaultValue; } } else if (jsdocInfo && jsdocInfo.jsdocDefault !== undefined) { schema.parameters.properties[paramName].default = jsdocInfo.jsdocDefault; } // A parameter is required if: // - Not optional in JSDoc // - No default in signature // - No default in JSDoc const hasDefault = param.defaultValue !== undefined || (jsdocInfo && jsdocInfo.jsdocDefault !== undefined); if (!jsdocInfo || (!jsdocInfo.isOptional && !hasDefault)) { schema.parameters.required.push(paramName); } } return schema; }; /** * Extracts tool call content from a string using the tool call markers. * Supports both LFM2 format (<|tool_call_start|>...<|tool_call_end|>) * and Granite format (...) */ export const extractToolCallContent = (content: string): string | null => { // Try LFM2 format first const lfm2Match = content.match( /<\|tool_call_start\|>(.*?)<\|tool_call_end\|>/s, ); if (lfm2Match) { return lfm2Match[1].trim(); } // Try Granite format (XML-style) const graniteMatch = content.match(/(.*?)<\/tool_call>/s); if (graniteMatch) { return graniteMatch[1].trim(); } return null; }; /** * Maps positional and keyword arguments to named parameters based on schema. */ export const mapArgsToNamedParams = ( paramNames: string[], positionalArgs: unknown[], keywordArgs: Record, ): Record => { const namedParams: Record = Object.create(null); positionalArgs.forEach((arg, idx) => { if (idx < paramNames.length) { namedParams[paramNames[idx]] = arg; } }); Object.assign(namedParams, keywordArgs); return namedParams; }; export const getErrorMessage = (error: unknown): string => { if (error instanceof Error) { return error.message; } if (typeof error === "string") { return error; } if (error && typeof error === "object") { return JSON.stringify(error); } return String(error); };