Spaces:
Paused
Paused
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
Flare β Main Application (Refactored)
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
# FastAPI imports
|
| 6 |
from fastapi import FastAPI, WebSocket, Request, status
|
|
@@ -17,15 +17,12 @@ import mimetypes
|
|
| 17 |
import uuid
|
| 18 |
import traceback
|
| 19 |
from datetime import datetime
|
|
|
|
|
|
|
| 20 |
from pydantic import ValidationError
|
| 21 |
from dotenv import load_dotenv
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
from routes.websocket_handler import websocket_endpoint
|
| 25 |
-
from routes.admin_routes import router as admin_router, start_cleanup_task
|
| 26 |
-
from llm.llm_startup import run_in_thread
|
| 27 |
-
from session import session_store, start_session_cleanup
|
| 28 |
-
from config.config_provider import ConfigProvider
|
| 29 |
from event_bus import event_bus
|
| 30 |
from state_orchestrator import StateOrchestrator
|
| 31 |
from websocket_manager import WebSocketManager
|
|
@@ -35,19 +32,26 @@ from tts_lifecycle_manager import TTSLifecycleManager
|
|
| 35 |
from llm_manager import LLMManager
|
| 36 |
from audio_buffer_manager import AudioBufferManager
|
| 37 |
|
| 38 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
from utils.logger import log_error, log_info, log_warning
|
| 40 |
|
| 41 |
# Exception imports
|
| 42 |
from utils.exceptions import (
|
| 43 |
DuplicateResourceError,
|
| 44 |
RaceConditionError,
|
| 45 |
-
ValidationError,
|
| 46 |
ResourceNotFoundError,
|
| 47 |
AuthenticationError,
|
| 48 |
AuthorizationError,
|
| 49 |
ConfigurationError,
|
| 50 |
-
get_http_status_code
|
|
|
|
| 51 |
)
|
| 52 |
|
| 53 |
# Load .env file if exists
|
|
@@ -148,9 +152,71 @@ async def add_request_id(request: Request, call_next):
|
|
| 148 |
)
|
| 149 |
raise
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
# ---------------- Core chat/session routes --------------------------
|
| 156 |
from routes.chat_handler import router as chat_router
|
|
@@ -163,8 +229,59 @@ app.include_router(audio_router, prefix="/api")
|
|
| 163 |
# ---------------- Admin API routes ----------------------------------
|
| 164 |
app.include_router(admin_router, prefix="/api/admin")
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
# ---------------- Exception Handlers ----------------------------------
|
| 167 |
-
# Add global exception handler
|
| 168 |
@app.exception_handler(Exception)
|
| 169 |
async def global_exception_handler(request: Request, exc: Exception):
|
| 170 |
"""Handle all unhandled exceptions"""
|
|
@@ -184,7 +301,13 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
| 184 |
# Special handling for FlareExceptions
|
| 185 |
if isinstance(exc, FlareException):
|
| 186 |
status_code = get_http_status_code(exc)
|
| 187 |
-
response_body =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
# Special message for race conditions
|
| 190 |
if isinstance(exc, RaceConditionError):
|
|
@@ -228,8 +351,8 @@ async def race_condition_handler(request: Request, exc: RaceConditionError):
|
|
| 228 |
content=exc.to_http_detail()
|
| 229 |
)
|
| 230 |
|
| 231 |
-
@app.exception_handler(
|
| 232 |
-
async def validation_error_handler(request: Request, exc:
|
| 233 |
"""Handle validation errors"""
|
| 234 |
return JSONResponse(
|
| 235 |
status_code=422,
|
|
@@ -290,7 +413,7 @@ async def configuration_error_handler(request: Request, exc: ConfigurationError)
|
|
| 290 |
# ---------------- Metrics endpoint -----------------
|
| 291 |
@app.get("/metrics")
|
| 292 |
async def get_metrics():
|
| 293 |
-
"""Get system metrics"""
|
| 294 |
import psutil
|
| 295 |
import gc
|
| 296 |
|
|
@@ -300,6 +423,23 @@ async def get_metrics():
|
|
| 300 |
|
| 301 |
# Session stats
|
| 302 |
session_stats = session_store.get_session_stats()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
metrics = {
|
| 305 |
"memory": {
|
|
@@ -312,6 +452,7 @@ async def get_metrics():
|
|
| 312 |
"num_threads": process.num_threads()
|
| 313 |
},
|
| 314 |
"sessions": session_stats,
|
|
|
|
| 315 |
"gc": {
|
| 316 |
"collections": gc.get_count(),
|
| 317 |
"objects": len(gc.get_objects())
|
|
@@ -325,18 +466,26 @@ async def get_metrics():
|
|
| 325 |
@app.get("/api/health")
|
| 326 |
def health_check():
|
| 327 |
"""Health check endpoint - moved to /api/health"""
|
|
|
|
|
|
|
|
|
|
| 328 |
return {
|
| 329 |
-
"status": "ok",
|
| 330 |
"version": "2.0.0",
|
| 331 |
"timestamp": datetime.utcnow().isoformat(),
|
| 332 |
-
"environment": os.getenv("ENVIRONMENT", "development")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
}
|
| 334 |
|
| 335 |
-
# ---------------- WebSocket route for real-time STT ------------------
|
| 336 |
-
@app.websocket("/ws/conversation/{session_id}")
|
| 337 |
-
async def conversation_websocket(websocket: WebSocket, session_id: str):
|
| 338 |
-
await websocket_endpoint(websocket, session_id)
|
| 339 |
-
|
| 340 |
# ---------------- Serve static files ------------------------------------
|
| 341 |
# UI static files (production build)
|
| 342 |
static_path = Path(__file__).parent / "static"
|
|
@@ -393,4 +542,4 @@ else:
|
|
| 393 |
|
| 394 |
if __name__ == "__main__":
|
| 395 |
log_info("π Starting Flare backend on port 7860...")
|
| 396 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Flare β Main Application (Refactored with Event-Driven Architecture)
|
| 3 |
+
====================================================================
|
| 4 |
"""
|
| 5 |
# FastAPI imports
|
| 6 |
from fastapi import FastAPI, WebSocket, Request, status
|
|
|
|
| 17 |
import uuid
|
| 18 |
import traceback
|
| 19 |
from datetime import datetime
|
| 20 |
+
import asyncio
|
| 21 |
+
import time
|
| 22 |
from pydantic import ValidationError
|
| 23 |
from dotenv import load_dotenv
|
| 24 |
|
| 25 |
+
# Event-driven architecture imports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
from event_bus import event_bus
|
| 27 |
from state_orchestrator import StateOrchestrator
|
| 28 |
from websocket_manager import WebSocketManager
|
|
|
|
| 32 |
from llm_manager import LLMManager
|
| 33 |
from audio_buffer_manager import AudioBufferManager
|
| 34 |
|
| 35 |
+
# Project imports
|
| 36 |
+
from routes.admin_routes import router as admin_router, start_cleanup_task
|
| 37 |
+
from llm.llm_startup import run_in_thread
|
| 38 |
+
from session import session_store, start_session_cleanup
|
| 39 |
+
from config.config_provider import ConfigProvider
|
| 40 |
+
|
| 41 |
+
# Logger imports
|
| 42 |
from utils.logger import log_error, log_info, log_warning
|
| 43 |
|
| 44 |
# Exception imports
|
| 45 |
from utils.exceptions import (
|
| 46 |
DuplicateResourceError,
|
| 47 |
RaceConditionError,
|
| 48 |
+
ValidationError as FlareValidationError,
|
| 49 |
ResourceNotFoundError,
|
| 50 |
AuthenticationError,
|
| 51 |
AuthorizationError,
|
| 52 |
ConfigurationError,
|
| 53 |
+
get_http_status_code,
|
| 54 |
+
FlareException
|
| 55 |
)
|
| 56 |
|
| 57 |
# Load .env file if exists
|
|
|
|
| 152 |
)
|
| 153 |
raise
|
| 154 |
|
| 155 |
+
# ===================== Event-Driven Architecture Initialization =====================
|
| 156 |
+
@app.on_event("startup")
|
| 157 |
+
async def startup_event():
|
| 158 |
+
"""Initialize event-driven components on startup"""
|
| 159 |
+
try:
|
| 160 |
+
# Initialize event bus
|
| 161 |
+
await event_bus.start()
|
| 162 |
+
log_info("β
Event bus started")
|
| 163 |
+
|
| 164 |
+
# Initialize resource manager
|
| 165 |
+
resource_manager = ResourceManager(event_bus)
|
| 166 |
+
await resource_manager.start()
|
| 167 |
+
log_info("β
Resource manager started")
|
| 168 |
+
|
| 169 |
+
# Initialize managers
|
| 170 |
+
state_orchestrator = StateOrchestrator(event_bus)
|
| 171 |
+
websocket_manager = WebSocketManager(event_bus)
|
| 172 |
+
audio_buffer_manager = AudioBufferManager(event_bus)
|
| 173 |
+
stt_manager = STTLifecycleManager(event_bus, resource_manager)
|
| 174 |
+
tts_manager = TTSLifecycleManager(event_bus, resource_manager)
|
| 175 |
+
llm_manager = LLMManager(event_bus, resource_manager)
|
| 176 |
+
|
| 177 |
+
# Store in app state for access in routes
|
| 178 |
+
app.state.event_bus = event_bus
|
| 179 |
+
app.state.resource_manager = resource_manager
|
| 180 |
+
app.state.state_orchestrator = state_orchestrator
|
| 181 |
+
app.state.websocket_manager = websocket_manager
|
| 182 |
+
app.state.audio_buffer_manager = audio_buffer_manager
|
| 183 |
+
app.state.stt_manager = stt_manager
|
| 184 |
+
app.state.tts_manager = tts_manager
|
| 185 |
+
app.state.llm_manager = llm_manager
|
| 186 |
+
|
| 187 |
+
log_info("β
All managers initialized")
|
| 188 |
+
|
| 189 |
+
# Start existing background tasks
|
| 190 |
+
run_in_thread() # Start LLM startup notifier if needed
|
| 191 |
+
start_cleanup_task() # Activity log cleanup
|
| 192 |
+
start_session_cleanup() # Session cleanup
|
| 193 |
+
|
| 194 |
+
log_info("β
Background tasks started")
|
| 195 |
+
|
| 196 |
+
except Exception as e:
|
| 197 |
+
log_error("β Failed to start event-driven components", error=str(e), traceback=traceback.format_exc())
|
| 198 |
+
raise
|
| 199 |
+
|
| 200 |
+
@app.on_event("shutdown")
|
| 201 |
+
async def shutdown_event():
|
| 202 |
+
"""Cleanup event-driven components on shutdown"""
|
| 203 |
+
try:
|
| 204 |
+
# Stop event bus
|
| 205 |
+
await event_bus.stop()
|
| 206 |
+
log_info("β
Event bus stopped")
|
| 207 |
+
|
| 208 |
+
# Stop resource manager
|
| 209 |
+
if hasattr(app.state, 'resource_manager'):
|
| 210 |
+
await app.state.resource_manager.stop()
|
| 211 |
+
log_info("β
Resource manager stopped")
|
| 212 |
+
|
| 213 |
+
# Close all WebSocket connections
|
| 214 |
+
if hasattr(app.state, 'websocket_manager'):
|
| 215 |
+
await app.state.websocket_manager.close_all_connections()
|
| 216 |
+
log_info("β
All WebSocket connections closed")
|
| 217 |
+
|
| 218 |
+
except Exception as e:
|
| 219 |
+
log_error("β Error during shutdown", error=str(e))
|
| 220 |
|
| 221 |
# ---------------- Core chat/session routes --------------------------
|
| 222 |
from routes.chat_handler import router as chat_router
|
|
|
|
| 229 |
# ---------------- Admin API routes ----------------------------------
|
| 230 |
app.include_router(admin_router, prefix="/api/admin")
|
| 231 |
|
| 232 |
+
# ---------------- WebSocket route for real-time chat ------------------
|
| 233 |
+
@app.websocket("/ws/conversation/{session_id}")
|
| 234 |
+
async def websocket_route(websocket: WebSocket, session_id: str):
|
| 235 |
+
"""Handle WebSocket connections using the new WebSocketManager"""
|
| 236 |
+
if hasattr(app.state, 'websocket_manager'):
|
| 237 |
+
await app.state.websocket_manager.handle_connection(websocket, session_id)
|
| 238 |
+
else:
|
| 239 |
+
log_error("WebSocketManager not initialized")
|
| 240 |
+
await websocket.close(code=1011, reason="Server not ready")
|
| 241 |
+
|
| 242 |
+
# ---------------- Test endpoint for event-driven flow ------------------
|
| 243 |
+
@app.post("/api/test/realtime")
|
| 244 |
+
async def test_realtime():
|
| 245 |
+
"""Test endpoint for event-driven realtime flow"""
|
| 246 |
+
from event_bus import Event, EventType
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
# Create a test session
|
| 250 |
+
session = session_store.create_session(
|
| 251 |
+
project_name="kronos_jet",
|
| 252 |
+
version_no=1,
|
| 253 |
+
is_realtime=True
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
# Get version config
|
| 257 |
+
cfg = ConfigProvider.get()
|
| 258 |
+
project = next((p for p in cfg.projects if p.name == "kronos_jet"), None)
|
| 259 |
+
if project:
|
| 260 |
+
version = next((v for v in project.versions if v.no == 1), None)
|
| 261 |
+
if version:
|
| 262 |
+
session.set_version_config(version)
|
| 263 |
+
|
| 264 |
+
# Publish session started event
|
| 265 |
+
await app.state.event_bus.publish(Event(
|
| 266 |
+
type=EventType.SESSION_STARTED,
|
| 267 |
+
session_id=session.session_id,
|
| 268 |
+
data={
|
| 269 |
+
"session": session,
|
| 270 |
+
"has_welcome": bool(version and version.welcome_prompt),
|
| 271 |
+
"welcome_text": version.welcome_prompt if version and version.welcome_prompt else "HoΕ geldiniz!"
|
| 272 |
+
}
|
| 273 |
+
))
|
| 274 |
+
|
| 275 |
+
return {
|
| 276 |
+
"session_id": session.session_id,
|
| 277 |
+
"message": "Test session created. Connect via WebSocket to continue."
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
except Exception as e:
|
| 281 |
+
log_error("Test endpoint error", error=str(e))
|
| 282 |
+
raise HTTPException(500, f"Test failed: {str(e)}")
|
| 283 |
+
|
| 284 |
# ---------------- Exception Handlers ----------------------------------
|
|
|
|
| 285 |
@app.exception_handler(Exception)
|
| 286 |
async def global_exception_handler(request: Request, exc: Exception):
|
| 287 |
"""Handle all unhandled exceptions"""
|
|
|
|
| 301 |
# Special handling for FlareExceptions
|
| 302 |
if isinstance(exc, FlareException):
|
| 303 |
status_code = get_http_status_code(exc)
|
| 304 |
+
response_body = {
|
| 305 |
+
"error": type(exc).__name__,
|
| 306 |
+
"message": str(exc),
|
| 307 |
+
"request_id": request_id,
|
| 308 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 309 |
+
"details": getattr(exc, 'details', {})
|
| 310 |
+
}
|
| 311 |
|
| 312 |
# Special message for race conditions
|
| 313 |
if isinstance(exc, RaceConditionError):
|
|
|
|
| 351 |
content=exc.to_http_detail()
|
| 352 |
)
|
| 353 |
|
| 354 |
+
@app.exception_handler(FlareValidationError)
|
| 355 |
+
async def validation_error_handler(request: Request, exc: FlareValidationError):
|
| 356 |
"""Handle validation errors"""
|
| 357 |
return JSONResponse(
|
| 358 |
status_code=422,
|
|
|
|
| 413 |
# ---------------- Metrics endpoint -----------------
|
| 414 |
@app.get("/metrics")
|
| 415 |
async def get_metrics():
|
| 416 |
+
"""Get system metrics including event-driven components"""
|
| 417 |
import psutil
|
| 418 |
import gc
|
| 419 |
|
|
|
|
| 423 |
|
| 424 |
# Session stats
|
| 425 |
session_stats = session_store.get_session_stats()
|
| 426 |
+
|
| 427 |
+
# Event-driven component stats
|
| 428 |
+
event_stats = {}
|
| 429 |
+
if hasattr(app.state, 'stt_manager'):
|
| 430 |
+
event_stats['stt'] = app.state.stt_manager.get_stats()
|
| 431 |
+
if hasattr(app.state, 'tts_manager'):
|
| 432 |
+
event_stats['tts'] = app.state.tts_manager.get_stats()
|
| 433 |
+
if hasattr(app.state, 'llm_manager'):
|
| 434 |
+
event_stats['llm'] = app.state.llm_manager.get_stats()
|
| 435 |
+
if hasattr(app.state, 'websocket_manager'):
|
| 436 |
+
event_stats['websocket'] = {
|
| 437 |
+
'active_connections': app.state.websocket_manager.get_connection_count()
|
| 438 |
+
}
|
| 439 |
+
if hasattr(app.state, 'resource_manager'):
|
| 440 |
+
event_stats['resources'] = app.state.resource_manager.get_stats()
|
| 441 |
+
if hasattr(app.state, 'audio_buffer_manager'):
|
| 442 |
+
event_stats['audio_buffers'] = app.state.audio_buffer_manager.get_all_stats()
|
| 443 |
|
| 444 |
metrics = {
|
| 445 |
"memory": {
|
|
|
|
| 452 |
"num_threads": process.num_threads()
|
| 453 |
},
|
| 454 |
"sessions": session_stats,
|
| 455 |
+
"event_driven_components": event_stats,
|
| 456 |
"gc": {
|
| 457 |
"collections": gc.get_count(),
|
| 458 |
"objects": len(gc.get_objects())
|
|
|
|
| 466 |
@app.get("/api/health")
|
| 467 |
def health_check():
|
| 468 |
"""Health check endpoint - moved to /api/health"""
|
| 469 |
+
# Check if event-driven components are healthy
|
| 470 |
+
event_bus_healthy = hasattr(app.state, 'event_bus') and app.state.event_bus._running
|
| 471 |
+
|
| 472 |
return {
|
| 473 |
+
"status": "ok" if event_bus_healthy else "degraded",
|
| 474 |
"version": "2.0.0",
|
| 475 |
"timestamp": datetime.utcnow().isoformat(),
|
| 476 |
+
"environment": os.getenv("ENVIRONMENT", "development"),
|
| 477 |
+
"event_driven": {
|
| 478 |
+
"event_bus": "running" if event_bus_healthy else "not_running",
|
| 479 |
+
"managers": {
|
| 480 |
+
"state_orchestrator": "initialized" if hasattr(app.state, 'state_orchestrator') else "not_initialized",
|
| 481 |
+
"websocket_manager": "initialized" if hasattr(app.state, 'websocket_manager') else "not_initialized",
|
| 482 |
+
"stt_manager": "initialized" if hasattr(app.state, 'stt_manager') else "not_initialized",
|
| 483 |
+
"tts_manager": "initialized" if hasattr(app.state, 'tts_manager') else "not_initialized",
|
| 484 |
+
"llm_manager": "initialized" if hasattr(app.state, 'llm_manager') else "not_initialized"
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
}
|
| 488 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
# ---------------- Serve static files ------------------------------------
|
| 490 |
# UI static files (production build)
|
| 491 |
static_path = Path(__file__).parent / "static"
|
|
|
|
| 542 |
|
| 543 |
if __name__ == "__main__":
|
| 544 |
log_info("π Starting Flare backend on port 7860...")
|
| 545 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|