feat: Tauri backend commands + wikipedia rewrite + github PR review pack

GUI integration
  - crates/jarvis-gui/src/tauri_commands/backends.rs — 5 new Tauri commands:
      get_active_backends()  -> {tts, llm, llm_model, profile}
      set_llm_backend(name)  -> swap LLM hot, persists to DB
      set_tts_backend(name)  -> persist choice (effective on jarvis-app restart)
      llm_reset_context()    -> clear conversation history
      llm_get_last_reply()   -> last assistant message or null
  - Registered in main.rs invoke_handler. Svelte side can now read active
    backend + swap from a settings page / footer without env-var gymnastics.

Wikipedia pack rewritten (resources/commands/wikipedia/)
  - Old `wiki/wiki.lua` (104 lines, hand-rolled Groq HTTP + inline SAPI PS) deleted.
  - Replacement `wikipedia/summary.lua` (87 lines):
    * REST summary endpoint (cleaner than opensearch+summary chain).
    * Russian first, English fallback with LLM translation.
    * Pure LLM fallback if Wikipedia has no article.
    * Uses jarvis.cmd.{ok,error,not_found} + jarvis.llm + jarvis.speak (no
      inline PowerShell SAPI). Phrases kept compatible with old pack.

GitHub PR voice review (resources/commands/github_pr/, 3 commands)
  - "текущий репо bossiara13/J.A.R.V.I.S-rust"  → github.set_repo
    Persists repo via jarvis.memory.
  - "какие пиары" / "открытые пиары"            → github.list_prs
    Calls `gh pr list --json number,title,author,createdAt --limit 10`.
    Parses count + first 3 titles by regex from JSON.
  - "разбери последний пиар" / "что в pr"       → github.summarize_pr
    `gh pr view N --json title,body,additions,deletions,changedFiles,author`,
    sends to LLM with "senior reviewer" system prompt, speaks 3-5 sentence
    review focusing on changes, risks, merge-readiness.
  - Requires gh CLI installed and `gh auth login` done.

Tests: 52/52 jarvis-core unit tests pass (Wikipedia pack TOML/script auto-checked).
Build: cargo build --release -p jarvis-app -p jarvis-gui green.
This commit is contained in:
Bossiara13 2026-05-15 16:31:54 +03:00
parent 385bd5c8ce
commit b243e67870
11 changed files with 439 additions and 131 deletions

View file

@ -0,0 +1,73 @@
# GitHub PR voice review — list open PRs and summarise the latest one via LLM.
# Requires `gh` CLI installed and authenticated (`gh auth login`).
# Repo can be set via voice ("текущий репо <owner/repo>") or auto-detected
# from cwd if jarvis-app is launched inside a git repo.
[[commands]]
id = "github.list_prs"
type = "lua"
script = "list.lua"
sandbox = "full"
timeout = 15000
[commands.phrases]
ru = [
"какие пиары",
"какие пулл реквесты",
"что в пиарах",
"что в pr",
"открытые пиары",
"список пиаров",
]
en = [
"list open prs",
"what's in the prs",
"open pull requests",
]
ua = [
"які піари",
"відкриті пр",
]
[[commands]]
id = "github.summarize_pr"
type = "lua"
script = "summarize.lua"
sandbox = "full"
timeout = 60000
[commands.phrases]
ru = [
"разбери последний пиар",
"объясни последний пиар",
"обзор последнего пиара",
"что в последнем pr",
"разбор pr",
]
en = [
"review the latest pr",
"explain the latest pr",
"summarize pr",
]
ua = [
"розбери останній пр",
]
[[commands]]
id = "github.set_repo"
type = "lua"
script = "set_repo.lua"
sandbox = "standard"
timeout = 3000
[commands.phrases]
ru = [
"текущий репо",
"установи репо",
"выбери репо",
"репозиторий",
]
en = ["set repo", "current repo"]
ua = ["встанови репо"]

View file

