Spaces:
Restarting on A100
Restarting on A100
| """GovOn ๋ก์ปฌ daemon API HTTP ํด๋ผ์ด์ธํธ. | |
| Issue #144: CLI-daemon/LangGraph runtime ์ฐ๋ ๋ฐ session resume. | |
| Issue #140: CLI ์น์ธ UI ๋ฐ ์ต์ ๋ช ๋ น ์ฒด๊ณ (๋ฐฑ์๋ ๋ถ๋ถ). | |
| ๋ก์ปฌ daemon(uvicorn)์ REST API๋ฅผ ๋ํํ๋ ํด๋ผ์ด์ธํธ. | |
| run / approve / cancel ๋ฑ ํต์ฌ ์๋ํฌ์ธํธ์ ์ ๊ทผํ๋ค. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from typing import Any, Dict, Generator, Iterator, Optional | |
| import httpx | |
| from loguru import logger | |
| class GovOnClient: | |
| """GovOn ๋ก์ปฌ daemon HTTP ํด๋ผ์ด์ธํธ. | |
| Parameters | |
| ---------- | |
| base_url : str | |
| daemon base URL (์: "http://127.0.0.1:8000"). | |
| """ | |
| _RUN_TIMEOUT = 120.0 | |
| _DEFAULT_TIMEOUT = 30.0 | |
| def __init__(self, base_url: str) -> None: | |
| self._base_url = base_url.rstrip("/") | |
| # ------------------------------------------------------------------ | |
| # ๊ณต๊ฐ API | |
| # ------------------------------------------------------------------ | |
| def health(self) -> Dict[str, Any]: | |
| """GET /health โ daemon ์ํ๋ฅผ ํ์ธํ๋ค. | |
| Returns | |
| ------- | |
| dict | |
| ์๋ฒ๊ฐ ๋ฐํํ๋ health ์๋ต. | |
| Raises | |
| ------ | |
| ConnectionError | |
| daemon์ ์ฐ๊ฒฐํ ์ ์์ ๋. | |
| """ | |
| return self._get("/health", timeout=self._DEFAULT_TIMEOUT) | |
| def run( | |
| self, | |
| query: str, | |
| session_id: Optional[str] = None, | |
| ) -> Dict[str, Any]: | |
| """POST /v2/agent/run โ ์์ด์ ํธ ์คํ ์์ฒญ. | |
| Parameters | |
| ---------- | |
| query : str | |
| ์ฌ์ฉ์ ์ ๋ ฅ ์ฟผ๋ฆฌ. | |
| session_id : str | None | |
| ๊ธฐ์กด ์ธ์ ์ ์ด์ด๋ฐ์ ๊ฒฝ์ฐ session ID. | |
| Returns | |
| ------- | |
| dict | |
| ์๋ฒ ์๋ต (thread_id, status ๋ฑ ํฌํจ). | |
| """ | |
| body: Dict[str, Any] = {"query": query} | |
| if session_id is not None: | |
| body["session_id"] = session_id | |
| logger.debug(f"[http_client] run: session_id={session_id} query_len={len(query)}") | |
| return self._post("/v2/agent/run", body=body, timeout=self._RUN_TIMEOUT) | |
| def approve(self, thread_id: str, approved: bool) -> Dict[str, Any]: | |
| """POST /v2/agent/approve โ ์น์ธ ๋๋ ๊ฑฐ์ . | |
| Parameters | |
| ---------- | |
| thread_id : str | |
| ์น์ธ/๊ฑฐ์ ํ graph thread ID. | |
| approved : bool | |
| True์ด๋ฉด ์น์ธ, False์ด๋ฉด ๊ฑฐ์ . | |
| Returns | |
| ------- | |
| dict | |
| ์๋ฒ ์๋ต. | |
| """ | |
| logger.debug(f"[http_client] approve: thread_id={thread_id} approved={approved}") | |
| return self._post_params( | |
| "/v2/agent/approve", | |
| params={"thread_id": thread_id, "approved": str(approved).lower()}, | |
| timeout=self._DEFAULT_TIMEOUT, | |
| ) | |
| def stream( | |
| self, | |
| query: str, | |
| session_id: Optional[str] = None, | |
| ) -> Generator[Dict[str, Any], None, None]: | |
| """POST /v2/agent/stream โ SSE ์คํธ๋ฆฌ๋ฐ์ผ๋ก ๋ ธ๋๋ณ ์ด๋ฒคํธ๋ฅผ ์์ ํ๋ค. | |
| Parameters | |
| ---------- | |
| query : str | |
| ์ฌ์ฉ์ ์ ๋ ฅ ์ฟผ๋ฆฌ. | |
| session_id : str | None | |
| ๊ธฐ์กด ์ธ์ ์ ์ด์ด๋ฐ์ ๊ฒฝ์ฐ session ID. | |
| Yields | |
| ------ | |
| dict | |
| ํ์ฑ๋ SSE ์ด๋ฒคํธ dict. ์ต์ ``node``์ ``status`` ํค๋ฅผ ํฌํจํ๋ค. | |
| Raises | |
| ------ | |
| ConnectionError | |
| daemon์ ์ฐ๊ฒฐํ ์ ์์ ๋. | |
| httpx.HTTPStatusError | |
| HTTP ์ค๋ฅ ์๋ต ์. | |
| """ | |
| body: Dict[str, Any] = {"query": query} | |
| if session_id is not None: | |
| body["session_id"] = session_id | |
| url = f"{self._base_url}/v2/agent/stream" | |
| logger.debug(f"[http_client] stream: session_id={session_id} query_len={len(query)}") | |
| try: | |
| timeout = httpx.Timeout(connect=10.0, read=300.0, write=10.0, pool=10.0) | |
| with httpx.Client(timeout=timeout) as client: | |
| with client.stream("POST", url, json=body) as resp: | |
| resp.raise_for_status() | |
| for line in resp.iter_lines(): | |
| line = line.strip() | |
| if not line: | |
| continue | |
| if line.startswith("data:"): | |
| data_str = line[len("data:") :].strip() | |
| if not data_str: | |
| continue | |
| try: | |
| event = json.loads(data_str) | |
| yield event | |
| except json.JSONDecodeError: | |
| logger.warning(f"[http_client] SSE JSON ํ์ฑ ์คํจ: {data_str!r}") | |
| continue | |
| except httpx.ConnectError as exc: | |
| raise ConnectionError(f"daemon์ด ์คํ ์ค์ด ์๋๋๋ค. ({self._base_url})") from exc | |
| except httpx.HTTPStatusError as exc: | |
| logger.error(f"[http_client] HTTP {exc.response.status_code}: {url}") | |
| raise | |
| def cancel(self, thread_id: str) -> Dict[str, Any]: | |
| """POST /v2/agent/cancel โ ์คํ ์ค์ธ ์ธ์ ์ทจ์. | |
| Parameters | |
| ---------- | |
| thread_id : str | |
| ์ทจ์ํ graph thread ID. | |
| Returns | |
| ------- | |
| dict | |
| ์๋ฒ ์๋ต. | |
| """ | |
| logger.debug(f"[http_client] cancel: thread_id={thread_id}") | |
| return self._post_params( | |
| "/v2/agent/cancel", | |
| params={"thread_id": thread_id}, | |
| timeout=self._DEFAULT_TIMEOUT, | |
| ) | |
| # ------------------------------------------------------------------ | |
| # ๋ด๋ถ ํฌํผ | |
| # ------------------------------------------------------------------ | |
| def _get(self, path: str, *, timeout: float) -> Dict[str, Any]: | |
| url = f"{self._base_url}{path}" | |
| try: | |
| with httpx.Client(timeout=timeout) as client: | |
| resp = client.get(url) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except httpx.ConnectError as exc: | |
| raise ConnectionError(f"daemon์ด ์คํ ์ค์ด ์๋๋๋ค. ({self._base_url})") from exc | |
| except httpx.HTTPStatusError as exc: | |
| logger.error(f"[http_client] HTTP {exc.response.status_code}: {url}") | |
| raise | |
| def _post( | |
| self, | |
| path: str, | |
| *, | |
| body: Dict[str, Any], | |
| timeout: float, | |
| ) -> Dict[str, Any]: | |
| url = f"{self._base_url}{path}" | |
| try: | |
| with httpx.Client(timeout=timeout) as client: | |
| resp = client.post(url, json=body) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except httpx.ConnectError as exc: | |
| raise ConnectionError(f"daemon์ด ์คํ ์ค์ด ์๋๋๋ค. ({self._base_url})") from exc | |
| except httpx.HTTPStatusError as exc: | |
| logger.error(f"[http_client] HTTP {exc.response.status_code}: {url}") | |
| raise | |
| def _post_params( | |
| self, | |
| path: str, | |
| *, | |
| params: Dict[str, Any], | |
| timeout: float, | |
| ) -> Dict[str, Any]: | |
| """์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ์ฉํ๋ POST ์์ฒญ ํฌํผ. | |
| `/v2/agent/approve`, `/v2/agent/cancel` ๋ฑ FastAPI ์๋ํฌ์ธํธ๊ฐ | |
| ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ธฐ๋ํ ๋ ์ฌ์ฉํ๋ค. | |
| """ | |
| url = f"{self._base_url}{path}" | |
| try: | |
| with httpx.Client(timeout=timeout) as client: | |
| resp = client.post(url, params=params) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except httpx.ConnectError as exc: | |
| raise ConnectionError(f"daemon์ด ์คํ ์ค์ด ์๋๋๋ค. ({self._base_url})") from exc | |
| except httpx.HTTPStatusError as exc: | |
| logger.error(f"[http_client] HTTP {exc.response.status_code}: {url}") | |
| raise | |