Files
cim_summary/backend/src/config/env.ts
Jon df079713c4 feat: Complete cloud-native CIM Document Processor with full BPCP template
🌐 Cloud-Native Architecture:
- Firebase Functions deployment (no Docker)
- Supabase database (replacing local PostgreSQL)
- Google Cloud Storage integration
- Document AI + Agentic RAG processing pipeline
- Claude-3.5-Sonnet LLM integration

 Full BPCP CIM Review Template (7 sections):
- Deal Overview
- Business Description
- Market & Industry Analysis
- Financial Summary (with historical financials table)
- Management Team Overview
- Preliminary Investment Thesis
- Key Questions & Next Steps

🔧 Cloud Migration Improvements:
- PostgreSQL → Supabase migration complete
- Local storage → Google Cloud Storage
- Docker deployment → Firebase Functions
- Schema mapping fixes (camelCase/snake_case)
- Enhanced error handling and logging
- Vector database with fallback mechanisms

📄 Complete End-to-End Cloud Workflow:
1. Upload PDF → Document AI extraction
2. Agentic RAG processing → Structured CIM data
3. Store in Supabase → Vector embeddings
4. Auto-generate PDF → Full BPCP template
5. Download complete CIM review

🚀 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-01 17:51:45 -04:00

386 lines
15 KiB
TypeScript

