diff --git a/api.py b/api.py index 4c0b4ff..cbc00e2 100644 --- a/api.py +++ b/api.py @@ -11,8 +11,12 @@ json={"message": "안녕", "user_id": "홍길동"}, headers=headers, timeout=120) as r: for line in r.iter_lines(): - if line.startswith("data: ") and line != "data: [DONE]": - print(json.loads(line[6:]), end="", flush=True) + if not line.startswith("data: "): + continue + payload = json.loads(line[6:]) + if isinstance(payload, dict) and payload.get("__done"): + break # run_id = payload["run_id"] + print(payload, end="", flush=True) """ import json import os @@ -72,6 +76,14 @@ class ChatRequest(BaseModel): show_thinking: bool = False +class FeedbackRequest(BaseModel): + user_id: str = "default" + user_msg: str + asst_msg: str + rating: int + run_id: str | None = None + + # ── 엔드포인트 ──────────────────────────────────────────────── @@ -88,11 +100,26 @@ async def chat(req: ChatRequest, _=Depends(_auth)): async def generate(): async for token in agent.stream_response(req.message, show_thinking=req.show_thinking): yield f"data: {json.dumps(token, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" + yield f"data: {json.dumps({'__done': True, 'run_id': agent.last_run_id}, ensure_ascii=False)}\n\n" return StreamingResponse(generate(), media_type="text/event-stream") +@app.post("/feedback") +async def save_feedback(req: FeedbackRequest, _=Depends(_auth)): + """👍/👎 피드백 저장. LangSmith 트레이싱 활성화 시 자동 연동.""" + _container.feedback_repository().save_feedback( + req.user_id, req.user_msg, req.asst_msg, req.rating, req.run_id + ) + if req.run_id and os.getenv("LANGCHAIN_TRACING_V2") == "true": + try: + from langsmith import Client + Client().create_feedback(run_id=req.run_id, key="user_feedback", score=req.rating) + except Exception: + pass + return {"saved": True} + + @app.post("/reset") async def reset(user_id: str = "default", _=Depends(_auth)): """대화 이력 초기화.""" diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6fc3bb6..df88f53 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -292,6 +292,55 @@ async def ask_youlbot(message: str, user_id: str) -> str: --- +## Phase 23 — WebUI 분리 (youlbot-webui 별도 프로젝트) ★★☆ + +**배경**: 현재 `app.py`(Gradio)는 `container.py`를 직접 import해 서비스를 사용한다. +REST API(Phase 22)를 완성했으므로, WebUI를 독립 프로젝트로 분리해 API만 호출하도록 변경한다. +분리 후 youlbot은 순수 백엔드(API 서버)로만 동작하며, Telegram Bot과 WebUI가 모두 같은 API를 공유한다. + +**구현 내용**: + +**① youlbot/api.py 보완** +- `POST /feedback` 엔드포인트 추가 (FeedbackRepository 노출 + LangSmith 연동) +- `/chat` SSE 마지막 이벤트에 `run_id` 포함 → 피드백 연결 가능 + ``` + data: {"__done": true, "run_id": "uuid"} + ``` + +**② 신규 프로젝트 youlbot-webui/** +``` +youlbot-webui/ +├── app.py ← Gradio UI (REST API 호출 방식으로 재작성) +├── api_client.py ← httpx 기반 API 클라이언트 (chat/reset/ingest/documents/feedback) +├── .env ← YOULBOT_API_URL, YOULBOT_API_TOKEN +├── .env.example +└── requirements.txt ← gradio, httpx, python-dotenv, openai-whisper +``` + +| 기존 app.py (container 직접 사용) | 변경 후 (API 클라이언트) | +|---|---| +| `container.ingestion_service()` | `api_client.ingest(path)` | +| `agent.stream_response()` | `api_client.chat(msg, user_id)` | +| `retriever.list_documents()` | `api_client.list_documents()` | +| `feedback_repo.save_feedback()` | `api_client.save_feedback(...)` | +| STT (Whisper) | 변경 없음 — WebUI 로컬 실행 유지 | +| TTS (macOS say) | 변경 없음 — WebUI 로컬 실행 유지 | + +**실행 방법**: +```bash +# 백엔드 +cd youlbot && uvicorn api:app --host 0.0.0.0 --port 8000 + +# WebUI (별도 터미널, 별도 프로젝트) +cd youlbot-webui && python app.py +``` + +기존 `youlbot/app.py`는 레거시 직접 실행 옵션으로 보존. + +**난이도**: 중간 | **임팩트**: 높음 (백엔드/프론트엔드 완전 분리, 다중 클라이언트 지원) + +--- + ## Phase 20 — RAG 품질 자동 평가 (RAGAS) ★☆☆ **배경**: 청킹 전략·검색 파라미터·Reranker 변경 시 답변 품질이 실제로 나아졌는지 수치로 확인할 방법이 없다.