diff --git a/.gitignore b/.gitignore index 992e6b7..8811858 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,14 @@ __pycache__/ # Custom 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 *.so diff --git a/commands.yaml b/commands.yaml index 2daba39..8869138 100644 --- a/commands.yaml +++ b/commands.yaml @@ -1495,3 +1495,57 @@ 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} diff --git a/extensions.py b/extensions.py index da3a8ab..9cf118f 100644 --- a/extensions.py +++ b/extensions.py @@ -28,6 +28,7 @@ import vision_handler import dev_handlers import llm_backend import wave1_handlers +import personality_handlers _speak_fn = print _set_clipboard_fn = None @@ -41,6 +42,7 @@ def set_speak(fn): vision_handler.set_speak(fn) dev_handlers.set_speak(fn) wave1_handlers.set_speak(fn) + personality_handlers.set_speak(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_habit_checkin(a, v): wave1_handlers.do_habit_checkin(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) diff --git a/main.py b/main.py index ffc2020..2f1b2bf 100644 --- a/main.py +++ b/main.py @@ -1689,6 +1689,16 @@ def run_action(action): extensions.do_habit_checkin(action, _current_voice or '') elif t == 'habit_streak': 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 = '' diff --git a/personality_handlers.py b/personality_handlers.py new file mode 100644 index 0000000..79477dc --- /dev/null +++ b/personality_handlers.py @@ -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)) diff --git a/tests/test_personality_handlers.py b/tests/test_personality_handlers.py new file mode 100644 index 0000000..7fdb82a --- /dev/null +++ b/tests/test_personality_handlers.py @@ -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