@ -0,0 +1,39 @@
local repo = jarvis.memory.recall("github.repo")
if not repo or repo == "" then
return jarvis.cmd.not_found("Сначала укажите репозиторий: текущий репо owner/repo.")
end
local result = jarvis.system.exec(string.format(
'gh pr list --repo "%s" --state open --json number,title,author,createdAt --limit 10',
repo
))
if not result or not result.success then
local err = (result and result.stderr) or "gh CLI не доступен"
jarvis.log("warn", "gh failed: " .. err)
return jarvis.cmd.error("gh CLI не отвечает. Установите gh и выполните gh auth login.")
end
local stdout = result.stdout or ""
if stdout:gsub("%s", "") == "" or stdout:gsub("%s", "") == "[]" then
return jarvis.cmd.ok("Открытых пиаров нет.")
end
-- Parse JSON via LLM (mlua doesn't ship a JSON parser by default).
-- Alternatively count number entries with a regex.
local _, count = stdout:gsub('"number"%s*:', "")
if count == 0 then
return jarvis.cmd.ok("Открытых пиаров нет.")
end
-- Get titles
local titles = {}
for title in stdout:gmatch('"title"%s*:%s*"([^"]+)"') do
table.insert(titles, title)
if #titles >= 3 then break end
end
local line = string.format("%d открытых пиаров. ", count)
if #titles > 0 then
line = line .. "Первые: " .. table.concat(titles, ". ") .. "."
end
return jarvis.cmd.ok(line)

View file

@ -0,0 +1,27 @@
-- "Текущий репо bossiara13/J.A.R.V.I.S-rust"
local phrase = (jarvis.context.phrase or "")
local repo = jarvis.text.strip_trigger(phrase:lower(), {
"установи репо",
"выбери репо",
"текущий репо",
"репозиторий",
"set repo",
"current repo",
"встанови репо",
})
repo = repo:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
-- preserve original casing
if repo ~= "" then
local start_idx = phrase:lower():find(repo, 1, true)
if start_idx then
repo = phrase:sub(start_idx, start_idx + #repo - 1)
end
end
if repo == "" or not repo:find("/") then
return jarvis.cmd.error("Скажите owner слеш repo, например bossiara13 слеш J A R V I S.")
end
jarvis.memory.remember("github.repo", repo)
return jarvis.cmd.ok("Репозиторий " .. repo .. " установлен.")

View file

@ -0,0 +1,62 @@
local repo = jarvis.memory.recall("github.repo")
if not repo or repo == "" then
return jarvis.cmd.not_found("Сначала укажите репозиторий: текущий репо owner/repo.")
end
-- get the most recent PR's number
local list = jarvis.system.exec(string.format(
'gh pr list --repo "%s" --state open --json number --limit 1',
repo
))
if not list or not list.success then
return jarvis.cmd.error("gh CLI не отвечает.")
end
local number = (list.stdout or ""):match('"number"%s*:%s*(%d+)')
if not number then
return jarvis.cmd.ok("Открытых пиаров нет.")
end
-- fetch title + body + diff stat
local detail = jarvis.system.exec(string.format(
'gh pr view %s --repo "%s" --json title,body,additions,deletions,changedFiles,author',
number, repo
))
if not detail or not detail.success then
return jarvis.cmd.error("Не получилось получить PR.")
end
local d = detail.stdout or ""
local title = d:match('"title"%s*:%s*"([^"]*)"') or "(no title)"
local body = d:match('"body"%s*:%s*"(.-)"') or ""
body = body:gsub("\\n", "\n"):gsub("\\r", " "):gsub('\\"', '"'):gsub("\\\\", "\\")
local additions = d:match('"additions"%s*:%s*(%d+)') or "?"
local deletions = d:match('"deletions"%s*:%s*(%d+)') or "?"
local files = d:match('"changedFiles"%s*:%s*(%d+)') or "?"
local author = d:match('"login"%s*:%s*"([^"]*)"') or "(unknown)"
-- Trim body for prompt
if #body > 3000 then body = body:sub(1, 3000) .. "\n... [truncated]" end
local user_prompt = string.format([[
Repo: %s
PR #%s: %s
Author: %s
Changed files: %s (+%s, -%s)
Description:
%s
Кратко (3-5 предложений) на русском объясни: что меняет этот PR, какие риски, стоит ли мёржить.
]], repo, number, title, author, files, additions, deletions, body)
local review = jarvis.llm({
{ role = "system", content = "Ты — старший разработчик, делаешь код-ревью. Отвечай по делу, без воды, без 'отличный PR' вступлений." },
{ role = "user", content = user_prompt }
}, { max_tokens = 400, temperature = 0.2 })
if not review or review == "" then
return jarvis.cmd.error("Не получилось проанализировать.")
end
jarvis.system.notify(string.format("PR #%s — %s", number, title), review:sub(1, 250))
return jarvis.cmd.ok(string.format("Пиар номер %s: %s", number, review))