File size: 13,118 Bytes
9f79da5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b10856a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
"""

Flare – Main Application (Refactored)

=====================================

"""
# FastAPI imports
from fastapi import FastAPI, WebSocket, Request, status
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.encoders import jsonable_encoder

# Standard library
import uvicorn
import os
from pathlib import Path
import mimetypes
import uuid
import traceback
from datetime import datetime
from pydantic import ValidationError
from dotenv import load_dotenv

# Project imports
from routes.websocket_handler import websocket_endpoint
from routes.admin_routes import router as admin_router, start_cleanup_task
from llm.llm_startup import run_in_thread
from session import session_store, start_session_cleanup
from config.config_provider import ConfigProvider

# Logger imports (utils.log yerine)
from utils.logger import log_error, log_info, log_warning

# Exception imports
from utils.exceptions import (
    DuplicateResourceError,
    RaceConditionError,
    ValidationError,
    ResourceNotFoundError,
    AuthenticationError,
    AuthorizationError,
    ConfigurationError,
    get_http_status_code
)

# Load .env file if exists
load_dotenv()

ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200").split(",")

# ===================== Environment Setup =====================
def setup_environment():
    """Setup environment based on deployment mode"""
    cfg = ConfigProvider.get()

    log_info("=" * 60)
    log_info("πŸš€ Flare Starting", version="2.0.0")
    log_info(f"πŸ”Œ LLM Provider: {cfg.global_config.llm_provider.name}")
    log_info(f"🎀 TTS Provider: {cfg.global_config.tts_provider.name}")
    log_info(f"🎧 STT Provider: {cfg.global_config.stt_provider.name}")
    log_info("=" * 60)

    if cfg.global_config.is_cloud_mode():
        log_info("☁️  Cloud Mode: Using HuggingFace Secrets")
        log_info("πŸ“Œ Required secrets: JWT_SECRET, FLARE_TOKEN_KEY")

        # Check for provider-specific tokens
        llm_config = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name)
        if llm_config and llm_config.requires_repo_info:
            log_info("πŸ“Œ LLM requires SPARK_TOKEN for repository operations")
    else:
        log_info("🏒 On-Premise Mode: Using .env file")
        if not Path(".env").exists():
            log_warning("⚠️  WARNING: .env file not found!")
            log_info("πŸ“Œ Copy .env.example to .env and configure it")

# Run setup
setup_environment()

# Fix MIME types for JavaScript files
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("text/css", ".css")

app = FastAPI(
    title="Flare Orchestration Service",
    version="2.0.0",
    description="LLM-driven intent & API flow engine with multi-provider support",
)

# CORS for development
if os.getenv("ENVIRONMENT", "development") == "development":
    app.add_middleware(
        CORSMiddleware,
        allow_origins=ALLOWED_ORIGINS,
        allow_credentials=True,
        allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        allow_headers=["*"],
        max_age=3600,
        expose_headers=["X-Request-ID"]
    )
    log_info(f"πŸ”§ CORS enabled for origins: {ALLOWED_ORIGINS}")

# Request ID middleware
@app.middleware("http")
async def add_request_id(request: Request, call_next):
    """Add request ID for tracking"""
    request_id = str(uuid.uuid4())
    request.state.request_id = request_id

    # Log request start
    log_info(
        "Request started",
        request_id=request_id,
        method=request.method,
        path=request.url.path,
        client=request.client.host if request.client else "unknown"
    )

    try:
        response = await call_next(request)

        # Add request ID to response headers
        response.headers["X-Request-ID"] = request_id

        # Log request completion
        log_info(
            "Request completed",
            request_id=request_id,
            status_code=response.status_code,
            method=request.method,
            path=request.url.path
        )

        return response
    except Exception as e:
        log_error(
            "Request failed",
            request_id=request_id,
            error=str(e),
            traceback=traceback.format_exc()
        )
        raise

run_in_thread()  # Start LLM startup notifier if needed
start_cleanup_task()  # Activity log cleanup
start_session_cleanup()  # Session cleanup

# ---------------- Core chat/session routes --------------------------
from routes.chat_handler import router as chat_router
app.include_router(chat_router, prefix="/api")

# ---------------- Audio (TTS/STT) routes ------------------------------
from routes.audio_routes import router as audio_router
app.include_router(audio_router, prefix="/api")

# ---------------- Admin API routes ----------------------------------
app.include_router(admin_router, prefix="/api/admin")

# ---------------- Exception Handlers ----------------------------------
# Add global exception handler
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """Handle all unhandled exceptions"""
    request_id = getattr(request.state, 'request_id', 'unknown')

    # Log the full exception
    log_error(
        "Unhandled exception",
        request_id=request_id,
        endpoint=str(request.url),
        method=request.method,
        error=str(exc),
        error_type=type(exc).__name__,
        traceback=traceback.format_exc()
    )

    # Special handling for FlareExceptions
    if isinstance(exc, FlareException):
        status_code = get_http_status_code(exc)
        response_body = format_error_response(exc, request_id)

        # Special message for race conditions
        if isinstance(exc, RaceConditionError):
            response_body["user_action"] = "Please reload the data and try again"

        return JSONResponse(
            status_code=status_code,
            content=jsonable_encoder(response_body)
        )

    # Generic error response
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content=jsonable_encoder({
            "error": "InternalServerError",
            "message": "An unexpected error occurred. Please try again later.",
            "request_id": request_id,
            "timestamp": datetime.utcnow().isoformat()
        })
    )

