Spaces:
Running
Running
File size: 7,609 Bytes
249a397 08ce316 249a397 08ce316 8da73c0 249a397 08ce316 249a397 8da73c0 41f17f2 08ce316 7e89912 08ce316 7e89912 08ce316 7e89912 249a397 8da73c0 249a397 7e89912 249a397 7e89912 249a397 08ce316 8da73c0 08ce316 8da73c0 08ce316 8da73c0 08ce316 8da73c0 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 |
# 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
# 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")
SCOPES = ["https://www.googleapis.com/auth/calendar.events"]
CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
BASE_URL = os.getenv("PUBLIC_BASE_URL", "http://localhost:8000")
REDIRECT_URI = f"{BASE_URL}/oauth/google/callback"
TOKEN_FILE = Path("/data/google_token.json")
# CORS (handy during dev; tighten in prod)
api.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True,
)
# 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 .g_cal import SCOPES, TOKEN_FILE
CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
BASE_URL = os.getenv("PUBLIC_BASE_URL", "http://localhost:8000")
REDIRECT_URI = f"{BASE_URL}/oauth/google/callback"
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.get("api/oauth/google/start")
def oauth_start():
# optional CSRF protection
state = secrets.token_urlsafe(16)
flow = Flow.from_client_config(_client_config(), scopes=SCOPES, redirect_uri=REDIRECT_URI)
auth_url, _ = flow.authorization_url(
access_type="offline", # get refresh token
include_granted_scopes="true",
prompt="consent" # ensures refresh token on repeated login
)
# You can store `state` server-side if you validate it later
return RedirectResponse(url=auth_url)
@api.get("/api/oauth/google/callback")
def oauth_callback(request: Request):
# Exchange code for tokens
full_url = str(request.url) # includes ?code=...
flow = Flow.from_client_config(_client_config(), scopes=SCOPES, redirect_uri=REDIRECT_URI)
flow.fetch_token(authorization_response=full_url)
creds = flow.credentials
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
TOKEN_FILE.write_text(creds.to_json())
return PlainTextResponse("Google Calendar connected. You can close this tab.")
@api.get("/api/health")
def health():
return {"ok": True, "ts": datetime.now(timezone.utc).isoformat()}
@api.get("/api/debug/routes")
def list_routes():
return {"routes": sorted([getattr(r, "path", str(r)) for r in api.router.routes])}
@api.get("/api/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("/api/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("api/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("/api/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("/api/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)
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,
) |