Merge pull request 'feat: route voice through OpenClaw agent session (full memory + tools)' (#1) from feature/openclaw-agent-session into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -34,3 +34,5 @@ FOLLOWUP_TIMEOUT=8
|
|||||||
|
|
||||||
# Логирование
|
# Логирование
|
||||||
LOG_FILE=errors.log
|
LOG_FILE=errors.log
|
||||||
|
|
||||||
|
VOICE_SESSION_KEY=agent:main:voice:home
|
||||||
|
|||||||
@@ -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 уже доcтримил — озвучиваем весь ответ одним куском с цельной интонацией
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user