@librechat/agents 3.0.42 → 3.0.43
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 +5 -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 +55 -32
- 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 +54 -33
- 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 +15 -23
- package/dist/types/tools/ToolNode.d.ts +9 -7
- package/dist/types/types/tools.d.ts +5 -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 +71 -39
- package/src/tools/ToolNode.ts +33 -14
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +9 -9
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +180 -5
- package/src/types/tools.ts +3 -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,51 @@ Requirements:
|
|
|
108
97
|
// Helper Functions
|
|
109
98
|
// ============================================================================
|
|
110
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Extracts tool names that are actually called in the Python code.
|
|
102
|
+
* Matches patterns like `await tool_name(`, `tool_name(`, and asyncio.gather calls.
|
|
103
|
+
* @param code - The Python code to analyze
|
|
104
|
+
* @param availableToolNames - Set of available tool names to match against
|
|
105
|
+
* @returns Set of tool names found in the code
|
|
106
|
+
*/
|
|
107
|
+
export function extractUsedToolNames(
|
|
108
|
+
code: string,
|
|
109
|
+
availableToolNames: Set<string>
|
|
110
|
+
): Set<string> {
|
|
111
|
+
const usedTools = new Set<string>();
|
|
112
|
+
|
|
113
|
+
for (const toolName of availableToolNames) {
|
|
114
|
+
const escapedName = toolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
115
|
+
const pattern = new RegExp(`\\b${escapedName}\\s*\\(`, 'g');
|
|
116
|
+
|
|
117
|
+
if (pattern.test(code)) {
|
|
118
|
+
usedTools.add(toolName);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return usedTools;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Filters tool definitions to only include tools actually used in the code.
|
|
127
|
+
* @param toolDefs - All available tool definitions
|
|
128
|
+
* @param code - The Python code to analyze
|
|
129
|
+
* @returns Filtered array of tool definitions
|
|
130
|
+
*/
|
|
131
|
+
export function filterToolsByUsage(
|
|
132
|
+
toolDefs: t.LCTool[],
|
|
133
|
+
code: string
|
|
134
|
+
): t.LCTool[] {
|
|
135
|
+
const availableToolNames = new Set(toolDefs.map((tool) => tool.name));
|
|
136
|
+
const usedToolNames = extractUsedToolNames(code, availableToolNames);
|
|
137
|
+
|
|
138
|
+
if (usedToolNames.size === 0) {
|
|
139
|
+
return toolDefs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return toolDefs.filter((tool) => usedToolNames.has(tool.name));
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
/**
|
|
112
146
|
* Makes an HTTP request to the Code API.
|
|
113
147
|
* @param endpoint - The API endpoint URL
|
|
@@ -287,35 +321,34 @@ export function createProgrammaticToolCallingTool(
|
|
|
287
321
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
288
322
|
|
|
289
323
|
const description = `
|
|
290
|
-
|
|
324
|
+
Run tools by writing Python code. Tools are available as async functions - just call them with await.
|
|
325
|
+
|
|
326
|
+
This is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.
|
|
291
327
|
|
|
292
328
|
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()
|
|
329
|
+
- Tools are pre-defined as async functions - call them with await
|
|
330
|
+
- Use asyncio.gather() to run multiple tools in parallel
|
|
331
|
+
- Only print() output is returned - tool results stay in Python
|
|
332
|
+
|
|
333
|
+
Examples:
|
|
334
|
+
- Simple: result = await get_weather(city="NYC")
|
|
335
|
+
- Loop: for user in users: data = await get_expenses(user_id=user['id'])
|
|
336
|
+
- Parallel: sf, ny = await asyncio.gather(get_weather(city="SF"), get_weather(city="NY"))
|
|
337
|
+
|
|
338
|
+
When to use this instead of calling tools directly:
|
|
339
|
+
- You need to call tools in a loop (process many items)
|
|
340
|
+
- You want parallel execution (asyncio.gather)
|
|
341
|
+
- You need conditionals based on tool results
|
|
342
|
+
- You want to aggregate/filter data before returning
|
|
311
343
|
`.trim();
|
|
312
344
|
|
|
313
345
|
return tool<typeof ProgrammaticToolCallingSchema>(
|
|
314
346
|
async (params, config) => {
|
|
315
|
-
const { code,
|
|
347
|
+
const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;
|
|
316
348
|
|
|
317
349
|
// Extra params injected by ToolNode (follows web_search pattern)
|
|
318
|
-
const { toolMap,
|
|
350
|
+
const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &
|
|
351
|
+
Partial<t.ProgrammaticCache>;
|
|
319
352
|
|
|
320
353
|
if (toolMap == null || toolMap.size === 0) {
|
|
321
354
|
throw new Error(
|
|
@@ -324,13 +357,10 @@ Patterns:
|
|
|
324
357
|
);
|
|
325
358
|
}
|
|
326
359
|
|
|
327
|
-
|
|
328
|
-
const effectiveTools = tools ?? programmaticToolDefs;
|
|
329
|
-
|
|
330
|
-
if (effectiveTools == null || effectiveTools.length === 0) {
|
|
360
|
+
if (toolDefs == null || toolDefs.length === 0) {
|
|
331
361
|
throw new Error(
|
|
332
362
|
'No tool definitions provided. ' +
|
|
333
|
-
'Either pass tools in the input or ensure ToolNode injects
|
|
363
|
+
'Either pass tools in the input or ensure ToolNode injects toolDefs.'
|
|
334
364
|
);
|
|
335
365
|
}
|
|
336
366
|
|
|
@@ -338,9 +368,11 @@ Patterns:
|
|
|
338
368
|
|
|
339
369
|
try {
|
|
340
370
|
// ====================================================================
|
|
341
|
-
// Phase 1:
|
|
371
|
+
// Phase 1: Filter tools and make initial request
|
|
342
372
|
// ====================================================================
|
|
343
373
|
|
|
374
|
+
const effectiveTools = filterToolsByUsage(toolDefs, code);
|
|
375
|
+
|
|
344
376
|
let response = await makeRequest(
|
|
345
377
|
EXEC_ENDPOINT,
|
|
346
378
|
apiKey,
|
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(
|
|
@@ -52,12 +52,12 @@ result = await get_weather(city="San Francisco")
|
|
|
52
52
|
print(f"Temperature: {result['temperature']}°F")
|
|
53
53
|
print(f"Condition: {result['condition']}")
|
|
54
54
|
`,
|
|
55
|
-
tools: toolDefinitions,
|
|
56
55
|
};
|
|
57
56
|
const toolCall = {
|
|
58
57
|
name: 'programmatic_code_execution',
|
|
59
58
|
args,
|
|
60
59
|
toolMap,
|
|
60
|
+
toolDefs: toolDefinitions,
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -85,12 +85,12 @@ for member in team:
|
|
|
85
85
|
|
|
86
86
|
print(f"Grand total: \${total:.2f}")
|
|
87
87
|
`,
|
|
88
|
-
tools: toolDefinitions,
|
|
89
88
|
};
|
|
90
89
|
const toolCall = {
|
|
91
90
|
name: 'programmatic_code_execution',
|
|
92
91
|
args,
|
|
93
92
|
toolMap,
|
|
93
|
+
toolDefs: toolDefinitions,
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -116,12 +116,12 @@ results = await asyncio.gather(*[
|
|
|
116
116
|
for city, weather in zip(cities, results):
|
|
117
117
|
print(f"{city}: {weather['temperature']}°F, {weather['condition']}")
|
|
118
118
|
`,
|
|
119
|
-
tools: toolDefinitions,
|
|
120
119
|
};
|
|
121
120
|
const toolCall = {
|
|
122
121
|
name: 'programmatic_code_execution',
|
|
123
122
|
args,
|
|
124
123
|
toolMap,
|
|
124
|
+
toolDefs: toolDefinitions,
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -150,12 +150,12 @@ if high_spenders:
|
|
|
150
150
|
else:
|
|
151
151
|
print("No high spenders found.")
|
|
152
152
|
`,
|
|
153
|
-
tools: toolDefinitions,
|
|
154
153
|
};
|
|
155
154
|
const toolCall = {
|
|
156
155
|
name: 'programmatic_code_execution',
|
|
157
156
|
args,
|
|
158
157
|
toolMap,
|
|
158
|
+
toolDefs: toolDefinitions,
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -180,12 +180,12 @@ for member in team:
|
|
|
180
180
|
else:
|
|
181
181
|
print("No equipment expenses found")
|
|
182
182
|
`,
|
|
183
|
-
tools: toolDefinitions,
|
|
184
183
|
};
|
|
185
184
|
const toolCall = {
|
|
186
185
|
name: 'programmatic_code_execution',
|
|
187
186
|
args,
|
|
188
187
|
toolMap,
|
|
188
|
+
toolDefs: toolDefinitions,
|
|
189
189
|
};
|
|
190
190
|
|
|
191
191
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -206,12 +206,12 @@ for city in cities:
|
|
|
206
206
|
except Exception as e:
|
|
207
207
|
print(f"{city}: Error - {e}")
|
|
208
208
|
`,
|
|
209
|
-
tools: toolDefinitions,
|
|
210
209
|
};
|
|
211
210
|
const toolCall = {
|
|
212
211
|
name: 'programmatic_code_execution',
|
|
213
212
|
args,
|
|
214
213
|
toolMap,
|
|
214
|
+
toolDefs: toolDefinitions,
|
|
215
215
|
};
|
|
216
216
|
|
|
217
217
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -230,12 +230,12 @@ result2 = await calculator(expression="(10 + 5) / 3")
|
|
|
230
230
|
print(f"2 + 2 * 3 = {result1['result']}")
|
|
231
231
|
print(f"(10 + 5) / 3 = {result2['result']:.2f}")
|
|
232
232
|
`,
|
|
233
|
-
tools: toolDefinitions,
|
|
234
233
|
};
|
|
235
234
|
const toolCall = {
|
|
236
235
|
name: 'programmatic_code_execution',
|
|
237
236
|
args,
|
|
238
237
|
toolMap,
|
|
238
|
+
toolDefs: toolDefinitions,
|
|
239
239
|
};
|
|
240
240
|
|
|
241
241
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -265,12 +265,12 @@ for member, expenses in zip(team, all_expenses):
|
|
|
265
265
|
total = sum(e['amount'] for e in expenses)
|
|
266
266
|
print(f" {member['name']}: \${total:.2f} ({len(expenses)} items)")
|
|
267
267
|
`,
|
|
268
|
-
tools: toolDefinitions,
|
|
269
268
|
};
|
|
270
269
|
const toolCall = {
|
|
271
270
|
name: 'programmatic_code_execution',
|
|
272
271
|
args,
|
|
273
272
|
toolMap,
|
|
273
|
+
toolDefs: toolDefinitions,
|
|
274
274
|
};
|
|
275
275
|
|
|
276
276
|
const output = await ptcTool.invoke(args, { toolCall });
|
|
@@ -302,12 +302,12 @@ print(f"SF: {sf['temperature']}°F vs NYC: {nyc['temperature']}°F")
|
|
|
302
302
|
difference = abs(sf['temperature'] - nyc['temperature'])
|
|
303
303
|
print(f"Temperature difference: {difference}°F")
|
|
304
304
|
`,
|
|
305
|
-
tools: [weatherToolDef!],
|
|
306
305
|
};
|
|
307
306
|
const toolCall = {
|
|
308
307
|
name: 'programmatic_code_execution',
|
|
309
308
|
args,
|
|
310
309
|
toolMap,
|
|
310
|
+
toolDefs: [weatherToolDef!],
|
|
311
311
|
};
|
|
312
312
|
|
|
313
313
|
const output = await ptcTool.invoke(args, { toolCall });
|