@librechat/agents 3.0.43 → 3.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +99 -13
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +99 -14
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +18 -5
- package/dist/types/types/tools.d.ts +2 -0
- package/package.json +1 -1
- package/src/tools/ProgrammaticToolCalling.ts +118 -15
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +70 -5
- package/src/types/tools.ts +2 -0
package/dist/cjs/main.cjs
CHANGED
|
@@ -84,6 +84,7 @@ exports.extractUsedToolNames = ProgrammaticToolCalling.extractUsedToolNames;
|
|
|
84
84
|
exports.filterToolsByUsage = ProgrammaticToolCalling.filterToolsByUsage;
|
|
85
85
|
exports.formatCompletedResponse = ProgrammaticToolCalling.formatCompletedResponse;
|
|
86
86
|
exports.makeRequest = ProgrammaticToolCalling.makeRequest;
|
|
87
|
+
exports.normalizeToPythonIdentifier = ProgrammaticToolCalling.normalizeToPythonIdentifier;
|
|
87
88
|
exports.countNestedGroups = ToolSearchRegex.countNestedGroups;
|
|
88
89
|
exports.createToolSearchRegexTool = ToolSearchRegex.createToolSearchRegexTool;
|
|
89
90
|
exports.escapeRegexSpecialChars = ToolSearchRegex.escapeRegexSpecialChars;
|
package/dist/cjs/main.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -81,34 +81,111 @@ Requirements:
|
|
|
81
81
|
// ============================================================================
|
|
82
82
|
// Helper Functions
|
|
83
83
|
// ============================================================================
|
|
84
|
+
/** Python reserved keywords that get `_tool` suffix in Code API */
|
|
85
|
+
const PYTHON_KEYWORDS = new Set([
|
|
86
|
+
'False',
|
|
87
|
+
'None',
|
|
88
|
+
'True',
|
|
89
|
+
'and',
|
|
90
|
+
'as',
|
|
91
|
+
'assert',
|
|
92
|
+
'async',
|
|
93
|
+
'await',
|
|
94
|
+
'break',
|
|
95
|
+
'class',
|
|
96
|
+
'continue',
|
|
97
|
+
'def',
|
|
98
|
+
'del',
|
|
99
|
+
'elif',
|
|
100
|
+
'else',
|
|
101
|
+
'except',
|
|
102
|
+
'finally',
|
|
103
|
+
'for',
|
|
104
|
+
'from',
|
|
105
|
+
'global',
|
|
106
|
+
'if',
|
|
107
|
+
'import',
|
|
108
|
+
'in',
|
|
109
|
+
'is',
|
|
110
|
+
'lambda',
|
|
111
|
+
'nonlocal',
|
|
112
|
+
'not',
|
|
113
|
+
'or',
|
|
114
|
+
'pass',
|
|
115
|
+
'raise',
|
|
116
|
+
'return',
|
|
117
|
+
'try',
|
|
118
|
+
'while',
|
|
119
|
+
'with',
|
|
120
|
+
'yield',
|
|
121
|
+
]);
|
|
122
|
+
/**
|
|
123
|
+
* Normalizes a tool name to Python identifier format.
|
|
124
|
+
* Must match the Code API's `normalizePythonFunctionName` exactly:
|
|
125
|
+
* 1. Replace hyphens and spaces with underscores
|
|
126
|
+
* 2. Remove any other invalid characters
|
|
127
|
+
* 3. Prefix with underscore if starts with number
|
|
128
|
+
* 4. Append `_tool` if it's a Python keyword
|
|
129
|
+
* @param name - The tool name to normalize
|
|
130
|
+
* @returns Normalized Python-safe identifier
|
|
131
|
+
*/
|
|
132
|
+
function normalizeToPythonIdentifier(name) {
|
|
133
|
+
let normalized = name.replace(/[-\s]/g, '_');
|
|
134
|
+
normalized = normalized.replace(/[^a-zA-Z0-9_]/g, '');
|
|
135
|
+
if (/^[0-9]/.test(normalized)) {
|
|
136
|
+
normalized = '_' + normalized;
|
|
137
|
+
}
|
|
138
|
+
if (PYTHON_KEYWORDS.has(normalized)) {
|
|
139
|
+
normalized = normalized + '_tool';
|
|
140
|
+
}
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
84
143
|
/**
|
|
85
144
|
* Extracts tool names that are actually called in the Python code.
|
|
86
|
-
*
|
|
145
|
+
* Handles hyphen/underscore conversion since Python identifiers use underscores.
|
|
87
146
|
* @param code - The Python code to analyze
|
|
88
|
-
* @param
|
|
89
|
-
* @returns Set of tool names found in the code
|
|
147
|
+
* @param toolNameMap - Map from normalized Python name to original tool name
|
|
148
|
+
* @returns Set of original tool names found in the code
|
|
90
149
|
*/
|
|
91
|
-
function extractUsedToolNames(code,
|
|
150
|
+
function extractUsedToolNames(code, toolNameMap) {
|
|
92
151
|
const usedTools = new Set();
|
|
93
|
-
for (const
|
|
94
|
-
const escapedName =
|
|
152
|
+
for (const [pythonName, originalName] of toolNameMap) {
|
|
153
|
+
const escapedName = pythonName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
154
|
const pattern = new RegExp(`\\b${escapedName}\\s*\\(`, 'g');
|
|
96
155
|
if (pattern.test(code)) {
|
|
97
|
-
usedTools.add(
|
|
156
|
+
usedTools.add(originalName);
|
|
98
157
|
}
|
|
99
158
|
}
|
|
100
159
|
return usedTools;
|
|
101
160
|
}
|
|
102
161
|
/**
|
|
103
162
|
* Filters tool definitions to only include tools actually used in the code.
|
|
163
|
+
* Handles the hyphen-to-underscore conversion for Python compatibility.
|
|
104
164
|
* @param toolDefs - All available tool definitions
|
|
105
165
|
* @param code - The Python code to analyze
|
|
166
|
+
* @param debug - Enable debug logging
|
|
106
167
|
* @returns Filtered array of tool definitions
|
|
107
168
|
*/
|
|
108
|
-
function filterToolsByUsage(toolDefs, code) {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
169
|
+
function filterToolsByUsage(toolDefs, code, debug = false) {
|
|
170
|
+
const toolNameMap = new Map();
|
|
171
|
+
for (const tool of toolDefs) {
|
|
172
|
+
const pythonName = normalizeToPythonIdentifier(tool.name);
|
|
173
|
+
toolNameMap.set(pythonName, tool.name);
|
|
174
|
+
}
|
|
175
|
+
const usedToolNames = extractUsedToolNames(code, toolNameMap);
|
|
176
|
+
if (debug) {
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.log(`[PTC Debug] Tool filtering: found ${usedToolNames.size}/${toolDefs.length} tools in code`);
|
|
179
|
+
if (usedToolNames.size > 0) {
|
|
180
|
+
// eslint-disable-next-line no-console
|
|
181
|
+
console.log(`[PTC Debug] Matched tools: ${Array.from(usedToolNames).join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
111
184
|
if (usedToolNames.size === 0) {
|
|
185
|
+
if (debug) {
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.log('[PTC Debug] No tools detected in code - sending all tools as fallback');
|
|
188
|
+
}
|
|
112
189
|
return toolDefs;
|
|
113
190
|
}
|
|
114
191
|
return toolDefs.filter((tool) => usedToolNames.has(tool.name));
|
|
@@ -254,6 +331,7 @@ function createProgrammaticToolCallingTool(initParams = {}) {
|
|
|
254
331
|
const baseUrl = initParams.baseUrl ?? CodeExecutor.getCodeBaseURL();
|
|
255
332
|
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
|
|
256
333
|
const proxy = initParams.proxy ?? process.env.PROXY;
|
|
334
|
+
const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
|
|
257
335
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
258
336
|
const description = `
|
|
259
337
|
Run tools by writing Python code. Tools are available as async functions - just call them with await.
|
|
@@ -293,7 +371,12 @@ When to use this instead of calling tools directly:
|
|
|
293
371
|
// ====================================================================
|
|
294
372
|
// Phase 1: Filter tools and make initial request
|
|
295
373
|
// ====================================================================
|
|
296
|
-
const effectiveTools = filterToolsByUsage(toolDefs, code);
|
|
374
|
+
const effectiveTools = filterToolsByUsage(toolDefs, code, debug);
|
|
375
|
+
if (debug) {
|
|
376
|
+
// eslint-disable-next-line no-console
|
|
377
|
+
console.log(`[PTC Debug] Sending ${effectiveTools.length} tools to API ` +
|
|
378
|
+
`(filtered from ${toolDefs.length})`);
|
|
379
|
+
}
|
|
297
380
|
let response = await makeRequest(EXEC_ENDPOINT, apiKey, {
|
|
298
381
|
code,
|
|
299
382
|
tools: effectiveTools,
|
|
@@ -310,8 +393,10 @@ When to use this instead of calling tools directly:
|
|
|
310
393
|
'This may indicate an infinite loop, excessive tool calls, ' +
|
|
311
394
|
'or a logic error in your code.');
|
|
312
395
|
}
|
|
313
|
-
|
|
314
|
-
|
|
396
|
+
if (debug) {
|
|
397
|
+
// eslint-disable-next-line no-console
|
|
398
|
+
console.log(`[PTC Debug] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`);
|
|
399
|
+
}
|
|
315
400
|
const toolResults = await executeTools(response.tool_calls ?? [], toolMap);
|
|
316
401
|
response = await makeRequest(EXEC_ENDPOINT, apiKey, {
|
|
317
402
|
continuation_token: response.continuation_token,
|
|
@@ -349,4 +434,5 @@ exports.extractUsedToolNames = extractUsedToolNames;
|
|
|
349
434
|
exports.filterToolsByUsage = filterToolsByUsage;
|
|
350
435
|
exports.formatCompletedResponse = formatCompletedResponse;
|
|
351
436
|
exports.makeRequest = makeRequest;
|
|
437
|
+
exports.normalizeToPythonIdentifier = normalizeToPythonIdentifier;
|
|
352
438
|
//# sourceMappingURL=ProgrammaticToolCalling.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgrammaticToolCalling.cjs","sources":["../../../src/tools/ProgrammaticToolCalling.ts"],"sourcesContent":["// src/tools/ProgrammaticToolCalling.ts\nimport { z } from 'zod';\nimport { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { getEnvironmentVariable } from '@langchain/core/utils/env';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type { ToolCall } from '@langchain/core/messages/tool';\nimport type * as t from '@/types';\nimport { imageExtRegex, getCodeBaseURL } from './CodeExecutor';\nimport { EnvVar, Constants } from '@/common';\n\nconfig();\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst imageMessage = 'Image is already displayed to the user';\nconst otherMessage = 'File is already downloaded by the user';\nconst accessMessage =\n 'Note: Files are READ-ONLY. Save changes to NEW filenames. To access these files in future executions, provide the `session_id` as a parameter (not in your code).';\nconst emptyOutputMessage =\n 'stdout: Empty. Ensure you\\'re writing output explicitly.\\n';\n\n/** Default max round-trips to prevent infinite loops */\nconst DEFAULT_MAX_ROUND_TRIPS = 20;\n\n/** Default execution timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 60000;\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst ProgrammaticToolCallingSchema = z.object({\n code: z\n .string()\n .min(1)\n .describe(\n `Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.\n\nThe Code API generates async function stubs from the tool definitions. Just call them directly:\n\nExample (Simple call):\n result = await get_weather(city=\"San Francisco\")\n print(result)\n\nExample (Parallel - Fastest):\n results = await asyncio.gather(\n get_weather(city=\"SF\"),\n get_weather(city=\"NYC\"),\n get_weather(city=\"London\")\n )\n for city, weather in zip([\"SF\", \"NYC\", \"London\"], results):\n print(f\"{city}: {weather['temperature']}°F\")\n\nExample (Loop with processing):\n team = await get_team_members()\n for member in team:\n expenses = await get_expenses(user_id=member['id'])\n total = sum(e['amount'] for e in expenses)\n print(f\"{member['name']}: \\${total:.2f}\")\n\nExample (Conditional logic):\n data = await fetch_data(source=\"primary\")\n if not data:\n data = await fetch_data(source=\"backup\")\n print(f\"Got {len(data)} records\")\n\nRequirements:\n- Tools are pre-defined as async functions - DO NOT write function definitions\n- Use await for all tool calls\n- Use asyncio.gather() for parallel execution of independent calls\n- Only print() output flows back to the context window\n- Tool results from programmatic calls do NOT consume context tokens`\n ),\n session_id: z\n .string()\n .optional()\n .describe(\n 'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'\n ),\n timeout: z\n .number()\n .int()\n .min(1000)\n .max(300000)\n .optional()\n .default(DEFAULT_TIMEOUT)\n .describe(\n 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'\n ),\n});\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Extracts tool names that are actually called in the Python code.\n * Matches patterns like `await tool_name(`, `tool_name(`, and asyncio.gather calls.\n * @param code - The Python code to analyze\n * @param availableToolNames - Set of available tool names to match against\n * @returns Set of tool names found in the code\n */\nexport function extractUsedToolNames(\n code: string,\n availableToolNames: Set<string>\n): Set<string> {\n const usedTools = new Set<string>();\n\n for (const toolName of availableToolNames) {\n const escapedName = toolName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = new RegExp(`\\\\b${escapedName}\\\\s*\\\\(`, 'g');\n\n if (pattern.test(code)) {\n usedTools.add(toolName);\n }\n }\n\n return usedTools;\n}\n\n/**\n * Filters tool definitions to only include tools actually used in the code.\n * @param toolDefs - All available tool definitions\n * @param code - The Python code to analyze\n * @returns Filtered array of tool definitions\n */\nexport function filterToolsByUsage(\n toolDefs: t.LCTool[],\n code: string\n): t.LCTool[] {\n const availableToolNames = new Set(toolDefs.map((tool) => tool.name));\n const usedToolNames = extractUsedToolNames(code, availableToolNames);\n\n if (usedToolNames.size === 0) {\n return toolDefs;\n }\n\n return toolDefs.filter((tool) => usedToolNames.has(tool.name));\n}\n\n/**\n * Makes an HTTP request to the Code API.\n * @param endpoint - The API endpoint URL\n * @param apiKey - The API key for authentication\n * @param body - The request body\n * @param proxy - Optional HTTP proxy URL\n * @returns The parsed API response\n */\nexport async function makeRequest(\n endpoint: string,\n apiKey: string,\n body: Record<string, unknown>,\n proxy?: string\n): Promise<t.ProgrammaticExecutionResponse> {\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify(body),\n };\n\n if (proxy != null && proxy !== '') {\n fetchOptions.agent = new HttpsProxyAgent(proxy);\n }\n\n const response = await fetch(endpoint, fetchOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `HTTP error! status: ${response.status}, body: ${errorText}`\n );\n }\n\n return (await response.json()) as t.ProgrammaticExecutionResponse;\n}\n\n/**\n * Executes tools in parallel when requested by the API.\n * Uses Promise.all for parallel execution, catching individual errors.\n * @param toolCalls - Array of tool calls from the API\n * @param toolMap - Map of tool names to executable tools\n * @returns Array of tool results\n */\nexport async function executeTools(\n toolCalls: t.PTCToolCall[],\n toolMap: t.ToolMap\n): Promise<t.PTCToolResult[]> {\n const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {\n const tool = toolMap.get(call.name);\n\n if (!tool) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,\n };\n }\n\n try {\n const result = await tool.invoke(call.input, {\n metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },\n });\n return {\n call_id: call.id,\n result,\n is_error: false,\n };\n } catch (error) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: (error as Error).message || 'Tool execution failed',\n };\n }\n });\n\n return await Promise.all(executions);\n}\n\n/**\n * Formats the completed response for the agent.\n * @param response - The completed API response\n * @returns Tuple of [formatted string, artifact]\n */\nexport function formatCompletedResponse(\n response: t.ProgrammaticExecutionResponse\n): [string, t.ProgrammaticExecutionArtifact] {\n let formatted = '';\n\n if (response.stdout != null && response.stdout !== '') {\n formatted += `stdout:\\n${response.stdout}\\n`;\n } else {\n formatted += emptyOutputMessage;\n }\n\n if (response.stderr != null && response.stderr !== '') {\n formatted += `stderr:\\n${response.stderr}\\n`;\n }\n\n if (response.files && response.files.length > 0) {\n formatted += 'Generated files:\\n';\n\n const fileCount = response.files.length;\n for (let i = 0; i < fileCount; i++) {\n const file = response.files[i];\n const isImage = imageExtRegex.test(file.name);\n formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;\n\n if (i < fileCount - 1) {\n formatted += fileCount <= 3 ? ', ' : ',\\n';\n }\n }\n\n formatted += `\\nsession_id: ${response.session_id}\\n\\n${accessMessage}`;\n }\n\n return [\n formatted.trim(),\n {\n session_id: response.session_id,\n files: response.files,\n },\n ];\n}\n\n// ============================================================================\n// Tool Factory\n// ============================================================================\n\n/**\n * Creates a Programmatic Tool Calling tool for complex multi-tool workflows.\n *\n * This tool enables AI agents to write Python code that orchestrates multiple\n * tool calls programmatically, reducing LLM round-trips and token usage.\n *\n * The tool map must be provided at runtime via config.configurable.toolMap.\n *\n * @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)\n * @returns A LangChain DynamicStructuredTool for programmatic tool calling\n *\n * @example\n * const ptcTool = createProgrammaticToolCallingTool({\n * apiKey: process.env.CODE_API_KEY,\n * maxRoundTrips: 20\n * });\n *\n * const [output, artifact] = await ptcTool.invoke(\n * { code, tools },\n * { configurable: { toolMap } }\n * );\n */\nexport function createProgrammaticToolCallingTool(\n initParams: t.ProgrammaticToolCallingParams = {}\n): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {\n const apiKey =\n (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??\n initParams.apiKey ??\n getEnvironmentVariable(EnvVar.CODE_API_KEY) ??\n '';\n\n if (!apiKey) {\n throw new Error(\n 'No API key provided for programmatic tool calling. ' +\n 'Set CODE_API_KEY environment variable or pass apiKey in initParams.'\n );\n }\n\n const baseUrl = initParams.baseUrl ?? getCodeBaseURL();\n const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;\n const proxy = initParams.proxy ?? process.env.PROXY;\n const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;\n\n const description = `\nRun tools by writing Python code. Tools are available as async functions - just call them with await.\n\nThis is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.\n\nUsage:\n- Tools are pre-defined as async functions - call them with await\n- Use asyncio.gather() to run multiple tools in parallel\n- Only print() output is returned - tool results stay in Python\n\nExamples:\n- Simple: result = await get_weather(city=\"NYC\")\n- Loop: for user in users: data = await get_expenses(user_id=user['id'])\n- Parallel: sf, ny = await asyncio.gather(get_weather(city=\"SF\"), get_weather(city=\"NY\"))\n\nWhen to use this instead of calling tools directly:\n- You need to call tools in a loop (process many items)\n- You want parallel execution (asyncio.gather)\n- You need conditionals based on tool results\n- You want to aggregate/filter data before returning\n`.trim();\n\n return tool<typeof ProgrammaticToolCallingSchema>(\n async (params, config) => {\n const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;\n\n // Extra params injected by ToolNode (follows web_search pattern)\n const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &\n Partial<t.ProgrammaticCache>;\n\n if (toolMap == null || toolMap.size === 0) {\n throw new Error(\n 'No toolMap provided. ' +\n 'ToolNode should inject this from AgentContext when invoked through the graph.'\n );\n }\n\n if (toolDefs == null || toolDefs.length === 0) {\n throw new Error(\n 'No tool definitions provided. ' +\n 'Either pass tools in the input or ensure ToolNode injects toolDefs.'\n );\n }\n\n let roundTrip = 0;\n\n try {\n // ====================================================================\n // Phase 1: Filter tools and make initial request\n // ====================================================================\n\n const effectiveTools = filterToolsByUsage(toolDefs, code);\n\n let response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n code,\n tools: effectiveTools,\n session_id,\n timeout,\n },\n proxy\n );\n\n // ====================================================================\n // Phase 2: Handle response loop\n // ====================================================================\n\n while (response.status === 'tool_call_required') {\n roundTrip++;\n\n if (roundTrip > maxRoundTrips) {\n throw new Error(\n `Exceeded maximum round trips (${maxRoundTrips}). ` +\n 'This may indicate an infinite loop, excessive tool calls, ' +\n 'or a logic error in your code.'\n );\n }\n\n // eslint-disable-next-line no-console\n console.log(\n `[PTC] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`\n );\n\n const toolResults = await executeTools(\n response.tool_calls ?? [],\n toolMap\n );\n\n response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n continuation_token: response.continuation_token,\n tool_results: toolResults,\n },\n proxy\n );\n }\n\n // ====================================================================\n // Phase 3: Handle final state\n // ====================================================================\n\n if (response.status === 'completed') {\n return formatCompletedResponse(response);\n }\n\n if (response.status === 'error') {\n throw new Error(\n `Execution error: ${response.error}` +\n (response.stderr != null && response.stderr !== ''\n ? `\\n\\nStderr:\\n${response.stderr}`\n : '')\n );\n }\n\n throw new Error(`Unexpected response status: ${response.status}`);\n } catch (error) {\n throw new Error(\n `Programmatic execution failed: ${(error as Error).message}`\n );\n }\n },\n {\n name: Constants.PROGRAMMATIC_TOOL_CALLING,\n description,\n schema: ProgrammaticToolCallingSchema,\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n"],"names":["config","z","HttpsProxyAgent","Constants","imageExtRegex","EnvVar","getEnvironmentVariable","getCodeBaseURL","tool"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProgrammaticToolCalling.cjs","sources":["../../../src/tools/ProgrammaticToolCalling.ts"],"sourcesContent":["// src/tools/ProgrammaticToolCalling.ts\nimport { z } from 'zod';\nimport { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { getEnvironmentVariable } from '@langchain/core/utils/env';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type { ToolCall } from '@langchain/core/messages/tool';\nimport type * as t from '@/types';\nimport { imageExtRegex, getCodeBaseURL } from './CodeExecutor';\nimport { EnvVar, Constants } from '@/common';\n\nconfig();\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst imageMessage = 'Image is already displayed to the user';\nconst otherMessage = 'File is already downloaded by the user';\nconst accessMessage =\n 'Note: Files are READ-ONLY. Save changes to NEW filenames. To access these files in future executions, provide the `session_id` as a parameter (not in your code).';\nconst emptyOutputMessage =\n 'stdout: Empty. Ensure you\\'re writing output explicitly.\\n';\n\n/** Default max round-trips to prevent infinite loops */\nconst DEFAULT_MAX_ROUND_TRIPS = 20;\n\n/** Default execution timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 60000;\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst ProgrammaticToolCallingSchema = z.object({\n code: z\n .string()\n .min(1)\n .describe(\n `Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.\n\nThe Code API generates async function stubs from the tool definitions. Just call them directly:\n\nExample (Simple call):\n result = await get_weather(city=\"San Francisco\")\n print(result)\n\nExample (Parallel - Fastest):\n results = await asyncio.gather(\n get_weather(city=\"SF\"),\n get_weather(city=\"NYC\"),\n get_weather(city=\"London\")\n )\n for city, weather in zip([\"SF\", \"NYC\", \"London\"], results):\n print(f\"{city}: {weather['temperature']}°F\")\n\nExample (Loop with processing):\n team = await get_team_members()\n for member in team:\n expenses = await get_expenses(user_id=member['id'])\n total = sum(e['amount'] for e in expenses)\n print(f\"{member['name']}: \\${total:.2f}\")\n\nExample (Conditional logic):\n data = await fetch_data(source=\"primary\")\n if not data:\n data = await fetch_data(source=\"backup\")\n print(f\"Got {len(data)} records\")\n\nRequirements:\n- Tools are pre-defined as async functions - DO NOT write function definitions\n- Use await for all tool calls\n- Use asyncio.gather() for parallel execution of independent calls\n- Only print() output flows back to the context window\n- Tool results from programmatic calls do NOT consume context tokens`\n ),\n session_id: z\n .string()\n .optional()\n .describe(\n 'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'\n ),\n timeout: z\n .number()\n .int()\n .min(1000)\n .max(300000)\n .optional()\n .default(DEFAULT_TIMEOUT)\n .describe(\n 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'\n ),\n});\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/** Python reserved keywords that get `_tool` suffix in Code API */\nconst PYTHON_KEYWORDS = new Set([\n 'False',\n 'None',\n 'True',\n 'and',\n 'as',\n 'assert',\n 'async',\n 'await',\n 'break',\n 'class',\n 'continue',\n 'def',\n 'del',\n 'elif',\n 'else',\n 'except',\n 'finally',\n 'for',\n 'from',\n 'global',\n 'if',\n 'import',\n 'in',\n 'is',\n 'lambda',\n 'nonlocal',\n 'not',\n 'or',\n 'pass',\n 'raise',\n 'return',\n 'try',\n 'while',\n 'with',\n 'yield',\n]);\n\n/**\n * Normalizes a tool name to Python identifier format.\n * Must match the Code API's `normalizePythonFunctionName` exactly:\n * 1. Replace hyphens and spaces with underscores\n * 2. Remove any other invalid characters\n * 3. Prefix with underscore if starts with number\n * 4. Append `_tool` if it's a Python keyword\n * @param name - The tool name to normalize\n * @returns Normalized Python-safe identifier\n */\nexport function normalizeToPythonIdentifier(name: string): string {\n let normalized = name.replace(/[-\\s]/g, '_');\n\n normalized = normalized.replace(/[^a-zA-Z0-9_]/g, '');\n\n if (/^[0-9]/.test(normalized)) {\n normalized = '_' + normalized;\n }\n\n if (PYTHON_KEYWORDS.has(normalized)) {\n normalized = normalized + '_tool';\n }\n\n return normalized;\n}\n\n/**\n * Extracts tool names that are actually called in the Python code.\n * Handles hyphen/underscore conversion since Python identifiers use underscores.\n * @param code - The Python code to analyze\n * @param toolNameMap - Map from normalized Python name to original tool name\n * @returns Set of original tool names found in the code\n */\nexport function extractUsedToolNames(\n code: string,\n toolNameMap: Map<string, string>\n): Set<string> {\n const usedTools = new Set<string>();\n\n for (const [pythonName, originalName] of toolNameMap) {\n const escapedName = pythonName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = new RegExp(`\\\\b${escapedName}\\\\s*\\\\(`, 'g');\n\n if (pattern.test(code)) {\n usedTools.add(originalName);\n }\n }\n\n return usedTools;\n}\n\n/**\n * Filters tool definitions to only include tools actually used in the code.\n * Handles the hyphen-to-underscore conversion for Python compatibility.\n * @param toolDefs - All available tool definitions\n * @param code - The Python code to analyze\n * @param debug - Enable debug logging\n * @returns Filtered array of tool definitions\n */\nexport function filterToolsByUsage(\n toolDefs: t.LCTool[],\n code: string,\n debug = false\n): t.LCTool[] {\n const toolNameMap = new Map<string, string>();\n for (const tool of toolDefs) {\n const pythonName = normalizeToPythonIdentifier(tool.name);\n toolNameMap.set(pythonName, tool.name);\n }\n\n const usedToolNames = extractUsedToolNames(code, toolNameMap);\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Tool filtering: found ${usedToolNames.size}/${toolDefs.length} tools in code`\n );\n if (usedToolNames.size > 0) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Matched tools: ${Array.from(usedToolNames).join(', ')}`\n );\n }\n }\n\n if (usedToolNames.size === 0) {\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n '[PTC Debug] No tools detected in code - sending all tools as fallback'\n );\n }\n return toolDefs;\n }\n\n return toolDefs.filter((tool) => usedToolNames.has(tool.name));\n}\n\n/**\n * Makes an HTTP request to the Code API.\n * @param endpoint - The API endpoint URL\n * @param apiKey - The API key for authentication\n * @param body - The request body\n * @param proxy - Optional HTTP proxy URL\n * @returns The parsed API response\n */\nexport async function makeRequest(\n endpoint: string,\n apiKey: string,\n body: Record<string, unknown>,\n proxy?: string\n): Promise<t.ProgrammaticExecutionResponse> {\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify(body),\n };\n\n if (proxy != null && proxy !== '') {\n fetchOptions.agent = new HttpsProxyAgent(proxy);\n }\n\n const response = await fetch(endpoint, fetchOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `HTTP error! status: ${response.status}, body: ${errorText}`\n );\n }\n\n return (await response.json()) as t.ProgrammaticExecutionResponse;\n}\n\n/**\n * Executes tools in parallel when requested by the API.\n * Uses Promise.all for parallel execution, catching individual errors.\n * @param toolCalls - Array of tool calls from the API\n * @param toolMap - Map of tool names to executable tools\n * @returns Array of tool results\n */\nexport async function executeTools(\n toolCalls: t.PTCToolCall[],\n toolMap: t.ToolMap\n): Promise<t.PTCToolResult[]> {\n const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {\n const tool = toolMap.get(call.name);\n\n if (!tool) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,\n };\n }\n\n try {\n const result = await tool.invoke(call.input, {\n metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },\n });\n return {\n call_id: call.id,\n result,\n is_error: false,\n };\n } catch (error) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: (error as Error).message || 'Tool execution failed',\n };\n }\n });\n\n return await Promise.all(executions);\n}\n\n/**\n * Formats the completed response for the agent.\n * @param response - The completed API response\n * @returns Tuple of [formatted string, artifact]\n */\nexport function formatCompletedResponse(\n response: t.ProgrammaticExecutionResponse\n): [string, t.ProgrammaticExecutionArtifact] {\n let formatted = '';\n\n if (response.stdout != null && response.stdout !== '') {\n formatted += `stdout:\\n${response.stdout}\\n`;\n } else {\n formatted += emptyOutputMessage;\n }\n\n if (response.stderr != null && response.stderr !== '') {\n formatted += `stderr:\\n${response.stderr}\\n`;\n }\n\n if (response.files && response.files.length > 0) {\n formatted += 'Generated files:\\n';\n\n const fileCount = response.files.length;\n for (let i = 0; i < fileCount; i++) {\n const file = response.files[i];\n const isImage = imageExtRegex.test(file.name);\n formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;\n\n if (i < fileCount - 1) {\n formatted += fileCount <= 3 ? ', ' : ',\\n';\n }\n }\n\n formatted += `\\nsession_id: ${response.session_id}\\n\\n${accessMessage}`;\n }\n\n return [\n formatted.trim(),\n {\n session_id: response.session_id,\n files: response.files,\n },\n ];\n}\n\n// ============================================================================\n// Tool Factory\n// ============================================================================\n\n/**\n * Creates a Programmatic Tool Calling tool for complex multi-tool workflows.\n *\n * This tool enables AI agents to write Python code that orchestrates multiple\n * tool calls programmatically, reducing LLM round-trips and token usage.\n *\n * The tool map must be provided at runtime via config.configurable.toolMap.\n *\n * @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)\n * @returns A LangChain DynamicStructuredTool for programmatic tool calling\n *\n * @example\n * const ptcTool = createProgrammaticToolCallingTool({\n * apiKey: process.env.CODE_API_KEY,\n * maxRoundTrips: 20\n * });\n *\n * const [output, artifact] = await ptcTool.invoke(\n * { code, tools },\n * { configurable: { toolMap } }\n * );\n */\nexport function createProgrammaticToolCallingTool(\n initParams: t.ProgrammaticToolCallingParams = {}\n): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {\n const apiKey =\n (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??\n initParams.apiKey ??\n getEnvironmentVariable(EnvVar.CODE_API_KEY) ??\n '';\n\n if (!apiKey) {\n throw new Error(\n 'No API key provided for programmatic tool calling. ' +\n 'Set CODE_API_KEY environment variable or pass apiKey in initParams.'\n );\n }\n\n const baseUrl = initParams.baseUrl ?? getCodeBaseURL();\n const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;\n const proxy = initParams.proxy ?? process.env.PROXY;\n const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';\n const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;\n\n const description = `\nRun tools by writing Python code. Tools are available as async functions - just call them with await.\n\nThis is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.\n\nUsage:\n- Tools are pre-defined as async functions - call them with await\n- Use asyncio.gather() to run multiple tools in parallel\n- Only print() output is returned - tool results stay in Python\n\nExamples:\n- Simple: result = await get_weather(city=\"NYC\")\n- Loop: for user in users: data = await get_expenses(user_id=user['id'])\n- Parallel: sf, ny = await asyncio.gather(get_weather(city=\"SF\"), get_weather(city=\"NY\"))\n\nWhen to use this instead of calling tools directly:\n- You need to call tools in a loop (process many items)\n- You want parallel execution (asyncio.gather)\n- You need conditionals based on tool results\n- You want to aggregate/filter data before returning\n`.trim();\n\n return tool<typeof ProgrammaticToolCallingSchema>(\n async (params, config) => {\n const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;\n\n // Extra params injected by ToolNode (follows web_search pattern)\n const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &\n Partial<t.ProgrammaticCache>;\n\n if (toolMap == null || toolMap.size === 0) {\n throw new Error(\n 'No toolMap provided. ' +\n 'ToolNode should inject this from AgentContext when invoked through the graph.'\n );\n }\n\n if (toolDefs == null || toolDefs.length === 0) {\n throw new Error(\n 'No tool definitions provided. ' +\n 'Either pass tools in the input or ensure ToolNode injects toolDefs.'\n );\n }\n\n let roundTrip = 0;\n\n try {\n // ====================================================================\n // Phase 1: Filter tools and make initial request\n // ====================================================================\n\n const effectiveTools = filterToolsByUsage(toolDefs, code, debug);\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Sending ${effectiveTools.length} tools to API ` +\n `(filtered from ${toolDefs.length})`\n );\n }\n\n let response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n code,\n tools: effectiveTools,\n session_id,\n timeout,\n },\n proxy\n );\n\n // ====================================================================\n // Phase 2: Handle response loop\n // ====================================================================\n\n while (response.status === 'tool_call_required') {\n roundTrip++;\n\n if (roundTrip > maxRoundTrips) {\n throw new Error(\n `Exceeded maximum round trips (${maxRoundTrips}). ` +\n 'This may indicate an infinite loop, excessive tool calls, ' +\n 'or a logic error in your code.'\n );\n }\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`\n );\n }\n\n const toolResults = await executeTools(\n response.tool_calls ?? [],\n toolMap\n );\n\n response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n continuation_token: response.continuation_token,\n tool_results: toolResults,\n },\n proxy\n );\n }\n\n // ====================================================================\n // Phase 3: Handle final state\n // ====================================================================\n\n if (response.status === 'completed') {\n return formatCompletedResponse(response);\n }\n\n if (response.status === 'error') {\n throw new Error(\n `Execution error: ${response.error}` +\n (response.stderr != null && response.stderr !== ''\n ? `\\n\\nStderr:\\n${response.stderr}`\n : '')\n );\n }\n\n throw new Error(`Unexpected response status: ${response.status}`);\n } catch (error) {\n throw new Error(\n `Programmatic execution failed: ${(error as Error).message}`\n );\n }\n },\n {\n name: Constants.PROGRAMMATIC_TOOL_CALLING,\n description,\n schema: ProgrammaticToolCallingSchema,\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n"],"names":["config","z","HttpsProxyAgent","Constants","imageExtRegex","EnvVar","getEnvironmentVariable","getCodeBaseURL","tool"],"mappings":"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"}
|
package/dist/esm/main.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export { Graph, StandardGraph } from './graphs/Graph.mjs';
|
|
|
13
13
|
export { MultiAgentGraph } from './graphs/MultiAgentGraph.mjs';
|
|
14
14
|
export { Calculator } from './tools/Calculator.mjs';
|
|
15
15
|
export { createCodeExecutionTool, getCodeBaseURL, imageExtRegex } from './tools/CodeExecutor.mjs';
|
|
16
|
-
export { createProgrammaticToolCallingTool, executeTools, extractUsedToolNames, filterToolsByUsage, formatCompletedResponse, makeRequest } from './tools/ProgrammaticToolCalling.mjs';
|
|
16
|
+
export { createProgrammaticToolCallingTool, executeTools, extractUsedToolNames, filterToolsByUsage, formatCompletedResponse, makeRequest, normalizeToPythonIdentifier } from './tools/ProgrammaticToolCalling.mjs';
|
|
17
17
|
export { countNestedGroups, createToolSearchRegexTool, escapeRegexSpecialChars, hasNestedQuantifiers, isDangerousPattern, sanitizeRegex } from './tools/ToolSearchRegex.mjs';
|
|
18
18
|
export { handleServerToolResult, handleToolCallChunks, handleToolCalls, toolResultTypes } from './tools/handlers.mjs';
|
|
19
19
|
export { createSearchTool } from './tools/search/tool.mjs';
|
|
@@ -79,34 +79,111 @@ Requirements:
|
|
|
79
79
|
// ============================================================================
|
|
80
80
|
// Helper Functions
|
|
81
81
|
// ============================================================================
|
|
82
|
+
/** Python reserved keywords that get `_tool` suffix in Code API */
|
|
83
|
+
const PYTHON_KEYWORDS = new Set([
|
|
84
|
+
'False',
|
|
85
|
+
'None',
|
|
86
|
+
'True',
|
|
87
|
+
'and',
|
|
88
|
+
'as',
|
|
89
|
+
'assert',
|
|
90
|
+
'async',
|
|
91
|
+
'await',
|
|
92
|
+
'break',
|
|
93
|
+
'class',
|
|
94
|
+
'continue',
|
|
95
|
+
'def',
|
|
96
|
+
'del',
|
|
97
|
+
'elif',
|
|
98
|
+
'else',
|
|
99
|
+
'except',
|
|
100
|
+
'finally',
|
|
101
|
+
'for',
|
|
102
|
+
'from',
|
|
103
|
+
'global',
|
|
104
|
+
'if',
|
|
105
|
+
'import',
|
|
106
|
+
'in',
|
|
107
|
+
'is',
|
|
108
|
+
'lambda',
|
|
109
|
+
'nonlocal',
|
|
110
|
+
'not',
|
|
111
|
+
'or',
|
|
112
|
+
'pass',
|
|
113
|
+
'raise',
|
|
114
|
+
'return',
|
|
115
|
+
'try',
|
|
116
|
+
'while',
|
|
117
|
+
'with',
|
|
118
|
+
'yield',
|
|
119
|
+
]);
|
|
120
|
+
/**
|
|
121
|
+
* Normalizes a tool name to Python identifier format.
|
|
122
|
+
* Must match the Code API's `normalizePythonFunctionName` exactly:
|
|
123
|
+
* 1. Replace hyphens and spaces with underscores
|
|
124
|
+
* 2. Remove any other invalid characters
|
|
125
|
+
* 3. Prefix with underscore if starts with number
|
|
126
|
+
* 4. Append `_tool` if it's a Python keyword
|
|
127
|
+
* @param name - The tool name to normalize
|
|
128
|
+
* @returns Normalized Python-safe identifier
|
|
129
|
+
*/
|
|
130
|
+
function normalizeToPythonIdentifier(name) {
|
|
131
|
+
let normalized = name.replace(/[-\s]/g, '_');
|
|
132
|
+
normalized = normalized.replace(/[^a-zA-Z0-9_]/g, '');
|
|
133
|
+
if (/^[0-9]/.test(normalized)) {
|
|
134
|
+
normalized = '_' + normalized;
|
|
135
|
+
}
|
|
136
|
+
if (PYTHON_KEYWORDS.has(normalized)) {
|
|
137
|
+
normalized = normalized + '_tool';
|
|
138
|
+
}
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
82
141
|
/**
|
|
83
142
|
* Extracts tool names that are actually called in the Python code.
|
|
84
|
-
*
|
|
143
|
+
* Handles hyphen/underscore conversion since Python identifiers use underscores.
|
|
85
144
|
* @param code - The Python code to analyze
|
|
86
|
-
* @param
|
|
87
|
-
* @returns Set of tool names found in the code
|
|
145
|
+
* @param toolNameMap - Map from normalized Python name to original tool name
|
|
146
|
+
* @returns Set of original tool names found in the code
|
|
88
147
|
*/
|
|
89
|
-
function extractUsedToolNames(code,
|
|
148
|
+
function extractUsedToolNames(code, toolNameMap) {
|
|
90
149
|
const usedTools = new Set();
|
|
91
|
-
for (const
|
|
92
|
-
const escapedName =
|
|
150
|
+
for (const [pythonName, originalName] of toolNameMap) {
|
|
151
|
+
const escapedName = pythonName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
152
|
const pattern = new RegExp(`\\b${escapedName}\\s*\\(`, 'g');
|
|
94
153
|
if (pattern.test(code)) {
|
|
95
|
-
usedTools.add(
|
|
154
|
+
usedTools.add(originalName);
|
|
96
155
|
}
|
|
97
156
|
}
|
|
98
157
|
return usedTools;
|
|
99
158
|
}
|
|
100
159
|
/**
|
|
101
160
|
* Filters tool definitions to only include tools actually used in the code.
|
|
161
|
+
* Handles the hyphen-to-underscore conversion for Python compatibility.
|
|
102
162
|
* @param toolDefs - All available tool definitions
|
|
103
163
|
* @param code - The Python code to analyze
|
|
164
|
+
* @param debug - Enable debug logging
|
|
104
165
|
* @returns Filtered array of tool definitions
|
|
105
166
|
*/
|
|
106
|
-
function filterToolsByUsage(toolDefs, code) {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
167
|
+
function filterToolsByUsage(toolDefs, code, debug = false) {
|
|
168
|
+
const toolNameMap = new Map();
|
|
169
|
+
for (const tool of toolDefs) {
|
|
170
|
+
const pythonName = normalizeToPythonIdentifier(tool.name);
|
|
171
|
+
toolNameMap.set(pythonName, tool.name);
|
|
172
|
+
}
|
|
173
|
+
const usedToolNames = extractUsedToolNames(code, toolNameMap);
|
|
174
|
+
if (debug) {
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.log(`[PTC Debug] Tool filtering: found ${usedToolNames.size}/${toolDefs.length} tools in code`);
|
|
177
|
+
if (usedToolNames.size > 0) {
|
|
178
|
+
// eslint-disable-next-line no-console
|
|
179
|
+
console.log(`[PTC Debug] Matched tools: ${Array.from(usedToolNames).join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
109
182
|
if (usedToolNames.size === 0) {
|
|
183
|
+
if (debug) {
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.log('[PTC Debug] No tools detected in code - sending all tools as fallback');
|
|
186
|
+
}
|
|
110
187
|
return toolDefs;
|
|
111
188
|
}
|
|
112
189
|
return toolDefs.filter((tool) => usedToolNames.has(tool.name));
|
|
@@ -252,6 +329,7 @@ function createProgrammaticToolCallingTool(initParams = {}) {
|
|
|
252
329
|
const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
|
|
253
330
|
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
|
|
254
331
|
const proxy = initParams.proxy ?? process.env.PROXY;
|
|
332
|
+
const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
|
|
255
333
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
256
334
|
const description = `
|
|
257
335
|
Run tools by writing Python code. Tools are available as async functions - just call them with await.
|
|
@@ -291,7 +369,12 @@ When to use this instead of calling tools directly:
|
|
|
291
369
|
// ====================================================================
|
|
292
370
|
// Phase 1: Filter tools and make initial request
|
|
293
371
|
// ====================================================================
|
|
294
|
-
const effectiveTools = filterToolsByUsage(toolDefs, code);
|
|
372
|
+
const effectiveTools = filterToolsByUsage(toolDefs, code, debug);
|
|
373
|
+
if (debug) {
|
|
374
|
+
// eslint-disable-next-line no-console
|
|
375
|
+
console.log(`[PTC Debug] Sending ${effectiveTools.length} tools to API ` +
|
|
376
|
+
`(filtered from ${toolDefs.length})`);
|
|
377
|
+
}
|
|
295
378
|
let response = await makeRequest(EXEC_ENDPOINT, apiKey, {
|
|
296
379
|
code,
|
|
297
380
|
tools: effectiveTools,
|
|
@@ -308,8 +391,10 @@ When to use this instead of calling tools directly:
|
|
|
308
391
|
'This may indicate an infinite loop, excessive tool calls, ' +
|
|
309
392
|
'or a logic error in your code.');
|
|
310
393
|
}
|
|
311
|
-
|
|
312
|
-
|
|
394
|
+
if (debug) {
|
|
395
|
+
// eslint-disable-next-line no-console
|
|
396
|
+
console.log(`[PTC Debug] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`);
|
|
397
|
+
}
|
|
313
398
|
const toolResults = await executeTools(response.tool_calls ?? [], toolMap);
|
|
314
399
|
response = await makeRequest(EXEC_ENDPOINT, apiKey, {
|
|
315
400
|
continuation_token: response.continuation_token,
|
|
@@ -341,5 +426,5 @@ When to use this instead of calling tools directly:
|
|
|
341
426
|
});
|
|
342
427
|
}
|
|
343
428
|
|
|
344
|
-
export { createProgrammaticToolCallingTool, executeTools, extractUsedToolNames, filterToolsByUsage, formatCompletedResponse, makeRequest };
|
|
429
|
+
export { createProgrammaticToolCallingTool, executeTools, extractUsedToolNames, filterToolsByUsage, formatCompletedResponse, makeRequest, normalizeToPythonIdentifier };
|
|
345
430
|
//# sourceMappingURL=ProgrammaticToolCalling.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgrammaticToolCalling.mjs","sources":["../../../src/tools/ProgrammaticToolCalling.ts"],"sourcesContent":["// src/tools/ProgrammaticToolCalling.ts\nimport { z } from 'zod';\nimport { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { getEnvironmentVariable } from '@langchain/core/utils/env';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type { ToolCall } from '@langchain/core/messages/tool';\nimport type * as t from '@/types';\nimport { imageExtRegex, getCodeBaseURL } from './CodeExecutor';\nimport { EnvVar, Constants } from '@/common';\n\nconfig();\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst imageMessage = 'Image is already displayed to the user';\nconst otherMessage = 'File is already downloaded by the user';\nconst accessMessage =\n 'Note: Files are READ-ONLY. Save changes to NEW filenames. To access these files in future executions, provide the `session_id` as a parameter (not in your code).';\nconst emptyOutputMessage =\n 'stdout: Empty. Ensure you\\'re writing output explicitly.\\n';\n\n/** Default max round-trips to prevent infinite loops */\nconst DEFAULT_MAX_ROUND_TRIPS = 20;\n\n/** Default execution timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 60000;\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst ProgrammaticToolCallingSchema = z.object({\n code: z\n .string()\n .min(1)\n .describe(\n `Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.\n\nThe Code API generates async function stubs from the tool definitions. Just call them directly:\n\nExample (Simple call):\n result = await get_weather(city=\"San Francisco\")\n print(result)\n\nExample (Parallel - Fastest):\n results = await asyncio.gather(\n get_weather(city=\"SF\"),\n get_weather(city=\"NYC\"),\n get_weather(city=\"London\")\n )\n for city, weather in zip([\"SF\", \"NYC\", \"London\"], results):\n print(f\"{city}: {weather['temperature']}°F\")\n\nExample (Loop with processing):\n team = await get_team_members()\n for member in team:\n expenses = await get_expenses(user_id=member['id'])\n total = sum(e['amount'] for e in expenses)\n print(f\"{member['name']}: \\${total:.2f}\")\n\nExample (Conditional logic):\n data = await fetch_data(source=\"primary\")\n if not data:\n data = await fetch_data(source=\"backup\")\n print(f\"Got {len(data)} records\")\n\nRequirements:\n- Tools are pre-defined as async functions - DO NOT write function definitions\n- Use await for all tool calls\n- Use asyncio.gather() for parallel execution of independent calls\n- Only print() output flows back to the context window\n- Tool results from programmatic calls do NOT consume context tokens`\n ),\n session_id: z\n .string()\n .optional()\n .describe(\n 'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'\n ),\n timeout: z\n .number()\n .int()\n .min(1000)\n .max(300000)\n .optional()\n .default(DEFAULT_TIMEOUT)\n .describe(\n 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'\n ),\n});\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Extracts tool names that are actually called in the Python code.\n * Matches patterns like `await tool_name(`, `tool_name(`, and asyncio.gather calls.\n * @param code - The Python code to analyze\n * @param availableToolNames - Set of available tool names to match against\n * @returns Set of tool names found in the code\n */\nexport function extractUsedToolNames(\n code: string,\n availableToolNames: Set<string>\n): Set<string> {\n const usedTools = new Set<string>();\n\n for (const toolName of availableToolNames) {\n const escapedName = toolName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = new RegExp(`\\\\b${escapedName}\\\\s*\\\\(`, 'g');\n\n if (pattern.test(code)) {\n usedTools.add(toolName);\n }\n }\n\n return usedTools;\n}\n\n/**\n * Filters tool definitions to only include tools actually used in the code.\n * @param toolDefs - All available tool definitions\n * @param code - The Python code to analyze\n * @returns Filtered array of tool definitions\n */\nexport function filterToolsByUsage(\n toolDefs: t.LCTool[],\n code: string\n): t.LCTool[] {\n const availableToolNames = new Set(toolDefs.map((tool) => tool.name));\n const usedToolNames = extractUsedToolNames(code, availableToolNames);\n\n if (usedToolNames.size === 0) {\n return toolDefs;\n }\n\n return toolDefs.filter((tool) => usedToolNames.has(tool.name));\n}\n\n/**\n * Makes an HTTP request to the Code API.\n * @param endpoint - The API endpoint URL\n * @param apiKey - The API key for authentication\n * @param body - The request body\n * @param proxy - Optional HTTP proxy URL\n * @returns The parsed API response\n */\nexport async function makeRequest(\n endpoint: string,\n apiKey: string,\n body: Record<string, unknown>,\n proxy?: string\n): Promise<t.ProgrammaticExecutionResponse> {\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify(body),\n };\n\n if (proxy != null && proxy !== '') {\n fetchOptions.agent = new HttpsProxyAgent(proxy);\n }\n\n const response = await fetch(endpoint, fetchOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `HTTP error! status: ${response.status}, body: ${errorText}`\n );\n }\n\n return (await response.json()) as t.ProgrammaticExecutionResponse;\n}\n\n/**\n * Executes tools in parallel when requested by the API.\n * Uses Promise.all for parallel execution, catching individual errors.\n * @param toolCalls - Array of tool calls from the API\n * @param toolMap - Map of tool names to executable tools\n * @returns Array of tool results\n */\nexport async function executeTools(\n toolCalls: t.PTCToolCall[],\n toolMap: t.ToolMap\n): Promise<t.PTCToolResult[]> {\n const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {\n const tool = toolMap.get(call.name);\n\n if (!tool) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,\n };\n }\n\n try {\n const result = await tool.invoke(call.input, {\n metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },\n });\n return {\n call_id: call.id,\n result,\n is_error: false,\n };\n } catch (error) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: (error as Error).message || 'Tool execution failed',\n };\n }\n });\n\n return await Promise.all(executions);\n}\n\n/**\n * Formats the completed response for the agent.\n * @param response - The completed API response\n * @returns Tuple of [formatted string, artifact]\n */\nexport function formatCompletedResponse(\n response: t.ProgrammaticExecutionResponse\n): [string, t.ProgrammaticExecutionArtifact] {\n let formatted = '';\n\n if (response.stdout != null && response.stdout !== '') {\n formatted += `stdout:\\n${response.stdout}\\n`;\n } else {\n formatted += emptyOutputMessage;\n }\n\n if (response.stderr != null && response.stderr !== '') {\n formatted += `stderr:\\n${response.stderr}\\n`;\n }\n\n if (response.files && response.files.length > 0) {\n formatted += 'Generated files:\\n';\n\n const fileCount = response.files.length;\n for (let i = 0; i < fileCount; i++) {\n const file = response.files[i];\n const isImage = imageExtRegex.test(file.name);\n formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;\n\n if (i < fileCount - 1) {\n formatted += fileCount <= 3 ? ', ' : ',\\n';\n }\n }\n\n formatted += `\\nsession_id: ${response.session_id}\\n\\n${accessMessage}`;\n }\n\n return [\n formatted.trim(),\n {\n session_id: response.session_id,\n files: response.files,\n },\n ];\n}\n\n// ============================================================================\n// Tool Factory\n// ============================================================================\n\n/**\n * Creates a Programmatic Tool Calling tool for complex multi-tool workflows.\n *\n * This tool enables AI agents to write Python code that orchestrates multiple\n * tool calls programmatically, reducing LLM round-trips and token usage.\n *\n * The tool map must be provided at runtime via config.configurable.toolMap.\n *\n * @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)\n * @returns A LangChain DynamicStructuredTool for programmatic tool calling\n *\n * @example\n * const ptcTool = createProgrammaticToolCallingTool({\n * apiKey: process.env.CODE_API_KEY,\n * maxRoundTrips: 20\n * });\n *\n * const [output, artifact] = await ptcTool.invoke(\n * { code, tools },\n * { configurable: { toolMap } }\n * );\n */\nexport function createProgrammaticToolCallingTool(\n initParams: t.ProgrammaticToolCallingParams = {}\n): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {\n const apiKey =\n (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??\n initParams.apiKey ??\n getEnvironmentVariable(EnvVar.CODE_API_KEY) ??\n '';\n\n if (!apiKey) {\n throw new Error(\n 'No API key provided for programmatic tool calling. ' +\n 'Set CODE_API_KEY environment variable or pass apiKey in initParams.'\n );\n }\n\n const baseUrl = initParams.baseUrl ?? getCodeBaseURL();\n const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;\n const proxy = initParams.proxy ?? process.env.PROXY;\n const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;\n\n const description = `\nRun tools by writing Python code. Tools are available as async functions - just call them with await.\n\nThis is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.\n\nUsage:\n- Tools are pre-defined as async functions - call them with await\n- Use asyncio.gather() to run multiple tools in parallel\n- Only print() output is returned - tool results stay in Python\n\nExamples:\n- Simple: result = await get_weather(city=\"NYC\")\n- Loop: for user in users: data = await get_expenses(user_id=user['id'])\n- Parallel: sf, ny = await asyncio.gather(get_weather(city=\"SF\"), get_weather(city=\"NY\"))\n\nWhen to use this instead of calling tools directly:\n- You need to call tools in a loop (process many items)\n- You want parallel execution (asyncio.gather)\n- You need conditionals based on tool results\n- You want to aggregate/filter data before returning\n`.trim();\n\n return tool<typeof ProgrammaticToolCallingSchema>(\n async (params, config) => {\n const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;\n\n // Extra params injected by ToolNode (follows web_search pattern)\n const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &\n Partial<t.ProgrammaticCache>;\n\n if (toolMap == null || toolMap.size === 0) {\n throw new Error(\n 'No toolMap provided. ' +\n 'ToolNode should inject this from AgentContext when invoked through the graph.'\n );\n }\n\n if (toolDefs == null || toolDefs.length === 0) {\n throw new Error(\n 'No tool definitions provided. ' +\n 'Either pass tools in the input or ensure ToolNode injects toolDefs.'\n );\n }\n\n let roundTrip = 0;\n\n try {\n // ====================================================================\n // Phase 1: Filter tools and make initial request\n // ====================================================================\n\n const effectiveTools = filterToolsByUsage(toolDefs, code);\n\n let response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n code,\n tools: effectiveTools,\n session_id,\n timeout,\n },\n proxy\n );\n\n // ====================================================================\n // Phase 2: Handle response loop\n // ====================================================================\n\n while (response.status === 'tool_call_required') {\n roundTrip++;\n\n if (roundTrip > maxRoundTrips) {\n throw new Error(\n `Exceeded maximum round trips (${maxRoundTrips}). ` +\n 'This may indicate an infinite loop, excessive tool calls, ' +\n 'or a logic error in your code.'\n );\n }\n\n // eslint-disable-next-line no-console\n console.log(\n `[PTC] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`\n );\n\n const toolResults = await executeTools(\n response.tool_calls ?? [],\n toolMap\n );\n\n response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n continuation_token: response.continuation_token,\n tool_results: toolResults,\n },\n proxy\n );\n }\n\n // ====================================================================\n // Phase 3: Handle final state\n // ====================================================================\n\n if (response.status === 'completed') {\n return formatCompletedResponse(response);\n }\n\n if (response.status === 'error') {\n throw new Error(\n `Execution error: ${response.error}` +\n (response.stderr != null && response.stderr !== ''\n ? `\\n\\nStderr:\\n${response.stderr}`\n : '')\n );\n }\n\n throw new Error(`Unexpected response status: ${response.status}`);\n } catch (error) {\n throw new Error(\n `Programmatic execution failed: ${(error as Error).message}`\n );\n }\n },\n {\n name: Constants.PROGRAMMATIC_TOOL_CALLING,\n description,\n schema: ProgrammaticToolCallingSchema,\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProgrammaticToolCalling.mjs","sources":["../../../src/tools/ProgrammaticToolCalling.ts"],"sourcesContent":["// src/tools/ProgrammaticToolCalling.ts\nimport { z } from 'zod';\nimport { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { getEnvironmentVariable } from '@langchain/core/utils/env';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type { ToolCall } from '@langchain/core/messages/tool';\nimport type * as t from '@/types';\nimport { imageExtRegex, getCodeBaseURL } from './CodeExecutor';\nimport { EnvVar, Constants } from '@/common';\n\nconfig();\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst imageMessage = 'Image is already displayed to the user';\nconst otherMessage = 'File is already downloaded by the user';\nconst accessMessage =\n 'Note: Files are READ-ONLY. Save changes to NEW filenames. To access these files in future executions, provide the `session_id` as a parameter (not in your code).';\nconst emptyOutputMessage =\n 'stdout: Empty. Ensure you\\'re writing output explicitly.\\n';\n\n/** Default max round-trips to prevent infinite loops */\nconst DEFAULT_MAX_ROUND_TRIPS = 20;\n\n/** Default execution timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 60000;\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst ProgrammaticToolCallingSchema = z.object({\n code: z\n .string()\n .min(1)\n .describe(\n `Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.\n\nThe Code API generates async function stubs from the tool definitions. Just call them directly:\n\nExample (Simple call):\n result = await get_weather(city=\"San Francisco\")\n print(result)\n\nExample (Parallel - Fastest):\n results = await asyncio.gather(\n get_weather(city=\"SF\"),\n get_weather(city=\"NYC\"),\n get_weather(city=\"London\")\n )\n for city, weather in zip([\"SF\", \"NYC\", \"London\"], results):\n print(f\"{city}: {weather['temperature']}°F\")\n\nExample (Loop with processing):\n team = await get_team_members()\n for member in team:\n expenses = await get_expenses(user_id=member['id'])\n total = sum(e['amount'] for e in expenses)\n print(f\"{member['name']}: \\${total:.2f}\")\n\nExample (Conditional logic):\n data = await fetch_data(source=\"primary\")\n if not data:\n data = await fetch_data(source=\"backup\")\n print(f\"Got {len(data)} records\")\n\nRequirements:\n- Tools are pre-defined as async functions - DO NOT write function definitions\n- Use await for all tool calls\n- Use asyncio.gather() for parallel execution of independent calls\n- Only print() output flows back to the context window\n- Tool results from programmatic calls do NOT consume context tokens`\n ),\n session_id: z\n .string()\n .optional()\n .describe(\n 'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'\n ),\n timeout: z\n .number()\n .int()\n .min(1000)\n .max(300000)\n .optional()\n .default(DEFAULT_TIMEOUT)\n .describe(\n 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'\n ),\n});\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/** Python reserved keywords that get `_tool` suffix in Code API */\nconst PYTHON_KEYWORDS = new Set([\n 'False',\n 'None',\n 'True',\n 'and',\n 'as',\n 'assert',\n 'async',\n 'await',\n 'break',\n 'class',\n 'continue',\n 'def',\n 'del',\n 'elif',\n 'else',\n 'except',\n 'finally',\n 'for',\n 'from',\n 'global',\n 'if',\n 'import',\n 'in',\n 'is',\n 'lambda',\n 'nonlocal',\n 'not',\n 'or',\n 'pass',\n 'raise',\n 'return',\n 'try',\n 'while',\n 'with',\n 'yield',\n]);\n\n/**\n * Normalizes a tool name to Python identifier format.\n * Must match the Code API's `normalizePythonFunctionName` exactly:\n * 1. Replace hyphens and spaces with underscores\n * 2. Remove any other invalid characters\n * 3. Prefix with underscore if starts with number\n * 4. Append `_tool` if it's a Python keyword\n * @param name - The tool name to normalize\n * @returns Normalized Python-safe identifier\n */\nexport function normalizeToPythonIdentifier(name: string): string {\n let normalized = name.replace(/[-\\s]/g, '_');\n\n normalized = normalized.replace(/[^a-zA-Z0-9_]/g, '');\n\n if (/^[0-9]/.test(normalized)) {\n normalized = '_' + normalized;\n }\n\n if (PYTHON_KEYWORDS.has(normalized)) {\n normalized = normalized + '_tool';\n }\n\n return normalized;\n}\n\n/**\n * Extracts tool names that are actually called in the Python code.\n * Handles hyphen/underscore conversion since Python identifiers use underscores.\n * @param code - The Python code to analyze\n * @param toolNameMap - Map from normalized Python name to original tool name\n * @returns Set of original tool names found in the code\n */\nexport function extractUsedToolNames(\n code: string,\n toolNameMap: Map<string, string>\n): Set<string> {\n const usedTools = new Set<string>();\n\n for (const [pythonName, originalName] of toolNameMap) {\n const escapedName = pythonName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = new RegExp(`\\\\b${escapedName}\\\\s*\\\\(`, 'g');\n\n if (pattern.test(code)) {\n usedTools.add(originalName);\n }\n }\n\n return usedTools;\n}\n\n/**\n * Filters tool definitions to only include tools actually used in the code.\n * Handles the hyphen-to-underscore conversion for Python compatibility.\n * @param toolDefs - All available tool definitions\n * @param code - The Python code to analyze\n * @param debug - Enable debug logging\n * @returns Filtered array of tool definitions\n */\nexport function filterToolsByUsage(\n toolDefs: t.LCTool[],\n code: string,\n debug = false\n): t.LCTool[] {\n const toolNameMap = new Map<string, string>();\n for (const tool of toolDefs) {\n const pythonName = normalizeToPythonIdentifier(tool.name);\n toolNameMap.set(pythonName, tool.name);\n }\n\n const usedToolNames = extractUsedToolNames(code, toolNameMap);\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Tool filtering: found ${usedToolNames.size}/${toolDefs.length} tools in code`\n );\n if (usedToolNames.size > 0) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Matched tools: ${Array.from(usedToolNames).join(', ')}`\n );\n }\n }\n\n if (usedToolNames.size === 0) {\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n '[PTC Debug] No tools detected in code - sending all tools as fallback'\n );\n }\n return toolDefs;\n }\n\n return toolDefs.filter((tool) => usedToolNames.has(tool.name));\n}\n\n/**\n * Makes an HTTP request to the Code API.\n * @param endpoint - The API endpoint URL\n * @param apiKey - The API key for authentication\n * @param body - The request body\n * @param proxy - Optional HTTP proxy URL\n * @returns The parsed API response\n */\nexport async function makeRequest(\n endpoint: string,\n apiKey: string,\n body: Record<string, unknown>,\n proxy?: string\n): Promise<t.ProgrammaticExecutionResponse> {\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify(body),\n };\n\n if (proxy != null && proxy !== '') {\n fetchOptions.agent = new HttpsProxyAgent(proxy);\n }\n\n const response = await fetch(endpoint, fetchOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `HTTP error! status: ${response.status}, body: ${errorText}`\n );\n }\n\n return (await response.json()) as t.ProgrammaticExecutionResponse;\n}\n\n/**\n * Executes tools in parallel when requested by the API.\n * Uses Promise.all for parallel execution, catching individual errors.\n * @param toolCalls - Array of tool calls from the API\n * @param toolMap - Map of tool names to executable tools\n * @returns Array of tool results\n */\nexport async function executeTools(\n toolCalls: t.PTCToolCall[],\n toolMap: t.ToolMap\n): Promise<t.PTCToolResult[]> {\n const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {\n const tool = toolMap.get(call.name);\n\n if (!tool) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,\n };\n }\n\n try {\n const result = await tool.invoke(call.input, {\n metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },\n });\n return {\n call_id: call.id,\n result,\n is_error: false,\n };\n } catch (error) {\n return {\n call_id: call.id,\n result: null,\n is_error: true,\n error_message: (error as Error).message || 'Tool execution failed',\n };\n }\n });\n\n return await Promise.all(executions);\n}\n\n/**\n * Formats the completed response for the agent.\n * @param response - The completed API response\n * @returns Tuple of [formatted string, artifact]\n */\nexport function formatCompletedResponse(\n response: t.ProgrammaticExecutionResponse\n): [string, t.ProgrammaticExecutionArtifact] {\n let formatted = '';\n\n if (response.stdout != null && response.stdout !== '') {\n formatted += `stdout:\\n${response.stdout}\\n`;\n } else {\n formatted += emptyOutputMessage;\n }\n\n if (response.stderr != null && response.stderr !== '') {\n formatted += `stderr:\\n${response.stderr}\\n`;\n }\n\n if (response.files && response.files.length > 0) {\n formatted += 'Generated files:\\n';\n\n const fileCount = response.files.length;\n for (let i = 0; i < fileCount; i++) {\n const file = response.files[i];\n const isImage = imageExtRegex.test(file.name);\n formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;\n\n if (i < fileCount - 1) {\n formatted += fileCount <= 3 ? ', ' : ',\\n';\n }\n }\n\n formatted += `\\nsession_id: ${response.session_id}\\n\\n${accessMessage}`;\n }\n\n return [\n formatted.trim(),\n {\n session_id: response.session_id,\n files: response.files,\n },\n ];\n}\n\n// ============================================================================\n// Tool Factory\n// ============================================================================\n\n/**\n * Creates a Programmatic Tool Calling tool for complex multi-tool workflows.\n *\n * This tool enables AI agents to write Python code that orchestrates multiple\n * tool calls programmatically, reducing LLM round-trips and token usage.\n *\n * The tool map must be provided at runtime via config.configurable.toolMap.\n *\n * @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)\n * @returns A LangChain DynamicStructuredTool for programmatic tool calling\n *\n * @example\n * const ptcTool = createProgrammaticToolCallingTool({\n * apiKey: process.env.CODE_API_KEY,\n * maxRoundTrips: 20\n * });\n *\n * const [output, artifact] = await ptcTool.invoke(\n * { code, tools },\n * { configurable: { toolMap } }\n * );\n */\nexport function createProgrammaticToolCallingTool(\n initParams: t.ProgrammaticToolCallingParams = {}\n): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {\n const apiKey =\n (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??\n initParams.apiKey ??\n getEnvironmentVariable(EnvVar.CODE_API_KEY) ??\n '';\n\n if (!apiKey) {\n throw new Error(\n 'No API key provided for programmatic tool calling. ' +\n 'Set CODE_API_KEY environment variable or pass apiKey in initParams.'\n );\n }\n\n const baseUrl = initParams.baseUrl ?? getCodeBaseURL();\n const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;\n const proxy = initParams.proxy ?? process.env.PROXY;\n const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';\n const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;\n\n const description = `\nRun tools by writing Python code. Tools are available as async functions - just call them with await.\n\nThis is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.\n\nUsage:\n- Tools are pre-defined as async functions - call them with await\n- Use asyncio.gather() to run multiple tools in parallel\n- Only print() output is returned - tool results stay in Python\n\nExamples:\n- Simple: result = await get_weather(city=\"NYC\")\n- Loop: for user in users: data = await get_expenses(user_id=user['id'])\n- Parallel: sf, ny = await asyncio.gather(get_weather(city=\"SF\"), get_weather(city=\"NY\"))\n\nWhen to use this instead of calling tools directly:\n- You need to call tools in a loop (process many items)\n- You want parallel execution (asyncio.gather)\n- You need conditionals based on tool results\n- You want to aggregate/filter data before returning\n`.trim();\n\n return tool<typeof ProgrammaticToolCallingSchema>(\n async (params, config) => {\n const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;\n\n // Extra params injected by ToolNode (follows web_search pattern)\n const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &\n Partial<t.ProgrammaticCache>;\n\n if (toolMap == null || toolMap.size === 0) {\n throw new Error(\n 'No toolMap provided. ' +\n 'ToolNode should inject this from AgentContext when invoked through the graph.'\n );\n }\n\n if (toolDefs == null || toolDefs.length === 0) {\n throw new Error(\n 'No tool definitions provided. ' +\n 'Either pass tools in the input or ensure ToolNode injects toolDefs.'\n );\n }\n\n let roundTrip = 0;\n\n try {\n // ====================================================================\n // Phase 1: Filter tools and make initial request\n // ====================================================================\n\n const effectiveTools = filterToolsByUsage(toolDefs, code, debug);\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Sending ${effectiveTools.length} tools to API ` +\n `(filtered from ${toolDefs.length})`\n );\n }\n\n let response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n code,\n tools: effectiveTools,\n session_id,\n timeout,\n },\n proxy\n );\n\n // ====================================================================\n // Phase 2: Handle response loop\n // ====================================================================\n\n while (response.status === 'tool_call_required') {\n roundTrip++;\n\n if (roundTrip > maxRoundTrips) {\n throw new Error(\n `Exceeded maximum round trips (${maxRoundTrips}). ` +\n 'This may indicate an infinite loop, excessive tool calls, ' +\n 'or a logic error in your code.'\n );\n }\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log(\n `[PTC Debug] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`\n );\n }\n\n const toolResults = await executeTools(\n response.tool_calls ?? [],\n toolMap\n );\n\n response = await makeRequest(\n EXEC_ENDPOINT,\n apiKey,\n {\n continuation_token: response.continuation_token,\n tool_results: toolResults,\n },\n proxy\n );\n }\n\n // ====================================================================\n // Phase 3: Handle final state\n // ====================================================================\n\n if (response.status === 'completed') {\n return formatCompletedResponse(response);\n }\n\n if (response.status === 'error') {\n throw new Error(\n `Execution error: ${response.error}` +\n (response.stderr != null && response.stderr !== ''\n ? `\\n\\nStderr:\\n${response.stderr}`\n : '')\n );\n }\n\n throw new Error(`Unexpected response status: ${response.status}`);\n } catch (error) {\n throw new Error(\n `Programmatic execution failed: ${(error as Error).message}`\n );\n }\n },\n {\n name: Constants.PROGRAMMATIC_TOOL_CALLING,\n description,\n schema: ProgrammaticToolCallingSchema,\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -14,21 +14,34 @@ declare const ProgrammaticToolCallingSchema: z.ZodObject<{
|
|
|
14
14
|
timeout?: number | undefined;
|
|
15
15
|
session_id?: string | undefined;
|
|
16
16
|
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Normalizes a tool name to Python identifier format.
|
|
19
|
+
* Must match the Code API's `normalizePythonFunctionName` exactly:
|
|
20
|
+
* 1. Replace hyphens and spaces with underscores
|
|
21
|
+
* 2. Remove any other invalid characters
|
|
22
|
+
* 3. Prefix with underscore if starts with number
|
|
23
|
+
* 4. Append `_tool` if it's a Python keyword
|
|
24
|
+
* @param name - The tool name to normalize
|
|
25
|
+
* @returns Normalized Python-safe identifier
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeToPythonIdentifier(name: string): string;
|
|
17
28
|
/**
|
|
18
29
|
* Extracts tool names that are actually called in the Python code.
|
|
19
|
-
*
|
|
30
|
+
* Handles hyphen/underscore conversion since Python identifiers use underscores.
|
|
20
31
|
* @param code - The Python code to analyze
|
|
21
|
-
* @param
|
|
22
|
-
* @returns Set of tool names found in the code
|
|
32
|
+
* @param toolNameMap - Map from normalized Python name to original tool name
|
|
33
|
+
* @returns Set of original tool names found in the code
|
|
23
34
|
*/
|
|
24
|
-
export declare function extractUsedToolNames(code: string,
|
|
35
|
+
export declare function extractUsedToolNames(code: string, toolNameMap: Map<string, string>): Set<string>;
|
|
25
36
|
/**
|
|
26
37
|
* Filters tool definitions to only include tools actually used in the code.
|
|
38
|
+
* Handles the hyphen-to-underscore conversion for Python compatibility.
|
|
27
39
|
* @param toolDefs - All available tool definitions
|
|
28
40
|
* @param code - The Python code to analyze
|
|
41
|
+
* @param debug - Enable debug logging
|
|
29
42
|
* @returns Filtered array of tool definitions
|
|
30
43
|
*/
|
|
31
|
-
export declare function filterToolsByUsage(toolDefs: t.LCTool[], code: string): t.LCTool[];
|
|
44
|
+
export declare function filterToolsByUsage(toolDefs: t.LCTool[], code: string, debug?: boolean): t.LCTool[];
|
|
32
45
|
/**
|
|
33
46
|
* Makes an HTTP request to the Code API.
|
|
34
47
|
* @param endpoint - The API endpoint URL
|
|
@@ -192,6 +192,8 @@ export type ProgrammaticToolCallingParams = {
|
|
|
192
192
|
maxRoundTrips?: number;
|
|
193
193
|
/** HTTP proxy URL */
|
|
194
194
|
proxy?: string;
|
|
195
|
+
/** Enable debug logging (or set PTC_DEBUG=true env var) */
|
|
196
|
+
debug?: boolean;
|
|
195
197
|
/** Environment variable key for API key */
|
|
196
198
|
[key: string]: unknown;
|
|
197
199
|
};
|
package/package.json
CHANGED
|
@@ -97,25 +97,90 @@ Requirements:
|
|
|
97
97
|
// Helper Functions
|
|
98
98
|
// ============================================================================
|
|
99
99
|
|
|
100
|
+
/** Python reserved keywords that get `_tool` suffix in Code API */
|
|
101
|
+
const PYTHON_KEYWORDS = new Set([
|
|
102
|
+
'False',
|
|
103
|
+
'None',
|
|
104
|
+
'True',
|
|
105
|
+
'and',
|
|
106
|
+
'as',
|
|
107
|
+
'assert',
|
|
108
|
+
'async',
|
|
109
|
+
'await',
|
|
110
|
+
'break',
|
|
111
|
+
'class',
|
|
112
|
+
'continue',
|
|
113
|
+
'def',
|
|
114
|
+
'del',
|
|
115
|
+
'elif',
|
|
116
|
+
'else',
|
|
117
|
+
'except',
|
|
118
|
+
'finally',
|
|
119
|
+
'for',
|
|
120
|
+
'from',
|
|
121
|
+
'global',
|
|
122
|
+
'if',
|
|
123
|
+
'import',
|
|
124
|
+
'in',
|
|
125
|
+
'is',
|
|
126
|
+
'lambda',
|
|
127
|
+
'nonlocal',
|
|
128
|
+
'not',
|
|
129
|
+
'or',
|
|
130
|
+
'pass',
|
|
131
|
+
'raise',
|
|
132
|
+
'return',
|
|
133
|
+
'try',
|
|
134
|
+
'while',
|
|
135
|
+
'with',
|
|
136
|
+
'yield',
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Normalizes a tool name to Python identifier format.
|
|
141
|
+
* Must match the Code API's `normalizePythonFunctionName` exactly:
|
|
142
|
+
* 1. Replace hyphens and spaces with underscores
|
|
143
|
+
* 2. Remove any other invalid characters
|
|
144
|
+
* 3. Prefix with underscore if starts with number
|
|
145
|
+
* 4. Append `_tool` if it's a Python keyword
|
|
146
|
+
* @param name - The tool name to normalize
|
|
147
|
+
* @returns Normalized Python-safe identifier
|
|
148
|
+
*/
|
|
149
|
+
export function normalizeToPythonIdentifier(name: string): string {
|
|
150
|
+
let normalized = name.replace(/[-\s]/g, '_');
|
|
151
|
+
|
|
152
|
+
normalized = normalized.replace(/[^a-zA-Z0-9_]/g, '');
|
|
153
|
+
|
|
154
|
+
if (/^[0-9]/.test(normalized)) {
|
|
155
|
+
normalized = '_' + normalized;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (PYTHON_KEYWORDS.has(normalized)) {
|
|
159
|
+
normalized = normalized + '_tool';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
|
164
|
+
|
|
100
165
|
/**
|
|
101
166
|
* Extracts tool names that are actually called in the Python code.
|
|
102
|
-
*
|
|
167
|
+
* Handles hyphen/underscore conversion since Python identifiers use underscores.
|
|
103
168
|
* @param code - The Python code to analyze
|
|
104
|
-
* @param
|
|
105
|
-
* @returns Set of tool names found in the code
|
|
169
|
+
* @param toolNameMap - Map from normalized Python name to original tool name
|
|
170
|
+
* @returns Set of original tool names found in the code
|
|
106
171
|
*/
|
|
107
172
|
export function extractUsedToolNames(
|
|
108
173
|
code: string,
|
|
109
|
-
|
|
174
|
+
toolNameMap: Map<string, string>
|
|
110
175
|
): Set<string> {
|
|
111
176
|
const usedTools = new Set<string>();
|
|
112
177
|
|
|
113
|
-
for (const
|
|
114
|
-
const escapedName =
|
|
178
|
+
for (const [pythonName, originalName] of toolNameMap) {
|
|
179
|
+
const escapedName = pythonName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
115
180
|
const pattern = new RegExp(`\\b${escapedName}\\s*\\(`, 'g');
|
|
116
181
|
|
|
117
182
|
if (pattern.test(code)) {
|
|
118
|
-
usedTools.add(
|
|
183
|
+
usedTools.add(originalName);
|
|
119
184
|
}
|
|
120
185
|
}
|
|
121
186
|
|
|
@@ -124,18 +189,45 @@ export function extractUsedToolNames(
|
|
|
124
189
|
|
|
125
190
|
/**
|
|
126
191
|
* Filters tool definitions to only include tools actually used in the code.
|
|
192
|
+
* Handles the hyphen-to-underscore conversion for Python compatibility.
|
|
127
193
|
* @param toolDefs - All available tool definitions
|
|
128
194
|
* @param code - The Python code to analyze
|
|
195
|
+
* @param debug - Enable debug logging
|
|
129
196
|
* @returns Filtered array of tool definitions
|
|
130
197
|
*/
|
|
131
198
|
export function filterToolsByUsage(
|
|
132
199
|
toolDefs: t.LCTool[],
|
|
133
|
-
code: string
|
|
200
|
+
code: string,
|
|
201
|
+
debug = false
|
|
134
202
|
): t.LCTool[] {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
203
|
+
const toolNameMap = new Map<string, string>();
|
|
204
|
+
for (const tool of toolDefs) {
|
|
205
|
+
const pythonName = normalizeToPythonIdentifier(tool.name);
|
|
206
|
+
toolNameMap.set(pythonName, tool.name);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const usedToolNames = extractUsedToolNames(code, toolNameMap);
|
|
210
|
+
|
|
211
|
+
if (debug) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.log(
|
|
214
|
+
`[PTC Debug] Tool filtering: found ${usedToolNames.size}/${toolDefs.length} tools in code`
|
|
215
|
+
);
|
|
216
|
+
if (usedToolNames.size > 0) {
|
|
217
|
+
// eslint-disable-next-line no-console
|
|
218
|
+
console.log(
|
|
219
|
+
`[PTC Debug] Matched tools: ${Array.from(usedToolNames).join(', ')}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
137
223
|
|
|
138
224
|
if (usedToolNames.size === 0) {
|
|
225
|
+
if (debug) {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.log(
|
|
228
|
+
'[PTC Debug] No tools detected in code - sending all tools as fallback'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
139
231
|
return toolDefs;
|
|
140
232
|
}
|
|
141
233
|
|
|
@@ -318,6 +410,7 @@ export function createProgrammaticToolCallingTool(
|
|
|
318
410
|
const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
|
|
319
411
|
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
|
|
320
412
|
const proxy = initParams.proxy ?? process.env.PROXY;
|
|
413
|
+
const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
|
|
321
414
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
322
415
|
|
|
323
416
|
const description = `
|
|
@@ -371,7 +464,15 @@ When to use this instead of calling tools directly:
|
|
|
371
464
|
// Phase 1: Filter tools and make initial request
|
|
372
465
|
// ====================================================================
|
|
373
466
|
|
|
374
|
-
const effectiveTools = filterToolsByUsage(toolDefs, code);
|
|
467
|
+
const effectiveTools = filterToolsByUsage(toolDefs, code, debug);
|
|
468
|
+
|
|
469
|
+
if (debug) {
|
|
470
|
+
// eslint-disable-next-line no-console
|
|
471
|
+
console.log(
|
|
472
|
+
`[PTC Debug] Sending ${effectiveTools.length} tools to API ` +
|
|
473
|
+
`(filtered from ${toolDefs.length})`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
375
476
|
|
|
376
477
|
let response = await makeRequest(
|
|
377
478
|
EXEC_ENDPOINT,
|
|
@@ -400,10 +501,12 @@ When to use this instead of calling tools directly:
|
|
|
400
501
|
);
|
|
401
502
|
}
|
|
402
503
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
504
|
+
if (debug) {
|
|
505
|
+
// eslint-disable-next-line no-console
|
|
506
|
+
console.log(
|
|
507
|
+
`[PTC Debug] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
407
510
|
|
|
408
511
|
const toolResults = await executeTools(
|
|
409
512
|
response.tool_calls ?? [],
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
extractUsedToolNames,
|
|
12
12
|
filterToolsByUsage,
|
|
13
13
|
executeTools,
|
|
14
|
+
normalizeToPythonIdentifier,
|
|
14
15
|
} from '../ProgrammaticToolCalling';
|
|
15
16
|
import {
|
|
16
17
|
createProgrammaticToolRegistry,
|
|
@@ -185,8 +186,58 @@ describe('ProgrammaticToolCalling', () => {
|
|
|
185
186
|
});
|
|
186
187
|
});
|
|
187
188
|
|
|
189
|
+
describe('normalizeToPythonIdentifier', () => {
|
|
190
|
+
it('converts hyphens to underscores', () => {
|
|
191
|
+
expect(normalizeToPythonIdentifier('my-tool-name')).toBe('my_tool_name');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('converts spaces to underscores', () => {
|
|
195
|
+
expect(normalizeToPythonIdentifier('my tool name')).toBe('my_tool_name');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('leaves underscores unchanged', () => {
|
|
199
|
+
expect(normalizeToPythonIdentifier('my_tool_name')).toBe('my_tool_name');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('handles mixed hyphens and underscores', () => {
|
|
203
|
+
expect(normalizeToPythonIdentifier('my-tool_name-v2')).toBe(
|
|
204
|
+
'my_tool_name_v2'
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('handles MCP-style names with hyphens', () => {
|
|
209
|
+
expect(
|
|
210
|
+
normalizeToPythonIdentifier('create_spreadsheet_mcp_Google-Workspace')
|
|
211
|
+
).toBe('create_spreadsheet_mcp_Google_Workspace');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('removes invalid characters', () => {
|
|
215
|
+
expect(normalizeToPythonIdentifier('tool@name!v2')).toBe('toolnamev2');
|
|
216
|
+
expect(normalizeToPythonIdentifier('get.data.v2')).toBe('getdatav2');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('prefixes with underscore if starts with number', () => {
|
|
220
|
+
expect(normalizeToPythonIdentifier('123tool')).toBe('_123tool');
|
|
221
|
+
expect(normalizeToPythonIdentifier('1-tool')).toBe('_1_tool');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('appends _tool suffix for Python keywords', () => {
|
|
225
|
+
expect(normalizeToPythonIdentifier('return')).toBe('return_tool');
|
|
226
|
+
expect(normalizeToPythonIdentifier('async')).toBe('async_tool');
|
|
227
|
+
expect(normalizeToPythonIdentifier('import')).toBe('import_tool');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
188
231
|
describe('extractUsedToolNames', () => {
|
|
189
|
-
const
|
|
232
|
+
const createToolMap = (names: string[]): Map<string, string> => {
|
|
233
|
+
const map = new Map<string, string>();
|
|
234
|
+
for (const name of names) {
|
|
235
|
+
map.set(normalizeToPythonIdentifier(name), name);
|
|
236
|
+
}
|
|
237
|
+
return map;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const availableTools = createToolMap([
|
|
190
241
|
'get_weather',
|
|
191
242
|
'get_team_members',
|
|
192
243
|
'get_expenses',
|
|
@@ -262,16 +313,30 @@ x = 1 + 2`;
|
|
|
262
313
|
expect(used.size).toBe(0);
|
|
263
314
|
});
|
|
264
315
|
|
|
265
|
-
it('handles tool names with special
|
|
266
|
-
const specialTools =
|
|
267
|
-
const code = `await
|
|
268
|
-
await
|
|
316
|
+
it('handles tool names with special characters via normalization', () => {
|
|
317
|
+
const specialTools = createToolMap(['get_data.v2', 'calc+plus']);
|
|
318
|
+
const code = `await get_datav2()
|
|
319
|
+
await calcplus()`;
|
|
269
320
|
|
|
270
321
|
const used = extractUsedToolNames(code, specialTools);
|
|
271
322
|
|
|
272
323
|
expect(used.has('get_data.v2')).toBe(true);
|
|
273
324
|
expect(used.has('calc+plus')).toBe(true);
|
|
274
325
|
});
|
|
326
|
+
|
|
327
|
+
it('matches hyphenated tool names using underscore in code', () => {
|
|
328
|
+
const mcpTools = createToolMap([
|
|
329
|
+
'create_spreadsheet_mcp_Google-Workspace',
|
|
330
|
+
'search_gmail_mcp_Google-Workspace',
|
|
331
|
+
]);
|
|
332
|
+
const code = `result = await create_spreadsheet_mcp_Google_Workspace(title="Test")
|
|
333
|
+
print(result)`;
|
|
334
|
+
|
|
335
|
+
const used = extractUsedToolNames(code, mcpTools);
|
|
336
|
+
|
|
337
|
+
expect(used.size).toBe(1);
|
|
338
|
+
expect(used.has('create_spreadsheet_mcp_Google-Workspace')).toBe(true);
|
|
339
|
+
});
|
|
275
340
|
});
|
|
276
341
|
|
|
277
342
|
describe('filterToolsByUsage', () => {
|
package/src/types/tools.ts
CHANGED
|
@@ -239,6 +239,8 @@ export type ProgrammaticToolCallingParams = {
|
|
|
239
239
|
maxRoundTrips?: number;
|
|
240
240
|
/** HTTP proxy URL */
|
|
241
241
|
proxy?: string;
|
|
242
|
+
/** Enable debug logging (or set PTC_DEBUG=true env var) */
|
|
243
|
+
debug?: boolean;
|
|
242
244
|
/** Environment variable key for API key */
|
|
243
245
|
[key: string]: unknown;
|
|
244
246
|
};
|