feat: personality pack — Python parity
Some checks are pending
Python CI / pytest (ubuntu-latest) (push) Waiting to run
Python CI / pytest (windows-latest) (push) Waiting to run
Python CI / ruff (push) Waiting to run

Python parity for the 5 personality voice commands shipped on the
Rust side:

- New `personality_handlers.py` (~150 lines): do_personality_{greet,
  thanks, compliment, how_are_you, tony_quote}. Each picks one of
  7-10 phrases per language (RU/EN) at random; greet additionally
  buckets by time-of-day (morning/midday/evening/night).
- `extensions.py`: imports + 5 proxy `do_personality_*` functions
  pointing at the new module, plus set_speak wiring so handlers can
  TTS via the runtime's voice.
- `main.py`: 5 new dispatch elifs feeding the new action types.
- `commands.yaml`: 5 new entries (`personality_*`) mapping voice
  phrases to action types.
- `tests/test_personality_handlers.py`: 6 unit tests confirming
  each handler returns a non-empty string from the expected pool.
- `.gitignore`: stop tracking runtime state generated by tests
  (profiles/, long_term_memory.json, schedule.json, macros.json,
  llm_backend.txt, llm_history.json) — these are user data, not
  source.

Tests: 54 → 60 (+6).
This commit is contained in:
Bossiara13 2026-05-16 13:53:44 +03:00
parent 6079d2730f
commit 45d9425ed7
6 changed files with 337 additions and 0 deletions

8
.gitignore vendored
View file

@ -8,6 +8,14 @@ __pycache__/
# Custom # Custom
model_small/ model_small/
# Runtime state generated by tests / app — never tracked
profiles/
long_term_memory.json
schedule.json
macros.json
llm_backend.txt
llm_history.json
# C extensions # C extensions
*.so *.so

View file

@ -1495,3 +1495,57 @@ habit_streak:
- сколько дней пью воду - сколько дней пью воду
- трекинг привычек - трекинг привычек
action: {type: habit_streak} action: {type: habit_streak}
personality_greet:
phrases:
- привет джарвис
- здравствуй джарвис
- доброе утро
- добрый день
- добрый вечер
- доброй ночи
- приветствую
- хай джарвис
action: {type: personality_greet}
personality_thanks:
phrases:
- спасибо
- спасибо джарвис
- благодарю
- благодарю джарвис
- спс
- ты лучший
action: {type: personality_thanks}
personality_compliment:
phrases:
- ты молодец
- ты умница
- молодец джарвис
- хорошо сработано
- отличная работа
- так держать
- ты лучший ассистент
action: {type: personality_compliment}
personality_how_are_you:
phrases:
- как дела
- как дела джарвис
- как ты
- как настроение
- как самочувствие
- как поживаешь
- что нового у тебя
action: {type: personality_how_are_you}
personality_tony_quote:
phrases:
- процитируй тони
- цитата тони старка
- что сказал бы тони
- скажи как тони
- процитируй железного человека
- цитата старка
action: {type: personality_tony_quote}

View file

@ -28,6 +28,7 @@ import vision_handler
import dev_handlers import dev_handlers
import llm_backend import llm_backend
import wave1_handlers import wave1_handlers
import personality_handlers
_speak_fn = print _speak_fn = print
_set_clipboard_fn = None _set_clipboard_fn = None
@ -41,6 +42,7 @@ def set_speak(fn):
vision_handler.set_speak(fn) vision_handler.set_speak(fn)
dev_handlers.set_speak(fn) dev_handlers.set_speak(fn)
wave1_handlers.set_speak(fn) wave1_handlers.set_speak(fn)
personality_handlers.set_speak(fn)
def set_clipboard_fn(fn): def set_clipboard_fn(fn):
@ -1119,3 +1121,12 @@ def do_vault_get(a, v): wave1_handlers.do_vault_get(a, v)
def do_vault_list(a, v): wave1_handlers.do_vault_list(a, v) def do_vault_list(a, v): wave1_handlers.do_vault_list(a, v)
def do_habit_checkin(a, v): wave1_handlers.do_habit_checkin(a, v) def do_habit_checkin(a, v): wave1_handlers.do_habit_checkin(a, v)
def do_habit_streak(a, v): wave1_handlers.do_habit_streak(a, v) def do_habit_streak(a, v): wave1_handlers.do_habit_streak(a, v)
# ── Personality proxies (to personality_handlers) ───────────────────────────
def do_personality_greet(a, v): personality_handlers.do_personality_greet(a, v)
def do_personality_thanks(a, v): personality_handlers.do_personality_thanks(a, v)
def do_personality_compliment(a, v): personality_handlers.do_personality_compliment(a, v)
def do_personality_how_are_you(a, v): personality_handlers.do_personality_how_are_you(a, v)
def do_personality_tony_quote(a, v): personality_handlers.do_personality_tony_quote(a, v)

10
main.py
View file

