J.A.R.V.I.S-rust/resources/commands/codebase_qa/ask.lua
Bossiara13 5c7245012e feat: hot-swap LLM backend + media keys + codebase Q&A + scheduler cancel-by-text + TTS pre-warm
Single biggest user-facing change: you can now switch local↔cloud LLM by voice
without restarting. Plus four new packs and a perf nudge.

Shared LLM global (crates/jarvis-core/src/llm/mod.rs)
  - GLOBAL: Lazy<RwLock<Option<Arc<LlmClient>>>>. Single source of truth.
  - init_global() / current() / swap_to(LlmBackend) / current_backend_name() / parse_backend(str).
  - llm_fallback + llm_router migrated: they no longer own a separate LlmClient,
    they read from the global on each request. Hot-swap is now LIVE — change
    backend, next chat call uses the new one.
  - parse_backend accepts both english and russian aliases:
      "groq"/"cloud"/"облако"/"клауд"        → Groq
      "ollama"/"local"/"локал"/"локальный"  → Ollama

Voice + Lua LLM switcher (resources/commands/llm_switch/, 3 commands)
  - "переключись на локальный" / "перейди на оллама"  → swap to Ollama
  - "переключись на облако" / "используй грок"        → swap to Groq
  - "какой у тебя мозг" / "облако или локально"       → status query

  Underlying Lua API (registered globally):
    jarvis.llm({messages}, opts)   -- as before, now uses global client
    jarvis.llm_status()            -> "groq" | "ollama" | "none"
    jarvis.llm_switch(name)        -> name on success, nil on failure

Media keys pack (resources/commands/media_keys/, 4 commands)
  - "пауза" / "плей"           → VK_MEDIA_PLAY_PAUSE
  - "следующий трек"           → VK_MEDIA_NEXT_TRACK
  - "предыдущий трек"          → VK_MEDIA_PREV_TRACK
  - "стоп музыка"              → VK_MEDIA_STOP
  - Helper _media.ps1 uses user32.keybd_event (P/Invoke through Add-Type).
  - Works with anything that listens to global media keys: Spotify, Yandex Music,
    YouTube (focused tab), Foobar2000, Winamp, etc. No OAuth, no API keys.

Codebase Q&A pack (resources/commands/codebase_qa/, 3 commands)
  - "укажи проект <path>"             → stores path in jarvis.memory under codebase.root
  - "какой проект сейчас"             → speaks current path
  - "что делает функция X" / "найди в коде Y" / "объясни код"
    → walks the folder (depth 3, 30 files cap, 4KB per file, 50KB total),
      filters by source extensions (rs/py/ts/lua/go/...), feeds digest to LLM
      with a "senior reviewer" system prompt, speaks 3-5 sentence answer.

Scheduler cancel-by-text (crates/jarvis-core/src/scheduler.rs)
  - new pub fn find_by_text(query, limit) -> Vec<ScheduledTask>
  - new pub fn remove_by_text(query) -> usize (count removed)
  - new pure helper find_by_text_in(tasks, query, limit) for tests
  - Lua: jarvis.scheduler.{find_by_text, remove_by_text}
  - Voice: "отмени напоминание про воду" / "удали задачу про разминку"
  - 3 new unit tests (52 total in jarvis-core).

TTS pre-warm (crates/jarvis-app/src/main.rs)
  - Call jarvis_core::tts::backend() once on startup so the first speak()
    doesn't pay Piper binary discovery + voice loading cost. Cuts first-speak
    latency by ~150-300ms depending on backend.

Build: cargo build --release -p jarvis-app -p jarvis-gui both green.
Tests: 52/52 jarvis-core unit tests pass.
2026-05-15 16:15:59 +03:00

98 lines
3.8 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.

-- "что делает функция X" / "найди в коде Y"
-- Reads a digest of source files in the configured codebase root, sends to LLM.
local root = jarvis.memory.recall("codebase.root")
if not root or root == "" then
return jarvis.cmd.not_found("Сначала укажите проект.")
end
local phrase = (jarvis.context.phrase or "")
local question = jarvis.text.strip_trigger(phrase:lower(), {
"что делает функция",
"вопрос по коду",
"спроси код",
"найди в проекте",
"найди в коде",
"что в коде",
"объясни код",
"ask the codebase",
"ask the code",
"what does the function",
"explain the code",
})
question = question:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
if question == "" then
return jarvis.cmd.error("Сформулируйте вопрос.")
end
-- Build a digest: walk depth-2, pick source files by extension, cap per-file size + total size.
local EXT_OK = {
rs=true, lua=true, py=true, ts=true, tsx=true, js=true, jsx=true, svelte=true,
go=true, java=true, kt=true, c=true, h=true, cpp=true, hpp=true, cs=true,
rb=true, php=true, sh=true, ps1=true, sql=true, toml=true, yaml=true, yml=true,
json=true, md=true,
}
local SKIP_DIR = {
[".git"]=true, ["target"]=true, ["node_modules"]=true, ["dist"]=true, ["build"]=true,
["__pycache__"]=true, [".venv"]=true, ["venv"]=true, [".idea"]=true, [".vscode"]=true,
}
local MAX_FILE_BYTES = 4096 -- ~1k tokens per file
local MAX_TOTAL_BYTES = 50000 -- ~12k tokens total for digest
local MAX_FILES = 30
local function walk(dir, depth, acc)
if depth > 3 then return end
local entries = jarvis.fs.list(dir)
if not entries then return end
for _, ent in ipairs(entries) do
if #acc.files >= MAX_FILES or acc.bytes >= MAX_TOTAL_BYTES then
return
end
local name = ent.name
local full = dir .. "\\" .. name
if ent.is_dir then
if not SKIP_DIR[name] and not name:match("^%.") then
walk(full, depth + 1, acc)
end
else
local ext = name:match("%.([%w]+)$")
if ext and EXT_OK[ext:lower()] then
local content = jarvis.fs.read(full)
if content then
if #content > MAX_FILE_BYTES then
content = content:sub(1, MAX_FILE_BYTES) .. "\n... [truncated]"
end
-- relative path for readability
local rel = full:sub(#root + 2)
table.insert(acc.files, "--- " .. rel .. " ---\n" .. content)
acc.bytes = acc.bytes + #content
end
end
end
end
end
local acc = { files = {}, bytes = 0 }
walk(root, 1, acc)
if #acc.files == 0 then
return jarvis.cmd.not_found("Не нашёл исходников в проекте.")
end
local digest = table.concat(acc.files, "\n\n")
local prompt = string.format(
"Ты — старший разработчик. По digest проекта ответь на вопрос пользователя кратко (3-5 предложений) на русском. Указывай файлы где уместно.\n\n=== ВОПРОС ===\n%s\n\n=== КОД ===\n%s",
question, digest
)
local reply = jarvis.llm({
{ role = "system", content = "Ты — внимательный код-ревьюер. Отвечай по существу, без воды." },
{ role = "user", content = prompt }
}, { max_tokens = 400, temperature = 0.2 })
if not reply or reply == "" then
return jarvis.cmd.error("Не получилось получить ответ.")
end
jarvis.system.notify("Codebase Q&A", reply:sub(1, 250))
return jarvis.cmd.ok(reply)