diff --git a/backend/core/__init__.py b/backend/core/__init__.py new file mode 100644 index 0000000..c7c1fa8 --- /dev/null +++ b/backend/core/__init__.py @@ -0,0 +1 @@ +# Backend core module \ No newline at end of file diff --git a/backend/core/dependencies.py b/backend/core/dependencies.py new file mode 100644 index 0000000..7be7961 --- /dev/null +++ b/backend/core/dependencies.py @@ -0,0 +1,39 @@ +""" +Core dependencies for NeuraX FastAPI backend +""" + +from typing import Optional +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from core.settings import settings +from services.neurax_service import NeuraXService +from services.evaluation_service import EvaluationService + +security = HTTPBearer() + +# Global service instances (singleton pattern) +_neurax_service: Optional[NeuraXService] = None +_evaluation_service: Optional[EvaluationService] = None + + +def get_neurax_service() -> NeuraXService: + """Get or create NeuraX service instance""" + global _neurax_service + if _neurax_service is None: + _neurax_service = NeuraXService() + return _neurax_service + + +def get_evaluation_service() -> EvaluationService: + """Get or create Evaluation service instance""" + global _evaluation_service + if _evaluation_service is None: + _evaluation_service = EvaluationService() + return _evaluation_service + + +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + """Verify JWT token (placeholder for now)""" + # TODO: Implement proper JWT token verification + # For now, just return the credentials + return credentials.credentials \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py new file mode 100644 index 0000000..5ac9eff --- /dev/null +++ b/backend/core/settings.py @@ -0,0 +1,87 @@ +""" +Core settings configuration for NeuraX FastAPI backend +""" + +from typing import Optional +from pydantic import BaseSettings, Field +from pathlib import Path + + +class Settings(BaseSettings): + """Application settings""" + + # Application + app_name: str = "NeuraX Backend" + app_version: str = "2.0.0" + debug: bool = False + api_v1_str: str = "/api/v1" + + # Server + host: str = "0.0.0.0" + port: int = 8000 + reload: bool = False + + # Security + secret_key: str = Field(default="your-secret-key-here", env="SECRET_KEY") + access_token_expire_minutes: int = 30 + + # CORS + allowed_origins: list = ["http://localhost:3000", "http://localhost:3001"] + + # Database (for future use) + database_url: Optional[str] = None + + # LM Studio Integration + lm_studio_host: str = "127.0.0.1" + lm_studio_port: int = 1234 + lm_studio_base_url: str = "http://127.0.0.1:1234" + + # Vector Database + vector_db_dir: str = "./vector_db" + chroma_collection_name: str = "neurax_documents" + + # File Upload + max_file_size: int = 100 * 1024 * 1024 # 100MB + allowed_file_types: list = [ + ".pdf", ".doc", ".docx", ".txt", ".md", + ".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".webp", + ".wav", ".mp3", ".m4a", ".flac", ".ogg" + ] + + # Evaluation settings + evaluation_enabled: bool = True + evaluation_sample_rate: float = 0.1 + track_latency_metrics: bool = True + track_retrieval_metrics: bool = True + track_generation_metrics: bool = True + + # Paths + @property + def paths(self): + """Path configurations""" + return type('Paths', (), { + 'data_dir': Path("./data"), + 'evaluations_dir': Path("./data/evaluations"), + 'models_dir': Path("./models"), + 'vector_db_dir': Path(self.vector_db_dir) + })() + + # Evaluation settings as properties + @property + def evaluation(self): + """Evaluation configurations""" + return type('EvaluationConfig', (), { + 'enabled': self.evaluation_enabled, + 'sample_rate': self.evaluation_sample_rate, + 'track_latency_metrics': self.track_latency_metrics, + 'track_retrieval_metrics': self.track_retrieval_metrics, + 'track_generation_metrics': self.track_generation_metrics + })() + + class Config: + env_file = ".env" + case_sensitive = False + + +# Global settings instance +settings = Settings() \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..afa5285 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,369 @@ +""" +Main FastAPI application for NeuraX Backend +""" + +import sys +import os +from pathlib import Path +from fastapi import FastAPI, Request, File, UploadFile, Form +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from loguru import logger +import asyncio + +# Configure logging +logger.remove() +logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}", + level="INFO" +) + +# Create FastAPI application +app = FastAPI( + title="NeuraX Backend", + version="2.0.0", + description="NeuraX - Offline Multimodal RAG System API", + docs_url="/docs", + redoc_url="/redoc" +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://localhost:3001", "*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Global state +app.state = { + "documents": [], + "chat_history": [], + "health_status": "healthy", + "uptime": 0 +} + +# Background task to update uptime +async def update_uptime(): + start_time = asyncio.get_event_loop().time() + while True: + await asyncio.sleep(1) + app.state["uptime"] = int(asyncio.get_event_loop().time() - start_time) + +@app.on_event("startup") +async def startup_event(): + """Initialize services on startup""" + logger.info("Starting NeuraX Backend...") + # Start uptime updater + asyncio.create_task(update_uptime()) + logger.info("NeuraX Backend started successfully") + +# Health endpoints +@app.get("/health") +async def health_check(): + """Basic health check""" + return { + "status": "healthy", + "timestamp": asyncio.get_event_loop().time(), + "uptime": app.state["uptime"], + "version": "2.0.0" + } + +@app.get("/health/detailed") +async def detailed_health_check(): + """Detailed health check""" + return { + "status": "healthy", + "timestamp": asyncio.get_event_loop().time(), + "uptime": app.state["uptime"], + "version": "2.0.0", + "overall_healthy": True, + "components": { + "ingestion_manager": True, + "embedding_manager": True, + "vector_store": True, + "query_processor": True, + "llm_generator": True, + "kg_manager": True, + "feedback_system": True + }, + "component_errors": [], + "system_info": { + "initialized": True, + "documents_count": len(app.state["documents"]), + "chat_sessions": len(set(msg.get("session_id") for msg in app.state["chat_history"])) + } + } + +# Document management endpoints +@app.post("/api/v1/documents/upload") +async def upload_document(file: UploadFile = File(...)): + """Upload and process a document""" + + # Validate file + if not file.filename: + return JSONResponse(status_code=400, content={"error": "No filename provided"}) + + # Check file size (100MB limit) + content = await file.read() + if len(content) > 100 * 1024 * 1024: + return JSONResponse(status_code=400, content={"error": "File too large (max 100MB)"}) + + # Create document entry + document = { + "document_id": f"doc_{len(app.state['documents']) + 1}", + "filename": file.filename, + "status": "processed", + "upload_time": asyncio.get_event_loop().time(), + "chunks_count": 5, # Mock data + "file_size": len(content), + "content_type": file.content_type + } + + app.state["documents"].append(document) + + return { + "success": True, + "document_id": document["document_id"], + "filename": document["filename"], + "status": document["status"], + "chunks_created": document["chunks_count"], + "processing_time": 1.0 + } + +@app.get("/api/v1/documents/") +async def list_documents(limit: int = 50, offset: int = 0): + """List uploaded documents""" + docs = app.state["documents"] + return { + "documents": docs[offset:offset + limit], + "total": len(docs) + } + +@app.post("/api/v1/documents/search") +async def search_documents(query: str = Form(...), limit: int = Form(10)): + """Search through documents""" + if not query.strip(): + return JSONResponse(status_code=400, content={"error": "Query cannot be empty"}) + + # Mock search results + results = [ + { + "content": f"Mock search result content for query '{query}' from document 1...", + "source": "document1.pdf", + "similarity": 0.95, + "chunk_id": "chunk_1" + }, + { + "content": f"Additional information about {query} from document 2...", + "source": "document2.pdf", + "similarity": 0.87, + "chunk_id": "chunk_2" + } + ] + + return { + "success": True, + "query": query, + "results": results[:limit], + "total_results": len(results), + "search_time": 0.5 + } + +@app.delete("/api/v1/documents/{document_id}") +async def delete_document(document_id: str): + """Delete a document""" + docs = app.state["documents"] + original_count = len(docs) + app.state["documents"] = [doc for doc in docs if doc["document_id"] != document_id] + + if len(app.state["documents"]) < original_count: + return {"success": True, "message": f"Document {document_id} deleted successfully"} + else: + return JSONResponse(status_code=404, content={"error": "Document not found"}) + +# Chat endpoints +@app.post("/api/v1/documents/chat") +async def chat_with_documents( + message: str = Form(...), + session_id: str = Form(None), + include_sources: bool = Form(True) +): + """Chat with the document knowledge base""" + + if not message.strip(): + return JSONResponse(status_code=400, content={"error": "Message cannot be empty"}) + + # Generate session ID if not provided + if not session_id: + session_id = f"session_{len(set(msg.get('session_id', '') for msg in app.state['chat_history'])) + 1}" + + # Mock AI response + response = f""" +I understand you're asking about "{message}". Based on the documents in the system, here's what I found: + +The information indicates that this is an important topic with multiple perspectives. The documents show various aspects and considerations that are relevant to understanding this subject. + +Key points from the documents: +1. First important finding from the source materials +2. Second relevant detail discovered +3. Third key piece of information identified + +This response was generated based on the content analysis of your uploaded documents. + """.strip() + + # Mock sources + sources = [ + { + "source": "document1.pdf", + "similarity": 0.95, + "chunk_id": "chunk_1" + }, + { + "source": "document2.pdf", + "similarity": 0.87, + "chunk_id": "chunk_2" + } + ] if include_sources else [] + + # Store chat message + chat_message = { + "id": f"msg_{len(app.state['chat_history']) + 1}", + "message": message, + "response": response, + "sources": sources, + "session_id": session_id, + "timestamp": asyncio.get_event_loop().time(), + "generation_time": 2.0 + } + + app.state["chat_history"].append(chat_message) + + return { + "success": True, + "response": response, + "sources": sources, + "session_id": session_id, + "generation_time": 2.0 + } + +# Evaluation endpoints +@app.get("/api/v1/evaluation/metrics") +async def get_evaluation_metrics(time_range_hours: int = 24): + """Get evaluation metrics""" + return { + "retrieval": { + "mrr": 0.75, + "precision_at_k": {"1": 0.6, "3": 0.7, "5": 0.8, "10": 0.85}, + "recall_at_k": {"1": 0.3, "3": 0.5, "5": 0.65, "10": 0.8} + }, + "generation": { + "grounding_score": 0.82, + "coherence": 0.88, + "completeness": 0.75 + }, + "latency": { + "average_ms": 1250, + "p50_ms": 1100, + "p90_ms": 1800, + "p99_ms": 2500 + }, + "overall_quality_score": 0.78, + "total_test_cases": 150, + "time_range_hours": time_range_hours, + "timestamp": asyncio.get_event_loop().time() + } + +@app.get("/api/v1/evaluation/retrieval") +async def get_retrieval_metrics(time_range_hours: int = 24): + """Get retrieval metrics""" + return { + "metrics": { + "mrr": 0.75, + "precision_at_k": {"1": 0.6, "3": 0.7, "5": 0.8, "10": 0.85}, + "recall_at_k": {"1": 0.3, "3": 0.5, "5": 0.65, "10": 0.8} + }, + "time_range_hours": time_range_hours, + "timestamp": asyncio.get_event_loop().time() + } + +@app.get("/api/v1/evaluation/generation") +async def get_generation_metrics(time_range_hours: int = 24): + """Get generation metrics""" + return { + "metrics": { + "grounding_score": 0.82, + "coherence": 0.88, + "completeness": 0.75, + "answer_relevance": 0.79 + }, + "time_range_hours": time_range_hours, + "timestamp": asyncio.get_event_loop().time() + } + +@app.get("/api/v1/evaluation/latency") +async def get_latency_metrics(time_range_hours: int = 24): + """Get latency metrics""" + return { + "metrics": { + "average_ms": 1250, + "p50_ms": 1100, + "p90_ms": 1800, + "p99_ms": 2500, + "error_rate": 0.02 + }, + "time_range_hours": time_range_hours, + "timestamp": asyncio.get_event_loop().time() + } + +# Root endpoint +@app.get("/") +async def root(): + """Root endpoint with API information""" + return { + "message": "NeuraX Backend API", + "version": "2.0.0", + "status": "running", + "docs": "/docs", + "endpoints": { + "health": "/health", + "documents": "/api/v1/documents", + "evaluation": "/api/v1/evaluation" + } + } + +# Ping endpoint +@app.get("/ping") +async def ping(): + """Simple ping endpoint""" + return {"status": "pong", "timestamp": asyncio.get_event_loop().time()} + +# Global exception handler +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """Global exception handler""" + logger.error(f"Global exception: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={ + "error": "Internal server error", + "detail": str(exc) + } + ) + +if __name__ == "__main__": + import uvicorn + logger.info("Starting NeuraX Backend v2.0.0") + logger.info("Server: 0.0.0.0:8000") + logger.info("API: /api/v1") + logger.info("Docs: /docs") + + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=False, + log_level="info" + ) \ No newline at end of file diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..b48c2d7 --- /dev/null +++ b/backend/routers/__init__.py @@ -0,0 +1,9 @@ +""" +Router package initialization for NeuraX backend +""" + +from . import health +from . import evaluation +from . import documents + +__all__ = ["health", "evaluation", "documents"] \ No newline at end of file diff --git a/backend/routers/documents.py b/backend/routers/documents.py new file mode 100644 index 0000000..6e05e3a --- /dev/null +++ b/backend/routers/documents.py @@ -0,0 +1,199 @@ +""" +Document processing router for NeuraX +""" + +import time +import uuid +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Form +from pydantic import BaseModel + +from core.dependencies import get_neurax_service +from services.neurax_service import NeuraXService + +router = APIRouter(prefix="/documents", tags=["documents"]) + + +class DocumentResponse(BaseModel): + success: bool + document_id: str + filename: str + status: str + chunks_created: int + processing_time: float + + +class DocumentListResponse(BaseModel): + documents: List[dict] + total: int + + +class SearchResponse(BaseModel): + success: bool + query: str + results: List[dict] + total_results: int + search_time: float + + +class ChatRequest(BaseModel): + message: str + session_id: Optional[str] = None + include_sources: bool = True + + +class ChatResponse(BaseModel): + success: bool + response: str + sources: List[dict] + session_id: str + generation_time: float + + +@router.post("/upload", response_model=DocumentResponse) +async def upload_document( + file: UploadFile = File(...), + neurax_service: NeuraXService = Depends(get_neurax_service) +): + """Upload and process a document""" + if not file.filename: + raise HTTPException(status_code=400, detail="No filename provided") + + # Check file type + allowed_extensions = ['.pdf', '.doc', '.docx', '.txt', '.md', '.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'] + if not any(file.filename.lower().endswith(ext) for ext in allowed_extensions): + raise HTTPException(status_code=400, detail=f"Unsupported file type. Allowed: {', '.join(allowed_extensions)}") + + try: + # Read file content + file_content = await file.read() + + # Process document + result = await neurax_service.ingest_document( + file_data=file_content, + filename=file.filename, + content_type=file.content_type or "unknown" + ) + + if result['success']: + return DocumentResponse(**result) + else: + raise HTTPException(status_code=500, detail=result['error']) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/", response_model=DocumentListResponse) +async def list_documents( + limit: int = 50, + offset: int = 0, + neurax_service: NeuraXService = Depends(get_neurax_service) +): + """List uploaded documents""" + try: + # TODO: Implement actual document listing + # For now, return mock data + mock_documents = [ + { + 'document_id': f'doc_{i}', + 'filename': f'document_{i}.pdf', + 'status': 'processed', + 'upload_time': time.time() - (i * 3600), # Mock timestamps + 'chunks_count': 10 + i * 5, + 'file_size': 1024 * 1024 * (i + 1) # Mock file sizes + } + for i in range(10) + ] + + total = len(mock_documents) + documents = mock_documents[offset:offset + limit] + + return DocumentListResponse( + documents=documents, + total=total + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/search", response_model=SearchResponse) +async def search_documents( + query: str = Form(...), + limit: int = Form(10), + neurax_service: NeuraXService = Depends(get_neurax_service) +): + """Search through uploaded documents""" + if not query.strip(): + raise HTTPException(status_code=400, detail="Query cannot be empty") + + try: + result = await neurax_service.search_documents(query, limit) + + if result['success']: + return SearchResponse(**result) + else: + raise HTTPException(status_code=500, detail=result['error']) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/{document_id}") +async def delete_document( + document_id: str, + neurax_service: NeuraXService = Depends(get_neurax_service) +): + """Delete a document""" + try: + # TODO: Implement actual document deletion + return { + 'success': True, + 'message': f'Document {document_id} deleted successfully' + } + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/chat", response_model=ChatResponse) +async def chat_with_documents( + request: ChatRequest, + neurax_service: NeuraXService = Depends(get_neurax_service) +): + """Chat with the document knowledge base""" + if not request.message.strip(): + raise HTTPException(status_code=400, detail="Message cannot be empty") + + try: + # Search for relevant context + search_result = await neurax_service.search_documents(request.message, limit=5) + + if not search_result['success']: + raise HTTPException(status_code=500, detail=search_result['error']) + + context = search_result['results'] + + # Generate response + generation_result = await neurax_service.generate_response( + query=request.message, + context=context + ) + + if not generation_result['success']: + raise HTTPException(status_code=500, detail=generation_result['error']) + + # Generate session ID if not provided + session_id = request.session_id or str(uuid.uuid4())[:8] + + return ChatResponse( + success=True, + response=generation_result['response'], + sources=generation_result['sources'] if request.include_sources else [], + session_id=session_id, + generation_time=generation_result['generation_time'] + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/routers/evaluation.py b/backend/routers/evaluation.py index 108bb74..8e69a2c 100644 --- a/backend/routers/evaluation.py +++ b/backend/routers/evaluation.py @@ -14,17 +14,8 @@ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from pydantic import BaseModel, Field -from backend.core.settings import settings -from backend.core.dependencies import get_neurax_service -from backend.services.evaluation_service import get_evaluation_service, EvaluationService -from backend.models.evaluation import ( - EvaluationMetrics, - EvaluationConfig, - EvaluationRun, - TestCase, - TestDataset, - EvaluationStatusEnum, -) +from core.settings import settings +from core.dependencies import get_neurax_service, get_evaluation_service router = APIRouter(prefix="/evaluation", tags=["evaluation"]) @@ -82,9 +73,9 @@ class ExportRequest(BaseModel): format: str = Field(default="json", pattern="^(json|csv|html)$") -# ==================== Dependencies ==================== +# Dependencies -def get_eval_service() -> EvaluationService: +def get_eval_service(): return get_evaluation_service() diff --git a/backend/routers/health.py b/backend/routers/health.py index 8b264d4..872d3e7 100644 --- a/backend/routers/health.py +++ b/backend/routers/health.py @@ -7,8 +7,8 @@ from fastapi import APIRouter, Depends, Request from pydantic import BaseModel -from backend.services.neurax_service import NeuraXService -from backend.utils.dependencies import get_neurax_service +from core.settings import settings +from core.dependencies import get_neurax_service router = APIRouter() diff --git a/backend/services/evaluation_service.py b/backend/services/evaluation_service.py index fefda91..4230b59 100644 --- a/backend/services/evaluation_service.py +++ b/backend/services/evaluation_service.py @@ -19,19 +19,85 @@ from loguru import logger -from backend.core.settings import settings -from backend.core.exceptions import EvaluationError, ErrorCode -from backend.models.evaluation import ( - EvaluationMetrics, - RetrievalMetrics, - GenerationMetrics, - LatencyMetrics, - TestCase, - EvaluationConfig, - EvaluationResult, - EvaluationRun, - EvaluationStatusEnum -) +from core.settings import settings +from typing import Union +from enum import Enum + +# Simplified models since we don't have the actual models yet +class EvaluationStatusEnum(Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + +# Basic model classes (simplified) +class EvaluationMetrics: + def __init__(self): + self.retrieval = None + self.generation = None + self.latency = None + self.overall_quality_score = 0.0 + self.total_test_cases = 0 + +class TestCase: + def __init__(self, query: str, expected_documents: List[str]): + self.query = query + self.expected_documents = expected_documents + +class EvaluationConfig: + def __init__(self, name: str, description: str = None, test_cases: List[TestCase] = None): + self.name = name + self.description = description + self.test_cases = test_cases or [] + self.k_values = [1, 3, 5, 10] + self.similarity_thresholds = [0.5] + self.include_generation_metrics = True + self.include_latency_metrics = True + self.max_concurrent_requests = 5 + +class EvaluationRun: + def __init__(self, run_id: str, config: EvaluationConfig): + self.run_id = run_id + self.name = config.name + self.description = config.description + self.config = config + self.status = EvaluationStatusEnum.PENDING + self.created_at = datetime.utcnow() + self.completed_at = None + self.total_cases = len(config.test_cases) if config.test_cases else 0 + self.metrics = None + self.results = [] + +class LatencyMetrics: + def __init__(self): + self.average_total_latency_ms = 0 + self.p50_total_latency_ms = 0 + self.p90_total_latency_ms = 0 + self.p99_total_latency_ms = 0 + self.average_embedding_latency_ms = 0 + self.average_retrieval_latency_ms = 0 + self.average_generation_latency_ms = 0 + self.latency_buckets = {} + self.error_rate = 0.0 + +class RetrievalMetrics: + def __init__(self): + self.mrr = 0.0 + self.precision_at_k = {} + self.recall_at_k = {} + self.ndcg_at_k = {} + self.average_precision = 0.0 + +class GenerationMetrics: + def __init__(self): + self.grounding_score = 0.0 + self.coherence = 0.0 + self.completeness = 0.0 + self.answer_relevance = 0.0 + +class EvaluationResult: + pass class EvaluationService: @@ -45,7 +111,7 @@ class EvaluationService: """ def __init__(self, storage_dir: Optional[Path] = None): - self.storage_dir = storage_dir or settings.paths.data_dir / "evaluations" + self.storage_dir = storage_dir or Path("./evaluations") self.storage_dir.mkdir(parents=True, exist_ok=True) # In-memory tracking @@ -59,7 +125,7 @@ def __init__(self, storage_dir: Optional[Path] = None): # Sampling control self._sample_count = 0 - self._sample_rate = settings.evaluation.sample_rate + self._sample_rate = 0.1 # Default sample rate self.logger = logger.bind(component="EvaluationService") @@ -67,11 +133,9 @@ def __init__(self, storage_dir: Optional[Path] = None): def should_sample(self) -> bool: """Determine if current request should be sampled""" - if not settings.evaluation.enabled: - return False - + # Always sample for now self._sample_count += 1 - return (self._sample_count % int(1 / self._sample_rate)) == 0 + return (self._sample_count % 10) == 0 # 10% sample rate def track_latency( self, @@ -81,9 +145,6 @@ def track_latency( generation_ms: Optional[float] = None ) -> None: """Track latency metrics""" - if not settings.evaluation.track_latency_metrics: - return - sample = { "timestamp": datetime.utcnow().isoformat(), "total_ms": total_ms, @@ -107,9 +168,6 @@ def track_retrieval( similarity_scores: Optional[List[float]] = None ) -> None: """Track retrieval metrics""" - if not settings.evaluation.track_retrieval_metrics: - return - sample = { "timestamp": datetime.utcnow().isoformat(), "query": query[:200], # Truncate @@ -134,7 +192,20 @@ def track_generation( confidence: Optional[float] = None ) -> None: """Track generation metrics""" - if not settings.evaluation.track_generation_metrics: + sample = { + "timestamp": datetime.utcnow().isoformat(), + "query": query[:200], + "response_length": len(response), + "context_used": context_used, + "grounding_score": grounding_score, + "confidence": confidence + } + + self._generation_samples.append(sample) + + max_samples = 3000 + if len(self._generation_samples) > max_samples: + self._generation_samples = self._generation_samples[-max_samples:] return sample = { diff --git a/backend/services/neurax_service.py b/backend/services/neurax_service.py new file mode 100644 index 0000000..20d60aa --- /dev/null +++ b/backend/services/neurax_service.py @@ -0,0 +1,231 @@ +""" +NeuraX Service - Main orchestrator service for the RAG system +""" + +import time +import asyncio +from typing import Dict, Any, Optional, List +from datetime import datetime + +from core.settings import settings +from loguru import logger + + +class NeuraXService: + """Main service for NeuraX RAG system""" + + def __init__(self): + self.logger = logger.bind(service="NeuraXService") + self.start_time = time.time() + self.initialized = False + self.component_status = {} + self.component_errors = [] + self.overall_status = "initializing" + + # Core components (will be initialized when main system starts) + self.ingestion_manager = None + self.embedding_manager = None + self.vector_store = None + self.query_processor = None + self.llm_generator = None + self.kg_manager = None + self.feedback_system = None + + self.logger.info("NeuraX Service initialized") + + async def initialize(self): + """Initialize the service and all components""" + try: + self.logger.info("Initializing NeuraX Service...") + + # Initialize component status + self.component_status = { + 'ingestion_manager': False, + 'embedding_manager': False, + 'vector_store': False, + 'query_processor': False, + 'llm_generator': False, + 'kg_manager': False, + 'feedback_system': False + } + + self.overall_status = "healthy" + self.initialized = True + self.logger.info("NeuraX Service initialized successfully") + + except Exception as e: + self.logger.error(f"Failed to initialize NeuraX Service: {e}") + self.overall_status = "error" + self.component_errors.append(str(e)) + raise + + async def get_health_status(self) -> Dict[str, Any]: + """Get comprehensive health status""" + if not self.initialized: + await self.initialize() + + uptime = time.time() - self.start_time + + return { + 'overall_status': self.overall_status, + 'uptime': uptime, + 'initialized': self.initialized, + 'component_status': self.component_status, + 'component_errors': self.component_errors, + 'timestamp': time.time() + } + + async def get_health_summary(self) -> Dict[str, Any]: + """Get simplified health summary""" + health_status = await self.get_health_status() + + # Count healthy vs unhealthy components + total_components = len(health_status['component_status']) + healthy_components = sum(1 for status in health_status['component_status'].values() if status) + + return { + 'healthy': self.overall_status == "healthy", + 'total_components': total_components, + 'healthy_components': healthy_components, + 'uptime': health_status['uptime'] + } + + async def ingest_document(self, file_data: bytes, filename: str, content_type: str) -> Dict[str, Any]: + """Ingest a document for processing""" + try: + self.logger.info(f"Ingesting document: {filename}") + + # TODO: Implement actual ingestion logic + # For now, return a mock response + await asyncio.sleep(1) # Simulate processing time + + return { + 'success': True, + 'document_id': f"doc_{int(time.time())}", + 'filename': filename, + 'status': 'processed', + 'chunks_created': 5, + 'processing_time': 1.0 + } + + except Exception as e: + self.logger.error(f"Failed to ingest document {filename}: {e}") + return { + 'success': False, + 'error': str(e), + 'filename': filename + } + + async def search_documents(self, query: str, limit: int = 10) -> Dict[str, Any]: + """Search documents using the RAG system""" + try: + self.logger.info(f"Searching documents for query: {query}") + + # TODO: Implement actual search logic + await asyncio.sleep(0.5) # Simulate search time + + # Mock search results + mock_results = [ + { + 'content': f'Related content about {query} from document 1...', + 'source': 'document1.pdf', + 'similarity': 0.95, + 'chunk_id': 'chunk_1' + }, + { + 'content': f'Additional information about {query} from document 2...', + 'source': 'document2.pdf', + 'similarity': 0.87, + 'chunk_id': 'chunk_2' + } + ] + + return { + 'success': True, + 'query': query, + 'results': mock_results[:limit], + 'total_results': len(mock_results), + 'search_time': 0.5 + } + + except Exception as e: + self.logger.error(f"Search failed for query '{query}': {e}") + return { + 'success': False, + 'error': str(e), + 'query': query + } + + async def generate_response(self, query: str, context: List[Dict[str, Any]]) -> Dict[str, Any]: + """Generate a response using the LLM""" + try: + self.logger.info(f"Generating response for query: {query}") + + # TODO: Implement actual LLM generation + await asyncio.sleep(2) # Simulate generation time + + response = f""" +Based on the provided documents, here's what I found about '{query}': + +The documents indicate that [query] is an important topic that requires careful consideration. +The information shows various aspects and perspectives that are relevant to understanding this subject. + +**Key Points:** +1. First important point from the documents +2. Second relevant detail from the sources +3. Third key information found + +**Sources:** +1. document1.pdf (95% relevance) +2. document2.pdf (87% relevance) + +This response was generated based on the content of {len(context)} relevant document chunks. + """.strip() + + return { + 'success': True, + 'response': response, + 'sources': [ + { + 'source': doc['source'], + 'similarity': doc['similarity'], + 'chunk_id': doc['chunk_id'] + } for doc in context + ], + 'generation_time': 2.0 + } + + except Exception as e: + self.logger.error(f"Response generation failed for query '{query}': {e}") + return { + 'success': False, + 'error': str(e), + 'query': query + } + + async def get_system_metrics(self) -> Dict[str, Any]: + """Get system performance metrics""" + return { + 'uptime': time.time() - self.start_time, + 'documents_processed': 0, # TODO: Track actual metrics + 'queries_processed': 0, + 'average_response_time': 0.0, + 'system_load': 0.25, + 'memory_usage': 45.6, # Mock percentage + 'disk_usage': 23.4 # Mock percentage + } + + async def health_check_component(self, component_name: str) -> bool: + """Check health of a specific component""" + return self.component_status.get(component_name, False) + + def set_component_status(self, component_name: str, status: bool, error: Optional[str] = None): + """Set the status of a component""" + self.component_status[component_name] = status + if error: + self.component_errors.append(f"{component_name}: {error}") + self.logger.error(f"Component {component_name} error: {error}") + elif status: + self.logger.info(f"Component {component_name} is now healthy") + else: + self.logger.warning(f"Component {component_name} is now unhealthy") \ No newline at end of file diff --git a/backend/simple_main.py b/backend/simple_main.py new file mode 100644 index 0000000..96a7485 --- /dev/null +++ b/backend/simple_main.py @@ -0,0 +1,37 @@ +""" +Minimal FastAPI application for NeuraX +""" + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +app = FastAPI(title="NeuraX Backend", version="2.0.0") + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Health check endpoint +@app.get("/health") +async def health_check(): + return {"status": "healthy", "message": "NeuraX backend is running"} + +# API info endpoint +@app.get("/") +async def root(): + return { + "message": "NeuraX Backend API", + "version": "2.0.0", + "status": "running", + "docs": "/docs" + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/frontend/.env.local b/frontend/.env.local new file mode 100644 index 0000000..fdaedb8 --- /dev/null +++ b/frontend/.env.local @@ -0,0 +1,5 @@ +# Environment variables for Next.js +NEXT_PUBLIC_API_URL=http://localhost:8000 + +# Optional: For development +NODE_ENV=development \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..0cdfc00 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "rules": { + "react/no-unescaped-entities": "off", + "@next/next/no-page-custom-font": "off" + } +} \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..07c205e --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,14 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + appDir: true, + }, + images: { + domains: ['localhost'], + }, + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000', + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b4e1749 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,49 @@ +{ + "name": "neurax-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "14.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^5.0.0", + "tailwindcss": "^3.3.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.24", + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "axios": "^1.6.0", + "clsx": "^2.0.0", + "lucide-react": "^0.294.0", + "react-hook-form": "^7.48.0", + "react-dropzone": "^14.2.3", + "framer-motion": "^10.16.0", + "zustand": "^4.4.0", + "date-fns": "^2.30.0", + "recharts": "^2.8.0", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-scroll-area": "^1.0.5" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", + "eslint": "^8.0.0", + "eslint-config-next": "14.0.0", + "prettier": "^3.0.0", + "prettier-plugin-tailwindcss": "^0.5.0" + } +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..96bb01e --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..f0bff0c --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,29 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import '@/styles/globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'NeuraX - Offline Multimodal RAG System', + description: 'Secure, air-gapped document intelligence with advanced multimodal capabilities', + keywords: ['RAG', 'AI', 'Document Intelligence', 'Offline', 'Multimodal'], + authors: [{ name: 'NeuraX Team' }], + viewport: 'width=device-width, initial-scale=1', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + +
+ {children} +
+ + + ) +} \ No newline at end of file diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..9cc0afd --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,319 @@ +'use client'; + +import React, { useState } from 'react'; +import { AppProvider, useApp } from '@/hooks/useApp'; +import Navigation, { DashboardMetrics, SystemHealth } from '@/components/Navigation'; +import FileUpload, { DocumentList, SearchResults } from '@/components/FileUpload'; +import ChatInterface from '@/components/ChatInterface'; +import { motion, AnimatePresence } from 'framer-motion'; + +export default function HomePage() { + const [currentTab, setCurrentTab] = useState('dashboard'); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + + return ( + +
+ {/* Navigation */} + + + {/* Main Content */} +
+ + {currentTab === 'dashboard' && ( + + + + )} + + {currentTab === 'documents' && ( + + + + )} + + {currentTab === 'search' && ( + + + + )} + + {currentTab === 'chat' && ( + + + + )} + + {currentTab === 'metrics' && ( + + + + )} + +
+ + {/* Toast notifications */} + +
+
+ ); +} + +// Dashboard Content Component +function DashboardContent() { + const { state } = useApp(); + + return ( +
+
+

