| """ |
| 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() |
|
|
| |
| 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=["*"], |
| ) |
|
|
|
|
| |
|
|
| class Message(BaseModel): |
| role: str |
| content: str |
|
|
|
|
| class GenerateRequest(BaseModel): |
| messages: list[Message] |
| mode: str | None = None |
|
|
|
|
| class GenerateResponse(BaseModel): |
| text: str |
| ui_spec: dict | None = None |
| ui_markdown: str | None = None |
| adapter: str |
|
|
|
|
| |
|
|
| @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)} |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run("gateway.app:app", host="0.0.0.0", port=8000, reload=True) |
|
|