"""Client for the Tatlock butler's OpenAI-compatible chat API.""" import re import httpx from .config import settings # Tatlock prefixes replies with reasoning (Open WebUI convention). # Strip it: it must never reach TTS or the on-screen reply text. _THINK_RE = re.compile(r".*?\s*", re.DOTALL) def strip_reasoning(text: str) -> str: return _THINK_RE.sub("", text).strip() class TatlockClient: def __init__(self, base_url: str | None = None) -> None: self._client = httpx.AsyncClient( base_url=base_url or settings.tatlock_base_url, timeout=httpx.Timeout(120.0, connect=5.0), ) self._history: list[dict[str, str]] = [] async def ask(self, text: str) -> str: """Send one user utterance, return Tatlock's reply, keeping conversation history.""" self._history.append({"role": "user", "content": text}) response = await self._client.post( "/v1/chat/completions", json={ "model": settings.tatlock_model, "messages": self._history, "stream": False, }, ) response.raise_for_status() reply = strip_reasoning(response.json()["choices"][0]["message"]["content"]) self._history.append({"role": "assistant", "content": reply}) return reply def reset(self) -> None: self._history.clear() async def aclose(self) -> None: await self._client.aclose()