""" Voice Service - PersonaPlex + TaskOrchestrator Integration Voice-First Interface fuer Breakpilot DSGVO-konform: - Keine Audio-Persistenz (nur RAM) - Namespace-Verschluesselung (Key nur auf Lehrergeraet) - TTL-basierte Auto-Loeschung Main FastAPI Application """ import structlog from contextlib import asynccontextmanager from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse import time from typing import Dict from config import settings # Configure structured logging structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() if not settings.is_development else structlog.dev.ConsoleRenderer(), ], wrapper_class=structlog.stdlib.BoundLogger, context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) logger = structlog.get_logger(__name__) # Active WebSocket connections (transient, not persisted) active_connections: Dict[str, WebSocket] = {} @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager.""" # Startup logger.info( "Starting Voice Service", environment=settings.environment, port=settings.port, personaplex_enabled=settings.personaplex_enabled, orchestrator_enabled=settings.orchestrator_enabled, audio_persistence=settings.audio_persistence, ) # Verify DSGVO compliance settings if settings.audio_persistence: logger.error("DSGVO VIOLATION: Audio persistence is enabled!") raise RuntimeError("Audio persistence must be disabled for DSGVO compliance") # Initialize services from services.task_orchestrator import TaskOrchestrator from services.encryption_service import EncryptionService app.state.orchestrator = TaskOrchestrator() app.state.encryption = EncryptionService() logger.info("Voice Service initialized successfully") yield # Shutdown logger.info("Shutting down Voice Service") # Clear all active connections for session_id in list(active_connections.keys()): try: await active_connections[session_id].close() except Exception: pass active_connections.clear() logger.info("Voice Service shutdown complete") # Create FastAPI app app = FastAPI( title="Breakpilot Voice Service", description="Voice-First Interface mit PersonaPlex-7B und Task-Orchestrierung", version="1.0.0", docs_url="/docs" if settings.is_development else None, redoc_url="/redoc" if settings.is_development else None, lifespan=lifespan, ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Request timing middleware @app.middleware("http") async def add_timing_header(request: Request, call_next): """Add X-Process-Time header to all responses.""" start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response # Import and register routers from api.sessions import router as sessions_router from api.streaming import router as streaming_router from api.tasks import router as tasks_router from api.bqas import router as bqas_router app.include_router(sessions_router, prefix="/api/v1/sessions", tags=["Sessions"]) app.include_router(tasks_router, prefix="/api/v1/tasks", tags=["Tasks"]) app.include_router(bqas_router, prefix="/api/v1/bqas", tags=["BQAS"]) # Note: streaming router is mounted at root level for WebSocket app.include_router(streaming_router, tags=["Streaming"]) # Health check endpoint @app.get("/health", tags=["System"]) async def health_check(): """ Health check endpoint for Docker/Kubernetes probes. Returns service status and DSGVO compliance verification. """ return { "status": "healthy", "service": "voice-service", "version": "1.0.0", "environment": settings.environment, "dsgvo_compliance": { "audio_persistence": settings.audio_persistence, "encryption_enabled": settings.encryption_enabled, "transcript_ttl_days": settings.transcript_ttl_days, "audit_log_ttl_days": settings.audit_log_ttl_days, }, "backends": { "personaplex_enabled": settings.personaplex_enabled, "orchestrator_enabled": settings.orchestrator_enabled, "fallback_llm": settings.fallback_llm_provider, }, "audio_config": { "sample_rate": settings.audio_sample_rate, "frame_size_ms": settings.audio_frame_size_ms, }, "active_connections": len(active_connections), } # Root endpoint @app.get("/", tags=["System"]) async def root(): """Root endpoint with service information.""" return { "service": "Breakpilot Voice Service", "description": "Voice-First Interface fuer Breakpilot", "version": "1.0.0", "docs": "/docs" if settings.is_development else "disabled", "endpoints": { "sessions": "/api/v1/sessions", "tasks": "/api/v1/tasks", "websocket": "/ws/voice", }, "privacy": { "audio_stored": False, "transcripts_encrypted": True, "data_retention": f"{settings.transcript_ttl_days} days", }, } # Error handlers @app.exception_handler(404) async def not_found_handler(request: Request, exc): """Handle 404 errors - preserve HTTPException details.""" from fastapi import HTTPException # If this is an HTTPException with a detail, use that if isinstance(exc, HTTPException) and exc.detail: return JSONResponse( status_code=404, content={"detail": exc.detail}, ) # Generic 404 for route not found return JSONResponse( status_code=404, content={"error": "Not found", "path": str(request.url.path)}, ) @app.exception_handler(500) async def internal_error_handler(request: Request, exc): """Handle 500 errors.""" logger.error("Internal server error", path=str(request.url.path), error=str(exc)) return JSONResponse( status_code=500, content={"error": "Internal server error"}, ) if __name__ == "__main__": import uvicorn uvicorn.run( "main:app", host="0.0.0.0", port=settings.port, reload=settings.is_development, )