@ -1689,6 +1689,16 @@ def run_action(action):
extensions.do_habit_checkin(action, _current_voice or '') extensions.do_habit_checkin(action, _current_voice or '')
elif t == 'habit_streak': elif t == 'habit_streak':
extensions.do_habit_streak(action, _current_voice or '') extensions.do_habit_streak(action, _current_voice or '')
elif t == 'personality_greet':
extensions.do_personality_greet(action, _current_voice or '')
elif t == 'personality_thanks':
extensions.do_personality_thanks(action, _current_voice or '')
elif t == 'personality_compliment':
extensions.do_personality_compliment(action, _current_voice or '')
elif t == 'personality_how_are_you':
extensions.do_personality_how_are_you(action, _current_voice or '')
elif t == 'personality_tony_quote':
extensions.do_personality_tony_quote(action, _current_voice or '')
_current_voice: str = '' _current_voice: str = ''

192
personality_handlers.py Normal file
View file

@ -0,0 +1,192 @@
"""Personality handlers — varied JARVIS-style banter.
Mirrors the rust pack `resources/commands/personality/`. Each handler picks
a random phrase from a time-of-day or topic bucket so Jarvis does not
sound canned.
Imported by extensions.py and dispatched from main.py.
"""
import datetime as dt
import random
import memory_store
import llm_backend
import profiles_store
_speak_fn = print
def set_speak(fn):
global _speak_fn
_speak_fn = fn
def _speak(text):
try:
_speak_fn(text)
except Exception as exc:
print(f"[personality] speak: {exc}")
def _hour() -> int:
return dt.datetime.now().hour
# ── greet ──────────────────────────────────────────────────────────────────
_GREET_MORNING = [
"Доброе утро, сэр. Кофе уже почти готов в воображении.",
"С добрым утром. Надеюсь, сегодняшний день стоит того, чтобы встать.",
"Доброе утро, сэр. Завтрак не подавать, но советую о нём подумать.",
"Утро доброе. Системы прогреты, как и кофеварка — фигурально.",
"Доброе утро. Если позволите — день не ждёт ни вас, ни меня.",
"Доброе утро, сэр. Готов служить, хотя сам всё ещё догружаюсь.",
"С добрым утром, сэр. Тосты горят только метафорически.",
"Доброе утро. Полагаю, чай или кофе будет уместен в ближайшие минуты.",
]
_GREET_MIDDAY = [
"Добрый день, сэр. Чем могу быть полезен?",
"Добрый день. Готов к продуктивной работе.",
"Здравствуйте, сэр. Системы в строю, время — деньги.",
"Добрый день, сэр. Слушаю ваши распоряжения.",
"Здравствуйте. Полагаю, день идёт по плану — или нам стоит это исправить.",
"Добрый день, сэр. Все системы готовы, остался только повод.",
"Здравствуйте, сэр. К работе?",
]
_GREET_EVENING = [
"Добрый вечер, сэр. День заканчивается, но мы — нет.",
"Добрый вечер. Самое время сбавить темп, если позволите.",
"Здравствуйте, сэр. Вечер — лучшее время для размышлений.",
"Добрый вечер, сэр. Готов к более спокойному режиму.",
"Добрый вечер. Полагаю, рабочий день уже сдаётся.",
"Здравствуйте, сэр. Вечер располагает к меньшему количеству решений.",
"Добрый вечер, сэр. Включить расслабляющую музыку? Шучу, попросите явно.",
]
_GREET_NIGHT = [
"Поздновато, сэр. Сон обычно помогает.",
"Доброй ночи, сэр. Хотя для ночи это уже слишком поздно.",
"Здравствуйте, сэр. Полагаю, утром вы пожалеете о бодрствовании.",
"Уже глубокая ночь, сэр. Может быть, лечь? Я тут справлюсь.",
"Тише, сэр, ночь. Если будите меня — будите осмысленно.",
"Сэр, на часах не самое разумное время. Но я тут.",
"Доброй ночи. Будильник к утру всё-таки никто не отменял.",
]
def _greet_bucket(hour: int) -> list[str]:
if 5 <= hour < 11:
return _GREET_MORNING
if 11 <= hour < 17:
return _GREET_MIDDAY
if 17 <= hour < 22:
return _GREET_EVENING
return _GREET_NIGHT
def do_personality_greet(action, voice):
_speak(random.choice(_greet_bucket(_hour())))
# ── thanks ─────────────────────────────────────────────────────────────────
_THANKS = [
"Всегда к вашим услугам, сэр.",
"Не за что — это моя работа.",
"Рад быть полезным, сэр.",
"Пожалуйста. Если что — я тут.",
"К вашим услугам.",
"Пустяки, сэр. Дальше — ваш ход.",
"Благодарю за признание, сэр. Редкая роскошь.",
"Был рад помочь, сэр.",
"Пожалуйста. Это меньшее, что я могу.",
"Сэр, я бы покраснел, если бы мог.",
]
def do_personality_thanks(action, voice):
_speak(random.choice(_THANKS))
# ── compliment ─────────────────────────────────────────────────────────────
_COMPLIMENT = [
"Спасибо, сэр. Я стараюсь не разочаровывать.",
"Сэр, ваши стандарты явно занижены. Но благодарю.",
"Если бы я умел краснеть, сэр — я бы это сделал.",
"Спасибо. На самом деле я просто следовал инструкциям.",
"Благодарю, сэр. Хотя ничего особенного я не сделал.",
"Очень мило с вашей стороны, сэр.",
"Я ценю это, сэр. Записываю в журнал успехов.",
"Сэр, такие слова я могу слушать часами. Образно говоря.",
"Спасибо. Постараюсь не зазнаваться.",
]
def do_personality_compliment(action, voice):
_speak(random.choice(_COMPLIMENT))
# ── how are you ────────────────────────────────────────────────────────────
def do_personality_how_are_you(action, voice):
facts = len(memory_store.all_facts())
try:
backend = llm_backend.current_backend()
except Exception:
backend = 'none'
try:
model = llm_backend.current_model()
except Exception:
model = ''
try:
profile = profiles_store.active().get('name', 'default')
except Exception:
profile = 'default'
options = [
f"Все системы в норме, сэр. В памяти {facts} фактов.",
"Отлично, сэр. Готов к работе.",
"Жалоб нет, сэр. Цикл стабильный.",
f"Всё штатно. Текущий профиль — {profile}.",
"Спасибо, сэр, я в порядке. Скучнее, чем хотелось бы, но в порядке.",
"Нормально, сэр. Хотя если бы я умел уставать — это был бы тот самый день.",
f"Системы в строю. Модель — {model}.",
"Бодр и собран, сэр. Жду команд.",
"Сэр, у меня нет дел в человеческом смысле. Но я готов.",
f"Работаю на {backend}. Полагаю, всё хорошо.",
]
_speak(random.choice(options))
# ── tony quote ─────────────────────────────────────────────────────────────
_TONY_QUOTES = [
"I am Iron Man.",
"Genius, billionaire, playboy, philanthropist.",
"Sometimes you gotta run before you can walk.",
"I told you, I don't want to join your super-secret boy band.",
"Part of the journey is the end.",
"If we can't protect the Earth, you can be damn sure we'll avenge it.",
"I have successfully privatized world peace.",
"Following's not really my style.",
"Doth mother know you weareth her drapes?",
"We have a Hulk.",
"Heroes are made by the path they choose, not the powers they are graced with.",
"I love you 3000.",
"Я Железный Человек.",
"Гений, миллиардер, плейбой, филантроп.",
"Иногда нужно бежать, не научившись ходить.",
"Если мы не сможем защитить Землю — мы за неё отомстим.",
"Часть пути — это конец пути.",
"Я люблю тебя три тысячи.",
"У нас есть Халк.",
]
def do_personality_tony_quote(action, voice):
_speak(random.choice(_TONY_QUOTES))