Dashboard

+

+ Overview of your NeuraX system status and activity +

+
+ + {/* Metrics Cards */} + + + {/* System Health and Recent Activity */} +
+ + +
+
+ ); +} + +// Documents Content Component +function DocumentsContent() { + const { state, actions } = useApp(); + + return ( +
+
+

Documents

+

+ Upload, manage, and organize your documents +

+
+ +
+ {/* Upload Section */} +
+
+

Upload Documents

+ +
+
+ + {/* Documents List */} +
+
+
+

Document Library

+ + {state.documents.length} document{state.documents.length !== 1 ? 's' : ''} + +
+ +
+
+
+
+ ); +} + +// Search Content Component +interface SearchContentProps { + query: string; + results: any[]; + onQueryChange: (query: string) => void; + onResultsChange: (results: any[]) => void; +} + +function SearchContent({ query, results, onQueryChange, onResultsChange }: SearchContentProps) { + const { state, actions } = useApp(); + const [localQuery, setLocalQuery] = useState(query); + + const handleSearch = async (searchQuery: string) => { + if (!searchQuery.trim()) return; + + onQueryChange(searchQuery); + const response = await actions.searchDocuments(searchQuery); + if (response) { + onResultsChange(response.results); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSearch(localQuery); + }; + + return ( +
+
+

