feat: route voice through OpenClaw agent session (full memory + tools) #1

Merged
daniil merged 1 commits from feature/openclaw-agent-session into main 2026-04-13 20:15:03 +00:00
2 changed files with 16 additions and 50 deletions
Showing only changes of commit d9d892664a - Show all commits

View File

@@ -34,3 +34,5 @@ FOLLOWUP_TIMEOUT=8
# Логирование # Логирование
LOG_FILE=errors.log LOG_FILE=errors.log
VOICE_SESSION_KEY=agent:main:voice:home

View File

@@ -2,21 +2,14 @@ import json
import os import os
import re import re
import requests import requests
from datetime import date
from .config import AGENTS, log from .config import AGENTS, log
from .text import clean_for_speech, find_sentence_end from .text import clean_for_speech, find_sentence_end
from .tts import speak, play_error_sound from .tts import speak, play_error_sound
SYSTEM_PROMPT = ( # Ключ голосовой сессии — Cosmo работает как полноценный агент
"Отвечай кратко, 1-2 предложения, без markdown, без эмодзи. " VOICE_SESSION_KEY = os.getenv("VOICE_SESSION_KEY", "agent:main:voice:home")
"Ответ будет озвучен голосом, поэтому: "
"числа пиши прописью (двадцать три, а не 23), "
"единицы измерения пиши полностью (километров в час, а не км/ч), "
"не используй спецсимволы (+, -, /, %, °) — заменяй словами (плюс, минус, из, процентов, градусов). "
"Температуру пиши так: 'плюс девять градусов', а не '+9°C'."
)
MAX_HISTORY = int(os.getenv("MAX_HISTORY", "20"))
# "stream" — режем по предложениям (быстро, но рваная интонация) # "stream" — режем по предложениям (быстро, но рваная интонация)
# "full" — собираем весь ответ, потом TTS (естественно, но пауза перед началом) # "full" — собираем весь ответ, потом TTS (естественно, но пауза перед началом)
TTS_MODE = os.getenv("TTS_MODE", "full") TTS_MODE = os.getenv("TTS_MODE", "full")
@@ -29,60 +22,34 @@ RESET_PATTERNS = re.compile(
) )
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: def is_reset_command(text: str) -> bool:
return bool(RESET_PATTERNS.search(text)) return bool(RESET_PATTERNS.search(text))
def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: str = "cosmo") -> str: def ask_agent_stream(text: str, conv=None, agent_id: str = "cosmo") -> str:
if conv is None: """
conv = Conversation(agent_id) Отправляет запрос к OpenClaw gateway как полноценный агент.
История хранится на стороне gateway (session_key).
conv.add_user(text) conv параметр сохранён для обратной совместимости, не используется.
"""
cfg = AGENTS.get(agent_id, AGENTS["cosmo"]) cfg = AGENTS.get(agent_id, AGENTS["cosmo"])
gateway_url = cfg["gateway_url"] gateway_url = cfg["gateway_url"]
session = cfg["session"] session = cfg["session"]
agent = cfg["agent"] agent = cfg["agent"]
session_key = cfg.get("session_key", VOICE_SESSION_KEY)
try: try:
resp = session.post( resp = session.post(
f"{gateway_url}/v1/chat/completions", f"{gateway_url}/v1/chat/completions",
headers={ headers={
"x-openclaw-model": cfg["voice_model"], "x-ocplatform-model": cfg["voice_model"],
"x-openclaw-session-key": cfg["session_key"], "x-openclaw-session-key": session_key,
}, },
json={ json={
"model": agent, "model": agent,
"stream": True, "stream": True,
"messages": conv.messages, "messages": [{"role": "user", "content": text}],
"max_tokens": 150, "max_tokens": 150,
}, },
stream=True, stream=True,
@@ -151,16 +118,13 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
result = clean_for_speech(full_text) result = clean_for_speech(full_text)
if TTS_MODE == "full": if TTS_MODE == "full":
# LLM уже доримил — озвучиваем весь ответ одним куском с цельной интонацией
if result.strip(): if result.strip():
print(f"🔊 Говорю: {result}") print(f"🔊 Говорю: {result}")
speak(result, agent_id) speak(result, agent_id)
else: else:
# остаток буфера в stream-режиме
if buffer.strip(): if buffer.strip():
tail = clean_for_speech(buffer) tail = clean_for_speech(buffer)
if tail: if tail:
speak(tail, agent_id) speak(tail, agent_id)
conv.add_assistant(full_text)
return result return result