Files
cosmo-voice-assistant/cosmo/memory.py
d.klimov 6010816f1d Initial commit: Cosmo voice assistant
Полностью локальный голосовой ассистент на Python.

Стек:
- Wake word: openWakeWord (onnxruntime)
- STT: RealtimeSTT + faster-whisper + Silero VAD (CUDA)
- LLM-агент: smolagents ToolCallingAgent + Ollama qwen2.5:7b
- TTS: Silero V4 (torch.hub) + sounddevice
- Shell: Git Bash (Windows) / bash (macOS)

Поддерживает Windows и macOS. Агент с памятью и tool calling —
находит программы самостоятельно, запоминает пути, выполняет
произвольные shell-команды.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 15:58:12 +03:00

137 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Персистентная память ассистента.
Хранится в data/memory.json — LLM читает и пишет её через инструменты.
Структура:
{
"facts": {
"user.name": "Даниил",
"app.webstorm": "C:/Program Files/JetBrains/WebStorm.../webstorm64.exe",
"user.browser": "chrome",
...
},
"history": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
}
"""
import json
import os
import threading
from datetime import datetime
from loguru import logger
MEMORY_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"data", "memory.json"
)
class Memory:
def __init__(self, path: str = MEMORY_PATH, history_limit: int = 20):
self.path = path
self.history_limit = history_limit
self._lock = threading.Lock()
self._data = {"facts": {}, "history": []}
self._load()
# ------------------------------------------------------------------
# Персистентность
# ------------------------------------------------------------------
def _load(self):
os.makedirs(os.path.dirname(self.path), exist_ok=True)
if os.path.exists(self.path):
try:
with open(self.path, "r", encoding="utf-8") as f:
self._data = json.load(f)
logger.info(
f"Память загружена: {len(self._data.get('facts', {}))} фактов, "
f"{len(self._data.get('history', []))} сообщений в истории"
)
except Exception as e:
logger.warning(f"Не удалось загрузить память: {e}. Начинаю с чистой.")
self._data = {"facts": {}, "history": []}
else:
logger.info("Файл памяти не найден — создаю новый")
def _save(self):
try:
with open(self.path, "w", encoding="utf-8") as f:
json.dump(self._data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"Не удалось сохранить память: {e}")
# ------------------------------------------------------------------
# Факты (key-value долгосрочная память)
# ------------------------------------------------------------------
def get(self, key: str) -> str | None:
"""Получить факт по ключу. Возвращает None если не найден."""
with self._lock:
return self._data["facts"].get(key)
def set(self, key: str, value: str):
"""Сохранить факт. Перезаписывает если уже существует."""
with self._lock:
self._data["facts"][key] = value
self._save()
logger.debug(f"Память: сохранено [{key}] = {value!r}")
def delete(self, key: str) -> bool:
"""Удалить факт. Возвращает True если был."""
with self._lock:
existed = key in self._data["facts"]
if existed:
del self._data["facts"][key]
self._save()
return existed
def list_facts(self, prefix: str = "") -> dict:
"""Вернуть все факты, опционально отфильтрованные по префиксу."""
with self._lock:
facts = self._data["facts"]
if prefix:
return {k: v for k, v in facts.items() if k.startswith(prefix)}
return dict(facts)
def facts_as_text(self) -> str:
"""Все факты в виде читаемого текста для системного промпта."""
facts = self.list_facts()
if not facts:
return "Память пуста."
lines = [f" {k}: {v}" for k, v in sorted(facts.items())]
return "\n".join(lines)
# ------------------------------------------------------------------
# История разговора (краткосрочная, последние N сообщений)
# ------------------------------------------------------------------
def add_message(self, role: str, content: str):
"""Добавить сообщение в историю (role: user/assistant/tool)."""
with self._lock:
self._data["history"].append({
"role": role,
"content": content,
"ts": datetime.now().isoformat(timespec="seconds"),
})
# Ограничиваем длину истории
if len(self._data["history"]) > self.history_limit:
self._data["history"] = self._data["history"][-self.history_limit:]
self._save()
def get_history(self) -> list[dict]:
"""Вернуть историю в формате для LLM API (без поля ts)."""
with self._lock:
return [
{"role": m["role"], "content": m["content"]}
for m in self._data["history"]
]
def clear_history(self):
with self._lock:
self._data["history"] = []
self._save()