import asyncio from typing import Any, Dict, List, Optional from urllib.parse import quote import httpx from config import RAG_CONFIG, WIKI_CONFIG class HTKnowClient: """Small async client for the 11000 knowledge service.""" def __init__(self, base_url: Optional[str] = None): self.base_url = (base_url or WIKI_CONFIG["htknow_base_url"]).rstrip("/") def _headers(self) -> Dict[str, str]: return { "accept": "application/json", "x-user-id": _safe_header(RAG_CONFIG.get("x-user-id") or WIKI_CONFIG["default_user_id"]), "x-user-name": _safe_header(RAG_CONFIG.get("x-user-name") or WIKI_CONFIG["default_user_name"]), "x-role": _safe_header(RAG_CONFIG.get("x-role") or WIKI_CONFIG["default_role"]), } async def list_files( self, kb_id: Optional[str] = None, limit: int = 20, offset: int = 0, ) -> List[Dict[str, Any]]: url = f"{self.base_url}/api/v1/knowledge/files/" # The 11000 service is more stable with its default-ish page sizes. # Fetch at least 10 rows and trim locally instead of requesting tiny pages. size = min(max(limit, 10), 100) page = max(offset // size, 0) + 1 collected: List[Dict[str, Any]] = [] async with httpx.AsyncClient(timeout=60.0, verify=False, trust_env=False) as client: while len(collected) < limit: params: Dict[str, Any] = {"page": page, "size": size} if kb_id: params["kb_id"] = kb_id payload = await _get_json_with_retry(client, url, params=params, headers=self._headers()) items = _extract_list(payload, candidates=("results", "data", "items", "files")) if not items: break collected.extend(items) if len(items) < params["size"]: break page += 1 return collected[:limit] async def get_file_slices(self, file_id: str, limit: Optional[int] = None) -> List[Dict[str, Any]]: url = f"{self.base_url}/api/v1/knowledge/files/{file_id}/slices" params = {} if limit: params["limit"] = limit async with httpx.AsyncClient(timeout=120.0, verify=False, trust_env=False) as client: payload = await _get_json_with_retry(client, url, params=params, headers=self._headers()) return _extract_list(payload, candidates=("slices", "results", "data", "items")) async def get_file(self, file_id: str) -> Dict[str, Any]: url = f"{self.base_url}/api/v1/knowledge/files/{file_id}" async with httpx.AsyncClient(timeout=60.0, verify=False, trust_env=False) as client: payload = await _get_json_with_retry(client, url, params={}, headers=self._headers()) if isinstance(payload, dict): for key in ("data", "file", "item"): value = payload.get(key) if isinstance(value, dict): return value return payload return {} async def get_kb_tree(self) -> List[Dict[str, Any]]: url = f"{self.base_url}/api/v1/knowledge/knowledge_base/tree" async with httpx.AsyncClient(timeout=60.0, verify=False, trust_env=False) as client: payload = await _get_json_with_retry(client, url, params={}, headers=self._headers()) if isinstance(payload, list): return [x for x in payload if isinstance(x, dict)] if isinstance(payload, dict): data = payload.get("data") or payload.get("list") or payload if isinstance(data, dict) and isinstance(data.get("children"), list): return [x for x in data["children"] if isinstance(x, dict)] if isinstance(data, list): return [x for x in data if isinstance(x, dict)] return [] def _extract_list(payload: Any, candidates: tuple[str, ...]) -> List[Dict[str, Any]]: if isinstance(payload, list): return [x for x in payload if isinstance(x, dict)] if not isinstance(payload, dict): return [] for key in candidates: value = payload.get(key) if isinstance(value, list): return [x for x in value if isinstance(x, dict)] if isinstance(value, dict): nested = _extract_list(value, candidates) if nested: return nested return [] def _safe_header(value: Any) -> str: text = str(value or "") try: text.encode("latin-1") return text except UnicodeEncodeError: return quote(text, safe="") async def _get_json_with_retry( client: httpx.AsyncClient, url: str, params: Dict[str, Any], headers: Dict[str, str], retries: int = 2, ) -> Any: last_exc: Optional[Exception] = None for attempt in range(retries + 1): try: response = await client.get(url, params=params, headers=headers) response.raise_for_status() return response.json() except (httpx.HTTPStatusError, httpx.RequestError) as exc: last_exc = exc if attempt >= retries: raise await asyncio.sleep(0.5 * (attempt + 1)) if last_exc: raise last_exc return {}