94 lines
3.5 KiB
Python
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()
|