From 26c45d966ee51931c3767115476414b9955e66e4 Mon Sep 17 00:00:00 2001 From: hkr04 Date: Tue, 8 Apr 2025 23:26:53 +0800 Subject: [PATCH] add humanus_server (mcp) --- agent/base.h | 11 +- agent/react.h | 6 +- agent/toolcall.cpp | 12 ++ examples/CMakeLists.txt | 8 -- examples/chat/CMakeLists.txt | 1 - examples/main/CMakeLists.txt | 9 +- examples/mcp/CMakeLists.txt | 9 +- examples/plan/CMakeLists.txt | 9 +- examples/server/CMakeLists.txt | 5 + examples/server/server.cpp | 222 +++++++++++++++++++++++++++++++++ include/llm.h | 9 +- include/logger.h | 173 +++++++++++++++++++++++-- mcp | 2 +- server/python_execute.cpp | 114 ++++++++++++++--- src/logger.cpp | 23 ++-- 15 files changed, 530 insertions(+), 83 deletions(-) create mode 100644 examples/server/CMakeLists.txt create mode 100644 examples/server/server.cpp diff --git a/agent/base.h b/agent/base.h index f009d54..a4e6763 100644 --- a/agent/base.h +++ b/agent/base.h @@ -157,7 +157,7 @@ struct BaseAgent : std::enable_shared_from_this { memory->add_message(Message::user_message(stuck_prompt)); } - // O(nm) LCS algorithm, could basically handle current LLM context + // O(n * m) LCS algorithm, could basically handle current LLM context size_t get_lcs_length(const std::string& s1, const std::string& s2) { std::vector> dp(s1.size() + 1, std::vector(s2.size() + 1)); for (size_t i = 1; i <= s1.size(); i++) { @@ -209,10 +209,19 @@ struct BaseAgent : std::enable_shared_from_this { void reset(bool reset_memory = true) { current_step = 0; state = AgentState::IDLE; + llm->reset_tokens(); if (reset_memory) { memory->clear(); } } + + size_t get_prompt_tokens() { + return llm->get_prompt_tokens(); + } + + size_t get_completion_tokens() { + return llm->get_completion_tokens(); + } }; } // namespace humanus diff --git a/agent/react.h b/agent/react.h index 5d7451e..b9f1558 100644 --- a/agent/react.h +++ b/agent/react.h @@ -35,11 +35,13 @@ struct ReActAgent : BaseAgent { // Execute a single step: think and act. virtual std::string step() { bool should_act = think(); - logger->info("Prompt tokens: " + std::to_string(llm->total_prompt_tokens()) + ", Completion tokens: " + std::to_string(llm->total_completion_tokens())); if (!should_act) { return "Thinking complete - no action needed"; } - return act(); + if (state == AgentState::RUNNING) { + return act(); + } + return "Agent is not running"; } }; diff --git a/agent/toolcall.cpp b/agent/toolcall.cpp index ead9b6a..c7f2f7a 100644 --- a/agent/toolcall.cpp +++ b/agent/toolcall.cpp @@ -30,6 +30,10 @@ bool ToolCallAgent::think() { ); } + if (state != AgentState::RUNNING) { + return false; + } + try { // Handle different tool_choices modes if (tool_choice == "none") { @@ -76,6 +80,10 @@ std::string ToolCallAgent::act() { std::vector results; for (const auto& tool_call : tool_calls) { + if (state != AgentState::RUNNING) { + break; + } + auto result = execute_tool(tool_call); logger->info( "🎯 Tool `" + tool_call.function.name + "` completed its mission! Result: " + result.substr(0, 500) + (result.size() > 500 ? "..." : "") @@ -94,6 +102,10 @@ std::string ToolCallAgent::act() { result_str += result + "\n\n"; } + if (state != AgentState::RUNNING) { + result_str += "Agent is not running, so no more tool calls will be executed.\n\n"; + } + return result_str; } diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e93b581..74bdaa2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,16 +1,8 @@ -# examples/CMakeLists.txt -# 构建所有examples目录 - -# 获取examples目录下的所有子目录 file(GLOB EXAMPLE_DIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*) -# 遍历所有子目录 foreach(EXAMPLE_DIR ${EXAMPLE_DIRS}) - # 检查是否是目录 if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${EXAMPLE_DIR}) - # 检查子目录中是否有CMakeLists.txt文件 if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${EXAMPLE_DIR}/CMakeLists.txt") - # 添加子目录 add_subdirectory(${EXAMPLE_DIR}) message(STATUS "Added example: ${EXAMPLE_DIR}") endif() diff --git a/examples/chat/CMakeLists.txt b/examples/chat/CMakeLists.txt index 1838281..25dfb29 100644 --- a/examples/chat/CMakeLists.txt +++ b/examples/chat/CMakeLists.txt @@ -2,5 +2,4 @@ set(target humanus_chat) add_executable(${target} humanus_chat.cpp) -# 链接到核心库 target_link_libraries(${target} PRIVATE humanus) \ No newline at end of file diff --git a/examples/main/CMakeLists.txt b/examples/main/CMakeLists.txt index b98fb05..2bf86ca 100644 --- a/examples/main/CMakeLists.txt +++ b/examples/main/CMakeLists.txt @@ -2,11 +2,4 @@ set(target humanus_cli) add_executable(${target} main.cpp) -# 链接到核心库 -target_link_libraries(${target} PRIVATE humanus) - -# 设置输出目录 -set_target_properties(${target} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) \ No newline at end of file +target_link_libraries(${target} PRIVATE humanus) \ No newline at end of file diff --git a/examples/mcp/CMakeLists.txt b/examples/mcp/CMakeLists.txt index d1868b4..d0ac5f9 100644 --- a/examples/mcp/CMakeLists.txt +++ b/examples/mcp/CMakeLists.txt @@ -2,11 +2,4 @@ set(target humanus_cli_mcp) add_executable(${target} humanus_mcp.cpp) -# 链接到核心库 -target_link_libraries(${target} PRIVATE humanus) - -# 设置输出目录 -set_target_properties(${target} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) \ No newline at end of file +target_link_libraries(${target} PRIVATE humanus) \ No newline at end of file diff --git a/examples/plan/CMakeLists.txt b/examples/plan/CMakeLists.txt index 1067993..539a777 100644 --- a/examples/plan/CMakeLists.txt +++ b/examples/plan/CMakeLists.txt @@ -2,11 +2,4 @@ set(target humanus_cli_plan) add_executable(${target} humanus_plan.cpp) -# 链接到核心库 -target_link_libraries(${target} PRIVATE humanus) - -# 设置输出目录 -set_target_properties(${target} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) \ No newline at end of file +target_link_libraries(${target} PRIVATE humanus) \ No newline at end of file diff --git a/examples/server/CMakeLists.txt b/examples/server/CMakeLists.txt new file mode 100644 index 0000000..e619073 --- /dev/null +++ b/examples/server/CMakeLists.txt @@ -0,0 +1,5 @@ +set(target humanus_server) + +add_executable(${target} server.cpp) + +target_link_libraries(${target} PRIVATE humanus) \ No newline at end of file diff --git a/examples/server/server.cpp b/examples/server/server.cpp new file mode 100644 index 0000000..1a51583 --- /dev/null +++ b/examples/server/server.cpp @@ -0,0 +1,222 @@ +#include "httplib.h" +#include "agent/humanus.h" +#include "logger.h" +#include "mcp_server.h" +#include "mcp_tool.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace humanus; + +static auto session_sink = SessionSink::get_instance(); + +class SessionManager { +public: + std::shared_ptr get_agent(const std::string& session_id) { + std::lock_guard lock(mutex_); + + auto it = agents_.find(session_id); + if (it != agents_.end()) { + return it->second; + } + + auto agent = std::make_shared(); + agents_[session_id] = agent; + + return agent; + } + + static std::vector get_logs_buffer(const std::string& session_id) { + return session_sink->get_buffer(session_id); + } + + static std::vector get_logs_history(const std::string& session_id) { + return session_sink->get_history(session_id); + } + + void set_result(const std::string& session_id, const std::string& result) { + std::lock_guard lock(mutex_); + results_[session_id] = result; + } + + std::string get_result(const std::string& session_id) { + std::lock_guard lock(mutex_); + + auto it = results_.find(session_id); + if (it != results_.end()) { + return it->second; + } + + return ""; + } + + void clear_result(const std::string& session_id) { + std::lock_guard lock(mutex_); + results_.erase(session_id); + } + + bool has_session(const std::string& session_id) { + std::lock_guard lock(mutex_); + return agents_.find(session_id) != agents_.end(); + } + + void close_session(const std::string& session_id) { + std::lock_guard lock(mutex_); + agents_.erase(session_id); + results_.erase(session_id); + session_sink->cleanup_session(session_id); + } + + std::vector get_all_sessions() { + std::lock_guard lock(mutex_); + std::vector sessions; + for (const auto& pair : agents_) { + sessions.push_back(pair.first); + } + return sessions; + } + +private: + std::mutex mutex_; + std::unordered_map> agents_; + std::unordered_map results_; + std::unordered_map> session_sinks_; +}; + +int main(int argc, char** argv) { + mcp::set_log_level(mcp::log_level::warning); + + int port = 8896; + + if (argc == 2) { + try { + port = std::stoi(argv[1]); + } catch (...) { + std::cerr << "Invalid port number: " << argv[1] << std::endl; + return 1; + } + } + + // Create and configure server + mcp::server server("localhost", port, "HumanusServer", "0.0.1"); + + // Set server capabilities + mcp::json capabilities = { + {"tools", mcp::json::object()} + }; + server.set_capabilities(capabilities); + + auto session_manager = std::make_shared(); + + auto run_tool = mcp::tool_builder("humanus_run") + .with_description("Request to start a new task. Best to give clear and concise prompts.") + .with_string_param("prompt", "The prompt text to process", true) + .build(); + + server.register_tool(run_tool, [session_manager](const json& args, const std::string& session_id) -> json { + if (!args.contains("prompt")) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing `prompt` parameter"); + } + + std::string prompt = args["prompt"].get(); + + auto agent = session_manager->get_agent(session_id); + + if (agent->state != AgentState::IDLE) { + throw mcp::mcp_exception(mcp::error_code::invalid_request, "The agent is busy, please wait for the current task to complete or terminate the current task."); + } + + agent->reset(); + + std::thread([agent, session_manager, prompt, session_id]() { + try { + session_sink->set_session_id(session_id); + logger->info("Processing your request: " + prompt); + auto result = agent->run(prompt); + logger->info("Task completed."); + session_manager->set_result(session_id, result); + } catch (const std::exception& e) { + logger->error("Session {} error: {}", session_id, e.what()); + } + }).detach(); + + return {{ + {"type", "text"}, + {"text", "Task started, call `humanus_status` to check the status."} + }}; + }); + + auto terminate_tool = mcp::tool_builder("humanus_terminate") + .with_description("Terminate the current task") + .build(); + + server.register_tool(terminate_tool, [session_manager](const json& args, const std::string& session_id) -> json { + if (!session_manager->has_session(session_id)) { + throw mcp::mcp_exception(mcp::error_code::invalid_request, "Session not found"); + } + + auto agent = session_manager->get_agent(session_id); + + if (agent->state == AgentState::IDLE) { + return {{ + {"type", "text"}, + {"text", "The agent is idle, no task to terminate."} + }}; + } + + agent->update_memory("user", "User interrupted the interaction. Consider rescheduling the previous task or switching to a different task according to the user's request."); + agent->state = AgentState::IDLE; + + logger->info("Task terminated by user."); + + return {{ + {"type", "text"}, + {"text", "Task terminated."} + }}; + }); + + auto status_tool = mcp::tool_builder("humanus_status") + .with_description("Get the status of the current task.") + .build(); + + server.register_tool(status_tool, [session_manager](const json& args, const std::string& session_id) -> json { + if (!session_manager->has_session(session_id)) { + throw mcp::mcp_exception(mcp::error_code::invalid_request, "Session not found"); + } + + auto agent = session_manager->get_agent(session_id); + auto result = session_manager->get_result(session_id); + + json status = { + {"state", agent_state_map[agent->state]}, + {"current_step", agent->current_step}, + {"max_steps", agent->max_steps}, + {"prompt_tokens", agent->get_prompt_tokens()}, + {"completion_tokens", agent->get_completion_tokens()}, + {"logs_buffer", session_sink->get_buffer(session_id)}, + {"result", result} + }; + + return {{ + {"type", "text"}, + {"text", status.dump(2)} + }}; + }); + + // Start server + std::cout << "Starting Humanus server at http://localhost:" << port << "..." << std::endl; + std::cout << "Press Ctrl+C to stop server" << std::endl; + server.start(true); // Blocking mode + + return 0; +} + diff --git a/include/llm.h b/include/llm.h index d365d2a..1700fb0 100644 --- a/include/llm.h +++ b/include/llm.h @@ -120,13 +120,18 @@ public: int max_retries = 3 ); - size_t total_prompt_tokens() const { + size_t get_prompt_tokens() const { return total_prompt_tokens_; } - size_t total_completion_tokens() const { + size_t get_completion_tokens() const { return total_completion_tokens_; } + + void reset_tokens() { + total_prompt_tokens_ = 0; + total_completion_tokens_ = 0; + } }; } // namespace humanus diff --git a/include/logger.h b/include/logger.h index 6b835eb..224a957 100644 --- a/include/logger.h +++ b/include/logger.h @@ -1,33 +1,180 @@ #ifndef HUMANUSlogger_H #define HUMANUSlogger_H +#include "config.h" #include "spdlog/spdlog.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/sinks/basic_file_sink.h" #include "spdlog/sinks/rotating_file_sink.h" #include "spdlog/sinks/daily_file_sink.h" #include "spdlog/sinks/dist_sink.h" - #include #include -#include "config.h" +#include +#include namespace humanus { static spdlog::level::level_enum _print_level = spdlog::level::info; +static spdlog::level::level_enum _logfile_level = spdlog::level::debug; -/** - * @brief Adjust the log level - * @param print_level The console output log level - * @param logfile_level The file record log level - * @param name The log file name prefix - * @return The log record instance - */ -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 = ""); +extern std::shared_ptr set_log_level(spdlog::level::level_enum print_level, spdlog::level::level_enum logfile_level); -static std::shared_ptr logger = define_log_level(); +class SessionSink : public spdlog::sinks::base_sink { +private: + inline static std::unordered_map> buffers_; // session_id -> buffer + inline static std::unordered_map> histories_; // session_id -> history + inline static std::unordered_map sessions_; // thread_id -> session_id + inline static std::mutex mutex_; + + SessionSink() = default; + SessionSink(const SessionSink&) = delete; + SessionSink& operator=(const SessionSink&) = delete; + +public: + ~SessionSink() = default; + + static std::shared_ptr get_instance() { + static SessionSink instance; + static std::shared_ptr shared_instance(&instance, [](SessionSink*){}); + return shared_instance; + } + + void sink_it_(const spdlog::details::log_msg& msg) override { + std::lock_guard lock(mutex_); + + if (sessions_.find(get_thread_id()) == sessions_.end()) { // Ignore messages if session_id is not set + return; + } + + auto session_id = sessions_[get_thread_id()]; + + auto time_t = std::chrono::system_clock::to_time_t(msg.time); + auto tm = fmt::localtime(time_t); + std::string log_message = fmt::format("[{:%Y-%m-%d %H:%M:%S}] {}", tm, msg.payload); + + buffers_[session_id].push_back(log_message); + } + + void flush_() override { + std::lock_guard lock(mutex_); + + if (sessions_.find(get_thread_id()) == sessions_.end()) { // Ignore messages if session_id is not set + return; + } + + auto session_id = sessions_[get_thread_id()]; + + if (!buffers_[session_id].empty()) { + histories_[session_id].insert(histories_[session_id].end(), + buffers_[session_id].begin(), + buffers_[session_id].end()); + buffers_[session_id].clear(); + } + } + + inline std::string get_thread_id() { + std::stringstream ss; + ss << std::this_thread::get_id(); + return ss.str(); + } + + void set_session_id(std::string session_id) { + if (session_id.empty()) { + throw std::invalid_argument("session_id is empty"); + } + std::lock_guard lock(mutex_); + sessions_[get_thread_id()] = session_id; + } + + // Messages in buffer are flushed to the history and cleared from the buffer. + std::vector get_buffer(const std::string& session_id) { + std::lock_guard lock(mutex_); + + if (buffers_.find(session_id) == buffers_.end()) { + throw std::invalid_argument("Invalid session_id: " + session_id); + } + + std::vector buffer = buffers_[session_id]; + if (!buffers_[session_id].empty()) { + histories_[session_id].insert(histories_[session_id].end(), + buffers_[session_id].begin(), + buffers_[session_id].end()); + buffers_[session_id].clear(); + } + return buffer; + } + + std::vector get_history(const std::string& session_id) { + std::lock_guard lock(mutex_); + + if (histories_.find(session_id) == histories_.end()) { + throw std::invalid_argument("Invalid session_id: " + session_id); + } + + return histories_[session_id]; + } + + void clear_buffer() { + std::lock_guard lock(mutex_); + + if (sessions_.find(get_thread_id()) == sessions_.end()) { // Ignore messages if session_id is not set + return; + } + + auto session_id = sessions_[get_thread_id()]; + + if (!buffers_[session_id].empty()) { + histories_[session_id].insert(histories_[session_id].end(), + buffers_[session_id].begin(), + buffers_[session_id].end()); + buffers_[session_id].clear(); + } + } + + void clear_history() { + std::lock_guard lock(mutex_); + + if (sessions_.find(get_thread_id()) == sessions_.end()) { // Ignore messages if session_id is not set + return; + } + + auto session_id = sessions_[get_thread_id()]; + + histories_[session_id].clear(); + } + + void cleanup_session(const std::string& session_id) { + std::lock_guard lock(mutex_); + + for (auto it = sessions_.begin(); it != sessions_.end();) { + if (it->second == session_id) { + it = sessions_.erase(it); + } else { + ++it; + } + } + + buffers_.erase(session_id); + histories_.erase(session_id); + } + + std::vector get_active_sessions() { + std::lock_guard lock(mutex_); + std::vector result; + std::unordered_set unique_sessions; + + for (const auto& [thread_id, session_id] : sessions_) { + if (unique_sessions.insert(session_id).second) { + result.push_back(session_id); + } + } + + return result; + } +}; + +static std::shared_ptr logger = set_log_level(_print_level, _logfile_level); } // namespace humanus diff --git a/mcp b/mcp index 21dc5cb..0dbcd1e 160000 --- a/mcp +++ b/mcp @@ -1 +1 @@ -Subproject commit 21dc5cb1448923abc2566568403d837f0a9da64a +Subproject commit 0dbcd1e6d5bef3443c9d4b3cff6c6e3adef72264 diff --git a/server/python_execute.cpp b/server/python_execute.cpp index 19fc87a..038bbd2 100644 --- a/server/python_execute.cpp +++ b/server/python_execute.cpp @@ -14,6 +14,7 @@ #include #include #include +#include // Check if Python is found #ifdef PYTHON_FOUND @@ -22,13 +23,16 @@ /** * @class python_interpreter - * @brief Python interpreter class for executing Python code + * @brief Python interpreter class for executing Python code with session support */ class python_interpreter { private: // Mutex to ensure thread safety of Python interpreter mutable std::mutex py_mutex; bool is_initialized; + + // Map to store Python thread states for each session + mutable std::unordered_map session_states; public: /** @@ -40,7 +44,9 @@ public: Py_Initialize(); if (Py_IsInitialized()) { is_initialized = true; - PyThreadState *_save = PyEval_SaveThread(); + // Create main thread state + PyThreadState* main_thread_state = PyThreadState_Get(); + PyThreadState_Swap(NULL); } else { std::cerr << "Failed to initialize Python interpreter" << std::endl; } @@ -57,27 +63,69 @@ public: #ifdef PYTHON_FOUND if (is_initialized) { std::lock_guard lock(py_mutex); + // Clean up all session states + for (auto& pair : session_states) { + PyThreadState_Swap(pair.second); + PyThreadState_Clear(pair.second); + PyThreadState_Delete(pair.second); + } + session_states.clear(); + Py_Finalize(); is_initialized = false; } #endif } + + /** + * @brief Get or create a thread state for a session + * @param session_id The session identifier + * @return PyThreadState for the session + */ +#ifdef PYTHON_FOUND + PyThreadState* get_session_state(const std::string& session_id) const { + std::lock_guard lock(py_mutex); + + // Check if session already exists + auto it = session_states.find(session_id); + if (it != session_states.end()) { + return it->second; + } + + // Create new thread state for this session + PyThreadState* new_state = Py_NewInterpreter(); + if (!new_state) { + throw std::runtime_error("Failed to create new Python interpreter for session" + session_id); + } + + // Store and return the new state + session_states[session_id] = new_state; + return new_state; + } +#endif /** - * @brief Execute Python code + * @brief Execute Python code in the context of a specific session * @param input JSON object containing Python code + * @param session_id The session identifier * @return JSON object with execution results */ - mcp::json forward(const mcp::json& input) const { + mcp::json forward(const mcp::json& input, const std::string& session_id) const { #ifdef PYTHON_FOUND if (!is_initialized) { return mcp::json{{"error", "Python interpreter not properly initialized"}}; } - // Acquire GIL lock - std::lock_guard lock(py_mutex); - PyGILState_STATE gstate = PyGILState_Ensure(); + // Get or create session thread state + PyThreadState* tstate = nullptr; + try { + tstate = get_session_state(session_id); + } catch (const std::exception& e) { + return mcp::json{{"error", e.what()}}; + } + PyThreadState* old_state = PyThreadState_Swap(tstate); + mcp::json result_json; try { @@ -87,13 +135,13 @@ public: // Get main module and dictionary PyObject *main_module = PyImport_AddModule("__main__"); if (!main_module) { - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to get Python main module"}}; } PyObject *main_dict = PyModule_GetDict(main_module); if (!main_dict) { - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to get Python main module dictionary"}}; } @@ -101,7 +149,7 @@ public: PyObject *sys_module = PyImport_ImportModule("sys"); if (!sys_module) { PyErr_Print(); - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to import sys module"}}; } @@ -109,7 +157,7 @@ public: if (!io_module) { Py_DECREF(sys_module); PyErr_Print(); - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to import io module"}}; } @@ -119,7 +167,7 @@ public: Py_DECREF(io_module); Py_DECREF(sys_module); PyErr_Print(); - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to get StringIO class"}}; } @@ -130,7 +178,7 @@ public: Py_DECREF(io_module); Py_DECREF(sys_module); PyErr_Print(); - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to create stdout StringIO object"}}; } @@ -141,7 +189,7 @@ public: Py_DECREF(io_module); Py_DECREF(sys_module); PyErr_Print(); - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to create stderr StringIO object"}}; } @@ -161,7 +209,7 @@ public: Py_DECREF(io_module); Py_DECREF(sys_module); PyErr_Print(); - PyGILState_Release(gstate); + PyThreadState_Swap(old_state); return mcp::json{{"error", "Failed to set stdout/stderr redirection"}}; } @@ -237,11 +285,32 @@ public: result_json["error"] = std::string("Python execution exception: ") + e.what(); } - // Release GIL - PyGILState_Release(gstate); + // Restore previous thread state + PyThreadState_Swap(old_state); + return result_json; #else return mcp::json{{"error", "Python interpreter not available"}}; +#endif + } + + /** + * @brief Clean up a session and remove its thread state + * @param session_id The session identifier to clean up + */ + void cleanup_session(const std::string& session_id) { +#ifdef PYTHON_FOUND + std::lock_guard lock(py_mutex); + + auto it = session_states.find(session_id); + if (it != session_states.end()) { + PyThreadState* old_state = PyThreadState_Swap(it->second); + PyThreadState_Clear(it->second); + PyThreadState_Delete(it->second); + PyThreadState_Swap(old_state); + + session_states.erase(it); + } #endif } }; @@ -250,14 +319,14 @@ public: static python_interpreter interpreter; // Python execution tool handler function -mcp::json python_execute_handler(const mcp::json& args) { +mcp::json python_execute_handler(const mcp::json& args, const std::string& session_id) { if (!args.contains("code")) { throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'code' parameter"); } try { - // Use Python interpreter to execute code - mcp::json result = interpreter.forward(args); + // Use Python interpreter to execute code with session context + mcp::json result = interpreter.forward(args, session_id); return {{ {"type", "text"}, @@ -277,4 +346,9 @@ void register_python_execute_tool(mcp::server& server) { .build(); server.register_tool(python_tool, python_execute_handler); + + // Register session cleanup handler + server.register_session_cleanup("python_execute", [](const std::string& session_id) { + interpreter.cleanup_session(session_id); + }); } \ No newline at end of file diff --git a/src/logger.cpp b/src/logger.cpp index d73efd1..82f7c5b 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -6,20 +6,16 @@ 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; - +std::shared_ptr set_log_level(spdlog::level::level_enum print_level, spdlog::level::level_enum logfile_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 + ss << std::put_time(&tm_info, "%Y-%m-%d"); + std::string formatted_date = ss.str(); // YYYY-MM-DD - std::string log_name = name.empty() ? formatted_date : name + "_" + formatted_date; + std::string log_name = formatted_date; std::string log_file_path = (PROJECT_ROOT / "logs" / (log_name + ".log")).string(); // Ensure the log directory exists @@ -29,13 +25,18 @@ std::shared_ptr define_log_level(spdlog::level::level_enum print std::shared_ptr _logger = std::make_shared(log_name); auto stderr_sink = std::make_shared(); - stderr_sink->set_level(print_level); + stderr_sink->set_level(_print_level); _logger->sinks().push_back(stderr_sink); auto file_sink = std::make_shared(log_file_path, false); - file_sink->set_level(logfile_level); + file_sink->set_level(_logfile_level); _logger->sinks().push_back(file_sink); - + + auto session_sink = SessionSink::get_instance(); + session_sink->set_level(_print_level); + _logger->sinks().push_back(session_sink); + + // Reset the log output return _logger; }