@librechat/agents 3.0.36 → 3.0.40
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 +71 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +3 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +5 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +12 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +329 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +34 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
- package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +71 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +4 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +5 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +324 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +34 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
- package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +25 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +2 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/test/mockTools.d.ts +28 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +86 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
- package/dist/types/types/graph.d.ts +7 -1
- package/dist/types/types/tools.d.ts +136 -0
- package/package.json +5 -1
- package/src/agents/AgentContext.ts +86 -0
- package/src/common/enum.ts +2 -0
- package/src/events.ts +5 -1
- package/src/graphs/Graph.ts +6 -0
- package/src/index.ts +2 -0
- package/src/scripts/code_exec_ptc.ts +277 -0
- package/src/scripts/programmatic_exec.ts +396 -0
- package/src/scripts/programmatic_exec_agent.ts +231 -0
- package/src/scripts/tool_search_regex.ts +162 -0
- package/src/test/mockTools.ts +366 -0
- package/src/tools/ProgrammaticToolCalling.ts +423 -0
- package/src/tools/ToolNode.ts +38 -4
- package/src/tools/ToolSearchRegex.ts +535 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +613 -0
- package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
- package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
- package/src/types/graph.ts +7 -1
- package/src/types/tools.ts +166 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/tools/ProgrammaticToolCalling.ts
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
import fetch, { RequestInit } from 'node-fetch';
|
|
5
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
6
|
+
import { getEnvironmentVariable } from '@langchain/core/utils/env';
|
|
7
|
+
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
8
|
+
import type * as t from '@/types';
|
|
9
|
+
import { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
|
|
10
|
+
import { EnvVar, Constants } from '@/common';
|
|
11
|
+
|
|
12
|
+
config();
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const imageMessage = 'Image is already displayed to the user';
|
|
19
|
+
const otherMessage = 'File is already downloaded by the user';
|
|
20
|
+
const accessMessage =
|
|
21
|
+
'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).';
|
|
22
|
+
const emptyOutputMessage =
|
|
23
|
+
'stdout: Empty. Ensure you\'re writing output explicitly.\n';
|
|
24
|
+
|
|
25
|
+
/** Default max round-trips to prevent infinite loops */
|
|
26
|
+
const DEFAULT_MAX_ROUND_TRIPS = 20;
|
|
27
|
+
|
|
28
|
+
/** Default execution timeout in milliseconds */
|
|
29
|
+
const DEFAULT_TIMEOUT = 60000;
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Schema
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const ProgrammaticToolCallingSchema = z.object({
|
|
36
|
+
code: z
|
|
37
|
+
.string()
|
|
38
|
+
.min(1)
|
|
39
|
+
.describe(
|
|
40
|
+
`Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.
|
|
41
|
+
|
|
42
|
+
The Code API generates async function stubs from the tool definitions. Just call them directly:
|
|
43
|
+
|
|
44
|
+
Example (Simple call):
|
|
45
|
+
result = await get_weather(city="San Francisco")
|
|
46
|
+
print(result)
|
|
47
|
+
|
|
48
|
+
Example (Parallel - Fastest):
|
|
49
|
+
results = await asyncio.gather(
|
|
50
|
+
get_weather(city="SF"),
|
|
51
|
+
get_weather(city="NYC"),
|
|
52
|
+
get_weather(city="London")
|
|
53
|
+
)
|
|
54
|
+
for city, weather in zip(["SF", "NYC", "London"], results):
|
|
55
|
+
print(f"{city}: {weather['temperature']}°F")
|
|
56
|
+
|
|
57
|
+
Example (Loop with processing):
|
|
58
|
+
team = await get_team_members()
|
|
59
|
+
for member in team:
|
|
60
|
+
expenses = await get_expenses(user_id=member['id'])
|
|
61
|
+
total = sum(e['amount'] for e in expenses)
|
|
62
|
+
print(f"{member['name']}: \${total:.2f}")
|
|
63
|
+
|
|
64
|
+
Example (Conditional logic):
|
|
65
|
+
data = await fetch_data(source="primary")
|
|
66
|
+
if not data:
|
|
67
|
+
data = await fetch_data(source="backup")
|
|
68
|
+
print(f"Got {len(data)} records")
|
|
69
|
+
|
|
70
|
+
Requirements:
|
|
71
|
+
- Tools are pre-defined as async functions - DO NOT write function definitions
|
|
72
|
+
- Use await for all tool calls
|
|
73
|
+
- Use asyncio.gather() for parallel execution of independent calls
|
|
74
|
+
- Only print() output flows back to the context window
|
|
75
|
+
- Tool results from programmatic calls do NOT consume context tokens`
|
|
76
|
+
),
|
|
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
|
+
session_id: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'
|
|
94
|
+
),
|
|
95
|
+
timeout: z
|
|
96
|
+
.number()
|
|
97
|
+
.int()
|
|
98
|
+
.min(1000)
|
|
99
|
+
.max(300000)
|
|
100
|
+
.optional()
|
|
101
|
+
.default(DEFAULT_TIMEOUT)
|
|
102
|
+
.describe(
|
|
103
|
+
'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'
|
|
104
|
+
),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Helper Functions
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Makes an HTTP request to the Code API.
|
|
113
|
+
* @param endpoint - The API endpoint URL
|
|
114
|
+
* @param apiKey - The API key for authentication
|
|
115
|
+
* @param body - The request body
|
|
116
|
+
* @param proxy - Optional HTTP proxy URL
|
|
117
|
+
* @returns The parsed API response
|
|
118
|
+
*/
|
|
119
|
+
export async function makeRequest(
|
|
120
|
+
endpoint: string,
|
|
121
|
+
apiKey: string,
|
|
122
|
+
body: Record<string, unknown>,
|
|
123
|
+
proxy?: string
|
|
124
|
+
): Promise<t.ProgrammaticExecutionResponse> {
|
|
125
|
+
const fetchOptions: RequestInit = {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'User-Agent': 'LibreChat/1.0',
|
|
130
|
+
'X-API-Key': apiKey,
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify(body),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (proxy != null && proxy !== '') {
|
|
136
|
+
fetchOptions.agent = new HttpsProxyAgent(proxy);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const response = await fetch(endpoint, fetchOptions);
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorText = await response.text();
|
|
143
|
+
throw new Error(
|
|
144
|
+
`HTTP error! status: ${response.status}, body: ${errorText}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (await response.json()) as t.ProgrammaticExecutionResponse;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Executes tools in parallel when requested by the API.
|
|
153
|
+
* Uses Promise.all for parallel execution, catching individual errors.
|
|
154
|
+
* @param toolCalls - Array of tool calls from the API
|
|
155
|
+
* @param toolMap - Map of tool names to executable tools
|
|
156
|
+
* @returns Array of tool results
|
|
157
|
+
*/
|
|
158
|
+
export async function executeTools(
|
|
159
|
+
toolCalls: t.PTCToolCall[],
|
|
160
|
+
toolMap: t.ToolMap
|
|
161
|
+
): Promise<t.PTCToolResult[]> {
|
|
162
|
+
const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {
|
|
163
|
+
const tool = toolMap.get(call.name);
|
|
164
|
+
|
|
165
|
+
if (!tool) {
|
|
166
|
+
return {
|
|
167
|
+
call_id: call.id,
|
|
168
|
+
result: null,
|
|
169
|
+
is_error: true,
|
|
170
|
+
error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const result = await tool.invoke(call.input, {
|
|
176
|
+
metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
call_id: call.id,
|
|
180
|
+
result,
|
|
181
|
+
is_error: false,
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
call_id: call.id,
|
|
186
|
+
result: null,
|
|
187
|
+
is_error: true,
|
|
188
|
+
error_message: (error as Error).message || 'Tool execution failed',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return await Promise.all(executions);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Formats the completed response for the agent.
|
|
198
|
+
* @param response - The completed API response
|
|
199
|
+
* @returns Tuple of [formatted string, artifact]
|
|
200
|
+
*/
|
|
201
|
+
export function formatCompletedResponse(
|
|
202
|
+
response: t.ProgrammaticExecutionResponse
|
|
203
|
+
): [string, t.ProgrammaticExecutionArtifact] {
|
|
204
|
+
let formatted = '';
|
|
205
|
+
|
|
206
|
+
if (response.stdout != null && response.stdout !== '') {
|
|
207
|
+
formatted += `stdout:\n${response.stdout}\n`;
|
|
208
|
+
} else {
|
|
209
|
+
formatted += emptyOutputMessage;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (response.stderr != null && response.stderr !== '') {
|
|
213
|
+
formatted += `stderr:\n${response.stderr}\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (response.files && response.files.length > 0) {
|
|
217
|
+
formatted += 'Generated files:\n';
|
|
218
|
+
|
|
219
|
+
const fileCount = response.files.length;
|
|
220
|
+
for (let i = 0; i < fileCount; i++) {
|
|
221
|
+
const file = response.files[i];
|
|
222
|
+
const isImage = imageExtRegex.test(file.name);
|
|
223
|
+
formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
|
|
224
|
+
|
|
225
|
+
if (i < fileCount - 1) {
|
|
226
|
+
formatted += fileCount <= 3 ? ', ' : ',\n';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
formatted += `\nsession_id: ${response.session_id}\n\n${accessMessage}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
formatted.trim(),
|
|
235
|
+
{
|
|
236
|
+
session_id: response.session_id,
|
|
237
|
+
files: response.files,
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// Tool Factory
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a Programmatic Tool Calling tool for complex multi-tool workflows.
|
|
248
|
+
*
|
|
249
|
+
* This tool enables AI agents to write Python code that orchestrates multiple
|
|
250
|
+
* tool calls programmatically, reducing LLM round-trips and token usage.
|
|
251
|
+
*
|
|
252
|
+
* The tool map must be provided at runtime via config.configurable.toolMap.
|
|
253
|
+
*
|
|
254
|
+
* @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)
|
|
255
|
+
* @returns A LangChain DynamicStructuredTool for programmatic tool calling
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* const ptcTool = createProgrammaticToolCallingTool({
|
|
259
|
+
* apiKey: process.env.CODE_API_KEY,
|
|
260
|
+
* maxRoundTrips: 20
|
|
261
|
+
* });
|
|
262
|
+
*
|
|
263
|
+
* const [output, artifact] = await ptcTool.invoke(
|
|
264
|
+
* { code, tools },
|
|
265
|
+
* { configurable: { toolMap } }
|
|
266
|
+
* );
|
|
267
|
+
*/
|
|
268
|
+
export function createProgrammaticToolCallingTool(
|
|
269
|
+
initParams: t.ProgrammaticToolCallingParams = {}
|
|
270
|
+
): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {
|
|
271
|
+
const apiKey =
|
|
272
|
+
(initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
|
|
273
|
+
initParams.apiKey ??
|
|
274
|
+
getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
|
|
275
|
+
'';
|
|
276
|
+
|
|
277
|
+
if (!apiKey) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
'No API key provided for programmatic tool calling. ' +
|
|
280
|
+
'Set CODE_API_KEY environment variable or pass apiKey in initParams.'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
|
|
285
|
+
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
|
|
286
|
+
const proxy = initParams.proxy ?? process.env.PROXY;
|
|
287
|
+
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
288
|
+
|
|
289
|
+
const description = `
|
|
290
|
+
Executes Python code with programmatic tool calling. Tools are automatically generated as async Python functions from the tool definitions - DO NOT define them in your code.
|
|
291
|
+
|
|
292
|
+
Usage:
|
|
293
|
+
- Write Python code that calls tools using await: result = await get_data()
|
|
294
|
+
- Tools are pre-defined as async functions - just call them
|
|
295
|
+
- Use asyncio.gather() for parallel execution (single round-trip!)
|
|
296
|
+
- Only print() output flows through the context window
|
|
297
|
+
- Tool results from programmatic calls do NOT consume context tokens
|
|
298
|
+
|
|
299
|
+
When to use:
|
|
300
|
+
- Processing multiple records with tool calls (10+ items)
|
|
301
|
+
- Loops, conditionals, or aggregation based on tool results
|
|
302
|
+
- Any workflow requiring 3+ sequential tool calls
|
|
303
|
+
- Parallel execution of independent tool calls
|
|
304
|
+
- Filtering/summarizing large data before returning to context
|
|
305
|
+
|
|
306
|
+
Patterns:
|
|
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()
|
|
311
|
+
`.trim();
|
|
312
|
+
|
|
313
|
+
return tool<typeof ProgrammaticToolCallingSchema>(
|
|
314
|
+
async (params, config) => {
|
|
315
|
+
const { code, tools, session_id, timeout = DEFAULT_TIMEOUT } = params;
|
|
316
|
+
|
|
317
|
+
// Extra params injected by ToolNode (follows web_search pattern)
|
|
318
|
+
const { toolMap, programmaticToolDefs } = config.toolCall ?? {};
|
|
319
|
+
|
|
320
|
+
if (toolMap == null || toolMap.size === 0) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
'No toolMap provided. ' +
|
|
323
|
+
'ToolNode should inject this from AgentContext when invoked through the graph.'
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Use provided tools or fall back to programmaticToolDefs from ToolNode
|
|
328
|
+
const effectiveTools = tools ?? programmaticToolDefs;
|
|
329
|
+
|
|
330
|
+
if (effectiveTools == null || effectiveTools.length === 0) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
'No tool definitions provided. ' +
|
|
333
|
+
'Either pass tools in the input or ensure ToolNode injects programmaticToolDefs.'
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let roundTrip = 0;
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
// ====================================================================
|
|
341
|
+
// Phase 1: Initial request
|
|
342
|
+
// ====================================================================
|
|
343
|
+
|
|
344
|
+
let response = await makeRequest(
|
|
345
|
+
EXEC_ENDPOINT,
|
|
346
|
+
apiKey,
|
|
347
|
+
{
|
|
348
|
+
code,
|
|
349
|
+
tools: effectiveTools,
|
|
350
|
+
session_id,
|
|
351
|
+
timeout,
|
|
352
|
+
},
|
|
353
|
+
proxy
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// ====================================================================
|
|
357
|
+
// Phase 2: Handle response loop
|
|
358
|
+
// ====================================================================
|
|
359
|
+
|
|
360
|
+
while (response.status === 'tool_call_required') {
|
|
361
|
+
roundTrip++;
|
|
362
|
+
|
|
363
|
+
if (roundTrip > maxRoundTrips) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Exceeded maximum round trips (${maxRoundTrips}). ` +
|
|
366
|
+
'This may indicate an infinite loop, excessive tool calls, ' +
|
|
367
|
+
'or a logic error in your code.'
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// eslint-disable-next-line no-console
|
|
372
|
+
console.log(
|
|
373
|
+
`[PTC] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const toolResults = await executeTools(
|
|
377
|
+
response.tool_calls ?? [],
|
|
378
|
+
toolMap
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
response = await makeRequest(
|
|
382
|
+
EXEC_ENDPOINT,
|
|
383
|
+
apiKey,
|
|
384
|
+
{
|
|
385
|
+
continuation_token: response.continuation_token,
|
|
386
|
+
tool_results: toolResults,
|
|
387
|
+
},
|
|
388
|
+
proxy
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ====================================================================
|
|
393
|
+
// Phase 3: Handle final state
|
|
394
|
+
// ====================================================================
|
|
395
|
+
|
|
396
|
+
if (response.status === 'completed') {
|
|
397
|
+
return formatCompletedResponse(response);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (response.status === 'error') {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Execution error: ${response.error}` +
|
|
403
|
+
(response.stderr != null && response.stderr !== ''
|
|
404
|
+
? `\n\nStderr:\n${response.stderr}`
|
|
405
|
+
: '')
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Programmatic execution failed: ${(error as Error).message}`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: Constants.PROGRAMMATIC_TOOL_CALLING,
|
|
418
|
+
description,
|
|
419
|
+
schema: ProgrammaticToolCallingSchema,
|
|
420
|
+
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
}
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { BaseMessage, AIMessage } from '@langchain/core/messages';
|
|
|
20
20
|
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
21
21
|
import type * as t from '@/types';
|
|
22
22
|
import { RunnableCallable } from '@/utils';
|
|
23
|
+
import { Constants } from '@/common';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Helper to check if a value is a Send object
|
|
@@ -38,6 +39,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
38
39
|
toolCallStepIds?: Map<string, string>;
|
|
39
40
|
errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
|
|
40
41
|
private toolUsageCount: Map<string, number>;
|
|
42
|
+
/** Tools available for programmatic code execution */
|
|
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) */
|
|
47
|
+
private toolRegistry?: t.LCToolRegistry;
|
|
41
48
|
|
|
42
49
|
constructor({
|
|
43
50
|
tools,
|
|
@@ -48,6 +55,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
48
55
|
toolCallStepIds,
|
|
49
56
|
handleToolErrors,
|
|
50
57
|
loadRuntimeTools,
|
|
58
|
+
programmaticToolMap,
|
|
59
|
+
programmaticToolDefs,
|
|
60
|
+
toolRegistry,
|
|
51
61
|
}: t.ToolNodeConstructorParams) {
|
|
52
62
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
53
63
|
this.tools = tools;
|
|
@@ -57,6 +67,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
57
67
|
this.loadRuntimeTools = loadRuntimeTools;
|
|
58
68
|
this.errorHandler = errorHandler;
|
|
59
69
|
this.toolUsageCount = new Map<string, number>();
|
|
70
|
+
this.programmaticToolMap = programmaticToolMap;
|
|
71
|
+
this.programmaticToolDefs = programmaticToolDefs;
|
|
72
|
+
this.toolRegistry = toolRegistry;
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
/**
|
|
@@ -83,10 +96,31 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
83
96
|
this.toolUsageCount.set(call.name, turn + 1);
|
|
84
97
|
const args = call.args;
|
|
85
98
|
const stepId = this.toolCallStepIds?.get(call.id!);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
|
|
100
|
+
// Build invoke params - LangChain extracts non-schema fields to config.toolCall
|
|
101
|
+
let invokeParams: Record<string, unknown> = {
|
|
102
|
+
...call,
|
|
103
|
+
args,
|
|
104
|
+
type: 'tool_call',
|
|
105
|
+
stepId,
|
|
106
|
+
turn,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Inject runtime data for special tools (becomes available at config.toolCall)
|
|
110
|
+
if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
|
|
111
|
+
invokeParams = {
|
|
112
|
+
...invokeParams,
|
|
113
|
+
toolMap: this.programmaticToolMap,
|
|
114
|
+
programmaticToolDefs: this.programmaticToolDefs,
|
|
115
|
+
};
|
|
116
|
+
} else if (call.name === Constants.TOOL_SEARCH_REGEX) {
|
|
117
|
+
invokeParams = {
|
|
118
|
+
...invokeParams,
|
|
119
|
+
toolRegistry: this.toolRegistry,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const output = await tool.invoke(invokeParams, config);
|
|
90
124
|
if (
|
|
91
125
|
(isBaseMessage(output) && output._getType() === 'tool') ||
|
|
92
126
|
isCommand(output)
|