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'}`); } };