diff --git a/cactus-engine/src/chat_tools.h b/cactus-engine/src/chat_tools.h index f81159911..244e18cfc 100644 --- a/cactus-engine/src/chat_tools.h +++ b/cactus-engine/src/chat_tools.h @@ -190,7 +190,11 @@ inline std::string py_literal_to_json(const std::string& s, size_t& p) { ++p; bool first = true; while (true) { skip_ws(s, p); - if (p >= s.size() || s[p] == close) { if (p < s.size()) ++p; break; } + // A mismatched closing bracket must not leave the parser stuck. + if (p >= s.size() || s[p] == close || s[p] == ')' || s[p] == ']' || s[p] == '}') { + if (p < s.size() && s[p] == close) ++p; + break; + } if (s[p] == ',') { ++p; continue; } if (!first) out += ", "; first = false; @@ -210,7 +214,8 @@ inline std::string py_literal_to_json(const std::string& s, size_t& p) { if (s.compare(p, 4, "None") == 0) { p += 4; return "null"; } size_t start = p; while (p < s.size() && s[p] != ',' && s[p] != ')' && s[p] != ']' && s[p] != '}') ++p; - return trim(s.substr(start, p - start)); + std::string v = trim(s.substr(start, p - start)); + return v.empty() ? "null" : v; } inline void extract_lfm2_tool_calls(std::string& text, std::vector& calls) { @@ -239,13 +244,15 @@ inline void extract_lfm2_tool_calls(std::string& text, std::vector& while (true) { skip_ws(inner, p); if (p >= inner.size() || inner[p] == ')') { if (p < inner.size()) ++p; break; } - if (inner[p] == ',') { ++p; continue; } + // Skip unexpected closing brackets between arguments. + if (inner[p] == ',' || inner[p] == '}' || inner[p] == ']') { ++p; continue; } size_t ks = p; while (p < inner.size() && (std::isalnum(static_cast(inner[p])) || inner[p] == '_')) ++p; std::string key = inner.substr(ks, p - ks); skip_ws(inner, p); if (p < inner.size() && inner[p] == '=') ++p; std::string val = py_literal_to_json(inner, p); + if (p == ks) { ++p; continue; } // Always advance on malformed input. if (!first) args += ", "; first = false; args += "\"" + key + "\": " + val; diff --git a/cactus-engine/tests/test_chat_tools_823.cpp b/cactus-engine/tests/test_chat_tools_823.cpp new file mode 100644 index 000000000..a2d9c65cf --- /dev/null +++ b/cactus-engine/tests/test_chat_tools_823.cpp @@ -0,0 +1,45 @@ +#include "../src/chat_tools.h" +#include +#include +#include + +int main() { + struct Case { + const char* input; + const char* expected; + }; + + const std::vector cases = { + {R"([get_weather(city="Paris", options={"units": "metric"})])", + R"({"name": "get_weather", "arguments": {"city": "Paris", "options": {"units": "metric"}}})"}, + + {R"([get_weather(city="Paris", options={"units": "metric")])", + R"({"name": "get_weather", "arguments": {"city": "Paris", "options": {"units": "metric"}}})"}, + + {R"([set_alarm(times=["07:00", "07:30"})])", + R"({"name": "set_alarm", "arguments": {"times": ["07:00", "07:30"]}})"}, + + {R"([get_weather(city="Paris"})])", + R"({"name": "get_weather", "arguments": {"city": "Paris"}})"} + }; + + for (size_t i = 0; i < cases.size(); ++i) { + std::string text = + std::string("<|tool_call_start|>") + + cases[i].input + "<|tool_call_end|>"; + + std::vector calls; + chat_tools::extract_lfm2_tool_calls(text, calls); + + if (calls.size() != 1 || + calls[0] != cases[i].expected || + !text.empty()) { + std::cerr << "FAIL case " << i + 1 << "\n"; + return 1; + } + + std::cout << "PASS case " << i + 1 << "\n"; + } + + return 0; +}