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
- 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>
226 lines
6.9 KiB
Python
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,
|
|
)
|