ciyidogan commited on
Commit
f6f2dbe
Β·
verified Β·
1 Parent(s): 7af9f5c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +395 -387
app.py CHANGED
@@ -1,388 +1,396 @@
1
- """
2
- Flare – Main Application (Refactored)
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
- from pydantic import ValidationError
21
- from dotenv import load_dotenv
22
-
23
- # Project imports
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
-
30
- # Logger imports (utils.log yerine)
31
- from utils.logger import log_error, log_info, log_warning
32
-
33
- # Exception imports
34
- from utils.exceptions import (
35
- DuplicateResourceError,
36
- RaceConditionError,
37
- ValidationError,
38
- ResourceNotFoundError,
39
- AuthenticationError,
40
- AuthorizationError,
41
- ConfigurationError,
42
- get_http_status_code
43
- )
44
-
45
- # Load .env file if exists
46
- load_dotenv()
47
-
48
- ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200").split(",")
49
-
50
- # ===================== Environment Setup =====================
51
- def setup_environment():
52
- """Setup environment based on deployment mode"""
53
- cfg = ConfigProvider.get()
54
-
55
- log_info("=" * 60)
56
- log_info("πŸš€ Flare Starting", version="2.0.0")
57
- log_info(f"πŸ”Œ LLM Provider: {cfg.global_config.llm_provider.name}")
58
- log_info(f"🎀 TTS Provider: {cfg.global_config.tts_provider.name}")
59
- log_info(f"🎧 STT Provider: {cfg.global_config.stt_provider.name}")
60
- log_info("=" * 60)
61
-
62
- if cfg.global_config.is_cloud_mode():
63
- log_info("☁️ Cloud Mode: Using HuggingFace Secrets")
64
- log_info("πŸ“Œ Required secrets: JWT_SECRET, FLARE_TOKEN_KEY")
65
-
66
- # Check for provider-specific tokens
67
- llm_config = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name)
68
- if llm_config and llm_config.requires_repo_info:
69
- log_info("πŸ“Œ LLM requires SPARK_TOKEN for repository operations")
70
- else:
71
- log_info("🏒 On-Premise Mode: Using .env file")
72
- if not Path(".env").exists():
73
- log_warning("⚠️ WARNING: .env file not found!")
74
- log_info("πŸ“Œ Copy .env.example to .env and configure it")
75
-
76
- # Run setup
77
- setup_environment()
78
-
79
- # Fix MIME types for JavaScript files
80
- mimetypes.add_type("application/javascript", ".js")
81
- mimetypes.add_type("text/css", ".css")
82
-
83
- app = FastAPI(
84
- title="Flare Orchestration Service",
85
- version="2.0.0",
86
- description="LLM-driven intent & API flow engine with multi-provider support",
87
- )
88
-
89
- # CORS for development
90
- if os.getenv("ENVIRONMENT", "development") == "development":
91
- app.add_middleware(
92
- CORSMiddleware,
93
- allow_origins=ALLOWED_ORIGINS,
94
- allow_credentials=True,
95
- allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
96
- allow_headers=["*"],
97
- max_age=3600,
98
- expose_headers=["X-Request-ID"]
99
- )
100
- log_info(f"πŸ”§ CORS enabled for origins: {ALLOWED_ORIGINS}")
101
-
102
- # Request ID middleware
103
- @app.middleware("http")
104
- async def add_request_id(request: Request, call_next):
105
- """Add request ID for tracking"""
106
- request_id = str(uuid.uuid4())
107
- request.state.request_id = request_id
108
-
109
- # Log request start
110
- log_info(
111
- "Request started",
112
- request_id=request_id,
113
- method=request.method,
114
- path=request.url.path,
115
- client=request.client.host if request.client else "unknown"
116
- )
117
-
118
- try:
119
- response = await call_next(request)
120
-
121
- # Add request ID to response headers
122
- response.headers["X-Request-ID"] = request_id
123
-
124
- # Log request completion
125
- log_info(
126
- "Request completed",
127
- request_id=request_id,
128
- status_code=response.status_code,
129
- method=request.method,
130
- path=request.url.path
131
- )
132
-
133
- return response
134
- except Exception as e:
135
- log_error(
136
- "Request failed",
137
- request_id=request_id,
138
- error=str(e),
139
- traceback=traceback.format_exc()
140
- )
141
- raise
142
-
143
- run_in_thread() # Start LLM startup notifier if needed
144
- start_cleanup_task() # Activity log cleanup
145
- start_session_cleanup() # Session cleanup
146
-
147
- # ---------------- Core chat/session routes --------------------------
148
- from routes.chat_handler import router as chat_router
149
- app.include_router(chat_router, prefix="/api")
150
-
151
- # ---------------- Audio (TTS/STT) routes ------------------------------
152
- from routes.audio_routes import router as audio_router
153
- app.include_router(audio_router, prefix="/api")
154
-
155
- # ---------------- Admin API routes ----------------------------------
156
- app.include_router(admin_router, prefix="/api/admin")
157
-
158
- # ---------------- Exception Handlers ----------------------------------
159
- # Add global exception handler
160
- @app.exception_handler(Exception)
161
- async def global_exception_handler(request: Request, exc: Exception):
162
- """Handle all unhandled exceptions"""
163
- request_id = getattr(request.state, 'request_id', 'unknown')
164
-
165
- # Log the full exception
166
- log_error(
167
- "Unhandled exception",
168
- request_id=request_id,
169
- endpoint=str(request.url),
170
- method=request.method,
171
- error=str(exc),
172
- error_type=type(exc).__name__,
173
- traceback=traceback.format_exc()
174
- )
175
-
176
- # Special handling for FlareExceptions
177
- if isinstance(exc, FlareException):
178
- status_code = get_http_status_code(exc)
179
- response_body = format_error_response(exc, request_id)
180
-
181
- # Special message for race conditions
182
- if isinstance(exc, RaceConditionError):
183
- response_body["user_action"] = "Please reload the data and try again"
184
-
185
- return JSONResponse(
186
- status_code=status_code,
187
- content=jsonable_encoder(response_body)
188
- )
189
-
190
- # Generic error response
191
- return JSONResponse(
192
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
193
- content=jsonable_encoder({
194
- "error": "InternalServerError",
195
- "message": "An unexpected error occurred. Please try again later.",
196
- "request_id": request_id,
197
- "timestamp": datetime.utcnow().isoformat()
198
- })
199
- )
200
-
201
- # Add custom exception handlers
202
- @app.exception_handler(DuplicateResourceError)
203
- async def duplicate_resource_handler(request: Request, exc: DuplicateResourceError):
204
- """Handle duplicate resource errors"""
205
- return JSONResponse(
206
- status_code=409,
207
- content={
208
- "detail": str(exc),
209
- "error_type": "duplicate_resource",
210
- "resource_type": exc.details.get("resource_type"),
211
- "identifier": exc.details.get("identifier")
212
- }
213
- )
214
-
215
- @app.exception_handler(RaceConditionError)
216
- async def race_condition_handler(request: Request, exc: RaceConditionError):
217
- """Handle race condition errors"""
218
- return JSONResponse(
219
- status_code=409,
220
- content=exc.to_http_detail()
221
- )
222
-
223
- @app.exception_handler(ValidationError)
224
- async def validation_error_handler(request: Request, exc: ValidationError):
225
- """Handle validation errors"""
226
- return JSONResponse(
227
- status_code=422,
228
- content={
229
- "detail": str(exc),
230
- "error_type": "validation_error",
231
- "details": exc.details
232
- }
233
- )
234
-
235
- @app.exception_handler(ResourceNotFoundError)
236
- async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError):
237
- """Handle resource not found errors"""
238
- return JSONResponse(
239
- status_code=404,
240
- content={
241
- "detail": str(exc),
242
- "error_type": "resource_not_found",
243
- "resource_type": exc.details.get("resource_type"),
244
- "identifier": exc.details.get("identifier")
245
- }
246
- )
247
-
248
- @app.exception_handler(AuthenticationError)
249
- async def authentication_error_handler(request: Request, exc: AuthenticationError):
250
- """Handle authentication errors"""
251
- return JSONResponse(
252
- status_code=401,
253
- content={
254
- "detail": str(exc),
255
- "error_type": "authentication_error"
256
- }
257
- )
258
-
259
- @app.exception_handler(AuthorizationError)
260
- async def authorization_error_handler(request: Request, exc: AuthorizationError):
261
- """Handle authorization errors"""
262
- return JSONResponse(
263
- status_code=403,
264
- content={
265
- "detail": str(exc),
266
- "error_type": "authorization_error"
267
- }
268
- )
269
-
270
- @app.exception_handler(ConfigurationError)
271
- async def configuration_error_handler(request: Request, exc: ConfigurationError):
272
- """Handle configuration errors"""
273
- return JSONResponse(
274
- status_code=500,
275
- content={
276
- "detail": str(exc),
277
- "error_type": "configuration_error",
278
- "config_key": exc.details.get("config_key")
279
- }
280
- )
281
-
282
- # ---------------- Metrics endpoint -----------------
283
- @app.get("/metrics")
284
- async def get_metrics():
285
- """Get system metrics"""
286
- import psutil
287
- import gc
288
-
289
- # Memory info
290
- process = psutil.Process()
291
- memory_info = process.memory_info()
292
-
293
- # Session stats
294
- session_stats = session_store.get_session_stats()
295
-
296
- metrics = {
297
- "memory": {
298
- "rss_mb": memory_info.rss / 1024 / 1024,
299
- "vms_mb": memory_info.vms / 1024 / 1024,
300
- "percent": process.memory_percent()
301
- },
302
- "cpu": {
303
- "percent": process.cpu_percent(interval=0.1),
304
- "num_threads": process.num_threads()
305
- },
306
- "sessions": session_stats,
307
- "gc": {
308
- "collections": gc.get_count(),
309
- "objects": len(gc.get_objects())
310
- },
311
- "uptime_seconds": time.time() - process.create_time()
312
- }
313
-
314
- return metrics
315
-
316
- # ---------------- Health probe (HF Spaces watchdog) -----------------
317
- @app.get("/api/health")
318
- def health_check():
319
- """Health check endpoint - moved to /api/health"""
320
- return {
321
- "status": "ok",
322
- "version": "2.0.0",
323
- "timestamp": datetime.utcnow().isoformat(),
324
- "environment": os.getenv("ENVIRONMENT", "development")
325
- }
326
-
327
- # ---------------- WebSocket route for real-time STT ------------------
328
- @app.websocket("/ws/conversation/{session_id}")
329
- async def conversation_websocket(websocket: WebSocket, session_id: str):
330
- await websocket_endpoint(websocket, session_id)
331
-
332
- # ---------------- Serve static files ------------------------------------
333
- # UI static files (production build)
334
- static_path = Path(__file__).parent / "static"
335
- if static_path.exists():
336
- app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
337
-
338
- # Serve index.html for all non-API routes (SPA support)
339
- @app.get("/", response_class=FileResponse)
340
- async def serve_index():
341
- """Serve Angular app"""
342
- index_path = static_path / "index.html"
343
- if index_path.exists():
344
- return FileResponse(str(index_path))
345
- else:
346
- return JSONResponse(
347
- status_code=404,
348
- content={"error": "UI not found. Please build the Angular app first."}
349
- )
350
-
351
- # Catch-all route for SPA
352
- @app.get("/{full_path:path}")
353
- async def serve_spa(full_path: str):
354
- """Serve Angular app for all routes"""
355
- # Skip API routes
356
- if full_path.startswith("api/"):
357
- return JSONResponse(status_code=404, content={"error": "Not found"})
358
-
359
- # Serve static files
360
- file_path = static_path / full_path
361
- if file_path.exists() and file_path.is_file():
362
- return FileResponse(str(file_path))
363
-
364
- # Fallback to index.html for SPA routing
365
- index_path = static_path / "index.html"
366
- if index_path.exists():
367
- return FileResponse(str(index_path))
368
-
369
- return JSONResponse(status_code=404, content={"error": "Not found"})
370
- else:
371
- log_warning(f"⚠️ Static files directory not found at {static_path}")
372
- log_warning(" Run 'npm run build' in flare-ui directory to build the UI")
373
-
374
- @app.get("/")
375
- async def no_ui():
376
- """No UI available"""
377
- return JSONResponse(
378
- status_code=503,
379
- content={
380
- "error": "UI not available",
381
- "message": "Please build the Angular UI first. Run: cd flare-ui && npm run build",
382
- "api_docs": "/docs"
383
- }
384
- )
385
-
386
- if __name__ == "__main__":
387
- log_info("🌐 Starting Flare backend on port 7860...")
 
 
 
 
 
 
 
 
388
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ """
2
+ Flare – Main Application (Refactored)
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
+ from pydantic import ValidationError
21
+ from dotenv import load_dotenv
22
+
23
+ # Project imports
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
32
+ from resource_manager import ResourceManager
33
+ from stt_lifecycle_manager import STTLifecycleManager
34
+ from tts_lifecycle_manager import TTSLifecycleManager
35
+ from llm_manager import LLMManager
36
+ from audio_buffer_manager import AudioBufferManager
37
+
38
+ # Logger imports (utils.log yerine)
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
54
+ load_dotenv()
55
+
56
+ ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200").split(",")
57
+
58
+ # ===================== Environment Setup =====================
59
+ def setup_environment():
60
+ """Setup environment based on deployment mode"""
61
+ cfg = ConfigProvider.get()
62
+
63
+ log_info("=" * 60)
64
+ log_info("πŸš€ Flare Starting", version="2.0.0")
65
+ log_info(f"πŸ”Œ LLM Provider: {cfg.global_config.llm_provider.name}")
66
+ log_info(f"🎀 TTS Provider: {cfg.global_config.tts_provider.name}")
67
+ log_info(f"🎧 STT Provider: {cfg.global_config.stt_provider.name}")
68
+ log_info("=" * 60)
69
+
70
+ if cfg.global_config.is_cloud_mode():
71
+ log_info("☁️ Cloud Mode: Using HuggingFace Secrets")
72
+ log_info("πŸ“Œ Required secrets: JWT_SECRET, FLARE_TOKEN_KEY")
73
+
74
+ # Check for provider-specific tokens
75
+ llm_config = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name)
76
+ if llm_config and llm_config.requires_repo_info:
77
+ log_info("πŸ“Œ LLM requires SPARK_TOKEN for repository operations")
78
+ else:
79
+ log_info("🏒 On-Premise Mode: Using .env file")
80
+ if not Path(".env").exists():
81
+ log_warning("⚠️ WARNING: .env file not found!")
82
+ log_info("πŸ“Œ Copy .env.example to .env and configure it")
83
+
84
+ # Run setup
85
+ setup_environment()
86
+
87
+ # Fix MIME types for JavaScript files
88
+ mimetypes.add_type("application/javascript", ".js")
89
+ mimetypes.add_type("text/css", ".css")
90
+
91
+ app = FastAPI(
92
+ title="Flare Orchestration Service",
93
+ version="2.0.0",
94
+ description="LLM-driven intent & API flow engine with multi-provider support",
95
+ )
96
+
97
+ # CORS for development
98
+ if os.getenv("ENVIRONMENT", "development") == "development":
99
+ app.add_middleware(
100
+ CORSMiddleware,
101
+ allow_origins=ALLOWED_ORIGINS,
102
+ allow_credentials=True,
103
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
104
+ allow_headers=["*"],
105
+ max_age=3600,
106
+ expose_headers=["X-Request-ID"]
107
+ )
108
+ log_info(f"πŸ”§ CORS enabled for origins: {ALLOWED_ORIGINS}")
109
+
110
+ # Request ID middleware
111
+ @app.middleware("http")
112
+ async def add_request_id(request: Request, call_next):
113
+ """Add request ID for tracking"""
114
+ request_id = str(uuid.uuid4())
115
+ request.state.request_id = request_id
116
+
117
+ # Log request start
118
+ log_info(
119
+ "Request started",
120
+ request_id=request_id,
121
+ method=request.method,
122
+ path=request.url.path,
123
+ client=request.client.host if request.client else "unknown"
124
+ )
125
+
126
+ try:
127
+ response = await call_next(request)
128
+
129
+ # Add request ID to response headers
130
+ response.headers["X-Request-ID"] = request_id
131
+
132
+ # Log request completion
133
+ log_info(
134
+ "Request completed",
135
+ request_id=request_id,
136
+ status_code=response.status_code,
137
+ method=request.method,
138
+ path=request.url.path
139
+ )
140
+
141
+ return response
142
+ except Exception as e:
143
+ log_error(
144
+ "Request failed",
145
+ request_id=request_id,
146
+ error=str(e),
147
+ traceback=traceback.format_exc()
148
+ )
149
+ raise
150
+
151
+ run_in_thread() # Start LLM startup notifier if needed
152
+ start_cleanup_task() # Activity log cleanup
153
+ start_session_cleanup() # Session cleanup
154
+
155
+ # ---------------- Core chat/session routes --------------------------
156
+ from routes.chat_handler import router as chat_router
157
+ app.include_router(chat_router, prefix="/api")
158
+
159
+ # ---------------- Audio (TTS/STT) routes ------------------------------
160
+ from routes.audio_routes import router as audio_router
161
+ app.include_router(audio_router, prefix="/api")
162
+
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"""
171
+ request_id = getattr(request.state, 'request_id', 'unknown')
172
+
173
+ # Log the full exception
174
+ log_error(
175
+ "Unhandled exception",
176
+ request_id=request_id,
177
+ endpoint=str(request.url),
178
+ method=request.method,
179
+ error=str(exc),
180
+ error_type=type(exc).__name__,
181
+ traceback=traceback.format_exc()
182
+ )
183
+
184
+ # Special handling for FlareExceptions
185
+ if isinstance(exc, FlareException):
186
+ status_code = get_http_status_code(exc)
187
+ response_body = format_error_response(exc, request_id)
188
+
189
+ # Special message for race conditions
190
+ if isinstance(exc, RaceConditionError):
191
+ response_body["user_action"] = "Please reload the data and try again"
192
+
193
+ return JSONResponse(
194
+ status_code=status_code,
195
+ content=jsonable_encoder(response_body)
196
+ )
197
+
198
+ # Generic error response
199
+ return JSONResponse(
200
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
201
+ content=jsonable_encoder({
202
+ "error": "InternalServerError",
203
+ "message": "An unexpected error occurred. Please try again later.",
204
+ "request_id": request_id,
205
+ "timestamp": datetime.utcnow().isoformat()
206
+ })
207
+ )
208
+
209
+ # Add custom exception handlers
210
+ @app.exception_handler(DuplicateResourceError)
211
+ async def duplicate_resource_handler(request: Request, exc: DuplicateResourceError):
212
+ """Handle duplicate resource errors"""
213
+ return JSONResponse(
214
+ status_code=409,
215
+ content={
216
+ "detail": str(exc),
217
+ "error_type": "duplicate_resource",
218
+ "resource_type": exc.details.get("resource_type"),
219
+ "identifier": exc.details.get("identifier")
220
+ }
221
+ )
222
+
223
+ @app.exception_handler(RaceConditionError)
224
+ async def race_condition_handler(request: Request, exc: RaceConditionError):
225
+ """Handle race condition errors"""
226
+ return JSONResponse(
227
+ status_code=409,
228
+ content=exc.to_http_detail()
229
+ )
230
+
231
+ @app.exception_handler(ValidationError)
232
+ async def validation_error_handler(request: Request, exc: ValidationError):
233
+ """Handle validation errors"""
234
+ return JSONResponse(
235
+ status_code=422,
236
+ content={
237
+ "detail": str(exc),
238
+ "error_type": "validation_error",
239
+ "details": exc.details
240
+ }
241
+ )
242
+
243
+ @app.exception_handler(ResourceNotFoundError)
244
+ async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError):
245
+ """Handle resource not found errors"""
246
+ return JSONResponse(
247
+ status_code=404,
248
+ content={
249
+ "detail": str(exc),
250
+ "error_type": "resource_not_found",
251
+ "resource_type": exc.details.get("resource_type"),
252
+ "identifier": exc.details.get("identifier")
253
+ }
254
+ )
255
+
256
+ @app.exception_handler(AuthenticationError)
257
+ async def authentication_error_handler(request: Request, exc: AuthenticationError):
258
+ """Handle authentication errors"""
259
+ return JSONResponse(
260
+ status_code=401,
261
+ content={
262
+ "detail": str(exc),
263
+ "error_type": "authentication_error"
264
+ }
265
+ )
266
+
267
+ @app.exception_handler(AuthorizationError)
268
+ async def authorization_error_handler(request: Request, exc: AuthorizationError):
269
+ """Handle authorization errors"""
270
+ return JSONResponse(
271
+ status_code=403,
272
+ content={
273
+ "detail": str(exc),
274
+ "error_type": "authorization_error"
275
+ }
276
+ )
277
+
278
+ @app.exception_handler(ConfigurationError)
279
+ async def configuration_error_handler(request: Request, exc: ConfigurationError):
280
+ """Handle configuration errors"""
281
+ return JSONResponse(
282
+ status_code=500,
283
+ content={
284
+ "detail": str(exc),
285
+ "error_type": "configuration_error",
286
+ "config_key": exc.details.get("config_key")
287
+ }
288
+ )
289
+
290
+ # ---------------- Metrics endpoint -----------------
291
+ @app.get("/metrics")
292
+ async def get_metrics():
293
+ """Get system metrics"""
294
+ import psutil
295
+ import gc
296
+
297
+ # Memory info
298
+ process = psutil.Process()
299
+ memory_info = process.memory_info()
300
+
301
+ # Session stats
302
+ session_stats = session_store.get_session_stats()
303
+
304
+ metrics = {
305
+ "memory": {
306
+ "rss_mb": memory_info.rss / 1024 / 1024,
307
+ "vms_mb": memory_info.vms / 1024 / 1024,
308
+ "percent": process.memory_percent()
309
+ },
310
+ "cpu": {
311
+ "percent": process.cpu_percent(interval=0.1),
312
+ "num_threads": process.num_threads()
313
+ },
314
+ "sessions": session_stats,
315
+ "gc": {
316
+ "collections": gc.get_count(),
317
+ "objects": len(gc.get_objects())
318
+ },
319
+ "uptime_seconds": time.time() - process.create_time()
320
+ }
321
+
322
+ return metrics
323
+
324
+ # ---------------- Health probe (HF Spaces watchdog) -----------------
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"
343
+ if static_path.exists():
344
+ app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
345
+
346
+ # Serve index.html for all non-API routes (SPA support)
347
+ @app.get("/", response_class=FileResponse)
348
+ async def serve_index():
349
+ """Serve Angular app"""
350
+ index_path = static_path / "index.html"
351
+ if index_path.exists():
352
+ return FileResponse(str(index_path))
353
+ else:
354
+ return JSONResponse(
355
+ status_code=404,
356
+ content={"error": "UI not found. Please build the Angular app first."}
357
+ )
358
+
359
+ # Catch-all route for SPA
360
+ @app.get("/{full_path:path}")
361
+ async def serve_spa(full_path: str):
362
+ """Serve Angular app for all routes"""
363
+ # Skip API routes
364
+ if full_path.startswith("api/"):
365
+ return JSONResponse(status_code=404, content={"error": "Not found"})
366
+
367
+ # Serve static files
368
+ file_path = static_path / full_path
369
+ if file_path.exists() and file_path.is_file():
370
+ return FileResponse(str(file_path))
371
+
372
+ # Fallback to index.html for SPA routing
373
+ index_path = static_path / "index.html"
374
+ if index_path.exists():
375
+ return FileResponse(str(index_path))
376
+
377
+ return JSONResponse(status_code=404, content={"error": "Not found"})
378
+ else:
379
+ log_warning(f"⚠️ Static files directory not found at {static_path}")
380
+ log_warning(" Run 'npm run build' in flare-ui directory to build the UI")
381
+
382
+ @app.get("/")
383
+ async def no_ui():
384
+ """No UI available"""
385
+ return JSONResponse(
386
+ status_code=503,
387
+ content={
388
+ "error": "UI not available",
389
+ "message": "Please build the Angular UI first. Run: cd flare-ui && npm run build",
390
+ "api_docs": "/docs"
391
+ }
392
+ )
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)