'use strict'; var index_js = require('openai/resources/index.js'); var OpenAI = require('openai'); var path = require('path'); var fs = require('fs/promises'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs); class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; } } class ToolNotFoundError extends Error { constructor(toolName) { super(`Tool not found: ${toolName}`); this.name = 'ToolNotFoundError'; } } class DirectoryAccessError extends Error { constructor(dirPath, cause) { super(`Was not possible to access the directory: ${dirPath}`); this.name = 'DirectoryAccessError'; this.cause = cause; } } class FileReadError extends Error { constructor(filePath, cause) { super(`Error reading file: ${filePath}`); this.name = 'FileReadError'; this.cause = cause; } } class FileImportError extends Error { constructor(filePath, cause) { super(`Error importing file: ${filePath}`); this.name = 'FileImportError'; this.cause = cause; } } class InvalidToolError extends Error { constructor(filePath, functionName, message) { super(`Invalid tool found at ${filePath}: ${functionName}. ${message}`); this.name = 'InvalidToolError'; } } class ToolConfigurationError extends Error { constructor(message) { super(`Error on the tools configuration: ${message}`); this.name = 'ToolConfigurationError'; } } /** * Singleton class for managing the tools registry. */ class ToolsRegistry { static instance = null; /** * Gets the current instance of the tools registry. * @returns {AgentTools | null} The current tools instance or null if not set. */ static getInstance() { return ToolsRegistry.instance; } /** * Sets the instance of the tools registry. * @param {AgentTools} tools - The tools instance to set. */ static setInstance(tools) { ToolsRegistry.instance = tools; } } /** * Validates the function name. * @param {string} name - The name of the function to validate. * @throws {ValidationError} If the function name is invalid. */ const validateFunctionName = (name) => { if (!name || typeof name !== 'string') { throw new ValidationError('Function name must be a non-empty string'); } if (name.length > 64) { throw new ValidationError('Function name must not exceed 64 characters'); } if (!/^[a-zA-Z0-9_-]+$/.test(name)) { throw new ValidationError('Function name must contain only alphanumeric characters, underscores, and hyphens'); } }; /** * Validates the function parameters. * @param {unknown} params - The parameters of the function to validate. * @throws {ValidationError} If the parameters are invalid. */ const validateFunctionParameters = (params) => { if (!params || typeof params !== 'object') { throw new ValidationError('Function parameters must be a non-null object'); } }; /** * Validates the function definition. * @param {unknown} func - The function definition to validate. * @throws {ValidationError} If the function definition is invalid. */ const validateFunctionDefinition = (func) => { if (!func || typeof func !== 'object') { throw new ValidationError('Function definition must be a non-null object'); } const { name, description, parameters, strict } = func; validateFunctionName(name); if (description !== undefined && typeof description !== 'string') { throw new ValidationError('Function description must be a string when provided'); } if (parameters !== undefined) { validateFunctionParameters(parameters); } if (strict !== undefined && strict !== null && typeof strict !== 'boolean') { throw new ValidationError('Function strict flag must be a boolean when provided'); } }; /** * Validates a chat completion tool definition. * @param {unknown} tool - The tool definition to validate. * @throws {ValidationError} If the tool definition is invalid. */ const validateChatCompletionTool = (tool) => { if (!tool || typeof tool !== 'object') { throw new ValidationError('Chat completion tool must be a non-null object'); } const { function: functionDefinition, type } = tool; if (type !== 'function') { throw new ValidationError('Chat completion tool type must be "function"'); } validateFunctionDefinition(functionDefinition); }; /** * Validates the configuration of tools and their definitions. * @param {ChatCompletionTool[]} fnDefinitions - The array of tool definitions. * @param {ToolFunctions} functions - The object containing tool implementations. * @throws {ValidationError | ToolNotFoundError} If there is a mismatch in definitions or missing implementations. */ const validateToolConfiguration = (fnDefinitions, functions) => { const definitionCount = fnDefinitions.length; const functionCount = Object.keys(functions).length; if (definitionCount !== functionCount) { throw new ValidationError(`Mismatch between number of function definitions (${definitionCount}) and implementations (${functionCount})`); } 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 from a specified directory. * @param {string} dirPath - The path to the directory containing tool files. * @returns {Promise} A promise that resolves to the loaded agent tools. * @throws {DirectoryAccessError | FileReadError | FileImportError | InvalidToolError | ToolConfigurationError | ValidationError} If an error occurs during loading. */ const loadToolsDirFiles = async (dirPath) => { try { // Validate directory access try { await fs__namespace.access(dirPath); } catch (error) { throw new DirectoryAccessError(dirPath, error instanceof Error ? error : undefined); } // Read directory contents let files; try { files = await fs__namespace.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__namespace.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 ValidationError) { throw new InvalidToolError(fullPath, fnName, `Invalid tool definition: ${error.message}`); } throw new InvalidToolError(fullPath, fnName, 'Unexpected error validating tool'); } } } // 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 ToolConfigurationError || error instanceof ValidationError) { throw error; } throw new Error(`Unexpected error loading tools: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Imports tool functions based on their names. * @param {string[]} toolNames - An array of tool names to import. * @returns {Promise} A promise that resolves to the imported tool functions and choices. * @throws {ValidationError | ToolNotFoundError} If the tools directory path is not set or tools are missing. */ const importToolFunctions = async (toolNames) => { try { if (!OpenAIAgent.toolsDirPath) { throw new ValidationError('Tools directory path not set. Call loadToolsDirFiles with your tools directory path first.'); } const tools = ToolsRegistry.getInstance() ?? (await loadToolsDirFiles(OpenAIAgent.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 ValidationError || error instanceof ToolNotFoundError) { throw error; } throw new Error(`Failed to import tool functions: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Calculates the sum of prompt tokens, completion tokens, and total tokens from multiple `CompletionUsage` objects. * * @param {...CompletionUsage} usages - One or more `CompletionUsage` objects to sum. * @returns {CompletionUsage} A new `CompletionUsage` object representing the sum of the input usages. */ const getTokensSum = (...usages) => { const initialUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, }; return usages.reduce((accumulator, currentUsage) => { return { prompt_tokens: accumulator.prompt_tokens + (currentUsage?.prompt_tokens ?? 0), completion_tokens: accumulator.completion_tokens + (currentUsage?.completion_tokens ?? 0), total_tokens: accumulator.total_tokens + (currentUsage?.total_tokens ?? 0), }; }, initialUsage); }; /** * Extracts and aggregates the `usage` information from multiple `ChatCompletion` objects. * * @param {...ChatCompletion} completions - One or more `ChatCompletion` objects. * @returns {CompletionUsage} A `CompletionUsage` object representing the aggregated usage data. * Returns an object with all properties set to 0 if no completions are provided or if none of them have a usage property. */ const getCompletionsUsage = (...completions) => { const usages = []; for (const completion of completions) { if (completion.usage) { usages.push(completion.usage); } } return getTokensSum(...usages); }; const handleNResponses = (response, queryParams) => { let responses = []; const responseMessage = response.choices[0].message; if (queryParams.n) { for (const choice of response.choices) { if (choice.message.content) responses.push(choice.message.content); } } else { responses = [responseMessage.content ?? 'Response not received.']; } return responses; }; class AgentStorage { redisClient = null; constructor(client) { this.redisClient = client; } async saveChatHistory(userId, messages) { if (!this.redisClient) { throw new Error('Redis client not initialized.'); } const id = userId ? userId : 'default'; if (messages[0]?.role === 'system') messages.shift(); try { for (const message of messages) { await this.redisClient.lPush(`user:${id}`, JSON.stringify(message)); } return await this.getStoredMessages(id); } catch (error) { throw new Error(`Error saving chat history of user: ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async deleteHistory(userId) { if (!this.redisClient) { throw new Error('Redis client not initialized.'); } try { return await this.redisClient.del(`user:${userId}`); } catch (error) { throw new Error(`Error deleting chat history of user: ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getStoredMessages(userId, options = {}) { if (!this.redisClient) { throw new Error('Redis client not initialized.'); } const { appended_messages, remove_tool_messages } = options; let messages; if (appended_messages === 0) { return []; } else if (appended_messages) { messages = await this.redisClient.lRange(`user:${userId}`, 0, appended_messages - 1); } else { messages = await this.redisClient.lRange(`user:${userId}`, 0, -1); } if (!messages || messages.length === 0) return []; const parsedMessages = messages .map((message) => JSON.parse(message)) .reverse(); if (remove_tool_messages) { return parsedMessages.filter((message) => message.role === 'user' || (message.role === 'assistant' && !message.tool_calls)); } return parsedMessages; } } /** * A class that extends the OpenAI API client to manage chat completions and tool interactions. */ class OpenAIAgent extends OpenAI { static REQUIRED_ENV_VARS = ['OPENAI_API_KEY']; completionParams; defaultHistoryMessages; Storage = null; static toolsDirPath = null; system_instruction; historyOptions; constructor(agentOptions, options) { OpenAIAgent.validateEnvironment(); super(options); if (!agentOptions.model) { throw new ValidationError('Model is required to initialize the agent instance'); } this.system_instruction = agentOptions.system_instruction; delete agentOptions.system_instruction; this.defaultHistoryMessages = agentOptions.messages; delete agentOptions.messages; this.completionParams = agentOptions; } get model() { return this.completionParams.model; } set model(value) { this.completionParams.model = value; } get temperature() { return this.completionParams.temperature; } set temperature(value) { this.completionParams.temperature = value; } get top_p() { return this.completionParams.top_p; } set top_p(value) { this.completionParams.top_p = value; } get max_completion_tokens() { return this.completionParams.max_completion_tokens; } set max_completion_tokens(value) { this.completionParams.max_completion_tokens = value; } get max_tokens() { return this.completionParams.max_tokens; } set max_tokens(value) { this.completionParams.max_tokens = value; } get n() { return this.completionParams.n; } set n(value) { this.completionParams.n = value; } get frequency_penalty() { return this.completionParams.frequency_penalty; } set frequency_penalty(value) { this.completionParams.frequency_penalty = value; } get presence_penalty() { return this.completionParams.presence_penalty; } set presence_penalty(value) { this.completionParams.presence_penalty = value; } get tool_choice() { return this.completionParams.tool_choice; } set tool_choice(value) { this.completionParams.tool_choice = value; } get parallel_tool_calls() { return this.completionParams.parallel_tool_calls; } set parallel_tool_calls(value) { this.completionParams.parallel_tool_calls = value; } get audioParams() { return this.completionParams.audio; } set audioParams(value) { this.completionParams.audio = value; } get response_format() { return this.completionParams.response_format; } set response_format(value) { this.completionParams.response_format = value; } get logit_bias() { return this.completionParams.logit_bias; } set logit_bias(value) { this.completionParams.logit_bias = value; } get logprobs() { return this.completionParams.logprobs; } set logprobs(value) { this.completionParams.logprobs = value; } get top_logprobs() { return this.completionParams.top_logprobs; } set top_logprobs(value) { this.completionParams.top_logprobs = value; } get metadata() { return this.completionParams.metadata; } set metadata(value) { this.completionParams.metadata = value; } get stop() { return this.completionParams.stop; } set stop(value) { this.completionParams.stop = value; } get modalities() { return this.completionParams.modalities; } set modalities(value) { this.completionParams.modalities = value; } get prediction() { return this.completionParams.prediction; } set prediction(value) { this.completionParams.prediction = value; } get seed() { return this.completionParams.seed; } set seed(value) { this.completionParams.seed = value; } get service_tier() { return this.completionParams.service_tier; } set service_tier(value) { this.completionParams.service_tier = value; } get store() { return this.completionParams.store; } set store(value) { this.completionParams.store = value; } /** * Validates that required environment variables are set. * @throws {Error} If any required environment variables are missing. */ static validateEnvironment() { const missingVars = OpenAIAgent.REQUIRED_ENV_VARS.filter((varName) => !process.env[varName]); if (missingVars.length > 0) { throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`); } } _handleSystemInstructionMessage(defaultInstruction, customInstruction) { let systemInstructionMessage = { role: 'system', content: '', }; if (defaultInstruction && !customInstruction) { systemInstructionMessage = { role: 'system', content: defaultInstruction, }; } else if (customInstruction) { systemInstructionMessage = { role: 'system', content: customInstruction, }; } return systemInstructionMessage; } async _getContextMessages(queryParams, historyOptions) { const userId = queryParams.user ? queryParams.user : 'default'; let templateMessages = []; if (this.defaultHistoryMessages) { templateMessages = this.defaultHistoryMessages; } if (this.Storage) { const storedMessages = await this.Storage.getStoredMessages(userId, historyOptions); templateMessages.push(...storedMessages); } return templateMessages; } async _callChosenFunctions(responseMessage, functions) { if (!responseMessage.tool_calls?.length) { throw new Error('No tool calls found in the response message'); } const toolMessages = []; for (const tool of responseMessage.tool_calls) { const { id, function: { name, arguments: args }, } = tool; try { const currentFunction = functions[name]; if (!currentFunction) { throw new Error(`Function '${name}' not found`); } let parsedArgs; try { parsedArgs = JSON.parse(args); } catch (error) { console.error(error); throw new Error(`Invalid arguments format for function '${name}': ${args}`); } const functionResponse = await Promise.resolve(currentFunction(parsedArgs)); if (functionResponse === undefined) { throw new Error(`Function '${name}' returned no response`); } toolMessages.push({ tool_call_id: id, role: 'tool', content: JSON.stringify(functionResponse), }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(`Error calling function '${name}':`, errorMessage); toolMessages.push({ tool_call_id: id, role: 'tool', content: JSON.stringify({ error: errorMessage }), }); } } return toolMessages; } async _handleToolCompletion(response, queryParams, newMessages, toolFunctions) { if (!queryParams?.messages?.length) queryParams.messages = []; const responseMessage = response.choices[0].message; queryParams.messages.push(responseMessage); const toolMessages = await this._callChosenFunctions(responseMessage, toolFunctions); queryParams.messages.push(...toolMessages); newMessages.push(...toolMessages); const secondResponse = await this.chat.completions.create(queryParams); const secondResponseMessage = secondResponse.choices[0].message; if (!secondResponseMessage) { throw new Error('No response message received from second tool query to OpenAI'); } newMessages.push(secondResponseMessage); if (this.Storage) await this.Storage.saveChatHistory(queryParams.user, newMessages); const responses = handleNResponses(secondResponse, queryParams); return { choices: responses, total_usage: getCompletionsUsage(response, secondResponse), completion_messages: newMessages, completions: [response, secondResponse], }; } async loadToolFuctions(toolsDirAddr) { if (!toolsDirAddr) throw new ValidationError('Tools directory path required.'); await loadToolsDirFiles(toolsDirAddr); OpenAIAgent.toolsDirPath = toolsDirAddr; return true; } async setStorage(client, options) { if (!client) throw new ValidationError('Instance of RedisClientType required.'); if (options?.history) this.historyOptions = options.history; this.Storage = new AgentStorage(client); return true; } async deleteChatHistory(userId) { if (!this.Storage) { throw new ValidationError('Agent storage is not initalized.'); } await this.Storage.deleteHistory(userId); return true; } async getChatHistory(userId, options) { if (this.Storage) { const messages = await this.Storage.getStoredMessages(userId, options); return messages; } return []; } async createChatCompletion(options) { const queryParams = { ...this.completionParams, ...options.custom_params, }; const historyOptions = { ...this.historyOptions, ...options.history, }; if (this.Storage && options.tool_choices && this.historyOptions?.appended_messages) historyOptions.remove_tool_messages = true; const storedMessages = await this._getContextMessages(queryParams, historyOptions); const systemImstructionMessage = this._handleSystemInstructionMessage(this.system_instruction, options.system_instruction); if (systemImstructionMessage.content) { // Overwrites the default instruction if there is a new instruction in the current request if (storedMessages[0]?.role === 'system') storedMessages.shift(); storedMessages.unshift(systemImstructionMessage); } const newMessages = [ { role: 'user', content: options.message }, ]; storedMessages.push(...newMessages); queryParams.messages = storedMessages; try { let toolFunctions; if (options.tool_choices?.length) { const toolChoices = await importToolFunctions(options.tool_choices); queryParams.tools = toolChoices.toolChoices; toolFunctions = toolChoices.toolFunctions; } const response = await this.chat.completions.create(queryParams); const responseMessage = response.choices[0].message; if (!responseMessage) { throw new Error('No response message received from OpenAI'); } newMessages.push(responseMessage); if (responseMessage.tool_calls && toolFunctions) { return await this._handleToolCompletion(response, queryParams, newMessages, toolFunctions); } else { if (this.Storage) await this.Storage.saveChatHistory(queryParams.user, newMessages); const responses = handleNResponses(response, queryParams); return { choices: responses, total_usage: getCompletionsUsage(response), completion_messages: newMessages, completions: [response], }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Chat completion failed: ${errorMessage}`); } } } exports.OpenAIAgent = OpenAIAgent; Object.keys(index_js).forEach(function (k) { if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, { enumerable: true, get: function () { return index_js[k]; } }); });