diff --git a/backend/app/cli/main.py b/backend/app/cli/main.py index 2f22f95..f919245 100644 --- a/backend/app/cli/main.py +++ b/backend/app/cli/main.py @@ -269,17 +269,34 @@ def analytics(): def ui( host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host address to bind the web server"), port: int = typer.Option(8000, "--port", "-p", help="Port to run the Model Router Control Room on"), + open_browser: bool = typer.Option(False, "--open-browser", help="Automatically open dashboard in default web browser"), ): """Launch the Model Router API Gateway & AI Traffic Control Room UI.""" import uvicorn - console.print(Panel.fit( + import webbrowser + import threading + import time + + url = f"http://{host}:{port}" + message = ( f"[bold cyan]Starting Model Router AI Traffic Control Room[/bold cyan]\n" - f"Web Dashboard: [bold green]http://{host}:{port}[/bold green]\n" - f"API Documentation: [bold green]http://{host}:{port}/docs[/bold green]", + f"Web Dashboard: [bold green]{url}[/bold green]\n" + f"API Documentation: [bold green]{url}/docs[/bold green]" + ) + console.print(Panel.fit( + message, title="MODEL ROUTER UI LAUNCHER", border_style="cyan" )) - uvicorn.run("main:app", host=host, port=port, reload=False, app_dir=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + + if open_browser: + def _open(): + time.sleep(1.2) + webbrowser.open(url) + threading.Thread(target=_open, daemon=True).start() + + from app.server import app as fastapi_app + uvicorn.run(fastapi_app, host=host, port=port, log_level="info") if __name__ == "__main__": diff --git a/backend/app/server.py b/backend/app/server.py new file mode 100644 index 0000000..cf38c16 --- /dev/null +++ b/backend/app/server.py @@ -0,0 +1,111 @@ +import asyncio +import json +import os +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from sse_starlette.sse import EventSourceResponse +from sqlalchemy import select, desc + +from app.config.settings import get_settings +from app.storage.database import init_db, AsyncSessionLocal +from app.storage.models import RequestRecord +from app.api.routes import router as api_router + +settings = get_settings() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Initialize SQLite database and seed defaults + await init_db() + yield + + +app = FastAPI( + title="Model Router", + description="Intelligent LLM Request Routing Platform — AI Traffic Control Room", + version="1.0.0", + lifespan=lifespan, +) + +# CORS configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router) + + +@app.get("/api/traffic/stream") +async def stream_live_traffic(): + """ + SSE Live Stream endpoint for the Traffic Control Room visualization. + Pushes newly persisted requests to connected frontend clients. + """ + async def event_generator(): + last_seen_id = None + while True: + try: + async with AsyncSessionLocal() as session: + res = await session.execute( + select(RequestRecord).order_by(desc(RequestRecord.timestamp)).limit(1) + ) + latest = res.scalar_one_or_none() + if latest and latest.request_id != last_seen_id: + last_seen_id = latest.request_id + payload = { + "request_id": latest.request_id, + "timestamp": latest.timestamp.isoformat() if latest.timestamp else "", + "prompt": latest.prompt, + "task_type": latest.task_type, + "complexity": latest.complexity, + "selected_model": latest.selected_model, + "provider": latest.provider, + "status": latest.status, + "total_latency_ms": latest.total_latency_ms, + "estimated_cost": latest.estimated_cost, + } + yield { + "event": "traffic_event", + "data": json.dumps(payload), + } + except Exception: + pass + await asyncio.sleep(1.5) + + return EventSourceResponse(event_generator()) + + +# Serve Bundled Frontend (SPA) if static directory exists +STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") + +if os.path.exists(STATIC_DIR): + assets_dir = os.path.join(STATIC_DIR, "assets") + if os.path.exists(assets_dir): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + @app.get("/{full_path:path}") + async def serve_spa(full_path: str): + if full_path.startswith("api/") or full_path.startswith("docs") or full_path.startswith("openapi.json"): + raise HTTPException(status_code=404, detail="Not Found") + + target_file = os.path.join(STATIC_DIR, full_path) + if os.path.exists(target_file) and os.path.isfile(target_file): + return FileResponse(target_file) + return FileResponse(os.path.join(STATIC_DIR, "index.html")) +else: + @app.get("/") + async def root(): + return { + "message": "Model Router AI Control Room Backend is active.", + "docs": "/docs", + "health": "/api/health", + } + diff --git a/backend/main.py b/backend/main.py index 8569f4d..2ea3eb9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,6 +4,8 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sse_starlette.sse import EventSourceResponse +import uvicorn +from app.server import app, settings from app.config.settings import get_settings from app.storage.database import init_db, AsyncSessionLocal @@ -116,3 +118,4 @@ if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG) + uvicorn.run("app.server:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG) diff --git a/pyproject.toml b/pyproject.toml index 5b0abcc..4cb8cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "model-router-cli" -version = "1.0.0" +version = "1.0.2" description = "Intelligent, explainable LLM request routing platform & AI Traffic Control Room" readme = "README.md" requires-python = ">=3.10"