# Add custom exception handlers
@app.exception_handler(DuplicateResourceError)
async def duplicate_resource_handler(request: Request, exc: DuplicateResourceError):
    """Handle duplicate resource errors"""
    return JSONResponse(
        status_code=409,
        content={
            "detail": str(exc),
            "error_type": "duplicate_resource",
            "resource_type": exc.details.get("resource_type"),
            "identifier": exc.details.get("identifier")
        }
    )

@app.exception_handler(RaceConditionError)
async def race_condition_handler(request: Request, exc: RaceConditionError):
    """Handle race condition errors"""
    return JSONResponse(
        status_code=409,
        content=exc.to_http_detail()
    )

@app.exception_handler(ValidationError)
async def validation_error_handler(request: Request, exc: ValidationError):
    """Handle validation errors"""
    return JSONResponse(
        status_code=422,
        content={
            "detail": str(exc),
            "error_type": "validation_error",
            "details": exc.details
        }
    )

@app.exception_handler(ResourceNotFoundError)
async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError):
    """Handle resource not found errors"""
    return JSONResponse(
        status_code=404,
        content={
            "detail": str(exc),
            "error_type": "resource_not_found",
            "resource_type": exc.details.get("resource_type"),
            "identifier": exc.details.get("identifier")
        }
    )

@app.exception_handler(AuthenticationError)
async def authentication_error_handler(request: Request, exc: AuthenticationError):
    """Handle authentication errors"""
    return JSONResponse(
        status_code=401,
        content={
            "detail": str(exc),
            "error_type": "authentication_error"
        }
    )

@app.exception_handler(AuthorizationError)
async def authorization_error_handler(request: Request, exc: AuthorizationError):
    """Handle authorization errors"""
    return JSONResponse(
        status_code=403,
        content={
            "detail": str(exc),
            "error_type": "authorization_error"
        }
    )

@app.exception_handler(ConfigurationError)
async def configuration_error_handler(request: Request, exc: ConfigurationError):
    """Handle configuration errors"""
    return JSONResponse(
        status_code=500,
        content={
            "detail": str(exc),
            "error_type": "configuration_error",
            "config_key": exc.details.get("config_key")
        }
    )

# ---------------- Metrics endpoint -----------------
@app.get("/metrics")
async def get_metrics():
    """Get system metrics"""
    import psutil
    import gc

    # Memory info
    process = psutil.Process()
    memory_info = process.memory_info()

    # Session stats
    session_stats = session_store.get_session_stats()

    metrics = {
        "memory": {
            "rss_mb": memory_info.rss / 1024 / 1024,
            "vms_mb": memory_info.vms / 1024 / 1024,
            "percent": process.memory_percent()
        },
        "cpu": {
            "percent": process.cpu_percent(interval=0.1),
            "num_threads": process.num_threads()
        },
        "sessions": session_stats,
        "gc": {
            "collections": gc.get_count(),
            "objects": len(gc.get_objects())
        },
        "uptime_seconds": time.time() - process.create_time()
    }

    return metrics

# ---------------- Health probe (HF Spaces watchdog) -----------------
@app.get("/api/health")
def health_check():
    """Health check endpoint - moved to /api/health"""
    return {
        "status": "ok",
        "version": "2.0.0",
        "timestamp": datetime.utcnow().isoformat(),
        "environment": os.getenv("ENVIRONMENT", "development")
    }

# ---------------- WebSocket route for real-time STT ------------------
@app.websocket("/ws/conversation/{session_id}")
async def conversation_websocket(websocket: WebSocket, session_id: str):
    await websocket_endpoint(websocket, session_id)

# ---------------- Serve static files ------------------------------------
# UI static files (production build)
static_path = Path(__file__).parent / "static"
if static_path.exists():
    app.mount("/static", StaticFiles(directory=str(static_path)), name="static")

    # Serve index.html for all non-API routes (SPA support)
    @app.get("/", response_class=FileResponse)
    async def serve_index():
        """Serve Angular app"""
        index_path = static_path / "index.html"
        if index_path.exists():
            return FileResponse(str(index_path))
        else:
            return JSONResponse(
                status_code=404,
                content={"error": "UI not found. Please build the Angular app first."}
            )

    # Catch-all route for SPA
    @app.get("/{full_path:path}")
    async def serve_spa(full_path: str):
        """Serve Angular app for all routes"""
        # Skip API routes
        if full_path.startswith("api/"):
            return JSONResponse(status_code=404, content={"error": "Not found"})

        # Serve static files
        file_path = static_path / full_path
        if file_path.exists() and file_path.is_file():
            return FileResponse(str(file_path))

        # Fallback to index.html for SPA routing
        index_path = static_path / "index.html"
        if index_path.exists():
            return FileResponse(str(index_path))

        return JSONResponse(status_code=404, content={"error": "Not found"})
else:
    log_warning(f"⚠️  Static files directory not found at {static_path}")
    log_warning("   Run 'npm run build' in flare-ui directory to build the UI")

    @app.get("/")
    async def no_ui():
        """No UI available"""
        return JSONResponse(
            status_code=503,
            content={
                "error": "UI not available",
                "message": "Please build the Angular UI first. Run: cd flare-ui && npm run build",
                "api_docs": "/docs"
            }
        )

if __name__ == "__main__":
    log_info("🌐 Starting Flare backend on port 7860...")
    uvicorn.run(app, host="0.0.0.0", port=7860)