mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-07-21 18:25:57 +00:00
read_file with append_loc emits "{n}\u2192 {line}". The space after the
arrow is meant as a separator, but it is indistinguishable from real
indentation. Models strip "{n}\u2192" yet keep the space, so the old_text
passed to edit_file carries a phantom leading space and never matches
(normalize_for_fuzzy_match trims trailing whitespace only, never leading).
Drop the separator space so the arrow abuts content: stripping "{n}\u2192"
now yields the exact line with its real indentation preserved, and the
failure mode cannot occur by construction. Update the description example
to match the new format.
1246 lines
48 KiB
C++
1246 lines
48 KiB
C++
#include "server-tools.h"
|
|
|
|
#include <sheredom/subprocess.h>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#include <climits>
|
|
#include <algorithm>
|
|
#include <unordered_set>
|
|
#include <functional>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
//
|
|
// internal helpers
|
|
//
|
|
|
|
json server_tool::to_json() const {
|
|
return {
|
|
{"display_name", display_name},
|
|
{"tool", name},
|
|
{"type", "builtin"},
|
|
{"permissions", json{
|
|
{"write", permission_write}
|
|
}},
|
|
{"definition", get_definition()},
|
|
};
|
|
}
|
|
|
|
static constexpr size_t SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT = 8 * 1024 * 1024; // 8 MB
|
|
static constexpr int SERVER_TOOL_GIT_LS_FILES_TIMEOUT = 15; // seconds
|
|
|
|
class tools_io {
|
|
public:
|
|
struct exec_result {
|
|
std::string output;
|
|
int exit_code = -1;
|
|
bool timed_out = false;
|
|
};
|
|
|
|
virtual ~tools_io() = default;
|
|
|
|
virtual bool is_directory(const std::string & path) const = 0;
|
|
virtual bool is_regular_file(const std::string & path) const = 0;
|
|
virtual bool file_size(const std::string & path, uintmax_t & out_size) const = 0;
|
|
virtual bool read_file(const std::string & path, std::string & out) const = 0;
|
|
virtual bool write_file(const std::string & path, const std::string & content) const = 0;
|
|
// paths relative to `base`, '/'-separated; sets `err` if `base` isn't a directory
|
|
virtual std::vector<std::string> list_files(const std::string & base, std::string & err) const = 0;
|
|
// on_chunk, if set, is called with each chunk of output as it is read (before truncation cuts in);
|
|
// returning false terminates the process early (e.g. the client disconnected)
|
|
virtual exec_result run(
|
|
const std::vector<std::string> & args,
|
|
size_t max_output,
|
|
int timeout_secs,
|
|
const std::function<bool(const std::string &)> & on_chunk = nullptr) const = 0;
|
|
};
|
|
|
|
class tools_io_basic : public tools_io {
|
|
public:
|
|
bool is_directory(const std::string & path) const override {
|
|
std::error_code ec;
|
|
return fs::is_directory(path, ec) && !ec;
|
|
}
|
|
|
|
bool is_regular_file(const std::string & path) const override {
|
|
std::error_code ec;
|
|
return fs::is_regular_file(path, ec) && !ec;
|
|
}
|
|
|
|
bool file_size(const std::string & path, uintmax_t & out_size) const override {
|
|
std::error_code ec;
|
|
out_size = fs::file_size(path, ec);
|
|
return !ec;
|
|
}
|
|
|
|
bool read_file(const std::string & path, std::string & out) const override {
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f) return false;
|
|
std::ostringstream ss;
|
|
ss << f.rdbuf();
|
|
out = ss.str();
|
|
return true;
|
|
}
|
|
|
|
bool write_file(const std::string & path, const std::string & content) const override {
|
|
std::error_code ec;
|
|
fs::path fpath(path);
|
|
if (fpath.has_parent_path()) {
|
|
fs::create_directories(fpath.parent_path(), ec);
|
|
if (ec) return false;
|
|
}
|
|
std::ofstream f(path, std::ios::binary);
|
|
if (!f) return false;
|
|
f << content;
|
|
return (bool) f;
|
|
}
|
|
|
|
std::vector<std::string> list_files(const std::string & base, std::string & err) const override {
|
|
err.clear();
|
|
if (!is_directory(base)) {
|
|
err = "path does not exist or is not a directory: " + base;
|
|
return {};
|
|
}
|
|
|
|
auto res = run(
|
|
{"git", "-C", base, "ls-files", "--cached", "--others", "--exclude-standard"},
|
|
SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT, SERVER_TOOL_GIT_LS_FILES_TIMEOUT);
|
|
|
|
if (res.exit_code == 0 && !res.timed_out) {
|
|
std::vector<std::string> result;
|
|
std::istringstream iss(res.output);
|
|
std::string line;
|
|
while (std::getline(iss, line)) {
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
if (line.empty()) continue;
|
|
std::replace(line.begin(), line.end(), '\\', '/');
|
|
if (is_regular_file((fs::path(base) / line).string())) {
|
|
result.push_back(line);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
return list_files_fallback(base);
|
|
}
|
|
|
|
exec_result run(
|
|
const std::vector<std::string> & args,
|
|
size_t max_output,
|
|
int timeout_secs,
|
|
const std::function<bool(const std::string &)> & on_chunk = nullptr) const override {
|
|
exec_result res;
|
|
|
|
subprocess_s proc;
|
|
auto argv = to_cstr_vec(args);
|
|
|
|
int options = subprocess_option_no_window
|
|
| subprocess_option_combined_stdout_stderr
|
|
| subprocess_option_inherit_environment
|
|
| subprocess_option_search_user_path;
|
|
|
|
if (subprocess_create(argv.data(), options, &proc) != 0) {
|
|
res.output = "failed to spawn process";
|
|
return res;
|
|
}
|
|
|
|
std::atomic<bool> done{false};
|
|
std::atomic<bool> timed_out{false};
|
|
|
|
std::thread timeout_thread([&]() {
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_secs);
|
|
while (!done.load()) {
|
|
if (std::chrono::steady_clock::now() >= deadline) {
|
|
timed_out.store(true);
|
|
subprocess_terminate(&proc);
|
|
return;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
});
|
|
|
|
FILE * f = subprocess_stdout(&proc);
|
|
std::string output;
|
|
bool truncated = false;
|
|
if (f) {
|
|
char buf[4096];
|
|
while (fgets(buf, sizeof(buf), f) != nullptr) {
|
|
if (!truncated) {
|
|
size_t len = strlen(buf);
|
|
if (output.size() + len <= max_output) {
|
|
output.append(buf, len);
|
|
if (on_chunk && !on_chunk(std::string(buf, len))) {
|
|
subprocess_terminate(&proc);
|
|
break;
|
|
}
|
|
} else {
|
|
size_t remaining = max_output - output.size();
|
|
output.append(buf, remaining);
|
|
if (on_chunk && remaining > 0) on_chunk(std::string(buf, remaining));
|
|
truncated = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
done.store(true);
|
|
if (timeout_thread.joinable()) {
|
|
timeout_thread.join();
|
|
}
|
|
|
|
subprocess_join(&proc, &res.exit_code);
|
|
subprocess_destroy(&proc);
|
|
|
|
res.output = output;
|
|
res.timed_out = timed_out.load();
|
|
if (truncated) {
|
|
res.output += "\n[output truncated]";
|
|
}
|
|
return res;
|
|
}
|
|
|
|
private:
|
|
static std::vector<char *> to_cstr_vec(const std::vector<std::string> & v) {
|
|
std::vector<char *> r;
|
|
r.reserve(v.size() + 1);
|
|
for (const auto & s : v) {
|
|
r.push_back(const_cast<char *>(s.c_str()));
|
|
}
|
|
r.push_back(nullptr);
|
|
return r;
|
|
}
|
|
|
|
static const std::unordered_set<std::string> & junk_dir_names() {
|
|
static const std::unordered_set<std::string> names = {
|
|
".git", ".svn", ".hg", "node_modules", "__pycache__",
|
|
".venv", "venv", "dist", "build", "target", ".cache", ".idea", ".vscode",
|
|
};
|
|
return names;
|
|
}
|
|
|
|
std::vector<std::string> list_files_fallback(const std::string & base) const {
|
|
std::vector<std::string> result;
|
|
std::error_code ec;
|
|
|
|
std::vector<std::pair<fs::path, fs::path>> stack;
|
|
stack.emplace_back(fs::path(base), fs::path());
|
|
|
|
while (!stack.empty()) {
|
|
auto [dir, rel_dir] = stack.back();
|
|
stack.pop_back();
|
|
|
|
for (const auto & entry : fs::directory_iterator(dir, fs::directory_options::skip_permission_denied, ec)) {
|
|
if (ec) break;
|
|
std::string fname = entry.path().filename().string();
|
|
std::error_code tec;
|
|
if (entry.is_directory(tec)) {
|
|
if (junk_dir_names().count(fname) > 0) continue;
|
|
stack.emplace_back(entry.path(), rel_dir / fname);
|
|
} else if (entry.is_regular_file(tec)) {
|
|
std::string rel = (rel_dir / fname).string();
|
|
std::replace(rel.begin(), rel.end(), '\\', '/');
|
|
result.push_back(rel);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
static std::unique_ptr<tools_io> make_tools_io(const json & params) {
|
|
GGML_UNUSED(params); // TODO in follow-up PR
|
|
return std::make_unique<tools_io_basic>();
|
|
}
|
|
|
|
// no '/' in pattern -> match basename at any depth; else match full relative path
|
|
static bool path_glob_match(const std::string & pattern, const std::string & rel_path) {
|
|
if (pattern.find('/') == std::string::npos) {
|
|
return glob_match(pattern, fs::path(rel_path).filename().string());
|
|
}
|
|
if (pattern == "**" || pattern.rfind("**/", 0) == 0 || pattern.rfind('/', 0) == 0) {
|
|
return glob_match(pattern, rel_path);
|
|
}
|
|
return glob_match("**/" + pattern, rel_path);
|
|
}
|
|
|
|
//
|
|
// read_file: read a file with optional line range and line-number prefix
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_READ_FILE_MAX_SIZE = 16 * 1024; // 16 KB
|
|
|
|
struct server_tool_read_file : server_tool {
|
|
server_tool_read_file() {
|
|
name = "read_file";
|
|
display_name = "Read file";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Read the contents of a file. Optionally specify a 1-based line range. "
|
|
"If append_loc is true, each line is prefixed with its line number (e.g. \"1\u2192...\")."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Path to the file"}}},
|
|
{"start_line", {{"type", "integer"}, {"description", "First line to read, 1-based (default: 1)"}}},
|
|
{"end_line", {{"type", "integer"}, {"description", "Last line to read, 1-based inclusive (default: end of file)"}}},
|
|
{"append_loc", {{"type", "boolean"}, {"description", "Prefix each line with its line number"}}},
|
|
}},
|
|
{"required", json::array({"path"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params, server_tool::stream *) const override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
int start_line = json_value(params, "start_line", 1);
|
|
int end_line = json_value(params, "end_line", -1); // -1 = no limit
|
|
bool append_loc = json_value(params, "append_loc", false);
|
|
|
|
auto io = make_tools_io(params);
|
|
|
|
uintmax_t file_size = 0;
|
|
if (!io->file_size(path, file_size)) {
|
|
return {{"error", "cannot stat file: " + path}};
|
|
}
|
|
if (file_size > SERVER_TOOL_READ_FILE_MAX_SIZE && end_line == -1) {
|
|
return {{"error", string_format(
|
|
"file too large (%zu bytes, max %zu). Use start_line/end_line to read a portion.",
|
|
(size_t)file_size, SERVER_TOOL_READ_FILE_MAX_SIZE)}};
|
|
}
|
|
|
|
std::string content;
|
|
if (!io->read_file(path, content)) {
|
|
return {{"error", "failed to open file: " + path}};
|
|
}
|
|
|
|
std::istringstream f(content);
|
|
std::string result;
|
|
std::string line;
|
|
int lineno = 0;
|
|
|
|
while (std::getline(f, line)) {
|
|
lineno++;
|
|
if (lineno < start_line) continue;
|
|
if (end_line != -1 && lineno > end_line) break;
|
|
|
|
std::string out_line;
|
|
if (append_loc) {
|
|
out_line = std::to_string(lineno) + "\u2192" + line + "\n";
|
|
} else {
|
|
out_line = line + "\n";
|
|
}
|
|
|
|
if (result.size() + out_line.size() > SERVER_TOOL_READ_FILE_MAX_SIZE) {
|
|
result += "[output truncated]";
|
|
break;
|
|
}
|
|
result += out_line;
|
|
}
|
|
|
|
return {{"plain_text_response", result}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// file_glob_search: find files matching a glob pattern under a base directory
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_FILE_SEARCH_MAX_RESULTS = 100;
|
|
|
|
struct server_tool_file_glob_search : server_tool {
|
|
server_tool_file_glob_search() {
|
|
name = "file_glob_search";
|
|
display_name = "File search";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description",
|
|
"Recursively search for files matching a glob pattern under a directory. "
|
|
"Automatically skips files ignored by .gitignore (when the directory is inside a git repo) "
|
|
"and common junk directories (.git, node_modules, build, dist, etc.) otherwise. "
|
|
"A pattern with no '/' (e.g. \"*.cpp\") matches the file's basename at any depth. "
|
|
"A pattern containing '/' matches the full relative path; unless already anchored with "
|
|
"\"**/\" or a leading '/', it is automatically prefixed with \"**/\"."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Base directory to search in"}}},
|
|
{"include", {{"type", "string"}, {"description", "Glob pattern for files to include (e.g. \"*.cpp\" or \"src/**/*.cpp\"). Default: **"}}},
|
|
{"exclude", {{"type", "string"}, {"description", "Glob pattern for files to exclude"}}},
|
|
}},
|
|
{"required", json::array({"path"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params, server_tool::stream *) const override {
|
|
std::string base = params.at("path").get<std::string>();
|
|
std::string include = json_value(params, "include", std::string("**"));
|
|
std::string exclude = json_value(params, "exclude", std::string(""));
|
|
|
|
auto io = make_tools_io(params);
|
|
std::string err;
|
|
auto files = io->list_files(base, err);
|
|
if (!err.empty()) {
|
|
return {{"error", err}};
|
|
}
|
|
|
|
std::vector<std::string> matches;
|
|
for (const auto & rel : files) {
|
|
if (!path_glob_match(include, rel)) continue;
|
|
if (!exclude.empty() && path_glob_match(exclude, rel)) continue;
|
|
matches.push_back(rel);
|
|
}
|
|
|
|
size_t total = matches.size();
|
|
size_t shown = std::min(total, SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);
|
|
|
|
std::ostringstream output_text;
|
|
for (size_t i = 0; i < shown; i++) {
|
|
output_text << matches[i] << "\n";
|
|
}
|
|
|
|
output_text << "\n---\nTotal matches: " << total << "\n";
|
|
if (total > shown) {
|
|
output_text << string_format(
|
|
"[%zu results limit reached (%zu total matches). Refine the glob pattern to narrow the search.]\n",
|
|
shown, total);
|
|
}
|
|
|
|
return {{"plain_text_response", output_text.str()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// grep_search: search for a regex pattern in files
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_GREP_SEARCH_MAX_RESULTS = 100;
|
|
|
|
struct server_tool_grep_search : server_tool {
|
|
server_tool_grep_search() {
|
|
name = "grep_search";
|
|
display_name = "Grep search";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description",
|
|
"Search for a pattern in files under a path. Returns matching lines with file paths "
|
|
"(and, unless searching a single file, paths relative to the given directory). "
|
|
"Automatically skips files ignored by .gitignore (when the directory is inside a git repo) "
|
|
"and common junk directories (.git, node_modules, build, dist, etc.) otherwise. "
|
|
"include/exclude: a pattern with no '/' matches the basename at any depth; a pattern "
|
|
"containing '/' matches the full relative path (auto-anchored with \"**/\" unless already anchored)."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "File or directory to search in"}}},
|
|
{"pattern", {{"type", "string"}, {"description", "Pattern to search for (regular expression unless literal is true)"}}},
|
|
{"include", {{"type", "string"}, {"description", "Glob pattern to filter files (default: **)"}}},
|
|
{"exclude", {{"type", "string"}, {"description", "Glob pattern to exclude files"}}},
|
|
{"return_line_numbers", {{"type", "boolean"}, {"description", "If true, include line numbers in results"}}},
|
|
{"literal", {{"type", "boolean"}, {"description", "Treat pattern as a literal string instead of a regular expression (default: false)"}}},
|
|
{"ignore_case", {{"type", "boolean"}, {"description", "Case-insensitive search (default: false)"}}},
|
|
{"context_lines", {{"type", "integer"}, {"description", "Number of lines of context to show before and after each match (default: 0)"}}},
|
|
}},
|
|
{"required", json::array({"path", "pattern"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params, server_tool::stream *) const override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
std::string pat_str = params.at("pattern").get<std::string>();
|
|
std::string include = json_value(params, "include", std::string("**"));
|
|
std::string exclude = json_value(params, "exclude", std::string(""));
|
|
bool show_lineno = json_value(params, "return_line_numbers", false);
|
|
bool literal = json_value(params, "literal", false);
|
|
bool ignore_case = json_value(params, "ignore_case", false);
|
|
int ctx_lines = std::max(0, json_value(params, "context_lines", 0));
|
|
|
|
std::string pattern_src = pat_str;
|
|
if (literal) {
|
|
static const std::string specials = "\\^$.|?*+()[]{}";
|
|
std::string escaped;
|
|
escaped.reserve(pat_str.size() * 2);
|
|
for (char c : pat_str) {
|
|
if (specials.find(c) != std::string::npos) escaped += '\\';
|
|
escaped += c;
|
|
}
|
|
pattern_src = escaped;
|
|
}
|
|
|
|
std::regex pattern;
|
|
try {
|
|
auto flags = std::regex::ECMAScript;
|
|
if (ignore_case) flags |= std::regex::icase;
|
|
pattern = std::regex(pattern_src, flags);
|
|
} catch (const std::regex_error & e) {
|
|
return {{"error", std::string("invalid regex: ") + e.what()}};
|
|
}
|
|
|
|
auto io = make_tools_io(params);
|
|
|
|
// collect (absolute_path, display_path) pairs to search
|
|
std::vector<std::pair<std::string, std::string>> files;
|
|
|
|
if (io->is_regular_file(path)) {
|
|
files.emplace_back(path, path);
|
|
} else if (io->is_directory(path)) {
|
|
std::string err;
|
|
auto candidates = io->list_files(path, err);
|
|
if (!err.empty()) {
|
|
return {{"error", err}};
|
|
}
|
|
for (const auto & rel : candidates) {
|
|
if (!path_glob_match(include, rel)) continue;
|
|
if (!exclude.empty() && path_glob_match(exclude, rel)) continue;
|
|
files.emplace_back((fs::path(path) / rel).string(), rel);
|
|
}
|
|
} else {
|
|
return {{"error", "path does not exist: " + path}};
|
|
}
|
|
|
|
std::ostringstream output_text;
|
|
size_t total = 0;
|
|
bool limit_reached = false;
|
|
bool show_num = show_lineno || ctx_lines > 0;
|
|
|
|
for (const auto & file_entry : files) {
|
|
if (limit_reached) break;
|
|
const std::string & fpath = file_entry.first;
|
|
const std::string & display_path = file_entry.second;
|
|
|
|
std::string content;
|
|
if (!io->read_file(fpath, content)) continue;
|
|
std::vector<std::string> lines;
|
|
{
|
|
std::istringstream f(content);
|
|
std::string line;
|
|
while (std::getline(f, line)) lines.push_back(line);
|
|
}
|
|
|
|
for (size_t i = 0; i < lines.size(); i++) {
|
|
if (total >= SERVER_TOOL_GREP_SEARCH_MAX_RESULTS) {
|
|
limit_reached = true;
|
|
break;
|
|
}
|
|
if (!std::regex_search(lines[i], pattern)) continue;
|
|
|
|
long ctx_start = ctx_lines > 0 ? std::max<long>(0, (long) i - ctx_lines) : (long) i;
|
|
long ctx_end = ctx_lines > 0 ? std::min<long>((long) lines.size() - 1, (long) i + ctx_lines) : (long) i;
|
|
|
|
for (long j = ctx_start; j <= ctx_end; j++) {
|
|
bool is_match = (j == (long) i);
|
|
output_text << display_path << (is_match ? ':' : '-');
|
|
if (show_num) {
|
|
output_text << (j + 1) << (is_match ? ':' : '-');
|
|
}
|
|
output_text << lines[j] << "\n";
|
|
}
|
|
if (ctx_lines > 0) {
|
|
output_text << "--\n";
|
|
}
|
|
total++;
|
|
}
|
|
}
|
|
|
|
output_text << "\n---\nTotal matches: " << total << "\n";
|
|
if (limit_reached) {
|
|
output_text << string_format(
|
|
"[%zu matches limit reached. Narrow the path/pattern/include to see more.]\n",
|
|
SERVER_TOOL_GREP_SEARCH_MAX_RESULTS);
|
|
}
|
|
|
|
return {{"plain_text_response", output_text.str()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// exec_shell_command: run an arbitrary shell command
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE = 16 * 1024; // 16 KB
|
|
static constexpr int SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT = 60; // seconds
|
|
|
|
struct server_tool_exec_shell_command : server_tool {
|
|
server_tool_exec_shell_command() {
|
|
name = "exec_shell_command";
|
|
display_name = "Execute shell command";
|
|
permission_write = true;
|
|
support_stream = true;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Execute a shell command and return its output (stdout and stderr combined)."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"command", {{"type", "string"}, {"description", "Shell command to execute"}}},
|
|
{"timeout", {{"type", "integer"}, {"description", string_format("Timeout in seconds (default 10, max %d)", SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT)}}},
|
|
{"max_output_size", {{"type", "integer"}, {"description", string_format("Maximum output size in bytes (default %zu)", SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE)}}},
|
|
}},
|
|
{"required", json::array({"command"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params, server_tool::stream * st) const override {
|
|
std::string command = params.at("command").get<std::string>();
|
|
int timeout = json_value(params, "timeout", 10);
|
|
size_t max_output = (size_t) json_value(params, "max_output_size", (int) SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE);
|
|
|
|
timeout = std::min(timeout, SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT);
|
|
max_output = std::min(max_output, SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE);
|
|
|
|
#ifdef _WIN32
|
|
std::vector<std::string> args = {"cmd", "/c", command};
|
|
#else
|
|
std::vector<std::string> args = {"sh", "-c", command};
|
|
#endif
|
|
|
|
auto io = make_tools_io(params);
|
|
|
|
if (st) {
|
|
auto res = io->run(args, max_output, timeout, [st](const std::string & chunk) {
|
|
st->push(chunk);
|
|
return !st->alive || st->alive();
|
|
});
|
|
if (st->alive && !st->alive()) {
|
|
return json();
|
|
}
|
|
std::string tail = string_format("\n[exit code: %d]", res.exit_code);
|
|
if (res.timed_out) {
|
|
tail += " [exit due to timed out]";
|
|
}
|
|
st->push(tail);
|
|
return json();
|
|
}
|
|
|
|
auto res = io->run(args, max_output, timeout);
|
|
|
|
std::string text_output = res.output;
|
|
text_output += string_format("\n[exit code: %d]", res.exit_code);
|
|
if (res.timed_out) {
|
|
text_output += " [exit due to timed out]";
|
|
}
|
|
|
|
return {{"plain_text_response", text_output}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// write_file: create or overwrite a file
|
|
//
|
|
|
|
struct server_tool_write_file : server_tool {
|
|
server_tool_write_file() {
|
|
name = "write_file";
|
|
display_name = "Write file";
|
|
permission_write = true;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"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."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Path of the file to write"}}},
|
|
{"content", {{"type", "string"}, {"description", "Content to write"}}},
|
|
}},
|
|
{"required", json::array({"path", "content"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params, server_tool::stream *) const override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
std::string content = params.at("content").get<std::string>();
|
|
|
|
auto io = make_tools_io(params);
|
|
if (!io->write_file(path, content)) {
|
|
return {{"error", "failed to write file: " + path}};
|
|
}
|
|
|
|
return {{"result", "file written successfully"}, {"path", path}, {"bytes", content.size()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// edit_file: exact text replacement, one or more edits per call
|
|
//
|
|
|
|
struct server_tool_edit_file : server_tool {
|
|
server_tool_edit_file() {
|
|
name = "edit_file";
|
|
display_name = "Edit file";
|
|
permission_write = true;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description",
|
|
"Edit a file using exact text replacement. Each edits[].old_text must be unique in the file "
|
|
"and is matched against the original content, not incrementally. Merge nearby changes into "
|
|
"one edit instead of overlapping edits. Use write_file to replace the whole file."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Path to the file to edit"}}},
|
|
{"edits", {
|
|
{"type", "array"},
|
|
{"description", "One or more exact text replacements to apply"},
|
|
{"items", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"old_text", {{"type", "string"}, {"description", "Exact text to find; must be unique in the file and must not overlap with other edits"}}},
|
|
{"new_text", {{"type", "string"}, {"description", "Text to replace old_text with"}}},
|
|
}},
|
|
{"required", json::array({"old_text", "new_text"})},
|
|
}},
|
|
}},
|
|
}},
|
|
{"required", json::array({"path", "edits"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params, server_tool::stream *) const override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
const json & edits_json = params.at("edits");
|
|
|
|
if (!edits_json.is_array() || edits_json.empty()) {
|
|
return {{"error", "\"edits\" must be a non-empty array"}};
|
|
}
|
|
|
|
struct edit_req {
|
|
std::string old_text;
|
|
std::string new_text;
|
|
};
|
|
std::vector<edit_req> edits;
|
|
edits.reserve(edits_json.size());
|
|
for (const auto & e : edits_json) {
|
|
edit_req er;
|
|
er.old_text = e.at("old_text").get<std::string>();
|
|
er.new_text = e.at("new_text").get<std::string>();
|
|
if (er.old_text.empty()) {
|
|
return {{"error", string_format("edits[%zu].old_text must not be empty", edits.size())}};
|
|
}
|
|
edits.push_back(std::move(er));
|
|
}
|
|
|
|
auto io = make_tools_io(params);
|
|
std::string original_content;
|
|
if (!io->read_file(path, original_content)) {
|
|
return {{"error", "failed to open file: " + path}};
|
|
}
|
|
|
|
// does any old_text need fuzzy matching (no exact match found)?
|
|
bool any_fuzzy = false;
|
|
for (size_t i = 0; i < edits.size(); i++) {
|
|
if (original_content.find(edits[i].old_text) != std::string::npos) continue;
|
|
std::string fuzzy_content = normalize_for_fuzzy_match(original_content);
|
|
std::string fuzzy_old = normalize_for_fuzzy_match(edits[i].old_text);
|
|
if (fuzzy_content.find(fuzzy_old) == std::string::npos) {
|
|
return {{"error", string_format(
|
|
"could not find edits[%zu].old_text in %s, it must match the file's current content exactly",
|
|
i, path.c_str())}};
|
|
}
|
|
any_fuzzy = true;
|
|
}
|
|
|
|
std::string base_content = any_fuzzy ? normalize_for_fuzzy_match(original_content) : original_content;
|
|
|
|
// uniqueness check always uses fuzzy-normalized text, so a whitespace-only duplicate still counts
|
|
std::vector<matched_edit> matched;
|
|
matched.reserve(edits.size());
|
|
for (size_t i = 0; i < edits.size(); i++) {
|
|
std::string needle = any_fuzzy ? normalize_for_fuzzy_match(edits[i].old_text) : edits[i].old_text;
|
|
size_t occurrences = count_occurrences(
|
|
normalize_for_fuzzy_match(original_content),
|
|
normalize_for_fuzzy_match(edits[i].old_text));
|
|
if (occurrences > 1) {
|
|
return {{"error", string_format(
|
|
"found %zu occurrences of edits[%zu].old_text in %s, it must be unique",
|
|
occurrences, i, path.c_str())}};
|
|
}
|
|
size_t idx = base_content.find(needle);
|
|
matched.push_back({i, idx, needle.size(), edits[i].new_text});
|
|
}
|
|
|
|
std::sort(matched.begin(), matched.end(), [](const matched_edit & a, const matched_edit & b) {
|
|
return a.match_index < b.match_index;
|
|
});
|
|
for (size_t i = 1; i < matched.size(); i++) {
|
|
if (matched[i - 1].match_index + matched[i - 1].match_length > matched[i].match_index) {
|
|
return {{"error", string_format(
|
|
"edits[%zu] and edits[%zu] overlap in %s; merge them into one edit or target disjoint regions",
|
|
matched[i - 1].edit_index, matched[i].edit_index, path.c_str())}};
|
|
}
|
|
}
|
|
|
|
std::string new_content = any_fuzzy
|
|
? apply_replacements_preserving_unchanged_lines(original_content, base_content, matched)
|
|
: apply_replacements(base_content, matched, 0);
|
|
|
|
if (new_content == original_content) {
|
|
return {{"error", "no changes made: the replacement(s) produced identical content"}};
|
|
}
|
|
|
|
if (!io->write_file(path, new_content)) {
|
|
return {{"error", "failed to write file: " + path}};
|
|
}
|
|
|
|
return {{"result", "file edited successfully"}, {"path", path}, {"edits_applied", (int) matched.size()}};
|
|
}
|
|
|
|
private:
|
|
// strip trailing whitespace, normalize smart quotes/dashes/spaces to ASCII
|
|
static std::string normalize_line_for_fuzzy_match(const std::string & line) {
|
|
size_t end = line.size();
|
|
while (end > 0 && (line[end - 1] == ' ' || line[end - 1] == '\t' || line[end - 1] == '\r')) {
|
|
end--;
|
|
}
|
|
std::string s = line.substr(0, end);
|
|
|
|
auto replace_all = [](std::string & str, const std::string & from, const std::string & to) {
|
|
if (from.empty()) return;
|
|
size_t pos = 0;
|
|
while ((pos = str.find(from, pos)) != std::string::npos) {
|
|
str.replace(pos, from.size(), to);
|
|
pos += to.size();
|
|
}
|
|
};
|
|
|
|
// smart single quotes -> '
|
|
for (unsigned char b : {0x98, 0x99, 0x9A, 0x9B}) {
|
|
replace_all(s, std::string("\xE2\x80") + (char) b, "'");
|
|
}
|
|
// smart double quotes -> "
|
|
for (unsigned char b : {0x9C, 0x9D, 0x9E, 0x9F}) {
|
|
replace_all(s, std::string("\xE2\x80") + (char) b, "\"");
|
|
}
|
|
// various dashes -> -
|
|
for (unsigned char b = 0x90; b <= 0x95; b++) {
|
|
replace_all(s, std::string("\xE2\x80") + (char) b, "-");
|
|
}
|
|
replace_all(s, "\xE2\x88\x92", "-"); // minus sign
|
|
// special spaces -> ' '
|
|
replace_all(s, "\xC2\xA0", " "); // no-break space
|
|
for (unsigned char b = 0x82; b <= 0x8A; b++) {
|
|
replace_all(s, std::string("\xE2\x80") + (char) b, " ");
|
|
}
|
|
replace_all(s, "\xE2\x80\xAF", " "); // narrow no-break space
|
|
replace_all(s, "\xE2\x81\x9F", " "); // medium mathematical space
|
|
replace_all(s, "\xE3\x80\x80", " "); // ideographic space
|
|
|
|
return s;
|
|
}
|
|
|
|
// applies the per-line transform above to every line; preserves line count/positions
|
|
static std::string normalize_for_fuzzy_match(const std::string & content) {
|
|
std::string result;
|
|
result.reserve(content.size());
|
|
size_t start = 0;
|
|
while (true) {
|
|
size_t nl = content.find('\n', start);
|
|
bool is_last = nl == std::string::npos;
|
|
std::string line = is_last ? content.substr(start) : content.substr(start, nl - start);
|
|
result += normalize_line_for_fuzzy_match(line);
|
|
if (is_last) break;
|
|
result += '\n';
|
|
start = nl + 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// lines with trailing '\n' kept, so untouched ones can be reconstructed verbatim
|
|
static std::vector<std::string> split_lines_with_endings(const std::string & content) {
|
|
std::vector<std::string> lines;
|
|
size_t start = 0;
|
|
while (start < content.size()) {
|
|
size_t nl = content.find('\n', start);
|
|
if (nl == std::string::npos) {
|
|
lines.push_back(content.substr(start));
|
|
break;
|
|
}
|
|
lines.push_back(content.substr(start, nl - start + 1));
|
|
start = nl + 1;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
struct line_span {
|
|
size_t start;
|
|
size_t end;
|
|
};
|
|
|
|
static std::vector<line_span> get_line_spans(const std::string & content) {
|
|
std::vector<line_span> spans;
|
|
size_t offset = 0;
|
|
for (const auto & line : split_lines_with_endings(content)) {
|
|
spans.push_back({offset, offset + line.size()});
|
|
offset += line.size();
|
|
}
|
|
return spans;
|
|
}
|
|
|
|
// count non-overlapping occurrences of `needle` in `content`
|
|
static size_t count_occurrences(const std::string & content, const std::string & needle) {
|
|
if (needle.empty()) return 0;
|
|
size_t count = 0, pos = 0;
|
|
while ((pos = content.find(needle, pos)) != std::string::npos) {
|
|
count++;
|
|
pos += needle.size();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
struct matched_edit {
|
|
size_t edit_index;
|
|
size_t match_index; // offset into the "base content" (see below)
|
|
size_t match_length;
|
|
std::string new_text;
|
|
};
|
|
|
|
// replacements must be sorted ascending by match_index and non-overlapping
|
|
static std::string apply_replacements(
|
|
const std::string & content,
|
|
const std::vector<matched_edit> & replacements,
|
|
size_t offset) {
|
|
std::string result = content;
|
|
for (auto it = replacements.rbegin(); it != replacements.rend(); ++it) {
|
|
size_t local_index = it->match_index - offset;
|
|
result = result.substr(0, local_index) + it->new_text + result.substr(local_index + it->match_length);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// widen a replacement's byte range to the line(s) of `lines` it touches
|
|
static bool get_replacement_line_range(
|
|
const std::vector<line_span> & lines,
|
|
size_t match_index, size_t match_length,
|
|
size_t & out_start_line, size_t & out_end_line /* exclusive */) {
|
|
size_t replacement_start = match_index;
|
|
size_t replacement_end = match_index + match_length;
|
|
|
|
size_t start_line = (size_t) -1;
|
|
for (size_t i = 0; i < lines.size(); i++) {
|
|
if (replacement_start >= lines[i].start && replacement_start < lines[i].end) {
|
|
start_line = i;
|
|
break;
|
|
}
|
|
}
|
|
if (start_line == (size_t) -1) return false;
|
|
|
|
size_t end_line = start_line;
|
|
while (end_line < lines.size() && lines[end_line].end < replacement_end) {
|
|
end_line++;
|
|
}
|
|
if (end_line >= lines.size()) return false;
|
|
|
|
out_start_line = start_line;
|
|
out_end_line = end_line + 1;
|
|
return true;
|
|
}
|
|
|
|
// like apply_replacements, but untouched lines come from `original_content`
|
|
static std::string apply_replacements_preserving_unchanged_lines(
|
|
const std::string & original_content,
|
|
const std::string & base_content,
|
|
const std::vector<matched_edit> & replacements /* ascending, non-overlapping */) {
|
|
auto original_lines = split_lines_with_endings(original_content);
|
|
auto base_lines = get_line_spans(base_content);
|
|
|
|
struct group {
|
|
size_t start_line;
|
|
size_t end_line; // exclusive
|
|
std::vector<matched_edit> reps;
|
|
};
|
|
std::vector<group> groups;
|
|
|
|
for (const auto & rep : replacements) {
|
|
size_t start_line = 0, end_line = 0;
|
|
get_replacement_line_range(base_lines, rep.match_index, rep.match_length, start_line, end_line);
|
|
if (!groups.empty() && start_line < groups.back().end_line) {
|
|
groups.back().end_line = std::max(groups.back().end_line, end_line);
|
|
groups.back().reps.push_back(rep);
|
|
} else {
|
|
groups.push_back({start_line, end_line, {rep}});
|
|
}
|
|
}
|
|
|
|
size_t original_line_index = 0;
|
|
std::string result;
|
|
for (auto & g : groups) {
|
|
for (size_t i = original_line_index; i < g.start_line; i++) {
|
|
result += original_lines[i];
|
|
}
|
|
|
|
size_t group_start_offset = base_lines[g.start_line].start;
|
|
size_t group_end_offset = base_lines[g.end_line - 1].end;
|
|
std::string slice = base_content.substr(group_start_offset, group_end_offset - group_start_offset);
|
|
result += apply_replacements(slice, g.reps, group_start_offset);
|
|
|
|
original_line_index = g.end_line;
|
|
}
|
|
for (size_t i = original_line_index; i < original_lines.size(); i++) {
|
|
result += original_lines[i];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
//
|
|
// get_datetime: returns the current date and time
|
|
//
|
|
|
|
struct server_tool_get_datetime : server_tool {
|
|
server_tool_get_datetime() {
|
|
name = "get_datetime";
|
|
display_name = "Get Date & Time";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() const override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Returns the current date and time"},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json, server_tool::stream *) const override {
|
|
auto now = std::chrono::system_clock::now();
|
|
auto time = std::chrono::system_clock::to_time_t(now);
|
|
|
|
return {{"result", std::ctime(&time)}};
|
|
}
|
|
};
|
|
|
|
struct server_tool_stream_result : server_task_result {
|
|
std::string chunk;
|
|
bool done = false;
|
|
std::string error_msg;
|
|
|
|
json to_json() override {
|
|
if (!done) {
|
|
return {{"chunk", chunk}};
|
|
} else {
|
|
json result = {{"done", true}};
|
|
if (!error_msg.empty()) {
|
|
result["error"] = error_msg;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
};
|
|
|
|
void server_tool::stream::push(const std::string & chunk) {
|
|
if (chunk.empty()) return;
|
|
auto r = std::make_unique<server_tool_stream_result>();
|
|
r->id = id;
|
|
r->chunk = chunk;
|
|
qr.send(std::move(r));
|
|
}
|
|
|
|
struct server_tools_res : server_http_res {
|
|
std::thread worker;
|
|
server_response * qr = nullptr; // set only for streaming responses
|
|
int id = -1;
|
|
|
|
~server_tools_res() override {
|
|
if (worker.joinable()) {
|
|
worker.join();
|
|
}
|
|
if (qr) {
|
|
qr->remove_waiting_task_id(id);
|
|
}
|
|
}
|
|
};
|
|
|
|
static server_tool & find_tool(std::vector<std::unique_ptr<server_tool>> & tools, const std::string & name, bool require_stream) {
|
|
for (auto & t : tools) {
|
|
if (t->name == name) {
|
|
if (require_stream && !t->support_stream) {
|
|
throw std::invalid_argument(string_format("tool \"%s\" does not support stream = true", name.c_str()));
|
|
}
|
|
return *t;
|
|
}
|
|
}
|
|
throw std::invalid_argument(string_format("unknown tool \"%s\"", name.c_str()));
|
|
}
|
|
|
|
//
|
|
// public API
|
|
//
|
|
|
|
static std::vector<std::unique_ptr<server_tool>> build_tools() {
|
|
std::vector<std::unique_ptr<server_tool>> tools;
|
|
tools.push_back(std::make_unique<server_tool_read_file>());
|
|
tools.push_back(std::make_unique<server_tool_file_glob_search>());
|
|
tools.push_back(std::make_unique<server_tool_grep_search>());
|
|
tools.push_back(std::make_unique<server_tool_exec_shell_command>());
|
|
tools.push_back(std::make_unique<server_tool_write_file>());
|
|
tools.push_back(std::make_unique<server_tool_edit_file>());
|
|
tools.push_back(std::make_unique<server_tool_get_datetime>());
|
|
return tools;
|
|
}
|
|
|
|
void server_tools::setup(const std::vector<std::string> & enabled_tools) {
|
|
if (!enabled_tools.empty()) {
|
|
std::unordered_set<std::string> enabled_set(enabled_tools.begin(), enabled_tools.end());
|
|
auto all_tools = build_tools();
|
|
|
|
// collect all known tool names for validation
|
|
std::vector<std::string> known_names;
|
|
known_names.reserve(all_tools.size());
|
|
for (const auto & t : all_tools) {
|
|
known_names.push_back(t->name);
|
|
}
|
|
|
|
// validate that every requested tool is known
|
|
for (const auto & name : enabled_tools) {
|
|
if (name == "all") continue;
|
|
if (std::find(known_names.begin(), known_names.end(), name) == known_names.end()) {
|
|
throw std::runtime_error(string_format(
|
|
"unknown tool \"%s\". available tools: %s",
|
|
name.c_str(),
|
|
string_join(known_names, ", ").c_str()));
|
|
}
|
|
}
|
|
|
|
tools.clear();
|
|
for (auto & t : all_tools) {
|
|
if (enabled_set.count(t->name) > 0 || enabled_set.count("all") > 0) {
|
|
tools.push_back(std::move(t));
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_get = [this](const server_http_req &) -> server_http_res_ptr {
|
|
auto res = std::make_unique<server_http_res>();
|
|
try {
|
|
json result = json::array();
|
|
for (const auto & t : tools) {
|
|
result.push_back(t->to_json());
|
|
}
|
|
res->data = safe_json_to_str(result);
|
|
} catch (const std::exception & e) {
|
|
SRV_ERR("got exception: %s\n", e.what());
|
|
res->status = 500;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_SERVER));
|
|
}
|
|
return res;
|
|
};
|
|
|
|
handle_post = [this](const server_http_req & req) -> server_http_res_ptr {
|
|
auto res = std::make_unique<server_tools_res>();
|
|
try {
|
|
json body = json::parse(req.body);
|
|
std::string tool_name = body.at("tool").get<std::string>();
|
|
json params = body.value("params", json::object());
|
|
bool stream = body.value("stream", false);
|
|
|
|
server_tool & tool = find_tool(tools, tool_name, stream);
|
|
|
|
if (stream) {
|
|
int id = res_id.fetch_add(1);
|
|
queue_res.add_waiting_task_id(id);
|
|
res->qr = &queue_res;
|
|
res->id = id;
|
|
|
|
res->worker = std::thread([this, id, &req, &tool, params]() mutable {
|
|
server_tool::stream st{queue_res, id, [&req]() {
|
|
return !req.should_stop();
|
|
}};
|
|
|
|
auto done = std::make_unique<server_tool_stream_result>();
|
|
try {
|
|
tool.invoke(params, &st);
|
|
} catch (const std::exception & e) {
|
|
done->error_msg = e.what();
|
|
} catch (...) {
|
|
done->error_msg = "An unknown error occurred";
|
|
}
|
|
done->id = st.id;
|
|
done->done = true;
|
|
st.qr.send(std::move(done));
|
|
});
|
|
|
|
res->content_type = "text/event-stream";
|
|
res->status = 200;
|
|
res->next = [this, id](std::string & output) -> bool {
|
|
auto result = queue_res.recv(id);
|
|
auto * r = dynamic_cast<server_tool_stream_result *>(result.get());
|
|
GGML_ASSERT(r != nullptr);
|
|
output = "data: " + safe_json_to_str(r->to_json()) + "\n\n";
|
|
if (r->done) {
|
|
queue_res.remove_waiting_task_id(id);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
} else {
|
|
json result = tool.invoke(params, nullptr);
|
|
res->status = 200;
|
|
res->data = safe_json_to_str(result);
|
|
}
|
|
} catch (const json::exception & e) {
|
|
res->status = 400;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST));
|
|
} catch (const std::invalid_argument & e) {
|
|
res->status = 404;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST));
|
|
} catch (const std::exception & e) {
|
|
SRV_ERR("got exception: %s\n", e.what());
|
|
res->status = 500;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_SERVER));
|
|
}
|
|
return res;
|
|
};
|
|
}
|