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