import dotenv from 'dotenv';
import Joi from 'joi';
// Load environment variables
dotenv.config();
// Environment validation schema
const envSchema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(5000),
// Firebase Configuration (Required for file storage and auth)
FB_PROJECT_ID: Joi.string().when('NODE_ENV', {
is: 'production',
then: Joi.string().required(),
otherwise: Joi.string().optional()
}),
FB_STORAGE_BUCKET: Joi.string().when('NODE_ENV', {
is: 'production',
then: Joi.string().required(),
otherwise: Joi.string().optional()
}),
FB_API_KEY: Joi.string().optional(),
FB_AUTH_DOMAIN: Joi.string().optional(),
// Supabase Configuration (Required for cloud-only architecture)
SUPABASE_URL: Joi.string().when('NODE_ENV', {
is: 'production',
then: Joi.string().required(),
otherwise: Joi.string().optional()
}),
SUPABASE_ANON_KEY: Joi.string().when('NODE_ENV', {
is: 'production',
then: Joi.string().required(),
otherwise: Joi.string().optional()
}),
SUPABASE_SERVICE_KEY: Joi.string().when('NODE_ENV', {
is: 'production',
then: Joi.string().required(),
otherwise: Joi.string().optional()
}),
// Google Cloud Configuration (Required)
GCLOUD_PROJECT_ID: Joi.string().required(),
DOCUMENT_AI_LOCATION: Joi.string().default('us'),
DOCUMENT_AI_PROCESSOR_ID: Joi.string().required(),
GCS_BUCKET_NAME: Joi.string().required(),
DOCUMENT_AI_OUTPUT_BUCKET_NAME: Joi.string().required(),
GOOGLE_APPLICATION_CREDENTIALS: Joi.string().default('./serviceAccountKey.json'),
// Vector Database Configuration
VECTOR_PROVIDER: Joi.string().valid('supabase', 'pinecone').default('supabase'),
// Pinecone Configuration (optional, only if using Pinecone)
PINECONE_API_KEY: Joi.string().when('VECTOR_PROVIDER', {
is: 'pinecone',
then: Joi.string().required(),
otherwise: Joi.string().allow('').optional()
}),
PINECONE_INDEX: Joi.string().when('VECTOR_PROVIDER', {
is: 'pinecone',
then: Joi.string().required(),
otherwise: Joi.string().allow('').optional()
}),
// JWT - Optional for Firebase Auth
JWT_SECRET: Joi.string().default('default-jwt-secret-change-in-production'),
JWT_EXPIRES_IN: Joi.string().default('1h'),
JWT_REFRESH_SECRET: Joi.string().default('default-refresh-secret-change-in-production'),
JWT_REFRESH_EXPIRES_IN: Joi.string().default('7d'),
// File Upload Configuration (Cloud-only)
MAX_FILE_SIZE: Joi.number().default(104857600), // 100MB
ALLOWED_FILE_TYPES: Joi.string().default('application/pdf'),
// LLM
LLM_PROVIDER: Joi.string().valid('openai', 'anthropic').default('openai'),
OPENAI_API_KEY: Joi.string().when('LLM_PROVIDER', {
is: 'openai',
then: Joi.string().required(),
otherwise: Joi.string().allow('').optional()
}),
ANTHROPIC_API_KEY: Joi.string().when('LLM_PROVIDER', {
is: 'anthropic',
then: Joi.string().required(),
otherwise: Joi.string().allow('').optional()
}),
LLM_MODEL: Joi.string().default('gpt-4'),
LLM_MAX_TOKENS: Joi.number().default(3500),
LLM_TEMPERATURE: Joi.number().min(0).max(2).default(0.1),
LLM_PROMPT_BUFFER: Joi.number().default(500),
// Security
BCRYPT_ROUNDS: Joi.number().default(12),
RATE_LIMIT_WINDOW_MS: Joi.number().default(900000), // 15 minutes
RATE_LIMIT_MAX_REQUESTS: Joi.number().default(100),
// Logging
LOG_LEVEL: Joi.string().valid('error', 'warn', 'info', 'debug').default('info'),
LOG_FILE: Joi.string().default('logs/app.log'),
// Processing Strategy
PROCESSING_STRATEGY: Joi.string().valid('document_ai_agentic_rag').default('document_ai_agentic_rag'),
// Agentic RAG Configuration
AGENTIC_RAG_ENABLED: Joi.boolean().default(false),
AGENTIC_RAG_MAX_AGENTS: Joi.number().default(6),
AGENTIC_RAG_PARALLEL_PROCESSING: Joi.boolean().default(true),
AGENTIC_RAG_VALIDATION_STRICT: Joi.boolean().default(true),
AGENTIC_RAG_RETRY_ATTEMPTS: Joi.number().default(3),
AGENTIC_RAG_TIMEOUT_PER_AGENT: Joi.number().default(60000),
// Agent-Specific Configuration
AGENT_DOCUMENT_UNDERSTANDING_ENABLED: Joi.boolean().default(true),
AGENT_FINANCIAL_ANALYSIS_ENABLED: Joi.boolean().default(true),
AGENT_MARKET_ANALYSIS_ENABLED: Joi.boolean().default(true),
AGENT_INVESTMENT_THESIS_ENABLED: Joi.boolean().default(true),
AGENT_SYNTHESIS_ENABLED: Joi.boolean().default(true),
AGENT_VALIDATION_ENABLED: Joi.boolean().default(true),
// Quality Control
AGENTIC_RAG_QUALITY_THRESHOLD: Joi.number().min(0).max(1).default(0.8),
AGENTIC_RAG_COMPLETENESS_THRESHOLD: Joi.number().min(0).max(1).default(0.9),
AGENTIC_RAG_CONSISTENCY_CHECK: Joi.boolean().default(true),
// Monitoring and Logging
AGENTIC_RAG_DETAILED_LOGGING: Joi.boolean().default(true),
AGENTIC_RAG_PERFORMANCE_TRACKING: Joi.boolean().default(true),
AGENTIC_RAG_ERROR_REPORTING: Joi.boolean().default(true),
}).unknown();
// Validate environment variables
const { error, value: envVars } = envSchema.validate(process.env);
// Enhanced error handling for serverless environments
if (error) {
const isProduction = process.env.NODE_ENV === 'production';
const isCriticalError = error.details.some(detail =>
detail.path.includes('SUPABASE_URL') ||
detail.path.includes('FB_PROJECT_ID') ||
detail.path.includes('ANTHROPIC_API_KEY') ||
detail.path.includes('GCLOUD_PROJECT_ID')
);
if (isProduction && isCriticalError) {
console.error(`[Config Validation Error] Critical configuration missing in production:`, error.message);
// In production, we still log but don't crash immediately to allow for runtime injection
console.error('Application may not function correctly without these variables');
} else {
console.warn(`[Config Validation Warning] ${error.message}`);
}
}
// Runtime configuration validation function
export const validateRuntimeConfig = (): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
// Check critical Firebase configuration
if (!config.firebase.projectId) {
errors.push('Firebase Project ID is missing');
}
// Check critical Supabase configuration
if (!config.supabase.url) {
errors.push('Supabase URL is missing');
}
// Check LLM configuration
if (config.llm.provider === 'anthropic' && !config.llm.anthropicApiKey) {
errors.push('Anthropic API key is missing but provider is set to anthropic');
}
if (config.llm.provider === 'openai' && !config.llm.openaiApiKey) {
errors.push('OpenAI API key is missing but provider is set to openai');
}
// Check Google Cloud configuration
if (!config.googleCloud.projectId) {
errors.push('Google Cloud Project ID is missing');
}
return {
isValid: errors.length === 0,
errors
};
};
// Export validated configuration
export const config = {
env: envVars.NODE_ENV,
nodeEnv: envVars.NODE_ENV,
port: envVars.PORT,
frontendUrl: process.env['FRONTEND_URL'] || 'http://localhost:3000',
// Firebase Configuration
firebase: {
projectId: envVars.FB_PROJECT_ID,
storageBucket: envVars.FB_STORAGE_BUCKET,
apiKey: envVars.FB_API_KEY,
authDomain: envVars.FB_AUTH_DOMAIN,
},
supabase: {
url: envVars.SUPABASE_URL,
anonKey: envVars.SUPABASE_ANON_KEY,
serviceKey: envVars.SUPABASE_SERVICE_KEY,
},
// Google Cloud Configuration
googleCloud: {
projectId: envVars.GCLOUD_PROJECT_ID,
documentAiLocation: envVars.DOCUMENT_AI_LOCATION,
documentAiProcessorId: envVars.DOCUMENT_AI_PROCESSOR_ID,
gcsBucketName: envVars.GCS_BUCKET_NAME,
documentAiOutputBucketName: envVars.DOCUMENT_AI_OUTPUT_BUCKET_NAME,
applicationCredentials: envVars.GOOGLE_APPLICATION_CREDENTIALS,
},
jwt: {
secret: envVars.JWT_SECRET,
expiresIn: envVars.JWT_EXPIRES_IN,
refreshSecret: envVars.JWT_REFRESH_SECRET,
refreshExpiresIn: envVars.JWT_REFRESH_EXPIRES_IN,
},
upload: {
maxFileSize: envVars.MAX_FILE_SIZE,
allowedFileTypes: envVars.ALLOWED_FILE_TYPES.split(','),
// Cloud-only: No local upload directory needed
uploadDir: '/tmp/uploads', // Temporary directory for file processing
},
llm: {
provider: envVars['LLM_PROVIDER'] || 'anthropic', // Default to Claude for cost efficiency
// Anthropic Configuration (Primary)
anthropicApiKey: envVars['ANTHROPIC_API_KEY'],
// OpenAI Configuration (Fallback)
openaiApiKey: envVars['OPENAI_API_KEY'],
// Model Selection - Hybrid approach optimized for different tasks
model: envVars['LLM_MODEL'] || 'claude-3-7-sonnet-20250219', // Primary model for analysis
fastModel: envVars['LLM_FAST_MODEL'] || 'claude-3-5-haiku-20241022', // Fast model for cost optimization
fallbackModel: envVars['LLM_FALLBACK_MODEL'] || 'gpt-4.5-preview-2025-02-27', // Fallback for creativity
// Task-specific model selection
financialModel: envVars['LLM_FINANCIAL_MODEL'] || 'claude-3-7-sonnet-20250219', // Best for financial analysis
creativeModel: envVars['LLM_CREATIVE_MODEL'] || 'gpt-4.5-preview-2025-02-27', // Best for creative content
reasoningModel: envVars['LLM_REASONING_MODEL'] || 'claude-3-7-sonnet-20250219', // Best for complex reasoning
// Token Limits - Optimized for CIM documents with hierarchical processing
maxTokens: parseInt(envVars['LLM_MAX_TOKENS'] || '4000'), // Output tokens (increased for better analysis)
maxInputTokens: parseInt(envVars['LLM_MAX_INPUT_TOKENS'] || '200000'), // Input tokens (increased for larger context)
chunkSize: parseInt(envVars['LLM_CHUNK_SIZE'] || '15000'), // Chunk size for section analysis (increased from 4000)
promptBuffer: parseInt(envVars['LLM_PROMPT_BUFFER'] || '1000'), // Buffer for prompt tokens (increased)
// Processing Configuration
temperature: parseFloat(envVars['LLM_TEMPERATURE'] || '0.1'), // Low temperature for consistent output
timeoutMs: parseInt(envVars['LLM_TIMEOUT_MS'] || '180000'), // 3 minutes timeout (increased for complex analysis)
// Cost Optimization
enableCostOptimization: envVars['LLM_ENABLE_COST_OPTIMIZATION'] === 'true',
maxCostPerDocument: parseFloat(envVars['LLM_MAX_COST_PER_DOCUMENT'] || '3.00'), // Max $3 per document (increased for better quality)
useFastModelForSimpleTasks: envVars['LLM_USE_FAST_MODEL_FOR_SIMPLE_TASKS'] === 'true',
// Hybrid approach settings
enableHybridApproach: envVars['LLM_ENABLE_HYBRID_APPROACH'] === 'true',
useClaudeForFinancial: envVars['LLM_USE_CLAUDE_FOR_FINANCIAL'] === 'true',
useGPTForCreative: envVars['LLM_USE_GPT_FOR_CREATIVE'] === 'true',
},
security: {
bcryptRounds: envVars.BCRYPT_ROUNDS,
rateLimit: {
windowMs: envVars.RATE_LIMIT_WINDOW_MS,
maxRequests: envVars.RATE_LIMIT_MAX_REQUESTS,
},
},
logging: {
level: envVars.LOG_LEVEL,
file: envVars.LOG_FILE,
},
// Processing Strategy
processingStrategy: envVars['PROCESSING_STRATEGY'] || 'agentic_rag', // 'chunking' | 'rag' | 'agentic_rag'
enableRAGProcessing: envVars['ENABLE_RAG_PROCESSING'] === 'true',
enableProcessingComparison: envVars['ENABLE_PROCESSING_COMPARISON'] === 'true',
// Agentic RAG Configuration
agenticRag: {
enabled: envVars.AGENTIC_RAG_ENABLED,
maxAgents: parseInt(envVars.AGENTIC_RAG_MAX_AGENTS || '6'),
parallelProcessing: envVars.AGENTIC_RAG_PARALLEL_PROCESSING,
validationStrict: envVars.AGENTIC_RAG_VALIDATION_STRICT,
retryAttempts: parseInt(envVars.AGENTIC_RAG_RETRY_ATTEMPTS || '3'),
timeoutPerAgent: parseInt(envVars.AGENTIC_RAG_TIMEOUT_PER_AGENT || '60000'),
},
// Agent-Specific Configuration
agentSpecific: {
documentUnderstandingEnabled: envVars['AGENT_DOCUMENT_UNDERSTANDING_ENABLED'] === 'true',
financialAnalysisEnabled: envVars['AGENT_FINANCIAL_ANALYSIS_ENABLED'] === 'true',
marketAnalysisEnabled: envVars['AGENT_MARKET_ANALYSIS_ENABLED'] === 'true',
investmentThesisEnabled: envVars['AGENT_INVESTMENT_THESIS_ENABLED'] === 'true',
synthesisEnabled: envVars['AGENT_SYNTHESIS_ENABLED'] === 'true',
validationEnabled: envVars['AGENT_VALIDATION_ENABLED'] === 'true',
},
// Quality Control
qualityControl: {
qualityThreshold: parseFloat(envVars['AGENTIC_RAG_QUALITY_THRESHOLD'] || '0.8'),
completenessThreshold: parseFloat(envVars['AGENTIC_RAG_COMPLETENESS_THRESHOLD'] || '0.9'),
consistencyCheck: envVars['AGENTIC_RAG_CONSISTENCY_CHECK'] === 'true',
},
// Monitoring and Logging
monitoringAndLogging: {
detailedLogging: envVars['AGENTIC_RAG_DETAILED_LOGGING'] === 'true',
performanceTracking: envVars['AGENTIC_RAG_PERFORMANCE_TRACKING'] === 'true',
errorReporting: envVars['AGENTIC_RAG_ERROR_REPORTING'] === 'true',
},
// Vector Database Configuration (Cloud-only)
vector: {
provider: envVars['VECTOR_PROVIDER'] || 'supabase', // 'pinecone' | 'supabase'
// Pinecone Configuration (if used)
pineconeApiKey: envVars['PINECONE_API_KEY'],
pineconeIndex: envVars['PINECONE_INDEX'],
},
// Legacy database configuration (for compatibility - using Supabase)
database: {
url: envVars.SUPABASE_URL,
host: 'db.supabase.co',
port: 5432,
name: 'postgres',
user: 'postgres',
password: envVars.SUPABASE_SERVICE_KEY,
},
// Legacy Redis configuration (for compatibility - using in-memory or cloud Redis)
redis: {
url: process.env['REDIS_URL'] || 'redis://localhost:6379',
host: 'localhost',
port: 6379,
},
};
// Configuration health check function
export const getConfigHealth = () => {
const runtimeValidation = validateRuntimeConfig();
return {
timestamp: new Date().toISOString(),
environment: config.nodeEnv,
configurationValid: runtimeValidation.isValid,
errors: runtimeValidation.errors,
services: {
firebase: {
configured: !!config.firebase.projectId && !!config.firebase.storageBucket,
projectId: config.firebase.projectId ? 'configured' : 'missing',
storageBucket: config.firebase.storageBucket ? 'configured' : 'missing'
},
supabase: {
configured: !!config.supabase.url && !!config.supabase.serviceKey,
url: config.supabase.url ? 'configured' : 'missing',
serviceKey: config.supabase.serviceKey ? 'configured' : 'missing'
},
googleCloud: {
configured: !!config.googleCloud.projectId && !!config.googleCloud.documentAiProcessorId,
projectId: config.googleCloud.projectId ? 'configured' : 'missing',
documentAiProcessorId: config.googleCloud.documentAiProcessorId ? 'configured' : 'missing'
},
llm: {
configured: config.llm.provider === 'anthropic' ? !!config.llm.anthropicApiKey : !!config.llm.openaiApiKey,
provider: config.llm.provider,
apiKey: (config.llm.provider === 'anthropic' ? config.llm.anthropicApiKey : config.llm.openaiApiKey) ? 'configured' : 'missing'
}
}
};
};
export default config;