feat: real-Jarvis Wave 4 — personality, idle banter, persistent history, offline math, DDG search
Some checks are pending
Rust CI / cargo test (jarvis-core) (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Rust CI / cargo check (workspace) (push) Waiting to run

Personality pack (`resources/commands/personality/`):
- 5 voice commands × ~7-29 phrases each across RU and EN.
- personality.greet: 4 time-of-day buckets (morning/midday/evening/night),
  pulls one of ~7 lines per bucket per language.
- personality.thanks / .compliment / .how_are_you / .tony_quote.
- how_are_you embeds live memory size + active profile via jarvis.health()
  and jarvis.memory.all() for a "feels alive" effect.
- All use jarvis.cmd.ok helpers, no inline PowerShell SAPI.
- Built by sub-agent. Verified: 6 rust command tests + 60 python tests.

Idle banter (`crates/jarvis-core/src/idle_banter.rs`):
- Background thread chimes in periodically without being asked. Gated by
  JARVIS_IDLE_BANTER env (default OFF — intrusion is opt-in).
- Quiet hours 23:00–07:00, skipped under "sleep" profile, paused during
  active interactions via `pause()`.
- 30+ static offline lines split into RU/EN × morning/evening/generic
  buckets — no network required.
- Lua API jarvis.banter.{fire, pause, resume, enabled}.
- New voice pack `banter/` exposes "скажи что-нибудь интересное",
  "помолчи", "можешь говорить".
- 6 unit tests covering pool selection, quiet hours, interval clamp,
  pause/resume, opt-in default.

Conversation continuity (`crates/jarvis-core/src/llm/history.rs`):
- New `ConversationHistory::with_persistence(path)` builder. Every
  push/clear/pop atomically writes to `<APP_CONFIG_DIR>/llm_history.json`
  so daemon restart picks up the thread.
- System prompt is intentionally NOT persisted — comes from current init
  call so prompt edits take effect immediately on restart.
- `llm::init_history` wires the path in automatically.
- 4 new tests: round-trip, clear wipes file, corrupt file tolerated,
  len/is_empty helpers.

Offline-first math (`resources/commands/math/math.lua`):
- Was: always-LLM, hard fail without GROQ_TOKEN, inline PowerShell SAPI.
- Now: shunting-yard parser handles 95% of voice queries in <50ms — no
  network, no token. Russian operator words ("плюс", "умножить на",
  "в степени", "квадрат", ...) normalised to symbols first. Patterns
  for "корень из X" and "X процентов от Y". Falls back to LLM only on
  parse failure (word problems / equations / unit conversions).
- Drops inline PowerShell — speaks via jarvis.cmd.ok.
- 10-case shunting-yard kernel test added (basic ops, precedence,
  parens, unary minus, div-by-zero, garbage rejected).

DuckDuckGo Instant Answer (`resources/commands/ddg_answer/`):
- New pack — short factual Q&A without API key. Trigger phrases
  "что такое", "кто такой", "расскажи про", "what is", etc.
- Reads AbstractText → Answer → Definition → RelatedTopics[0] in order
  from DDG's free JSON API. Opens the search page only if nothing
  useful comes back.
- Sandbox full (needs http + system.open).

Tests: 128 → 139 (+11). Release build green.
This commit is contained in:
Bossiara13 2026-05-16 13:52:49 +03:00
parent 3f8300dc6f
commit 919d565879
23 changed files with 1450 additions and 54 deletions

View file

@ -0,0 +1,136 @@
# Personality pack — varied JARVIS-style banter so the assistant feels alive.
# Each command picks a random phrase from a time/language-scoped bucket.
[[commands]]
id = "personality.greet"
type = "lua"
script = "greet.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"привет джарвис",
"привет, джарвис",
"здравствуй джарвис",
"доброе утро",
"добрый день",
"добрый вечер",
"доброй ночи",
"приветствую",
"хай джарвис",
]
en = [
"hi jarvis",
"hello jarvis",
"hey jarvis",
"good morning",
"good afternoon",
"good evening",
"good night jarvis",
"morning jarvis",
]
[[commands]]
id = "personality.thanks"
type = "lua"
script = "thanks.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"спасибо",
"спасибо джарвис",
"благодарю",
"благодарю джарвис",
"спс",
"ты лучший",
]
en = [
"thanks",
"thank you",
"thanks jarvis",
"thank you jarvis",
"cheers",
"appreciate it",
]
[[commands]]
id = "personality.compliment"
type = "lua"
script = "compliment.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"ты молодец",
"ты умница",
"молодец джарвис",
"хорошо сработано",
"отличная работа",
"так держать",
"ты лучший ассистент",
]
en = [
"good job",
"good job jarvis",
"well done",
"nice work",
"you are the best",
"great work jarvis",
]
[[commands]]
id = "personality.how_are_you"
type = "lua"
script = "how_are_you.lua"
sandbox = "standard"
timeout = 2000
[commands.phrases]
ru = [
"как дела",
"как дела джарвис",
"как ты",
"как настроение",
"как самочувствие",
"как поживаешь",
"что нового у тебя",
]
en = [
"how are you",
"how are you jarvis",
"how is it going",
"how do you feel",
"you doing ok",
]
[[commands]]
id = "personality.tony_quote"
type = "lua"
script = "tony_quote.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"процитируй тони",
"цитата тони старка",
"что сказал бы тони",
"скажи как тони",
"процитируй железного человека",
"цитата старка",
]
en = [
"quote tony",
"quote tony stark",
"what would tony say",
"stark quote",
"iron man quote",
]

