{"repo": "akullganesh19/justiceai", "files": {".env": {"ea84ce1aa9a4add4eace26cb4d0f97059bbf9f00": {"t": 1785745098.5693457, "n": 1169, "h": "d4a11f8b2035edb8", "c": "# JusticeAI Environment Configuration\n# Copy this file to .env.local and update with your values\n\n# Server Configuration\nPORT=3001\nNODE_ENV=development\n\n# Ollama Configuration\nOLLAMA_BASE_URL=http://localhost:11434\nEMBEDDING_MODEL=nomic-embed-text\nCHAT_MODEL=gemma3:4b\n\n# Frontend Configuration\nVITE_API_URL=http://localhost:3001\nVITE_APP_NAME=JusticeAI\nVITE_APP_VERSION=2.0.0\n\n# Optional: Analytics (disabled by default for privacy)\nVITE_ENABLE_ANALYTICS=false\n\n# Optional: Feature Flags\nVITE_ENABLE_STREAMING=true\nVITE_ENABLE_OFFLINE_MODE=true\n\n# Bhashini API Configuration (For Indian Language Voice Support)\n# Get credentials from: https://bhashini.gov.in/\nBHASHINI_API_KEY=your_bhashini_api_key_here\nBHASHINI_USER_ID=your_bhashini_user_id_here\nBHASHINI_PIPELINE_ID=64392f0502070e176378e90e\n\n# Google Gemini Configuration (Fallback AI)\n# Get your API key from: https://aistudio.google.com/\nGEMINI_API_KEY=AIzaSyBlYdkJBOQ4IojQXt--c0ELRkxDeL4O3Wo\nGEMINI_MODEL=gemini-2.0-flash\n\n# DeepSeek Cloud Configuration (Secondary Fallback)\n# Get your API key from: https://platform.deepseek.com/\nDEEPSEEK_API_KEY=sk-6e666c025b3a4ba4af0e1d1cf5295828\nDEEPSEEK_MODEL=deepseek-chat"}, "9b3a73a27cd5cf8ecab0eef9e4a097e22b59d1f1": {"t": 1785747783.971312, "n": 1169, "h": "d4a11f8b2035edb8", "c": "# JusticeAI Environment Configuration\n# Copy this file to .env.local and update with your values\n\n# Server Configuration\nPORT=3001\nNODE_ENV=development\n\n# Ollama Configuration\nOLLAMA_BASE_URL=http://localhost:11434\nEMBEDDING_MODEL=nomic-embed-text\nCHAT_MODEL=gemma3:4b\n\n# Frontend Configuration\nVITE_API_URL=http://localhost:3001\nVITE_APP_NAME=JusticeAI\nVITE_APP_VERSION=2.0.0\n\n# Optional: Analytics (disabled by default for privacy)\nVITE_ENABLE_ANALYTICS=false\n\n# Optional: Feature Flags\nVITE_ENABLE_STREAMING=true\nVITE_ENABLE_OFFLINE_MODE=true\n\n# Bhashini API Configuration (For Indian Language Voice Support)\n# Get credentials from: https://bhashini.gov.in/\nBHASHINI_API_KEY=your_bhashini_api_key_here\nBHASHINI_USER_ID=your_bhashini_user_id_here\nBHASHINI_PIPELINE_ID=64392f0502070e176378e90e\n\n# Google Gemini Configuration (Fallback AI)\n# Get your API key from: https://aistudio.google.com/\nGEMINI_API_KEY=AIzaSyBlYdkJBOQ4IojQXt--c0ELRkxDeL4O3Wo\nGEMINI_MODEL=gemini-2.0-flash\n\n# DeepSeek Cloud Configuration (Secondary Fallback)\n# Get your API key from: https://platform.deepseek.com/\nDEEPSEEK_API_KEY=sk-6e666c025b3a4ba4af0e1d1cf5295828\nDEEPSEEK_MODEL=deepseek-chat"}}, "server.js": {"ea84ce1aa9a4add4eace26cb4d0f97059bbf9f00": {"t": 1786068491.1275938, "n": 32712, "h": "a6bf10cf1b039c5e", "c": "import express from 'express';\nimport cors from 'cors';\nimport helmet from 'helmet';\nimport rateLimit from 'express-rate-limit';\nimport compression from 'compression';\nimport hpp from 'hpp';\nimport multer from 'multer';\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { createRequire } from 'module';\nimport { OllamaEmbeddings } from '@langchain/ollama';\nimport winston from 'winston';\n\nconst require = createRequire(import.meta.url);\nconst { PDFParse: pdfParse } = require('pdf-parse');\nimport dotenv from 'dotenv';\n\n// Load environment variables\ndotenv.config();\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Initialize Winston logger\nconst logger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n format: winston.format.combine(\n winston.format.timestamp(),\n winston.format.json()\n ),\n transports: [\n new winston.transports.Console(),\n new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),\n new winston.transports.File({ filename: 'logs/combined.log' })\n ]\n});\n\nconst app = express();\n\n// Configuration from environment variables with fallbacks\nconst PORT = process.env.PORT || 3001;\nconst NODE_ENV = process.env.NODE_ENV || 'development';\nconst OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\nconst EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'nomic-embed-text';\nconst CHAT_MODEL = process.env.CHAT_MODEL || 'gemma3:4b';\nconst MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024; // 10MB default\nconst BHASHINI_API_KEY = process.env.BHASHINI_API_KEY;\nconst BHASHINI_USER_ID = process.env.BHASHINI_USER_ID;\nconst BHASHINI_PIPELINE_ID = process.env.BHASHINI_PIPELINE_ID || '64392f96daac500b55c543cd';\nconst BHASHINI_BASE_URL = 'https://dhruva-api.bhashini.gov.in/services/inference/pipeline';\nconst GEMINI_API_KEY = process.env.GEMINI_API_KEY;\nconst GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.0-flash-exp';\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst DEEPSEEK_BASE_URL = 'https://api.deepseek.com/chat/completions';\n\n// Security: Validate required environment variables\nconst requiredEnvVars = ['PORT', 'NODE_ENV', 'OLLAMA_BASE_URL', 'EMBEDDING_MODEL', 'CHAT_MODEL'];\nconst missingEnvVars = requiredEnvVars.filter(varName => !process.env[varName]);\nif (missingEnvVars.length > 0) {\n throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);\n}\n\nlogger.info('Environment validation passed', { nodeEnv: NODE_ENV, port: PORT });\n\n// ===== SECURITY MIDDLEWARE =====\n\n// Security headers\napp.use(helmet({\n contentSecurityPolicy: {\n directives: {\n defaultSrc: [\"'self'\"],\n scriptSrc: [\"'self'\", \"'unsafe-inline'\", \"'unsafe-eval'\", \"blob:\"],\n styleSrc: [\"'self'\", \"'unsafe-inline'\", \"https://fonts.googleapis.com\"],\n imgSrc: [\"'self'\", \"data:\", \"blob:\", \"https://*.googleapis.com\", \"https://*.gstatic.com\"],\n connectSrc: [\"'self'\", \"http://localhost:11434\", \"http://localhost:3001\", \"https://*.bhashini.gov.in\", \"https://*.googleapis.com\", \"https://api.deepseek.com\"],\n fontSrc: [\"'self'\", \"https://fonts.gstatic.com\", \"data:\"],\n objectSrc: [\"'none'\"],\n mediaSrc: [\"'self'\", \"blob:\"],\n frameSrc: [\"'none'\"],\n workerSrc: [\"'self'\", \"blob:\"]\n }\n },\n hsts: NODE_ENV === 'production' ? {\n maxAge: 31536000,\n includeUnsubmitted: true,\n preload: true\n } : false,\n // Allow React DevTools in development\n sourceMaps: NODE_ENV === 'development'\n}));\n\n// CORS with better security\napp.use(cors({\n origin: process.env.CORS_ORIGIN || (NODE_ENV === 'production' ? false : '*'),\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: ['Content-Type', 'Authorization']\n}));\n\n// Compression\napp.use(compression({\n level: 6,\n threshold: 1024 // Only compress responses > 1KB\n}));\n\n// Body parsing with limits\napp.use(express.json({ limit: '10mb' }));\napp.use(express.urlencoded({ extended: true, limit: '10mb' }));\n\n// Prevent parameter pollution\napp.use(hpp());\n\n// Custom XSS protection middleware\nconst xssMiddleware = (req, res, next) => {\n const escapeHtml = (str) => {\n if (!str) return str;\n return str\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '\"')\n .replace(/'/g, ''');\n };\n\n const sanitize = (obj) => {\n if (typeof obj === 'string') return escapeHtml(obj);\n if (Array.isArray(obj)) return obj.map(sanitize);\n if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {\n const sanitized = {};\n for (const key in obj) {\n sanitized[key] = sanitize(obj[key]);\n }\n return sanitized;\n }\n return obj;\n };\n\n // Sanitize body and query (req.params is read-only in Express 5)\n if (req.body) req.body = sanitize(req.body);\n if (req.query) {\n // For query params, we need to handle them differently\n Object.keys(req.query).forEach(key => {\n req.query[key] = sanitize(req.query[key]);\n });\n }\n\n next();\n};\n\napp.use(xssMiddleware);\n\n// ===== RATE LIMITING =====\n\n// General API rate limiting (generous for development, stricter for production)\nconst apiLimiter = rateLimit({\n windowMs: 15 * 60 * 1000, // 15 minutes\n max: NODE_ENV === 'production' ? 200 : 1000, // 200 for prod, 1000 for dev\n message: {\n error: 'Too many requests',\n message: 'Please try again later',\n retryAfter: 900\n },\n standardHeaders: true,\n legacyHeaders: false,\n skip: (req) => NODE_ENV === 'test'\n});\n\n// Rate limiting for chat endpoints (balanced to prevent abuse while allowing active use)\nconst chatLimiter = rateLimit({\n windowMs: 15 * 60 * 1000, // 15 minutes\n max: NODE_ENV === 'production' ? 100 : 500, // 100 for prod (~7/min), 500 for dev\n message: {\n error: 'Too many chat requests',\n message: 'Please wait before sending more messages',\n retryAfter: 900\n },\n standardHeaders: true,\n legacyHeaders: false,\n skip: (req) => NODE_ENV === 'test'\n});\n\n// Apply rate limiting\napp.use('/api', apiLimiter);\napp.use('/api/chat', chatLimiter);\n\n// Request logging middleware with Winston\napp.use((req, res, next) => {\n const start = Date.now();\n res.on('finish', () => {\n const duration = Date.now() - start;\n const level = res.statusCode >= 400 ? 'warn' : 'info';\n logger.log(level, `${req.method} ${req.path}`, {\n method: req.method,\n path: req.path,\n statusCode: res.statusCode,\n duration: `${duration}ms`,\n ip: req.ip\n });\n });\n next();\n});\n\nconst DOCUMENTS_DIR = path.join(__dirname, 'public', 'documents');\nconst UPLOADS_DIR = path.join(__dirname, 'uploads');\nlet documentChunks = [];\n\n// Ensure directories exist\n[DOCUMENTS_DIR, UPLOADS_DIR].forEach(dir => {\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n});\n\n// Configure multer for file uploads\nconst storage = multer.diskStorage({\n destination: (req, file, cb) => {\n cb(null, UPLOADS_DIR);\n },\n filename: (req, file, cb) => {\n const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);\n cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));\n }\n});\n\nconst upload = multer({\n storage,\n limits: { fileSize: MAX_FILE_SIZE, files: 5 },\n fileFilter: (req, file, cb) => {\n const allowedTypes = ['.pdf', '.txt', '.md', '.doc', '.docx'];\n const ext = path.extname(file.originalname).toLowerCase();\n if (allowedTypes.includes(ext)) {\n cb(null, true);\n } else {\n cb(new Error(`File type ${ext} not allowed. Allowed types: ${allowedTypes.join(', ')}`));\n }\n }\n});\n\n// 1. Initialize RAG using Ollama for Embeddings\nconst embeddings = new OllamaEmbeddings({\n model: EMBEDDING_MODEL,\n baseUrl: OLLAMA_BASE_URL,\n});\n\n// Dynamic import for pdf-parse (CJS module in ESM project)\nasync function parsePdf(dataBuffer) {\n try {\n const instance = new pdfParse(new Uint8Array(dataBuffer));\n const result = await instance.getText();\n // This modern package (pdf-parse 2.4.5) requires calling .getText()\n return { text: result.text || '' };\n } catch (err) {\n throw new Error(`PDF Parsing failed: ${err.message}`);\n }\n}\n\n// Custom lightweight text splitter \nfunction splitTextIntoChunks(text, chunkSize = 1000, overlap = 200) {\n const chunks = [];\n let i = 0;\n while (i < text.length) {\n chunks.push(text.substring(i, i + chunkSize));\n i += chunkSize - overlap;\n }\n return chunks;\n}\n\n// Native Cosine Similarity\nfunction cosineSimilarity(vecA, vecB) {\n let dotProduct = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < vecA.length; i++) {\n dotProduct += vecA[i] * vecB[i];\n normA += vecA[i] * vecA[i];\n normB += vecB[i] * vecB[i];\n }\n let div = (Math.sqrt(normA) * Math.sqrt(normB));\n return div === 0 ? 0 : (dotProduct / div);\n}\n\nasync function callGemini(messages, systemPrompt, overrideApiKey = null) {\n const apiKey = overrideApiKey || GEMINI_API_KEY;\n if (!apiKey) {\n throw new Error('Gemini API Key missing.');\n }\n\n const url = `https://generativelanguage.googleapis.com/v1/models/${GEMINI_MODEL}:generateContent?key=${apiKey}`;\n \n // Transform messages for Gemini format (v1 compatible)\n const contents = [];\n \n // Prepend system prompt to the first user message for maximum compatibility\n let firstUserMessageIndex = messages.findIndex(m => m.role === 'user');\n \n for (let i = 0; i < messages.length; i++) {\n const m = messages[i];\n if (m.role === 'system') continue;\n \n let text = m.content;\n if (i === firstUserMessageIndex) {\n text = `SYSTEM_INSTRUCTION: ${systemPrompt}\\n\\nUSER_QUERY: ${text}`;\n }\n\n contents.push({\n role: m.role === 'assistant' ? 'model' : 'user',\n parts: [{ text }]\n });\n }\n\n const payload = {\n contents,\n generationConfig: {\n temperature: 0.7,\n maxOutputTokens: 2048,\n }\n };\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errText = await response.text();\n throw new Error(`Gemini API Error: ${response.status} - ${errText}`);\n }\n\n const data = await response.json();\n const content = data.candidates?.[0]?.content?.parts?.[0]?.text || '';\n \n return {\n message: { role: 'assistant', content },\n model: `Gemini (${GEMINI_MODEL})`\n };\n}\n\nasync function callDeepSeek(messages, systemPrompt, overrideApiKey = null) {\n const apiKey = overrideApiKey || DEEPSEEK_API_KEY;\n if (!apiKey) {\n throw new Error('DeepSeek API Key missing.');\n }\n\n const payload = {\n model: DEEPSEEK_MODEL,\n messages: [\n { role: 'system', content: systemPrompt },\n ...messages.map(m => ({ role: m.role, content: m.content }))\n ],\n temperature: 0.7,\n max_tokens: 2048\n };\n\n const response = await fetch(DEEPSEEK_BASE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errText = await response.text();\n throw new Error(`DeepSeek API Error: ${response.status} - ${errText}`);\n }\n\n const data = await response.json();\n const content = data.choices[0].message.content;\n\n return {\n message: { role: 'assistant', content },\n model: `DeepSeek (${DEEPSEEK_MODEL})`\n };\n}\n\nasync function loadAndIndexDocuments() {\n try {\n const allFiles = fs.readdirSync(DOCUMENTS_DIR);\n const pdfFiles = allFiles.filter(f => f.endsWith('.pdf'));\n const txtFiles = allFiles.filter(f => f.endsWith('.txt'));\n const mdFiles = allFiles.filter(f => f.endsWith('.md'));\n\n if (pdfFiles.length === 0 && txtFiles.length === 0 && mdFiles.length === 0) {\n console.log('No documents found in public/documents/. RAG memory is empty.');\n return;\n }\n\n console.log(`Found ${pdfFiles.length} PDFs, ${txtFiles.length} TXT files, ${mdFiles.length} MD files. Extracting text...`);\n \n // Track text per source file for proper source attribution\n const sourceTexts = [];\n\n // Load PDFs\n for (const file of pdfFiles) {\n try {\n const dataBuffer = fs.readFileSync(path.join(DOCUMENTS_DIR, file));\n const pdfData = await parsePdf(dataBuffer);\n sourceTexts.push({ source: file, text: pdfData.text });\n console.log(` ✓ Loaded PDF: ${file} (${pdfData.text.length} chars)`);\n } catch (pdfErr) {\n console.warn(` ✗ Failed to parse PDF: ${file} — ${pdfErr.message}`);\n }\n }\n\n // Load TXT files\n for (const file of txtFiles) {\n try {\n const text = fs.readFileSync(path.join(DOCUMENTS_DIR, file), 'utf-8');\n sourceTexts.push({ source: file, text });\n console.log(` ✓ Loaded TXT: ${file} (${text.length} chars)`);\n } catch (txtErr) {\n console.warn(` ✗ Failed to read TXT: ${file} — ${txtErr.message}`);\n }\n }\n\n // Load MD files\n for (const file of mdFiles) {\n try {\n const text = fs.readFileSync(path.join(DOCUMENTS_DIR, file), 'utf-8');\n sourceTexts.push({ source: file, text });\n console.log(` ✓ Loaded MD: ${file} (${text.length} chars)`);\n } catch (mdErr) {\n console.warn(` ✗ Failed to read MD: ${file} — ${mdErr.message}`);\n }\n }\n\n // Chunk and embed each source separately for proper attribution\n let totalChunks = 0;\n for (const { source, text } of sourceTexts) {\n const rawChunks = splitTextIntoChunks(text, 1000, 200);\n totalChunks += rawChunks.length;\n \n for (let j = 0; j < rawChunks.length; j++) {\n const chunk = rawChunks[j];\n const vector = await embeddings.embedQuery(chunk);\n documentChunks.push({\n content: chunk,\n vector: vector,\n source: source\n });\n }\n console.log(` 📦 Embedded ${rawChunks.length} chunks from ${source}`);\n }\n \n console.log(`✅ RAG Vector Database Loaded! Total chunks indexed: ${totalChunks}`);\n } catch (error) {\n console.error('Error loading documents:', error);\n }\n}\n\n// ============ API ROUTES ============\n// Root endpoint\napp.get('/', (req, res) => {\n res.send(`\n
\nThe legal co-pilot backend is live and operational.
\n \n \n `);\n});\n\n// Health check endpoint\napp.get('/api/health', (req, res) => {\n res.json({\n status: 'healthy',\n timestamp: new Date().toISOString(),\n uptime: process.uptime(),\n memory: process.memoryUsage(),\n rag: {\n loaded: documentChunks.length > 0,\n chunks: documentChunks.length\n },\n ollama: {\n baseUrl: OLLAMA_BASE_URL,\n embeddingModel: EMBEDDING_MODEL,\n chatModel: CHAT_MODEL\n }\n });\n});\n\n// Get RAG statistics\napp.get('/api/stats', (req, res) => {\n const sources = [...new Set(documentChunks.map(c => c.source))];\n res.json({\n totalChunks: documentChunks.length,\n totalSources: sources.length,\n sources: sources,\n memoryUsage: process.memoryUsage()\n });\n});\n\n// Voice Configuration Endpoint (tells client which STT methods are available)\napp.get('/api/voice/config', (req, res) => {\n // Check if Bhashini credentials are properly configured (not empty or placeholder values)\n const isPlaceholder = (value) => {\n if (!value || typeof value !== 'string') return true;\n const trimmed = value.trim().toLowerCase();\n return (\n trimmed === '' ||\n trimmed === 'your_bhashini_api_key_here' ||\n trimmed === 'your_bhashini_user_id_here' ||\n trimmed.startsWith('your_') ||\n trimmed === 'placeholder'\n );\n };\n\n const bhashiniConfigured = !isPlaceholder(BHASHINI_API_KEY) && !isPlaceholder(BHASHINI_USER_ID);\n \n // Only return pipelineId when Bhashini is actually configured (avoid info leak)\n const bhashiniResponse = { available: bhashiniConfigured };\n if (bhashiniConfigured) {\n bhashiniResponse.pipelineId = BHASHINI_PIPELINE_ID;\n }\n \n res.json({\n bhashini: bhashiniResponse\n // Web Speech API availability is checked client-side\n });\n});\n\n// Bhashini Voice Processing Endpoint (STT & TTS Proxy)\napp.post('/api/voice/process', async (req, res) => {\n try {\n const { task, audioContent, text, sourceLanguage, targetLanguage } = req.body;\n \n // Check if Bhashini credentials are provided, otherwise enter Mock Mode\n if (!BHASHINI_API_KEY || !BHASHINI_USER_ID) {\n console.log('⚠️ Bhashini Credentials missing. Running in MOCK MODE.');\n \n if (task === 'asr') {\n // Mock transcription\n return res.json({ \n status: 'mock',\n output: [{ source: 'JusticeAI Mock ASR', transcription: 'This is a simulated transcription because Bhashini API keys are not configured.' }] \n });\n } else if (task === 'tts') {\n // Mock TTS (no audio)\n return res.json({ \n status: 'mock',\n output: [{ audioContent: '', message: 'Simulated TTS output' }] \n });\n }\n }\n\n // Step 1: Fetch Pipeline Configuration\n const configPayload = {\n pipelineTasks: [\n {\n taskType: task, // 'asr' or 'tts'\n config: {\n language: {\n sourceLanguage: sourceLanguage || 'hi'\n }\n }\n }\n ],\n pipelineConfig: {\n pipelineId: BHASHINI_PIPELINE_ID\n }\n };\n\n const configResponse = await fetch(`${BHASHINI_BASE_URL}/config`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'ulcaApiKey': BHASHINI_API_KEY,\n 'userID': BHASHINI_USER_ID\n },\n body: JSON.stringify(configPayload)\n });\n\n if (!configResponse.ok) {\n const errorText = await configResponse.text();\n throw new Error(`Bhashini Config Error: ${configResponse.status} - ${errorText}`);\n }\n\n const configData = await configResponse.json();\n\n // Step 2: Compute Inference\n const computePayload = {\n pipelineTasks: [\n {\n taskType: task,\n config: configData.pipelineResponseConfig[0].config,\n input: task === 'asr' \n ? [{ audioContent }] \n : [{ source: text }]\n }\n ],\n pipelineResponseConfig: configData.pipelineResponseConfig\n };\n\n const computeResponse = await fetch(`${BHASHINI_BASE_URL}/compute`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': configData.pipelineInferenceAPIEndPoint.inferenceApiKey.value,\n 'Accept': '*/*'\n },\n body: JSON.stringify(computePayload)\n });\n\n if (!computeResponse.ok) {\n const errorText = await computeResponse.text();\n throw new Error(`Bhashini Compute Error: ${computeResponse.status} - ${errorText}`);\n }\n\n const computeData = await computeResponse.json();\n \n // Return the specific output based on task\n if (task === 'asr') {\n res.json({\n transcription: computeData.pipelineResponse[0].output[0].source,\n raw: computeData\n });\n } else {\n res.json({\n audioContent: computeData.pipelineResponse[0].audio[0].audioContent,\n raw: computeData\n });\n }\n\n } catch (err) {\n console.error('Bhashini Proxy Error:', err);\n res.status(500).json({ error: err.message });\n }\n});\n\n// Upload documents endpoint\napp.post('/api/upload', upload.array('documents', 5), async (req, res) => {\n try {\n if (!req.files || req.files.length === 0) {\n return res.status(400).json({ error: 'No files uploaded' });\n }\n\n const uploadedFiles = [];\n const failedFiles = [];\n\n for (const file of req.files) {\n try {\n const filePath = file.path;\n const fileName = file.originalname;\n const ext = path.extname(fileName).toLowerCase();\n let text = '';\n\n if (ext === '.pdf') {\n const dataBuffer = fs.readFileSync(filePath);\n const pdfData = await parsePdf(dataBuffer);\n text = pdfData.text;\n } else if (ext === '.txt' || ext === '.md') {\n text = fs.readFileSync(filePath, 'utf-8');\n } else {\n failedFiles.push({ name: fileName, error: 'Unsupported file type' });\n continue;\n }\n\n // Chunk and embed\n const rawChunks = splitTextIntoChunks(text, 1000, 200);\n let chunksEmbedded = 0;\n\n for (const chunk of rawChunks) {\n const vector = await embeddings.embedQuery(chunk);\n documentChunks.push({\n content: chunk,\n vector: vector,\n source: fileName\n });\n chunksEmbedded++;\n }\n\n uploadedFiles.push({\n name: fileName,\n size: file.size,\n chunks: chunksEmbedded\n });\n\n // Clean up uploaded file\n fs.unlinkSync(filePath);\n\n } catch (err) {\n failedFiles.push({ name: file.originalname, error: err.message });\n }\n }\n\n res.json({\n message: 'Documents processed',\n uploaded: uploadedFiles,\n failed: failedFiles,\n totalChunks: documentChunks.length\n });\n\n } catch (error) {\n console.error('Upload error:', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// Delete specific document chunks by source\napp.delete('/api/documents/:source', (req, res) => {\n const source = decodeURIComponent(req.params.source);\n const beforeCount = documentChunks.length;\n documentChunks = documentChunks.filter(chunk => chunk.source !== source);\n const removed = beforeCount - documentChunks.length;\n \n res.json({\n message: `Removed ${removed} chunks from ${source}`,\n remainingChunks: documentChunks.length\n });\n});\n\n// Clear all documents\napp.delete('/api/documents', (req, res) => {\n const count = documentChunks.length;\n documentChunks = [];\n res.json({\n message: `Cleared ${count} chunks`,\n remainingChunks: 0\n });\n});\n\n// Endpoint to chat using RAG (with optional streaming)\napp.post('/api/chat', async (req, res) => {\n console.log(`[ROUTE] Incoming POST /api/chat - Body Keys: ${Object.keys(req.body || {}).join(', ')}`);\n try {\n const { \n messages, \n personality, \n mode, \n jurisdiction, \n basePrompt, \n stream = false,\n provider = 'auto', \n apiKeys = {} \n } = req.body;\n \n if (!messages || !Array.isArray(messages) || messages.length === 0) {\n return res.status(400).json({ error: 'Messages array is required' });\n }\n\n // Get the latest user message to search for context\n const latestUserMessage = messages[messages.length - 1].content;\n \n let contextStr = '';\n let retrievedSources = [];\n \n if (documentChunks.length > 0) {\n try {\n const userVector = await embeddings.embedQuery(latestUserMessage);\n \n // Calculate scores for all chunks, prioritizing ones that match the jurisdiction if specified\n const scoredChunks = documentChunks.map(chunk => {\n let score = cosineSimilarity(userVector, chunk.vector);\n \n // Boost chunks from the correct jurisdiction (filename-based tagging)\n if (jurisdiction && jurisdiction !== 'National' && chunk.source?.toLowerCase().includes(jurisdiction.toLowerCase())) {\n score += 0.2;\n }\n \n return {\n content: chunk.content,\n score: score,\n source: chunk.source\n };\n });\n \n // Sort descending by score\n scoredChunks.sort((a, b) => b.score - a.score);\n \n // Take top 5 chunks for richer context\n const topChunks = scoredChunks.slice(0, 5).filter(c => c.score > 0.1); // Filter low-relevance chunks\n \n retrievedSources = [...new Set(topChunks.map(c => c.source))];\n \n contextStr = \"\\n\\nCRITICAL LEGAL PRECEDENTS (From Knowledge Base):\\n\" + \n topChunks.map(c => `[Source: ${c.source}] [Relevance: ${(c.score * 100).toFixed(1)}%]\\n${c.content}`).join(\"\\n\\n---\\n\\n\");\n \n console.log(` 🔍 RAG retrieved top chunks from: ${retrievedSources.join(', ')}`);\n } catch (embedError) {\n console.warn('Embedding failed, proceeding without RAG context:', embedError.message);\n }\n }\n\n // Format the prompt\n let systemPrompt = basePrompt || `You are JusticeAI, a powerful legal co-pilot specializing in Indian law.`;\n \n systemPrompt += `\\n\\nCURRENT CONTEXT: You are providing assistance under the jurisdiction of ${jurisdiction || 'India (National)'}.`;\n\n if (mode === 'simulator') {\n systemPrompt += `\\n\\nROLE PLAY: You are opposing counsel. Be adversarial and challenge the user's arguments aggressively. Exploit weaknesses in their position. This is a simulation for moot practice only.`;\n }\n if (personality) {\n systemPrompt += `\\n\\nYour judicial/opponent personality is: ${personality}.`;\n }\n \n if (mode === 'analyzer') {\n systemPrompt += `\\n\\nCRITICAL RULE: You must return your response strictly as a structured JSON object.`;\n } else {\n systemPrompt += `\\n\\nCRITICAL CONVERSATIONAL RULE: Keep your conversational responses clear, articulate, and well-formatted. Do not output raw JSON objects in this mode. Use elegant bullet points and line breaks.`;\n }\n\n systemPrompt += contextStr;\n\n // Build the request for Ollama\n const requestData = {\n model: CHAT_MODEL,\n messages: [\n { role: 'system', content: systemPrompt },\n ...messages.map(m => ({\n role: m.role,\n content: m.content\n }))\n ],\n stream: stream\n };\n\n console.log(`Routing query to Ollama: \"${latestUserMessage.substring(0, 50)}...\"`);\n \n try {\n if (stream) {\n // Streaming response\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n\n const response = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(requestData),\n signal: AbortSignal.timeout(10000) // 10s timeout for local Ollama\n });\n\n if (!response.ok) {\n throw new Error(`Ollama Error: ${response.statusText}`);\n }\n\n // Pipe the streaming response\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n \n const chunk = decoder.decode(value);\n res.write(`data: ${chunk}\\n\\n`);\n }\n\n res.end();\n } else {\n // Non-streaming response\n const ollamaResponse = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(requestData),\n signal: AbortSignal.timeout(15000) // 15s timeout\n });\n\n if (!ollamaResponse.ok) {\n throw new Error(`Ollama Error: ${await ollamaResponse.text()}`);\n }\n\n const data = await ollamaResponse.json();\n res.json({ \n message: { \n role: 'assistant', \n content: data.message.content \n },\n metadata: {\n sources: retrievedSources,\n model: CHAT_MODEL,\n provider: 'Ollama (Local)',\n timestamp: new Date().toISOString()\n }\n });\n }\n } catch (ollamaErr) {\n console.warn(`⚠️ Ollama failure detected: ${ollamaErr.message}. Shifting to Gemini fallback...`);\n \n let finalResult = null;\n let usedProvider = null;\n let errorChain = [`Ollama: ${ollamaErr.message}`];\n\n if (provider !== 'auto') {\n console.log(`User-forced provider: ${provider}`);\n if (provider === 'gemini') {\n finalResult = await callGemini(messages, systemPrompt, apiKeys.gemini);\n usedProvider = 'Gemini (User Controlled)';\n } else if (provider === 'deepseek') {\n finalResult = await callDeepSeek(messages, systemPrompt, apiKeys.deepseek);\n usedProvider = 'DeepSeek (User Controlled)';\n } else {\n // Fall back to Ollama if forced provider is ollama or invalid\n throw ollamaErr; \n }\n } else {\n // Waterfall Tier 2: Gemini\n if (apiKeys.gemini || GEMINI_API_KEY) {\n try {\n finalResult = await callGemini(messages, systemPrompt, apiKeys.gemini);\n usedProvider = 'Gemini (Cloud Fallback)';\n } catch (geminiErr) {\n console.warn(`⚠️ Gemini fallback failed: ${geminiErr.message}. Shifting to DeepSeek...`);\n errorChain.push(`Gemini: ${geminiErr.message}`);\n }\n }\n\n // Waterfall Tier 3: DeepSeek\n if (!finalResult && (apiKeys.deepseek || DEEPSEEK_API_KEY)) {\n try {\n finalResult = await callDeepSeek(messages, systemPrompt, apiKeys.deepseek);\n usedProvider = 'DeepSeek (Cloud Fallback)';\n } catch (deepseekErr) {\n console.error(`❌ DeepSeek fallback failed: ${deepseekErr.message}`);\n errorChain.push(`DeepSeek: ${deepseekErr.message}`);\n }\n }\n }\n\n if (finalResult) {\n res.json({\n ...finalResult,\n metadata: {\n sources: retrievedSources,\n provider: usedProvider,\n timestamp: new Date().toISOString(),\n errorChain: errorChain\n }\n });\n } else {\n throw new Error(`Multi-tier failover exhausted. Chain: ${errorChain.join(' -> ')}`);\n }\n }\n \n } catch (err) {\n console.error(\"Chat Error:\", err);\n res.status(500).json({ \n error: err.message,\n details: process.env.NODE_ENV === 'development' ? err.stack : undefined\n });\n }\n});\n\n// Embedding endpoint (for testing/debugging)\napp.post('/api/embed', async (req, res) => {\n try {\n const { text } = req.body;\n if (!text) {\n return res.status(400).json({ error: 'Text is required' });\n }\n \n const vector = await embeddings.embedQuery(text);\n res.json({\n vector: vector.slice(0, 10), // Return first 10 dimensions for brevity\n dimensions: vector.length,\n model: EMBEDDING_MODEL\n });\n } catch (err) {\n res.status(500).json({ error: err.message });\n }\n});\n\n// Error handling middleware\napp.use((err, req, res, next) => {\n console.error('Unhandled error:', err);\n res.status(500).json({\n error: 'Internal server error',\n message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'\n });\n});\n\n// 404 handler\napp.use((req, res) => {\n res.status(404).json({\n error: 'Not found',\n path: req.path,\n method: req.method\n });\n});\n\n// Start the server\n// Relocating document indexing to keep the server alive correctly\n(async () => {\n await loadAndIndexDocuments();\n})();\n\n// Keep process alive if event loop becomes empty (Express should handle this, but adding a fail-safe)\nsetInterval(() => {}, 1000 * 60 * 60); // 1 hour tick\n\napp.listen(PORT, () => {\n console.log(`\\n⚖️ JusticeAI RAG Server running on port ${PORT}`);\n console.log(`📡 API available at: http://localhost:${PORT}/api`);\n console.log(`🏥 Health check: http://localhost:${PORT}/api/health`);\n console.log(`\\nMake sure Ollama is running:`);\n console.log(` ollama serve`);\n console.log(`\\nRequired models:`);\n console.log(` ollama pull ${EMBEDDING_MODEL}`);\n console.log(` ollama pull ${CHAT_MODEL}\\n`);\n});\n\n// Graceful shutdown\nprocess.on('SIGTERM', () => {\n console.log('SIGTERM received, shutting down gracefully...');\n process.exit(0);\n});\n\nprocess.on('SIGINT', () => {\n console.log('SIGINT received, shutting down gracefully...');\n process.exit(0);\n});"}}}}