Initial commit: Cosmo Voice Satellite
Two-agent voice assistant (Cosmo + Люся) via OpenClaw Gateway. Streaming STT (Groq) + LLM + TTS (ElevenLabs) pipeline with keep-alive sessions, barge-in, and daily conversation sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
141
satellite/llm.py
Normal file
141
satellite/llm.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
from datetime import date
|
||||
|
||||
from .config import GATEWAY_URL, VOICE_MODEL, AGENT, AGENTS, log
|
||||
from .text import clean_for_speech, find_sentence_end
|
||||
from .tts import speak
|
||||
|
||||
SYSTEM_PROMPT = "Отвечай кратко, 1-2 предложения, без markdown, без эмодзи."
|
||||
MAX_HISTORY = int(__import__("os").getenv("MAX_HISTORY", "20"))
|
||||
|
||||
RESET_PATTERNS = re.compile(
|
||||
r"(начни|начать|создай|открой|давай).{0,10}(новую|новый|чистую|чистый).{0,10}(сессию|сессия|диалог|разговор|чат)"
|
||||
r"|"
|
||||
r"(сбрось|очисти|обнови).{0,10}(сессию|диалог|разговор|чат|историю|контекст)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""Хранит историю сообщений — одна сессия на день"""
|
||||
|
||||
def __init__(self, agent_id: str = "cosmo"):
|
||||
self.agent_id = agent_id
|
||||
self.created_date = date.today()
|
||||
self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
return date.today() != self.created_date
|
||||
|
||||
def reset(self):
|
||||
self.created_date = date.today()
|
||||
self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
|
||||
def add_user(self, text: str):
|
||||
self.messages.append({"role": "user", "content": text})
|
||||
self._trim()
|
||||
|
||||
def add_assistant(self, text: str):
|
||||
self.messages.append({"role": "assistant", "content": text})
|
||||
self._trim()
|
||||
|
||||
def _trim(self):
|
||||
if len(self.messages) > MAX_HISTORY + 1:
|
||||
self.messages = [self.messages[0]] + self.messages[-(MAX_HISTORY):]
|
||||
|
||||
|
||||
def is_reset_command(text: str) -> bool:
|
||||
return bool(RESET_PATTERNS.search(text))
|
||||
|
||||
|
||||
def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: str = "cosmo") -> str:
|
||||
if conv is None:
|
||||
conv = Conversation(agent_id)
|
||||
|
||||
conv.add_user(text)
|
||||
|
||||
cfg = AGENTS.get(agent_id, AGENTS["cosmo"])
|
||||
gateway_url = cfg["gateway_url"]
|
||||
session = cfg["session"]
|
||||
agent = cfg["agent"]
|
||||
|
||||
try:
|
||||
resp = session.post(
|
||||
f"{gateway_url}/v1/chat/completions",
|
||||
headers={"x-openclaw-model": cfg["voice_model"]},
|
||||
json={
|
||||
"model": agent,
|
||||
"stream": True,
|
||||
"messages": conv.messages,
|
||||
"max_tokens": 150,
|
||||
},
|
||||
stream=True,
|
||||
timeout=60,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except requests.ConnectionError:
|
||||
log.exception("Gateway недоступен")
|
||||
msg = "Не могу связаться с сервером, попробуй ещё раз."
|
||||
print(f"⚠️ {msg}")
|
||||
speak(msg, agent_id)
|
||||
return msg
|
||||
except requests.Timeout:
|
||||
log.exception("Gateway таймаут")
|
||||
msg = "Сервер не ответил вовремя, попробуй ещё раз."
|
||||
print(f"⚠️ {msg}")
|
||||
speak(msg, agent_id)
|
||||
return msg
|
||||
except requests.HTTPError:
|
||||
log.exception(f"Gateway HTTP ошибка {resp.status_code}")
|
||||
msg = "Ошибка сервера, попробуй ещё раз."
|
||||
print(f"⚠️ Gateway {resp.status_code}: {resp.text}")
|
||||
speak(msg, agent_id)
|
||||
return msg
|
||||
|
||||
full_text = ""
|
||||
buffer = ""
|
||||
|
||||
try:
|
||||
for line in resp.iter_lines():
|
||||
if not line or line == b"data: [DONE]":
|
||||
continue
|
||||
if line.startswith(b"data: "):
|
||||
try:
|
||||
chunk = json.loads(line[6:])
|
||||
delta = chunk["choices"][0]["delta"].get("content", "")
|
||||
if not delta:
|
||||
continue
|
||||
|
||||
full_text += delta
|
||||
buffer += delta
|
||||
|
||||
last_punct = find_sentence_end(buffer, min_len=60)
|
||||
if last_punct > -1:
|
||||
sentence = clean_for_speech(buffer[:last_punct + 1])
|
||||
if sentence.strip():
|
||||
print(f"🔊 Говорю: {sentence}")
|
||||
speak(sentence, agent_id)
|
||||
buffer = buffer[last_punct + 1:].lstrip()
|
||||
|
||||
except (json.JSONDecodeError, KeyError, IndexError):
|
||||
continue
|
||||
except Exception as e:
|
||||
log.exception("Ошибка при чтении стрима")
|
||||
print(f"⚠️ Стрим прервался: {e}")
|
||||
|
||||
# Остаток
|
||||
if buffer.strip():
|
||||
sentence = clean_for_speech(buffer)
|
||||
if sentence:
|
||||
speak(sentence, agent_id)
|
||||
|
||||
if not full_text:
|
||||
msg = "Не получил ответ, попробуй ещё раз."
|
||||
speak(msg, agent_id)
|
||||
return msg
|
||||
|
||||
result = clean_for_speech(full_text)
|
||||
conv.add_assistant(full_text)
|
||||
return result
|
||||
Reference in New Issue
Block a user