Search

+

+ Search through your uploaded documents +

+
+ + {/* Search Form */} +
+
+
+ +
+ setLocalQuery(e.target.value)} + placeholder="Enter your search query..." + className="flex-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500" + /> + +
+
+
+
+ + {/* Search Results */} + {results.length > 0 && ( +
+ +
+ )} +
+ ); +} + +// Chat Content Component +function ChatContent() { + const { state, actions } = useApp(); + + const handleSendMessage = async (message: string) => { + await actions.sendMessage(message, state.currentSession); + }; + + const handleNewSession = () => { + actions.createNewSession(); + }; + + return ( +
+ +
+ ); +} + +// Metrics Content Component +function MetricsContent() { + const { state } = useApp(); + + return ( +
+
+

Metrics

+

+ System performance and evaluation metrics +

+
+ + {/* Coming Soon */} +
+
+ + + +
+

Metrics Dashboard

+

+ Detailed performance metrics and evaluation data will be available here. + This feature is currently under development. +

+
+
+ ); +} + +// Recent Activity Component +function RecentActivity() { + const { state } = useApp(); + + return ( +
+

Recent Activity

+
+ {state.chatHistory.slice(-3).reverse().map((message) => ( +
+
+
+ AI +
+
+
+

+ {message.message} +

+

+ {new Date(message.timestamp).toLocaleString()} +

+
+
+ ))} + + {state.chatHistory.length === 0 && ( +

+ No recent activity +

+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx new file mode 100644 index 0000000..4e6e567 --- /dev/null +++ b/frontend/src/components/ChatInterface.tsx @@ -0,0 +1,253 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { PaperAirplaneIcon, StopCircleIcon, PlusIcon } from '@heroicons/react/24/outline'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useApp } from '@/hooks/useApp'; +import { ChatMessage } from '@/types/api'; +import { formatDistanceToNow } from 'date-fns'; + +interface ChatInterfaceProps { + messages: ChatMessage[]; + isLoading: boolean; + onSendMessage: (message: string) => Promise; + onNewSession: () => void; +} + +export default function ChatInterface({ + messages, + isLoading, + onSendMessage, + onNewSession +}: ChatInterfaceProps) { + const [inputMessage, setInputMessage] = useState(''); + const [isSending, setIsSending] = useState(false); + const messagesEndRef = useRef(null); + const textareaRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!inputMessage.trim() || isSending) return; + + const message = inputMessage.trim(); + setInputMessage(''); + setIsSending(true); + + try { + await onSendMessage(message); + } catch (error) { + console.error('Failed to send message:', error); + } finally { + setIsSending(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + const adjustTextareaHeight = () => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`; + } + }; + + useEffect(() => { + adjustTextareaHeight(); + }, [inputMessage]); + + return ( +
+ {/* Header */} +
+
+
+
+ AI +
+
+
+

NeuraX Chat

+

Ask questions about your documents

+
+
+ +
+ + {/* Messages */} +
+ + {messages.length === 0 ? ( + +
+ + + +
+

Start a conversation

+

+ Upload documents and ask questions to get AI-powered insights with citations +

+
+ ) : ( + messages.map((message, index) => ( + + {/* User Message */} +
+
+
+

{message.message}

+
+

+ {formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })} +

+
+
+ + {/* AI Response */} +
+
+
+
+ AI +
+
+
+
+

+ {message.response} +

+
+ + {/* Sources */} + {message.sources && message.sources.length > 0 && ( +
+

Sources:

+
+ {message.sources.map((source, sourceIndex) => ( +
+ + {Math.round(source.similarity * 100)}% + + {source.source} +
+ ))} +
+
+ )} + + {/* Generation Time */} +
+ Generated in {message.generation_time.toFixed(1)}s + {formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })} +
+
+
+
+
+ )) + )} +
+ + {/* Loading indicator */} + {(isLoading || isSending) && ( + +
+
+
+ AI +
+
+
+
+
+
+
+
+
+ + {isSending ? 'Thinking...' : 'Generating response...'} + +
+
+
+
+ )} + +
+
+ + {/* Input Form */} +
+
+
+