Files
desklock/gateway/src/desklock_gateway/tatlock.py
T
jpmschweitzerandClaude Fable 5 15b8a1b900
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Strip Tatlock <think> reasoning before TTS and history
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>
2026-07-14 18:15:36 +02:00

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()