J.A.R.V.I.S-py/memory_store.py

137 lines
3.4 KiB
Python
Raw Normal View History

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
"""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
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
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'