Files
Benjamin Admin 9912997187
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 25s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m55s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 18s
refactor: Jitsi/Matrix/Voice von Core übernommen, Camunda/BPMN gelöscht, Kommunikation-Nav
- Voice-Service von Core nach Lehrer verschoben (bp-lehrer-voice-service)
- 4 Jitsi-Services + 2 Synapse-Services in docker-compose.yml aufgenommen
- Camunda komplett gelöscht: workflow pages, workflow-config.ts, bpmn-js deps
- CAMUNDA_URL aus backend-lehrer environment entfernt
- Sidebar: Kategorie "Compliance SDK" + "Katalogverwaltung" entfernt
- Sidebar: Neue Kategorie "Kommunikation" mit Video & Chat, Voice Service, Alerts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:01:47 +01:00

226 lines
6.9 KiB
Python

"""
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,
)