CoolFace
Datasetpublic

echodict/llama.cpp

version https://git-lfs.github.com/spec/v1 oid sha256:cfc44b7ba25614df70e6b65e3341cae0310163bd32fd31a6b928a542df433faf size 30786

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes762downloads
server-tools.cpp769 linesDownload Raw Back to server
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