| import { |
| ToolDefinition, |
| ToolCall, |
| ToolChoice, |
| ChatCompletionMessage, |
| FunctionDefinition, |
| } from "./types"; |
|
|
| export class ToolService { |
| |
| |
| |
| generateToolSystemPrompt( |
| tools: ToolDefinition[], |
| toolChoice: ToolChoice = "auto" |
| ): string { |
| const toolDescriptions = tools |
| .map((tool) => { |
| const func = tool.function; |
| let description = `${func.name}`; |
|
|
| if (func.description) { |
| description += `: ${func.description}`; |
| } |
|
|
| if (func.parameters) { |
| const params = func.parameters.properties || {}; |
| const required = func.parameters.required || []; |
|
|
| const paramDescriptions = Object.entries(params) |
| .map(([name, schema]: [string, any]) => { |
| const isRequired = required.includes(name); |
| const type = schema.type || "any"; |
| const desc = schema.description || ""; |
| return ` - ${name} (${type}${isRequired ? ", required" : ", optional"}): ${desc}`; |
| }) |
| .join("\n"); |
|
|
| if (paramDescriptions) { |
| description += `\nParameters:\n${paramDescriptions}`; |
| } |
| } |
|
|
| return description; |
| }) |
| .join("\n\n"); |
|
|
| let prompt = `You are an AI assistant with access to the following functions. When you need to call a function, respond with a JSON object in this exact format: |
| |
| { |
| "tool_calls": [ |
| { |
| "id": "call_<unique_id>", |
| "type": "function", |
| "function": { |
| "name": "<function_name>", |
| "arguments": "<json_string_of_arguments>" |
| } |
| } |
| ] |
| } |
| |
| Available functions: |
| ${toolDescriptions} |
| |
| Important rules: |
| 1. Only call functions when necessary to answer the user's question |
| 2. Use the exact function names provided |
| 3. Provide arguments as a JSON string |
| 4. Generate unique IDs for each tool call (e.g., call_1, call_2, etc.) |
| 5. If you don't need to call any functions, respond normally without the tool_calls format`; |
|
|
| if (toolChoice === "required") { |
| prompt += |
| "\n6. You MUST call at least one function to answer this request"; |
| } else if (toolChoice === "none") { |
| prompt += "\n6. Do NOT call any functions, respond normally"; |
| } else if ( |
| typeof toolChoice === "object" && |
| toolChoice.type === "function" |
| ) { |
| prompt += `\n6. You MUST call the function "${toolChoice.function.name}"`; |
| } |
|
|
| return prompt; |
| } |
|
|
| |
| |
| |
| detectFunctionCalls(content: string): boolean { |
| try { |
| const parsed = JSON.parse(content.trim()); |
| return ( |
| parsed.tool_calls && |
| Array.isArray(parsed.tool_calls) && |
| parsed.tool_calls.length > 0 |
| ); |
| } catch { |
| |
| return /["']?tool_calls["']?\s*:\s*\[/.test(content); |
| } |
| } |
|
|
| |
| |
| |
| extractFunctionCalls(content: string): ToolCall[] { |
| try { |
| |
| const parsed = JSON.parse(content.trim()); |
| if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) { |
| return parsed.tool_calls.map((call: any, index: number) => ({ |
| id: call.id || `call_${Date.now()}_${index}`, |
| type: "function", |
| function: { |
| name: call.function.name, |
| arguments: |
| typeof call.function.arguments === "string" |
| ? call.function.arguments |
| : JSON.stringify(call.function.arguments), |
| }, |
| })); |
| } |
| } catch { |
| |
| const toolCallsMatch = content.match( |
| /["']?tool_calls["']?\s*:\s*\[(.*?)\]/s |
| ); |
| if (toolCallsMatch) { |
| try { |
| const toolCallsStr = `[${toolCallsMatch[1]}]`; |
| const toolCalls = JSON.parse(toolCallsStr); |
| return toolCalls.map((call: any, index: number) => ({ |
| id: call.id || `call_${Date.now()}_${index}`, |
| type: "function", |
| function: { |
| name: call.function.name, |
| arguments: |
| typeof call.function.arguments === "string" |
| ? call.function.arguments |
| : JSON.stringify(call.function.arguments), |
| }, |
| })); |
| } catch { |
| |
| return this.extractFunctionCallsFromText(content); |
| } |
| } |
| } |
|
|
| return []; |
| } |
|
|
| |
| |
| |
| private extractFunctionCallsFromText(content: string): ToolCall[] { |
| const calls: ToolCall[] = []; |
|
|
| |
| const functionPattern = |
| /["']?function["']?\s*:\s*\{[^}]*["']?name["']?\s*:\s*["']([^"']+)["'][^}]*["']?arguments["']?\s*:\s*["']([^"']*)["']/g; |
| let match; |
| let index = 0; |
|
|
| while ((match = functionPattern.exec(content)) !== null) { |
| calls.push({ |
| id: `call_${Date.now()}_${index}`, |
| type: "function", |
| function: { |
| name: match[1], |
| arguments: match[2], |
| }, |
| }); |
| index++; |
| } |
|
|
| return calls; |
| } |
|
|
| |
| |
| |
| async executeFunctionCall( |
| toolCall: ToolCall, |
| availableFunctions: Record<string, Function> |
| ): Promise<string> { |
| const functionName = toolCall.function.name; |
| const functionToCall = availableFunctions[functionName]; |
|
|
| if (!functionToCall) { |
| return JSON.stringify({ |
| error: `Function '${functionName}' not found`, |
| available_functions: Object.keys(availableFunctions), |
| }); |
| } |
|
|
| try { |
| const args = JSON.parse(toolCall.function.arguments); |
| const result = await functionToCall(args); |
| return typeof result === "string" ? result : JSON.stringify(result); |
| } catch (error) { |
| return JSON.stringify({ |
| error: `Error executing function '${functionName}': ${error instanceof Error ? error.message : "Unknown error"}`, |
| arguments_received: toolCall.function.arguments, |
| }); |
| } |
| } |
|
|
| |
| |
| |
| createToolResultMessage( |
| toolCallId: string, |
| result: string |
| ): ChatCompletionMessage { |
| return { |
| role: "tool", |
| content: result, |
| tool_call_id: toolCallId, |
| }; |
| } |
|
|
| |
| |
| |
| validateTools(tools: ToolDefinition[]): { valid: boolean; errors: string[] } { |
| const errors: string[] = []; |
|
|
| if (!Array.isArray(tools)) { |
| errors.push("Tools must be an array"); |
| return { valid: false, errors }; |
| } |
|
|
| tools.forEach((tool, index) => { |
| if (!tool.type || tool.type !== "function") { |
| errors.push(`Tool at index ${index}: type must be "function"`); |
| } |
|
|
| if (!tool.function) { |
| errors.push(`Tool at index ${index}: function definition is required`); |
| return; |
| } |
|
|
| if (!tool.function.name || typeof tool.function.name !== "string") { |
| errors.push( |
| `Tool at index ${index}: function name is required and must be a string` |
| ); |
| } |
|
|
| if (tool.function.parameters) { |
| if (tool.function.parameters.type !== "object") { |
| errors.push( |
| `Tool at index ${index}: function parameters type must be "object"` |
| ); |
| } |
| } |
| }); |
|
|
| return { valid: errors.length === 0, errors }; |
| } |
|
|
| |
| |
| |
| shouldUseFunctionCalling( |
| tools?: ToolDefinition[], |
| toolChoice?: ToolChoice |
| ): boolean { |
| if (!tools || tools.length === 0) { |
| return false; |
| } |
|
|
| if (toolChoice === "none") { |
| return false; |
| } |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| generateToolCallId(): string { |
| return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
| } |
| } |
|
|