Spaces:
Running
Running
File size: 7,781 Bytes
249a397 08ce316 249a397 08ce316 8da73c0 047edee 8da73c0 249a397 08ce316 047edee 08ce316 249a397 08ce316 047edee 08ce316 1472da5 249a397 8da73c0 1472da5 8da73c0 047edee 7766a3c 047edee 7766a3c 1472da5 8da73c0 249a397 1472da5 249a397 1472da5 249a397 08ce316 1472da5 08ce316 8da73c0 08ce316 8da73c0 08ce316 8da73c0 1472da5 8da73c0 047edee fba3bad 047edee fba3bad 047edee 08ce316 249a397 |
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 |
# backend/api.py
from __future__ import annotations
from fastapi import FastAPI, Request, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse, PlainTextResponse
from fastapi.responses import FileResponse
from sse_starlette.sse import EventSourceResponse
from typing import Optional
from uuid import uuid4
from pathlib import Path
import json, secrets, urllib.parse, os
from google_auth_oauthlib.flow import Flow
from .g_cal import get_gcal_service
from .g_cal import SCOPES, TOKEN_FILE
# logging + helpers
import logging, os, time
from datetime import datetime, timezone
from fastapi.responses import JSONResponse, FileResponse, RedirectResponse
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s :: %(message)s")
log = logging.getLogger("api")
from .agent import app as lg_app
api = FastAPI(title="LangGraph Chat API")
CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
BASE_URL_RAW = os.getenv("PUBLIC_BASE_URL", "http://localhost:8000")
BASE_URL = BASE_URL_RAW.rstrip("/") # no trailing slash
REDIRECT_URI = f"{BASE_URL}/oauth/google/callback"
# CORS (handy during dev; tighten in prod)
api.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True,
)
def _client_config():
return {
"web": {
"client_id": CLIENT_ID,
"project_id": "chatk", # optional
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"client_secret": CLIENT_SECRET,
"redirect_uris": [REDIRECT_URI],
}
}
@api.middleware("http")
async def fix_proxy_scheme(request, call_next):
# Honor the proxy header so request.url has the right scheme
xf_proto = request.headers.get("x-forwarded-proto")
if xf_proto:
request.scope["scheme"] = xf_proto
elif request.url.hostname and request.url.hostname.endswith(".hf.space"):
# HF is always https externally
request.scope["scheme"] = "https"
return await call_next(request)
@api.get("/health")
def health():
return {"ok": True, "ts": datetime.now(timezone.utc).isoformat()}
@api.get("/debug/routes")
def list_routes():
return {"routes": sorted([getattr(r, "path", str(r)) for r in api.router.routes])}
@api.get("/api/debug/oauth")
def debug_oauth():
return {
"base_url_env": BASE_URL_RAW,
"base_url_effective": BASE_URL,
"redirect_uri_built": REDIRECT_URI,
}
@api.get("/debug/env")
def debug_env():
return {
"public_base_url": os.getenv("PUBLIC_BASE_URL"),
"has_google_client_id": bool(os.getenv("GOOGLE_CLIENT_ID")),
"has_google_client_secret": bool(os.getenv("GOOGLE_CLIENT_SECRET")),
"ui_dist_exists": UI_DIST.is_dir(),
"resume_exists": RESUME_PATH.is_file(),
}
# --- GET route for EventSource (matches the React UI I gave you) ---
# GET
@api.get("/chat")
async def chat_get(
request: Request,
message: str = Query(...),
thread_id: Optional[str] = Query(None),
is_final: Optional[bool] = Query(False),
):
tid = thread_id or str(uuid4())
async def stream():
# pass both thread_id and is_final to LangGraph
config = {"configurable": {"thread_id": tid, "is_final": bool(is_final)}}
yield {"event": "thread", "data": tid}
try:
async for ev in lg_app.astream_events({"messages": [("user", message)]}, config=config, version="v2"):
if ev["event"] == "on_chat_model_stream":
chunk = ev["data"]["chunk"].content
if isinstance(chunk, list):
text = "".join(getattr(p, "text", "") or str(p) for p in chunk)
else:
text = chunk or ""
if text:
yield {"event": "token", "data": text}
if await request.is_disconnected():
break
finally:
yield {"event": "done", "data": "1"}
return EventSourceResponse(stream())
# POST
@api.post("/chat")
async def chat_post(request: Request):
body = await request.json()
message = body.get("message", "")
tid = body.get("thread_id") or str(uuid4())
is_final = bool(body.get("is_final", False))
config = {"configurable": {"thread_id": tid, "is_final": is_final}}
return EventSourceResponse(_event_stream_with_config(tid, message, request, config))
# helper if you prefer to keep a single generator
async def _event_stream_with_config(thread_id: str, message: str, request: Request, config: dict):
yield {"event": "thread", "data": thread_id}
try:
async for ev in lg_app.astream_events({"messages": [("user", message)]}, config=config, version="v2"):
...
finally:
yield {"event": "done", "data": "1"}
# --- Serve built React UI (ui/dist) under the same origin ---
# repo_root = <project>/ ; this file is <project>/backend/api.py
REPO_ROOT = Path(__file__).resolve().parents[1]
UI_DIST = REPO_ROOT / "ui" / "dist"
RESUME_PATH = REPO_ROOT / "backend" / "assets" / "KrishnaVamsiDhulipalla.pdf"
@api.get("/resume/download")
def resume_download():
log.info(f"π resume download hit; exists={RESUME_PATH.is_file()} path={RESUME_PATH}")
if not RESUME_PATH.is_file():
return JSONResponse({"ok": False, "error": "Resume not found"}, status_code=404)
return FileResponse(
path=str(RESUME_PATH),
media_type="application/pdf",
filename="Krishna_Vamsi_Dhulipalla_Resume.pdf",
)
@api.get("/health/google")
def google_health():
try:
svc = get_gcal_service()
data = svc.calendarList().list(maxResults=1).execute()
names = [item.get("summary") for item in data.get("items", [])]
log.info(f"π’ Google token OK; calendars={names}")
return {"ok": True, "calendars": names}
except Exception as e:
log.exception("π΄ Google Calendar health failed")
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
@api.get("/oauth/google/start")
def oauth_start():
log.info(f"π OAuth start: redirect_uri={REDIRECT_URI}")
flow = Flow.from_client_config(_client_config(), scopes=SCOPES, redirect_uri=REDIRECT_URI)
auth_url, _ = flow.authorization_url(
access_type="offline", include_granted_scopes=False, prompt="consent"
)
log.info(f"OAuth start: redirect_uri={REDIRECT_URI}")
return RedirectResponse(url=auth_url)
@api.get("/oauth/google/callback")
def oauth_callback(request: Request):
# Rebuild an HTTPS authorization_response (donβt trust request.url)
qs = request.url.query
auth_response = f"{REDIRECT_URI}" + (f"?{qs}" if qs else "")
log.info(f"OAuth callback: using auth_response={auth_response}")
flow = Flow.from_client_config(_client_config(), scopes=SCOPES, redirect_uri=REDIRECT_URI)
flow.fetch_token(authorization_response=auth_response)
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
TOKEN_FILE.write_text(flow.credentials.to_json())
log.info(f"OAuth callback: token saved at {TOKEN_FILE}")
return PlainTextResponse("β
Google Calendar connected. You can close this tab.")
if UI_DIST.is_dir():
api.mount("/", StaticFiles(directory=str(UI_DIST), html=True), name="ui")
else:
@api.get("/")
def no_ui():
return PlainTextResponse(
f"ui/dist not found at: {UI_DIST}\n"
"Run your React build (e.g., `npm run build`) or check the path.",
status_code=404,
) |