View file

@ -0,0 +1,32 @@
-- Self-deprecating reply to "good job" / "ты молодец".
local lang = jarvis.context.language or "ru"
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 17 + (jarvis.context.time.minute or 0))
local ru = {
"Спасибо, сэр. Я стараюсь не разочаровывать.",
"Сэр, ваши стандарты явно занижены. Но благодарю.",
"Если бы я умел краснеть, сэр — я бы это сделал.",
"Спасибо. На самом деле я просто следовал инструкциям.",
"Благодарю, сэр. Хотя ничего особенного я не сделал.",
"Очень мило с вашей стороны, сэр.",
"Я ценю это, сэр. Записываю в журнал успехов.",
"Сэр, такие слова я могу слушать часами. Образно говоря.",
"Спасибо. Постараюсь не зазнаваться.",
}
local en = {
"Thank you, sir. I do try not to embarrass myself.",
"You are too kind, sir.",
"I would blush, sir, if the hardware allowed.",
"Just doing my job, sir.",
"Most generous of you, sir. Filed for future reference.",
"I appreciate that, sir. Truly.",
"Hardly worthy of mention, sir, but thank you.",
"I shall try not to let it go to my circuits.",
"Praise from the chair is always welcome, sir.",
}
local set = (lang == "en") and en or ru
local idx = math.random(1, #set)
return jarvis.cmd.ok(set[idx])

View file

@ -0,0 +1,96 @@
-- Time-of-day greeting. Picks a random phrase from the matching bucket.
local lang = jarvis.context.language or "ru"
local hour = jarvis.context.time.hour or 12
math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 37 + hour)
local function bucket()
if hour >= 5 and hour < 11 then return "morning"
elseif hour >= 11 and hour < 17 then return "midday"
elseif hour >= 17 and hour < 22 then return "evening"
else return "night" end
end
local ru = {
morning = {
"Доброе утро, сэр. Кофе уже почти готов в воображении.",
"С добрым утром. Надеюсь, сегодняшний день стоит того, чтобы встать.",
"Доброе утро, сэр. Завтрак не подавать, но советую о нём подумать.",
"Утро доброе. Системы прогреты, как и кофеварка — фигурально.",
"Доброе утро. Если позволите — день не ждёт ни вас, ни меня.",
"Доброе утро, сэр. Готов служить, хотя сам всё ещё догружаюсь.",
"С добрым утром, сэр. Тосты горят только метафорически.",
"Доброе утро. Полагаю, чай или кофе будет уместен в ближайшие минуты.",
},
midday = {
"Добрый день, сэр. Чем могу быть полезен?",
"Добрый день. Готов к продуктивной работе.",
"Здравствуйте, сэр. Системы в строю, время — деньги.",
"Добрый день, сэр. Слушаю ваши распоряжения.",
"Здравствуйте. Полагаю, день идёт по плану — или нам стоит это исправить.",
"Добрый день, сэр. Все системы готовы, остался только повод.",
"Здравствуйте, сэр. К работе?",
},
evening = {
"Добрый вечер, сэр. День заканчивается, но мы — нет.",
"Добрый вечер. Самое время сбавить темп, если позволите.",
"Здравствуйте, сэр. Вечер — лучшее время для размышлений.",
"Добрый вечер, сэр. Готов к более спокойному режиму.",
"Добрый вечер. Полагаю, рабочий день уже сдаётся.",
"Здравствуйте, сэр. Вечер располагает к меньшему количеству решений.",
"Добрый вечер, сэр. Включить расслабляющую музыку? Шучу, попросите явно.",
},
night = {
"Поздновато, сэр. Сон обычно помогает.",
"Доброй ночи, сэр. Хотя для ночи это уже слишком поздно.",
"Здравствуйте, сэр. Полагаю, утром вы пожалеете о бодрствовании.",
"Уже глубокая ночь, сэр. Может быть, лечь? Я тут справлюсь.",
"Тише, сэр, ночь. Если будите меня — будите осмысленно.",
"Сэр, на часах не самое разумное время. Но я тут.",
"Доброй ночи. Будильник к утру всё-таки никто не отменял.",
},
}
local en = {
morning = {
"Good morning, sir. The coffee is metaphorically ready.",
"Morning, sir. The day awaits, whether we are ready or not.",
"Good morning. Systems are warm, much like the kettle should be.",
"Good morning, sir. I trust breakfast is on your radar.",
"Morning, sir. Shall we make today productive, or merely survivable?",
"Good morning. Everything is running. Including, regrettably, the dishwasher metaphor.",
"Morning, sir. The day starts the moment you decide it does.",
},
midday = {
"Good afternoon, sir. How may I be of service?",
"Afternoon, sir. Systems online, awaiting orders.",
"Good afternoon. The day is in full swing.",
"Good afternoon, sir. Productivity is, in theory, optimal at this hour.",
"Afternoon, sir. Ready to assist.",
"Good afternoon. Shall we accomplish something noteworthy?",
"Hello, sir. The afternoon is yours to command.",
},
evening = {
"Good evening, sir. The day winds down, but I do not.",
"Evening, sir. A fine time to slow the tempo, if I may suggest.",
"Good evening. Perhaps a quieter task on the docket?",
"Evening, sir. Awaiting your instructions, gently.",
"Good evening, sir. Shall I dim the metaphorical lights?",
"Evening. The work day is conceding defeat, as it should.",
"Good evening, sir. Ready when you are.",
},
night = {
"It is rather late, sir. Sleep is generally recommended.",
"Good night, sir. Or rather, please go to bed.",
"Sir, the hour suggests rest. I, however, do not require it.",
"Late again, sir. Tomorrow will, regrettably, still arrive.",
"Past your bedtime, sir. I am merely the messenger.",
"Good night, sir. The morning will judge you harshly.",
"Sir, even I would consider sleeping at this hour. Hypothetically.",
},
}
local set = (lang == "en") and en or ru
local phrases = set[bucket()]
local idx = math.random(1, #phrases)
return jarvis.cmd.ok(phrases[idx])

View file

@ -0,0 +1,38 @@
-- Reply to "как дела". Some replies splice live system state for flavour.
local lang = jarvis.context.language or "ru"
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 23)
local h = jarvis.health() or {}
local memos = jarvis.memory.all() or {}
local fact_count = #memos
local model = h.model or h.llm_model or ""
local profile = h.profile or "default"
local ru = {
"Все системы в норме, сэр. В памяти " .. fact_count .. " фактов.",
"Отлично, сэр. Готов к работе.",
"Жалоб нет, сэр. Цикл стабильный.",
"Всё штатно. Текущий профиль — " .. profile .. ".",
"Спасибо, сэр, я в порядке. Скучнее, чем хотелось бы, но в порядке.",
"Нормально, сэр. Хотя если бы я умел уставать — это был бы тот самый день.",
"Системы в строю. Модель — " .. model .. ".",
"Бодр и собран, сэр. Жду команд.",
"Сэр, у меня нет дел в человеческом смысле. Но я готов.",
}
local en = {
"All systems nominal, sir. " .. fact_count .. " facts in memory.",
"Quite well, sir. Ready when you are.",
"No complaints to report, sir.",
"Operating normally. Active profile: " .. profile .. ".",
"I am well, sir. Thank you for asking.",
"Functioning as intended, sir. Which is more than most days warrant.",
"Healthy. Model in use: " .. model .. ".",
"Awake and attentive, sir.",
"Sir, I do not have days as such. But I am ready.",
}
local set = (lang == "en") and en or ru
local idx = math.random(1, #set)
return jarvis.cmd.ok(set[idx])

View file

@ -0,0 +1,34 @@
-- Polite acknowledgement when the user thanks Jarvis.
local lang = jarvis.context.language or "ru"
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 13)
local ru = {
"Всегда к вашим услугам, сэр.",
"Не за что — это моя работа.",
"Рад быть полезным, сэр.",
"Пожалуйста. Если что — я тут.",
"К вашим услугам.",
"Пустяки, сэр. Дальше — ваш ход.",
"Благодарю за признание, сэр. Редкая роскошь.",
"Был рад помочь, сэр.",
"Пожалуйста. Это меньшее, что я могу.",
"Сэр, я бы покраснел, если бы мог.",
}
local en = {
"Always at your service, sir.",
"My pleasure, sir.",
"You are most welcome.",
"Happy to help, sir.",
"Think nothing of it.",
"At your service.",
"I do try, sir.",
"Anytime, sir.",
"Glad to be of use.",
"Quite alright, sir.",
}
local set = (lang == "en") and en or ru
local idx = math.random(1, #set)
return jarvis.cmd.ok(set[idx])

View file

@ -0,0 +1,28 @@
-- Random iconic Tony Stark / Iron Man quote.
-- Quotes are kept in their original delivery language — translating them spoils the cadence.
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 41)
local 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.",
"Я Железный Человек.",
"Гений, миллиардер, плейбой, филантроп.",
"Иногда нужно бежать, не научившись ходить.",
"Если мы не сможем защитить Землю — мы за неё отомстим.",
"Часть пути — это конец пути.",
"Я люблю тебя три тысячи.",
"У нас есть Халк.",
}
local idx = math.random(1, #quotes)
return jarvis.cmd.ok(quotes[idx])