99 lines
3.8 KiB
Lua
99 lines
3.8 KiB
Lua
|
|
-- "что делает функция 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)
|