Spaces:
Running
Running
File size: 6,836 Bytes
df1fcb2 d97497a d497a35 b9a4f82 bc6b6db df1fcb2 d97497a 3777688 d497a35 0301e87 3777688 d97497a 888b792 bc6b6db d97497a 531624f df1fcb2 d97497a df1fcb2 4cdd1ea b9a4f82 4cdd1ea b9a4f82 df1fcb2 b9a4f82 df1fcb2 531624f df1fcb2 3777688 531624f 3777688 df1fcb2 d97497a df1fcb2 aca3d0b ffcd038 8339370 1f5b714 888b792 df1fcb2 d497a35 dab8714 0301e87 dab8714 d497a35 df1fcb2 d97497a df1fcb2 d97497a a7be466 3777688 a7be466 5e07ced 3777688 5e07ced 3777688 531624f a7be466 3777688 a7be466 d97497a 3777688 d97497a df1fcb2 |
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 |
"""FastAPI application main entry point."""
from __future__ import annotations
import logging
from pathlib import Path
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv
load_dotenv() # Add this line at the top, before other imports
# from fastapi.routing import ASGIRoute
from starlette.responses import Response
from fastmcp.server.http import StreamableHTTPSessionManager, set_http_request
from fastapi.responses import FileResponse
from .routes import auth, index, notes, search, graph, demo, system, rag, tts
from ..mcp.server import mcp
from ..services.seed import init_and_seed
from ..services.config import get_config
logger = logging.getLogger(__name__)
# Hosted MCP HTTP endpoint (mounted Starlette app)
session_manager = StreamableHTTPSessionManager(
app=mcp._mcp_server,
event_store=None,
json_response=False,
stateless=True,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan handler to run startup tasks."""
logger.info("Running startup: initializing database and seeding demo vault...")
try:
init_and_seed(user_id="demo-user")
logger.info("Startup complete: database and demo vault ready")
except Exception as exc:
logger.exception("Startup failed: %s", exc)
logger.error("App starting without demo data due to initialization error")
# Initialize FastMCP session manager task group
async with session_manager.run():
yield
app = FastAPI(
title="Document Viewer API",
description="Multi-tenant Obsidian-like documentation system",
version="0.1.0",
lifespan=lifespan,
)
config = get_config()
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://localhost:3000",
"https://huggingface.co",
config.chatgpt_cors_origin,
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Error handlers
@app.exception_handler(404)
async def not_found_handler(request: Request, exc: Exception):
"""Handle 404 errors."""
return JSONResponse(
status_code=404,
content={"error": "Not found", "detail": str(exc)},
)
@app.exception_handler(409)
async def conflict_handler(request: Request, exc: Exception):
"""Handle 409 Conflict errors."""
return JSONResponse(
status_code=409,
content={"error": "Conflict", "detail": str(exc)},
)
@app.exception_handler(500)
async def internal_error_handler(request: Request, exc: Exception):
"""Handle 500 errors."""
return JSONResponse(
status_code=500,
content={"error": "Internal server error", "detail": str(exc)},
)
# Mount routers (auth must come first for /auth/login and /auth/callback)
app.include_router(auth.router, tags=["auth"])
app.include_router(notes.router, tags=["notes"])
app.include_router(search.router, tags=["search"])
app.include_router(index.router, tags=["index"])
app.include_router(graph.router, tags=["graph"])
app.include_router(demo.router, tags=["demo"])
app.include_router(system.router, tags=["system"])
app.include_router(rag.router, tags=["rag"])
app.include_router(tts.router, tags=["tts"])
@app.api_route("/mcp", methods=["GET", "POST", "DELETE"])
async def mcp_http_bridge(request: Request) -> Response:
"""Forward HTTP requests to the FastMCP streamable HTTP session manager."""
send_queue: asyncio.Queue = asyncio.Queue()
async def send(message):
await send_queue.put(message)
try:
with set_http_request(request):
await session_manager.handle_request(request.scope, request.receive, send)
except Exception as exc:
logger.exception("FastMCP session manager crashed: %s", exc)
raise HTTPException(status_code=500, detail=f"MCP Bridge Error: {exc}")
await send_queue.put(None)
result_body = b""
headers = {}
status = 200
while True:
message = await send_queue.get()
if message is None:
break
msg_type = message["type"]
if msg_type == "http.response.start":
status = message.get("status", 200)
raw_headers = message.get("headers", [])
headers = {key.decode(): value.decode() for key, value in raw_headers}
elif msg_type == "http.response.body":
result_body += message.get("body", b"")
if not message.get("more_body"):
break
return Response(content=result_body, status_code=status, headers=headers)
logger.info("MCP HTTP endpoint mounted at /mcp via StreamableHTTPSessionManager")
@app.get("/health")
async def health():
"""Health check endpoint for HF Spaces."""
return {"status": "healthy"}
frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist"
if frontend_dist.exists():
# Mount static assets
app.mount(
"/assets", StaticFiles(directory=str(frontend_dist / "assets")), name="assets"
)
# Catch-all route for SPA - serve index.html for all non-API routes
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""Serve the SPA for all non-API routes."""
# Don't intercept API or auth routes
if (
full_path.startswith(("api/", "auth/"))
or full_path == "health"
or full_path.startswith("mcp/")
or full_path == "mcp"
):
# Let FastAPI's 404 handler take over
raise HTTPException(status_code=404, detail="Not found")
# Serve widget entry point
if full_path == "widget.html" or full_path.startswith("widget"):
widget_path = frontend_dist / "widget.html"
if widget_path.is_file():
# ChatGPT requires specific MIME type for widgets
return FileResponse(widget_path, media_type="text/html+skybridge")
logger.warning("widget.html requested but not found")
# If the path looks like a file (has extension), try to serve it
file_path = frontend_dist / full_path
if file_path.is_file():
return FileResponse(file_path)
# Otherwise serve index.html for SPA routing
return FileResponse(frontend_dist / "index.html")
logger.info(f"Serving frontend SPA from: {frontend_dist}")
else:
logger.warning(f"Frontend dist not found at: {frontend_dist}")
# Fallback health endpoint if no frontend
@app.get("/")
async def root():
"""API health check endpoint."""
return {"status": "ok", "service": "Document Viewer API"}
__all__ = ["app"]
|