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.
168 lines
5.6 KiB
Python
168 lines
5.6 KiB
Python
"""Profile switching — port of `crates/jarvis-core/src/profiles.rs`.
|
|
|
|
Profiles live in `<script_dir>/profiles/<name>.json`. Active profile name
|
|
persists in `<script_dir>/active_profile.txt`. Seeds 5 defaults on first run.
|
|
|
|
Public API:
|
|
active() -> dict # currently selected profile
|
|
set_active(name) -> dict # raises ValueError if profile missing
|
|
list_names() -> list[str]
|
|
allows_command(cmd_id) -> bool
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_PROFILES_DIR = os.path.join(_HERE, 'profiles')
|
|
_ACTIVE_FILE = os.path.join(_HERE, 'active_profile.txt')
|
|
|
|
_active_cache: dict | None = None
|
|
|
|
|
|
def _seed_defaults():
|
|
defaults = [
|
|
{
|
|
'name': 'default',
|
|
'description': 'Обычный режим, все команды доступны.',
|
|
'llm_personality': '',
|
|
'allowed_command_prefixes': [],
|
|
'disabled_command_prefixes': [],
|
|
'greeting': '',
|
|
'icon': '★',
|
|
},
|
|
{
|
|
'name': 'work',
|
|
'description': 'Рабочий режим — фокус, отключены развлечения.',
|
|
'llm_personality': 'Отвечай кратко и по делу. Минимум шуток.',
|
|
'allowed_command_prefixes': [],
|
|
'disabled_command_prefixes': ['games.', 'fun.', 'music.'],
|
|
'greeting': 'Режим работы. Сосредоточимся.',
|
|
'icon': '💼',
|
|
},
|
|
{
|
|
'name': 'game',
|
|
'description': 'Игровой режим — голосовые макросы и игры.',
|
|
'llm_personality': 'Будь дружелюбен. Краткие ответы. Игровая атмосфера.',
|
|
'allowed_command_prefixes': [],
|
|
'disabled_command_prefixes': ['reminders.', 'calendar.'],
|
|
'greeting': 'Игровой режим активирован.',
|
|
'icon': '🎮',
|
|
},
|
|
{
|
|
'name': 'sleep',
|
|
'description': 'Спокойный режим — тихий голос, никаких уведомлений.',
|
|
'llm_personality': 'Говори спокойно, короткими фразами.',
|
|
'allowed_command_prefixes': ['time.', 'weather.', 'lights.'],
|
|
'disabled_command_prefixes': [],
|
|
'greeting': 'Спокойной ночи.',
|
|
'icon': '🌙',
|
|
},
|
|
{
|
|
'name': 'driving',
|
|
'description': 'Режим за рулём — только безопасные команды.',
|
|
'llm_personality': 'Краткие безопасные ответы. Никаких длинных текстов.',
|
|
'allowed_command_prefixes': [
|
|
'music.', 'navigation.', 'calls.',
|
|
'time.', 'weather.', 'reminders.',
|
|
],
|
|
'disabled_command_prefixes': ['windows.', 'screen.'],
|
|
'greeting': 'Режим за рулём. Веди безопасно.',
|
|
'icon': '🚗',
|
|
},
|
|
]
|
|
os.makedirs(_PROFILES_DIR, exist_ok=True)
|
|
for p in defaults:
|
|
path = os.path.join(_PROFILES_DIR, f"{p['name']}.json")
|
|
if not os.path.isfile(path):
|
|
try:
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
json.dump(p, f, ensure_ascii=False, indent=2)
|
|
except OSError as exc:
|
|
print(f"[profiles] seed {p['name']} failed: {exc}")
|
|
|
|
|
|
def _default_profile() -> dict:
|
|
return {
|
|
'name': 'default',
|
|
'description': 'fallback',
|
|
'llm_personality': '',
|
|
'allowed_command_prefixes': [],
|
|
'disabled_command_prefixes': [],
|
|
'greeting': '',
|
|
'icon': '★',
|
|
}
|
|
|
|
|
|
def _load_profile(name: str) -> dict | None:
|
|
path = os.path.join(_PROFILES_DIR, f'{name}.json')
|
|
if not os.path.isfile(path):
|
|
return None
|
|
try:
|
|
with open(path, encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
print(f"[profiles] load {name} failed: {exc}")
|
|
return None
|
|
|
|
|
|
def _read_active_name() -> str:
|
|
try:
|
|
with open(_ACTIVE_FILE, encoding='utf-8') as f:
|
|
return f.read().strip() or 'default'
|
|
except OSError:
|
|
return 'default'
|
|
|
|
|
|
def _init():
|
|
global _active_cache
|
|
if _active_cache is not None:
|
|
return
|
|
_seed_defaults()
|
|
name = _read_active_name()
|
|
_active_cache = _load_profile(name) or _default_profile()
|
|
|
|
|
|
def active() -> dict:
|
|
_init()
|
|
return dict(_active_cache or _default_profile())
|
|
|
|
|
|
def active_name() -> str:
|
|
return active().get('name', 'default')
|
|
|
|
|
|
def set_active(name: str) -> dict:
|
|
global _active_cache
|
|
_init()
|
|
profile = _load_profile(name)
|
|
if not profile:
|
|
raise ValueError(f"profile '{name}' not found")
|
|
_active_cache = profile
|
|
try:
|
|
with open(_ACTIVE_FILE, 'w', encoding='utf-8') as f:
|
|
f.write(name)
|
|
except OSError as exc:
|
|
print(f"[profiles] persist failed: {exc}")
|
|
return dict(profile)
|
|
|
|
|
|
def list_names() -> list[str]:
|
|
_init()
|
|
names = []
|
|
if os.path.isdir(_PROFILES_DIR):
|
|
for fname in os.listdir(_PROFILES_DIR):
|
|
if fname.endswith('.json'):
|
|
names.append(fname[:-5])
|
|
return sorted(names)
|
|
|
|
|
|
def allows_command(cmd_id: str) -> bool:
|
|
p = active()
|
|
disabled = p.get('disabled_command_prefixes', []) or []
|
|
if any(cmd_id.startswith(pfx) for pfx in disabled):
|
|
return False
|
|
allowed = p.get('allowed_command_prefixes', []) or []
|
|
if not allowed:
|
|
return True
|
|
return any(cmd_id.startswith(pfx) for pfx in allowed)
|