UNPKG

@robota-sdk/openai

Version:

OpenAI integration for Robota SDK - GPT-4, GPT-3.5, function calling, and tool integration with OpenAI's API

510 lines (503 loc) 15.8 kB
'use strict'; var core = require('@robota-sdk/core'); var fs = require('fs'); var path = require('path'); function _interopNamespace(e) { if (e && e.__esModule) return 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__*/_interopNamespace(fs); var path__namespace = /*#__PURE__*/_interopNamespace(path); // src/provider.ts // src/adapter.ts var OpenAIConversationAdapter = class { /** * Filter messages for OpenAI compatibility * * OpenAI has specific requirements: * - Tool messages must have valid toolCallId * - Messages must be in proper sequence * - Tool messages without toolCallId should be excluded */ static filterMessagesForOpenAI(messages) { return messages.filter((msg) => { if (msg.role === "user" || msg.role === "assistant" || msg.role === "system") { return true; } if (msg.role === "tool") { const toolMsg = msg; return !!(toolMsg.toolCallId && toolMsg.toolCallId.trim() !== "" && toolMsg.toolCallId !== "unknown"); } return false; }); } /** * Convert UniversalMessage array to OpenAI message format * Now properly handles tool messages for OpenAI's tool calling feature */ static toOpenAIFormat(messages) { const filteredMessages = this.filterMessagesForOpenAI(messages); return filteredMessages.map((msg) => this.convertMessage(msg)); } /** * Convert a single UniversalMessage to OpenAI format * Handles all message types including tool messages */ static convertMessage(msg) { const messageRole = msg.role; if (messageRole === "user") { const userMsg = msg; const result = { role: "user", content: userMsg.content }; if (userMsg.name) { result.name = userMsg.name; } return result; } if (messageRole === "assistant") { const assistantMsg = msg; if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) { const result = { role: "assistant", content: assistantMsg.content || null, tool_calls: assistantMsg.toolCalls.map((toolCall) => ({ id: toolCall.id, type: "function", function: { name: toolCall.function.name, arguments: toolCall.function.arguments } })) }; return result; } return { role: "assistant", content: assistantMsg.content || "" }; } if (messageRole === "system") { const systemMsg = msg; return { role: "system", content: systemMsg.content }; } if (messageRole === "tool") { const toolMsg = msg; const result = { role: "tool", content: toolMsg.content, tool_call_id: toolMsg.toolCallId || "unknown" }; return result; } const _exhaustiveCheck = msg; return _exhaustiveCheck; } /** * Add system prompt to message array if needed */ static addSystemPromptIfNeeded(messages, systemPrompt) { if (!systemPrompt) { return messages; } const hasSystemMessage = messages.some((msg) => msg.role === "system"); if (hasSystemMessage) { return messages; } return [ { role: "system", content: systemPrompt }, ...messages ]; } }; var PayloadLogger = class { enabled; logDir; includeTimestamp; constructor(enabled = false, logDir = "./logs/api-payloads", includeTimestamp = true) { this.enabled = enabled; this.logDir = logDir; this.includeTimestamp = includeTimestamp; if (this.enabled) { this.ensureLogDirectoryExists(); } } /** * Log API payload to file * @param payload - The API request payload * @param type - Type of request ('chat' or 'stream') */ async logPayload(payload, type = "chat") { if (!this.enabled) { return; } try { const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); const filename = this.includeTimestamp ? `openai-${type}-${timestamp}.json` : `openai-${type}-${Date.now()}.json`; const filepath = path__namespace.join(this.logDir, filename); const logData = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), type, provider: "openai", payload: this.sanitizePayload(payload) }; await fs__namespace.promises.writeFile( filepath, JSON.stringify(logData, null, 2), "utf8" ); } catch (error) { } } /** * Ensure log directory exists */ ensureLogDirectoryExists() { if (!fs__namespace.existsSync(this.logDir)) { try { fs__namespace.mkdirSync(this.logDir, { recursive: true }); } catch (error) { } } } /** * Sanitize payload to remove sensitive information * @param payload - Raw payload object * @returns Sanitized payload */ sanitizePayload(payload) { const sanitized = JSON.parse(JSON.stringify(payload)); return sanitized; } /** * Check if logging is enabled */ isEnabled() { return this.enabled; } }; // src/provider.ts var OpenAIProvider = class extends core.BaseAIProvider { /** * Provider identifier name * @readonly */ name = "openai"; /** * OpenAI client instance * @internal */ client; /** * Client type identifier * @readonly */ type = "openai"; /** * OpenAI client instance (alias for backwards compatibility) * @readonly * @deprecated Use the private client property instead */ instance; /** * Provider configuration options * @readonly */ options; /** * Payload logger for API request logging * @internal */ payloadLogger; /** * Create a new OpenAI provider instance * * @param options - Configuration options for the OpenAI provider * * @throws {Error} When client is not provided in options */ constructor(options) { super(); this.options = { temperature: 0.7, maxTokens: void 0, ...options }; if (!options.client) { throw new Error("OpenAI client is not injected. The client option is required."); } this.client = options.client; this.instance = options.client; this.payloadLogger = new PayloadLogger( this.options.enablePayloadLogging || false, this.options.payloadLogDir || "./logs/api-payloads", this.options.includeTimestampInLogFiles !== false ); } /** * Convert function definitions to OpenAI tool format * * Transforms universal function definitions into OpenAI's specific tool format * required by the Chat Completions API. * * @param functions - Array of universal function definitions * @returns Array of OpenAI-formatted tools */ formatFunctions(functions) { return functions.map((fn) => ({ type: "function", function: { name: fn.name, description: fn.description || "", parameters: fn.parameters } })); } /** * Filter conversation history for OpenAI API compatibility * * Converts messages to OpenAI format and filters out invalid tool messages. * * @param messages - Array of messages to filter * @returns OpenAI-formatted messages array */ filterHistory(messages) { return OpenAIConversationAdapter.toOpenAIFormat(messages); } /** * Configure tools for OpenAI API request * * Transforms function schemas into OpenAI tool format and sets tool_choice. * * @param tools - Array of function schemas * @returns OpenAI tool configuration object */ configureTools(tools) { if (!tools || !Array.isArray(tools)) { return void 0; } return { tools: this.formatFunctions(tools), tool_choice: "auto" // Force new tool_calls format }; } /** * Send a chat request to OpenAI and receive a complete response * * Processes the provided context and sends it to OpenAI's Chat Completions API. * Handles message format conversion, error handling, and response parsing. * * @param model - Model name to use (e.g., 'gpt-4', 'gpt-3.5-turbo') * @param context - Context object containing messages and system prompt * @param options - Optional generation parameters and tools * @returns Promise resolving to the model's response * * @throws {Error} When context is invalid * @throws {Error} When messages array is invalid * @throws {Error} When message format conversion fails * @throws {Error} When OpenAI API call fails */ async chat(model, context, options) { this.validateContext(context); const openaiMessages = this.filterHistory(context.messages); const completionOptions = { model, messages: openaiMessages, max_tokens: options?.maxTokens || this.options.maxTokens, temperature: options?.temperature || this.options.temperature }; const toolConfig = this.configureTools(options?.tools); if (toolConfig) { completionOptions.tools = toolConfig.tools; completionOptions.tool_choice = toolConfig.tool_choice; } if (this.options.responseFormat) { if (this.options.responseFormat === "text") { completionOptions.response_format = { type: "text" }; } else if (this.options.responseFormat === "json_object") { completionOptions.response_format = { type: "json_object" }; } else if (this.options.responseFormat === "json_schema") { if (!this.options.jsonSchema) { throw new Error('jsonSchema is required when responseFormat is "json_schema"'); } completionOptions.response_format = { type: "json_schema", json_schema: this.options.jsonSchema }; } } try { await this.payloadLogger.logPayload(completionOptions, "chat"); const response = await this.client.chat.completions.create(completionOptions); return this.parseResponse(response); } catch (error) { this.handleApiError(error, "chat"); } } /** * Convert OpenAI API response to universal ModelResponse format * * Transforms the OpenAI-specific response format into the standard format * used across all providers in Robota. * * @param response - Raw response from OpenAI Chat Completions API * @returns Parsed model response in universal format * * @internal */ parseResponse(response) { const message = response.choices[0].message; const result = { content: message.content || void 0, usage: response.usage ? { promptTokens: response.usage.prompt_tokens, completionTokens: response.usage.completion_tokens, totalTokens: response.usage.total_tokens } : void 0, metadata: { model: response.model, finishReason: response.choices[0].finish_reason, systemFingerprint: response.system_fingerprint } }; if (message.tool_calls && message.tool_calls.length > 0) { result.toolCalls = message.tool_calls.map((toolCall) => ({ id: toolCall.id, type: "function", function: { name: toolCall.function.name, arguments: toolCall.function.arguments } })); } return result; } /** * Convert OpenAI streaming response chunk to universal format * * Transforms individual chunks from OpenAI's streaming response into the * standard StreamingResponseChunk format used across all providers. * * @param chunk - Raw chunk from OpenAI streaming API * @returns Parsed streaming response chunk * * @internal */ parseStreamingChunk(chunk) { const delta = chunk.choices[0].delta; const result = { content: delta.content || void 0, isComplete: chunk.choices[0].finish_reason !== null }; if (delta.tool_calls && delta.tool_calls.length > 0) { const toolCall = delta.tool_calls[0]; if (toolCall.type === "function") { result.functionCall = { name: toolCall.function?.name, arguments: toolCall.function?.arguments }; } } return result; } /** * Send a streaming chat request to OpenAI and receive response chunks * * Similar to chat() but returns an async iterator that yields response chunks * as they arrive from OpenAI's streaming API. Useful for real-time display * of responses or handling large responses incrementally. * * @param model - Model name to use * @param context - Context object containing messages and system prompt * @param options - Optional generation parameters and tools * @returns Async generator yielding response chunks * * @throws {Error} When context is invalid * @throws {Error} When messages array is invalid * @throws {Error} When message format conversion fails * @throws {Error} When OpenAI streaming API call fails * * @see {@link ../../../apps/examples/01-basic | Basic Usage Examples} */ async *chatStream(model, context, options) { if (!context || typeof context !== "object") { core.logger.error("[OpenAIProvider] Invalid context:", context); throw new Error("Valid Context object is required"); } const { messages } = context; if (!Array.isArray(messages)) { core.logger.error("[OpenAIProvider] Invalid message array:", messages); throw new Error("Valid message array is required"); } const openaiMessages = OpenAIConversationAdapter.toOpenAIFormat(context.messages); const completionOptions = { model, messages: openaiMessages, max_tokens: options?.maxTokens || this.options.maxTokens, temperature: options?.temperature || this.options.temperature, stream: true }; if (options?.tools && Array.isArray(options.tools)) { completionOptions.tools = this.formatFunctions(options.tools); completionOptions.tool_choice = "auto"; } if (this.options.responseFormat) { if (this.options.responseFormat === "text") { completionOptions.response_format = { type: "text" }; } else if (this.options.responseFormat === "json_object") { completionOptions.response_format = { type: "json_object" }; } else if (this.options.responseFormat === "json_schema") { if (!this.options.jsonSchema) { throw new Error('jsonSchema is required when responseFormat is "json_schema"'); } completionOptions.response_format = { type: "json_schema", json_schema: this.options.jsonSchema }; } } try { await this.payloadLogger.logPayload(completionOptions, "stream"); const stream = await this.client.chat.completions.create(completionOptions); for await (const chunk of stream) { yield this.parseStreamingChunk(chunk); } } catch (error) { core.logger.error("[OpenAIProvider] Streaming API call error:", error); throw error; } } /** * Release resources and close connections * * Performs cleanup operations when the provider is no longer needed. * OpenAI client doesn't require explicit cleanup, so this is a no-op. * * @returns Promise that resolves when cleanup is complete */ async close() { } }; exports.OpenAIConversationAdapter = OpenAIConversationAdapter; exports.OpenAIProvider = OpenAIProvider; exports.PayloadLogger = PayloadLogger; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map