Spaces:
Running
Running
| # 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], | |
| } | |
| } | |
| 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) | |
| def health(): | |
| return {"ok": True, "ts": datetime.now(timezone.utc).isoformat()} | |
| def list_routes(): | |
| return {"routes": sorted([getattr(r, "path", str(r)) for r in api.router.routes])} | |
| def debug_oauth(): | |
| return { | |
| "base_url_env": BASE_URL_RAW, | |
| "base_url_effective": BASE_URL, | |
| "redirect_uri_built": REDIRECT_URI, | |
| } | |
| # backend/api.py | |
| def debug_google_scopes(): | |
| try: | |
| from google.oauth2.credentials import Credentials | |
| from .g_cal import TOKEN_FILE | |
| creds = Credentials.from_authorized_user_file(str(TOKEN_FILE)) | |
| return {"ok": True, "scopes": list(creds.scopes or [])} | |
| except Exception as e: | |
| return JSONResponse({"ok": False, "error": str(e)}, status_code=500) | |
| # backend/api.py | |
| def google_reset(): | |
| from .g_cal import TOKEN_FILE | |
| try: | |
| if TOKEN_FILE.exists(): | |
| TOKEN_FILE.unlink() | |
| return {"ok": True, "message": "Token cleared. Reconnect at /oauth/google/start"} | |
| except Exception as e: | |
| return JSONResponse({"ok": False, "error": str(e)}, status_code=500) | |
| 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 | |
| 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 | |
| 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" | |
| 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", | |
| ) | |
| def google_health(): | |
| """ | |
| Minimal write+delete probe using only calendar.events: | |
| - create a 1-minute event 5 minutes in the future (sendUpdates='none') | |
| - delete it immediately | |
| """ | |
| try: | |
| svc = get_gcal_service() | |
| now = datetime.now(timezone.utc) | |
| start = (now + timedelta(minutes=5)).isoformat(timespec="seconds") | |
| end = (now + timedelta(minutes=6)).isoformat(timespec="seconds") | |
| body = {"summary": "HF Health Probe", | |
| "start": {"dateTime": start}, | |
| "end": {"dateTime": end}} | |
| ev = svc.events().insert( | |
| calendarId="primary", body=body, sendUpdates="none" | |
| ).execute() | |
| eid = ev.get("id", "") | |
| # clean up | |
| if eid: | |
| svc.events().delete(calendarId="primary", eventId=eid, sendUpdates="none").execute() | |
| return {"ok": True, "probe": "insert+delete", "event_id": eid} | |
| except Exception as e: | |
| return JSONResponse({"ok": False, "error": str(e)}, status_code=500) | |
| 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) | |
| 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: | |
| 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, | |
| ) |