@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.
Files changed (62) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +71 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +2 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +3 -0
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +5 -1
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +12 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +329 -0
  12. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
  13. package/dist/cjs/tools/ToolNode.cjs +34 -3
  14. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  15. package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
  16. package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
  17. package/dist/esm/agents/AgentContext.mjs +71 -2
  18. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  19. package/dist/esm/common/enum.mjs +2 -0
  20. package/dist/esm/common/enum.mjs.map +1 -1
  21. package/dist/esm/events.mjs +4 -1
  22. package/dist/esm/events.mjs.map +1 -1
  23. package/dist/esm/graphs/Graph.mjs +5 -1
  24. package/dist/esm/graphs/Graph.mjs.map +1 -1
  25. package/dist/esm/main.mjs +2 -0
  26. package/dist/esm/main.mjs.map +1 -1
  27. package/dist/esm/tools/ProgrammaticToolCalling.mjs +324 -0
  28. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
  29. package/dist/esm/tools/ToolNode.mjs +34 -3
  30. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  31. package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
  32. package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
  33. package/dist/types/agents/AgentContext.d.ts +25 -1
  34. package/dist/types/common/enum.d.ts +2 -0
  35. package/dist/types/graphs/Graph.d.ts +2 -1
  36. package/dist/types/index.d.ts +2 -0
  37. package/dist/types/test/mockTools.d.ts +28 -0
  38. package/dist/types/tools/ProgrammaticToolCalling.d.ts +86 -0
  39. package/dist/types/tools/ToolNode.d.ts +7 -1
  40. package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
  41. package/dist/types/types/graph.d.ts +7 -1
  42. package/dist/types/types/tools.d.ts +136 -0
  43. package/package.json +5 -1
  44. package/src/agents/AgentContext.ts +86 -0
  45. package/src/common/enum.ts +2 -0
  46. package/src/events.ts +5 -1
  47. package/src/graphs/Graph.ts +6 -0
  48. package/src/index.ts +2 -0
  49. package/src/scripts/code_exec_ptc.ts +277 -0
  50. package/src/scripts/programmatic_exec.ts +396 -0
  51. package/src/scripts/programmatic_exec_agent.ts +231 -0
  52. package/src/scripts/tool_search_regex.ts +162 -0
  53. package/src/test/mockTools.ts +366 -0
  54. package/src/tools/ProgrammaticToolCalling.ts +423 -0
  55. package/src/tools/ToolNode.ts +38 -4
  56. package/src/tools/ToolSearchRegex.ts +535 -0
  57. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
  58. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +613 -0
  59. package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
  60. package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
  61. package/src/types/graph.ts +7 -1
  62. package/src/types/tools.ts +166 -0
@@ -13,6 +13,19 @@ import type * as t from '@/types';
13
13
  import type { createPruneMessages } from '@/messages';
14
14
  import { ContentTypes, Providers } from '@/common';
15
15
 
16
+ /**
17
+ * Checks if a tool allows the specified caller type.
18
+ * Default is 'direct' only if allowed_callers is not specified.
19
+ */
20
+ function toolAllowsCaller(
21
+ toolDef: t.LCTool | undefined,
22
+ caller: t.AllowedCaller
23
+ ): boolean {
24
+ if (!toolDef) return false;
25
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
26
+ return allowedCallers.includes(caller);
27
+ }
28
+
16
29
  /**
17
30
  * Encapsulates agent-specific state that can vary between agents in a multi-agent system
18
31
  */