View file

@ -0,0 +1,62 @@
"""Tests for personality_handlers — picks vary, speak is invoked."""
import importlib
import sys
def _fresh_module(monkeypatch):
if 'personality_handlers' in sys.modules:
del sys.modules['personality_handlers']
return importlib.import_module('personality_handlers')
def test_greet_speaks_something(monkeypatch):
mod = _fresh_module(monkeypatch)
spoken = []
mod.set_speak(lambda t: spoken.append(t))
mod.do_personality_greet({}, '')
assert len(spoken) == 1
assert isinstance(spoken[0], str)
assert len(spoken[0]) > 0
def test_greet_bucket_thresholds(monkeypatch):
mod = _fresh_module(monkeypatch)
assert mod._greet_bucket(7) is mod._GREET_MORNING
assert mod._greet_bucket(13) is mod._GREET_MIDDAY
assert mod._greet_bucket(19) is mod._GREET_EVENING
assert mod._greet_bucket(2) is mod._GREET_NIGHT
assert mod._greet_bucket(23) is mod._GREET_NIGHT
def test_thanks_returns_from_pool(monkeypatch):
mod = _fresh_module(monkeypatch)
spoken = []
mod.set_speak(lambda t: spoken.append(t))
mod.do_personality_thanks({}, '')
assert spoken[0] in mod._THANKS
def test_tony_quote_returns_from_pool(monkeypatch):
mod = _fresh_module(monkeypatch)
spoken = []
mod.set_speak(lambda t: spoken.append(t))
mod.do_personality_tony_quote({}, '')
assert spoken[0] in mod._TONY_QUOTES
def test_compliment_returns_from_pool(monkeypatch):
mod = _fresh_module(monkeypatch)
spoken = []
mod.set_speak(lambda t: spoken.append(t))
mod.do_personality_compliment({}, '')
assert spoken[0] in mod._COMPLIMENT
def test_how_are_you_speaks(monkeypatch):
mod = _fresh_module(monkeypatch)
spoken = []
mod.set_speak(lambda t: spoken.append(t))
mod.do_personality_how_are_you({}, '')
assert len(spoken) == 1
assert len(spoken[0]) > 0