Spaces:
Running
Running
| interface ParsedCall { | |
| name: string; | |
| positionalArgs: unknown[]; | |
| keywordArgs: Record<string, unknown>; | |
| } | |
| 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<string, unknown> = {}; | |
| 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<string, JSDocParam & { jsdocDefault?: string }> => { | |
| const jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> = | |
| {}; | |
| 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<string, JSDocParam & { jsdocDefault?: string }> = {}; | |
| 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 (<tool_call>...</tool_call>) | |
| */ | |
| 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>(.*?)<\/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<string, unknown>, | |
| ): Record<string, unknown> => { | |
| const namedParams: Record<string, unknown> = 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); | |
| }; | |