openagentic
Version:
A TypeScript framework for building AI agents with self-contained tool orchestration capabilities
1 lines • 267 kB
Source Map (JSON)
{"version":3,"sources":["../../src/tools/openai.ts","../../src/tools/utils.ts","../../src/types.ts","../../src/tools/openai-image.ts","../../src/utils/s3.ts","../../src/tools/anthropic.ts","../../src/tools/gemini.ts","../../src/tools/github.ts","../../src/tools/grok.ts","../../src/tools/llama.ts","../../src/tools/newsdata.ts","../../src/tools/perplexity.ts","../../src/tools/qrcode.ts","../../src/tools/websearch.ts","../../src/tools/elevenlabs.ts","../../src/tools/video-generation.ts","../../src/tools/gemini-tts.ts","../../src/tools/inception-labs.ts","../../src/tools/index.ts"],"sourcesContent":["import { tool } from 'ai';\nimport { z } from 'zod';\nimport { generateText } from 'ai';\nimport { createOpenAI } from '@ai-sdk/openai';\nimport type { ToolDetails } from '../types';\nimport { toOpenAgenticTool } from './utils';\n\n// Supported OpenAI models with validation\nconst SUPPORTED_MODELS = [\n 'gpt-4o',\n 'gpt-4o-mini', \n 'gpt-4-turbo',\n 'gpt-4',\n 'gpt-3.5-turbo'\n] as const;\n\nconst rawOpenAITool = tool({\n description: 'Generate high-quality text responses using OpenAI GPT models with advanced parameter control',\n parameters: z.object({\n prompt: z.string()\n .min(1)\n .max(50000)\n .describe('The text prompt to send to OpenAI (required, max 50,000 characters)'),\n \n model: z.string()\n .optional()\n .default('gpt-4o-mini')\n .describe('OpenAI model to use (gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo)'),\n \n maxTokens: z.number()\n .int()\n .min(1)\n .max(4096)\n .optional()\n .default(1000)\n .describe('Maximum number of tokens to generate (1-4096, default: 1000)'),\n \n temperature: z.number()\n .min(0)\n .max(2)\n .optional()\n .default(0.7)\n .describe('Controls randomness - lower values are more focused (0-2, default: 0.7)'),\n \n topP: z.number()\n .min(0)\n .max(1)\n .optional()\n .describe('Controls diversity via nucleus sampling (0-1, optional)'),\n \n presencePenalty: z.number()\n .min(-2)\n .max(2)\n .optional()\n .describe('Penalizes repeated tokens (-2 to 2, optional)'),\n \n frequencyPenalty: z.number()\n .min(-2)\n .max(2)\n .optional()\n .describe('Penalizes frequent tokens (-2 to 2, optional)'),\n }),\n \n execute: async ({ \n prompt, \n model = 'gpt-4o-mini',\n maxTokens = 1000,\n temperature = 0.7,\n topP,\n presencePenalty,\n frequencyPenalty\n }) => {\n // Validate API key\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error('OPENAI_API_KEY environment variable is required');\n }\n\n // Validate prompt\n if (!prompt || prompt.trim().length === 0) {\n throw new Error('Prompt cannot be empty');\n }\n\n if (prompt.length > 50000) {\n throw new Error('Prompt exceeds maximum length of 50,000 characters');\n }\n\n // Validate model\n if (!SUPPORTED_MODELS.includes(model as any)) {\n throw new Error(`Model \"${model}\" not in supported list`);\n }\n\n // Start logging\n console.log('🤖 OpenAI Tool - Generation started:', {\n model,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n topP,\n presencePenalty,\n frequencyPenalty,\n });\n\n try {\n // Initialize OpenAI client\n const openai = createOpenAI({\n apiKey,\n });\n\n // Prepare generation config\n const generateConfig: any = {\n model: openai(model),\n prompt: prompt.trim(),\n maxTokens,\n temperature,\n };\n\n // Add optional parameters only if provided\n if (topP !== undefined) {\n generateConfig.topP = topP;\n }\n if (presencePenalty !== undefined) {\n generateConfig.presencePenalty = presencePenalty;\n }\n if (frequencyPenalty !== undefined) {\n generateConfig.frequencyPenalty = frequencyPenalty;\n }\n\n // Generate text\n const { text, usage, finishReason } = await generateText(generateConfig);\n\n // Log completion\n console.log('✅ OpenAI Tool - Generation completed:', {\n model,\n tokensUsed: usage?.totalTokens || 0,\n responseLength: text.length,\n finishReason,\n });\n\n // Return structured result\n return {\n success: true,\n text,\n model,\n usage: {\n promptTokens: usage?.promptTokens || 0,\n completionTokens: usage?.completionTokens || 0,\n totalTokens: usage?.totalTokens || 0,\n },\n finishReason,\n parameters: {\n temperature,\n maxTokens,\n topP,\n presencePenalty,\n frequencyPenalty,\n },\n metadata: {\n generatedAt: new Date().toISOString(),\n promptLength: prompt.length,\n responseLength: text.length,\n },\n };\n\n } catch (error) {\n console.error('❌ OpenAI Tool - Generation failed:', {\n model,\n promptLength: prompt.length,\n error: error instanceof Error ? error.message : JSON.stringify(error),\n });\n\n // Handle specific error types\n if (error instanceof Error) {\n // Rate limiting error\n if (error.message.includes('rate limit') || error.message.includes('429')) {\n throw new Error('OpenAI API rate limit exceeded. Please try again in a moment.');\n }\n \n // Authentication error\n if (error.message.includes('401') || error.message.includes('authentication')) {\n throw new Error('OpenAI API authentication failed. Please check your API key.');\n }\n \n // Token limit error\n if (error.message.includes('token') && error.message.includes('limit')) {\n throw new Error(`Token limit exceeded. Try reducing maxTokens or prompt length.`);\n }\n \n // Invalid model error\n if (error.message.includes('model') && error.message.includes('not found')) {\n throw new Error(`Invalid model \"${model}\". Please use a supported OpenAI model.`);\n }\n \n // Network errors\n if (error.message.includes('network') || error.message.includes('timeout')) {\n throw new Error('Network error connecting to OpenAI API. Please try again.');\n }\n }\n\n // Generic error fallback\n throw new Error(`OpenAI text generation failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`);\n }\n },\n});\n\nconst toolDetails: ToolDetails = {\n toolId: 'openai_text_generation',\n name: 'OpenAI Text Generation',\n useCases: [\n 'Generate creative content and stories',\n 'Answer questions and provide explanations',\n 'Summarize text and documents',\n 'Write code and technical documentation',\n 'Translate text between languages',\n 'Proofread and edit content',\n 'Generate marketing copy and descriptions',\n 'Create blog posts and articles',\n 'Brainstorm ideas and concepts',\n 'Generate product descriptions',\n 'Write emails and communications',\n ],\n logo: 'https://www.openagentic.org/tools/openai.svg',\n};\n\nexport const openaiTool = toOpenAgenticTool(rawOpenAITool, toolDetails);","import { type Tool } from 'ai';\nimport { type OpenAgenticTool, type ToolDetails } from '../types';\n\n/**\n * Helper function to add a custom toolId to an AI SDK tool\n * This provides better identification and debugging capabilities\n * \n * @param tool - AI SDK tool created with tool() function, see ai's ToolParameters type for more details\n * @param toolId - Unique identifier for the tool\n * @returns Tool with toolId property for better identification\n */\nexport function toOpenAgenticTool(tool: Tool, details: ToolDetails): OpenAgenticTool {\n return {\n ...tool,\n ...details,\n }\n}\n","import { z } from 'zod';\nimport { type Tool } from 'ai';\n\n// =============================================================================\n// CORE TYPES FOR OPENAGENTIC FRAMEWORK\n// =============================================================================\n\nexport const AIModelSchema = z.object({\n provider: z.enum(['openai', 'anthropic', 'google', 'google-vertex', 'perplexity', 'xai', 'custom']),\n model: z.string(),\n apiKey: z.string().optional(),\n baseURL: z.string().optional(),\n temperature: z.number().min(0).max(2).optional().default(0.7),\n maxTokens: z.number().positive().optional(),\n topP: z.number().min(0).max(1).optional(),\n project: z.string().optional(),\n location: z.string().optional(),\n});\n\nexport type AIModel = z.infer<typeof AIModelSchema>;\n\nexport const MessageSchema = z.object({\n role: z.enum(['system', 'user', 'assistant', 'tool']),\n content: z.string(),\n toolCallId: z.string().optional(),\n toolCalls: z.array(z.object({\n toolCallId: z.string(),\n toolName: z.string(),\n args: z.record(z.any()),\n })).optional(),\n});\n\nexport type Message = z.infer<typeof MessageSchema>;\n\n// =============================================================================\n// API KEY MAP TYPE\n// =============================================================================\n\nexport type ApiKeyMap = Record<string, string>;\n\n// =============================================================================\n// CORE MESSAGE TYPES (AI SDK COMPATIBLE)\n// =============================================================================\n\nexport interface CoreMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string | Array<any>;\n toolCallId?: string;\n toolCalls?: Array<{\n toolCallId: string;\n toolName: string;\n args: Record<string, any>;\n }>;\n}\n\n// =============================================================================\n// LOGGING AND DEBUG TYPES\n// =============================================================================\n\nexport type LogLevel = 'none' | 'basic' | 'detailed';\n\nexport interface LoggingConfig {\n enableDebugLogging?: boolean;\n logLevel?: LogLevel;\n enableStepLogging?: boolean;\n enableToolLogging?: boolean;\n enableTimingLogging?: boolean;\n enableStatisticsLogging?: boolean;\n}\n\nexport interface ExecutionStats {\n totalDuration: number;\n stepsExecuted: number;\n toolCallsExecuted: number;\n tokensUsed?: number;\n averageStepDuration: number;\n averageToolCallDuration: number;\n}\n\nexport interface StepInfo {\n stepIndex: number;\n stepType: string;\n duration: number;\n toolCalls: string[];\n errors: string[];\n startTime: number;\n endTime: number;\n}\n\n// =============================================================================\n// OPENAGENTIC TOOL TYPES\n// =============================================================================\n\nexport type ToolDetails = {\n toolId: string;\n name: string;\n useCases: string[];\n logo: string;\n internal?: boolean;\n}\n\nexport type OpenAgenticTool = Tool & ToolDetails;\n\n// =============================================================================\n// EXECUTION RESULT TYPES\n// =============================================================================\n\nexport const ExecutionResultSchema = z.object({\n success: z.boolean(),\n result: z.any().optional(),\n error: z.string().optional(),\n messages: z.array(MessageSchema),\n iterations: z.number(),\n toolCallsUsed: z.array(z.string()),\n executionStats: z.object({\n totalDuration: z.number(),\n stepsExecuted: z.number(),\n toolCallsExecuted: z.number(),\n tokensUsed: z.number().optional(),\n averageStepDuration: z.number(),\n averageToolCallDuration: z.number(),\n }).optional(),\n});\n\nexport type ExecutionResult = z.infer<typeof ExecutionResultSchema>;\n\n// =============================================================================\n// ORCHESTRATOR TYPES\n// =============================================================================\n\n/**\n * Supported orchestrator types\n */\nexport type OrchestratorType = 'prompt-based' | 'custom-logic';\n\n/**\n * Context object passed to orchestrator execute methods\n */\nexport interface OrchestratorContext {\n model: AIModel;\n tools: OpenAgenticTool[];\n messages: Message[];\n iterations: number;\n maxIterations: number;\n loggingConfig: LoggingConfig;\n orchestratorParams?: Record<string, any>;\n}\n\n/**\n * Base interface for all orchestrators\n */\nexport interface BaseOrchestrator {\n /** Unique identifier for the orchestrator */\n id: string;\n \n /** Human-readable name */\n name: string;\n \n /** Description of what this orchestrator does */\n description: string;\n \n /** Type of orchestrator */\n type: OrchestratorType;\n \n /** Execute the orchestration logic */\n execute(input: string | CoreMessage[], context: OrchestratorContext): Promise<ExecutionResult>;\n \n /** Get the orchestrator name */\n getName(): string;\n \n /** Get the orchestrator description */\n getDescription(): string;\n \n /** Get the orchestrator type */\n getType(): OrchestratorType;\n \n /** Optional validation method for inputs */\n validate?(input: string | CoreMessage[], context: OrchestratorContext): Promise<boolean>;\n \n /** Optional initialization method */\n initialize?(context: OrchestratorContext): Promise<void>;\n \n /** Optional cleanup method */\n cleanup?(context: OrchestratorContext): Promise<void>;\n}\n\n/**\n * Orchestrator that uses system prompts and standard LLM interaction\n */\nexport interface PromptBasedOrchestrator extends BaseOrchestrator {\n type: 'prompt-based';\n \n /** System prompt used for orchestration */\n systemPrompt: string;\n \n /** Get the system prompt */\n getSystemPrompt(): string;\n \n /** Optional method to modify system prompt based on context */\n buildSystemPrompt?(context: OrchestratorContext): string;\n}\n\n/**\n * Orchestrator that uses custom logic instead of standard LLM flow\n */\nexport interface CustomLogicOrchestrator extends BaseOrchestrator {\n type: 'custom-logic';\n \n /** Custom logic function for orchestration */\n customLogic(input: string | CoreMessage[], context: OrchestratorContext): Promise<any>;\n \n /** Optional method to determine if custom logic should be used */\n shouldUseCustomLogic?(input: string | CoreMessage[], context: OrchestratorContext): boolean;\n}\n\n/**\n * Options for creating orchestrator-enabled agents\n */\nexport interface OrchestratorOptions {\n /** Orchestrator instance or ID to use */\n orchestrator?: string | BaseOrchestrator;\n \n /** Alternative parameter name for orchestrator ID */\n orchestratorId?: string;\n \n /** Parameters to pass to the orchestrator */\n orchestratorParams?: Record<string, any>;\n \n /** Whether to allow orchestrator to override system prompt */\n allowOrchestratorPromptOverride?: boolean;\n \n /** Whether to allow orchestrator to modify tool execution */\n allowOrchestratorToolControl?: boolean;\n}","import { tool } from 'ai';\nimport { z } from 'zod';\nimport type { ToolDetails } from '../types';\nimport { toOpenAgenticTool } from './utils';\nimport { uploadImageToS3, generateImageFileName } from '../utils/s3';\n\n// Supported DALL-E models with validation\nconst SUPPORTED_MODELS = [\n 'dall-e-3',\n 'dall-e-2',\n] as const;\n\n// Supported image sizes for each model\nconst MODEL_SIZES = {\n 'dall-e-3': ['1024x1024', '1024x1792', '1792x1024'],\n 'dall-e-2': ['256x256', '512x512', '1024x1024'],\n} as const;\n\nconst rawOpenAIImageTool = tool({\n description: 'Generate high-quality images using OpenAI DALL-E models with automatic S3 upload and storage',\n parameters: z.object({\n prompt: z.string()\n .min(1)\n .max(4000)\n .describe('The text prompt to generate an image from (required, max 4000 characters)'),\n \n model: z.string()\n .optional()\n .default('dall-e-3')\n .describe('The DALL-E model to use (dall-e-3, dall-e-2, default: dall-e-3)'),\n \n size: z.string()\n .optional()\n .default('1024x1024')\n .describe('The size of the image - DALL-E 3: 1024x1024, 1024x1792, 1792x1024 | DALL-E 2: 256x256, 512x512, 1024x1024'),\n \n quality: z.string()\n .optional()\n .default('standard')\n .describe('The quality of the image (standard, hd) - DALL-E 3 only, default: standard'),\n \n style: z.string()\n .optional()\n .default('vivid')\n .describe('The style of the image (vivid, natural) - DALL-E 3 only, default: vivid'),\n }),\n \n execute: async ({ \n prompt,\n model = 'dall-e-3',\n size = '1024x1024',\n quality = 'standard',\n style = 'vivid'\n }) => {\n // Validate API key\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error('OPENAI_API_KEY environment variable is required');\n }\n\n // Validate prompt\n if (!prompt || prompt.trim().length === 0) {\n throw new Error('Prompt cannot be empty');\n }\n\n if (prompt.length > 4000) {\n throw new Error('Prompt exceeds maximum length of 4000 characters');\n }\n\n // Validate model\n if (!SUPPORTED_MODELS.includes(model as any)) {\n throw new Error(`Model \"${model}\" not in supported list`);\n }\n\n // Validate size for model\n const validSizes = MODEL_SIZES[model as keyof typeof MODEL_SIZES] || MODEL_SIZES['dall-e-3'];\n if (!validSizes.includes(size as any)) {\n throw new Error(`Invalid size \"${size}\" for model \"${model}\". Supported sizes: ${validSizes.join(', ')}`);\n }\n\n // Validate quality parameter (only for DALL-E 3)\n if (quality !== 'standard' && quality !== 'hd') {\n throw new Error('Quality must be either \"standard\" or \"hd\"');\n }\n\n if (model === 'dall-e-2' && quality === 'hd') {\n console.warn('Quality parameter \"hd\" not supported for DALL-E 2, using \"standard\"');\n quality = 'standard';\n }\n\n // Validate style parameter (only for DALL-E 3)\n if (style !== 'vivid' && style !== 'natural') {\n throw new Error('Style must be either \"vivid\" or \"natural\"');\n }\n\n if (model === 'dall-e-2' && style === 'natural') {\n console.warn('Style parameter not supported for DALL-E 2, ignoring');\n }\n\n // Start logging\n console.log('🎨 OpenAI Image Generation Tool - Generation started:', {\n timestamp: new Date().toISOString(),\n prompt: prompt.substring(0, 100) + (prompt.length > 100 ? '...' : ''),\n promptLength: prompt.length,\n model,\n size,\n quality,\n style,\n });\n\n try {\n // Prepare request body for OpenAI Images API\n const requestBody: any = {\n model,\n prompt: prompt.trim(),\n size,\n response_format: 'b64_json', // Get base64 for easier handling\n n: 1, // Generate one image\n };\n\n // Add DALL-E 3 specific parameters\n if (model === 'dall-e-3') {\n requestBody.quality = quality;\n requestBody.style = style;\n }\n\n // Make direct API call to OpenAI Images endpoint\n const response = await fetch('https://api.openai.com/v1/images/generations', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(requestBody),\n });\n\n // Check if response is ok\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage = `OpenAI Images API error: ${response.status} - ${response.statusText}`;\n \n try {\n const errorJson = JSON.parse(errorText);\n if (errorJson.error && errorJson.error.message) {\n errorMessage = errorJson.error.message;\n }\n } catch {\n // Use default error message if parsing fails\n }\n\n throw new Error(errorMessage);\n }\n\n // Parse response\n let imageData: any;\n try {\n imageData = await response.json();\n } catch (error) {\n throw new Error(`Failed to parse OpenAI Images API response: ${error instanceof Error ? error.message : JSON.stringify(error)}`);\n }\n\n // Validate response structure\n if (!imageData || !imageData.data || !Array.isArray(imageData.data) || imageData.data.length === 0) {\n throw new Error('Invalid response structure from OpenAI Images API');\n }\n\n const generatedImage = imageData.data[0];\n if (!generatedImage.b64_json) {\n throw new Error('No base64 image data received from OpenAI Images API');\n }\n\n // Convert base64 to buffer\n const imageBuffer = Buffer.from(generatedImage.b64_json, 'base64');\n\n // Generate filename for S3 upload\n const fileName = generateImageFileName(prompt, 'png');\n\n // Upload to S3\n console.log('📤 Uploading generated image to S3...');\n const imageUrl = await uploadImageToS3(\n imageBuffer,\n fileName,\n 'image/png',\n `DALL-E ${model} generated image: ${prompt.substring(0, 100)}`\n );\n\n // Log completion\n console.log('✅ OpenAI Image Generation Tool - Generation completed:', {\n model,\n size,\n quality,\n style,\n imageUrl,\n fileName,\n imageSize: imageBuffer.length,\n revisedPrompt: generatedImage.revised_prompt || null,\n });\n\n // Return structured result\n return {\n success: true,\n imageUrl,\n fileName,\n model,\n size,\n quality,\n style,\n originalPrompt: prompt.trim(),\n revisedPrompt: generatedImage.revised_prompt || null,\n metadata: {\n generatedAt: new Date().toISOString(),\n promptLength: prompt.length,\n fileSize: imageBuffer.length,\n uploadedToS3: true,\n },\n };\n\n } catch (error) {\n console.error('❌ OpenAI Image Generation Tool - Generation failed:', {\n model,\n size,\n quality,\n style,\n promptLength: prompt.length,\n error: error instanceof Error ? error.message : JSON.stringify(error),\n });\n\n // Handle specific error types\n if (error instanceof Error) {\n // Rate limiting error\n if (error.message.includes('rate limit') || error.message.includes('429')) {\n throw new Error('OpenAI API rate limit exceeded. Please try again in a moment.');\n }\n \n // Authentication error\n if (error.message.includes('401') || error.message.includes('authentication')) {\n throw new Error('OpenAI API authentication failed. Please check your API key.');\n }\n \n // Invalid model error\n if (error.message.includes('model') && error.message.includes('not found')) {\n throw new Error(`Invalid model \"${model}\". Please use a supported DALL-E model.`);\n }\n \n // Content policy violation\n if (error.message.includes('content policy') || error.message.includes('safety')) {\n throw new Error('Image generation request violates OpenAI content policy. Please modify your prompt.');\n }\n \n // Prompt too long error\n if (error.message.includes('prompt') && error.message.includes('too long')) {\n throw new Error('Prompt is too long. Please reduce the prompt length and try again.');\n }\n \n // Image size errors\n if (error.message.includes('size') || error.message.includes('dimensions')) {\n throw new Error(`Invalid image size \"${size}\" for model \"${model}\". Please use a supported size.`);\n }\n \n // Quality/style parameter errors\n if (error.message.includes('quality') || error.message.includes('style')) {\n throw new Error('Invalid quality or style parameter. Please check the supported values for your model.');\n }\n \n // Network errors\n if (error.message.includes('network') || error.message.includes('timeout') || \n error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {\n throw new Error('Network error connecting to OpenAI API. Please try again.');\n }\n \n // S3 upload errors\n if (error.message.includes('S3') || error.message.includes('upload')) {\n throw new Error('Failed to upload generated image to S3. Please check your S3 configuration.');\n }\n \n // Base64 conversion errors\n if (error.message.includes('base64') || error.message.includes('buffer')) {\n throw new Error('Failed to process generated image data. Please try again.');\n }\n \n // Service availability errors\n if (error.message.includes('502') || error.message.includes('503') || error.message.includes('504')) {\n throw new Error('OpenAI service temporarily unavailable. Please try again later.');\n }\n }\n\n // Generic error fallback\n throw new Error(`OpenAI image generation failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`);\n }\n },\n});\n\nconst toolDetails: ToolDetails = {\n toolId: 'openai_image_generator',\n name: 'OpenAI Image Generator',\n useCases: [\n 'Generate photorealistic images from text descriptions',\n 'Create artistic illustrations and digital art',\n 'Design logos and brand imagery',\n 'Generate product mockups and prototypes',\n 'Create concept art for creative projects',\n 'Generate marketing visuals and advertisements',\n 'Create custom artwork for presentations',\n 'Generate book covers and poster designs',\n 'Create social media content and graphics',\n 'Generate architectural and interior design concepts',\n 'Create character designs and illustrations',\n 'Generate landscape and nature imagery',\n ],\n logo: 'https://www.openagentic.org/tools/openai.svg',\n};\n\nexport const openaiImageTool = toOpenAgenticTool(rawOpenAIImageTool, toolDetails);","import { S3Client, PutObjectCommand, type PutObjectCommandInput } from '@aws-sdk/client-s3';\n\n// =============================================================================\n// TYPES AND INTERFACES\n// =============================================================================\n\nexport interface S3Config {\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n bucketName: string;\n}\n\nexport interface UploadResult {\n success: boolean;\n url: string;\n key: string;\n bucket: string;\n contentType: string;\n size: number;\n uploadedAt: string;\n error?: string;\n}\n\nexport interface UploadOptions {\n directory?: S3Directory;\n description?: string;\n metadata?: Record<string, string>;\n cacheControl?: string;\n contentEncoding?: string;\n}\n\nexport enum FileType {\n IMAGE = 'image',\n AUDIO = 'audio',\n VIDEO = 'video',\n DOCUMENT = 'document',\n WEBSITE = 'website',\n GENERIC = 'upload'\n}\n\nconst DIRECTORY_PREFIX = 'openagentic/';\nexport enum S3Directory {\n IMAGES = DIRECTORY_PREFIX + 'images',\n AUDIO = DIRECTORY_PREFIX + 'audio', \n VIDEOS = DIRECTORY_PREFIX + 'videos',\n DOCUMENTS = DIRECTORY_PREFIX + 'documents',\n WEBSITES = DIRECTORY_PREFIX + 'websites',\n UPLOADS = DIRECTORY_PREFIX + 'uploads'\n}\n\n// =============================================================================\n// CONFIGURATION AND VALIDATION\n// =============================================================================\n\nlet s3Client: S3Client | null = null;\nlet s3Config: S3Config | null = null;\n\n/**\n * Validate S3 configuration from environment variables\n * @throws {Error} If required environment variables are missing\n */\nexport function validateS3Config(): void {\n const requiredVars = [\n 'AWS_ACCESS_KEY_ID',\n 'AWS_SECRET_ACCESS_KEY', \n 'AWS_REGION',\n 'S3_BUCKET_NAME'\n ];\n\n const missing = requiredVars.filter(varName => !process.env[varName]);\n \n if (missing.length > 0) {\n throw new Error(\n `Missing required environment variables: ${missing.join(', ')}\\n` +\n 'Please ensure all AWS S3 configuration variables are set:\\n' +\n '- AWS_ACCESS_KEY_ID\\n' +\n '- AWS_SECRET_ACCESS_KEY\\n' +\n '- AWS_REGION\\n' +\n '- S3_BUCKET_NAME'\n );\n }\n\n s3Config = {\n accessKeyId: process.env.AWS_ACCESS_KEY_ID!,\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,\n region: process.env.AWS_REGION!,\n bucketName: process.env.S3_BUCKET_NAME!,\n };\n\n // Initialize S3 client\n s3Client = new S3Client({\n region: s3Config.region,\n credentials: {\n accessKeyId: s3Config.accessKeyId,\n secretAccessKey: s3Config.secretAccessKey,\n },\n });\n}\n\n/**\n * Get S3 client instance, initializing if necessary\n * @returns {S3Client} Configured S3 client\n */\nfunction getS3Client(): S3Client {\n if (!s3Client || !s3Config) {\n validateS3Config();\n }\n return s3Client!;\n}\n\n/**\n * Get S3 configuration, validating if necessary\n * @returns {S3Config} S3 configuration object\n */\nfunction getS3Config(): S3Config {\n if (!s3Config) {\n validateS3Config();\n }\n return s3Config!;\n}\n\n// =============================================================================\n// UTILITY FUNCTIONS\n// =============================================================================\n\n/**\n * Sanitize filename by removing or replacing invalid characters\n * @param {string} input - Input filename\n * @returns {string} Sanitized filename\n */\nexport function sanitizeFilename(input: string): string {\n if (!input || typeof input !== 'string') {\n throw new Error('Filename must be a non-empty string');\n }\n\n return input\n .trim()\n // Remove or replace invalid characters\n .replace(/[<>:\"/\\\\|?*\\x00-\\x1f]/g, '')\n // Replace spaces with underscores\n .replace(/\\s+/g, '_')\n // Remove multiple consecutive underscores\n .replace(/_+/g, '_')\n // Remove leading/trailing underscores\n .replace(/^_+|_+$/g, '')\n // Limit length\n .substring(0, 200)\n // Ensure it's not empty after sanitization\n || 'unnamed_file';\n}\n\n/**\n * Generate a unique filename with timestamp and optional prefix\n * @param {string} prompt - Description or base name\n * @param {string} extension - File extension (with or without dot)\n * @param {string} prefix - Optional prefix for the filename\n * @returns {string} Generated filename\n */\nexport function generateFileName(\n prompt: string,\n extension: string,\n prefix?: string\n): string {\n if (!prompt || !extension) {\n throw new Error('Prompt and extension are required');\n }\n\n const timestamp = new Date().toISOString()\n .replace(/[:.]/g, '-')\n .replace(/T/, '_')\n .replace(/Z/, '');\n \n const sanitizedPrompt = sanitizeFilename(prompt)\n .toLowerCase()\n .substring(0, 50); // Limit prompt length\n \n const cleanExtension = extension.startsWith('.') ? extension : `.${extension}`;\n const prefixPart = prefix ? `${sanitizeFilename(prefix)}_` : '';\n \n // Generate random suffix for uniqueness\n const randomSuffix = Math.random().toString(36).substring(2, 8);\n \n return `${prefixPart}${sanitizedPrompt}_${timestamp}_${randomSuffix}${cleanExtension}`;\n}\n\n/**\n * Generate filename specifically for images\n * @param {string} prompt - Image description\n * @param {string} extension - Image extension (default: 'png')\n * @returns {string} Generated image filename\n */\nexport function generateImageFileName(prompt: string, extension = 'png'): string {\n return generateFileName(prompt, extension, 'img');\n}\n\n/**\n * Generate filename specifically for audio files\n * @param {string} prompt - Audio description\n * @param {string} extension - Audio extension (default: 'mp3')\n * @returns {string} Generated audio filename\n */\nexport function generateAudioFileName(prompt: string, extension = 'mp3'): string {\n return generateFileName(prompt, extension, 'audio');\n}\n\n/**\n * Generate filename specifically for video files\n * @param {string} prompt - Video description\n * @param {string} extension - Video extension (default: 'mp4')\n * @returns {string} Generated video filename\n */\nexport function generateVideoFileName(prompt: string, extension = 'mp4'): string {\n return generateFileName(prompt, extension, 'video');\n}\n\n/**\n * Generate filename specifically for HTML files\n * @param {string} prompt - HTML description\n * @param {string} extension - HTML extension (default: 'html')\n * @returns {string} Generated HTML filename\n */\nexport function generateHtmlFileName(prompt: string, extension = 'html'): string {\n return generateFileName(prompt, extension, 'page');\n}\n\n/**\n * Determine content type from file extension\n * @param {string} extension - File extension\n * @returns {string} MIME content type\n */\nexport function getContentTypeFromExtension(extension: string): string {\n const cleanExt = extension.toLowerCase().replace('.', '');\n \n const contentTypes: Record<string, string> = {\n // Images\n 'jpg': 'image/jpeg',\n 'jpeg': 'image/jpeg',\n 'png': 'image/png',\n 'gif': 'image/gif',\n 'webp': 'image/webp',\n 'svg': 'image/svg+xml',\n 'bmp': 'image/bmp',\n 'ico': 'image/x-icon',\n \n // Audio\n 'mp3': 'audio/mpeg',\n 'wav': 'audio/wav',\n 'ogg': 'audio/ogg',\n 'flac': 'audio/flac',\n 'aac': 'audio/aac',\n 'm4a': 'audio/mp4',\n \n // Video\n 'mp4': 'video/mp4',\n 'avi': 'video/x-msvideo',\n 'mov': 'video/quicktime',\n 'wmv': 'video/x-ms-wmv',\n 'flv': 'video/x-flv',\n 'webm': 'video/webm',\n 'mkv': 'video/x-matroska',\n \n // Documents\n 'pdf': 'application/pdf',\n 'doc': 'application/msword',\n 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'xls': 'application/vnd.ms-excel',\n 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'ppt': 'application/vnd.ms-powerpoint',\n 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'txt': 'text/plain',\n 'rtf': 'application/rtf',\n \n // Web\n 'html': 'text/html',\n 'htm': 'text/html',\n 'css': 'text/css',\n 'js': 'application/javascript',\n 'json': 'application/json',\n 'xml': 'application/xml',\n \n // Archives\n 'zip': 'application/zip',\n 'tar': 'application/x-tar',\n 'gz': 'application/gzip',\n 'rar': 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n \n // Other\n 'bin': 'application/octet-stream',\n };\n \n return contentTypes[cleanExt] || 'application/octet-stream';\n}\n\n/**\n * Validate file size against reasonable limits\n * @param {number} size - File size in bytes\n * @param {FileType} fileType - Type of file being uploaded\n * @throws {Error} If file size exceeds limits\n */\nfunction validateFileSize(size: number, fileType: FileType): void {\n const sizeLimits: Record<FileType, number> = {\n [FileType.IMAGE]: 50 * 1024 * 1024, // 50MB\n [FileType.AUDIO]: 100 * 1024 * 1024, // 100MB\n [FileType.VIDEO]: 500 * 1024 * 1024, // 500MB\n [FileType.DOCUMENT]: 25 * 1024 * 1024, // 25MB\n [FileType.WEBSITE]: 10 * 1024 * 1024, // 10MB\n [FileType.GENERIC]: 100 * 1024 * 1024, // 100MB\n };\n\n const limit = sizeLimits[fileType];\n if (size > limit) {\n const limitMB = Math.round(limit / (1024 * 1024));\n const sizeMB = Math.round(size / (1024 * 1024));\n throw new Error(\n `File size (${sizeMB}MB) exceeds limit for ${fileType} files (${limitMB}MB)`\n );\n }\n}\n\n// =============================================================================\n// CORE UPLOAD FUNCTIONS\n// =============================================================================\n\n/**\n * Generic file upload function to S3\n * @param {Buffer} buffer - File buffer\n * @param {string} fileName - Name of the file\n * @param {string} contentType - MIME content type\n * @param {string} directory - S3 directory/folder\n * @param {string} description - Optional description for metadata\n * @returns {Promise<string>} Public URL of uploaded file\n */\nexport async function uploadFileToS3(\n buffer: Buffer,\n fileName: string,\n contentType: string,\n directory = S3Directory.UPLOADS,\n description?: string\n): Promise<string> {\n try {\n // Validate inputs\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n throw new Error('Invalid or empty buffer provided');\n }\n\n if (!fileName || typeof fileName !== 'string') {\n throw new Error('Valid filename is required');\n }\n\n if (!contentType || typeof contentType !== 'string') {\n throw new Error('Valid content type is required');\n }\n\n // Validate file size (generic limit)\n validateFileSize(buffer.length, FileType.GENERIC);\n\n // Sanitize filename\n const sanitizedFileName = sanitizeFilename(fileName);\n \n // Construct S3 key (path)\n const key = `${directory}/${sanitizedFileName}`;\n \n // Get S3 configuration\n const config = getS3Config();\n const client = getS3Client();\n\n // Prepare upload parameters\n const uploadParams: PutObjectCommandInput = {\n Bucket: config.bucketName,\n Key: key,\n Body: buffer,\n ContentType: contentType,\n CacheControl: 'public, max-age=31536000', // 1 year cache\n Metadata: {\n 'upload-timestamp': new Date().toISOString(),\n 'original-name': fileName,\n 'file-size': buffer.length.toString(),\n ...(description && { description }),\n },\n };\n\n // Execute upload\n console.log(`Uploading file to S3: ${key} (${buffer.length} bytes)`);\n const command = new PutObjectCommand(uploadParams);\n await client.send(command);\n\n // Construct public URL\n const publicUrl = `https://${config.bucketName}.s3.${config.region}.amazonaws.com/${key}`;\n \n console.log(`✅ File uploaded successfully: ${publicUrl}`);\n return publicUrl;\n\n } catch (error) {\n console.error('❌ S3 upload failed:', error);\n throw new Error(\n `S3 upload failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n/**\n * Upload image to S3 with image-specific handling\n * @param {Buffer} imageBuffer - Image file buffer\n * @param {string} fileName - Image filename\n * @param {string} contentType - Image content type (auto-detected if not provided)\n * @param {string} description - Optional description\n * @returns {Promise<string>} Public URL of uploaded image\n */\nexport async function uploadImageToS3(\n imageBuffer: Buffer,\n fileName: string,\n contentType?: string,\n description?: string\n): Promise<string> {\n try {\n // Validate image buffer\n if (!Buffer.isBuffer(imageBuffer) || imageBuffer.length === 0) {\n throw new Error('Invalid or empty image buffer');\n }\n\n // Validate file size for images\n validateFileSize(imageBuffer.length, FileType.IMAGE);\n\n // Auto-detect content type if not provided\n let finalContentType = contentType;\n if (!finalContentType) {\n const extension = fileName.split('.').pop()?.toLowerCase() || '';\n finalContentType = getContentTypeFromExtension(extension);\n \n // Ensure it's an image content type\n if (!finalContentType.startsWith('image/')) {\n finalContentType = 'image/jpeg'; // Default fallback\n }\n }\n\n console.log(`Uploading image: ${fileName} (${imageBuffer.length} bytes)`);\n \n return await uploadFileToS3(\n imageBuffer,\n fileName,\n finalContentType,\n S3Directory.IMAGES,\n description || 'Image upload'\n );\n\n } catch (error) {\n console.error('❌ Image upload failed:', error);\n throw new Error(\n `Image upload failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n/**\n * Upload audio file to S3 with audio-specific handling\n * @param {Buffer} audioBuffer - Audio file buffer\n * @param {string} fileName - Audio filename\n * @param {string} contentType - Audio content type (auto-detected if not provided)\n * @param {string} description - Optional description\n * @returns {Promise<string>} Public URL of uploaded audio\n */\nexport async function uploadAudioToS3(\n audioBuffer: Buffer,\n fileName: string,\n contentType?: string,\n description?: string\n): Promise<string> {\n try {\n // Validate audio buffer\n if (!Buffer.isBuffer(audioBuffer) || audioBuffer.length === 0) {\n throw new Error('Invalid or empty audio buffer');\n }\n\n // Validate file size for audio\n validateFileSize(audioBuffer.length, FileType.AUDIO);\n\n // Auto-detect content type if not provided\n let finalContentType = contentType;\n if (!finalContentType) {\n const extension = fileName.split('.').pop()?.toLowerCase() || '';\n finalContentType = getContentTypeFromExtension(extension);\n \n // Ensure it's an audio content type\n if (!finalContentType.startsWith('audio/')) {\n finalContentType = 'audio/mpeg'; // Default fallback\n }\n }\n\n console.log(`Uploading audio: ${fileName} (${audioBuffer.length} bytes)`);\n \n return await uploadFileToS3(\n audioBuffer,\n fileName,\n finalContentType,\n S3Directory.AUDIO,\n description || 'Audio upload'\n );\n\n } catch (error) {\n console.error('❌ Audio upload failed:', error);\n throw new Error(\n `Audio upload failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n/**\n * Upload video file to S3 with video-specific handling\n * @param {Buffer} videoBuffer - Video file buffer\n * @param {string} fileName - Video filename\n * @param {string} contentType - Video content type (auto-detected if not provided)\n * @param {string} description - Optional description\n * @returns {Promise<string>} Public URL of uploaded video\n */\nexport async function uploadVideoToS3(\n videoBuffer: Buffer,\n fileName: string,\n contentType?: string,\n description?: string\n): Promise<string> {\n try {\n // Validate video buffer\n if (!Buffer.isBuffer(videoBuffer) || videoBuffer.length === 0) {\n throw new Error('Invalid or empty video buffer');\n }\n\n // Validate file size for videos\n validateFileSize(videoBuffer.length, FileType.VIDEO);\n\n // Auto-detect content type if not provided\n let finalContentType = contentType;\n if (!finalContentType) {\n const extension = fileName.split('.').pop()?.toLowerCase() || '';\n finalContentType = getContentTypeFromExtension(extension);\n \n // Ensure it's a video content type\n if (!finalContentType.startsWith('video/')) {\n finalContentType = 'video/mp4'; // Default fallback\n }\n }\n\n console.log(`Uploading video: ${fileName} (${videoBuffer.length} bytes)`);\n \n return await uploadFileToS3(\n videoBuffer,\n fileName,\n finalContentType,\n S3Directory.VIDEOS,\n description || 'Video upload'\n );\n\n } catch (error) {\n console.error('❌ Video upload failed:', error);\n throw new Error(\n `Video upload failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n/**\n * Upload HTML content to S3 as a website file\n * @param {string} htmlContent - HTML content as string\n * @param {string} fileName - HTML filename\n * @param {string} contentType - Content type (default: 'text/html')\n * @param {string} description - Optional description\n * @returns {Promise<string>} Public URL of uploaded HTML\n */\nexport async function uploadHtmlToS3(\n htmlContent: string,\n fileName: string,\n contentType = 'text/html',\n description?: string\n): Promise<string> {\n try {\n // Validate HTML content\n if (!htmlContent || typeof htmlContent !== 'string') {\n throw new Error('Valid HTML content is required');\n }\n\n if (htmlContent.trim().length === 0) {\n throw new Error('HTML content cannot be empty');\n }\n\n // Convert string to buffer\n const htmlBuffer = Buffer.from(htmlContent, 'utf-8');\n\n // Validate file size for HTML\n validateFileSize(htmlBuffer.length, FileType.WEBSITE);\n\n // Ensure proper HTML extension\n let finalFileName = fileName;\n if (!fileName.toLowerCase().endsWith('.html') && !fileName.toLowerCase().endsWith('.htm')) {\n finalFileName = `${fileName}.html`;\n }\n\n console.log(`Uploading HTML: ${finalFileName} (${htmlBuffer.length} bytes)`);\n \n return await uploadFileToS3(\n htmlBuffer,\n finalFileName,\n contentType,\n S3Directory.WEBSITES,\n description || 'HTML website upload'\n );\n\n } catch (error) {\n console.error('❌ HTML upload failed:', error);\n throw new Error(\n `HTML upload failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n// =============================================================================\n// ADVANCED UPLOAD FUNCTIONS\n// =============================================================================\n\n/**\n * Upload file with detailed result information\n * @param {Buffer} buffer - File buffer\n * @param {string} fileName - Filename\n * @param {UploadOptions} options - Upload options\n * @returns {Promise<UploadResult>} Detailed upload result\n */\nexport async function uploadFileWithDetails(\n buffer: Buffer,\n fileName: string,\n options: UploadOptions = {}\n): Promise<UploadResult> {\n const startTime = Date.now();\n \n try {\n // Auto-detect content type\n const extension = fileName.split('.').pop()?.toLowerCase() || '';\n const contentType = getContentTypeFromExtension(extension);\n \n // Determine directory\n const directory = options.directory || S3Directory.UPLOADS;\n \n // Upload file\n const url = await uploadFileToS3(\n buffer,\n fileName,\n contentType,\n directory,\n options.description\n );\n \n // Extract key from URL\n const config = getS3Config();\n const key = url.replace(`https://${config.bucketName}.s3.${config.region}.amazonaws.com/`, '');\n \n const result: UploadResult = {\n success: true,\n url,\n key,\n bucket: config.bucketName,\n contentType,\n size: buffer.length,\n uploadedAt: new Date().toISOString(),\n };\n \n console.log(`✅ Upload completed in ${Date.now() - startTime}ms`);\n return result;\n \n } catch (error) {\n const result: UploadResult = {\n success: false,\n url: '',\n key: '',\n bucket: '',\n contentType: '',\n size: 0,\n uploadedAt: new Date().toISOString(),\n error: error instanceof Error ? error.message : String(error),\n };\n \n console.error(`❌ Upload failed after ${Date.now() - startTime}ms:`, error);\n return result;\n }\n}\n\n/**\n * Batch upload multiple files\n * @param {Array<{buffer: Buffer, fileName: string, options?: UploadOptions}>} files - Files to upload\n * @returns {Promise<UploadResult[]>} Array of upload results\n */\nexport async function batchUploadToS3(\n files: Array<{\n buffer: Buffer;\n fileName: string;\n options?: UploadOptions;\n }>\n): Promise<UploadResult[]> {\n console.log(`Starting batch upload of ${files.length} files...`);\n \n const results = await Promise.allSettled(\n files.map(async (file, index) => {\n console.log(`Uploading file ${index + 1}/${files.length}: ${file.fileName}`);\n return uploadFileWithDetails(file.buffer, file.fileName, file.options);\n })\n );\n \n const uploadResults = results.map((result, index) => {\n if (result.status === 'fulfilled') {\n return result.value;\n } else {\n return {\n success: false,\n url: '',\n key: '',\n bucket: '',\n contentType: '',\n size: 0,\n uploadedAt: new Date().toISOString(),\n error: `Batch upload failed for file ${index}: ${result.reason}`,\n } as UploadResult;\n }\n });\n \n const successful = uploadResults.filter(r => r.success).length;\n const failed = uploadResults.length - successful;\n \n console.log(`✅ Batch upload completed: ${successful} successful, ${failed} failed`);\n \n return uploadResults;\n}\n\n// =============================================================================\n// INITIALIZATION AND HEALTH CHECK\n// =============================================================================\n\n/**\n * Initialize S3 utility and verify configuration\n * @returns {Promise<boolean>} True if initialization successful\n */\nexport async function initializeS3(): Promise<boolean> {\n try {\n console.log('🔧 Initializing S3 utility...');\n \n // Validate configuration\n validateS3Config();\n \n console.log('✅ S3 utility initialized successfully');\n console.log(`📦 Bucket: ${s3Config!.bucketName}`);\n console.log(`🌍 Region: ${s3Config!.region}`);\n \n return true;\n } catch (error) {\n console.error('❌ S3 initialization failed:', error);\n return false;\n }\n}\n\n/**\n * Test S3 connection by attempting a simple operation\n * @returns {Promise<boolean>} True if connection test successful\n */\nexport async function testS3Connection(): Promise<boolean> {\n try {\n const testContent = 'S3 connection test';\n const testFileName = `test-connection-${Date.now()}.txt`;\n \n await uploadFileToS3(\n Buffer.from(testContent),\n testFileName,\n 'text/plain',\n S3D