echodict/llama.cpp
version https://git-lfs.github.com/spec/v1 oid sha256:cfc44b7ba25614df70e6b65e3341cae0310163bd32fd31a6b928a542df433faf size 30786
0762
1#include "server-tools.h"2 3#include <sheredom/subprocess.h>4 5#include <filesystem>6#include <fstream>7#include <regex>8#include <thread>9#include <chrono>10#include <atomic>11#include <cstring>12#include <climits>13 14namespace fs = std::filesystem;15 16//17// internal helpers18//19 20static std::vector<char *> to_cstr_vec(const std::vector<std::string> & v) {21 std::vector<char *> r;22 r.reserve(v.size() + 1);23 for (const auto & s : v) {24 r.push_back(const_cast<char *>(s.c_str()));25 }26 r.push_back(nullptr);27 return r;28}29 30struct run_proc_result {31 std::string output;32 int exit_code = -1;33 bool timed_out = false;34};35 36static run_proc_result run_process(37 const std::vector<std::string> & args,38 size_t max_output,39 int timeout_secs) {40 run_proc_result res;41 42 subprocess_s proc;43 auto argv = to_cstr_vec(args);44 45 int options = subprocess_option_no_window46 | subprocess_option_combined_stdout_stderr47 | subprocess_option_inherit_environment48 | subprocess_option_search_user_path;49 50 if (subprocess_create(argv.data(), options, &proc) != 0) {51 res.output = "failed to spawn process";52 return res;53 }54 55 std::atomic<bool> done{false};56 std::atomic<bool> timed_out{false};57 58 std::thread timeout_thread([&]() {59 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_secs);60 while (!done.load()) {61 if (std::chrono::steady_clock::now() >= deadline) {62 timed_out.store(true);63 subprocess_terminate(&proc);64 return;65 }66 std::this_thread::sleep_for(std::chrono::milliseconds(100));67 }68 });69 70 FILE * f = subprocess_stdout(&proc);71 std::string output;72 bool truncated = false;73 if (f) {74 char buf[4096];75 while (fgets(buf, sizeof(buf), f) != nullptr) {76 if (!truncated) {77 size_t len = strlen(buf);78 if (output.size() + len <= max_output) {79 output.append(buf, len);80 } else {81 output.append(buf, max_output - output.size());82 truncated = true;83 }84 }85 }86 }87 88 done.store(true);89 if (timeout_thread.joinable()) {90 timeout_thread.join();91 }92 93 subprocess_join(&proc, &res.exit_code);94 subprocess_destroy(&proc);95 96 res.output = output;97 res.timed_out = timed_out.load();98 if (truncated) {99 res.output += "\n[output truncated]";100 }101 return res;102}103 104json server_tool::to_json() {105 return {106 {"display_name", display_name},107 {"tool", name},108 {"type", "builtin"},109 {"permissions", json{110 {"write", permission_write}111 }},112 {"definition", get_definition()},113 };114}115 116//117// read_file: read a file with optional line range and line-number prefix118//119 120static constexpr size_t SERVER_TOOL_READ_FILE_MAX_SIZE = 16 * 1024; // 16 KB121 122struct server_tool_read_file : server_tool {123 server_tool_read_file() {124 name = "read_file";125 display_name = "Read file";126 permission_write = false;127 }128 129 json get_definition() override {130 return {131 {"type", "function"},132 {"function", {133 {"name", name},134 {"description", "Read the contents of a file. Optionally specify a 1-based line range. "135 "If append_loc is true, each line is prefixed with its line number (e.g. \"1\u2192 ...\")."},136 {"parameters", {137 {"type", "object"},138 {"properties", {139 {"path", {{"type", "string"}, {"description", "Path to the file"}}},140 {"start_line", {{"type", "integer"}, {"description", "First line to read, 1-based (default: 1)"}}},141 {"end_line", {{"type", "integer"}, {"description", "Last line to read, 1-based inclusive (default: end of file)"}}},142 {"append_loc", {{"type", "boolean"}, {"description", "Prefix each line with its line number"}}},143 }},144 {"required", json::array({"path"})},145 }},146 }},147 };148 }149 150 json invoke(json params) override {151 std::string path = params.at("path").get<std::string>();152 int start_line = json_value(params, "start_line", 1);153 int end_line = json_value(params, "end_line", -1); // -1 = no limit154 bool append_loc = json_value(params, "append_loc", false);155 156 std::error_code ec;157 uintmax_t file_size = fs::file_size(path, ec);158 if (ec) {159 return {{"error", "cannot stat file: " + ec.message()}};160 }161 if (file_size > SERVER_TOOL_READ_FILE_MAX_SIZE && end_line == -1) {162 return {{"error", string_format(163 "file too large (%zu bytes, max %zu). Use start_line/end_line to read a portion.",164 (size_t)file_size, SERVER_TOOL_READ_FILE_MAX_SIZE)}};165 }166 167 std::ifstream f(path);168 if (!f) {169 return {{"error", "failed to open file: " + path}};170 }171 172 std::string result;173 std::string line;174 int lineno = 0;175 176 while (std::getline(f, line)) {177 lineno++;178 if (lineno < start_line) continue;179 if (end_line != -1 && lineno > end_line) break;180 181 std::string out_line;182 if (append_loc) {183 out_line = std::to_string(lineno) + "\u2192 " + line + "\n";184 } else {185 out_line = line + "\n";186 }187 188 if (result.size() + out_line.size() > SERVER_TOOL_READ_FILE_MAX_SIZE) {189 result += "[output truncated]";190 break;191 }192 result += out_line;193 }194 195 return {{"plain_text_response", result}};196 }197};198 199//200// file_glob_search: find files matching a glob pattern under a base directory201//202 203static constexpr size_t SERVER_TOOL_FILE_SEARCH_MAX_RESULTS = 100;204 205struct server_tool_file_glob_search : server_tool {206 server_tool_file_glob_search() {207 name = "file_glob_search";208 display_name = "File search";209 permission_write = false;210 }211 212 json get_definition() override {213 return {214 {"type", "function"},215 {"function", {216 {"name", name},217 {"description", "Recursively search for files matching a glob pattern under a directory."},218 {"parameters", {219 {"type", "object"},220 {"properties", {221 {"path", {{"type", "string"}, {"description", "Base directory to search in"}}},222 {"include", {{"type", "string"}, {"description", "Glob pattern for files to include (e.g. \"**/*.cpp\"). Default: **"}}},223 {"exclude", {{"type", "string"}, {"description", "Glob pattern for files to exclude"}}},224 }},225 {"required", json::array({"path"})},226 }},227 }},228 };229 }230 231 json invoke(json params) override {232 std::string base = params.at("path").get<std::string>();233 std::string include = json_value(params, "include", std::string("**"));234 std::string exclude = json_value(params, "exclude", std::string(""));235 236 std::ostringstream output_text;237 size_t count = 0;238 239 std::error_code ec;240 for (const auto & entry : fs::recursive_directory_iterator(base,241 fs::directory_options::skip_permission_denied, ec)) {242 if (!entry.is_regular_file()) continue;243 244 std::string rel = fs::relative(entry.path(), base, ec).string();245 if (ec) continue;246 std::replace(rel.begin(), rel.end(), '\\', '/');247 248 if (!glob_match(include, rel)) continue;249 if (!exclude.empty() && glob_match(exclude, rel)) continue;250 251 output_text << entry.path().string() << "\n";252 if (++count >= SERVER_TOOL_FILE_SEARCH_MAX_RESULTS) {253 break;254 }255 }256 257 output_text << "\n---\nTotal matches: " << count << "\n";258 259 return {{"plain_text_response", output_text.str()}};260 }261};262 263//264// grep_search: search for a regex pattern in files265//266 267static constexpr size_t SERVER_TOOL_GREP_SEARCH_MAX_RESULTS = 100;268 269struct server_tool_grep_search : server_tool {270 server_tool_grep_search() {271 name = "grep_search";272 display_name = "Grep search";273 permission_write = false;274 }275 276 json get_definition() override {277 return {278 {"type", "function"},279 {"function", {280 {"name", name},281 {"description", "Search for a regex pattern in files under a path. Returns matching lines."},282 {"parameters", {283 {"type", "object"},284 {"properties", {285 {"path", {{"type", "string"}, {"description", "File or directory to search in"}}},286 {"pattern", {{"type", "string"}, {"description", "Regular expression pattern to search for"}}},287 {"include", {{"type", "string"}, {"description", "Glob pattern to filter files (default: **)"}}},288 {"exclude", {{"type", "string"}, {"description", "Glob pattern to exclude files"}}},289 {"return_line_numbers", {{"type", "boolean"}, {"description", "If true, include line numbers in results"}}},290 }},291 {"required", json::array({"path", "pattern"})},292 }},293 }},294 };295 }296 297 json invoke(json params) override {298 std::string path = params.at("path").get<std::string>();299 std::string pat_str = params.at("pattern").get<std::string>();300 std::string include = json_value(params, "include", std::string("**"));301 std::string exclude = json_value(params, "exclude", std::string(""));302 bool show_lineno = json_value(params, "return_line_numbers", false);303 304 std::regex pattern;305 try {306 pattern = std::regex(pat_str);307 } catch (const std::regex_error & e) {308 return {{"error", std::string("invalid regex: ") + e.what()}};309 }310 311 std::ostringstream output_text;312 size_t total = 0;313 314 auto search_file = [&](const fs::path & fpath) {315 std::ifstream f(fpath);316 if (!f) return;317 std::string line;318 int lineno = 0;319 while (std::getline(f, line) && total < SERVER_TOOL_GREP_SEARCH_MAX_RESULTS) {320 lineno++;321 if (std::regex_search(line, pattern)) {322 output_text << fpath.string() << ":";323 if (show_lineno) {324 output_text << lineno << ":";325 }326 output_text << line << "\n";327 total++;328 }329 }330 };331 332 std::error_code ec;333 if (fs::is_regular_file(path, ec)) {334 search_file(path);335 } else if (fs::is_directory(path, ec)) {336 for (const auto & entry : fs::recursive_directory_iterator(path,337 fs::directory_options::skip_permission_denied, ec)) {338 if (!entry.is_regular_file()) continue;339 if (total >= SERVER_TOOL_GREP_SEARCH_MAX_RESULTS) break;340 341 std::string rel = fs::relative(entry.path(), path, ec).string();342 if (ec) continue;343 std::replace(rel.begin(), rel.end(), '\\', '/');344 345 if (!glob_match(include, rel)) continue;346 if (!exclude.empty() && glob_match(exclude, rel)) continue;347 348 search_file(entry.path());349 }350 } else {351 return {{"error", "path does not exist: " + path}};352 }353 354 output_text << "\n\n---\nTotal matches: " << total << "\n";355 356 return {{"plain_text_response", output_text.str()}};357 }358};359 360//361// exec_shell_command: run an arbitrary shell command362//363 364static constexpr size_t SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE = 16 * 1024; // 16 KB365static constexpr int SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT = 60; // seconds366 367struct server_tool_exec_shell_command : server_tool {368 server_tool_exec_shell_command() {369 name = "exec_shell_command";370 display_name = "Execute shell command";371 permission_write = true;372 }373 374 json get_definition() override {375 return {376 {"type", "function"},377 {"function", {378 {"name", name},379 {"description", "Execute a shell command and return its output (stdout and stderr combined)."},380 {"parameters", {381 {"type", "object"},382 {"properties", {383 {"command", {{"type", "string"}, {"description", "Shell command to execute"}}},384 {"timeout", {{"type", "integer"}, {"description", string_format("Timeout in seconds (default 10, max %d)", SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT)}}},385 {"max_output_size", {{"type", "integer"}, {"description", string_format("Maximum output size in bytes (default %zu)", SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE)}}},386 }},387 {"required", json::array({"command"})},388 }},389 }},390 };391 }392 393 json invoke(json params) override {394 std::string command = params.at("command").get<std::string>();395 int timeout = json_value(params, "timeout", 10);396 size_t max_output = (size_t) json_value(params, "max_output_size", (int) SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE);397 398 timeout = std::min(timeout, SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT);399 max_output = std::min(max_output, SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE);400 401#ifdef _WIN32402 std::vector<std::string> args = {"cmd", "/c", command};403#else404 std::vector<std::string> args = {"sh", "-c", command};405#endif406 407 auto res = run_process(args, max_output, timeout);408 409 std::string text_output = res.output;410 text_output += string_format("\n[exit code: %d]", res.exit_code);411 if (res.timed_out) {412 text_output += " [exit due to timed out]";413 }414 415 return {{"plain_text_response", text_output}};416 }417};418 419//420// write_file: create or overwrite a file421//422 423struct server_tool_write_file : server_tool {424 server_tool_write_file() {425 name = "write_file";426 display_name = "Write file";427 permission_write = true;428 }429 430 json get_definition() override {431 return {432 {"type", "function"},433 {"function", {434 {"name", name},435 {"description", "Write content to a file, creating it (including parent directories) if it does not exist. May use with edit_file for more complex edits."},436 {"parameters", {437 {"type", "object"},438 {"properties", {439 {"path", {{"type", "string"}, {"description", "Path of the file to write"}}},440 {"content", {{"type", "string"}, {"description", "Content to write"}}},441 }},442 {"required", json::array({"path", "content"})},443 }},444 }},445 };446 }447 448 json invoke(json params) override {449 std::string path = params.at("path").get<std::string>();450 std::string content = params.at("content").get<std::string>();451 452 std::error_code ec;453 fs::path fpath(path);454 if (fpath.has_parent_path()) {455 fs::create_directories(fpath.parent_path(), ec);456 if (ec) {457 return {{"error", "failed to create directories: " + ec.message()}};458 }459 }460 461 std::ofstream f(path, std::ios::binary);462 if (!f) {463 return {{"error", "failed to open file for writing: " + path}};464 }465 f << content;466 if (!f) {467 return {{"error", "failed to write file: " + path}};468 }469 470 return {{"result", "file written successfully"}, {"path", path}, {"bytes", content.size()}};471 }472};473 474//475// edit_file: edit file content via line-based changes476//477 478struct server_tool_edit_file : server_tool {479 server_tool_edit_file() {480 name = "edit_file";481 display_name = "Edit file";482 permission_write = true;483 }484 485 json get_definition() override {486 return {487 {"type", "function"},488 {"function", {489 {"name", name},490 {"description",491 "Edit a file by applying a list of line-based changes. "492 "Each change targets a 1-based inclusive line range and has a mode: "493 "\"replace\" (replace lines with content), "494 "\"delete\" (remove lines, content must be empty string), "495 "\"append\" (insert content after line_end). "496 "Set line_start to -1 to target the end of file (line_end is ignored in that case). "497 "Changes must not overlap. They are applied in reverse line order automatically."},498 {"parameters", {499 {"type", "object"},500 {"properties", {501 {"path", {{"type", "string"}, {"description", "Path to the file to edit"}}},502 {"changes", {503 {"type", "array"},504 {"description", "List of changes to apply"},505 {"items", {506 {"type", "object"},507 {"properties", {508 {"mode", {{"type", "string"}, {"description", "\"replace\", \"delete\", or \"append\""}}},509 {"line_start", {{"type", "integer"}, {"description", "First line of the range (1-based); use -1 for end of file"}}},510 {"line_end", {{"type", "integer"}, {"description", "Last line of the range (1-based, inclusive); ignored when line_start is -1"}}},511 {"content", {{"type", "string"}, {"description", "Content to insert; must be empty string for delete mode"}}},512 }},513 {"required", json::array({"mode", "line_start", "line_end", "content"})},514 }},515 }},516 }},517 {"required", json::array({"path", "changes"})},518 }},519 }},520 };521 }522 523 json invoke(json params) override {524 std::string path = params.at("path").get<std::string>();525 const json & changes = params.at("changes");526 527 if (!changes.is_array()) {528 return {{"error", "\"changes\" must be an array"}};529 }530 531 // read file into lines532 std::ifstream fin(path);533 if (!fin) {534 return {{"error", "failed to open file: " + path}};535 }536 std::vector<std::string> lines;537 {538 std::string line;539 while (std::getline(fin, line)) {540 lines.push_back(line);541 }542 }543 fin.close();544 545 // validate and collect changes, then sort descending by line_start546 struct change_entry {547 std::string mode;548 int line_start; // 1-based549 int line_end; // 1-based inclusive550 std::string content;551 };552 std::vector<change_entry> entries;553 entries.reserve(changes.size());554 555 for (const auto & ch : changes) {556 change_entry e;557 e.mode = ch.at("mode").get<std::string>();558 e.line_start = ch.at("line_start").get<int>();559 e.line_end = ch.at("line_end").get<int>();560 e.content = ch.at("content").get<std::string>();561 562 if (e.mode != "replace" && e.mode != "delete" && e.mode != "append") {563 return {{"error", "invalid mode \"" + e.mode + "\"; must be replace, delete, or append"}};564 }565 if (e.mode == "delete" && !e.content.empty()) {566 return {{"error", "content must be empty string for delete mode"}};567 }568 int n = (int) lines.size();569 if (e.line_start == -1) {570 // -1 means end of file; line_end is ignored — normalize to point past last line571 e.line_start = n + 1;572 e.line_end = n + 1;573 } else {574 if (e.line_start < 1 || e.line_end < e.line_start) {575 return {{"error", string_format("invalid line range [%d, %d]", e.line_start, e.line_end)}};576 }577 if (e.line_end > n) {578 return {{"error", string_format("line_end %d exceeds file length %d", e.line_end, n)}};579 }580 }581 entries.push_back(std::move(e));582 }583 584 // sort descending so earlier-indexed changes don't shift later ones585 std::sort(entries.begin(), entries.end(), [](const change_entry & a, const change_entry & b) {586 return a.line_start > b.line_start;587 });588 589 // apply changes (0-based indices internally)590 for (const auto & e : entries) {591 int idx_start = e.line_start - 1; // 0-based592 int idx_end = e.line_end - 1; // 0-based inclusive593 594 // split content into lines (preserve trailing newline awareness)595 std::vector<std::string> new_lines;596 if (!e.content.empty()) {597 std::istringstream ss(e.content);598 std::string ln;599 while (std::getline(ss, ln)) {600 new_lines.push_back(ln);601 }602 // if content ends with \n, getline consumed it — no extra empty line needed603 // if content does NOT end with \n, last line is still captured correctly604 }605 606 if (e.mode == "replace") {607 // erase [idx_start, idx_end] and insert new_lines608 lines.erase(lines.begin() + idx_start, lines.begin() + idx_end + 1);609 lines.insert(lines.begin() + idx_start, new_lines.begin(), new_lines.end());610 } else if (e.mode == "delete") {611 lines.erase(lines.begin() + idx_start, lines.begin() + idx_end + 1);612 } else { // append613 // idx_end + 1 may equal lines.size() when line_start == -1 (end of file)614 lines.insert(lines.begin() + idx_end + 1, new_lines.begin(), new_lines.end());615 }616 }617 618 // write file back619 std::ofstream fout(path, std::ios::binary);620 if (!fout) {621 return {{"error", "failed to open file for writing: " + path}};622 }623 for (size_t i = 0; i < lines.size(); i++) {624 fout << lines[i];625 if (i + 1 < lines.size()) {626 fout << "\n";627 }628 }629 if (!lines.empty()) {630 fout << "\n";631 }632 if (!fout) {633 return {{"error", "failed to write file: " + path}};634 }635 636 return {{"result", "file edited successfully"}, {"path", path}, {"lines", (int) lines.size()}};637 }638};639 640//641// apply_diff: apply a unified diff via git apply642//643 644struct server_tool_apply_diff : server_tool {645 server_tool_apply_diff() {646 name = "apply_diff";647 display_name = "Apply diff";648 permission_write = true;649 }650 651 json get_definition() override {652 return {653 {"type", "function"},654 {"function", {655 {"name", name},656 {"description", "Apply a unified diff to edit one or more files using git apply. Use this instead of edit_file when the changes are complex."},657 {"parameters", {658 {"type", "object"},659 {"properties", {660 {"diff", {{"type", "string"}, {"description", "Unified diff content in git diff format"}}},661 }},662 {"required", json::array({"diff"})},663 }},664 }},665 };666 }667 668 json invoke(json params) override {669 std::string diff = params.at("diff").get<std::string>();670 671 // write diff to a temporary file672 static std::atomic<int> counter{0};673 std::string tmp_path = (fs::temp_directory_path() /674 ("llama_patch_" + std::to_string(++counter) + ".patch")).string();675 676 {677 std::ofstream f(tmp_path, std::ios::binary);678 if (!f) {679 return {{"error", "failed to create temp patch file"}};680 }681 f << diff;682 }683 684 auto res = run_process({"git", "apply", tmp_path}, 4096, 10);685 686 std::error_code ec;687 fs::remove(tmp_path, ec);688 689 if (res.exit_code != 0) {690 return {{"error", "git apply failed (exit " + std::to_string(res.exit_code) + "): " + res.output}};691 }692 return {{"result", "patch applied successfully"}};693 }694};695 696//697// public API698//699 700static std::vector<std::unique_ptr<server_tool>> build_tools() {701 std::vector<std::unique_ptr<server_tool>> tools;702 tools.push_back(std::make_unique<server_tool_read_file>());703 tools.push_back(std::make_unique<server_tool_file_glob_search>());704 tools.push_back(std::make_unique<server_tool_grep_search>());705 tools.push_back(std::make_unique<server_tool_exec_shell_command>());706 tools.push_back(std::make_unique<server_tool_write_file>());707 tools.push_back(std::make_unique<server_tool_edit_file>());708 tools.push_back(std::make_unique<server_tool_apply_diff>());709 return tools;710}711 712void server_tools::setup(const std::vector<std::string> & enabled_tools) {713 if (!enabled_tools.empty()) {714 std::unordered_set<std::string> enabled_set(enabled_tools.begin(), enabled_tools.end());715 auto all_tools = build_tools();716 717 tools.clear();718 for (auto & t : all_tools) {719 if (enabled_set.count(t->name) > 0 || enabled_set.count("all") > 0) {720 tools.push_back(std::move(t));721 }722 }723 }724 725 handle_get = [this](const server_http_req &) -> server_http_res_ptr {726 auto res = std::make_unique<server_http_res>();727 try {728 json result = json::array();729 for (const auto & t : tools) {730 result.push_back(t->to_json());731 }732 res->data = safe_json_to_str(result);733 } catch (const std::exception & e) {734 SRV_ERR("got exception: %s\n", e.what());735 res->status = 500;736 res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_SERVER));737 }738 return res;739 };740 741 handle_post = [this](const server_http_req & req) -> server_http_res_ptr {742 auto res = std::make_unique<server_http_res>();743 try {744 json body = json::parse(req.body);745 std::string tool_name = body.at("tool").get<std::string>();746 json params = body.value("params", json::object());747 json result = invoke(tool_name, params);748 res->data = safe_json_to_str(result);749 } catch (const json::exception & e) {750 res->status = 400;751 res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST));752 } catch (const std::exception & e) {753 SRV_ERR("got exception: %s\n", e.what());754 res->status = 500;755 res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_SERVER));756 }757 return res;758 };759}760 761json server_tools::invoke(const std::string & name, const json & params) {762 for (auto & t : tools) {763 if (t->name == name) {764 return t->invoke(params);765 }766 }767 return {{"error", "unknown tool: " + name}};768}769 