J.A.R.V.I.S-py/memory_store.py
Bossiara13 02c6f2f90d feat: memory_store + profiles_store + 10 new yaml entries (119 → 129)
Mirrors `long_term_memory` and `profiles` modules from rust fork.

memory_store.py (new, ~95 lines)
  - JSON store at <here>/long_term_memory.json (atomic write).
  - remember(key, value) / recall(key) / forget(key) / search(query, limit) /
    all_facts() / build_llm_context(prompt, limit).
  - Uses the same JSON shape as the rust side, so the file is
    interoperable if you ever symlink the two installations to the same dir.

profiles_store.py (new, ~145 lines)
  - Profiles at <here>/profiles/<name>.json. active_profile.txt persists.
  - Seeds 5 defaults on first init: default ★, work 💼, game 🎮, sleep 🌙,
    driving 🚗 — same as rust fork.
  - active() / active_name() / set_active(name) / list_names() /
    allows_command(cmd_id).

extensions.py (+6 handlers, +160 lines)
  - do_memory_remember  — derives key (first 6 words or split on '=')
  - do_memory_recall    — substring search or top-3 if no query
  - do_memory_forget    — exact then substring fallback
  - do_memory_list      — count + first 5 keys
  - do_profile_set      — target via action.target or voice ("работа" → work)
  - do_profile_what     — speaks current profile + description

main.py
  - +6 dispatch entries for the new action types.

commands.yaml (+10 entries, 119 → 129)
  - memory_remember, memory_recall, memory_forget, memory_list
  - profile_work, profile_game, profile_sleep, profile_driving, profile_default
  - profile_what

Tests: ast.parse passes for all .py files. yaml.safe_load gives 129 entries.

Python parity is now meaningful: memory + profiles are usable by voice
without any rust install. Still missing for full parity: scheduler (needs
background thread), macros (needs phrase-capture state), vision (needs
Groq vision model call), codebase_qa, github_pr.
2026-05-15 23:54:33 +03:00

125 lines
3.2 KiB
Python
Raw 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.

"""Long-term memory store — port of `crates/jarvis-core/src/long_term_memory.rs`.
Stores user facts in JSON at `<script_dir>/long_term_memory.json`.
Atomic write-through (write-then-rename).
Public API:
remember(key, value) -> None
recall(key) -> str | None
forget(key) -> bool
search(query, limit=5) -> list[dict]
all_facts() -> list[dict]
build_llm_context(prompt, limit=5) -> str # used by chat handler
"""
import json
import os
import time
# JSON file lives in the same folder as main.py so the user can move the
# install and the data follows.
_HERE = os.path.dirname(os.path.abspath(__file__))
_PATH = os.path.join(_HERE, 'long_term_memory.json')
_store: dict[str, dict] = {}
_loaded = False
def _load():
global _store, _loaded
if _loaded:
return
if os.path.isfile(_PATH):
try:
with open(_PATH, encoding='utf-8') as f:
data = json.load(f)
_store = data.get('entries', {}) if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError) as exc:
print(f"[memory] corrupt store ({exc}) — starting empty")
_store = {}
_loaded = True
def _save():
tmp = _PATH + '.tmp'
try:
with open(tmp, 'w', encoding='utf-8') as f:
json.dump({'entries': _store}, f, ensure_ascii=False, indent=2)
os.replace(tmp, _PATH)
except OSError as exc:
print(f"[memory] save failed: {exc}")
def _normalize_key(k: str) -> str:
return (k or '').strip().lower()
def remember(key: str, value: str) -> None:
_load()
nk = _normalize_key(key)
if not nk:
return
now = int(time.time())
if nk in _store:
_store[nk]['value'] = value
_store[nk]['last_used_at'] = now
else:
_store[nk] = {
'key': nk,
'value': value,
'created_at': now,
'last_used_at': now,
'use_count': 0,
}
_save()
def recall(key: str) -> str | None:
_load()
nk = _normalize_key(key)
entry = _store.get(nk)
if not entry:
return None
entry['last_used_at'] = int(time.time())
entry['use_count'] = entry.get('use_count', 0) + 1
_save()
return entry['value']
def forget(key: str) -> bool:
_load()
nk = _normalize_key(key)
if nk in _store:
del _store[nk]
_save()
return True
return False
def search(query: str, limit: int = 5) -> list[dict]:
_load()
nq = _normalize_key(query)
if not nq:
return []
hits = [
e for e in _store.values()
if nq in e['key'] or nq in e['value'].lower()
]
hits.sort(key=lambda e: e.get('last_used_at', 0), reverse=True)
return hits[: max(1, limit)]
def all_facts() -> list[dict]:
_load()
return list(_store.values())
def build_llm_context(prompt: str, limit: int = 5) -> str:
"""Format relevant memory facts as a system-prompt addendum."""
hits = search(prompt, limit)
if not hits:
return ''
lines = ["Известные факты о пользователе (используй если уместно):"]
for h in hits:
lines.append(f"- {h['key']} = {h['value']}")
return '\n'.join(lines) + '\n'