Spaces:
Building
Building
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)
|