File size: 10,743 Bytes
25f9893
 
 
 
f8b2916
 
b10856a
f8b2916
f6b1e89
f8b2916
 
12832d8
b10856a
 
cfdd7b3
f8b2916
 
 
e627b57
f8b2916
 
 
 
 
 
385aed6
f8b2916
c3bf14b
d8f52d8
 
f8b2916
 
 
 
 
 
 
 
 
 
 
d8f52d8
 
25f9893
d8f52d8
 
f8b2916
 
 
 
 
 
d8f52d8
25f9893
f8b2916
 
25f9893
 
 
 
f8b2916
d8f52d8
f8b2916
d8f52d8
f8b2916
 
d8f52d8
 
 
12832d8
34caeeb
 
 
 
6fdf170
 
25f9893
 
6fdf170
29f5dfb
f6b1e89
f8b2916
f6b1e89
 
f8b2916
f6b1e89
f8b2916
f6b1e89
f8b2916
 
f6b1e89
f8b2916
 
 
 
 
 
 
 
f6b1e89
f8b2916
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25f9893
385aed6
78be6d7
fb62834
6fdf170
ee25b7a
12832d8
b10856a
65c64a7
b10856a
f8b2916
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d20563d
f8b2916
d20563d
f8b2916
 
 
 
 
 
 
ce181f4
 
 
 
 
d20563d
 
 
 
 
a50f65d
d20563d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b10856a
d20563d
 
 
 
 
 
 
25f9893
d20563d
25f9893
d20563d
25f9893
d20563d
 
 
 
 
 
 
 
 
 
 
 
 
 
b10856a
12832d8
a3cefdf
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
"""
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

# Standard library
import uvicorn
import os
from pathlib import Path
import mimetypes
import uuid
import traceback
from datetime import datetime

# Pydantic
from pydantic import ValidationError

# Project imports
from websocket_handler import websocket_endpoint
from chat_handler import router as chat_router
from admin_routes import router as admin_router, start_cleanup_task
from llm_startup import run_in_thread
from session import session_store, start_session_cleanup
from config_provider import ConfigProvider

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

# Exception imports
from exceptions import (
    FlareException, RaceConditionError, format_error_response, 
    get_http_status_code
)

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 --------------------------
app.include_router(chat_router, prefix="/api")

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

# 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=response_body
        )
    
    # Generic error response
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={
            "error": "InternalServerError",
            "message": "An unexpected error occurred. Please try again later.",
            "request_id": request_id,
            "timestamp": datetime.utcnow().isoformat()
        }
    )

# Validation error handler

@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
    """Handle Pydantic validation errors"""
    request_id = getattr(request.state, 'request_id', 'unknown')
    
    errors = []
    for error in exc.errors():
        field = " -> ".join(str(x) for x in error['loc'])
        errors.append({
            'field': field,
            'message': error['msg'],
            'type': error['type'],
            'input': error.get('input')
        })
    
    log_warning(
        "Validation error",
        request_id=request_id,
        errors=errors
    )
    
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            "error": "ValidationError",
            "message": "Invalid request data. Please check the fields and try again.",
            "details": errors,
            "request_id": request_id,
            "timestamp": datetime.utcnow().isoformat()
        }
    )

# ---------------- 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)