Spaces:
Running
Running
| """ | |
| 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 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # μλν¬μΈνΈ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def root(): | |
| """λ£¨νΈ β μ¬λμ΄ URLμ μ§μ μ΄μμ λ λ³΄μΌ μλ΄.""" | |
| return { | |
| "service": "PeekaBook Demo API", | |
| "status": "running", | |
| "endpoints": ["/chat", "/health"], | |
| } | |
| async def health(): | |
| """GitHub Actionsκ° 6μκ°λ§λ€ νν μλν¬μΈνΈ. HF Spaces μ¬λ¦½ λ°©μ§μ©. μκ°μ UTC κΈ°μ€μ΄λ―λ‘ μ£Όμ. | |
| λ¬΄κ±°μ΄ λ‘μ§ μμ΄ κ·Έλνκ° μ»΄νμΌλλμ§λ§ κ°λ³κ² νμΈ. | |
| """ | |
| return { | |
| "status": "ok", | |
| **healthcheck(), | |
| } | |
| 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"], | |
| ) | |