@@ -32,6 +45,7 @@ export class AgentContext {
32
45
  tools,
33
46
  toolMap,
34
47
  toolEnd,
48
+ toolRegistry,
35
49
  instructions,
36
50
  additional_instructions,
37
51
  streamBuffer,
@@ -48,6 +62,7 @@ export class AgentContext {
48
62
  streamBuffer,
49
63
  tools,
50
64
  toolMap,
65
+ toolRegistry,
51
66
  instructions,
52
67
  additionalInstructions: additional_instructions,
53
68
  reasoningKey,
@@ -102,6 +117,11 @@ export class AgentContext {
102
117
  tools?: t.GraphTools;
103
118
  /** Tool map for this agent */
104
119
  toolMap?: t.ToolMap;
120
+ /**
121
+ * Tool definitions registry (includes deferred and programmatic tool metadata).
122
+ * Used for tool search and programmatic tool calling.
123
+ */
124
+ toolRegistry?: t.LCToolRegistry;
105
125
  /** Instructions for this agent */
106
126
  instructions?: string;
107
127
  /** Additional instructions for this agent */
@@ -137,6 +157,7 @@ export class AgentContext {
137
157
  tokenCounter,
138
158
  tools,
139
159
  toolMap,
160
+ toolRegistry,
140
161
  instructions,
141
162
  additionalInstructions,
142
163
  reasoningKey,
@@ -152,6 +173,7 @@ export class AgentContext {
152
173
  tokenCounter?: t.TokenCounter;
153
174
  tools?: t.GraphTools;
154
175
  toolMap?: t.ToolMap;
176
+ toolRegistry?: t.LCToolRegistry;
155
177
  instructions?: string;
156
178
  additionalInstructions?: string;
157
179
  reasoningKey?: 'reasoning_content' | 'reasoning';
@@ -167,6 +189,7 @@ export class AgentContext {
167
189
  this.tokenCounter = tokenCounter;
168
190
  this.tools = tools;
169
191
  this.toolMap = toolMap;
192
+ this.toolRegistry = toolRegistry;
170
193
  this.instructions = instructions;
171
194
  this.additionalInstructions = additionalInstructions;
172
195
  if (reasoningKey) {
@@ -320,4 +343,67 @@ export class AgentContext {
320
343
  // Add tool tokens to existing instruction tokens (which may already include system message tokens)
321
344
  this.instructionTokens += toolTokens;
322
345
  }
346
+
347
+ /**
348
+ * Gets a map of tools that allow programmatic (code_execution) calling.
349
+ * Filters toolMap based on toolRegistry's allowed_callers settings.
350
+ * @returns ToolMap containing only tools that allow code_execution
351
+ */
352
+ getProgrammaticToolMap(): t.ToolMap {
353
+ const programmaticMap: t.ToolMap = new Map();
354
+
355
+ if (!this.toolMap) {
356
+ return programmaticMap;
357
+ }
358
+
359
+ for (const [name, tool] of this.toolMap) {
360
+ const toolDef = this.toolRegistry?.get(name);
361
+ if (toolAllowsCaller(toolDef, 'code_execution')) {
362
+ programmaticMap.set(name, tool);
363
+ }
364
+ }
365
+
366
+ return programmaticMap;
367
+ }
368
+
369
+ /**
370
+ * Gets tool definitions for tools that allow programmatic calling.
371
+ * Used to send to the Code API for stub generation.
372
+ * @returns Array of LCTool definitions for programmatic tools
373
+ */
374
+ getProgrammaticToolDefs(): t.LCTool[] {
375
+ if (!this.toolRegistry) {
376
+ return [];
377
+ }
378
+
379
+ const defs: t.LCTool[] = [];
380
+ for (const [_name, toolDef] of this.toolRegistry) {
381
+ if (toolAllowsCaller(toolDef, 'code_execution')) {
382
+ defs.push(toolDef);
383
+ }
384
+ }
385
+
386
+ return defs;
387
+ }
388
+
389
+ /**
390
+ * Gets the tool registry for deferred tools (for tool search).
391
+ * @param onlyDeferred If true, only returns tools with defer_loading=true
392
+ * @returns LCToolRegistry with tool definitions
393
+ */
394
+ getDeferredToolRegistry(onlyDeferred: boolean = true): t.LCToolRegistry {
395
+ const registry: t.LCToolRegistry = new Map();
396
+
397
+ if (!this.toolRegistry) {
398
+ return registry;
399
+ }
400
+
401
+ for (const [name, toolDef] of this.toolRegistry) {
402
+ if (!onlyDeferred || toolDef.defer_loading === true) {
403
+ registry.set(name, toolDef);
404
+ }
405
+ }
406
+
407
+ return registry;
408
+ }
323
409
  }
@@ -159,6 +159,8 @@ export enum Callback {
159
159
  export enum Constants {
160
160
  OFFICIAL_CODE_BASEURL = 'https://api.librechat.ai/v1',
161
161
  EXECUTE_CODE = 'execute_code',
162
+ TOOL_SEARCH_REGEX = 'tool_search_regex',
163
+ PROGRAMMATIC_TOOL_CALLING = 'programmatic_code_execution',
162
164
  WEB_SEARCH = 'web_search',
163
165
  CONTENT_AND_ARTIFACT = 'content_and_artifact',
164
166
  LC_TRANSFER_TO_ = 'lc_transfer_to_',
package/src/events.ts CHANGED
@@ -9,7 +9,7 @@ import type { MultiAgentGraph, StandardGraph } from '@/graphs';
9
9
  import type { Logger } from 'winston';
10
10
  import type * as t from '@/types';
11
11
  import { handleToolCalls } from '@/tools/handlers';
12
- import { Providers } from '@/common';
12
+ import { Constants, Providers } from '@/common';
13
13
 
14
14
  export class HandlerRegistry {
15
15
  private handlers: Map<string, t.EventHandler> = new Map();
@@ -112,6 +112,10 @@ export class ToolEndHandler implements t.EventHandler {
112
112
  return;
113
113
  }
114
114
 
115
+ if (metadata[Constants.PROGRAMMATIC_TOOL_CALLING] === true) {
116
+ return;
117
+ }
118
+
115
119
  if (this.callback) {
116
120
  await this.callback(toolEndData, metadata);
117
121
  }
@@ -443,9 +443,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
443
443
  initializeTools({
444
444
  currentTools,
445
445
  currentToolMap,
446
+ agentContext,
446
447
  }: {
447
448
  currentTools?: t.GraphTools;
448
449
  currentToolMap?: t.ToolMap;
450
+ agentContext?: AgentContext;
449
451
  }): CustomToolNode<t.BaseGraphState> | ToolNode<t.BaseGraphState> {
450
452
  return new CustomToolNode<t.BaseGraphState>({
451
453
  tools: (currentTools as t.GenericTool[] | undefined) ?? [],
@@ -453,6 +455,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
453
455
  toolCallStepIds: this.toolCallStepIds,
454
456
  errorHandler: (data, metadata) =>
455
457
  StandardGraph.handleToolCallErrorStatic(this, data, metadata),
458
+ toolRegistry: agentContext?.toolRegistry,
459
+ programmaticToolMap: agentContext?.getProgrammaticToolMap(),
460
+ programmaticToolDefs: agentContext?.getProgrammaticToolDefs(),
456
461
  });
457
462
  }
458
463
 
@@ -879,6 +884,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
879
884
  this.initializeTools({
880
885
  currentTools: agentContext.tools,
881
886
  currentToolMap: agentContext.toolMap,
887
+ agentContext,
882
888
  })
883
889
  )
884
890
  .addEdge(START, agentNode)
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@ export * from './graphs';
11
11
  /* Tools */
12
12
  export * from './tools/Calculator';
13
13
  export * from './tools/CodeExecutor';
14
+ export * from './tools/ProgrammaticToolCalling';
15
+ export * from './tools/ToolSearchRegex';
14
16
  export * from './tools/handlers';
15
17
  export * from './tools/search';
16
18
 
@@ -0,0 +1,277 @@
1
+ // src/scripts/code_exec_ptc.ts
2
+ /**
3
+ * Live LLM test for Programmatic Tool Calling (PTC).
4
+ * Run with: npm run code_exec_ptc
5
+ *
6
+ * Tests PTC with a real LLM in the loop, demonstrating:
7
+ * 1. LLM decides when to use PTC
8
+ * 2. LLM writes Python code that calls tools programmatically
9
+ * 3. ToolNode automatically injects programmatic tools
10
+ * 4. Tools filtered by allowed_callers
11
+ */
12
+ import { config } from 'dotenv';
13
+ config();
14
+
15
+ import { randomUUID } from 'crypto';
16
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
17
+ import type { RunnableConfig } from '@langchain/core/runnables';
18
+ import type * as t from '@/types';
19
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
20
+ import {
21
+ ToolEndHandler,
22
+ ModelEndHandler,
23
+ createMetadataAggregator,
24
+ } from '@/events';
25
+ import { getLLMConfig } from '@/utils/llmConfig';
26
+ import { getArgs } from '@/scripts/args';
27
+ import { GraphEvents } from '@/common';
28
+ 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
+
38
+ const conversationHistory: BaseMessage[] = [];
39
+
40
+ async function testProgrammaticToolCalling(): Promise<void> {
41
+ const { userName, location, provider, currentDate } = await getArgs();
42
+ const { contentParts, aggregateContent } = createContentAggregator();
43
+
44
+ const customHandlers = {
45
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
46
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
47
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
48
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
49
+ handle: (
50
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
51
+ data: t.StreamEventData
52
+ ): void => {
53
+ console.log('====== ON_RUN_STEP_COMPLETED ======');
54
+ console.dir(data, { depth: null });
55
+ aggregateContent({
56
+ event,
57
+ data: data as unknown as { result: t.ToolEndEvent },
58
+ });
59
+ },
60
+ },
61
+ [GraphEvents.ON_RUN_STEP]: {
62
+ handle: (
63
+ event: GraphEvents.ON_RUN_STEP,
64
+ data: t.StreamEventData
65
+ ): void => {
66
+ console.log('====== ON_RUN_STEP ======');
67
+ console.dir(data, { depth: null });
68
+ aggregateContent({ event, data: data as t.RunStep });
69
+ },
70
+ },
71
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
72
+ handle: (
73
+ event: GraphEvents.ON_RUN_STEP_DELTA,
74
+ data: t.StreamEventData
75
+ ): void => {
76
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
77
+ },
78
+ },
79
+ [GraphEvents.ON_MESSAGE_DELTA]: {
80
+ handle: (
81
+ event: GraphEvents.ON_MESSAGE_DELTA,
82
+ data: t.StreamEventData
83
+ ): void => {
84
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
85
+ },
86
+ },
87
+ [GraphEvents.TOOL_START]: {
88
+ handle: (
89
+ _event: string,
90
+ data: t.StreamEventData,
91
+ metadata?: Record<string, unknown>
92
+ ): void => {
93
+ console.log('====== TOOL_START ======');
94
+ console.dir(data, { depth: null });
95
+ },
96
+ },
97
+ };
98
+
99
+ const llmConfig = getLLMConfig(provider);
100
+
101
+ // Create mock tool instances
102
+ const teamTool = createGetTeamMembersTool();
103
+ const expensesTool = createGetExpensesTool();
104
+ const weatherTool = createGetWeatherTool();
105
+
106
+ // Create special tools
107
+ const codeExecTool = createCodeExecutionTool();
108
+ const ptcTool = createProgrammaticToolCallingTool();
109
+
110
+ // Build complete tool list and map
111
+ const allTools = [teamTool, expensesTool, weatherTool, codeExecTool, ptcTool];
112
+ const toolMap = new Map(allTools.map((t) => [t.name, t]));
113
+
114
+ // Create tool registry with allowed_callers configuration
115
+ // Only includes business logic tools (not special tools)
116
+ // Special tools (execute_code, PTC) are always bound directly to LLM
117
+ const toolRegistry = createProgrammaticToolRegistry();
118
+
119
+ console.log('\n' + '='.repeat(70));
120
+ console.log('Tool Configuration Summary:');
121
+ console.log('='.repeat(70));
122
+ console.log('Total tools:', allTools.length);
123
+ console.log(
124
+ 'Programmatic-allowed:',
125
+ Array.from(toolRegistry.values())
126
+ .filter((t) => t.allowed_callers?.includes('code_execution'))
127
+ .map((t) => t.name)
128
+ .join(', ')
129
+ );
130
+ console.log(
131
+ 'Direct-callable:',
132
+ Array.from(toolRegistry.values())
133
+ .filter((t) => !t.allowed_callers || t.allowed_callers.includes('direct'))
134
+ .map((t) => t.name)
135
+ .join(', ')
136
+ );
137
+ console.log('='.repeat(70) + '\n');
138
+
139
+ const run = await Run.create<t.IState>({
140
+ runId: randomUUID(),
141
+ graphConfig: {
142
+ type: 'standard',
143
+ llmConfig,
144
+ agents: [
145
+ {
146
+ agentId: 'default',
147
+ provider: llmConfig.provider,
148
+ clientOptions: llmConfig,
149
+ tools: allTools,
150
+ toolMap,
151
+ toolRegistry,
152
+ instructions:
153
+ 'You are a friendly AI assistant with advanced coding capabilities. ' +
154
+ 'You have access to team and expense management tools, but ONLY through programmatic code execution. ' +
155
+ 'When you need to analyze expenses or process team data, use the programmatic_code_execution tool ' +
156
+ 'to write Python code that calls get_team_members(), get_expenses(), and get_weather() functions. ' +
157
+ 'These functions are async - use await. Use asyncio.gather() for parallel execution.',
158
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}. Today is ${currentDate}.`,
159
+ },
160
+ ],
161
+ },
162
+ returnContent: true,
163
+ customHandlers,
164
+ });
165
+
166
+ const config: Partial<RunnableConfig> & {
167
+ version: 'v1' | 'v2';
168
+ run_id?: string;
169
+ streamMode: string;
170
+ } = {
171
+ configurable: {
172
+ provider,
173
+ thread_id: 'ptc-conversation-1',
174
+ },
175
+ streamMode: 'values',
176
+ version: 'v2' as const,
177
+ };
178
+
179
+ console.log('Test 1: Team Expense Analysis with PTC');
180
+ console.log('='.repeat(70) + '\n');
181
+
182
+ const userMessage1 = `Hi ${userName}! I need you to analyze our team's expenses. Please:
183
+
184
+ 1. Get the list of all team members
185
+ 2. For each member, fetch their expense records
186
+ 3. Calculate the total expenses per member
187
+ 4. Identify anyone who spent more than $500
188
+ 5. Show me a summary report
189
+
190
+ IMPORTANT: Use the programmatic_code_execution tool to do this efficiently.
191
+ Don't call each tool separately - write Python code that orchestrates all the calls!`;
192
+
193
+ conversationHistory.push(new HumanMessage(userMessage1));
194
+
195
+ let inputs = {
196
+ messages: conversationHistory,
197
+ };
198
+
199
+ const finalContentParts1 = await run.processStream(inputs, config);
200
+ const finalMessages1 = run.getRunMessages();
201
+ if (finalMessages1) {
202
+ conversationHistory.push(...finalMessages1);
203
+ }
204
+
205
+ console.log('\n\n====================\n\n');
206
+ console.log('Content Parts:');
207
+ console.dir(contentParts, { depth: null });
208
+
209
+ console.log('\n\n' + '='.repeat(70));
210
+ console.log('Test 2: Conditional Logic and Parallel Execution');
211
+ console.log('='.repeat(70) + '\n');
212
+
213
+ const userMessage2 = `Great job! Now let's test some advanced patterns. Please:
214
+
215
+ 1. Check the weather in both San Francisco and New York (in parallel!)
216
+ 2. Based on which city has better weather (warmer), fetch the team members
217
+ 3. For the Engineering team members only, calculate their travel expenses
218
+ 4. Show me the results
219
+
220
+ Again, use programmatic_code_execution for maximum efficiency. Use asyncio.gather()
221
+ to check both cities' weather at the same time!`;
222
+
223
+ conversationHistory.push(new HumanMessage(userMessage2));
224
+
225
+ inputs = {
226
+ messages: conversationHistory,
227
+ };
228
+
229
+ const finalContentParts2 = await run.processStream(inputs, config);
230
+ const finalMessages2 = run.getRunMessages();
231
+ if (finalMessages2) {
232
+ conversationHistory.push(...finalMessages2);
233
+ }
234
+
235
+ console.log('\n\n====================\n\n');
236
+ console.log('Final Content Parts:');
237
+ console.dir(finalContentParts2, { depth: null });
238
+
239
+ console.log('\n\n' + '='.repeat(70));
240
+ console.log('Generating conversation title...');
241
+ console.log('='.repeat(70) + '\n');
242
+
243
+ const { handleLLMEnd, collected } = createMetadataAggregator();
244
+ const titleResult = await run.generateTitle({
245
+ provider,
246
+ inputText: userMessage1,
247
+ contentParts,
248
+ chainOptions: {
249
+ callbacks: [
250
+ {
251
+ handleLLMEnd,
252
+ },
253
+ ],
254
+ },
255
+ });
256
+
257
+ console.log('Generated Title:', titleResult);
258
+ console.log('Collected metadata:', collected);
259
+ }
260
+
261
+ process.on('unhandledRejection', (reason, promise) => {
262
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
263
+ console.log('Conversation history:');
264
+ console.dir(conversationHistory, { depth: null });
265
+ process.exit(1);
266
+ });
267
+
268
+ process.on('uncaughtException', (err) => {
269
+ console.error('Uncaught Exception:', err);
270
+ });
271
+
272
+ testProgrammaticToolCalling().catch((err) => {
273
+ console.error(err);
274
+ console.log('Conversation history:');
275
+ console.dir(conversationHistory, { depth: null });
276
+ process.exit(1);
277
+ });