J.A.R.V.I.S-py/memory_store.py
Bossiara13 94edb73f26 feat: Python parity for Wave 6-8 — memory admin / cooking / leftoff / trivia / routines
New module `wave68_handlers.py` (~270 lines) covering the Rust packs that
were Rust-only after Waves 6-8:

  - memory_admin: do_memory_count/list/wipe (with full RU plural decl.)
  - cooking: do_cooking + reusable match_recipe(phrase) for the 15-dish
    preset table. Schedules a "ready!" reminder via scheduler_store.
  - leftoff: do_leftoff stitches memory + scheduled tasks + last reply
    into a one-paragraph recap. Offline.
  - trivia: do_trivia random one-liner from RU/EN MCU pools.
  - routines: do_good_night (sleep profile + cancel one-shots),
    do_good_morning (default profile + agenda hint),
    do_coffee_break (5-min check-in via scheduler).

memory_store: new clear_all() returning removed count, atomic via _save.

extensions.py: imports + set_speak wiring + 9 proxy do_* fns.
main.py: 9 new dispatch elifs.
commands.yaml: 9 new entries (now 196 total).

tests/test_wave68_handlers.py: 15 unit tests covering each handler.
Test helper `_rewire()` re-imports state modules via the existing
isolated_state fixture then re-binds the wave68_handlers module-level
refs — fixes the gotcha where module-level imports cache the OLD
memory_store / scheduler_store instances from process startup.

Tests: 81 → 96 (+15).
2026-05-16 14:55:47 +03:00

136 lines
3.4 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.

"""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 clear_all() -> int:
"""Wipe every fact. Returns count of removed entries. Atomic via _save."""
_load()
n = len(_store)
if n == 0:
return 0
_store.clear()
_save()
return n
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'