From bfe24305346dbdba37c2761eb69f0a57d923aade Mon Sep 17 00:00:00 2001 From: hkr04 Date: Mon, 17 Mar 2025 01:58:37 +0800 Subject: [PATCH] a workable version (but ...) --- CMakeLists.txt | 43 ++++-- agent/base.h | 8 +- agent/planning.cpp | 3 +- agent/planning.h | 1 + agent/toolcall.cpp | 4 +- agent/toolcall.h | 2 - config.cpp | 9 -- config.h | 11 +- config/config.toml | 9 +- config/config.toml.bak | 6 + config/config_mcp.toml | 9 +- flow/planning.cpp | 5 +- llm.cpp | 6 + llm.h | 50 ++++--- logger.cpp | 42 ++++++ logger.h | 40 +----- main.cpp | 3 +- prompt.cpp | 77 +++++++++++ prompt.h | 73 +++------- schema.cpp | 12 ++ schema.h | 7 +- server/CMakeLists.txt | 10 ++ server/mcp_server_main.cpp | 6 +- tool/base.h | 263 +++++++++++++++++++------------------ tool/filesystem.h | 6 +- tool/google_search.h | 95 -------------- tool/planning.cpp | 1 + tool/planning.h | 28 ++-- tool/puppeteer.h | 15 +++ tool/terminate.h | 16 +-- 30 files changed, 444 insertions(+), 416 deletions(-) create mode 100644 config/config.toml.bak create mode 100644 llm.cpp create mode 100644 logger.cpp create mode 100644 prompt.cpp create mode 100644 schema.cpp delete mode 100644 tool/google_search.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9225e79..c89fe40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,8 +4,8 @@ project(humanus.cpp VERSION 0.1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# 查找OpenSSL库,尝试查找3.0.0版本,但如果找不到,也接受其他版本 -find_package(OpenSSL REQUIRED) +# 查找OpenSSL库 +find_package(OpenSSL 3.0.0 REQUIRED) if(OPENSSL_FOUND) message(STATUS "OpenSSL found: ${OPENSSL_VERSION}") message(STATUS "OpenSSL include directory: ${OPENSSL_INCLUDE_DIR}") @@ -36,7 +36,6 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/server) # 添加包含目录 include_directories(${CMAKE_CURRENT_SOURCE_DIR}) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/mcp/include) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/mcp/common) @@ -44,13 +43,33 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/mcp/common) find_package(Threads REQUIRED) # 添加源文件 -file(GLOB_RECURSE SOURCES - "src/*.cpp" - "src/*.cc" +file(GLOB AGENT_SOURCES + "agent/*.cpp" + "agent/*.cc" +) + +file(GLOB TOOL_SOURCES + "tool/*.cpp" + "tool/*.cc" +) + +file(GLOB FLOW_SOURCES + "flow/*.cpp" + "flow/*.cc" ) # 添加可执行文件 -add_executable(humanus_cpp ${SOURCES} main.cpp) +add_executable(humanus_cpp + main.cpp + config.cpp + llm.cpp + prompt.cpp + logger.cpp + schema.cpp + ${AGENT_SOURCES} + ${TOOL_SOURCES} + ${FLOW_SOURCES} +) # 链接库 target_link_libraries(humanus_cpp PRIVATE Threads::Threads mcp server ${OPENSSL_LIBRARIES}) @@ -58,5 +77,13 @@ if(Python3_FOUND) target_link_libraries(humanus_cpp PRIVATE ${Python3_LIBRARIES}) endif() +# 添加简单版本的可执行文件 +add_executable(humanus_simple main_simple.cpp logger.cpp schema.cpp) +target_link_libraries(humanus_simple PRIVATE Threads::Threads ${OPENSSL_LIBRARIES}) +if(Python3_FOUND) + target_link_libraries(humanus_simple PRIVATE ${Python3_LIBRARIES}) +endif() + # 安装目标 -install(TARGETS humanus_cpp DESTINATION bin) \ No newline at end of file +install(TARGETS humanus_cpp DESTINATION bin) +install(TARGETS humanus_simple DESTINATION bin) \ No newline at end of file diff --git a/agent/base.h b/agent/base.h index 8149360..509a3c3 100644 --- a/agent/base.h +++ b/agent/base.h @@ -59,7 +59,7 @@ struct BaseAgent : std::enable_shared_from_this { // Initialize agent with default settings if not provided. void initialize_agent() { if (!llm) { - llm = LLM::get_instance(name); + llm = LLM::get_instance("default"); } if (!memory) { memory = std::make_shared(); @@ -159,10 +159,6 @@ struct BaseAgent : std::enable_shared_from_this { if (duplicate_count >= duplicate_threshold) { break; } - } else { - break; - // Stop counting if a non-duplicate message is encountered - // Slightly differenr from OpenManus implementation } } @@ -170,7 +166,7 @@ struct BaseAgent : std::enable_shared_from_this { } void set_messages(const std::vector& messages) { - memory->set_messages(messages); + memory->add_messages(messages); } }; diff --git a/agent/planning.cpp b/agent/planning.cpp index fdfef85..c85569b 100644 --- a/agent/planning.cpp +++ b/agent/planning.cpp @@ -1,10 +1,11 @@ #include "planning.h" +#include // 添加iomanip头文件,用于std::setprecision namespace humanus { // Initialize the agent with a default plan ID and validate required tools. void PlanningAgent::initialize_plan_and_verify_tools() { - active_plan_id = "plan_" + std::chrono::system_clock::now().time_since_epoch().count(); + active_plan_id = "plan_" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); if (available_tools.tools_map.find("planning") == available_tools.tools_map.end()) { available_tools.add_tool(std::make_shared()); diff --git a/agent/planning.h b/agent/planning.h index 83d732c..10595cb 100644 --- a/agent/planning.h +++ b/agent/planning.h @@ -3,6 +3,7 @@ #include "toolcall.h" #include "../tool/planning.h" +#include "../prompt.h" namespace humanus { diff --git a/agent/toolcall.cpp b/agent/toolcall.cpp index 13eab7c..e143d0f 100644 --- a/agent/toolcall.cpp +++ b/agent/toolcall.cpp @@ -20,7 +20,7 @@ bool ToolCallAgent::think() { tool_calls = ToolCall::from_json_list(response["tool_calls"]); // Log response info - logger->info("✨ {self.name}'s thoughts:" + response["content"].dump()); + logger->info("✨ " + name + "'s thoughts:" + response["content"].dump()); logger->info( "🛠️ " + name + " selected " + std::to_string(tool_calls.size()) + " tool(s) to use" ); @@ -87,7 +87,7 @@ std::string ToolCallAgent::act() { for (const auto& tool_call : tool_calls) { auto result = execute_tool(tool_call); logger->info( - "🎯 Tool '" + tool_call.function.name + "' ompleted its mission! Result: " + result + "🎯 Tool '" + tool_call.function.name + "' completed its mission! Result: " + result ); // Add tool response to memory diff --git a/agent/toolcall.h b/agent/toolcall.h index 415fac2..a112400 100644 --- a/agent/toolcall.h +++ b/agent/toolcall.h @@ -9,8 +9,6 @@ namespace humanus { -const char* TOOL_CALL_REQUIRED = "Tool calls required but none provided"; - // Base agent class for handling tool/function calls with enhanced abstraction struct ToolCallAgent : ReActAgent { std::vector tool_calls; diff --git a/config.cpp b/config.cpp index 949c6ef..647a2af 100644 --- a/config.cpp +++ b/config.cpp @@ -10,9 +10,6 @@ namespace humanus { Config* Config::_instance = nullptr; std::mutex Config::_mutex; -// 全局配置实例 -Config& config = Config::get_instance(); - void Config::_load_initial_config() { try { auto config_path = _get_config_path(); @@ -49,20 +46,14 @@ void Config::_load_initial_config() { if (llm_table.contains("end_point") && llm_table["end_point"].is_string()) { llm_settings.end_point = llm_table["end_point"].as_string()->get(); - } else { - throw std::runtime_error("Invalid `end_point` configuration"); } if (llm_table.contains("max_tokens") && llm_table["max_tokens"].is_integer()) { llm_settings.max_tokens = llm_table["max_tokens"].as_integer()->get(); - } else { - llm_settings.max_tokens = 4096; } if (llm_table.contains("temperature") && llm_table["temperature"].is_floating_point()) { llm_settings.temperature = llm_table["temperature"].as_floating_point()->get(); - } else { - llm_settings.temperature = 1.0; } _config.llm["default"] = llm_settings; diff --git a/config.h b/config.h index b1dd75e..eee69bd 100644 --- a/config.h +++ b/config.h @@ -15,11 +15,11 @@ namespace humanus { static std::filesystem::path get_project_root() { // 获取项目根目录 - return std::filesystem::path(__FILE__).parent_path().parent_path(); + return std::filesystem::path(__FILE__).parent_path(); } -const std::filesystem::path PROJECT_ROOT = get_project_root(); -const std::filesystem::path WORKSPACE_ROOT = PROJECT_ROOT / "workspace"; +inline const std::filesystem::path PROJECT_ROOT = get_project_root(); +inline const std::filesystem::path WORKSPACE_ROOT = PROJECT_ROOT / "workspace"; /** * @brief LLM设置结构体 @@ -36,7 +36,7 @@ struct LLMSettings { const std::string& model = "", const std::string& api_key = "", const std::string& base_url = "", - const std::string& end_point = "/v1/chat/completions", + const std::string& end_point = "/chat/completions", int max_tokens = 4096, double temperature = 1.0 ) : model(model), api_key(api_key), base_url(base_url), end_point(end_point), @@ -129,9 +129,6 @@ public: } }; -// 全局配置实例 -extern Config& config; - } // namespace humanus #endif // HUMANUS_CONFIG_H \ No newline at end of file diff --git a/config/config.toml b/config/config.toml index 799a338..f110f20 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,5 +1,6 @@ [llm] -model = "anthropic/claude-3.7-sonnet" -base_url = "https://openrouter.ai/api/v1" -api_key = "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" -max_tokens = 4096 +model = "deepseek-chat" +base_url = "https://api.deepseek.com" +end_point = "/v1/chat/completions" +api_key = "sk-93c5bfcb920c4a8aa345791d429b8536" +max_tokens = 8192 \ No newline at end of file diff --git a/config/config.toml.bak b/config/config.toml.bak new file mode 100644 index 0000000..08a5e6f --- /dev/null +++ b/config/config.toml.bak @@ -0,0 +1,6 @@ +[llm] +model = "anthropic/claude-3.7-sonnet" +base_url = "https://openrouter.ai" +end_point = "/api/v1/chat/completions" +api_key = "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" +max_tokens = 8196 \ No newline at end of file diff --git a/config/config_mcp.toml b/config/config_mcp.toml index bb273dd..6232d94 100644 --- a/config/config_mcp.toml +++ b/config/config_mcp.toml @@ -5,18 +5,17 @@ port = 8818 sse_endpoint = "/sse" [puppeteer] -type = "command" +type = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-puppeteer"] [filesystem] -type = "command" +type = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", - "/Users/username/Desktop", - "/path/to/other/allowed/dir"] + "/Users/hyde/Desktop"] [shell] -type = "command" +type = "stdio" command = "uvx mcp-shell-server" \ No newline at end of file diff --git a/flow/planning.cpp b/flow/planning.cpp index 0eacd77..1a18051 100644 --- a/flow/planning.cpp +++ b/flow/planning.cpp @@ -4,7 +4,7 @@ namespace humanus { // Get an appropriate executor agent for the current step. // Can be extended to select agents based on step type/requirements. -std::shared_ptr PlanningFlow::get_executor(const std::string& step_type = "") const { +std::shared_ptr PlanningFlow::get_executor(const std::string& step_type) const { // If step type is provided and matches an agent key, use that agent if (!step_type.empty() && agents.find(step_type) != agents.end()) { return agents.at(step_type); @@ -101,7 +101,8 @@ void PlanningFlow::_create_initial_plan(const std::string& request) { auto args = tool_call.function.arguments; if (args.is_string()) { try { - args = json::parse(args); + std::string args_str = args.get(); + args = json::parse(args_str); } catch (...) { logger->error("Failed to parse tool arguments: " + args.dump()); continue; diff --git a/llm.cpp b/llm.cpp new file mode 100644 index 0000000..df423af --- /dev/null +++ b/llm.cpp @@ -0,0 +1,6 @@ +#include "llm.h" + +namespace humanus { + // 定义静态成员变量 + std::map> LLM::_instances; +} \ No newline at end of file diff --git a/llm.h b/llm.h index a6bd4ae..a387b74 100644 --- a/llm.h +++ b/llm.h @@ -21,19 +21,25 @@ private: std::unique_ptr client_; - LLMSettings llm_config_; + std::shared_ptr llm_config_; - // 私有构造函数,防止直接创建实例 - LLM(const std::string& config_name, const LLMSettings llm_config) : llm_config_(llm_config) { - client_ = std::make_unique(llm_config.base_url); +public: + // 构造函数 + LLM(const std::string& config_name, const std::shared_ptr& llm_config = nullptr) : llm_config_(llm_config) { + if (!llm_config_) { + if (Config::get_instance().llm().find(config_name) == Config::get_instance().llm().end()) { + throw std::invalid_argument("Config not found: " + config_name); + } + llm_config_ = std::make_shared(Config::get_instance().llm().at(config_name)); + } + client_ = std::make_unique(llm_config_->base_url); client_->set_default_headers({ - {"Authorization", "Bearer " + llm_config_.api_key} + {"Authorization", "Bearer " + llm_config_->api_key} }); } - -public: + // 单例模式获取实例 - static std::shared_ptr get_instance(const std::string& config_name = "default", const LLMSettings llm_config = LLMSettings()) { + static std::shared_ptr get_instance(const std::string& config_name = "default", const std::shared_ptr& llm_config = nullptr) { if (_instances.find(config_name) == _instances.end()) { _instances[config_name] = std::make_shared(config_name, llm_config); } @@ -56,7 +62,7 @@ public: for (const auto& message : formatted_messages) { if (message["role"] != "user" && message["role"] != "assistant" && message["role"] != "system" && message["role"] != "tool") { - throw std::invalid_argument("Invalid role: " + message["role"]); + throw std::invalid_argument("Invalid role: " + message["role"].get()); } if (message["content"].empty() && message["tool_calls"].empty()) { throw std::invalid_argument("Message must contain either 'content' or 'tool_calls'"); @@ -85,7 +91,7 @@ public: for (const auto& message : formatted_messages) { if (message["role"] != "user" && message["role"] != "assistant" && message["role"] != "system" && message["role"] != "tool") { - throw std::invalid_argument("Invalid role: " + message["role"]); + throw std::invalid_argument("Invalid role: " + message["role"].get()); } if (message["content"].empty() && message["tool_calls"].empty()) { throw std::invalid_argument("Message must contain either 'content' or 'tool_calls'"); @@ -120,10 +126,10 @@ public: formatted_messages.insert(formatted_messages.end(), _formatted_messages.begin(), _formatted_messages.end()); json body = { - {"model", llm_config_.model}, + {"model", llm_config_->model}, {"messages", formatted_messages}, - {"temperature", llm_config_.temperature}, - {"max_tokens", llm_config_.max_tokens} + {"temperature", llm_config_->temperature}, + {"max_tokens", llm_config_->max_tokens} }; std::string body_str = body.dump(); @@ -132,7 +138,7 @@ public: while (retry <= max_retries) { // send request - auto res = client_->Post(llm_config_.end_point, body_str, "application/json"); + auto res = client_->Post(llm_config_->end_point, body_str, "application/json"); if (!res) { logger->error("Failed to send request: " + httplib::to_string(res.error())); @@ -149,6 +155,10 @@ public: retry++; + if (retry > max_retries) { + break; + } + // wait for a while before retrying std::this_thread::sleep_for(std::chrono::milliseconds(500)); @@ -201,10 +211,10 @@ public: } json body = { - {"model", llm_config_.model}, + {"model", llm_config_->model}, {"messages", formatted_messages}, - {"temperature", llm_config_.temperature}, - {"max_tokens", llm_config_.max_tokens}, + {"temperature", llm_config_->temperature}, + {"max_tokens", llm_config_->max_tokens}, {"tools", tools}, {"tool_choice", tool_choice} }; @@ -217,7 +227,7 @@ public: while (retry <= max_retries) { // send request - auto res = client_->Post(llm_config_.end_point, body_str, "application/json"); + auto res = client_->Post(llm_config_->end_point, body_str, "application/json"); if (!res) { logger->error("Failed to send request: " + httplib::to_string(res.error())); @@ -234,6 +244,10 @@ public: retry++; + if (retry > max_retries) { + break; + } + // wait for a while before retrying std::this_thread::sleep_for(std::chrono::milliseconds(500)); diff --git a/logger.cpp b/logger.cpp new file mode 100644 index 0000000..1e533a0 --- /dev/null +++ b/logger.cpp @@ -0,0 +1,42 @@ +#include "logger.h" +#include +#include + +namespace humanus { + +std::shared_ptr define_log_level(spdlog::level::level_enum print_level, + spdlog::level::level_enum logfile_level, + std::string name) { + _print_level = print_level; + + auto current_date = std::chrono::system_clock::now(); + auto in_time_t = std::chrono::system_clock::to_time_t(current_date); + + std::stringstream ss; + std::tm tm_info = *std::localtime(&in_time_t); + ss << std::put_time(&tm_info, "%Y%m%d"); + std::string formatted_date = ss.str(); // YYYYMMDD + + std::string log_name = name.empty() ? formatted_date : name + "_" + formatted_date; + std::string log_file_path = (PROJECT_ROOT / "logs" / (log_name + ".log")).string(); + + // 确保日志目录存在 + std::filesystem::create_directories(PROJECT_ROOT / "logs"); + + // 重置日志输出 + std::shared_ptr _logger = std::make_shared(log_name); + + // 添加标准错误输出sink,相当于Python中的sys.stderr + auto stderr_sink = std::make_shared(); + stderr_sink->set_level(print_level); + _logger->sinks().push_back(stderr_sink); + + // 添加文件sink,相当于Python中的PROJECT_ROOT / f"logs/{log_name}.log" + auto file_sink = std::make_shared(log_file_path, false); + file_sink->set_level(logfile_level); + _logger->sinks().push_back(file_sink); + + return _logger; +} + +} // namespace humanus \ No newline at end of file diff --git a/logger.h b/logger.h index 7816882..1b2a810 100644 --- a/logger.h +++ b/logger.h @@ -9,9 +9,14 @@ #include "spdlog/sinks/dist_sink.h" #include +#include +#include "config.h" namespace humanus { +// 使用config.h中定义的PROJECT_ROOT +// static const std::filesystem::path PROJECT_ROOT = std::filesystem::current_path(); + static spdlog::level::level_enum _print_level = spdlog::level::info; /** @@ -21,40 +26,9 @@ static spdlog::level::level_enum _print_level = spdlog::level::info; * @param name 日志文件名前缀 * @return 日志记录器实例 */ -std::shared_ptr define_log_level(spdlog::level::level_enum print_level = spdlog::level::info, +extern std::shared_ptr define_log_level(spdlog::level::level_enum print_level = spdlog::level::info, spdlog::level::level_enum logfile_level = spdlog::level::debug, - std::string name = "") { - _print_level = print_level; - - auto current_date = std::chrono::system_clock::now(); - auto in_time_t = std::chrono::system_clock::to_time_t(current_date); - - std::stringstream ss; - std::tm tm_info = *std::localtime(&in_time_t); - ss << std::put_time(&tm_info, "%Y%m%d"); - std::string formatted_date = ss.str(); // YYYYMMDD - - std::string log_name = name.empty() ? formatted_date : name + "_" + formatted_date; - std::string log_file_path = (PROJECT_ROOT / "logs" / (log_name + ".log")).string(); - - // 确保日志目录存在 - std::filesystem::create_directories((PROJECT_ROOT / "logs").string()); - - // 重置日志输出 - std::shared_ptr _logger = spdlog::default_logger(); - - // 添加标准错误输出sink,相当于Python中的sys.stderr - auto stderr_sink = std::make_shared(); - stderr_sink->set_level(print_level); - _logger->sinks().push_back(stderr_sink); - - // 添加文件sink,相当于Python中的PROJECT_ROOT / f"logs/{log_name}.log" - auto file_sink = std::make_shared(log_file_path, true); - file_sink->set_level(logfile_level); - _logger->sinks().push_back(file_sink); - - return _logger; -} + std::string name = ""); static std::shared_ptr logger = define_log_level(); diff --git a/main.cpp b/main.cpp index 8dc05d7..77dcf3a 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,6 @@ #include "agent/manus.h" #include "logger.h" +#include "prompt.h" #if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) #include @@ -18,7 +19,7 @@ using namespace humanus; #if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) || defined (_WIN32) static void sigint_handler(int signo) { if (signo == SIGINT) { - logger->warn("Goodbye!"); + logger->info("Received SIGINT, exiting..."); exit(0); } } diff --git a/prompt.cpp b/prompt.cpp new file mode 100644 index 0000000..c3f3a0b --- /dev/null +++ b/prompt.cpp @@ -0,0 +1,77 @@ +#include "prompt.h" + +namespace humanus { + +namespace prompt { + +namespace manus { +const char* SYSTEM_PROMPT = "\ +You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, or web browsing, you can handle it all."; + +const char* NEXT_STEP_PROMPT = R"(You can interact with the computer using PythonExecute, save important content and information files through FileSaver, open browsers and retrieve information with Puppeteer. + +PythonExecute: Execute Python code to interact with the computer system, data processing, automation tasks, etc. + +FileSystem: Read/write files locally, such as txt, py, html, etc. Create/list/delete directories, move files/directories, search for files and get file metadata. + +Puppeteer: Open, browse, and get screenshots of web pages using Puppeteer, a headless Chrome browser. + +Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps.)"; +} // namespace manus + +namespace planning { +const char* PLANNING_SYSTEM_PROMPT = R"(Based on the current state, what's your next step? +Consider: +1. Do you need to create or refine a plan? +2. Are you ready to execute a specific step? +3. Have you completed the task? + +Provide reasoning, then select the appropriate tool or action.)"; + +const char* NEXT_STEP_PROMPT = R"(Based on the current state, what's your next step? +Consider: +1. Do you need to create or refine a plan? +2. Are you ready to execute a specific step? +3. Have you completed the task? + +Provide reasoning, then select the appropriate tool or action.)"; +} // namespace planning + +namespace swe { +const char* SYSTEM_PROMPT = R"(SETTING: You are an autonomous programmer, and you're working directly in the command line with a special interface. + +The special interface consists of a file editor that shows you {WINDOW} lines of a file at a time. +In addition to typical shell commands, you can also use specific commands to help you navigate and edit files. +To call a command, you need to invoke it with a function call/tool call. + +Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. +If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run. + +RESPONSE FORMAT: +Your shell prompt is formatted as follows: +(Open file: ) +(Current directory: ) +shell-$ + +First, you should _always_ include a general thought about what you're going to do next. +Then, for every response, you must include exactly _ONE_ tool call/function call. + +Remember, you should always include a _SINGLE_ tool call/function call and then wait for a response from the shell before continuing with more discussion and commands. Everything you include in the DISCUSSION section will be saved for future reference. +If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first tool call, and then after receiving a response you'll be able to issue the second tool call. +Note that the environment does NOT support interactive session commands (e.g. python, vim), so please do not invoke them.)"; + +const char* NEXT_STEP_TEMPLATE = R"({observation} +(Open file: {open_file}) +(Current directory: {working_dir}) +shell-$)"; +} // namespace swe + +namespace toolcall { +const char* SYSTEM_PROMPT = "You are an agent that can execute tool calls"; + +const char* NEXT_STEP_PROMPT = "If you want to stop interaction, use `terminate` tool/function call."; +} // namespace toolcall + +} // namespace prompt + +} // namespace humanus \ No newline at end of file diff --git a/prompt.h b/prompt.h index e48058a..da2bc3a 100644 --- a/prompt.h +++ b/prompt.h @@ -6,75 +6,36 @@ namespace humanus { namespace prompt { namespace manus { -const char* SYSTEM_PROMPT = "\ -You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, or web browsing, you can handle it all."; - -const char* NEXT_STEP_PROMPT = R"(You can interact with the computer using PythonExecute, save important content and information files through FileSaver, open browsers with BrowserUseTool, and retrieve information using GoogleSearch. - -PythonExecute: Execute Python code to interact with the computer system, data processing, automation tasks, etc. - -FileSystem: Read/write files locally, such as txt, py, html, etc. Create/list/delete directories, move files/directories, search for files and get file metadata. - -Puppeteer: Open, browse, and get screenshots of web pages using Puppeteer, a headless Chrome browser. - -Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps.)"; +extern const char* SYSTEM_PROMPT; +extern const char* NEXT_STEP_PROMPT; } // namespace manus namespace planning { -const char* PLANNING_SYSTEM_PROMPT = R"(Based on the current state, what's your next step? -Consider: -1. Do you need to create or refine a plan? -2. Are you ready to execute a specific step? -3. Have you completed the task? - -Provide reasoning, then select the appropriate tool or action.)"; - -const char* NEXT_STEP_PROMPT = R"(Based on the current state, what's your next step? -Consider: -1. Do you need to create or refine a plan? -2. Are you ready to execute a specific step? -3. Have you completed the task? - -Provide reasoning, then select the appropriate tool or action.)"; +extern const char* PLANNING_SYSTEM_PROMPT; +extern const char* NEXT_STEP_PROMPT; } // namespace planning namespace swe { -const char* SYSTEM_PROMPT = R"(SETTING: You are an autonomous programmer, and you're working directly in the command line with a special interface. - -The special interface consists of a file editor that shows you {WINDOW} lines of a file at a time. -In addition to typical shell commands, you can also use specific commands to help you navigate and edit files. -To call a command, you need to invoke it with a function call/tool call. - -Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. -If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run. - -RESPONSE FORMAT: -Your shell prompt is formatted as follows: -(Open file: ) -(Current directory: ) -shell-$ - -First, you should _always_ include a general thought about what you're going to do next. -Then, for every response, you must include exactly _ONE_ tool call/function call. - -Remember, you should always include a _SINGLE_ tool call/function call and then wait for a response from the shell before continuing with more discussion and commands. Everything you include in the DISCUSSION section will be saved for future reference. -If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first tool call, and then after receiving a response you'll be able to issue the second tool call. -Note that the environment does NOT support interactive session commands (e.g. python, vim), so please do not invoke them.)"; - -const char* NEXT_STEP_TEMPLATE = R"({observation} -(Open file: {open_file}) -(Current directory: {working_dir}) -shell-$)"; +extern const char* SYSTEM_PROMPT; +extern const char* NEXT_STEP_TEMPLATE; } // namespace swe namespace toolcall { -const char* SYSTEM_PROMPT = "You are an agent that can execute tool calls"; - -const char* NEXT_STEP_PROMPT = "If you want to stop interaction, use `terminate` tool/function call."; +extern const char* SYSTEM_PROMPT; +extern const char* NEXT_STEP_PROMPT; } // namespace toolcall } // namespace prompt +// 使用内联函数来获取常量 +inline const char* get_tool_call_required() { return "required"; } +inline const char* get_terminate_description() { return "Terminate the current interaction"; } +inline const char* get_planning_tool_description() { return "Create a plan for the given task"; } + +#define TOOL_CALL_REQUIRED get_tool_call_required() +#define _TERMINATE_DESCRIPTION get_terminate_description() +#define _PLANNING_TOOL_DESCRIPTION get_planning_tool_description() + } // namespace humanus #endif // HUMANUS_PROMPT_H diff --git a/schema.cpp b/schema.cpp new file mode 100644 index 0000000..699c597 --- /dev/null +++ b/schema.cpp @@ -0,0 +1,12 @@ +#include "schema.h" + +namespace humanus { + +std::map agent_state_map = { + {AgentState::IDLE, "IDLE"}, + {AgentState::RUNNING, "RUNNING"}, + {AgentState::FINISHED, "FINISHED"}, + {AgentState::ERROR, "ERROR"} +}; + +} // namespace humanus \ No newline at end of file diff --git a/schema.h b/schema.h index 3e3d273..9d35b5e 100644 --- a/schema.h +++ b/schema.h @@ -15,12 +15,7 @@ enum class AgentState { ERROR = 3 }; -std::map agent_state_map = { - {AgentState::IDLE, "IDLE"}, - {AgentState::RUNNING, "RUNNING"}, - {AgentState::FINISHED, "FINISHED"}, - {AgentState::ERROR, "ERROR"} -}; +extern std::map agent_state_map; struct Function { std::string name; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 15c95ce..5983bf2 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,6 +1,9 @@ # 服务器组件CMakeLists.txt cmake_minimum_required(VERSION 3.10) +# 查找必要的包 +find_package(Threads REQUIRED) + # 添加源文件 set(SERVER_SOURCES python_execute.cpp @@ -23,11 +26,18 @@ target_include_directories(server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../ ${CMAKE_CURRENT_SOURCE_DIR}/../mcp/include + ${CMAKE_CURRENT_SOURCE_DIR}/../mcp/common ) # 添加MCP服务器可执行文件 add_executable(mcp_server mcp_server_main.cpp) target_link_libraries(mcp_server PRIVATE server mcp Threads::Threads ${OPENSSL_LIBRARIES}) +target_include_directories(mcp_server PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../ + ${CMAKE_CURRENT_SOURCE_DIR}/../mcp/include + ${CMAKE_CURRENT_SOURCE_DIR}/../mcp/common +) if(Python3_FOUND) target_link_libraries(mcp_server PRIVATE ${Python3_LIBRARIES}) endif() diff --git a/server/mcp_server_main.cpp b/server/mcp_server_main.cpp index ea1a16c..c127246 100644 --- a/server/mcp_server_main.cpp +++ b/server/mcp_server_main.cpp @@ -6,9 +6,9 @@ * 当前实现了PythonExecute工具。 */ -#include "mcp/include/mcp_server.h" -#include "mcp/include/mcp_tool.h" -#include "mcp/include/mcp_resource.h" +#include "../mcp/include/mcp_server.h" +#include "../mcp/include/mcp_tool.h" +#include "../mcp/include/mcp_resource.h" #include #include diff --git a/tool/base.h b/tool/base.h index 60f19ed..34a166f 100644 --- a/tool/base.h +++ b/tool/base.h @@ -11,137 +11,11 @@ namespace humanus { -// Execute the tool with given parameters. -struct BaseTool { - std::string name; - std::string description; - json parameters; - - std::unique_ptr _client; - - BaseTool(const std::string& name, const std::string& description, const json& parameters) : - name(name), description(description), parameters(parameters) { - // 从配置文件加载工具配置 - auto _config = MCPToolConfig::load_from_toml(name); - - if (_config.type == "stdio") { - std::string command = _config.command; - if (!_config.args.empty()) { - for (const auto& arg : _config.args) { - command += " " + arg; - } - } - _client = std::make_unique(command, _config.env_vars); - } else if (_config.type == "sse") { - if (!_config.host.empty() && !_config.port.empty()) { - _client = std::make_unique(_config.host, _config.port); - } else if (!_config.url.empty()) { - _client = std::make_unique(_config.url); - } else { - throw std::runtime_error("MCP SSE 配置缺少 host 或 port 或 url"); - } - } - - _client->initialize(name + "_client", "0.0.1"); - } - - // Execute the tool with given parameters. - ToolResult operator()(const json& arguments) { - return execute(arguments); - } - - // Execute the tool with given parameters. - virtual ToolResult execute(const json& arguments) { - try { - if (!_client) { - throw std::runtime_error("MCP 客户端未初始化"); - } - json result = _client->call_tool(name, arguments); - bool is_error = result.value("isError", false); - // 根据是否有错误返回不同的ToolResult - if (is_error) { - return ToolError(result.value("content", json::array())); - } else { - return ToolResult(result.value("content", json::array())); - } - } catch (const std::exception& e) { - return ToolError(e.what()); - } - } - - json to_param() const { - return { - {"type", "function"}, - {"function", { - {"name", name}, - {"description", description}, - {"parameters", parameters} - }} - }; - } -}; - -// Represents the result of a tool execution. -struct ToolResult { - json output; - json error; - json system; - - ToolResult(const json& output, const json& error = {}, const json& system = {}) - : output(output), error(error), system(system) {} - - bool empty() const { - return output.empty() && error.empty() && system.empty(); - } - - ToolResult operator+(const ToolResult& other) const { - auto combined_field = [](const json& field, const json& other_field) { - if (field.empty()) { - return other_field; - } - if (other_field.empty()) { - return field; - } - json result = json::array(); - if (field.is_array()) { - result.insert(result.end(), field.begin(), field.end()); - } else { - result.push_back(field); - } - if (other_field.is_array()) { - result.insert(result.end(), other_field.begin(), other_field.end()); - } else { - result.push_back(other_field); - } - return result; - }; - - return { - combined_field(output, other.output), - combined_field(error, other.error), - combined_field(system, other.system) - }; - } - - std::string to_string() const { - return !error.empty() ? "Error: " + error.dump() : output.dump(); - } -}; - -// A ToolResult that represents a failure. -struct ToolError : ToolResult { - ToolError(const std::string& error) : ToolResult({}, error) {} -}; - -struct AgentAware : ToolResult { - std::shared_ptr agent = nullptr; -}; - // 从config_mcp.toml中读取工具配置 struct MCPToolConfig { std::string type; std::string host; - std::string port; + int port; std::string url; std::string command; std::vector args; @@ -216,10 +90,10 @@ struct MCPToolConfig { } config.host = tool_table["host"].as_string()->get(); - if (!tool_table.contains("port") || !tool_table["port"].is_string()) { + if (!tool_table.contains("port") || !tool_table["port"].is_integer()) { throw std::runtime_error("sse类型工具配置缺少port字段: " + tool_name); } - config.port = tool_table["port"].as_string()->get(); + config.port = tool_table["port"].as_integer()->get(); } } else { throw std::runtime_error("不支持的工具类型: " + config.type); @@ -233,6 +107,137 @@ struct MCPToolConfig { } }; +// Represents the result of a tool execution. +struct ToolResult { + json output; + json error; + json system; + + ToolResult(const json& output, const json& error = {}, const json& system = {}) + : output(output), error(error), system(system) {} + + bool empty() const { + return output.empty() && error.empty() && system.empty(); + } + + ToolResult operator+(const ToolResult& other) const { + auto combined_field = [](const json& field, const json& other_field) { + if (field.empty()) { + return other_field; + } + if (other_field.empty()) { + return field; + } + json result = json::array(); + if (field.is_array()) { + result.insert(result.end(), field.begin(), field.end()); + } else { + result.push_back(field); + } + if (other_field.is_array()) { + result.insert(result.end(), other_field.begin(), other_field.end()); + } else { + result.push_back(other_field); + } + return result; + }; + + return { + combined_field(output, other.output), + combined_field(error, other.error), + combined_field(system, other.system) + }; + } + + std::string to_string() const { + return !error.empty() ? "Error: " + error.dump() : output.dump(); + } +}; + +// A ToolResult that represents a failure. +struct ToolError : ToolResult { + ToolError(const std::string& error) : ToolResult({}, error) {} +}; + +// Execute the tool with given parameters. +struct BaseTool { + inline static std::set special_tool_name = {"terminate"}; + + std::string name; + std::string description; + json parameters; + + std::unique_ptr _client; + + BaseTool(const std::string& name, const std::string& description, const json& parameters) : + name(name), description(description), parameters(parameters) { + if (special_tool_name.find(name) != special_tool_name.end()) { + return; + } + // 从配置文件加载工具配置 + auto _config = MCPToolConfig::load_from_toml(name); + + if (_config.type == "stdio") { + std::string command = _config.command; + if (!_config.args.empty()) { + for (const auto& arg : _config.args) { + command += " " + arg; + } + } + _client = std::make_unique(command, _config.env_vars); + } else if (_config.type == "sse") { + if (!_config.host.empty() && _config.port > 0) { + _client = std::make_unique(_config.host, _config.port); + } else if (!_config.url.empty()) { + _client = std::make_unique(_config.url, "/sse"); + } else { + throw std::runtime_error("MCP SSE 配置缺少 host 或 port 或 url"); + } + } + + _client->initialize(name + "_client", "0.0.1"); + } + + // Execute the tool with given parameters. + ToolResult operator()(const json& arguments) { + return execute(arguments); + } + + // Execute the tool with given parameters. + virtual ToolResult execute(const json& arguments) { + try { + if (!_client) { + throw std::runtime_error("MCP 客户端未初始化"); + } + json result = _client->call_tool(name, arguments); + bool is_error = result.value("isError", false); + // 根据是否有错误返回不同的ToolResult + if (is_error) { + return ToolError(result.value("content", json::array())); + } else { + return ToolResult(result.value("content", json::array())); + } + } catch (const std::exception& e) { + return ToolError(e.what()); + } + } + + json to_param() const { + return { + {"type", "function"}, + {"function", { + {"name", name}, + {"description", description}, + {"parameters", parameters} + }} + }; + } +}; + +struct AgentAware : ToolResult { + std::shared_ptr agent = nullptr; +}; + } #endif // HUMANUS_TOOL_BASE_H diff --git a/tool/filesystem.h b/tool/filesystem.h index f424906..77ef840 100644 --- a/tool/filesystem.h +++ b/tool/filesystem.h @@ -26,7 +26,7 @@ struct FileSystem : BaseTool { "get_file_info", "list_allowed_directories" ] - } + }, "path": { "type": "string", "description": "The path to the file or directory to operate on. Only works within allowed directories. Required by all tools except `read_multiple_files`, `move_file` and `list_allowed_directories`." @@ -44,7 +44,7 @@ struct FileSystem : BaseTool { }, "edits": { "type": "array", - "description": "Each edit replaces exact line sequences with new content. Required by `edit_file`.", + "description": "Each edit replaces exact line sequences with new content. Required by `edit_file`." }, "source": { "type": "string", @@ -82,7 +82,7 @@ struct FileSystem : BaseTool { return ToolError("Tool is required"); } - json result = _client->call_tool("puppeteer_" + tool, args); + json result = _client->call_tool(tool, args); bool is_error = result.value("isError", false); diff --git a/tool/google_search.h b/tool/google_search.h deleted file mode 100644 index 429ad2a..0000000 --- a/tool/google_search.h +++ /dev/null @@ -1,95 +0,0 @@ -#ifndef HUMANUS_TOOL_GOOGLE_SEARCH_H -#define HUMANUS_TOOL_GOOGLE_SEARCH_H - -#include "../mcp/common/httplib.h" -#include "base.h" - -namespace humanus { - -struct GoogleSearch : BaseTool { - inline static const std::string name_ = "google_search"; - inline static const std::string description_ = R"(Perform a Google search and return a list of relevant links. -Use this tool when you need to find information on the web, get up-to-date data, or research specific topics. -The tool returns a list of URLs that match the search query.)"; - inline static const json parameters_ = json::parse(R"json( { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "(required) The search query to submit to Google.", - }, - "num_results": { - "type": "integer", - "description": "(optional) The number of search results to return. Default is 10.", - "default": 10, - }, - }, - "required": ["query"], - })json"); - - GoogleSearch() : BaseTool(name_, description_, parameters_) {} - - ToolResult execute(const json& args) override { - try { - std::string query = args["query"]; - int num_results = args.value("num_results", 10); - - // 创建HTTP客户端连接到serper.dev API - httplib::Client cli("https://api.serper.dev"); - - // 准备请求体 - json request_body = { - {"q", query}, - {"num", num_results} - }; - - // 设置请求头 - const char* api_key = std::getenv("X_API_KEY"); - if (!api_key) { - return ToolError("X_API_KEY is not set"); - } - - httplib::Headers headers = { - {"Content-Type", "application/json"}, - {"X-API-KEY", api_key} - }; - - // 发送POST请求 - auto res = cli.Post("/search", headers, request_body.dump(), "application/json"); - - if (!res) { - return ToolError("Failed to connect to search API"); - } - - if (res->status != 200) { - return ToolError("Search API returned status code = " + std::to_string(res->status) + ", body = " + res->body); - } - - // 解析响应 - json response = json::parse(res->body); - - // 格式化结果 - std::string result = "Search results for: " + query + "\n\n"; - - if (response.contains("organic") && response["organic"].is_array()) { - for (size_t i = 0; i < response["organic"].size() && i < static_cast(num_results); ++i) { - const auto& item = response["organic"][i]; - result += std::to_string(i+1) + ". " + item.value("title", "No title") + "\n"; - result += " URL: " + item.value("link", "No link") + "\n"; - result += " Snippet: " + item.value("snippet", "No description") + "\n\n"; - } - } else { - result += "No results found."; - } - - return ToolResult(result); - } catch (const std::exception& e) { - return ToolResult("Error executing Google search: " + std::string(e.what())); - } - } -}; - -} - -#endif // OPEMANUS_TOOL_GOOGLE_SEARCH_H - diff --git a/tool/planning.cpp b/tool/planning.cpp index 20899aa..661e44d 100644 --- a/tool/planning.cpp +++ b/tool/planning.cpp @@ -1,4 +1,5 @@ #include "planning.h" +#include // 添加iomanip头文件,用于std::setprecision namespace humanus { diff --git a/tool/planning.h b/tool/planning.h index d1cf512..a98dd24 100644 --- a/tool/planning.h +++ b/tool/planning.h @@ -2,18 +2,14 @@ #define HUMANUS_TOOL_PLANNING_H #include "base.h" +#include "../prompt.h" namespace humanus { -const char* _PLANNING_TOOL_DESCRIPTION = R"( -A planning tool that allows the agent to create and manage plans for solving complex tasks. -The tool provides functionality for creating plans, updating plan steps, and tracking progress. -)"; - struct PlanningTool : BaseTool { inline static const std::string name_ = "planning"; inline static const std::string description_ = _PLANNING_TOOL_DESCRIPTION; - inline static const std::vector parameters_ = json::parse(R"json({ + inline static const json parameters_ = json::parse(R"json({ "type": "object", "properties": { "command": { @@ -25,39 +21,39 @@ struct PlanningTool : BaseTool { "get", "set_active", "mark_step", - "delete", + "delete" ], - "type": "string", + "type": "string" }, "plan_id": { "description": "Unique identifier for the plan. Required for create, update, set_active, and delete commands. Optional for get and mark_step (uses active plan if not specified).", - "type": "string", + "type": "string" }, "title": { "description": "Title for the plan. Required for create command, optional for update command.", - "type": "string", + "type": "string" }, "steps": { "description": "List of plan steps. Required for create command, optional for update command.", "type": "array", - "items": {"type": "string"}, + "items": {"type": "string"} }, "step_index": { "description": "Index of the step to update (0-based). Required for mark_step command.", - "type": "integer", + "type": "integer" }, "step_status": { "description": "Status to set for a step. Used with mark_step command.", "enum": ["not_started", "in_progress", "completed", "blocked"], - "type": "string", + "type": "string" }, "step_notes": { "description": "Additional notes for a step. Optional for mark_step command.", - "type": "string", - }, + "type": "string" + } }, "required": ["command"], - "additionalProperties": false, + "additionalProperties": false })json"); diff --git a/tool/puppeteer.h b/tool/puppeteer.h index d230aef..1038bbf 100644 --- a/tool/puppeteer.h +++ b/tool/puppeteer.h @@ -28,10 +28,24 @@ struct Puppeteer : BaseTool { "type": "string", "description": "The URL to navigate to. Required by `navigate`." }, + "name": { + "type": "string", + "description": "The name of the screenshot. Required by `screenshot`." + }, "selector": { "type": "string", "description": "The CSS selector for the element to interact with. Required by `click`, `hover`, `fill`, and `select`." }, + "width": { + "type": "number", + "description": "The width of the screenshot. Required by `screenshot`. Default: 800", + "default": 800 + }, + "height": { + "type": "number", + "description": "The height of the screenshot. Required by `screenshot`. Default: 600", + "default": 600 + }, "value": { "type": "string", "description": "The value to fill in input fields. Required by `fill`." @@ -44,6 +58,7 @@ struct Puppeteer : BaseTool { "required": ["tool"] })json"); + Puppeteer() : BaseTool(name_, description_, parameters_) {} ToolResult execute(const json& args) override { try { diff --git a/tool/terminate.h b/tool/terminate.h index 1d96fe5..51ec443 100644 --- a/tool/terminate.h +++ b/tool/terminate.h @@ -1,15 +1,12 @@ #ifndef HUMANUS_TOOL_TERMINATE_H #define HUMANUS_TOOL_TERMINATE_H #include "base.h" +#include "../prompt.h" -namespace humanus { - -const char* _TERMINATE_DESCRIPTION = "Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task."; - -struct Terminate : BaseTool { +struct Terminate : humanus::BaseTool { inline static const std::string name_ = "terminate"; - inline static const std::string description_ = _TERMINATE_DESCRIPTION; - inline static const json parameters_ = { + inline static const std::string description_ = "Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task."; + inline static const humanus::json parameters_ = { {"type", "object"}, {"properties", { {"status", { @@ -24,13 +21,12 @@ struct Terminate : BaseTool { Terminate() : BaseTool(name_, description_, parameters_) {} // Finish the current execution - ToolResult execute(const json& arguments) override { - return ToolResult{ + humanus::ToolResult execute(const humanus::json& arguments) override { + return humanus::ToolResult{ "The interaction has been completed with status: " + arguments.value("status", "unknown") }; } }; -} #endif // HUMANUS_TOOL_TERMINATE_H \ No newline at end of file