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:
parent
385bd5c8ce
commit
b243e67870
11 changed files with 439 additions and 131 deletions
|
|
@ -100,6 +100,13 @@ fn main() {
|
|||
tauri_commands::list_voices,
|
||||
tauri_commands::get_voice,
|
||||
tauri_commands::preview_voice,
|
||||
|
||||
// LLM/TTS backends (active config + hot-swap)
|
||||
tauri_commands::get_active_backends,
|
||||
tauri_commands::set_llm_backend,
|
||||
tauri_commands::set_tts_backend,
|
||||
tauri_commands::llm_reset_context,
|
||||
tauri_commands::llm_get_last_reply,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
|
|
@ -37,4 +37,8 @@ pub use commands::*;
|
|||
|
||||
// import voices commands
|
||||
mod voices;
|
||||
pub use voices::*;
|
||||
pub use voices::*;
|
||||
|
||||
// LLM/TTS backend introspection + hot-swap
|
||||
mod backends;
|
||||
pub use backends::*;
|
||||
81
crates/jarvis-gui/src/tauri_commands/backends.rs
Normal file
81
crates/jarvis-gui/src/tauri_commands/backends.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//! Tauri commands for backend introspection + hot-swap. Used by the GUI
|
||||
//! footer to display the active LLM/TTS and let the user change them.
|
||||
//!
|
||||
//! Note: `get_active_backends` doesn't initialise the LLM — if the user
|
||||
//! never spoke to it, `llm_backend` may be "none" until first chat. That's
|
||||
//! the trade-off for avoiding cold-start latency in the GUI.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use jarvis_core::llm;
|
||||
use jarvis_core::tts;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ActiveBackends {
|
||||
pub tts: String,
|
||||
pub llm: String,
|
||||
pub llm_model: Option<String>,
|
||||
pub profile: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_active_backends() -> ActiveBackends {
|
||||
let llm_model = llm::current().map(|c| c.model().to_string());
|
||||
ActiveBackends {
|
||||
tts: tts::backend().name().to_string(),
|
||||
llm: llm::current_backend_name().to_string(),
|
||||
llm_model,
|
||||
profile: jarvis_core::profiles::active_name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hot-swap the LLM backend. Accepts "groq"/"ollama"/"cloud"/"local"/...
|
||||
/// Returns the new backend name on success, Err with reason on failure.
|
||||
#[tauri::command]
|
||||
pub fn set_llm_backend(name: String) -> Result<String, String> {
|
||||
let backend = llm::parse_backend(&name)
|
||||
.ok_or_else(|| format!("unknown backend: '{}'", name))?;
|
||||
// Ensure global is initialised first (so swap can read DB-persisted choice).
|
||||
if llm::current().is_none() {
|
||||
let _ = llm::init_global();
|
||||
}
|
||||
llm::swap_to(backend)
|
||||
.map(|n| n.to_string())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Set TTS backend. Persists to settings DB. Effective on next jarvis-app start
|
||||
/// (TTS backend is OnceCell, can't hot-swap mid-process).
|
||||
#[tauri::command]
|
||||
pub fn set_tts_backend(name: String) -> Result<String, String> {
|
||||
let normalized = match name.trim().to_lowercase().as_str() {
|
||||
"" | "auto" => "".to_string(),
|
||||
"sapi" | "piper" | "silero" => name.to_lowercase(),
|
||||
other => return Err(format!("unknown TTS backend: '{}'", other)),
|
||||
};
|
||||
|
||||
if let Some(db) = jarvis_core::DB.get() {
|
||||
let snapshot = {
|
||||
let mut s = db.write();
|
||||
s.tts_backend = normalized.clone();
|
||||
s.clone()
|
||||
};
|
||||
jarvis_core::db::save_settings(&snapshot)
|
||||
.map_err(|e| format!("failed to persist: {}", e))?;
|
||||
Ok(if normalized.is_empty() { "auto".into() } else { normalized })
|
||||
} else {
|
||||
Err("settings DB not initialised".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset conversation context (clears LLM history turns, keeps system prompt).
|
||||
#[tauri::command]
|
||||
pub fn llm_reset_context() {
|
||||
llm::history_clear();
|
||||
}
|
||||
|
||||
/// Return the most recent assistant message text (for "Repeat" button in GUI).
|
||||
#[tauri::command]
|
||||
pub fn llm_get_last_reply() -> Option<String> {
|
||||
llm::history_last_assistant()
|
||||
}
|
||||
73
resources/commands/github_pr/command.toml
Normal file
73
resources/commands/github_pr/command.toml
Normal 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 = ["встанови репо"]
|
||||
39
resources/commands/github_pr/list.lua
Normal file
39
resources/commands/github_pr/list.lua
Normal 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)
|
||||
27
resources/commands/github_pr/set_repo.lua
Normal file
27
resources/commands/github_pr/set_repo.lua
Normal 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 .. " установлен.")
|
||||
62
resources/commands/github_pr/summarize.lua
Normal file
62
resources/commands/github_pr/summarize.lua
Normal 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))
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
[[commands]]
|
||||
id = "wiki_lookup"
|
||||
type = "lua"
|
||||
script = "wiki.lua"
|
||||
sandbox = "full"
|
||||
timeout = 25000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"что такое",
|
||||
"кто такой",
|
||||
"кто такая",
|
||||
"расскажи про",
|
||||
"википедия про",
|
||||
"найди в википедии про",
|
||||
]
|
||||
en = [
|
||||
"what is",
|
||||
"who is",
|
||||
"tell me about",
|
||||
"wikipedia about",
|
||||
]
|
||||
ua = [
|
||||
"що таке",
|
||||
"хто такий",
|
||||
"розкажи про",
|
||||
]
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
local lang = jarvis.context.language
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
|
||||
local triggers = {
|
||||
"найди в википедии про", "википедия про", "расскажи про",
|
||||
"кто такая", "кто такой", "что такое",
|
||||
"tell me about", "wikipedia about", "who is", "what is",
|
||||
"розкажи про", "хто такий", "що таке",
|
||||
}
|
||||
local query = phrase
|
||||
for _, t in ipairs(triggers) do
|
||||
local s, e = string.find(query, t, 1, true)
|
||||
if s == 1 then
|
||||
query = query:sub(e + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
query = query:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
if query == "" then
|
||||
jarvis.system.notify("Wiki", lang == "ru" and "О чём рассказать?" or "What to look up?")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local function url_encode(s)
|
||||
return (s:gsub("([^%w%-_%.~])", function(c) return string.format("%%%02X", string.byte(c)) end))
|
||||
end
|
||||
|
||||
local host = lang == "en" and "en.wikipedia.org" or "ru.wikipedia.org"
|
||||
|
||||
local search_url = string.format(
|
||||
"https://%s/w/api.php?action=opensearch&search=%s&limit=1&format=json",
|
||||
host, url_encode(query)
|
||||
)
|
||||
local search = jarvis.http.get(search_url)
|
||||
if not search.ok then
|
||||
jarvis.system.notify("Wiki", "Поиск не удался")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local title = search.body:match('%["[^"]+",%s*%["([^"]+)"')
|
||||
if not title or title == "" then
|
||||
jarvis.system.notify("Wiki", (lang == "ru" and "Не нашёл: " or "Not found: ") .. query)
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local summary_url = string.format(
|
||||
"https://%s/api/rest_v1/page/summary/%s",
|
||||
host, url_encode(title)
|
||||
)
|
||||
local summary = jarvis.http.json(summary_url)
|
||||
if not summary or type(summary) ~= "table" or not summary.extract then
|
||||
jarvis.system.notify("Wiki", "Не получил конспект")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local extract = summary.extract
|
||||
local article_title = summary.title or title
|
||||
|
||||
local token = jarvis.system.env("GROQ_TOKEN")
|
||||
local say_text = extract:sub(1, 400)
|
||||
|
||||
if token and token ~= "" and #extract > 350 then
|
||||
local base = jarvis.system.env("GROQ_BASE_URL"); if not base or base == "" then base = "https://api.groq.com/openai/v1" end
|
||||
local model = jarvis.system.env("GROQ_MODEL"); if not model or model == "" then model = "llama-3.3-70b-versatile" end
|
||||
local sys = lang == "ru"
|
||||
and "Тебе дана статья из Википедии. Перескажи суть простым разговорным языком в 2-3 предложения. Без 'википедия гласит' и подобных вступлений."
|
||||
or "Summarise this Wikipedia text in 2-3 plain-language sentences. No 'wikipedia says' preambles."
|
||||
local payload = {
|
||||
model = model,
|
||||
messages = {
|
||||
{ role = "system", content = sys },
|
||||
{ role = "user", content = extract:sub(1, 2500) },
|
||||
},
|
||||
max_tokens = 220,
|
||||
temperature = 0.3,
|
||||
}
|
||||
local lr = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token })
|
||||
if lr.ok then
|
||||
local c = (lr.body or ""):match('"content"%s*:%s*"(.-[^\\])"')
|
||||
if c then
|
||||
c = c:gsub('\\n', ' '):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub("%s+", " ")
|
||||
say_text = c:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
jarvis.system.clipboard.set(article_title .. "\n\n" .. extract)
|
||||
jarvis.system.notify(article_title, say_text:sub(1, 240))
|
||||
|
||||
local sapi = say_text:gsub("'", "''"):gsub("\r?\n", " ")
|
||||
local ps = string.format(
|
||||
[[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]],
|
||||
sapi
|
||||
)
|
||||
jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"')))
|
||||
|
||||
jarvis.audio.play_ok()
|
||||
return { chain = false }
|
||||
40
resources/commands/wikipedia/command.toml
Normal file
40
resources/commands/wikipedia/command.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Wikipedia summary — fetch a topic's intro from Wikipedia REST API, speak it.
|
||||
|
||||
[[commands]]
|
||||
id = "wikipedia.summary"
|
||||
type = "lua"
|
||||
script = "summary.lua"
|
||||
sandbox = "full"
|
||||
timeout = 15000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"что такое",
|
||||
"кто такой",
|
||||
"кто такая",
|
||||
"расскажи про",
|
||||
"википедия",
|
||||
"найди в википедии",
|
||||
"что говорит википедия",
|
||||
"поищи в википедии",
|
||||
"википедия о",
|
||||
"википедия про",
|
||||
"найди в википедии про",
|
||||
"статья из википедии",
|
||||
]
|
||||
en = [
|
||||
"wikipedia",
|
||||
"look up in wikipedia",
|
||||
"wikipedia article on",
|
||||
"what is",
|
||||
"who is",
|
||||
"tell me about",
|
||||
"wikipedia about",
|
||||
]
|
||||
ua = [
|
||||
"вікіпедія",
|
||||
"знайди у вікіпедії",
|
||||
"що таке",
|
||||
"хто такий",
|
||||
"розкажи про",
|
||||
]
|
||||
105
resources/commands/wikipedia/summary.lua
Normal file
105
resources/commands/wikipedia/summary.lua
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
-- "Википедия Эйнштейн" → fetch ru.wikipedia.org/api/rest_v1/page/summary/<topic>
|
||||
-- Falls back to LLM if Wikipedia doesn't have the article.
|
||||
local phrase = (jarvis.context.phrase or "")
|
||||
|
||||
local query = jarvis.text.strip_trigger(phrase:lower(), {
|
||||
"найди в википедии про",
|
||||
"найди в википедии",
|
||||
"что говорит википедия",
|
||||
"поищи в википедии",
|
||||
"статья из википедии",
|
||||
"википедия про",
|
||||
"википедия о",
|
||||
"википедия",
|
||||
"расскажи про",
|
||||
"что такое",
|
||||
"кто такая",
|
||||
"кто такой",
|
||||
"look up in wikipedia",
|
||||
"wikipedia article on",
|
||||
"wikipedia about",
|
||||
"wikipedia",
|
||||
"tell me about",
|
||||
"what is",
|
||||
"who is",
|
||||
"знайди у вікіпедії",
|
||||
"вікіпедія",
|
||||
"що таке",
|
||||
"хто такий",
|
||||
"розкажи про",
|
||||
})
|
||||
query = query:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
|
||||
|
||||
if query == "" then
|
||||
return jarvis.cmd.error("Какая статья?")
|
||||
end
|
||||
|
||||
-- Wikipedia title needs underscores for spaces. Capitalize first letter of each word
|
||||
-- to match canonical titles (good enough heuristic).
|
||||
local title = query:gsub("(%S+)", function(w)
|
||||
-- Capitalize first byte (works for ASCII; for cyrillic, leave alone).
|
||||
local first = w:sub(1, 1)
|
||||
if first:match("[%a]") then
|
||||
return first:upper() .. w:sub(2)
|
||||
end
|
||||
return w
|
||||
end):gsub(" ", "_")
|
||||
|
||||
local function urlencode(s)
|
||||
return (s:gsub("[^A-Za-z0-9%-_%.~]", function(c)
|
||||
return string.format("%%%02X", string.byte(c))
|
||||
end))
|
||||
end
|
||||
|
||||
-- Try Russian first, fall back to English.
|
||||
local lang_chain = { "ru", "en" }
|
||||
local data, source_lang
|
||||
|
||||
for _, lang in ipairs(lang_chain) do
|
||||
local url = string.format("https://%s.wikipedia.org/api/rest_v1/page/summary/%s",
|
||||
lang, urlencode(title))
|
||||
local d = jarvis.http.json(url)
|
||||
if d and d.extract and d.extract ~= "" then
|
||||
data = d
|
||||
source_lang = lang
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not data then
|
||||
-- last resort: ask LLM directly
|
||||
local llm_answer = jarvis.llm({
|
||||
{ role = "system", content = "Кратко (2-3 предложения) на русском объясни тему. Если не знаешь — скажи." },
|
||||
{ role = "user", content = "Расскажи про: " .. query }
|
||||
}, { max_tokens = 200, temperature = 0.3 })
|
||||
if llm_answer and llm_answer ~= "" then
|
||||
return jarvis.cmd.ok(llm_answer)
|
||||
end
|
||||
return jarvis.cmd.not_found("В Википедии нет статьи про " .. query .. ".")
|
||||
end
|
||||
|
||||
local extract = data.extract
|
||||
local title_actual = data.title or query
|
||||
|
||||
-- Truncate extract to ~3 sentences for spoken output
|
||||
local sentences = {}
|
||||
for sentence in extract:gmatch("[^%.%!%?]+[%.%!%?]") do
|
||||
table.insert(sentences, sentence)
|
||||
if #sentences >= 3 then break end
|
||||
end
|
||||
local spoken = #sentences > 0 and table.concat(sentences) or extract:sub(1, 400)
|
||||
|
||||
if source_lang == "en" then
|
||||
-- Translate English extract to Russian via LLM (no point in speaking English
|
||||
-- through a Russian voice).
|
||||
local translated = jarvis.llm({
|
||||
{ role = "system", content = "Переведи английский текст на русский, без вводных и комментариев." },
|
||||
{ role = "user", content = spoken }
|
||||
}, { max_tokens = 300, temperature = 0.0 })
|
||||
if translated and translated ~= "" then
|
||||
spoken = translated
|
||||
end
|
||||
end
|
||||
|
||||
jarvis.system.notify("Wikipedia: " .. title_actual, spoken:sub(1, 200))
|
||||
return jarvis.cmd.ok(spoken)
|
||||
Loading…
Add table
Add a link
Reference in a new issue