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,
        )