UNPKG

@openfiles-ai/sdk

Version:

OpenFiles SDK - AI-native file storage for your AI agents

283 lines 11.7 kB
/** * @openfiles-ai/sdk/openai * * OpenAI client with OpenFiles tool integration * Follows OpenAI 2025 best practices for tool calling with structured outputs */ import { OpenAI as OriginalOpenAI } from 'openai'; import { OpenFilesClient } from '../core'; import { OpenFilesTools } from '../tools'; import { logger } from '../utils'; /** * Drop-in replacement for OpenAI client with automatic file operations * * Simply replace your OpenAI import and add openFilesApiKey to get * automatic file operation capabilities with zero code changes. * * @example * ```typescript * // Before: import OpenAI from 'openai' * // After: import OpenAI from '@openfiles-ai/sdk/openai' * * const ai = new OpenAI({ * apiKey: 'sk_your_openai_key', // Same as before * openFilesApiKey: 'oa_your_key', // Add this * basePath: 'projects/website', // Optional: organize files * onFileOperation: (op) => console.log(`${op.action}: ${op.path}`) // Optional * }) * * // Create scoped clients for different areas * const configAI = ai.withBasePath('config') * const docsAI = ai.withBasePath('documentation') * * // Everything else works exactly the same! * const response = await configAI.chat.completions.create({ * model: 'gpt-4', * messages: [{ role: 'user', content: 'Generate app configuration file' }], * tools: [...myCustomTools] // Your tools + OpenFiles tools (auto-injected) * }) * * // File operations happen automatically, response ready to use! * console.log(response.choices[0].message.content) * ``` */ export class OpenAI extends OriginalOpenAI { artifacts; toolsInstance; config; constructor(config) { // Extract OpenAI-specific options const { openFilesApiKey, openFilesBaseUrl, basePath, ...openAIConfig } = config; super(openAIConfig); this.config = config; const clientConfig = { apiKey: openFilesApiKey, ...(openFilesBaseUrl && { baseUrl: openFilesBaseUrl }), ...(basePath && { basePath: basePath }) }; this.artifacts = new OpenFilesClient(clientConfig); this.toolsInstance = new OpenFilesTools(this.artifacts); logger.info(`OpenAI client initialized with file operations${basePath ? ` (basePath: ${basePath})` : ''}`); // Override chat.completions.create to auto-handle OpenFiles tools const originalCreate = this.chat.completions.create.bind(this.chat.completions); this.chat.completions.create = this.createEnhancedMethod(originalCreate); } /** * Create a new OpenAI client instance with a base path prefix * All file operations will automatically prefix paths with the base path * * @param basePath - The base path to prefix to all operations * @returns New OpenAI client instance with the specified base path * * @example * ```typescript * const ai = new OpenAI({ apiKey: 'sk_...', openFilesApiKey: 'oa_...' }) * const projectAI = ai.withBasePath('projects/website') * * // AI operations will create files under 'projects/website/' * const response = await projectAI.chat.completions.create({ * model: 'gpt-4', * messages: [{ role: 'user', content: 'Create config.json' }] * }) * ``` */ withBasePath(basePath) { const enhancedConfig = { ...this.config, basePath: this.config.basePath ? `${this.config.basePath}/${basePath}`.replace(/\/+/g, '/').replace(/\/+$/, '') : basePath }; return new OpenAI(enhancedConfig); } /** * Create enhanced method with proper typing for OpenAI API overloads */ createEnhancedMethod(originalCreate) { return async (params) => { return this.enhancedCreate(originalCreate, params); }; } /** * Enhanced create method that auto-handles OpenFiles tools * True drop-in replacement - user doesn't need to manage tool flow */ async enhancedCreate(originalCreate, params) { // Auto-inject OpenFiles tools alongside user's tools const enhancedParams = { ...params, parallel_tool_calls: false, // Force sequential execution for reliable file operations tools: [ ...this.toolsInstance.openai.definitions, ...(params.tools || []) ] }; // Call OpenAI API const response = await originalCreate(enhancedParams); // Check if response is streamable (has Symbol.asyncIterator) if (response && typeof response === 'object' && Symbol.asyncIterator in response) { // For streaming responses, return as-is without tool processing return response; } const chatResponse = response; // Auto-execute OpenFiles tools if present const toolMessages = await this.executeTools(chatResponse); if (toolMessages.length > 0) { // Continue conversation with tool results automatically const finalResponse = await originalCreate({ ...params, messages: [ ...params.messages, chatResponse.choices[0].message, ...toolMessages ] }); return finalResponse; } return chatResponse; } /** * Execute OpenFiles tools from a completion response * Returns tool messages that should be added to the conversation * * @param response - OpenAI completion response containing tool calls * @returns Array of tool messages to add to conversation */ async executeTools(response) { const toolMessages = []; // Track timing for the entire tool processing const startTime = Date.now(); // Use the tools layer to process the response const processed = await this.toolsInstance.openai.processToolCalls(response); const totalDuration = Date.now() - startTime; // Convert framework-agnostic results to OpenAI tool message format for (const result of processed.results) { if (result.status === 'success') { toolMessages.push({ role: 'tool', tool_call_id: result.toolCallId, content: JSON.stringify({ success: true, data: result.data, operation: result.function, message: result.data ? this.getOperationMessage(result.function, result.args || {}, result.data) : `Completed ${result.function} operation` }) }); // Trigger callbacks for successful operations let operationPath = result.data?.path || result.args?.path; // Handle special cases for operations without specific paths if (result.function === 'list_files') { const directory = result.args?.directory || '/'; operationPath = `${directory} (${result.data?.files?.length || 0} files)`; } this.config.onFileOperation?.({ action: result.function.replaceAll('_', ' '), path: operationPath, version: result.data?.version, success: true, data: result.data }); this.config.onToolExecution?.({ toolCallId: result.toolCallId, function: result.function, success: true, result: result.data, duration: totalDuration }); // Handle logging with same logic as callbacks let logPath = result.data?.path || result.args?.path; if (result.function === 'list_files') { const directory = result.args?.directory || '/'; logPath = `${directory} (${result.data?.files?.length || 0} files)`; } logger.success(result.function, logPath); } else { // Handle error results toolMessages.push({ role: 'tool', tool_call_id: result.toolCallId, content: JSON.stringify({ success: false, error: { code: 'EXECUTION_ERROR', message: result.error || 'Unknown error' }, operation: result.function }) }); this.config.onError?.(new Error(result.error || 'Unknown error')); this.config.onToolExecution?.({ toolCallId: result.toolCallId, function: result.function, success: false, error: result.error, duration: totalDuration }); logger.error(`${result.function} failed: ${result.error}`); } } return toolMessages; } /** * Generate descriptive message for tool operation result */ getOperationMessage(operation, args, result) { // Helper function to safely access path from result const getPath = (result) => { if (typeof result === 'object' && result && 'path' in result) { return result.path || ''; } return ''; }; const fileName = args.path || getPath(result) || 'file'; switch (operation) { case 'write_file': case 'overwrite_file': { const size = (typeof result === 'object' && result && 'size' in result) ? result.size : 0; return `${operation === 'write_file' ? 'Created' : 'Overwrote'} file "${fileName}" (${size || 0} bytes)`; } case 'read_file': { const content = typeof result === 'string' ? result : ''; return `Read content from "${fileName}" (${content.length || 0} characters)`; } case 'list_files': { const files = (typeof result === 'object' && result && 'files' in result) ? result.files : []; return `Listed ${files?.length || 0} files in directory`; } case 'get_file_versions': { const versions = (typeof result === 'object' && result && 'versions' in result) ? result.versions : []; return `Retrieved ${versions?.length || 0} versions for "${fileName}"`; } case 'edit_file': return `Updated file "${fileName}" with string replacement`; case 'append_to_file': return `Added content to "${fileName}"`; case 'get_file_metadata': return `Retrieved metadata for "${fileName}"`; default: return `Completed ${operation} operation`; } } /** * Get OpenFiles tool definitions for use in chat completions */ get tools() { return { definitions: this.toolsInstance.openai.definitions }; } /** * Access to the underlying OpenFiles client * For direct API calls when needed */ get openfiles() { return this.artifacts; } } // Default export for drop-in replacement export default OpenAI; //# sourceMappingURL=client.js.map