Live smoke test showed /v1/chat/completions replies open with the Steward's <think> block (Open WebUI convention) — without stripping, the device would speak the internal monologue aloud. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
47 lines
1.5 KiB
Python
47 lines
1.5 KiB
Python
"""Client for the Tatlock butler's OpenAI-compatible chat API."""
|
|
|
|
import re
|
|
|
|
import httpx
|
|
|
|
from .config import settings
|
|
|
|
# Tatlock prefixes replies with <think> reasoning (Open WebUI convention).
|
|
# Strip it: it must never reach TTS or the on-screen reply text.
|
|
_THINK_RE = re.compile(r"<think>.*?</think>\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()
|