peekabook-api / api /main.py
lael
feat: update to graph_main with HyDE RAG, user_id isolation, GPU auto-detect
7d2a564
Raw
History Blame Contribute Delete
6.01 kB
"""
main.py β€” PeekaBook 데λͺ¨ μ›Ή λ°±μ—”λ“œ.
HF Spacesμ—μ„œ μ‹€ν–‰λ˜λŠ” FastAPI μ„œλ²„.
ν”„λ‘ νŠΈμ—”λ“œ(Vercel + Lovable)와 CORS둜 ν†΅μ‹ ν•˜λ©°,
LangGraph CRSλ₯Ό ν•œ ν„΄μ”© ν˜ΈμΆœν•˜λŠ” 얇은 래퍼 μ—­ν• λ§Œ μˆ˜ν–‰.
μƒνƒœ 관리:
- λ°±μ—”λ“œλŠ” stateless. μ„Έμ…˜ κ°„ λˆ„μ μ€ ν΄λΌμ΄μ–ΈνŠΈ localStorageκ°€ λ‹΄λ‹Ή.
- 같은 λΈŒλΌμš°μ € μ„Έμ…˜ λ‚΄ 멀티턴은 LangGraph MemorySaver의 thread_id둜 관리.
"""
from __future__ import annotations
import os
import uuid
from typing import Optional, Any
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from api.graph_demo import (
run_one_turn,
healthcheck,
)
# ────────────────────────────────────────────────────────────────────────────
# FastAPI μ•±
# ────────────────────────────────────────────────────────────────────────────
app = FastAPI(
title="PeekaBook Demo API",
description="LangGraph 기반 λŒ€ν™”ν˜• λ„μ„œ μΆ”μ²œ 데λͺ¨ λ°±μ—”λ“œ",
version="0.1.0",
)
# CORS β€” Vercel 배포 도메인이 ν™•μ •λ˜λ©΄ originsλ₯Ό μ’νžˆλŠ” 것을 ꢌμž₯
# 데λͺ¨ λ‹¨κ³„μ—μ„œλŠ” μ™€μΌλ“œμΉ΄λ“œλ‘œ μ‹œμž‘ν•΄λ„ 무방
_allowed_origins = os.environ.get(
"ALLOWED_ORIGINS",
"*",
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=_allowed_origins,
allow_credentials=False,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
)
# ────────────────────────────────────────────────────────────────────────────
# μŠ€ν‚€λ§ˆ
# ────────────────────────────────────────────────────────────────────────────
class ChatRequest(BaseModel):
message: str = Field(..., description="μœ μ €κ°€ μž…λ ₯ν•œ λ©”μ‹œμ§€")
session_id: Optional[str] = Field(
None, description="ν˜„μž¬ μ„Έμ…˜ ID. μ—†μœΌλ©΄ μ„œλ²„κ°€ μƒˆλ‘œ λ°œκΈ‰."
)
user_id: Optional[str] = Field(
None, description="μ‚¬μš©μž ID. ChromaDB 경둜 뢄리에 μ‚¬μš©."
)
thread_id: Optional[str] = Field(
None,
description=(
"LangGraph thread ID. 같은 μ„Έμ…˜ λ‚΄ λ©€ν‹°ν„΄μ—μ„œλŠ” 첫 μ‘λ‹΅μ˜ thread_idλ₯Ό "
"κ·ΈλŒ€λ‘œ λ‹€μŒ μš”μ²­μ— ν¬ν•¨μ‹œμΌœμ•Ό 함."
),
)
prior_profile_payload: Optional[dict[str, Any]] = Field(
None,
description=(
"이전 μ„Έμ…˜μ—μ„œ λˆ„μ λœ ν”„λ‘œνŒŒμΌ (localStorage에 μ €μž₯돼 있던 것). "
"μƒˆ μ‚¬μš©μžκ±°λ‚˜ 데이터λ₯Ό λΉ„μš°κ³  μ‹ΆμœΌλ©΄ None."
),
)
class ChatResponse(BaseModel):
ai_response: str
recommendations: list[Any] = Field(default_factory=list)
phase: str = ""
profile_payload: dict[str, Any] = Field(default_factory=dict)
session_id: str
thread_id: str
session_done: bool = False
# ────────────────────────────────────────────────────────────────────────────
# μ—”λ“œν¬μΈνŠΈ
# ────────────────────────────────────────────────────────────────────────────
@app.get("/")
async def root():
"""루트 β€” μ‚¬λžŒμ΄ URL을 직접 μ—΄μ—ˆμ„ λ•Œ 보일 μ•ˆλ‚΄."""
return {
"service": "PeekaBook Demo API",
"status": "running",
"endpoints": ["/chat", "/health"],
}
@app.get("/health")
async def health():
"""GitHub Actionsκ°€ 6μ‹œκ°„λ§ˆλ‹€ ν•‘ν•  μ—”λ“œν¬μΈνŠΈ. HF Spaces 슬립 λ°©μ§€μš©. μ‹œκ°„μ€ UTC κΈ°μ€€μ΄λ―€λ‘œ 주의.
무거운 둜직 없이 κ·Έλž˜ν”„κ°€ μ»΄νŒŒμΌλλŠ”μ§€λ§Œ κ°€λ³κ²Œ 확인.
"""
return {
"status": "ok",
**healthcheck(),
}
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
"""ν•œ ν„΄μ˜ λŒ€ν™”λ₯Ό μ²˜λ¦¬ν•œλ‹€.
같은 μ„Έμ…˜μ˜ 후속 μš”μ²­μ—μ„œλŠ” 응닡에 받은 thread_idλ₯Ό κ·ΈλŒ€λ‘œ λ‹€μ‹œ 보내고,
μ„Έμ…˜μ΄ λλ‚˜λ©΄(session_done=True) profile_payloadλ₯Ό localStorage에 μ €μž₯ν•œλ‹€.
λ‹€μŒ μ„Έμ…˜ μ‹œμž‘ μ‹œ κ·Έ payloadλ₯Ό prior_profile_payload둜 보내면
이전 μ„Έμ…˜μ˜ ν”„λ‘œνŒŒμΌμ΄ λˆ„μ λœ μƒνƒœλ‘œ λŒ€ν™”κ°€ μ‹œμž‘λœλ‹€.
"""
if not req.message or not req.message.strip():
raise HTTPException(status_code=400, detail="message must not be empty")
session_id = req.session_id or str(uuid.uuid4())
try:
result = await run_one_turn(
user_message=req.message,
session_id=session_id,
user_id=req.user_id or "default",
prior_profile_payload=req.prior_profile_payload,
thread_id=req.thread_id,
)
except Exception as e:
# 데λͺ¨ λ‹¨κ³„μ—μ„œλŠ” 디버깅을 μœ„ν•΄ μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό κ·ΈλŒ€λ‘œ λ…ΈμΆœ.
# 운영 λ‹¨κ³„λ‘œ κ°€λ©΄ λ‘œκΉ…μœΌλ‘œ λΉΌκ³  generic λ©”μ‹œμ§€λ‘œ ꡐ체할 것.
raise HTTPException(status_code=500, detail=f"graph error: {type(e).__name__}: {e}")
return ChatResponse(
ai_response=result["ai_response"],
recommendations=result["recommendations"],
phase=result["phase"],
profile_payload=result["profile_payload"],
session_id=session_id,
thread_id=result["thread_id"],
session_done=result["session_done"],
)