mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-13 13:04:24 +01:00
🦥 refactor: Event-Driven Lazy Tool Loading (#11588)
* refactor: json schema tools with lazy loading - Added LocalToolExecutor class for lazy loading and caching of tools during execution. - Introduced ToolExecutionContext and ToolExecutor interfaces for better type management. - Created utility functions to generate tool proxies with JSON schema support. - Added ExtendedJsonSchema type for enhanced schema definitions. - Updated existing toolkits to utilize the new schema and executor functionalities. - Introduced a comprehensive tool definitions registry for managing various tool schemas. chore: update @librechat/agents to version 3.1.2 refactor: enhance tool loading optimization and classification - Improved the loadAgentToolsOptimized function to utilize a proxy pattern for all tools, enabling deferred execution and reducing overhead. - Introduced caching for tool instances and refined tool classification logic to streamline tool management. - Updated the handling of MCP tools to improve logging and error reporting for missing tools in the cache. - Enhanced the structure of tool definitions to support better classification and integration with existing tools. refactor: modularize tool loading and enhance optimization - Moved the loadAgentToolsOptimized function to a new service file for better organization and maintainability. - Updated the ToolService to utilize the new service for optimized tool loading, improving code clarity. - Removed legacy tool loading methods and streamlined the tool loading process to enhance performance and reduce complexity. - Introduced feature flag handling for optimized tool loading, allowing for easier toggling of this functionality. refactor: replace loadAgentToolsWithFlag with loadAgentTools in tool loader refactor: enhance MCP tool loading with proxy creation and classification refactor: optimize MCP tool loading by grouping tools by server - Introduced a Map to group cached tools by server name, improving the organization of tool data. - Updated the createMCPProxyTool function to accept server name directly, enhancing clarity. - Refactored the logic for handling MCP tools, streamlining the process of creating proxy tools for classification. refactor: enhance MCP tool loading and proxy creation - Added functionality to retrieve MCP server tools and reinitialize servers if necessary, improving tool availability. - Updated the tool loading logic to utilize a Map for organizing tools by server, enhancing clarity and performance. - Refactored the createToolProxy function to ensure a default response format, streamlining tool creation. refactor: update createToolProxy to ensure consistent response format - Modified the createToolProxy function to await the executor's execution and validate the result format. - Ensured that the function returns a default response structure when the result is not an array of two elements, enhancing reliability in tool proxy creation. refactor: ToolExecutionContext with toolCall property - Added toolCall property to ToolExecutionContext interface for improved context handling during tool execution. - Updated LocalToolExecutor to include toolCall in the runnable configuration, allowing for more flexible tool invocation. - Modified createToolProxy to pass toolCall from the configuration, ensuring consistent context across tool executions. refactor: enhance event-driven tool execution and logging - Introduced ToolExecuteOptions for improved handling of event-driven tool execution, allowing for parallel execution of tool calls. - Updated getDefaultHandlers to include support for ON_TOOL_EXECUTE events, enhancing the flexibility of tool invocation. - Added detailed logging in LocalToolExecutor to track tool loading and execution metrics, improving observability and debugging capabilities. - Refactored initializeClient to integrate event-driven tool loading, ensuring compatibility with the new execution model. chore: update @librechat/agents to version 3.1.21 refactor: remove legacy tool loading and executor components - Eliminated the loadAgentToolsWithFlag function, simplifying the tool loading process by directly using loadAgentTools. - Removed the LocalToolExecutor and related executor components to streamline the tool execution architecture. - Updated ToolService and related files to reflect the removal of deprecated features, enhancing code clarity and maintainability. refactor: enhance tool classification and definitions handling - Updated the loadAgentTools function to return toolDefinitions alongside toolRegistry, improving the structure of tool data returned to clients. - Removed the convertRegistryToDefinitions function from the initialize.js file, simplifying the initialization process. - Adjusted the buildToolClassification function to ensure toolDefinitions are built and returned simultaneously with the toolRegistry, enhancing efficiency in tool management. - Updated type definitions in initialize.ts to include toolDefinitions, ensuring consistency across the codebase. refactor: implement event-driven tool execution handler - Introduced createToolExecuteHandler function to streamline the handling of ON_TOOL_EXECUTE events, allowing for parallel execution of tool calls. - Updated getDefaultHandlers to utilize the new handler, simplifying the event-driven architecture. - Added handlers.ts file to encapsulate tool execution logic, improving code organization and maintainability. - Enhanced OpenAI handlers to integrate the new tool execution capabilities, ensuring consistent event handling across the application. refactor: integrate event-driven tool execution options - Added toolExecuteOptions to support event-driven tool execution in OpenAI and responses controllers, enhancing flexibility in tool handling. - Updated handlers to utilize createToolExecuteHandler, allowing for streamlined execution of tools during agent interactions. - Refactored service dependencies to include toolExecuteOptions, ensuring consistent integration across the application. refactor: enhance tool loading with definitionsOnly parameter - Updated createToolLoader and loadAgentTools functions to include a definitionsOnly parameter, allowing for the retrieval of only serializable tool definitions in event-driven mode. - Adjusted related interfaces and documentation to reflect the new parameter, improving clarity and flexibility in tool management. - Ensured compatibility across various components by integrating the definitionsOnly option in the initialization process. refactor: improve agent tool presence check in initialization - Added a check for tool presence using a new hasAgentTools variable, which evaluates both structuredTools and toolDefinitions. - Updated the conditional logic in the agent initialization process to utilize the hasAgentTools variable, enhancing clarity and maintainability in tool management. refactor: enhance agent tool extraction to support tool definitions - Updated the extractMCPServers function to handle both tool instances and serializable tool definitions, improving flexibility in agent tool management. - Added a new property toolDefinitions to the AgentWithTools type for better integration of event-driven mode. - Enhanced documentation to clarify the function's capabilities in extracting unique MCP server names from both tools and tool definitions. refactor: enhance tool classification and registry building - Added serverName property to ToolDefinition for improved tool identification. - Introduced buildToolRegistry function to streamline the creation of tool registries based on MCP tool definitions and agent options. - Updated buildToolClassification to utilize the new registry building logic, ensuring basic definitions are returned even when advanced classification features are not allowed. - Enhanced documentation and logging for clarity in tool classification processes. refactor: update @librechat/agents dependency to version 3.1.22 fix: expose loadTools function in ToolService - Added loadTools function to the exported module in ToolService.js, enhancing the accessibility of tool loading functionality. chore: remove configurable options from tool execute options in OpenAI controller refactor: enhance tool loading mechanism to utilize agent-specific context chore: update @librechat/agents dependency to version 3.1.23 fix: simplify result handling in createToolExecuteHandler * refactor: loadToolDefinitions for efficient tool loading in event-driven mode * refactor: replace legacy tool loading with loadToolsForExecution in OpenAI and responses controllers - Updated OpenAIChatCompletionController and createResponse functions to utilize loadToolsForExecution for improved tool loading. - Removed deprecated loadToolsLegacy references, streamlining the tool execution process. - Enhanced tool loading options to include agent-specific context and configurations. * refactor: enhance tool loading and execution handling - Introduced loadActionToolsForExecution function to streamline loading of action tools, improving organization and maintainability. - Updated loadToolsForExecution to handle both regular and action tools, optimizing the tool loading process. - Added detailed logging for missing tools in createToolExecuteHandler, enhancing error visibility. - Refactored tool definitions to normalize action tool names, improving consistency in tool management. * refactor: enhance built-in tool definitions loading - Updated loadToolDefinitions to include descriptions and parameters from the tool registry for built-in tools, improving the clarity and usability of tool definitions. - Integrated getToolDefinition to streamline the retrieval of tool metadata, enhancing the overall tool management process. * feat: add action tool definitions loading to tool service - Introduced getActionToolDefinitions function to load action tool definitions based on agent ID and tool names, enhancing the tool loading process. - Updated loadToolDefinitions to integrate action tool definitions, allowing for better management and retrieval of action-specific tools. - Added comprehensive tests for action tool definitions to ensure correct loading and parameter handling, improving overall reliability and functionality. * chore: update @librechat/agents dependency to version 3.1.26 * refactor: add toolEndCallback to handle tool execution results * fix: tool definitions and execution handling - Introduced native tools (execute_code, file_search, web_search) to the tool service, allowing for better integration and management of these tools. - Updated isBuiltInTool function to include native tools in the built-in check, improving tool recognition. - Added comprehensive tests for loading parameters of native tools, ensuring correct functionality and parameter handling. - Enhanced tool definitions registry to include new agent tool definitions, streamlining tool retrieval and management. * refactor: enhance tool loading and execution context - Added toolRegistry to the context for OpenAIChatCompletionController and createResponse functions, improving tool management. - Updated loadToolsForExecution to utilize toolRegistry for better integration of programmatic tools and tool search functionalities. - Enhanced the initialization process to include toolRegistry in agent context, streamlining tool access and configuration. - Refactored tool classification logic to support event-driven execution, ensuring compatibility with new tool definitions. * chore: add request duration logging to OpenAI and Responses controllers - Introduced logging for request start and completion times in OpenAIChatCompletionController and createResponse functions. - Calculated and logged the duration of each request, enhancing observability and performance tracking. - Improved debugging capabilities by providing detailed logs for both streaming and non-streaming responses. * chore: update @librechat/agents dependency to version 3.1.27 * refactor: implement buildToolSet function for tool management - Introduced buildToolSet function to streamline the creation of tool sets from agent configurations, enhancing tool management across various controllers. - Updated AgentClient, OpenAIChatCompletionController, and createResponse functions to utilize buildToolSet, improving consistency in tool handling. - Added comprehensive tests for buildToolSet to ensure correct functionality and edge case handling, enhancing overall reliability. * refactor: update import paths for ToolExecuteOptions and createToolExecuteHandler * fix: update GoogleSearch.js description for maximum search results - Changed the default maximum number of search results from 10 to 5 in the Google Search JSON schema description, ensuring accurate documentation of the expected behavior. * chore: remove deprecated Browser tool and associated assets - Deleted the Browser tool definition from manifest.json, which included its name, plugin key, description, and authentication configuration. - Removed the web-browser.svg asset as it is no longer needed following the removal of the Browser tool. * fix: ensure tool definitions are valid before processing - Added a check to verify the existence of tool definitions in the registry before accessing their properties, preventing potential runtime errors. - Updated the loading logic for built-in tool definitions to ensure that only valid definitions are pushed to the built-in tool definitions array. * fix: extend ExtendedJsonSchema to support 'null' type and nullable enums - Updated the ExtendedJsonSchema type to include 'null' as a valid type option. - Modified the enum property to accept an array of values that can include strings, numbers, booleans, and null, enhancing schema flexibility. * test: add comprehensive tests for tool definitions loading and registry behavior - Implemented tests to verify the handling of built-in tools without registry definitions, ensuring they are skipped correctly. - Added tests to confirm that built-in tools include descriptions and parameters in the registry. - Enhanced tests for action tools, checking for proper inclusion of metadata and handling of tools without parameters in the registry. * test: add tests for mixed-type and number enum schema handling - Introduced tests to validate the parsing of mixed-type enum values, including strings, numbers, booleans, and null. - Added tests for number enum schema values to ensure correct parsing of numeric inputs, enhancing schema validation coverage. * fix: update mock implementation for @librechat/agents - Changed the mock for @librechat/agents to spread the actual module's properties, ensuring that all necessary functionalities are preserved in tests. - This adjustment enhances the accuracy of the tests by reflecting the real structure of the module. * fix: change max_results type in GoogleSearch schema from number to integer - Updated the type of max_results in the Google Search JSON schema to 'integer' for better type accuracy and validation consistency. * fix: update max_results description and type in GoogleSearch schema - Changed the type of max_results from 'number' to 'integer' for improved type accuracy. - Updated the description to reflect the new default maximum number of search results, changing it from 10 to 5. * refactor: remove unused code and improve tool registry handling - Eliminated outdated comments and conditional logic related to event-driven mode in the ToolService. - Enhanced the handling of the tool registry by ensuring it is configurable for better integration during tool execution. * feat: add definitionsOnly option to buildToolClassification for event-driven mode - Introduced a new parameter, definitionsOnly, to the BuildToolClassificationParams interface to enable a mode that skips tool instance creation. - Updated the buildToolClassification function to conditionally add tool definitions without instantiating tools when definitionsOnly is true. - Modified the loadToolDefinitions function to pass definitionsOnly as true, ensuring compatibility with the new feature. * test: add unit tests for buildToolClassification with definitionsOnly option - Implemented tests to verify the behavior of buildToolClassification when definitionsOnly is set to true or false. - Ensured that tool instances are not created when definitionsOnly is true, while still adding necessary tool definitions. - Confirmed that loadAuthValues is called appropriately based on the definitionsOnly parameter, enhancing test coverage for this new feature.
This commit is contained in:
parent
6279ea8dd7
commit
5af1342dbb
46 changed files with 3297 additions and 565 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { Agent, TEphemeralAgent } from 'librechat-data-provider';
|
||||
import type { LCTool } from '@librechat/agents';
|
||||
import type { Logger } from 'winston';
|
||||
import type { MCPManager } from '~/mcp/MCPManager';
|
||||
|
||||
|
|
@ -11,27 +12,43 @@ import type { MCPManager } from '~/mcp/MCPManager';
|
|||
export type AgentWithTools = Pick<Agent, 'id'> &
|
||||
Partial<Omit<Agent, 'id' | 'tools'>> & {
|
||||
tools?: Array<DynamicStructuredTool | string>;
|
||||
/** Serializable tool definitions for event-driven mode */
|
||||
toolDefinitions?: LCTool[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts unique MCP server names from an agent's tools.
|
||||
* @param agent - The agent with tools
|
||||
* Extracts unique MCP server names from an agent's tools or tool definitions.
|
||||
* Supports both full tool instances (tools) and serializable definitions (toolDefinitions).
|
||||
* @param agent - The agent with tools and/or tool definitions
|
||||
* @returns Array of unique MCP server names
|
||||
*/
|
||||
export function extractMCPServers(agent: AgentWithTools): string[] {
|
||||
if (!agent?.tools?.length) {
|
||||
return [];
|
||||
}
|
||||
const mcpServers = new Set<string>();
|
||||
for (let i = 0; i < agent.tools.length; i++) {
|
||||
const tool = agent.tools[i];
|
||||
if (tool instanceof DynamicStructuredTool && tool.name.includes(Constants.mcp_delimiter)) {
|
||||
const serverName = tool.name.split(Constants.mcp_delimiter).pop();
|
||||
if (serverName) {
|
||||
mcpServers.add(serverName);
|
||||
|
||||
/** Check tool instances (non-event-driven mode) */
|
||||
if (agent?.tools?.length) {
|
||||
for (const tool of agent.tools) {
|
||||
if (tool instanceof DynamicStructuredTool && tool.name.includes(Constants.mcp_delimiter)) {
|
||||
const serverName = tool.name.split(Constants.mcp_delimiter).pop();
|
||||
if (serverName) {
|
||||
mcpServers.add(serverName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Check tool definitions (event-driven mode) */
|
||||
if (agent?.toolDefinitions?.length) {
|
||||
for (const toolDef of agent.toolDefinitions) {
|
||||
if (toolDef.name?.includes(Constants.mcp_delimiter)) {
|
||||
const serverName = toolDef.name.split(Constants.mcp_delimiter).pop();
|
||||
if (serverName) {
|
||||
mcpServers.add(serverName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(mcpServers);
|
||||
}
|
||||
|
||||
|
|
|
|||
168
packages/api/src/agents/handlers.ts
Normal file
168
packages/api/src/agents/handlers.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { GraphEvents, Constants } from '@librechat/agents';
|
||||
import type {
|
||||
LCTool,
|
||||
EventHandler,
|
||||
LCToolRegistry,
|
||||
ToolCallRequest,
|
||||
ToolExecuteResult,
|
||||
ToolExecuteBatchRequest,
|
||||
} from '@librechat/agents';
|
||||
import type { StructuredToolInterface } from '@langchain/core/tools';
|
||||
|
||||
export interface ToolEndCallbackData {
|
||||
output: {
|
||||
name: string;
|
||||
tool_call_id: string;
|
||||
content: string | unknown;
|
||||
artifact?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolEndCallbackMetadata {
|
||||
run_id?: string;
|
||||
thread_id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type ToolEndCallback = (
|
||||
data: ToolEndCallbackData,
|
||||
metadata: ToolEndCallbackMetadata,
|
||||
) => Promise<void>;
|
||||
|
||||
export interface ToolExecuteOptions {
|
||||
/** Loads tools by name, using agentId to look up agent-specific context */
|
||||
loadTools: (
|
||||
toolNames: string[],
|
||||
agentId?: string,
|
||||
) => Promise<{
|
||||
loadedTools: StructuredToolInterface[];
|
||||
/** Additional configurable properties to merge (e.g., userMCPAuthMap) */
|
||||
configurable?: Record<string, unknown>;
|
||||
}>;
|
||||
/** Callback to process tool artifacts (code output files, file citations, etc.) */
|
||||
toolEndCallback?: ToolEndCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the ON_TOOL_EXECUTE handler for event-driven tool execution.
|
||||
* This handler receives batched tool calls, loads the required tools,
|
||||
* executes them in parallel, and resolves with the results.
|
||||
*/
|
||||
export function createToolExecuteHandler(options: ToolExecuteOptions): EventHandler {
|
||||
const { loadTools, toolEndCallback } = options;
|
||||
|
||||
return {
|
||||
handle: async (_event: string, data: ToolExecuteBatchRequest) => {
|
||||
const { toolCalls, agentId, configurable, metadata, resolve, reject } = data;
|
||||
|
||||
try {
|
||||
const toolNames = [...new Set(toolCalls.map((tc: ToolCallRequest) => tc.name))];
|
||||
const { loadedTools, configurable: toolConfigurable } = await loadTools(toolNames, agentId);
|
||||
const toolMap = new Map(loadedTools.map((t) => [t.name, t]));
|
||||
const mergedConfigurable = { ...configurable, ...toolConfigurable };
|
||||
|
||||
const results: ToolExecuteResult[] = await Promise.all(
|
||||
toolCalls.map(async (tc: ToolCallRequest) => {
|
||||
const tool = toolMap.get(tc.name);
|
||||
|
||||
if (!tool) {
|
||||
logger.warn(
|
||||
`[ON_TOOL_EXECUTE] Tool "${tc.name}" not found. Available: ${[...toolMap.keys()].join(', ')}`,
|
||||
);
|
||||
return {
|
||||
toolCallId: tc.id,
|
||||
status: 'error' as const,
|
||||
content: '',
|
||||
errorMessage: `Tool ${tc.name} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const toolCallConfig: Record<string, unknown> = {
|
||||
id: tc.id,
|
||||
stepId: tc.stepId,
|
||||
turn: tc.turn,
|
||||
};
|
||||
|
||||
if (tc.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
|
||||
const toolRegistry = mergedConfigurable?.toolRegistry as LCToolRegistry | undefined;
|
||||
const ptcToolMap = mergedConfigurable?.ptcToolMap as
|
||||
| Map<string, StructuredToolInterface>
|
||||
| undefined;
|
||||
if (toolRegistry) {
|
||||
const toolDefs: LCTool[] = Array.from(toolRegistry.values()).filter(
|
||||
(t) =>
|
||||
t.name !== Constants.PROGRAMMATIC_TOOL_CALLING &&
|
||||
t.name !== Constants.TOOL_SEARCH,
|
||||
);
|
||||
toolCallConfig.toolDefs = toolDefs;
|
||||
toolCallConfig.toolMap = ptcToolMap ?? toolMap;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await tool.invoke(tc.args, {
|
||||
toolCall: toolCallConfig,
|
||||
configurable: mergedConfigurable,
|
||||
metadata,
|
||||
} as Record<string, unknown>);
|
||||
|
||||
if (toolEndCallback) {
|
||||
await toolEndCallback(
|
||||
{
|
||||
output: {
|
||||
name: tc.name,
|
||||
tool_call_id: tc.id,
|
||||
content: result.content,
|
||||
artifact: result.artifact,
|
||||
},
|
||||
},
|
||||
{
|
||||
run_id: (metadata as Record<string, unknown>)?.run_id as string | undefined,
|
||||
thread_id: (metadata as Record<string, unknown>)?.thread_id as
|
||||
| string
|
||||
| undefined,
|
||||
...metadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
toolCallId: tc.id,
|
||||
content: result.content,
|
||||
artifact: result.artifact,
|
||||
status: 'success' as const,
|
||||
};
|
||||
} catch (toolError) {
|
||||
const error = toolError as Error;
|
||||
logger.error(`[ON_TOOL_EXECUTE] Tool ${tc.name} error:`, error);
|
||||
return {
|
||||
toolCallId: tc.id,
|
||||
status: 'error' as const,
|
||||
content: '',
|
||||
errorMessage: error.message,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
resolve(results);
|
||||
} catch (error) {
|
||||
logger.error('[ON_TOOL_EXECUTE] Fatal error:', error);
|
||||
reject(error as Error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a handlers object that includes ON_TOOL_EXECUTE.
|
||||
* Can be merged with other handler objects.
|
||||
*/
|
||||
export function createToolExecuteHandlers(
|
||||
options: ToolExecuteOptions,
|
||||
): Record<string, EventHandler> {
|
||||
return {
|
||||
[GraphEvents.ON_TOOL_EXECUTE]: createToolExecuteHandler(options),
|
||||
};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ export * from './avatars';
|
|||
export * from './chain';
|
||||
export * from './context';
|
||||
export * from './edges';
|
||||
export * from './handlers';
|
||||
export * from './initialize';
|
||||
export * from './legacy';
|
||||
export * from './memory';
|
||||
|
|
@ -10,4 +11,5 @@ export * from './openai';
|
|||
export * from './resources';
|
||||
export * from './responses';
|
||||
export * from './run';
|
||||
export * from './tools';
|
||||
export * from './validation';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type {
|
|||
Agent,
|
||||
TUser,
|
||||
} from 'librechat-data-provider';
|
||||
import type { GenericTool, LCToolRegistry, ToolMap } from '@librechat/agents';
|
||||
import type { GenericTool, LCToolRegistry, ToolMap, LCTool } from '@librechat/agents';
|
||||
import type { Response as ServerResponse } from 'express';
|
||||
import type { IMongoFile } from '@librechat/data-schemas';
|
||||
import type { InitializeResultBase, ServerRequest, EndpointDbMethods } from '~/types';
|
||||
|
|
@ -47,6 +47,8 @@ export type InitializedAgent = Agent & {
|
|||
toolMap?: ToolMap;
|
||||
/** Tool registry for PTC and tool search (only present when MCP tools with env classification exist) */
|
||||
toolRegistry?: LCToolRegistry;
|
||||
/** Serializable tool definitions for event-driven execution */
|
||||
toolDefinitions?: LCTool[];
|
||||
/** Precomputed flag indicating if any tools have defer_loading enabled (for efficient runtime checks) */
|
||||
hasDeferredTools?: boolean;
|
||||
};
|
||||
|
|
@ -79,10 +81,13 @@ export interface InitializeAgentParams {
|
|||
tool_options: AgentToolOptions | undefined;
|
||||
tool_resources: AgentToolResources | undefined;
|
||||
}) => Promise<{
|
||||
tools: GenericTool[];
|
||||
toolContextMap: Record<string, unknown>;
|
||||
/** Full tool instances (only present when definitionsOnly=false) */
|
||||
tools?: GenericTool[];
|
||||
toolContextMap?: Record<string, unknown>;
|
||||
userMCPAuthMap?: Record<string, Record<string, string>>;
|
||||
toolRegistry?: LCToolRegistry;
|
||||
/** Serializable tool definitions for event-driven mode */
|
||||
toolDefinitions?: LCTool[];
|
||||
hasDeferredTools?: boolean;
|
||||
} | null>;
|
||||
/** Endpoint option (contains model_parameters and endpoint info) */
|
||||
|
|
@ -272,11 +277,12 @@ export async function initializeAgent(
|
|||
});
|
||||
|
||||
const {
|
||||
tools: structuredTools,
|
||||
toolRegistry,
|
||||
toolContextMap,
|
||||
userMCPAuthMap,
|
||||
toolRegistry,
|
||||
toolDefinitions,
|
||||
hasDeferredTools,
|
||||
tools: structuredTools,
|
||||
} = (await loadTools?.({
|
||||
req,
|
||||
res,
|
||||
|
|
@ -291,6 +297,7 @@ export async function initializeAgent(
|
|||
toolContextMap: {},
|
||||
userMCPAuthMap: undefined,
|
||||
toolRegistry: undefined,
|
||||
toolDefinitions: [],
|
||||
hasDeferredTools: false,
|
||||
};
|
||||
|
||||
|
|
@ -343,13 +350,17 @@ export async function initializeAgent(
|
|||
agent.provider = options.provider;
|
||||
}
|
||||
|
||||
/** Check for tool presence from either full instances or definitions (event-driven mode) */
|
||||
const hasAgentTools = (structuredTools?.length ?? 0) > 0 || (toolDefinitions?.length ?? 0) > 0;
|
||||
|
||||
let tools: GenericTool[] = options.tools?.length
|
||||
? (options.tools as GenericTool[])
|
||||
: structuredTools;
|
||||
: (structuredTools ?? []);
|
||||
|
||||
if (
|
||||
(agent.provider === Providers.GOOGLE || agent.provider === Providers.VERTEXAI) &&
|
||||
options.tools?.length &&
|
||||
structuredTools?.length
|
||||
hasAgentTools
|
||||
) {
|
||||
throw new Error(`{ "type": "${ErrorTypes.GOOGLE_TOOL_CONFLICT}"}`);
|
||||
} else if (
|
||||
|
|
@ -396,6 +407,7 @@ export async function initializeAgent(
|
|||
resendFiles,
|
||||
userMCPAuthMap,
|
||||
toolRegistry,
|
||||
toolDefinitions,
|
||||
hasDeferredTools,
|
||||
toolContextMap: toolContextMap ?? {},
|
||||
useLegacyContent: !!options.useLegacyContent,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import type {
|
|||
CompletionUsage,
|
||||
ToolCall,
|
||||
} from './types';
|
||||
import type { ToolExecuteOptions } from '~/agents/handlers';
|
||||
import { createToolExecuteHandler } from '~/agents/handlers';
|
||||
|
||||
/**
|
||||
* Create a chat completion chunk in OpenAI format
|
||||
|
|
@ -167,6 +169,7 @@ export const GraphEvents = {
|
|||
ON_RUN_STEP_COMPLETED: 'on_run_step_completed',
|
||||
ON_MESSAGE_DELTA: 'on_message_delta',
|
||||
ON_REASONING_DELTA: 'on_reasoning_delta',
|
||||
ON_TOOL_EXECUTE: 'on_tool_execute',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
@ -404,8 +407,9 @@ export class OpenAIReasoningDeltaHandler implements EventHandler {
|
|||
*/
|
||||
export function createOpenAIHandlers(
|
||||
config: OpenAIStreamHandlerConfig,
|
||||
toolExecuteOptions?: ToolExecuteOptions,
|
||||
): Record<string, EventHandler> {
|
||||
return {
|
||||
const handlers: Record<string, EventHandler> = {
|
||||
[GraphEvents.ON_MESSAGE_DELTA]: new OpenAIMessageDeltaHandler(config),
|
||||
[GraphEvents.ON_RUN_STEP_DELTA]: new OpenAIRunStepDeltaHandler(config),
|
||||
[GraphEvents.ON_RUN_STEP]: new OpenAIRunStepHandler(config),
|
||||
|
|
@ -415,6 +419,12 @@ export function createOpenAIHandlers(
|
|||
[GraphEvents.TOOL_END]: new OpenAIToolEndHandler(),
|
||||
[GraphEvents.ON_REASONING_DELTA]: new OpenAIReasoningDeltaHandler(config),
|
||||
};
|
||||
|
||||
if (toolExecuteOptions) {
|
||||
handlers[GraphEvents.ON_TOOL_EXECUTE] = createToolExecuteHandler(toolExecuteOptions);
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
createChunk,
|
||||
writeSSE,
|
||||
} from './handlers';
|
||||
import type { ToolExecuteOptions } from '../handlers';
|
||||
|
||||
/**
|
||||
* Dependencies for the chat completion service
|
||||
|
|
@ -67,6 +68,8 @@ export interface ChatCompletionDependencies {
|
|||
createRun?: CreateRunFn;
|
||||
/** App config */
|
||||
appConfig?: AppConfig;
|
||||
/** Tool execute options for event-driven tool execution */
|
||||
toolExecuteOptions?: ToolExecuteOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -438,7 +441,10 @@ export async function createAgentChatCompletion(
|
|||
: null;
|
||||
|
||||
// Create event handlers
|
||||
const eventHandlers = isStreaming && handlerConfig ? createOpenAIHandlers(handlerConfig) : {};
|
||||
const eventHandlers =
|
||||
isStreaming && handlerConfig
|
||||
? createOpenAIHandlers(handlerConfig, deps.toolExecuteOptions)
|
||||
: {};
|
||||
|
||||
// Convert messages to internal format
|
||||
const messages = convertMessages(request.messages);
|
||||
|
|
|
|||
133
packages/api/src/agents/run.spec.ts
Normal file
133
packages/api/src/agents/run.spec.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { ToolMessage, AIMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { extractDiscoveredToolsFromHistory } from './run';
|
||||
|
||||
describe('extractDiscoveredToolsFromHistory', () => {
|
||||
it('extracts tool names from tool_search JSON output', () => {
|
||||
const toolSearchOutput = JSON.stringify({
|
||||
found: 3,
|
||||
tools: [
|
||||
{ name: 'tool_a', score: 1.0 },
|
||||
{ name: 'tool_b', score: 0.8 },
|
||||
{ name: 'tool_c', score: 0.5 },
|
||||
],
|
||||
});
|
||||
|
||||
const messages = [
|
||||
new HumanMessage('Find tools'),
|
||||
new AIMessage({ content: '', tool_calls: [{ id: 'call_1', name: 'tool_search', args: {} }] }),
|
||||
new ToolMessage({ content: toolSearchOutput, tool_call_id: 'call_1', name: 'tool_search' }),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
expect(discovered.size).toBe(3);
|
||||
expect(discovered.has('tool_a')).toBe(true);
|
||||
expect(discovered.has('tool_b')).toBe(true);
|
||||
expect(discovered.has('tool_c')).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts tool names from legacy tool_search format', () => {
|
||||
const legacyOutput = `Found 2 tools:
|
||||
- tool_x (score: 0.95)
|
||||
- tool_y (score: 0.80)`;
|
||||
|
||||
const messages = [
|
||||
new ToolMessage({ content: legacyOutput, tool_call_id: 'call_1', name: 'tool_search' }),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
expect(discovered.size).toBe(2);
|
||||
expect(discovered.has('tool_x')).toBe(true);
|
||||
expect(discovered.has('tool_y')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty set when no tool_search messages exist', () => {
|
||||
const messages = [new HumanMessage('Hello'), new AIMessage('Hi there!')];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
expect(discovered.size).toBe(0);
|
||||
});
|
||||
|
||||
it('ignores non-tool_search ToolMessages', () => {
|
||||
const messages = [
|
||||
new ToolMessage({
|
||||
content: '[{"sha": "abc123"}]',
|
||||
tool_call_id: 'call_1',
|
||||
name: 'list_commits_mcp_github',
|
||||
}),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
expect(discovered.size).toBe(0);
|
||||
});
|
||||
|
||||
it('handles multiple tool_search calls in history', () => {
|
||||
const firstOutput = JSON.stringify({
|
||||
tools: [{ name: 'tool_1' }, { name: 'tool_2' }],
|
||||
});
|
||||
const secondOutput = JSON.stringify({
|
||||
tools: [{ name: 'tool_2' }, { name: 'tool_3' }],
|
||||
});
|
||||
|
||||
const messages = [
|
||||
new ToolMessage({ content: firstOutput, tool_call_id: 'call_1', name: 'tool_search' }),
|
||||
new AIMessage('Using discovered tools'),
|
||||
new ToolMessage({ content: secondOutput, tool_call_id: 'call_2', name: 'tool_search' }),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
expect(discovered.size).toBe(3);
|
||||
expect(discovered.has('tool_1')).toBe(true);
|
||||
expect(discovered.has('tool_2')).toBe(true);
|
||||
expect(discovered.has('tool_3')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles malformed JSON in tool_search output', () => {
|
||||
const messages = [
|
||||
new ToolMessage({
|
||||
content: 'This is not valid JSON',
|
||||
tool_call_id: 'call_1',
|
||||
name: 'tool_search',
|
||||
}),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
// Should not throw, just return empty set
|
||||
expect(discovered.size).toBe(0);
|
||||
});
|
||||
|
||||
it('handles tool_search output with empty tools array', () => {
|
||||
const output = JSON.stringify({
|
||||
found: 0,
|
||||
tools: [],
|
||||
});
|
||||
|
||||
const messages = [
|
||||
new ToolMessage({ content: output, tool_call_id: 'call_1', name: 'tool_search' }),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
expect(discovered.size).toBe(0);
|
||||
});
|
||||
|
||||
it('handles non-string content in ToolMessage', () => {
|
||||
const messages = [
|
||||
new ToolMessage({
|
||||
content: [{ type: 'text', text: 'array content' }],
|
||||
tool_call_id: 'call_1',
|
||||
name: 'tool_search',
|
||||
}),
|
||||
];
|
||||
|
||||
const discovered = extractDiscoveredToolsFromHistory(messages);
|
||||
|
||||
// Should handle gracefully
|
||||
expect(discovered.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
GenericTool,
|
||||
RunConfig,
|
||||
IState,
|
||||
LCTool,
|
||||
} from '@librechat/agents';
|
||||
import type { IUser } from '@librechat/data-schemas';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
|
|
@ -166,6 +167,8 @@ type RunAgent = Omit<Agent, 'tools'> & {
|
|||
useLegacyContent?: boolean;
|
||||
toolContextMap?: Record<string, string>;
|
||||
toolRegistry?: LCToolRegistry;
|
||||
/** Serializable tool definitions for event-driven execution */
|
||||
toolDefinitions?: LCTool[];
|
||||
/** Precomputed flag indicating if any tools have defer_loading enabled */
|
||||
hasDeferredTools?: boolean;
|
||||
};
|
||||
|
|
@ -279,23 +282,39 @@ export async function createRun({
|
|||
/**
|
||||
* Override defer_loading for tools that were discovered in previous turns.
|
||||
* This prevents the LLM from having to re-discover tools via tool_search.
|
||||
* Also add the discovered tools' definitions so the LLM has their schemas.
|
||||
*/
|
||||
let toolDefinitions = agent.toolDefinitions ?? [];
|
||||
if (discoveredTools.size > 0 && agent.toolRegistry) {
|
||||
overrideDeferLoadingForDiscoveredTools(agent.toolRegistry, discoveredTools);
|
||||
|
||||
/** Add discovered tools' definitions so the LLM can see their schemas */
|
||||
const existingToolNames = new Set(toolDefinitions.map((d) => d.name));
|
||||
for (const toolName of discoveredTools) {
|
||||
if (existingToolNames.has(toolName)) {
|
||||
continue;
|
||||
}
|
||||
const toolDef = agent.toolRegistry.get(toolName);
|
||||
if (toolDef) {
|
||||
toolDefinitions = [...toolDefinitions, toolDef];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint);
|
||||
const agentInput: AgentInputs = {
|
||||
provider,
|
||||
reasoningKey,
|
||||
toolDefinitions,
|
||||
agentId: agent.id,
|
||||
name: agent.name ?? undefined,
|
||||
tools: agent.tools,
|
||||
clientOptions: llmConfig,
|
||||
instructions: systemContent,
|
||||
name: agent.name ?? undefined,
|
||||
toolRegistry: agent.toolRegistry,
|
||||
maxContextTokens: agent.maxContextTokens,
|
||||
useLegacyContent: agent.useLegacyContent ?? false,
|
||||
discoveredTools: discoveredTools.size > 0 ? Array.from(discoveredTools) : undefined,
|
||||
};
|
||||
agentInputs.push(agentInput);
|
||||
};
|
||||
|
|
|
|||
126
packages/api/src/agents/tools.spec.ts
Normal file
126
packages/api/src/agents/tools.spec.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { buildToolSet, BuildToolSetConfig } from './tools';
|
||||
|
||||
describe('buildToolSet', () => {
|
||||
describe('event-driven mode (toolDefinitions)', () => {
|
||||
it('builds toolSet from toolDefinitions when available', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
toolDefinitions: [
|
||||
{ name: 'tool_search', description: 'Search for tools' },
|
||||
{ name: 'list_commits_mcp_github', description: 'List commits' },
|
||||
{ name: 'calculator', description: 'Calculate' },
|
||||
],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.size).toBe(3);
|
||||
expect(toolSet.has('tool_search')).toBe(true);
|
||||
expect(toolSet.has('list_commits_mcp_github')).toBe(true);
|
||||
expect(toolSet.has('calculator')).toBe(true);
|
||||
});
|
||||
|
||||
it('includes tool_search in toolSet for deferred tools workflow', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
toolDefinitions: [
|
||||
{ name: 'tool_search', description: 'Search for deferred tools' },
|
||||
{ name: 'deferred_tool_1', description: 'A deferred tool', defer_loading: true },
|
||||
{ name: 'deferred_tool_2', description: 'Another deferred tool', defer_loading: true },
|
||||
],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.has('tool_search')).toBe(true);
|
||||
expect(toolSet.has('deferred_tool_1')).toBe(true);
|
||||
expect(toolSet.has('deferred_tool_2')).toBe(true);
|
||||
});
|
||||
|
||||
it('prefers toolDefinitions over tools when both are present', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
toolDefinitions: [{ name: 'from_definitions' }],
|
||||
tools: [{ name: 'from_tools' }],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.size).toBe(1);
|
||||
expect(toolSet.has('from_definitions')).toBe(true);
|
||||
expect(toolSet.has('from_tools')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy mode (tools)', () => {
|
||||
it('falls back to tools when toolDefinitions is empty', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
toolDefinitions: [],
|
||||
tools: [{ name: 'web_search' }, { name: 'calculator' }],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.size).toBe(2);
|
||||
expect(toolSet.has('web_search')).toBe(true);
|
||||
expect(toolSet.has('calculator')).toBe(true);
|
||||
});
|
||||
|
||||
it('falls back to tools when toolDefinitions is undefined', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
tools: [{ name: 'tool_a' }, { name: 'tool_b' }],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.size).toBe(2);
|
||||
expect(toolSet.has('tool_a')).toBe(true);
|
||||
expect(toolSet.has('tool_b')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('returns empty set when agentConfig is null', () => {
|
||||
const toolSet = buildToolSet(null);
|
||||
expect(toolSet.size).toBe(0);
|
||||
});
|
||||
|
||||
it('returns empty set when agentConfig is undefined', () => {
|
||||
const toolSet = buildToolSet(undefined);
|
||||
expect(toolSet.size).toBe(0);
|
||||
});
|
||||
|
||||
it('returns empty set when both toolDefinitions and tools are empty', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
toolDefinitions: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
expect(toolSet.size).toBe(0);
|
||||
});
|
||||
|
||||
it('filters out null/undefined tool entries', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
tools: [{ name: 'valid_tool' }, null, undefined, { name: 'another_valid' }],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.size).toBe(2);
|
||||
expect(toolSet.has('valid_tool')).toBe(true);
|
||||
expect(toolSet.has('another_valid')).toBe(true);
|
||||
});
|
||||
|
||||
it('filters out empty string tool names', () => {
|
||||
const agentConfig: BuildToolSetConfig = {
|
||||
toolDefinitions: [{ name: 'valid' }, { name: '' }, { name: 'also_valid' }],
|
||||
};
|
||||
|
||||
const toolSet = buildToolSet(agentConfig);
|
||||
|
||||
expect(toolSet.size).toBe(2);
|
||||
expect(toolSet.has('valid')).toBe(true);
|
||||
expect(toolSet.has('also_valid')).toBe(true);
|
||||
expect(toolSet.has('')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
39
packages/api/src/agents/tools.ts
Normal file
39
packages/api/src/agents/tools.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
interface ToolDefLike {
|
||||
name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ToolInstanceLike {
|
||||
name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface BuildToolSetConfig {
|
||||
toolDefinitions?: ToolDefLike[];
|
||||
tools?: (ToolInstanceLike | null | undefined)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Set of tool names for use with formatAgentMessages.
|
||||
*
|
||||
* In event-driven mode, tools are defined via toolDefinitions (which includes
|
||||
* deferred tools like tool_search). In legacy mode, tools come from loaded
|
||||
* tool instances.
|
||||
*
|
||||
* This ensures tool_search and other deferred tools are included in the toolSet,
|
||||
* allowing their ToolMessages to be preserved in conversation history.
|
||||
*/
|
||||
export function buildToolSet(agentConfig: BuildToolSetConfig | null | undefined): Set<string> {
|
||||
if (!agentConfig) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const { toolDefinitions, tools } = agentConfig;
|
||||
|
||||
const toolNames =
|
||||
toolDefinitions && toolDefinitions.length > 0
|
||||
? toolDefinitions.map((def) => def.name)
|
||||
: (tools ?? []).map((tool) => tool?.name);
|
||||
|
||||
return new Set(toolNames.filter((name): name is string => Boolean(name)));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue