ciyidogan commited on
Commit
518eaee
·
verified ·
1 Parent(s): d8adb66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +545 -545
app.py CHANGED
@@ -1,545 +1,545 @@
1
- """
2
- Flare – Main Application (Refactored with Event-Driven Architecture)
3
- ====================================================================
4
- """
5
- # FastAPI imports
6
- from fastapi import FastAPI, WebSocket, Request, status
7
- from fastapi.staticfiles import StaticFiles
8
- from fastapi.responses import FileResponse, JSONResponse
9
- from fastapi.middleware.cors import CORSMiddleware
10
- from fastapi.encoders import jsonable_encoder
11
-
12
- # Standard library
13
- import uvicorn
14
- import os
15
- from pathlib import Path
16
- import mimetypes
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 chat_session.event_bus import event_bus
27
- from chat_session.state_orchestrator import StateOrchestrator
28
- from chat_session.websocket_manager import WebSocketManager
29
- from chat_session.resource_manager import ResourceManager
30
- from stt.stt_lifecycle_manager import STTLifecycleManager
31
- from tts.tts_lifecycle_manager import TTSLifecycleManager
32
- from llm.llm_manager import LLMManager
33
- from stt.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 chat_session.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
58
- load_dotenv()
59
-
60
- ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200").split(",")
61
-
62
- # ===================== Environment Setup =====================
63
- def setup_environment():
64
- """Setup environment based on deployment mode"""
65
- cfg = ConfigProvider.get()
66
-
67
- log_info("=" * 60)
68
- log_info("🚀 Flare Starting", version="2.0.0")
69
- log_info(f"🔌 LLM Provider: {cfg.global_config.llm_provider.name}")
70
- log_info(f"🎤 TTS Provider: {cfg.global_config.tts_provider.name}")
71
- log_info(f"🎧 STT Provider: {cfg.global_config.stt_provider.name}")
72
- log_info("=" * 60)
73
-
74
- if cfg.global_config.is_cloud_mode():
75
- log_info("☁️ Cloud Mode: Using HuggingFace Secrets")
76
- log_info("📌 Required secrets: JWT_SECRET, FLARE_TOKEN_KEY")
77
-
78
- # Check for provider-specific tokens
79
- llm_config = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name)
80
- if llm_config and llm_config.requires_repo_info:
81
- log_info("📌 LLM requires SPARK_TOKEN for repository operations")
82
- else:
83
- log_info("🏢 On-Premise Mode: Using .env file")
84
- if not Path(".env").exists():
85
- log_warning("⚠️ WARNING: .env file not found!")
86
- log_info("📌 Copy .env.example to .env and configure it")
87
-
88
- # Run setup
89
- setup_environment()
90
-
91
- # Fix MIME types for JavaScript files
92
- mimetypes.add_type("application/javascript", ".js")
93
- mimetypes.add_type("text/css", ".css")
94
-
95
- app = FastAPI(
96
- title="Flare Orchestration Service",
97
- version="2.0.0",
98
- description="LLM-driven intent & API flow engine with multi-provider support",
99
- )
100
-
101
- # CORS for development
102
- if os.getenv("ENVIRONMENT", "development") == "development":
103
- app.add_middleware(
104
- CORSMiddleware,
105
- allow_origins=ALLOWED_ORIGINS,
106
- allow_credentials=True,
107
- allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
108
- allow_headers=["*"],
109
- max_age=3600,
110
- expose_headers=["X-Request-ID"]
111
- )
112
- log_info(f"🔧 CORS enabled for origins: {ALLOWED_ORIGINS}")
113
-
114
- # Request ID middleware
115
- @app.middleware("http")
116
- async def add_request_id(request: Request, call_next):
117
- """Add request ID for tracking"""
118
- request_id = str(uuid.uuid4())
119
- request.state.request_id = request_id
120
-
121
- # Log request start
122
- log_info(
123
- "Request started",
124
- request_id=request_id,
125
- method=request.method,
126
- path=request.url.path,
127
- client=request.client.host if request.client else "unknown"
128
- )
129
-
130
- try:
131
- response = await call_next(request)
132
-
133
- # Add request ID to response headers
134
- response.headers["X-Request-ID"] = request_id
135
-
136
- # Log request completion
137
- log_info(
138
- "Request completed",
139
- request_id=request_id,
140
- status_code=response.status_code,
141
- method=request.method,
142
- path=request.url.path
143
- )
144
-
145
- return response
146
- except Exception as e:
147
- log_error(
148
- "Request failed",
149
- request_id=request_id,
150
- error=str(e),
151
- traceback=traceback.format_exc()
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
223
- app.include_router(chat_router, prefix="/api")
224
-
225
- # ---------------- Audio (TTS/STT) routes ------------------------------
226
- from routes.audio_routes import router as audio_router
227
- app.include_router(audio_router, prefix="/api")
228
-
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 chat_session.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"""
288
- request_id = getattr(request.state, 'request_id', 'unknown')
289
-
290
- # Log the full exception
291
- log_error(
292
- "Unhandled exception",
293
- request_id=request_id,
294
- endpoint=str(request.url),
295
- method=request.method,
296
- error=str(exc),
297
- error_type=type(exc).__name__,
298
- traceback=traceback.format_exc()
299
- )
300
-
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):
314
- response_body["user_action"] = "Please reload the data and try again"
315
-
316
- return JSONResponse(
317
- status_code=status_code,
318
- content=jsonable_encoder(response_body)
319
- )
320
-
321
- # Generic error response
322
- return JSONResponse(
323
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
324
- content=jsonable_encoder({
325
- "error": "InternalServerError",
326
- "message": "An unexpected error occurred. Please try again later.",
327
- "request_id": request_id,
328
- "timestamp": datetime.utcnow().isoformat()
329
- })
330
- )
331
-
332
- # Add custom exception handlers
333
- @app.exception_handler(DuplicateResourceError)
334
- async def duplicate_resource_handler(request: Request, exc: DuplicateResourceError):
335
- """Handle duplicate resource errors"""
336
- return JSONResponse(
337
- status_code=409,
338
- content={
339
- "detail": str(exc),
340
- "error_type": "duplicate_resource",
341
- "resource_type": exc.details.get("resource_type"),
342
- "identifier": exc.details.get("identifier")
343
- }
344
- )
345
-
346
- @app.exception_handler(RaceConditionError)
347
- async def race_condition_handler(request: Request, exc: RaceConditionError):
348
- """Handle race condition errors"""
349
- return JSONResponse(
350
- status_code=409,
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,
359
- content={
360
- "detail": str(exc),
361
- "error_type": "validation_error",
362
- "details": exc.details
363
- }
364
- )
365
-
366
- @app.exception_handler(ResourceNotFoundError)
367
- async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError):
368
- """Handle resource not found errors"""
369
- return JSONResponse(
370
- status_code=404,
371
- content={
372
- "detail": str(exc),
373
- "error_type": "resource_not_found",
374
- "resource_type": exc.details.get("resource_type"),
375
- "identifier": exc.details.get("identifier")
376
- }
377
- )
378
-
379
- @app.exception_handler(AuthenticationError)
380
- async def authentication_error_handler(request: Request, exc: AuthenticationError):
381
- """Handle authentication errors"""
382
- return JSONResponse(
383
- status_code=401,
384
- content={
385
- "detail": str(exc),
386
- "error_type": "authentication_error"
387
- }
388
- )
389
-
390
- @app.exception_handler(AuthorizationError)
391
- async def authorization_error_handler(request: Request, exc: AuthorizationError):
392
- """Handle authorization errors"""
393
- return JSONResponse(
394
- status_code=403,
395
- content={
396
- "detail": str(exc),
397
- "error_type": "authorization_error"
398
- }
399
- )
400
-
401
- @app.exception_handler(ConfigurationError)
402
- async def configuration_error_handler(request: Request, exc: ConfigurationError):
403
- """Handle configuration errors"""
404
- return JSONResponse(
405
- status_code=500,
406
- content={
407
- "detail": str(exc),
408
- "error_type": "configuration_error",
409
- "config_key": exc.details.get("config_key")
410
- }
411
- )
412
-
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
-
420
- # Memory info
421
- process = psutil.Process()
422
- memory_info = process.memory_info()
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": {
446
- "rss_mb": memory_info.rss / 1024 / 1024,
447
- "vms_mb": memory_info.vms / 1024 / 1024,
448
- "percent": process.memory_percent()
449
- },
450
- "cpu": {
451
- "percent": process.cpu_percent(interval=0.1),
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())
459
- },
460
- "uptime_seconds": time.time() - process.create_time()
461
- }
462
-
463
- return metrics
464
-
465
- # ---------------- Health probe (HF Spaces watchdog) -----------------
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"
492
- if static_path.exists():
493
- app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
494
-
495
- # Serve index.html for all non-API routes (SPA support)
496
- @app.get("/", response_class=FileResponse)
497
- async def serve_index():
498
- """Serve Angular app"""
499
- index_path = static_path / "index.html"
500
- if index_path.exists():
501
- return FileResponse(str(index_path))
502
- else:
503
- return JSONResponse(
504
- status_code=404,
505
- content={"error": "UI not found. Please build the Angular app first."}
506
- )
507
-
508
- # Catch-all route for SPA
509
- @app.get("/{full_path:path}")
510
- async def serve_spa(full_path: str):
511
- """Serve Angular app for all routes"""
512
- # Skip API routes
513
- if full_path.startswith("api/"):
514
- return JSONResponse(status_code=404, content={"error": "Not found"})
515
-
516
- # Serve static files
517
- file_path = static_path / full_path
518
- if file_path.exists() and file_path.is_file():
519
- return FileResponse(str(file_path))
520
-
521
- # Fallback to index.html for SPA routing
522
- index_path = static_path / "index.html"
523
- if index_path.exists():
524
- return FileResponse(str(index_path))
525
-
526
- return JSONResponse(status_code=404, content={"error": "Not found"})
527
- else:
528
- log_warning(f"⚠️ Static files directory not found at {static_path}")
529
- log_warning(" Run 'npm run build' in flare-ui directory to build the UI")
530
-
531
- @app.get("/")
532
- async def no_ui():
533
- """No UI available"""
534
- return JSONResponse(
535
- status_code=503,
536
- content={
537
- "error": "UI not available",
538
- "message": "Please build the Angular UI first. Run: cd flare-ui && npm run build",
539
- "api_docs": "/docs"
540
- }
541
- )
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)
 
1
+ """
2
+ Flare – Main Application (Refactored with Event-Driven Architecture)
3
+ ====================================================================
4
+ """
5
+ # FastAPI imports
6
+ from fastapi import FastAPI, WebSocket, Request, status
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.responses import FileResponse, JSONResponse
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.encoders import jsonable_encoder
11
+
12
+ # Standard library
13
+ import uvicorn
14
+ import os
15
+ from pathlib import Path
16
+ import mimetypes
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 chat_session.event_bus import event_bus
27
+ from chat_session.state_orchestrator import StateOrchestrator
28
+ from chat_session.websocket_manager import WebSocketManager
29
+ from chat_session.resource_manager import ResourceManager
30
+ from stt.stt_lifecycle_manager import STTLifecycleManager
31
+ from tts.tts_lifecycle_manager import TTSLifecycleManager
32
+ from llm.llm_lifecycle_manager import LLMLifecycleManager
33
+ from stt.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 chat_session.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
58
+ load_dotenv()
59
+
60
+ ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200").split(",")
61
+
62
+ # ===================== Environment Setup =====================
63
+ def setup_environment():
64
+ """Setup environment based on deployment mode"""
65
+ cfg = ConfigProvider.get()
66
+
67
+ log_info("=" * 60)
68
+ log_info("🚀 Flare Starting", version="2.0.0")
69
+ log_info(f"🔌 LLM Provider: {cfg.global_config.llm_provider.name}")
70
+ log_info(f"🎤 TTS Provider: {cfg.global_config.tts_provider.name}")
71
+ log_info(f"🎧 STT Provider: {cfg.global_config.stt_provider.name}")
72
+ log_info("=" * 60)
73
+
74
+ if cfg.global_config.is_cloud_mode():
75
+ log_info("☁️ Cloud Mode: Using HuggingFace Secrets")
76
+ log_info("📌 Required secrets: JWT_SECRET, FLARE_TOKEN_KEY")
77
+
78
+ # Check for provider-specific tokens
79
+ llm_config = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name)
80
+ if llm_config and llm_config.requires_repo_info:
81
+ log_info("📌 LLM requires SPARK_TOKEN for repository operations")
82
+ else:
83
+ log_info("🏢 On-Premise Mode: Using .env file")
84
+ if not Path(".env").exists():
85
+ log_warning("⚠️ WARNING: .env file not found!")
86
+ log_info("📌 Copy .env.example to .env and configure it")
87
+
88
+ # Run setup
89
+ setup_environment()
90
+
91
+ # Fix MIME types for JavaScript files
92
+ mimetypes.add_type("application/javascript", ".js")
93
+ mimetypes.add_type("text/css", ".css")
94
+
95
+ app = FastAPI(
96
+ title="Flare Orchestration Service",
97
+ version="2.0.0",
98
+ description="LLM-driven intent & API flow engine with multi-provider support",
99
+ )
100
+
101
+ # CORS for development
102
+ if os.getenv("ENVIRONMENT", "development") == "development":
103
+ app.add_middleware(
104
+ CORSMiddleware,
105
+ allow_origins=ALLOWED_ORIGINS,
106
+ allow_credentials=True,
107
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
108
+ allow_headers=["*"],
109
+ max_age=3600,
110
+ expose_headers=["X-Request-ID"]
111
+ )
112
+ log_info(f"🔧 CORS enabled for origins: {ALLOWED_ORIGINS}")
113
+
114
+ # Request ID middleware
115
+ @app.middleware("http")
116
+ async def add_request_id(request: Request, call_next):
117
+ """Add request ID for tracking"""
118
+ request_id = str(uuid.uuid4())
119
+ request.state.request_id = request_id
120
+
121
+ # Log request start
122
+ log_info(
123
+ "Request started",
124
+ request_id=request_id,
125
+ method=request.method,
126
+ path=request.url.path,
127
+ client=request.client.host if request.client else "unknown"
128
+ )
129
+
130
+ try:
131
+ response = await call_next(request)
132
+
133
+ # Add request ID to response headers
134
+ response.headers["X-Request-ID"] = request_id
135
+
136
+ # Log request completion
137
+ log_info(
138
+ "Request completed",
139
+ request_id=request_id,
140
+ status_code=response.status_code,
141
+ method=request.method,
142
+ path=request.url.path
143
+ )
144
+
145
+ return response
146
+ except Exception as e:
147
+ log_error(
148
+ "Request failed",
149
+ request_id=request_id,
150
+ error=str(e),
151
+ traceback=traceback.format_exc()
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 = LLMLifecycleManager(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
223
+ app.include_router(chat_router, prefix="/api")
224
+
225
+ # ---------------- Audio (TTS/STT) routes ------------------------------
226
+ from routes.audio_routes import router as audio_router
227
+ app.include_router(audio_router, prefix="/api")
228
+
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 chat_session.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"""
288
+ request_id = getattr(request.state, 'request_id', 'unknown')
289
+
290
+ # Log the full exception
291
+ log_error(
292
+ "Unhandled exception",
293
+ request_id=request_id,
294
+ endpoint=str(request.url),
295
+ method=request.method,
296
+ error=str(exc),
297
+ error_type=type(exc).__name__,
298
+ traceback=traceback.format_exc()
299
+ )
300
+
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):
314
+ response_body["user_action"] = "Please reload the data and try again"
315
+
316
+ return JSONResponse(
317
+ status_code=status_code,
318
+ content=jsonable_encoder(response_body)
319
+ )
320
+
321
+ # Generic error response
322
+ return JSONResponse(
323
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
324
+ content=jsonable_encoder({
325
+ "error": "InternalServerError",
326
+ "message": "An unexpected error occurred. Please try again later.",
327
+ "request_id": request_id,
328
+ "timestamp": datetime.utcnow().isoformat()
329
+ })
330
+ )
331
+
332
+ # Add custom exception handlers
333
+ @app.exception_handler(DuplicateResourceError)
334
+ async def duplicate_resource_handler(request: Request, exc: DuplicateResourceError):
335
+ """Handle duplicate resource errors"""
336
+ return JSONResponse(
337
+ status_code=409,
338
+ content={
339
+ "detail": str(exc),
340
+ "error_type": "duplicate_resource",
341
+ "resource_type": exc.details.get("resource_type"),
342
+ "identifier": exc.details.get("identifier")
343
+ }
344
+ )
345
+
346
+ @app.exception_handler(RaceConditionError)
347
+ async def race_condition_handler(request: Request, exc: RaceConditionError):
348
+ """Handle race condition errors"""
349
+ return JSONResponse(
350
+ status_code=409,
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,
359
+ content={
360
+ "detail": str(exc),
361
+ "error_type": "validation_error",
362
+ "details": exc.details
363
+ }
364
+ )
365
+
366
+ @app.exception_handler(ResourceNotFoundError)
367
+ async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError):
368
+ """Handle resource not found errors"""
369
+ return JSONResponse(
370
+ status_code=404,
371
+ content={
372
+ "detail": str(exc),
373
+ "error_type": "resource_not_found",
374
+ "resource_type": exc.details.get("resource_type"),
375
+ "identifier": exc.details.get("identifier")
376
+ }
377
+ )
378
+
379
+ @app.exception_handler(AuthenticationError)
380
+ async def authentication_error_handler(request: Request, exc: AuthenticationError):
381
+ """Handle authentication errors"""
382
+ return JSONResponse(
383
+ status_code=401,
384
+ content={
385
+ "detail": str(exc),
386
+ "error_type": "authentication_error"
387
+ }
388
+ )
389
+
390
+ @app.exception_handler(AuthorizationError)
391
+ async def authorization_error_handler(request: Request, exc: AuthorizationError):
392
+ """Handle authorization errors"""
393
+ return JSONResponse(
394
+ status_code=403,
395
+ content={
396
+ "detail": str(exc),
397
+ "error_type": "authorization_error"
398
+ }
399
+ )
400
+
401
+ @app.exception_handler(ConfigurationError)
402
+ async def configuration_error_handler(request: Request, exc: ConfigurationError):
403
+ """Handle configuration errors"""
404
+ return JSONResponse(
405
+ status_code=500,
406
+ content={
407
+ "detail": str(exc),
408
+ "error_type": "configuration_error",
409
+ "config_key": exc.details.get("config_key")
410
+ }
411
+ )
412
+
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
+
420
+ # Memory info
421
+ process = psutil.Process()
422
+ memory_info = process.memory_info()
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": {
446
+ "rss_mb": memory_info.rss / 1024 / 1024,
447
+ "vms_mb": memory_info.vms / 1024 / 1024,
448
+ "percent": process.memory_percent()
449
+ },
450
+ "cpu": {
451
+ "percent": process.cpu_percent(interval=0.1),
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())
459
+ },
460
+ "uptime_seconds": time.time() - process.create_time()
461
+ }
462
+
463
+ return metrics
464
+
465
+ # ---------------- Health probe (HF Spaces watchdog) -----------------
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"
492
+ if static_path.exists():
493
+ app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
494
+
495
+ # Serve index.html for all non-API routes (SPA support)
496
+ @app.get("/", response_class=FileResponse)
497
+ async def serve_index():
498
+ """Serve Angular app"""
499
+ index_path = static_path / "index.html"
500
+ if index_path.exists():
501
+ return FileResponse(str(index_path))
502
+ else:
503
+ return JSONResponse(
504
+ status_code=404,
505
+ content={"error": "UI not found. Please build the Angular app first."}
506
+ )
507
+
508
+ # Catch-all route for SPA
509
+ @app.get("/{full_path:path}")
510
+ async def serve_spa(full_path: str):
511
+ """Serve Angular app for all routes"""
512
+ # Skip API routes
513
+ if full_path.startswith("api/"):
514
+ return JSONResponse(status_code=404, content={"error": "Not found"})
515
+
516
+ # Serve static files
517
+ file_path = static_path / full_path
518
+ if file_path.exists() and file_path.is_file():
519
+ return FileResponse(str(file_path))
520
+
521
+ # Fallback to index.html for SPA routing
522
+ index_path = static_path / "index.html"
523
+ if index_path.exists():
524
+ return FileResponse(str(index_path))
525
+
526
+ return JSONResponse(status_code=404, content={"error": "Not found"})
527
+ else:
528
+ log_warning(f"⚠️ Static files directory not found at {static_path}")
529
+ log_warning(" Run 'npm run build' in flare-ui directory to build the UI")
530
+
531
+ @app.get("/")
532
+ async def no_ui():
533
+ """No UI available"""
534
+ return JSONResponse(
535
+ status_code=503,
536
+ content={
537
+ "error": "UI not available",
538
+ "message": "Please build the Angular UI first. Run: cd flare-ui && npm run build",
539
+ "api_docs": "/docs"
540
+ }
541
+ )
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)