UNPKG

openai-agents

Version:

A TypeScript library extending the OpenAI Node.js SDK for building highly customizable agents and simplifying 'function calling'. Easily create and manage tools to extend LLM capabilities.

217 lines (216 loc) 8.71 kB
import path from 'path'; import * as fs from 'fs/promises'; import { ValidationError, ToolNotFoundError, DirectoryAccessError, FileReadError, FileImportError, InvalidToolError, } from '../errors'; /** * @class ToolsRegistry * @description Singleton class for managing the tools registry. Holds the currently loaded tools. */ export class ToolsRegistry { static instance = null; static toolsDirPath = null; /** * Gets the current instance of the tools registry. */ static getInstance() { return ToolsRegistry.instance; } /** * Sets the instance of the tools registry. */ static setInstance(tools) { ToolsRegistry.instance = tools; } } /** * Validates the function name, ensuring it meets OpenAI's requirements. */ const validateFunctionName = (name) => { if (!name || typeof name !== 'string') { throw new InvalidToolError(name, 'Function name must be a non-empty string'); } if (name.length > 64) { throw new InvalidToolError(name, 'Function name must not exceed 64 characters'); } if (!/^[a-zA-Z0-9_-]+$/.test(name)) { throw new InvalidToolError(name, 'Function name must contain only alphanumeric characters, underscores, and hyphens'); } }; /** * Validates the function parameters, ensuring they are a non-null object. */ const validateFunctionParameters = (params, name) => { if (!params || typeof params !== 'object') { throw new InvalidToolError(name, 'Function parameters must be a non-null object'); } }; /** * Validates the function definition, * checking name, description, parameters, and strict flag. */ const validateFunctionDefinition = (func) => { const { name, description, parameters, strict } = func; if (!func || typeof func !== 'object') { throw new InvalidToolError(name, 'Function definition must be a non-null object'); } validateFunctionName(name); if (description !== undefined && typeof description !== 'string') { throw new InvalidToolError(name, 'Function description must be a string when provided'); } if (parameters !== undefined) { validateFunctionParameters(parameters, name); } if (strict !== undefined && strict !== null && typeof strict !== 'boolean') { throw new InvalidToolError(name, 'Function strict flag must be a boolean when provided'); } }; /** * Validates a chat completion tool definition, * ensuring it has the correct type and a valid function definition. */ const validateChatCompletionTool = (tool) => { const { function: functionDefinition, function: { name }, type, } = tool; if (!tool || typeof tool !== 'object') { throw new InvalidToolError(name, 'Chat completion tool must be a non-null object'); } if (type !== 'function') { throw new InvalidToolError(name, 'Chat completion tool type must be "function"'); } validateFunctionDefinition(functionDefinition); }; /** * Validates the configuration of tools, ensuring that all defined tools have corresponding implementations. */ const validateToolConfiguration = (fnDefinitions, functions) => { for (const def of fnDefinitions) { const functionName = def.function.name; if (!functions[functionName]) { throw new ToolNotFoundError(`Missing function implementation for tool: ${functionName}`); } } }; /** * Loads tool files (both definitions and implementations) from a specified directory. * * @param {string} dirPath - The path to the directory containing tool files. * @returns {Promise<AgentTools>} A promise that resolves to the loaded agent tools. * @throws {DirectoryAccessError | FileReadError | FileImportError | InvalidToolError | ToolNotFoundError} If an error occurs during loading. */ export const loadToolsDirFunctions = async (dirPath) => { try { // Validate directory access try { await fs.access(dirPath); } catch (error) { throw new DirectoryAccessError(dirPath, error instanceof Error ? error : undefined); } // Read directory contents let files; try { files = await fs.readdir(dirPath); } catch (error) { throw new FileReadError(dirPath, error instanceof Error ? error : undefined); } const toolDefinitions = []; const toolFunctions = {}; // Process each file for (const file of files) { if (!file.endsWith('.js') && !file.endsWith('.ts')) continue; const fullPath = path.join(dirPath, file); // Validate file status try { const stat = await fs.stat(fullPath); if (!stat.isFile()) continue; } catch (error) { throw new FileReadError(fullPath, error instanceof Error ? error : undefined); } // Import file contents let fileFunctions; try { fileFunctions = await import(fullPath); } catch (error) { throw new FileImportError(fullPath, error instanceof Error ? error : undefined); } // Process functions const funcs = fileFunctions.default || fileFunctions; for (const [fnName, fn] of Object.entries(funcs)) { try { if (typeof fn === 'function') { toolFunctions[fnName] = fn; } else { // Validate as tool definition validateChatCompletionTool(fn); toolDefinitions.push(fn); } } catch (error) { if (error instanceof InvalidToolError) throw error; throw new InvalidToolError(fnName, `Unexpected error validating tool: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } // Validate final configuration validateToolConfiguration(toolDefinitions, toolFunctions); const tools = { toolDefinitions, toolFunctions }; ToolsRegistry.setInstance(tools); return tools; } catch (error) { if (error instanceof DirectoryAccessError || error instanceof FileReadError || error instanceof FileImportError || error instanceof InvalidToolError || error instanceof ToolNotFoundError) throw error; throw new Error(`Unexpected error loading tools: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Imports and returns specific tool functions based on their names. * Loads tools from the directory if they haven't been loaded yet. * * @param {string[]} toolNames - An array of tool names to import. * @returns {Promise<ToolChoices>} A promise that resolves to the imported tool functions and choices. * @throws {ValidationError | ToolNotFoundError | InvalidToolError} If the tools directory path is not set or if any requested tools are missing. */ export const importToolFunctions = async (toolNames) => { try { if (!ToolsRegistry.toolsDirPath) { throw new ValidationError('Tools directory path not set. Call loadToolsDirFunctions with your tools directory path first.'); } const tools = ToolsRegistry.getInstance() ?? (await loadToolsDirFunctions(ToolsRegistry.toolsDirPath)); const toolChoices = toolNames .map((toolName) => tools.toolDefinitions.find((tool) => tool.function.name === toolName)) .filter((tool) => tool !== undefined); const missingTools = toolNames.filter((name) => !toolChoices.some((tool) => tool.function.name === name)); if (missingTools.length > 0) { throw new ToolNotFoundError(`The following tools were not found: ${missingTools.join(', ')}`); } return { toolFunctions: tools.toolFunctions, toolChoices, }; } catch (error) { if (error instanceof DirectoryAccessError || error instanceof FileReadError || error instanceof FileImportError || error instanceof InvalidToolError || error instanceof ValidationError || error instanceof ToolNotFoundError) throw error; throw new Error(`Failed to import tool functions: ${error instanceof Error ? error.message : 'Unknown error'}`); } };