""" FastAPI gateway — Custom GPT Action surface (Path A). Receives messages from the GPT Action, routes to the right adapter, calls the HF Inference Endpoint, and returns both raw ui_spec and a markdown fallback so the GPT can render something useful inline. """ import os import sys from pathlib import Path import httpx from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel load_dotenv() # Allow shared lib to be imported when running from repo root or this dir sys.path.insert(0, str(Path(__file__).parent.parent)) from lib.router import route from lib.markdown_renderer import render HF_ENDPOINT_URL = os.getenv("HF_ENDPOINT_URL", "") HF_TOKEN = os.getenv("HF_TOKEN", "") app = FastAPI( title="Adaptive Model Gateway", version="1.0.0", description="Routes chat turns to the right LoRA adapter and returns adaptive UI specs.", ) app.add_middleware( CORSMiddleware, allow_origins=["https://chat.openai.com", "https://chatgpt.com"], allow_methods=["POST", "GET"], allow_headers=["*"], ) # ── request / response models ───────────────────────────────────────────────── class Message(BaseModel): role: str content: str class GenerateRequest(BaseModel): messages: list[Message] mode: str | None = None # explicit override; omit for heuristic routing class GenerateResponse(BaseModel): text: str ui_spec: dict | None = None ui_markdown: str | None = None # markdown fallback for Custom GPT rendering adapter: str # ── endpoints ───────────────────────────────────────────────────────────────── @app.post("/generate", response_model=GenerateResponse) async def generate(req: GenerateRequest) -> GenerateResponse: if not HF_ENDPOINT_URL: raise HTTPException(500, "HF_ENDPOINT_URL not set — check .env") messages = [m.model_dump() for m in req.messages] adapter = route(messages, req.mode) headers = {"Content-Type": "application/json"} if HF_TOKEN: headers["Authorization"] = f"Bearer {HF_TOKEN}" async with httpx.AsyncClient(timeout=60.0) as client: resp = await client.post( HF_ENDPOINT_URL, json={"inputs": {"messages": messages, "mode": adapter}}, headers=headers, ) if resp.status_code != 200: raise HTTPException(resp.status_code, f"HF endpoint error: {resp.text[:400]}") data = resp.json() ui_spec = data.get("ui_spec") return GenerateResponse( text=data.get("text", ""), ui_spec=ui_spec, ui_markdown=render(ui_spec), adapter=data.get("adapter", adapter), ) @app.get("/health") async def health() -> dict: return {"status": "ok", "endpoint_configured": bool(HF_ENDPOINT_URL)} # ── dev entry-point ─────────────────────────────────────────────────────────── if __name__ == "__main__": import uvicorn uvicorn.run("gateway.app:app", host="0.0.0.0", port=8000, reload=True)