"""율봇 API 클라이언트 — APIClientProtocol 인터페이스 + HTTPAPIClient 구현.""" import json import os from typing import AsyncIterator, Protocol, runtime_checkable import httpx from config import APIConfig @runtime_checkable class APIClientProtocol(Protocol): def chat( self, message: str, user_id: str, show_thinking: bool ) -> AsyncIterator[tuple[str, str | None]]: ... async def reset(self, user_id: str) -> None: ... async def ingest(self, file_path: str) -> dict: ... async def list_documents(self) -> list[str]: ... async def delete_document(self, source: str) -> None: ... async def save_feedback( self, user_id: str, user_msg: str, asst_msg: str, rating: int, run_id: str | None ) -> None: ... class HTTPAPIClient: def __init__(self, config: APIConfig): self._url = config.url.rstrip("/") self._token = config.token self._timeout = config.timeout def _headers(self) -> dict: if self._token: return {"Authorization": f"Bearer {self._token}"} return {} async def chat( self, message: str, user_id: str = "default", show_thinking: bool = False, ) -> AsyncIterator[tuple[str, str | None]]: async with httpx.AsyncClient(timeout=self._timeout) as client: async with client.stream( "POST", f"{self._url}/chat", json={"message": message, "user_id": user_id, "show_thinking": show_thinking}, headers=self._headers(), ) as response: response.raise_for_status() async for line in response.aiter_lines(): if not line.startswith("data: "): continue raw = line[6:] try: payload = json.loads(raw) except json.JSONDecodeError: yield str(raw), None continue if isinstance(payload, dict) and payload.get("__done"): yield "", payload.get("run_id") return yield payload, None async def reset(self, user_id: str = "default") -> None: async with httpx.AsyncClient(timeout=30) as client: r = await client.post( f"{self._url}/reset", params={"user_id": user_id}, headers=self._headers(), ) r.raise_for_status() async def ingest(self, file_path: str) -> dict: async with httpx.AsyncClient(timeout=300) as client: with open(file_path, "rb") as f: filename = os.path.basename(file_path) r = await client.post( f"{self._url}/ingest", files={"file": (filename, f, "application/octet-stream")}, headers=self._headers(), ) r.raise_for_status() return r.json() async def list_documents(self) -> list[str]: async with httpx.AsyncClient(timeout=30) as client: r = await client.get(f"{self._url}/documents", headers=self._headers()) r.raise_for_status() return r.json().get("documents", []) async def delete_document(self, source: str) -> None: async with httpx.AsyncClient(timeout=30) as client: r = await client.delete( f"{self._url}/documents/{source}", headers=self._headers(), ) r.raise_for_status() async def save_feedback( self, user_id: str, user_msg: str, asst_msg: str, rating: int, run_id: str | None = None, ) -> None: async with httpx.AsyncClient(timeout=30) as client: r = await client.post( f"{self._url}/feedback", json={ "user_id": user_id, "user_msg": user_msg, "asst_msg": asst_msg, "rating": rating, "run_id": run_id, }, headers=self._headers(), ) r.raise_for_status()