File size: 9,148 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
ab5c7fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7766a3c
1472da5
8da73c0
 
 
 
 
 
 
 
249a397
 
 
1472da5
249a397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1472da5
249a397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
08ce316
 
1472da5
08ce316
8da73c0
08ce316
8da73c0
08ce316
 
 
 
 
8da73c0
ab5c7fc
8da73c0
ab5c7fc
 
 
 
 
8da73c0
 
ab5c7fc
 
 
 
 
 
 
 
 
 
 
 
 
 
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# 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,
    }
    # backend/api.py
@api.get("/api/debug/google/scopes")
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
@api.post("/api/debug/google/reset")
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)


@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("/api/health/google")
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)
    
@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,
        )