|
| 1 | +#include "tool_call_corruption.hpp" |
| 2 | + |
| 3 | +#include <nlohmann/json.hpp> |
| 4 | + |
| 5 | +namespace acecode { |
| 6 | + |
| 7 | +namespace { |
| 8 | + |
| 9 | +constexpr std::string_view kToolCallOpen = "<tool_call>"; |
| 10 | +constexpr std::string_view kToolCallClose = "</tool_call>"; |
| 11 | +constexpr std::string_view kArgKeyOpen = "<arg_key>"; |
| 12 | +constexpr std::string_view kArgKeyClose = "</arg_key>"; |
| 13 | +constexpr std::string_view kArgValueOpen = "<arg_value>"; |
| 14 | +constexpr std::string_view kArgValueClose = "</arg_value>"; |
| 15 | +constexpr std::string_view kGlmMarkers[] = { |
| 16 | + kToolCallOpen, kToolCallClose, kArgKeyOpen, kArgKeyClose, kArgValueOpen, kArgValueClose}; |
| 17 | + |
| 18 | +// 名字里出现这些字符,说明模型把调用表达式当成了名字: |
| 19 | +// skill_view(name="x")、skill_view["x"]、{"name": …}。 |
| 20 | +constexpr std::string_view kCallSyntaxChars = "()[]{}\"'=<>`"; |
| 21 | + |
| 22 | +constexpr std::string_view kWhitespace = " \t\r\n"; |
| 23 | + |
| 24 | +bool is_blank(std::string_view text) { |
| 25 | + return text.find_first_not_of(kWhitespace) == std::string_view::npos; |
| 26 | +} |
| 27 | + |
| 28 | +// pos 起跳过空白后是否以 expected 开头。 |
| 29 | +bool followed_by(std::string_view text, std::size_t pos, std::string_view expected) { |
| 30 | + const auto next = text.find_first_not_of(kWhitespace, pos); |
| 31 | + return next != std::string_view::npos && text.compare(next, expected.size(), expected) == 0; |
| 32 | +} |
| 33 | + |
| 34 | +// 字符串值里的模板结构,命中返回结构开头的标记。规则见头文件。 |
| 35 | +std::optional<std::string> find_template_structure(std::string_view value) { |
| 36 | + for (auto pos = value.find(kArgKeyClose); pos != std::string_view::npos; |
| 37 | + pos = value.find(kArgKeyClose, pos + 1)) { |
| 38 | + if (followed_by(value, pos + kArgKeyClose.size(), kArgValueOpen)) { |
| 39 | + return std::string(kArgKeyClose); |
| 40 | + } |
| 41 | + } |
| 42 | + for (auto pos = value.find(kArgValueClose); pos != std::string_view::npos; |
| 43 | + pos = value.find(kArgValueClose, pos + 1)) { |
| 44 | + const std::size_t after = pos + kArgValueClose.size(); |
| 45 | + if (is_blank(value.substr(after)) || followed_by(value, after, kArgKeyOpen) || |
| 46 | + followed_by(value, after, kToolCallOpen) || followed_by(value, after, kToolCallClose)) { |
| 47 | + return std::string(kArgValueClose); |
| 48 | + } |
| 49 | + } |
| 50 | + return std::nullopt; |
| 51 | +} |
| 52 | + |
| 53 | +std::optional<std::string> find_corrupted_json_text(const nlohmann::json& value) { |
| 54 | + if (value.is_string()) return find_template_structure(value.get_ref<const std::string&>()); |
| 55 | + if (value.is_object()) { |
| 56 | + for (auto it = value.begin(); it != value.end(); ++it) { |
| 57 | + if (auto marker = find_glm_tool_call_marker(it.key())) return marker; |
| 58 | + if (auto marker = find_corrupted_json_text(it.value())) return marker; |
| 59 | + } |
| 60 | + } else if (value.is_array()) { |
| 61 | + for (const auto& item : value) { |
| 62 | + if (auto marker = find_corrupted_json_text(item)) return marker; |
| 63 | + } |
| 64 | + } |
| 65 | + return std::nullopt; |
| 66 | +} |
| 67 | + |
| 68 | +} // namespace |
| 69 | + |
| 70 | +bool is_conventional_tool_name(std::string_view name) { |
| 71 | + if (name.empty() || name.size() > 64) return false; |
| 72 | + for (const char c : name) { |
| 73 | + const bool allowed = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || |
| 74 | + (c >= '0' && c <= '9') || c == '_' || c == '-'; |
| 75 | + if (!allowed) return false; |
| 76 | + } |
| 77 | + return true; |
| 78 | +} |
| 79 | + |
| 80 | +std::optional<std::string> find_glm_tool_call_marker(std::string_view text) { |
| 81 | + std::size_t earliest = std::string_view::npos; |
| 82 | + std::string_view found; |
| 83 | + for (const auto marker : kGlmMarkers) { |
| 84 | + const auto pos = text.find(marker); |
| 85 | + if (pos < earliest) { |
| 86 | + earliest = pos; |
| 87 | + found = marker; |
| 88 | + } |
| 89 | + } |
| 90 | + if (earliest == std::string_view::npos) return std::nullopt; |
| 91 | + return std::string(found); |
| 92 | +} |
| 93 | + |
| 94 | +std::optional<ToolCallCorruption> diagnose_corrupted_tool_name(std::string_view name) { |
| 95 | + using Field = ToolCallCorruption::Field; |
| 96 | + using Reason = ToolCallCorruption::Reason; |
| 97 | + if (is_blank(name)) return ToolCallCorruption{Field::Name, Reason::EmptyName, {}}; |
| 98 | + if (is_conventional_tool_name(name)) return std::nullopt; |
| 99 | + if (auto marker = find_glm_tool_call_marker(name)) { |
| 100 | + return ToolCallCorruption{Field::Name, Reason::TemplateMarkup, std::move(*marker)}; |
| 101 | + } |
| 102 | + const auto syntax = name.find_first_of(kCallSyntaxChars); |
| 103 | + if (syntax != std::string_view::npos) { |
| 104 | + return ToolCallCorruption{Field::Name, Reason::CallSyntax, std::string(1, name[syntax])}; |
| 105 | + } |
| 106 | + return std::nullopt; |
| 107 | +} |
| 108 | + |
| 109 | +std::optional<ToolCallCorruption> diagnose_corrupted_tool_arguments(std::string_view arguments) { |
| 110 | + // 快路径:绝大多数参数里连标签名都没有(file_write 的参数可能有几百 KB)。 |
| 111 | + if (arguments.find("arg_") == std::string_view::npos && |
| 112 | + arguments.find("tool_call>") == std::string_view::npos) { |
| 113 | + return std::nullopt; |
| 114 | + } |
| 115 | + const auto parsed = nlohmann::json::parse(arguments, nullptr, false); |
| 116 | + std::optional<std::string> marker = parsed.is_discarded() |
| 117 | + ? find_glm_tool_call_marker(arguments) |
| 118 | + : find_corrupted_json_text(parsed); |
| 119 | + if (!marker) return std::nullopt; |
| 120 | + return ToolCallCorruption{ToolCallCorruption::Field::Arguments, |
| 121 | + ToolCallCorruption::Reason::TemplateMarkup, std::move(*marker)}; |
| 122 | +} |
| 123 | + |
| 124 | +std::optional<CorruptedToolCall> find_corrupted_tool_call( |
| 125 | + const std::vector<ToolCall>& calls, |
| 126 | + const std::function<bool(const std::string&)>& is_registered) { |
| 127 | + for (std::size_t i = 0; i < calls.size(); ++i) { |
| 128 | + const auto& call = calls[i]; |
| 129 | + if (!is_registered || !is_registered(call.function_name)) { |
| 130 | + if (auto corruption = diagnose_corrupted_tool_name(call.function_name)) { |
| 131 | + return CorruptedToolCall{i, std::move(*corruption)}; |
| 132 | + } |
| 133 | + } |
| 134 | + if (auto corruption = diagnose_corrupted_tool_arguments(call.function_arguments)) { |
| 135 | + return CorruptedToolCall{i, std::move(*corruption)}; |
| 136 | + } |
| 137 | + } |
| 138 | + return std::nullopt; |
| 139 | +} |
| 140 | + |
| 141 | +} // namespace acecode |
0 commit comments