| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| |
| |
|
|
| function describeParams(schema) {
|
| if (!schema || !schema.properties) return ' (no parameters)';
|
| const required = new Set(schema.required || []);
|
| const lines = [];
|
| for (const [name, prop] of Object.entries(schema.properties)) {
|
| const req = required.has(name) ? ' (required)' : ' (optional)';
|
| const type = prop.type || 'any';
|
| const desc = prop.description ? ` - ${prop.description}` : '';
|
| const enumStr = prop.enum ? ` [enum: ${prop.enum.join(', ')}]` : '';
|
| lines.push(` - ${name}: ${type}${req}${desc}${enumStr}`);
|
| }
|
| return lines.join('\n');
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| export function serializeTools(tools, toolChoice) {
|
| if (!tools || tools.length === 0) return '';
|
|
|
|
|
| if (toolChoice === 'none') return '';
|
|
|
|
|
| const exampleTool = tools[0]?.function || tools[0] || { name: 'example_func' };
|
| const exampleArgs = {};
|
| const exampleParams = (exampleTool.parameters || {}).properties || {};
|
| for (const [k, v] of Object.entries(exampleParams)) {
|
| if (v.type === 'number' || v.type === 'integer') exampleArgs[k] = 0;
|
| else if (v.type === 'boolean') exampleArgs[k] = true;
|
| else if (v.type === 'array') exampleArgs[k] = [];
|
| else if (v.type === 'object') exampleArgs[k] = {};
|
| else exampleArgs[k] = 'value';
|
| if (Object.keys(exampleArgs).length >= 2) break;
|
| }
|
|
|
| let prompt = `\n\n---\nYou have access to the following tools/functions. When you need to call a tool, you MUST respond ONLY with a JSON block wrapped in \`\`\`tool_calls\`\`\` markers.
|
|
|
| FORMAT (follow this EXACTLY):
|
| \`\`\`tool_calls
|
| [{"name": "${exampleTool.name}", "arguments": ${JSON.stringify(exampleArgs)}}]
|
| \`\`\`
|
|
|
| CRITICAL RULES:
|
| 1. The response must contain ONLY the \`\`\`tool_calls\`\`\` block when calling tools — no explanation, no extra text before or after.
|
| 2. "arguments" must be a valid JSON object matching the function's parameter schema.
|
| 3. The JSON array can contain one or multiple tool calls: [{"name": "func1", "arguments": {...}}, {"name": "func2", "arguments": {...}}]
|
| 4. If you do NOT need to call any tool, respond normally with plain text (no \`\`\`tool_calls\`\`\` block).
|
|
|
| Available tools:\n`;
|
|
|
| for (const tool of tools) {
|
| const fn = tool.function || tool;
|
| prompt += `\n### ${fn.name}\n`;
|
| if (fn.description) prompt += `${fn.description}\n`;
|
| prompt += `Parameters:\n${describeParams(fn.parameters)}\n`;
|
| }
|
|
|
|
|
| if (toolChoice === 'required') {
|
| prompt += `\nIMPORTANT: You MUST call at least one tool. Always respond with a tool_calls block.\n`;
|
| } else if (typeof toolChoice === 'object' && toolChoice?.function?.name) {
|
| prompt += `\nIMPORTANT: You MUST call the tool "${toolChoice.function.name}". Always respond with a tool_calls block containing this tool.\n`;
|
| }
|
|
|
| prompt += '---\n';
|
| return prompt;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| export function serializeToolsAnthropic(tools, toolChoice) {
|
| if (!tools || tools.length === 0) return '';
|
|
|
|
|
| const openaiTools = tools.map(t => ({
|
| type: 'function',
|
| function: {
|
| name: t.name,
|
| description: t.description || '',
|
| parameters: t.input_schema || {},
|
| },
|
| }));
|
|
|
|
|
| let choice = 'auto';
|
| if (toolChoice) {
|
| if (toolChoice.type === 'any') choice = 'required';
|
| else if (toolChoice.type === 'tool') choice = { function: { name: toolChoice.name } };
|
| else if (toolChoice.type === 'auto') choice = 'auto';
|
| }
|
|
|
| return serializeTools(openaiTools, choice);
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| export function parseToolCalls(text) {
|
| if (!text) return { hasToolCalls: false, toolCalls: [], textContent: text || '' };
|
|
|
|
|
| const blockRegex = /```tool_calls\s*\n?([\s\S]*?)```/;
|
| const match = text.match(blockRegex);
|
|
|
| if (match) {
|
| try {
|
| let parsed = JSON.parse(match[1].trim());
|
|
|
| if (!Array.isArray(parsed)) parsed = [parsed];
|
|
|
| const toolCalls = parsed
|
| .filter(tc => tc && tc.name)
|
| .map(tc => ({
|
| name: tc.name,
|
| arguments: tc.arguments || {},
|
| }));
|
|
|
| if (toolCalls.length > 0) {
|
|
|
| const textContent = text.replace(blockRegex, '').trim();
|
| return { hasToolCalls: true, toolCalls, textContent };
|
| }
|
| } catch {}
|
| }
|
|
|
|
|
| const jsonBlockRegex = /```(?:json)?\s*\n?(\[\s*\{[\s\S]*?"name"[\s\S]*?\}\s*\])\s*```/;
|
| const jsonMatch = text.match(jsonBlockRegex);
|
|
|
| if (jsonMatch) {
|
| const result = tryParseToolArray(jsonMatch[1], text, jsonBlockRegex);
|
| if (result) return result;
|
| }
|
|
|
|
|
|
|
| const tailText = text.slice(-2000);
|
| const nakedJsonRegex = /(\[\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:[\s\S]*?\}\s*\])\s*$/;
|
| const nakedMatch = tailText.match(nakedJsonRegex);
|
|
|
| if (nakedMatch) {
|
| const result = tryParseToolArray(nakedMatch[1], text, new RegExp(nakedMatch[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&').slice(0, 100) + '[\\s\\S]*'));
|
| if (result) return result;
|
| }
|
|
|
| return { hasToolCalls: false, toolCalls: [], textContent: text };
|
| }
|
|
|
| |
| |
|
|
| function tryParseToolArray(jsonStr, fullText, removeRegex) {
|
| try {
|
| let parsed = JSON.parse(jsonStr.trim());
|
| if (!Array.isArray(parsed)) parsed = [parsed];
|
| const toolCalls = parsed
|
| .filter(tc => tc && tc.name)
|
| .map(tc => ({
|
| name: tc.name,
|
| arguments: tc.arguments || {},
|
| }));
|
| if (toolCalls.length > 0) {
|
| const textContent = fullText.replace(removeRegex, '').trim();
|
| return { hasToolCalls: true, toolCalls, textContent };
|
| }
|
| } catch {}
|
| return null;
|
| }
|
|
|
| |
| |
|
|
| export function toOpenAIToolCalls(toolCalls) {
|
| return toolCalls.map((tc, i) => ({
|
| id: `call_${Date.now().toString(36)}_${i}`,
|
| type: 'function',
|
| function: {
|
| name: tc.name,
|
| arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments),
|
| },
|
| }));
|
| }
|
|
|
| |
| |
|
|
| export function toAnthropicToolUse(toolCalls) {
|
| return toolCalls.map((tc, i) => ({
|
| type: 'tool_use',
|
| id: `toolu_${Date.now().toString(36)}_${i}`,
|
| name: tc.name,
|
| input: typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : tc.arguments,
|
| }));
|
| }
|
|
|
| |
| |
| |
|
|
| export function detectToolCallStart(accumulatedText) {
|
|
|
| return accumulatedText.includes('```tool_calls');
|
| }
|
|
|
| |
| |
|
|
| export function detectToolCallEnd(accumulatedText) {
|
| const start = accumulatedText.indexOf('```tool_calls');
|
| if (start < 0) return false;
|
| const afterStart = accumulatedText.substring(start + '```tool_calls'.length);
|
| return afterStart.includes('```');
|
| }
|
|
|