@librechat/agents 3.0.42 → 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/agents/AgentContext.cjs +134 -70
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +1 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +85 -0
- package/dist/cjs/messages/tools.cjs.map +1 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +143 -34
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +30 -13
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +134 -70
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +8 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +82 -0
- package/dist/esm/messages/tools.mjs.map +1 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +141 -35
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +30 -13
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +37 -17
- package/dist/types/common/enum.d.ts +1 -1
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/tools.d.ts +17 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +28 -23
- package/dist/types/tools/ToolNode.d.ts +9 -7
- package/dist/types/types/tools.d.ts +7 -5
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +157 -85
- package/src/agents/__tests__/AgentContext.test.ts +805 -0
- package/src/common/enum.ts +1 -1
- package/src/graphs/Graph.ts +9 -21
- package/src/messages/__tests__/tools.test.ts +473 -0
- package/src/messages/index.ts +1 -0
- package/src/messages/tools.ts +99 -0
- package/src/scripts/code_exec_ptc.ts +78 -21
- package/src/scripts/programmatic_exec.ts +3 -3
- package/src/scripts/programmatic_exec_agent.ts +4 -4
- package/src/tools/ProgrammaticToolCalling.ts +178 -43
- package/src/tools/ToolNode.ts +33 -14
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +9 -9
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +245 -5
- package/src/types/tools.ts +5 -5
|
@@ -18,25 +18,79 @@ import type { RunnableConfig } from '@langchain/core/runnables';
|
|
|
18
18
|
import type * as t from '@/types';
|
|
19
19
|
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// createProgrammaticToolRegistry,
|
|
22
|
+
createGetTeamMembersTool,
|
|
23
|
+
createGetExpensesTool,
|
|
24
|
+
createGetWeatherTool,
|
|
25
|
+
} from '@/test/mockTools';
|
|
26
|
+
import {
|
|
23
27
|
createMetadataAggregator,
|
|
28
|
+
ModelEndHandler,
|
|
29
|
+
ToolEndHandler,
|
|
24
30
|
} from '@/events';
|
|
31
|
+
import { createProgrammaticToolCallingTool } from '@/tools/ProgrammaticToolCalling';
|
|
32
|
+
import { createCodeExecutionTool } from '@/tools/CodeExecutor';
|
|
25
33
|
import { getLLMConfig } from '@/utils/llmConfig';
|
|
26
34
|
import { getArgs } from '@/scripts/args';
|
|
27
35
|
import { GraphEvents } from '@/common';
|
|
28
36
|
import { Run } from '@/run';
|
|
29
|
-
import { createCodeExecutionTool } from '@/tools/CodeExecutor';
|
|
30
|
-
import { createProgrammaticToolCallingTool } from '@/tools/ProgrammaticToolCalling';
|
|
31
|
-
import {
|
|
32
|
-
createGetTeamMembersTool,
|
|
33
|
-
createGetExpensesTool,
|
|
34
|
-
createGetWeatherTool,
|
|
35
|
-
createProgrammaticToolRegistry,
|
|
36
|
-
} from '@/test/mockTools';
|
|
37
37
|
|
|
38
38
|
const conversationHistory: BaseMessage[] = [];
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Creates a tool registry where ALL business tools are code_execution ONLY.
|
|
42
|
+
* This forces the LLM to use PTC - it cannot call these tools directly.
|
|
43
|
+
*/
|
|
44
|
+
function createPTCOnlyToolRegistry(): t.LCToolRegistry {
|
|
45
|
+
const toolDefs: t.LCTool[] = [
|
|
46
|
+
{
|
|
47
|
+
name: 'get_team_members',
|
|
48
|
+
description:
|
|
49
|
+
'Get list of team members. Returns array of objects with id, name, and department fields.',
|
|
50
|
+
parameters: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {},
|
|
53
|
+
required: [],
|
|
54
|
+
},
|
|
55
|
+
allowed_callers: ['code_execution'], // PTC ONLY - not direct
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'get_expenses',
|
|
59
|
+
description:
|
|
60
|
+
'Get expense records for a user. Returns array of objects with amount and category fields.',
|
|
61
|
+
parameters: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
user_id: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'The user ID to fetch expenses for',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['user_id'],
|
|
70
|
+
},
|
|
71
|
+
allowed_callers: ['code_execution'], // PTC ONLY - not direct
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'get_weather',
|
|
75
|
+
description:
|
|
76
|
+
'Get current weather for a city. Returns object with temperature (number) and condition (string) fields.',
|
|
77
|
+
parameters: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
city: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'City name',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['city'],
|
|
86
|
+
},
|
|
87
|
+
allowed_callers: ['code_execution'], // PTC ONLY - not direct (changed from ['direct', 'code_execution'])
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
return new Map(toolDefs.map((def) => [def.name, def]));
|
|
92
|
+
}
|
|
93
|
+
|
|
40
94
|
async function testProgrammaticToolCalling(): Promise<void> {
|
|
41
95
|
const { userName, location, provider, currentDate } = await getArgs();
|
|
42
96
|
const { contentParts, aggregateContent } = createContentAggregator();
|
|
@@ -111,10 +165,10 @@ async function testProgrammaticToolCalling(): Promise<void> {
|
|
|
111
165
|
const allTools = [teamTool, expensesTool, weatherTool, codeExecTool, ptcTool];
|
|
112
166
|
const toolMap = new Map(allTools.map((t) => [t.name, t]));
|
|
113
167
|
|
|
114
|
-
// Create tool registry
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
const toolRegistry =
|
|
168
|
+
// Create tool registry where ALL business tools are PTC-only
|
|
169
|
+
// This means the LLM CANNOT call get_team_members, get_expenses, get_weather directly
|
|
170
|
+
// It MUST use run_tools_with_code to invoke them
|
|
171
|
+
const toolRegistry = createPTCOnlyToolRegistry();
|
|
118
172
|
|
|
119
173
|
console.log('\n' + '='.repeat(70));
|
|
120
174
|
console.log('Tool Configuration Summary:');
|
|
@@ -150,11 +204,14 @@ async function testProgrammaticToolCalling(): Promise<void> {
|
|
|
150
204
|
toolMap,
|
|
151
205
|
toolRegistry,
|
|
152
206
|
instructions:
|
|
153
|
-
'You are a friendly AI assistant with advanced coding capabilities
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
207
|
+
'You are a friendly AI assistant with advanced coding capabilities.\n\n' +
|
|
208
|
+
'IMPORTANT: The tools get_team_members(), get_expenses(), and get_weather() are NOT available ' +
|
|
209
|
+
'for direct function calling. You MUST use the run_tools_with_code tool to invoke them.\n\n' +
|
|
210
|
+
'When you need to use these tools, write Python code using run_tools_with_code that calls:\n' +
|
|
211
|
+
'- await get_team_members() - returns list of team members\n' +
|
|
212
|
+
'- await get_expenses(user_id="...") - returns expenses for a user\n' +
|
|
213
|
+
'- await get_weather(city="...") - returns weather data\n\n' +
|
|
214
|
+
'Use asyncio.gather() for parallel execution when calling multiple tools.',
|
|
158
215
|
additional_instructions: `The user's name is ${userName} and they are located in ${location}. Today is ${currentDate}.`,
|
|
159
216
|
},
|
|
160
217
|
],
|
|
@@ -187,7 +244,7 @@ async function testProgrammaticToolCalling(): Promise<void> {
|
|
|
187
244
|
4. Identify anyone who spent more than $500
|
|
188
245
|
5. Show me a summary report
|
|
189
246
|
|
|
190
|
-
IMPORTANT: Use the
|
|
247
|
+
IMPORTANT: Use the run_tools_with_code tool to do this efficiently.
|
|
191
248
|
Don't call each tool separately - write Python code that orchestrates all the calls!`;
|
|
192
249
|
|
|
193
250
|
conversationHistory.push(new HumanMessage(userMessage1));
|
|
@@ -217,7 +274,7 @@ Don't call each tool separately - write Python code that orchestrates all the ca
|
|
|
217
274
|
3. For the Engineering team members only, calculate their travel expenses
|
|
218
275
|
4. Show me the results
|
|
219
276
|
|
|
220
|
-
Again, use
|
|
277
|
+
Again, use run_tools_with_code for maximum efficiency. Use asyncio.gather()
|
|
221
278
|
to check both cities' weather at the same time!`;
|
|
222
279
|
|
|
223
280
|
conversationHistory.push(new HumanMessage(userMessage2));
|
|
@@ -353,8 +353,8 @@ print(f"Temperature difference: {difference}°F")
|
|
|
353
353
|
console.log('='.repeat(70));
|
|
354
354
|
console.log(
|
|
355
355
|
'\nWhen PTC is invoked through ToolNode in a real agent:\n' +
|
|
356
|
-
'- ToolNode detects call.name === "
|
|
357
|
-
'- ToolNode injects: { ...invokeParams, toolMap,
|
|
356
|
+
'- ToolNode detects call.name === "run_tools_with_code"\n' +
|
|
357
|
+
'- ToolNode injects: { ...invokeParams, toolMap, toolDefs }\n' +
|
|
358
358
|
'- PTC tool extracts these from params (not from config)\n' +
|
|
359
359
|
'- No explicit tools parameter needed in schema\n\n' +
|
|
360
360
|
'This test demonstrates param injection with explicit tools:\n'
|
|
@@ -364,7 +364,7 @@ print(f"Temperature difference: {difference}°F")
|
|
|
364
364
|
ptcTool,
|
|
365
365
|
'Runtime injection with explicit tools',
|
|
366
366
|
`
|
|
367
|
-
# ToolNode would inject toolMap+
|
|
367
|
+
# ToolNode would inject toolMap+toolDefs
|
|
368
368
|
# For this test, we pass tools explicitly (same effect)
|
|
369
369
|
team = await get_team_members()
|
|
370
370
|
print(f"Team size: {len(team)}")
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Tool registry only needs business logic tools that require filtering.
|
|
43
|
-
* Special tools (execute_code,
|
|
43
|
+
* Special tools (execute_code, run_tools_with_code, tool_search_regex)
|
|
44
44
|
* are always bound directly to the LLM and don't need registry entries.
|
|
45
45
|
*/
|
|
46
46
|
function createAgentToolRegistry(): t.LCToolRegistry {
|
|
@@ -133,7 +133,7 @@ async function main(): Promise<void> {
|
|
|
133
133
|
instructions:
|
|
134
134
|
'You are an AI assistant with access to programmatic tool calling. ' +
|
|
135
135
|
'When you need to process multiple items or perform complex data operations, ' +
|
|
136
|
-
'use the
|
|
136
|
+
'use the run_tools_with_code tool to write Python code that calls tools efficiently.',
|
|
137
137
|
},
|
|
138
138
|
],
|
|
139
139
|
},
|
|
@@ -164,7 +164,7 @@ async function main(): Promise<void> {
|
|
|
164
164
|
4. Identify anyone who spent more than $300
|
|
165
165
|
5. Show me the results in a nice format
|
|
166
166
|
|
|
167
|
-
Use the
|
|
167
|
+
Use the run_tools_with_code tool to do this efficiently - don't call each tool separately!`
|
|
168
168
|
);
|
|
169
169
|
|
|
170
170
|
conversationHistory.push(userMessage);
|
|
@@ -199,7 +199,7 @@ Use the programmatic_code_execution tool to do this efficiently - don't call eac
|
|
|
199
199
|
console.log('='.repeat(70));
|
|
200
200
|
console.log('\nKey observations:');
|
|
201
201
|
console.log(
|
|
202
|
-
'1. LLM only sees tools with allowed_callers including "direct" (get_weather, execute_code,
|
|
202
|
+
'1. LLM only sees tools with allowed_callers including "direct" (get_weather, execute_code, run_tools_with_code, tool_search_regex)'
|
|
203
203
|
);
|
|
204
204
|
console.log(
|
|
205
205
|
'2. When PTC is invoked, ToolNode automatically injects programmatic tools (get_team_members, get_expenses, get_weather)'
|
|
@@ -5,6 +5,7 @@ import fetch, { RequestInit } from 'node-fetch';
|
|
|
5
5
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
6
6
|
import { getEnvironmentVariable } from '@langchain/core/utils/env';
|
|
7
7
|
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
8
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
8
9
|
import type * as t from '@/types';
|
|
9
10
|
import { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
|
|
10
11
|
import { EnvVar, Constants } from '@/common';
|
|
@@ -74,18 +75,6 @@ Requirements:
|
|
|
74
75
|
- Only print() output flows back to the context window
|
|
75
76
|
- Tool results from programmatic calls do NOT consume context tokens`
|
|
76
77
|
),
|
|
77
|
-
tools: z
|
|
78
|
-
.array(
|
|
79
|
-
z.object({
|
|
80
|
-
name: z.string(),
|
|
81
|
-
description: z.string().optional(),
|
|
82
|
-
parameters: z.any(), // JsonSchemaType
|
|
83
|
-
})
|
|
84
|
-
)
|
|
85
|
-
.optional()
|
|
86
|
-
.describe(
|
|
87
|
-
'Optional array of tool definitions that can be called from the code. If not provided, uses programmatic tools configured in the agent context. Tool names must match tools available in the toolMap.'
|
|
88
|
-
),
|
|
89
78
|
session_id: z
|
|
90
79
|
.string()
|
|
91
80
|
.optional()
|
|
@@ -108,6 +97,143 @@ Requirements:
|
|
|
108
97
|
// Helper Functions
|
|
109
98
|
// ============================================================================
|
|
110
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
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extracts tool names that are actually called in the Python code.
|
|
167
|
+
* Handles hyphen/underscore conversion since Python identifiers use underscores.
|
|
168
|
+
* @param code - The Python code to analyze
|
|
169
|
+
* @param toolNameMap - Map from normalized Python name to original tool name
|
|
170
|
+
* @returns Set of original tool names found in the code
|
|
171
|
+
*/
|
|
172
|
+
export function extractUsedToolNames(
|
|
173
|
+
code: string,
|
|
174
|
+
toolNameMap: Map<string, string>
|
|
175
|
+
): Set<string> {
|
|
176
|
+
const usedTools = new Set<string>();
|
|
177
|
+
|
|
178
|
+
for (const [pythonName, originalName] of toolNameMap) {
|
|
179
|
+
const escapedName = pythonName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
180
|
+
const pattern = new RegExp(`\\b${escapedName}\\s*\\(`, 'g');
|
|
181
|
+
|
|
182
|
+
if (pattern.test(code)) {
|
|
183
|
+
usedTools.add(originalName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return usedTools;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Filters tool definitions to only include tools actually used in the code.
|
|
192
|
+
* Handles the hyphen-to-underscore conversion for Python compatibility.
|
|
193
|
+
* @param toolDefs - All available tool definitions
|
|
194
|
+
* @param code - The Python code to analyze
|
|
195
|
+
* @param debug - Enable debug logging
|
|
196
|
+
* @returns Filtered array of tool definitions
|
|
197
|
+
*/
|
|
198
|
+
export function filterToolsByUsage(
|
|
199
|
+
toolDefs: t.LCTool[],
|
|
200
|
+
code: string,
|
|
201
|
+
debug = false
|
|
202
|
+
): t.LCTool[] {
|
|
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
|
+
}
|
|
223
|
+
|
|
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
|
+
}
|
|
231
|
+
return toolDefs;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return toolDefs.filter((tool) => usedToolNames.has(tool.name));
|
|
235
|
+
}
|
|
236
|
+
|
|
111
237
|
/**
|
|
112
238
|
* Makes an HTTP request to the Code API.
|
|
113
239
|
* @param endpoint - The API endpoint URL
|
|
@@ -284,38 +410,38 @@ export function createProgrammaticToolCallingTool(
|
|
|
284
410
|
const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
|
|
285
411
|
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
|
|
286
412
|
const proxy = initParams.proxy ?? process.env.PROXY;
|
|
413
|
+
const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
|
|
287
414
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
288
415
|
|
|
289
416
|
const description = `
|
|
290
|
-
|
|
417
|
+
Run tools by writing Python code. Tools are available as async functions - just call them with await.
|
|
418
|
+
|
|
419
|
+
This is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.
|
|
291
420
|
|
|
292
421
|
Usage:
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
- Simple: result = await get_data()
|
|
308
|
-
- Loop: for item in items: data = await fetch(item)
|
|
309
|
-
- Parallel: results = await asyncio.gather(t1(), t2(), t3())
|
|
310
|
-
- Conditional: if x: await tool_a() else: await tool_b()
|
|
422
|
+
- Tools are pre-defined as async functions - call them with await
|
|
423
|
+
- Use asyncio.gather() to run multiple tools in parallel
|
|
424
|
+
- Only print() output is returned - tool results stay in Python
|
|
425
|
+
|
|
426
|
+
Examples:
|
|
427
|
+
- Simple: result = await get_weather(city="NYC")
|
|
428
|
+
- Loop: for user in users: data = await get_expenses(user_id=user['id'])
|
|
429
|
+
- Parallel: sf, ny = await asyncio.gather(get_weather(city="SF"), get_weather(city="NY"))
|
|
430
|
+
|
|
431
|
+
When to use this instead of calling tools directly:
|
|
432
|
+
- You need to call tools in a loop (process many items)
|
|
433
|
+
- You want parallel execution (asyncio.gather)
|
|
434
|
+
- You need conditionals based on tool results
|
|
435
|
+
- You want to aggregate/filter data before returning
|
|
311
436
|
`.trim();
|
|
312
437
|
|
|
313
438
|
return tool<typeof ProgrammaticToolCallingSchema>(
|
|
314
439
|
async (params, config) => {
|
|
315
|
-
const { code,
|
|
440
|
+
const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;
|
|
316
441
|
|
|
317
442
|
// Extra params injected by ToolNode (follows web_search pattern)
|
|
318
|
-
const { toolMap,
|
|
443
|
+
const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &
|
|
444
|
+
Partial<t.ProgrammaticCache>;
|
|
319
445
|
|
|
320
446
|
if (toolMap == null || toolMap.size === 0) {
|
|
321
447
|
throw new Error(
|
|
@@ -324,13 +450,10 @@ Patterns:
|
|
|
324
450
|
);
|
|
325
451
|
}
|
|
326
452
|
|
|
327
|
-
|
|
328
|
-
const effectiveTools = tools ?? programmaticToolDefs;
|
|
329
|
-
|
|
330
|
-
if (effectiveTools == null || effectiveTools.length === 0) {
|
|
453
|
+
if (toolDefs == null || toolDefs.length === 0) {
|
|
331
454
|
throw new Error(
|
|
332
455
|
'No tool definitions provided. ' +
|
|
333
|
-
'Either pass tools in the input or ensure ToolNode injects
|
|
456
|
+
'Either pass tools in the input or ensure ToolNode injects toolDefs.'
|
|
334
457
|
);
|
|
335
458
|
}
|
|
336
459
|
|
|
@@ -338,9 +461,19 @@ Patterns:
|
|
|
338
461
|
|
|
339
462
|
try {
|
|
340
463
|
// ====================================================================
|
|
341
|
-
// Phase 1:
|
|
464
|
+
// Phase 1: Filter tools and make initial request
|
|
342
465
|
// ====================================================================
|
|
343
466
|
|
|
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
|
+
}
|
|
476
|
+
|
|
344
477
|
let response = await makeRequest(
|
|
345
478
|
EXEC_ENDPOINT,
|
|
346
479
|
apiKey,
|
|
@@ -368,10 +501,12 @@ Patterns:
|
|
|
368
501
|
);
|
|
369
502
|
}
|
|
370
503
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
}
|
|
375
510
|
|
|
376
511
|
const toolResults = await executeTools(
|
|
377
512
|
response.tool_calls ?? [],
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -31,7 +31,6 @@ function isSend(value: unknown): value is Send {
|
|
|
31
31
|
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
33
|
export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
34
|
-
tools: t.GenericTool[];
|
|
35
34
|
private toolMap: Map<string, StructuredToolInterface | RunnableToolLike>;
|
|
36
35
|
private loadRuntimeTools?: t.ToolRefGenerator;
|
|
37
36
|
handleToolErrors = true;
|
|
@@ -39,12 +38,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
39
38
|
toolCallStepIds?: Map<string, string>;
|
|
40
39
|
errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
|
|
41
40
|
private toolUsageCount: Map<string, number>;
|
|
42
|
-
/**
|
|
43
|
-
private programmaticToolMap?: t.ToolMap;
|
|
44
|
-
/** Tool definitions for programmatic code execution (sent to Code API) */
|
|
45
|
-
private programmaticToolDefs?: t.LCTool[];
|
|
46
|
-
/** Tool registry for tool search (deferred tools) */
|
|
41
|
+
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
47
42
|
private toolRegistry?: t.LCToolRegistry;
|
|
43
|
+
/** Cached programmatic tools (computed once on first PTC call) */
|
|
44
|
+
private programmaticCache?: t.ProgrammaticCache;
|
|
48
45
|
|
|
49
46
|
constructor({
|
|
50
47
|
tools,
|
|
@@ -55,23 +52,44 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
55
52
|
toolCallStepIds,
|
|
56
53
|
handleToolErrors,
|
|
57
54
|
loadRuntimeTools,
|
|
58
|
-
programmaticToolMap,
|
|
59
|
-
programmaticToolDefs,
|
|
60
55
|
toolRegistry,
|
|
61
56
|
}: t.ToolNodeConstructorParams) {
|
|
62
57
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
63
|
-
this.tools = tools;
|
|
64
58
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
65
59
|
this.toolCallStepIds = toolCallStepIds;
|
|
66
60
|
this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
|
|
67
61
|
this.loadRuntimeTools = loadRuntimeTools;
|
|
68
62
|
this.errorHandler = errorHandler;
|
|
69
63
|
this.toolUsageCount = new Map<string, number>();
|
|
70
|
-
this.programmaticToolMap = programmaticToolMap;
|
|
71
|
-
this.programmaticToolDefs = programmaticToolDefs;
|
|
72
64
|
this.toolRegistry = toolRegistry;
|
|
73
65
|
}
|
|
74
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Returns cached programmatic tools, computing once on first access.
|
|
69
|
+
* Single iteration builds both toolMap and toolDefs simultaneously.
|
|
70
|
+
*/
|
|
71
|
+
private getProgrammaticTools(): { toolMap: t.ToolMap; toolDefs: t.LCTool[] } {
|
|
72
|
+
if (this.programmaticCache) return this.programmaticCache;
|
|
73
|
+
|
|
74
|
+
const toolMap: t.ToolMap = new Map();
|
|
75
|
+
const toolDefs: t.LCTool[] = [];
|
|
76
|
+
|
|
77
|
+
if (this.toolRegistry) {
|
|
78
|
+
for (const [name, toolDef] of this.toolRegistry) {
|
|
79
|
+
if (
|
|
80
|
+
(toolDef.allowed_callers ?? ['direct']).includes('code_execution')
|
|
81
|
+
) {
|
|
82
|
+
toolDefs.push(toolDef);
|
|
83
|
+
const tool = this.toolMap.get(name);
|
|
84
|
+
if (tool) toolMap.set(name, tool);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.programmaticCache = { toolMap, toolDefs };
|
|
90
|
+
return this.programmaticCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
/**
|
|
76
94
|
* Returns a snapshot of the current tool usage counts.
|
|
77
95
|
* @returns A ReadonlyMap where keys are tool names and values are their usage counts.
|
|
@@ -108,10 +126,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
108
126
|
|
|
109
127
|
// Inject runtime data for special tools (becomes available at config.toolCall)
|
|
110
128
|
if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
|
|
129
|
+
const { toolMap, toolDefs } = this.getProgrammaticTools();
|
|
111
130
|
invokeParams = {
|
|
112
131
|
...invokeParams,
|
|
113
|
-
toolMap
|
|
114
|
-
|
|
132
|
+
toolMap,
|
|
133
|
+
toolDefs,
|
|
115
134
|
};
|
|
116
135
|
} else if (call.name === Constants.TOOL_SEARCH_REGEX) {
|
|
117
136
|
invokeParams = {
|
|
@@ -228,9 +247,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
228
247
|
const { tools, toolMap } = this.loadRuntimeTools(
|
|
229
248
|
aiMessage.tool_calls ?? []
|
|
230
249
|
);
|
|
231
|
-
this.tools = tools;
|
|
232
250
|
this.toolMap =
|
|
233
251
|
toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
252
|
+
this.programmaticCache = undefined; // Invalidate cache on toolMap change
|
|
234
253
|
}
|
|
235
254
|
|
|
236
255
|
outputs = await Promise.all(
|