J.A.R.V.I.S-rust/resources/commands/stocks/quote.lua
Bossiara13 45243c3e3c feat: Ollama LLM backend + 5 utility packs (daily briefing, pomodoro, currency convert, stocks, habits, translate clipboard)
Local-LLM support so J.A.R.V.I.S. works offline without GROQ_TOKEN.

LLM multi-backend (crates/jarvis-core/src/llm/client.rs)
  - LlmBackend enum {Groq, Ollama}. Both use OpenAI-compatible /v1/chat/completions
    (Ollama exposes one natively at :11434/v1).
  - LlmClient::from_env() now dispatches on JARVIS_LLM=groq|ollama. Auto-detect:
    Groq if GROQ_TOKEN set, else Ollama.
  - New helpers: LlmClient::groq() (was: from_env), LlmClient::ollama().
  - Reqwest client now has a 60s timeout (was: unbounded — Groq freezes could hang).
  - api_key skipped when empty (Ollama doesn't need auth).
  - Defaults: OLLAMA_BASE_URL=http://localhost:11434/v1, OLLAMA_MODEL=qwen2.5:3b.
    Override via OLLAMA_BASE_URL / OLLAMA_MODEL env.
  - llm_fallback + llm_router log the active backend on init.
  - Tests: 32/32 pass. Renamed `from_env_fails` → `groq_fails_when_token_missing`,
    added `ollama_works_without_token`.

Daily briefing pack (resources/commands/daily_briefing/, 3 commands)
  - setup.lua: "настрой утренний брифинг на 9:00" → adds a daily scheduled task
    that runs now.lua via the scheduler's Lua-action support.
  - now.lua: greeting + time + active profile + scheduled task count + memory recap
    + LLM motivational nudge. Voice-triggered ("утренний брифинг") works the same.
  - off.lua: removes the scheduled task.

Pomodoro pack (resources/commands/pomodoro/, 2 commands)
  - start.lua / stop.lua + tick.lua (re-scheduled by itself). State stored in
    jarvis.state — phase alternates work (25 min) / break (5 min) until stop.

Currency conversion (resources/commands/currency/convert.lua)
  - "сколько будет 1000 долларов в рублях" — fetches CBR daily rates from
    cbr-xml-daily.ru, normalises by Nominal, prints both directions.
  - Recognises USD/EUR/CNY/GBP/JPY/RUB by substring. Pluralises russian units.

Stocks pack (resources/commands/stocks/, 1 command)
  - "сколько Сбер" → MOEX ISS /iss/engines/stock/markets/shares/securities/<TKR>.
  - Mapping of 22 popular Russian tickers to common names (Сбер→SBER, Газпром→GAZP,
    Лукойл→LKOH, Яндекс→YDEX, Т-банк→T, etc).
  - Picks TQBR (main board) row, reports LAST + LASTTOPREVPRICE%.

Habit nudges (resources/commands/habit_nudge/, 4 commands)
  - "напоминай мне пить воду"     → every 2 hours
  - "напоминай мне размяться"     → every 50 minutes
  - "напоминай отдыхать глазам"  → every 20 minutes (20-20-20 rule)
  - "отключи все привычки"        → stops all three by id.

Translate clipboard (resources/commands/translate/clipboard.lua)
  - "переведи буфер на английский" → grabs clipboard, sends to LLM, speaks first
    sentence, replaces clipboard with full translation, fires notification.

Build: cargo build --release -p jarvis-app -p jarvis-gui both green. 32/32 tests.

To use Ollama:
  1. Install + run Ollama (https://ollama.com).
  2. `ollama pull qwen2.5:3b` (or any chat model).
  3. Optional: `set JARVIS_LLM=ollama` (auto-picked if GROQ_TOKEN unset).
2026-05-15 15:51:24 +03:00

119 lines
3.7 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- "Сколько сейчас Сбер" / "котировка газпром" — query MOEX ISS public API.
local phrase = (jarvis.context.phrase or ""):lower()
-- Common name → ticker map.
local TICKERS = {
["сбер"] = "SBER", ["сбербанк"] = "SBER",
["газпром"] = "GAZP",
["лукойл"] = "LKOH",
["яндекс"] = "YDEX", ["yandex"] = "YDEX",
["вк"] = "VKCO", ["вконтакте"] = "VKCO",
["втб"] = "VTBR",
["магнит"] = "MGNT",
["мтс"] = "MTSS",
["роснефть"] = "ROSN",
["новатэк"] = "NVTK",
["норникель"] = "GMKN", ["норильск"] = "GMKN",
["полюс"] = "PLZL",
["тинькофф"] = "T", ["тбанк"] = "T", ["т банк"] = "T",
["сургутнефтегаз"] = "SNGS",
["мосбиржа"] = "MOEX", ["биржа"] = "MOEX",
["x5"] = "X5", ["икс пять"] = "X5",
["алроса"] = "ALRS",
["русал"] = "RUAL",
["мечел"] = "MTLR",
["фосагро"] = "PHOR",
["сегежа"] = "SGZH",
["белуга"] = "BELU",
}
-- find ticker
local ticker, ticker_name
for name, tk in pairs(TICKERS) do
if phrase:find(name, 1, true) then
ticker = tk
ticker_name = name
break
end
end
-- Allow direct ticker mention "сколько SBER" etc.
if not ticker then
local maybe = phrase:upper():match("([A-Z][A-Z][A-Z]+)")
if maybe then ticker = maybe; ticker_name = maybe end
end
if not ticker then
jarvis.speak("Не понял какую бумагу. Скажите название.")
jarvis.audio.play_error()
return { chain = false }
end
local url = string.format(
"https://iss.moex.com/iss/engines/stock/markets/shares/securities/%s.json?iss.meta=off",
ticker
)
local data = jarvis.http.json(url)
if not data then
jarvis.speak("Не получилось получить котировки.")
jarvis.audio.play_error()
return { chain = false }
end
-- MOEX ISS returns parallel arrays: columns + data rows. We want marketdata.LAST.
local md = data.marketdata
if not md or not md.columns or not md.data then
jarvis.speak("Биржа вернула пустой ответ.")
jarvis.audio.play_error()
return { chain = false }
end
local cols = md.columns
local rows = md.data
-- find indexes
local idx_last, idx_change, idx_board
for i, c in ipairs(cols) do
if c == "LAST" then idx_last = i end
if c == "LASTTOPREVPRICE" then idx_change = i end
if c == "BOARDID" then idx_board = i end
end
-- pick the TQBR (main board) row if present, else first non-nil LAST
local last, change
for _, row in ipairs(rows) do
local board = idx_board and row[idx_board]
if board == "TQBR" and row[idx_last] then
last = row[idx_last]
change = idx_change and row[idx_change]
break
end
end
if not last then
for _, row in ipairs(rows) do
if row[idx_last] then
last = row[idx_last]
change = idx_change and row[idx_change]
break
end
end
end
if not last then
jarvis.speak("По " .. ticker_name .. " сегодня нет торгов.")
jarvis.audio.play_not_found()
return { chain = false }
end
local rub = string.format("%.2f", last):gsub("%.", " и ")
local line = string.format("%s — %s рубля.", ticker_name, rub)
if change and tonumber(change) then
local pct = tonumber(change)
local sign = pct >= 0 and "плюс" or "минус"
line = line .. string.format(" Сегодня %s %.2f процента.", sign, math.abs(pct))
end
jarvis.speak(line)
jarvis.audio.play_ok()
return { chain = false }