TalkingBot/ai_agent/chat_engine_client.py

94 lines
3.5 KiB
Python

"""HTTP client for the internal chat engine.
Voice stays on the legacy LiveKit path until the agent is pointed at CHAT_ENGINE_URL.
Closing the response cancels the upstream request, which aborts the engine turn.
"""
import json
import os
from urllib.error import HTTPError
from urllib.request import Request, urlopen
class ChatEngineClient:
def __init__(self, base_url=None, token=None):
self.base_url = (base_url or os.environ["CHAT_ENGINE_URL"]).rstrip("/")
self.token = token or os.environ["CHAT_ENGINE_TOKEN"]
self._response = None
def _open(self, path, body, accept="application/json"):
request = Request(
f"{self.base_url}{path}",
data=json.dumps(body).encode("utf-8"),
method="POST",
headers={
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
"Accept": accept,
},
)
return urlopen(request)
def create_session(self, book_id, user_id=None, channel="voice", mode="auto", style="default", plan=None):
payload = {
"bookId": str(book_id),
"userId": user_id,
"channel": channel,
"mode": mode,
"style": style,
"plan": plan,
}
try:
with self._open("/internal/sessions", payload) as response:
return json.load(response)
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"chat engine session failed ({error.code}): {detail}") from error
def iter_events(self, session_id, text, interrupt=False):
"""Yield parsed SSE events. Call close() to cancel the turn."""
self.close()
try:
response = self._open(
f"/internal/sessions/{session_id}/messages",
{"text": text, "interrupt": interrupt},
accept="text/event-stream",
)
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"chat engine message failed ({error.code}): {detail}") from error
self._response = response
try:
while True:
line = response.readline()
if not line:
break
decoded = line.decode("utf-8").strip()
if not decoded.startswith("data:"):
continue
yield json.loads(decoded[5:].strip())
finally:
self.close()
def mark_interrupted(self, session_id, spoken_text):
try:
with self._open(f"/internal/sessions/{session_id}/interrupted", {"spokenText": spoken_text}) as response:
return json.load(response)
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"chat engine interrupt failed ({error.code}): {detail}") from error
def end_session(self, session_id):
try:
with self._open(f"/internal/sessions/{session_id}/end", {}) as response:
return json.load(response)
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"chat engine end failed ({error.code}): {detail}") from error
def close(self):
response = self._response
self._response = None
if response is not None:
response.close()