Add OpenAI tool calling to the chat service
#1
by nobuddhy - opened
- CMakeLists.txt +5 -0
- include/ling3/chat_protocol.h +242 -38
- src/portable_main.cpp +31 -5
- tests/chat_protocol_test.cpp +1 -1
- tests/chat_tools_golden.json +464 -0
- tests/chat_tools_test.cpp +94 -0
- tools/render_chat_tools_golden.py +96 -0
CMakeLists.txt
CHANGED
|
@@ -122,6 +122,11 @@ if(BUILD_TESTING)
|
|
| 122 |
target_compile_features(ling3-chat-protocol-test PRIVATE cxx_std_20)
|
| 123 |
target_include_directories(ling3-chat-protocol-test PRIVATE include third_party/nlohmann)
|
| 124 |
add_test(NAME ling3-chat-protocol-test COMMAND ling3-chat-protocol-test)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
add_executable(ling3-router-test tests/router_test.cpp)
|
| 126 |
target_link_libraries(ling3-router-test PRIVATE ling3_core)
|
| 127 |
add_test(NAME ling3-router-test COMMAND ling3-router-test)
|
|
|
|
| 122 |
target_compile_features(ling3-chat-protocol-test PRIVATE cxx_std_20)
|
| 123 |
target_include_directories(ling3-chat-protocol-test PRIVATE include third_party/nlohmann)
|
| 124 |
add_test(NAME ling3-chat-protocol-test COMMAND ling3-chat-protocol-test)
|
| 125 |
+
add_executable(ling3-chat-tools-test tests/chat_tools_test.cpp)
|
| 126 |
+
target_compile_features(ling3-chat-tools-test PRIVATE cxx_std_20)
|
| 127 |
+
target_include_directories(ling3-chat-tools-test PRIVATE include third_party/nlohmann)
|
| 128 |
+
add_test(NAME ling3-chat-tools-test COMMAND ling3-chat-tools-test
|
| 129 |
+
${CMAKE_CURRENT_SOURCE_DIR}/tests/chat_tools_golden.json)
|
| 130 |
add_executable(ling3-router-test tests/router_test.cpp)
|
| 131 |
target_link_libraries(ling3-router-test PRIVATE ling3_core)
|
| 132 |
add_test(NAME ling3-router-test COMMAND ling3-router-test)
|
include/ling3/chat_protocol.h
CHANGED
|
@@ -3,14 +3,20 @@
|
|
| 3 |
#include <nlohmann/json.hpp>
|
| 4 |
#include <algorithm>
|
| 5 |
#include <cmath>
|
|
|
|
| 6 |
#include <cstdint>
|
|
|
|
| 7 |
#include <limits>
|
| 8 |
#include <stdexcept>
|
| 9 |
#include <string>
|
|
|
|
|
|
|
| 10 |
#include <vector>
|
| 11 |
|
| 12 |
namespace ling3::chat {
|
| 13 |
using Json = nlohmann::json;
|
|
|
|
|
|
|
| 14 |
inline constexpr const char * kModel = "mindnano-ling3-tiny";
|
| 15 |
struct Error : std::runtime_error {
|
| 16 |
int status;
|
|
@@ -36,6 +42,8 @@ struct Request {
|
|
| 36 |
bool enable_thinking = false, flow_ack = false;
|
| 37 |
std::uint32_t seed = 0;
|
| 38 |
std::vector<std::string> stops;
|
|
|
|
|
|
|
| 39 |
};
|
| 40 |
inline std::size_t OutputBudget(std::size_t prompt, std::size_t capacity,
|
| 41 |
std::size_t requested) {
|
|
@@ -44,14 +52,43 @@ inline std::size_t OutputBudget(std::size_t prompt, std::size_t capacity,
|
|
| 44 |
std::to_string(capacity) + " tokens, or is empty");
|
| 45 |
return std::min(requested, capacity - prompt);
|
| 46 |
}
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
auto fail = [](std::string m) { throw Error(400, "invalid_parameter", std::move(m)); };
|
| 49 |
if (!j.is_object()) fail("request must be a JSON object");
|
| 50 |
for (auto it = j.begin(); it != j.end(); ++it) {
|
| 51 |
const std::vector<std::string> supported {"model", "messages", "max_tokens",
|
| 52 |
"max_completion_tokens", "stream", "stream_options", "temperature", "top_p",
|
| 53 |
"seed", "stop", "n", "user", "cache_prompt", "top_k", "repeat_penalty",
|
| 54 |
-
"enable_thinking", "flow_control", "session_id", "reuse_generated_state"
|
|
|
|
| 55 |
if (std::find(supported.begin(), supported.end(), it.key()) == supported.end())
|
| 56 |
fail("unsupported parameter: " + it.key());
|
| 57 |
}
|
|
@@ -131,10 +168,12 @@ inline Request Parse(const Json & j) {
|
|
| 131 |
r.seed = j["seed"].get<std::uint32_t>();
|
| 132 |
}
|
| 133 |
if (j.contains("n") && (!j["n"].is_number_integer() || j["n"] != 1)) fail("only n=1 is supported");
|
|
|
|
|
|
|
| 134 |
if (j.contains("user") && !j["user"].is_string()) fail("user must be a string");
|
| 135 |
if (j.contains("user")) r.cache_user = j["user"].get<std::string>();
|
| 136 |
if (j.contains("stop") && !j["stop"].is_null()) {
|
| 137 |
-
const auto s = j["stop"].is_string() ?
|
| 138 |
if (!s.is_array() || s.size() > 4) fail("stop must be a string or at most 4 strings");
|
| 139 |
for (const auto & v : s) {
|
| 140 |
if (!v.is_string() || v.get_ref<const std::string &>().empty() ||
|
|
@@ -142,59 +181,133 @@ inline Request Parse(const Json & j) {
|
|
| 142 |
r.stops.push_back(v.get<std::string>());
|
| 143 |
}
|
| 144 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
if (!j.contains("messages") || !j["messages"].is_array() || j["messages"].empty())
|
| 146 |
fail("messages must be a non-empty array");
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
const auto role =
|
| 153 |
-
if (role != "system" && role != "user" && role != "assistant") fail("unsupported message role");
|
|
|
|
|
|
|
|
|
|
| 154 |
for (auto it = m.begin(); it != m.end(); ++it)
|
| 155 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
| 157 |
-
if (messages.back()
|
| 158 |
-
|
|
|
|
| 159 |
const std::string thinking = r.enable_thinking ? "on" : "off";
|
| 160 |
r.prompt = "<role>SYSTEM</role>";
|
| 161 |
std::size_t start = 0;
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
| 164 |
// The explicit API switch owns template control. Normalize conflicting
|
| 165 |
// legacy instructions rather than producing two contradictory modes.
|
| 166 |
for (const auto old : {std::string("detailed thinking off"), std::string("detailed thinking on")}) {
|
| 167 |
std::size_t at = 0;
|
| 168 |
-
while ((at =
|
| 169 |
const auto replacement = "detailed thinking " + thinking;
|
| 170 |
-
|
| 171 |
}
|
| 172 |
}
|
| 173 |
-
|
| 174 |
-
if (s.find("detailed thinking off") == std::string::npos && s.find("detailed thinking on") == std::string::npos)
|
| 175 |
-
r.prompt += "\ndetailed thinking " + thinking;
|
| 176 |
start = 1;
|
| 177 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
r.prompt += "<|role_end|>";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
for (std::size_t i = start; i < messages.size(); ++i) {
|
| 180 |
-
const auto
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
if (
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
| 195 |
}
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
r.prompt += r.enable_thinking ? "<role>ASSISTANT</role>\n<think>" : "<role>ASSISTANT</role>\n<think></think>";
|
| 200 |
return r;
|
|
@@ -245,4 +358,95 @@ public:
|
|
| 245 |
return TakeUtf8(utf8_, final);
|
| 246 |
}
|
| 247 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
} // namespace ling3::chat
|
|
|
|
| 3 |
#include <nlohmann/json.hpp>
|
| 4 |
#include <algorithm>
|
| 5 |
#include <cmath>
|
| 6 |
+
#include <cctype>
|
| 7 |
#include <cstdint>
|
| 8 |
+
#include <functional>
|
| 9 |
#include <limits>
|
| 10 |
#include <stdexcept>
|
| 11 |
#include <string>
|
| 12 |
+
#include <string_view>
|
| 13 |
+
#include <utility>
|
| 14 |
#include <vector>
|
| 15 |
|
| 16 |
namespace ling3::chat {
|
| 17 |
using Json = nlohmann::json;
|
| 18 |
+
// Request bodies keep key order: the template renders tools as the client sent them.
|
| 19 |
+
using OrderedJson = nlohmann::ordered_json;
|
| 20 |
inline constexpr const char * kModel = "mindnano-ling3-tiny";
|
| 21 |
struct Error : std::runtime_error {
|
| 22 |
int status;
|
|
|
|
| 42 |
bool enable_thinking = false, flow_ack = false;
|
| 43 |
std::uint32_t seed = 0;
|
| 44 |
std::vector<std::string> stops;
|
| 45 |
+
OrderedJson tools = OrderedJson::array();
|
| 46 |
+
bool tool_calls = false, parallel_tool_calls = true; // parse calls from the output
|
| 47 |
};
|
| 48 |
inline std::size_t OutputBudget(std::size_t prompt, std::size_t capacity,
|
| 49 |
std::size_t requested) {
|
|
|
|
| 52 |
std::to_string(capacity) + " tokens, or is empty");
|
| 53 |
return std::min(requested, capacity - prompt);
|
| 54 |
}
|
| 55 |
+
// The template's tojson filter is Python json.dumps(ensure_ascii=False):
|
| 56 |
+
// ", " and ": " separators, keys in the order the client sent them.
|
| 57 |
+
inline std::string TemplateJson(const OrderedJson & v) {
|
| 58 |
+
if (!v.is_object() && !v.is_array()) return v.dump();
|
| 59 |
+
std::string s = v.is_object() ? "{" : "[";
|
| 60 |
+
for (auto it = v.begin(); it != v.end(); ++it) {
|
| 61 |
+
if (it != v.begin()) s += ", ";
|
| 62 |
+
if (v.is_object()) s += OrderedJson(it.key()).dump() + ": ";
|
| 63 |
+
s += TemplateJson(*it);
|
| 64 |
+
}
|
| 65 |
+
return s + (v.is_object() ? "}" : "]");
|
| 66 |
+
}
|
| 67 |
+
inline constexpr const char * kToolsHeader = "# Tools\n\nYou may call one or more functions to assist "
|
| 68 |
+
"with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>";
|
| 69 |
+
inline constexpr const char * kToolsFooter = "\n</tools>\n\nIf none of the functions can be used, point it "
|
| 70 |
+
"out. If the given question lacks the parameters required by the function, also point it out.\nIf you need "
|
| 71 |
+
"to use a function, for each function call, output the function name and arguments within the following XML "
|
| 72 |
+
"format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n"
|
| 73 |
+
"<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\n";
|
| 74 |
+
struct ToolCall { std::string name; OrderedJson arguments = OrderedJson::object(); };
|
| 75 |
+
// String values are raw text and others JSON, exactly as the template renders history.
|
| 76 |
+
inline std::string RenderToolCall(const ToolCall & call) {
|
| 77 |
+
std::string s = "<tool_call>" + call.name + "\n";
|
| 78 |
+
for (auto it = call.arguments.begin(); it != call.arguments.end(); ++it)
|
| 79 |
+
s += "<arg_key>" + it.key() + "</arg_key>\n<arg_value>" +
|
| 80 |
+
(it->is_string() ? it->get<std::string>() : TemplateJson(*it)) + "</arg_value>";
|
| 81 |
+
return s + "\n</tool_call>";
|
| 82 |
+
}
|
| 83 |
+
inline Request Parse(const OrderedJson & j) {
|
| 84 |
auto fail = [](std::string m) { throw Error(400, "invalid_parameter", std::move(m)); };
|
| 85 |
if (!j.is_object()) fail("request must be a JSON object");
|
| 86 |
for (auto it = j.begin(); it != j.end(); ++it) {
|
| 87 |
const std::vector<std::string> supported {"model", "messages", "max_tokens",
|
| 88 |
"max_completion_tokens", "stream", "stream_options", "temperature", "top_p",
|
| 89 |
"seed", "stop", "n", "user", "cache_prompt", "top_k", "repeat_penalty",
|
| 90 |
+
"enable_thinking", "flow_control", "session_id", "reuse_generated_state",
|
| 91 |
+
"tools", "tool_choice", "parallel_tool_calls", "store"};
|
| 92 |
if (std::find(supported.begin(), supported.end(), it.key()) == supported.end())
|
| 93 |
fail("unsupported parameter: " + it.key());
|
| 94 |
}
|
|
|
|
| 168 |
r.seed = j["seed"].get<std::uint32_t>();
|
| 169 |
}
|
| 170 |
if (j.contains("n") && (!j["n"].is_number_integer() || j["n"] != 1)) fail("only n=1 is supported");
|
| 171 |
+
// Agent clients send OpenAI's store flag; this server never stores completions.
|
| 172 |
+
if (j.contains("store") && !j["store"].is_boolean()) fail("store must be boolean");
|
| 173 |
if (j.contains("user") && !j["user"].is_string()) fail("user must be a string");
|
| 174 |
if (j.contains("user")) r.cache_user = j["user"].get<std::string>();
|
| 175 |
if (j.contains("stop") && !j["stop"].is_null()) {
|
| 176 |
+
const auto s = j["stop"].is_string() ? OrderedJson::array({j["stop"]}) : j["stop"];
|
| 177 |
if (!s.is_array() || s.size() > 4) fail("stop must be a string or at most 4 strings");
|
| 178 |
for (const auto & v : s) {
|
| 179 |
if (!v.is_string() || v.get_ref<const std::string &>().empty() ||
|
|
|
|
| 181 |
r.stops.push_back(v.get<std::string>());
|
| 182 |
}
|
| 183 |
}
|
| 184 |
+
if (j.contains("tools") && !j["tools"].is_null()) {
|
| 185 |
+
if (!j["tools"].is_array()) fail("tools must be an array");
|
| 186 |
+
for (const auto & t : j["tools"])
|
| 187 |
+
if (!t.is_object() || !t.contains("type") || t["type"] != "function" || !t.contains("function") ||
|
| 188 |
+
!t["function"].is_object() || !t["function"].contains("name") || !t["function"]["name"].is_string() ||
|
| 189 |
+
t["function"]["name"].get_ref<const std::string &>().empty())
|
| 190 |
+
fail("tools must be named function tools");
|
| 191 |
+
r.tools = j["tools"];
|
| 192 |
+
}
|
| 193 |
+
// Calls are parsed after generation, so only auto and none can be honored.
|
| 194 |
+
const bool no_tool_choice = !j.contains("tool_choice") || j["tool_choice"].is_null();
|
| 195 |
+
if (!no_tool_choice && j["tool_choice"] != "auto" && j["tool_choice"] != "none")
|
| 196 |
+
fail("tool_choice must be auto or none");
|
| 197 |
+
if (j.contains("parallel_tool_calls")) {
|
| 198 |
+
if (!j["parallel_tool_calls"].is_boolean()) fail("parallel_tool_calls must be boolean");
|
| 199 |
+
r.parallel_tool_calls = j["parallel_tool_calls"].get<bool>();
|
| 200 |
+
}
|
| 201 |
+
r.tool_calls = !r.tools.empty() && (no_tool_choice || j["tool_choice"] == "auto");
|
| 202 |
if (!j.contains("messages") || !j["messages"].is_array() || j["messages"].empty())
|
| 203 |
fail("messages must be a non-empty array");
|
| 204 |
+
struct Message { std::string role, content, reasoning; std::vector<ToolCall> calls; };
|
| 205 |
+
std::vector<Message> messages;
|
| 206 |
+
for (const auto & m : j["messages"]) {
|
| 207 |
+
if (!m.is_object() || !m.contains("role") || !m["role"].is_string()) fail("messages require a string role");
|
| 208 |
+
Message message {m["role"].get<std::string>()};
|
| 209 |
+
const auto & role = message.role;
|
| 210 |
+
if (role != "system" && role != "user" && role != "assistant" && role != "tool") fail("unsupported message role");
|
| 211 |
+
std::vector<std::string> fields {"role", "content"};
|
| 212 |
+
if (role == "assistant") fields.insert(fields.end(), {"tool_calls", "reasoning_content"});
|
| 213 |
+
if (role == "tool") fields.insert(fields.end(), {"tool_call_id", "name"});
|
| 214 |
for (auto it = m.begin(); it != m.end(); ++it)
|
| 215 |
+
if (std::find(fields.begin(), fields.end(), it.key()) == fields.end()) fail("unsupported message field: " + it.key());
|
| 216 |
+
// Only an assistant turn may omit content. Text parts are joined with
|
| 217 |
+
// newlines, as vLLM flattens them for string-content templates.
|
| 218 |
+
if (!m.contains("content") || m["content"].is_null()) {
|
| 219 |
+
if (role != "assistant") fail("messages require string or text-part content");
|
| 220 |
+
} else if (m["content"].is_string()) message.content = m["content"].get<std::string>();
|
| 221 |
+
else if (m["content"].is_array()) {
|
| 222 |
+
for (std::size_t k = 0; k < m["content"].size(); ++k) {
|
| 223 |
+
const auto & part = m["content"][k];
|
| 224 |
+
if (!part.is_object() || !part.contains("type") || part["type"] != "text" ||
|
| 225 |
+
!part.contains("text") || !part["text"].is_string()) fail("content parts must be text");
|
| 226 |
+
message.content += (k ? "\n" : "") + part["text"].get<std::string>();
|
| 227 |
+
}
|
| 228 |
+
} else fail("messages require string or text-part content");
|
| 229 |
+
if (role == "assistant" && m.contains("reasoning_content") && !m["reasoning_content"].is_null()) {
|
| 230 |
+
if (!m["reasoning_content"].is_string()) fail("reasoning_content must be a string");
|
| 231 |
+
message.reasoning = m["reasoning_content"].get<std::string>();
|
| 232 |
+
}
|
| 233 |
+
if (role == "assistant" && m.contains("tool_calls") && !m["tool_calls"].is_null()) {
|
| 234 |
+
if (!m["tool_calls"].is_array()) fail("tool_calls must be an array");
|
| 235 |
+
for (const auto & c : m["tool_calls"]) {
|
| 236 |
+
if (!c.is_object() || !c.contains("function") || !c["function"].is_object() ||
|
| 237 |
+
!c["function"].contains("name") || !c["function"]["name"].is_string())
|
| 238 |
+
fail("tool_calls require a function name");
|
| 239 |
+
ToolCall call {c["function"]["name"].get<std::string>()};
|
| 240 |
+
// OpenAI clients send arguments as a JSON string; the template iterates a mapping.
|
| 241 |
+
if (c["function"].contains("arguments")) {
|
| 242 |
+
const auto & a = c["function"]["arguments"];
|
| 243 |
+
call.arguments = a.is_string() ? OrderedJson::parse(a.get<std::string>(), nullptr, false) : a;
|
| 244 |
+
if (!call.arguments.is_object()) fail("tool call arguments must be a JSON object");
|
| 245 |
+
}
|
| 246 |
+
message.calls.push_back(std::move(call));
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
messages.push_back(std::move(message));
|
| 250 |
}
|
| 251 |
+
if (messages.back().role != "user" && messages.back().role != "tool")
|
| 252 |
+
fail("last message must be a user or tool message");
|
| 253 |
+
// Bailing V3 template (chat_template.jinja). No hidden, server-owned conversation.
|
| 254 |
const std::string thinking = r.enable_thinking ? "on" : "off";
|
| 255 |
r.prompt = "<role>SYSTEM</role>";
|
| 256 |
std::size_t start = 0;
|
| 257 |
+
std::string system;
|
| 258 |
+
bool system_mode = false;
|
| 259 |
+
if (messages.front().role == "system") {
|
| 260 |
+
system = messages.front().content;
|
| 261 |
// The explicit API switch owns template control. Normalize conflicting
|
| 262 |
// legacy instructions rather than producing two contradictory modes.
|
| 263 |
for (const auto old : {std::string("detailed thinking off"), std::string("detailed thinking on")}) {
|
| 264 |
std::size_t at = 0;
|
| 265 |
+
while ((at = system.find(old, at)) != std::string::npos) {
|
| 266 |
const auto replacement = "detailed thinking " + thinking;
|
| 267 |
+
system.replace(at, old.size(), replacement); at += replacement.size();
|
| 268 |
}
|
| 269 |
}
|
| 270 |
+
system_mode = system.find("detailed thinking " + thinking) != std::string::npos;
|
|
|
|
|
|
|
| 271 |
start = 1;
|
| 272 |
+
}
|
| 273 |
+
if (!r.tools.empty()) {
|
| 274 |
+
if (start) r.prompt += system + "\n";
|
| 275 |
+
r.prompt += kToolsHeader;
|
| 276 |
+
for (const auto & tool : r.tools) r.prompt += "\n" + TemplateJson(tool);
|
| 277 |
+
r.prompt += kToolsFooter;
|
| 278 |
+
if (!system_mode) r.prompt += "detailed thinking " + thinking;
|
| 279 |
+
} else if (start) r.prompt += system + (system_mode ? "" : "\ndetailed thinking " + thinking);
|
| 280 |
+
else r.prompt += "detailed thinking " + thinking;
|
| 281 |
r.prompt += "<|role_end|>";
|
| 282 |
+
auto strip = [](std::string s, bool left, bool right) {
|
| 283 |
+
if (right) s.erase(s.find_last_not_of('\n') + 1);
|
| 284 |
+
if (left) s.erase(0, std::min(s.find_first_not_of('\n'), s.size()));
|
| 285 |
+
return s;
|
| 286 |
+
};
|
| 287 |
for (std::size_t i = start; i < messages.size(); ++i) {
|
| 288 |
+
const auto & m = messages[i];
|
| 289 |
+
if (m.role == "tool") {
|
| 290 |
+
if (i == 0 || messages[i - 1].role != "tool") r.prompt += "<role>OBSERVATION</role>";
|
| 291 |
+
r.prompt += "\n<tool_response>\n" + m.content + "\n</tool_response>";
|
| 292 |
+
if (i + 1 == messages.size() || messages[i + 1].role != "tool") r.prompt += "<|role_end|>";
|
| 293 |
+
continue;
|
| 294 |
+
}
|
| 295 |
+
if (m.role != "assistant") {
|
| 296 |
+
r.prompt += (m.role == "user" ? "<role>HUMAN</role>" : "<role>SYSTEM</role>") + m.content + "<|role_end|>";
|
| 297 |
+
continue;
|
| 298 |
+
}
|
| 299 |
+
auto content = m.content, reasoning = m.reasoning;
|
| 300 |
+
if (reasoning.empty() && content.find("</think>") != std::string::npos) {
|
| 301 |
+
reasoning = strip(content.substr(0, content.find("</think>")), false, true);
|
| 302 |
+
if (const auto open = reasoning.rfind("<think>"); open != std::string::npos) reasoning.erase(0, open + 7);
|
| 303 |
+
reasoning = strip(reasoning, true, false);
|
| 304 |
+
content = strip(content.substr(content.rfind("</think>") + 8), true, false);
|
| 305 |
}
|
| 306 |
+
r.prompt += reasoning.empty() ? "<role>ASSISTANT</role>\n<think></think>" + content
|
| 307 |
+
: "<role>ASSISTANT</role>\n<think>" + strip(reasoning, true, true) + "</think>" + strip(content, true, false);
|
| 308 |
+
for (std::size_t k = 0; k < m.calls.size(); ++k)
|
| 309 |
+
r.prompt += (k || !content.empty() ? "\n" : "") + RenderToolCall(m.calls[k]);
|
| 310 |
+
r.prompt += "<|role_end|>";
|
| 311 |
}
|
| 312 |
r.prompt += r.enable_thinking ? "<role>ASSISTANT</role>\n<think>" : "<role>ASSISTANT</role>\n<think></think>";
|
| 313 |
return r;
|
|
|
|
| 358 |
return TakeUtf8(utf8_, final);
|
| 359 |
}
|
| 360 |
};
|
| 361 |
+
// Declared string arguments stay raw text; other values parse as JSON and fall
|
| 362 |
+
// back to text, as llama.cpp's ling3 parser types them from the tool schema.
|
| 363 |
+
inline OrderedJson ArgumentValue(const OrderedJson & function, const std::string & key, const std::string & value) {
|
| 364 |
+
bool text = false, other = false;
|
| 365 |
+
std::function<void(const OrderedJson &)> scan = [&](const OrderedJson & schema) {
|
| 366 |
+
if (!schema.is_object()) return;
|
| 367 |
+
if (schema.contains("type"))
|
| 368 |
+
for (const auto & type : schema["type"].is_array() ? schema["type"] : OrderedJson::array({schema["type"]}))
|
| 369 |
+
(type == "string" ? text : other) = true;
|
| 370 |
+
if (schema.contains("enum") && schema["enum"].is_array())
|
| 371 |
+
for (const auto & choice : schema["enum"]) (choice.is_string() ? text : other) = true;
|
| 372 |
+
for (const auto * union_key : {"anyOf", "oneOf"})
|
| 373 |
+
if (schema.contains(union_key) && schema[union_key].is_array())
|
| 374 |
+
for (const auto & branch : schema[union_key]) scan(branch);
|
| 375 |
+
};
|
| 376 |
+
if (function.contains("parameters") && function["parameters"].is_object() &&
|
| 377 |
+
function["parameters"].contains("properties") && function["parameters"]["properties"].is_object() &&
|
| 378 |
+
function["parameters"]["properties"].contains(key)) scan(function["parameters"]["properties"][key]);
|
| 379 |
+
if (text && !other) return value;
|
| 380 |
+
auto parsed = OrderedJson::parse(value, nullptr, false);
|
| 381 |
+
return parsed.is_discarded() ? OrderedJson(value) : parsed;
|
| 382 |
+
}
|
| 383 |
+
// Bailing V3 calls: <tool_call>NAME\n<arg_key>K</arg_key>\n<arg_value>V</arg_value>...\n</tool_call>.
|
| 384 |
+
// Whitespace between the parts is optional and only declared tools are calls.
|
| 385 |
+
// Text after the last call is returned as trailing content, as llama.cpp does.
|
| 386 |
+
struct ToolCalls { bool ok = false; std::vector<ToolCall> calls; std::string trailing; };
|
| 387 |
+
inline ToolCalls ParseToolCalls(std::string_view text, const OrderedJson & tools, bool parallel) {
|
| 388 |
+
ToolCalls result;
|
| 389 |
+
auto space = [&] { while (!text.empty() && std::isspace(static_cast<unsigned char>(text.front()))) text.remove_prefix(1); };
|
| 390 |
+
auto take = [&](std::string_view tag) { if (!text.starts_with(tag)) return false; text.remove_prefix(tag.size()); return true; };
|
| 391 |
+
auto until = [&](std::string_view tag, std::string & out) {
|
| 392 |
+
const auto at = text.find(tag);
|
| 393 |
+
if (at == std::string_view::npos) return false;
|
| 394 |
+
out = text.substr(0, at); text.remove_prefix(at + tag.size()); return true;
|
| 395 |
+
};
|
| 396 |
+
while (space(), take("<tool_call>")) {
|
| 397 |
+
std::size_t n = 0;
|
| 398 |
+
while (n < text.size() && !std::isspace(static_cast<unsigned char>(text[n])) && text[n] != '<') ++n;
|
| 399 |
+
ToolCall call {std::string(text.substr(0, n))};
|
| 400 |
+
text.remove_prefix(n);
|
| 401 |
+
const OrderedJson * function = nullptr;
|
| 402 |
+
for (const auto & tool : tools) if (tool["function"]["name"] == call.name) function = &tool["function"];
|
| 403 |
+
if (!function) return {};
|
| 404 |
+
while (space(), !take("</tool_call>")) {
|
| 405 |
+
std::string key, value;
|
| 406 |
+
if (!take("<arg_key>") || !until("</arg_key>", key)) return {};
|
| 407 |
+
space();
|
| 408 |
+
if (!take("<arg_value>") || !until("</arg_value>", value)) return {};
|
| 409 |
+
call.arguments[key] = ArgumentValue(*function, key, value);
|
| 410 |
+
}
|
| 411 |
+
if (parallel || result.calls.empty()) result.calls.push_back(std::move(call));
|
| 412 |
+
}
|
| 413 |
+
if (result.calls.empty()) return {};
|
| 414 |
+
result.ok = true;
|
| 415 |
+
result.trailing = text;
|
| 416 |
+
return result;
|
| 417 |
+
}
|
| 418 |
+
// Holds back generated text from the first <tool_call> so a call never streams
|
| 419 |
+
// as content; with thinking on this also ends the reasoning, as in llama.cpp.
|
| 420 |
+
// The newline the template writes before a call is held too and dropped.
|
| 421 |
+
class ToolCallFilter {
|
| 422 |
+
std::string pending_, captured_;
|
| 423 |
+
bool enabled_, newline_ = false;
|
| 424 |
+
public:
|
| 425 |
+
explicit ToolCallFilter(bool enabled) : enabled_(enabled) {}
|
| 426 |
+
const std::string & captured() const { return captured_; }
|
| 427 |
+
// The exact held text, for output that does not parse as calls.
|
| 428 |
+
std::string Released() const { return (newline_ ? "\n" : "") + captured_; }
|
| 429 |
+
std::string Push(std::string_view piece, bool final = false) {
|
| 430 |
+
if (!enabled_) return std::string(piece);
|
| 431 |
+
if (!captured_.empty()) { captured_ += piece; return {}; }
|
| 432 |
+
pending_ += piece;
|
| 433 |
+
constexpr std::string_view tag = "<tool_call>";
|
| 434 |
+
if (const auto at = pending_.find(tag); at != std::string::npos) {
|
| 435 |
+
captured_ = pending_.substr(at);
|
| 436 |
+
pending_.resize(at);
|
| 437 |
+
newline_ = !pending_.empty() && pending_.back() == '\n';
|
| 438 |
+
if (newline_) pending_.pop_back();
|
| 439 |
+
return std::exchange(pending_, {});
|
| 440 |
+
}
|
| 441 |
+
std::size_t keep = 0;
|
| 442 |
+
if (!final) {
|
| 443 |
+
for (std::size_t k = std::min(tag.size() - 1, pending_.size()); k && !keep; --k)
|
| 444 |
+
if (pending_.ends_with(tag.substr(0, k))) keep = k;
|
| 445 |
+
if (keep < pending_.size() && pending_[pending_.size() - keep - 1] == '\n') ++keep;
|
| 446 |
+
}
|
| 447 |
+
auto out = pending_.substr(0, pending_.size() - keep);
|
| 448 |
+
pending_.erase(0, pending_.size() - keep);
|
| 449 |
+
return out;
|
| 450 |
+
}
|
| 451 |
+
};
|
| 452 |
} // namespace ling3::chat
|
src/portable_main.cpp
CHANGED
|
@@ -166,7 +166,7 @@ Json CheckDevice() {
|
|
| 166 |
return report;
|
| 167 |
}
|
| 168 |
|
| 169 |
-
struct Result { std::string text, reason = "length"; Json usage, metrics; };
|
| 170 |
class Engine {
|
| 171 |
ModelPackage package_;
|
| 172 |
std::size_t context_;
|
|
@@ -457,6 +457,7 @@ public:
|
|
| 457 |
const bool retain_generated = r.cache_prompt && r.reuse_generated_state;
|
| 458 |
std::vector<std::uint32_t> evaluated_tokens = retain_generated ? tokens : std::vector<std::uint32_t>{};
|
| 459 |
TextFilter filter(r.stops);
|
|
|
|
| 460 |
std::mt19937 random(r.seed);
|
| 461 |
std::vector<bool> seen(logits_.size(), false);
|
| 462 |
for (auto token : tokens) if (token < seen.size()) seen[token] = true;
|
|
@@ -484,7 +485,7 @@ public:
|
|
| 484 |
if (!generated) { first = last; first_ms = Ms(start, first); }
|
| 485 |
++generated; // includes EOS and stop tokens, like completion usage.
|
| 486 |
if (token == package_.header().eos_token) { result.reason = "stop"; break; }
|
| 487 |
-
if (!output(filter.Push(tokenizer_.Piece(token)))) break;
|
| 488 |
if (filter.stopped) { result.reason = "stop"; break; }
|
| 489 |
if (i + 1 < output_budget) {
|
| 490 |
if (!flow_.Wait([&] { return !quitting && connected(); })) { aborted(); break; }
|
|
@@ -493,7 +494,18 @@ public:
|
|
| 493 |
++evaluated_steps_;
|
| 494 |
}
|
| 495 |
}
|
| 496 |
-
if (result.reason != "canceled") output(filter.Push({}, true));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
if (result.reason != "canceled" && !alive()) aborted();
|
| 498 |
bool continuation_stored = false, continuation_skipped_budget = false;
|
| 499 |
double continuation_save_ms = 0;
|
|
@@ -536,6 +548,7 @@ public:
|
|
| 536 |
{"queue_ms", 0}, {"cached_tokens", cached_tokens}, {"warmed", true},
|
| 537 |
{"cache_status", cache_status}, {"cache_restore_ms", cache_restore_ms},
|
| 538 |
{"cache_save_ms", cache_save_ms}, {"prefix_cache_mib", prefix_cache_.bytes() / 1048576.0}};
|
|
|
|
| 539 |
result.metrics["context_length"] = context_;
|
| 540 |
result.metrics["reuse_generated_state"] = r.reuse_generated_state;
|
| 541 |
result.metrics["generated_state_lineage"] = prefix_cache_.generated_lineage;
|
|
@@ -743,7 +756,7 @@ void Handle(int fd, Engine & engine, const Json & device) {
|
|
| 743 |
}
|
| 744 |
if (http.method != "POST" || http.path != "/v1/chat/completions")
|
| 745 |
throw Error(404, "not_found", "endpoint not found");
|
| 746 |
-
const auto request = Parse(
|
| 747 |
const auto id = Id(); const auto created = std::time(nullptr);
|
| 748 |
auto chunk = [&](Json delta, Json finish = nullptr) {
|
| 749 |
Json out {{"id", id}, {"object", "chat.completion.chunk"}, {"created", created}, {"model", kModel},
|
|
@@ -764,14 +777,27 @@ void Handle(int fd, Engine & engine, const Json & device) {
|
|
| 764 |
if (result.reason == "canceled") {
|
| 765 |
error(Error(409, "request_canceled", "generation canceled; partial text is not a completed reply")); return;
|
| 766 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
if (request.stream) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
chunk(Json::object(), result.reason);
|
| 769 |
Json final {{"id", id}, {"object", "chat.completion.chunk"}, {"created", created}, {"model", kModel},
|
| 770 |
{"choices", Json::array()}, {"mindnano_metrics", result.metrics}};
|
| 771 |
if (request.include_usage) final["usage"] = result.usage;
|
| 772 |
Send(fd, "data: " + final.dump() + "\n\ndata: [DONE]\n\n");
|
| 773 |
} else Response(fd, 200, {{"id", id}, {"object", "chat.completion"}, {"created", created}, {"model", kModel},
|
| 774 |
-
{"choices", Json::array({{{"index", 0}, {"message",
|
| 775 |
{"finish_reason", result.reason}, {"logprobs", nullptr}}})},
|
| 776 |
{"usage", result.usage}, {"mindnano_metrics", result.metrics}});
|
| 777 |
} catch (const Error & e) { error(e); }
|
|
|
|
| 166 |
return report;
|
| 167 |
}
|
| 168 |
|
| 169 |
+
struct Result { std::string text, reason = "length"; Json usage, metrics; std::vector<ToolCall> tool_calls; };
|
| 170 |
class Engine {
|
| 171 |
ModelPackage package_;
|
| 172 |
std::size_t context_;
|
|
|
|
| 457 |
const bool retain_generated = r.cache_prompt && r.reuse_generated_state;
|
| 458 |
std::vector<std::uint32_t> evaluated_tokens = retain_generated ? tokens : std::vector<std::uint32_t>{};
|
| 459 |
TextFilter filter(r.stops);
|
| 460 |
+
ToolCallFilter calls(r.tool_calls);
|
| 461 |
std::mt19937 random(r.seed);
|
| 462 |
std::vector<bool> seen(logits_.size(), false);
|
| 463 |
for (auto token : tokens) if (token < seen.size()) seen[token] = true;
|
|
|
|
| 485 |
if (!generated) { first = last; first_ms = Ms(start, first); }
|
| 486 |
++generated; // includes EOS and stop tokens, like completion usage.
|
| 487 |
if (token == package_.header().eos_token) { result.reason = "stop"; break; }
|
| 488 |
+
if (!output(calls.Push(filter.Push(tokenizer_.Piece(token))))) break;
|
| 489 |
if (filter.stopped) { result.reason = "stop"; break; }
|
| 490 |
if (i + 1 < output_budget) {
|
| 491 |
if (!flow_.Wait([&] { return !quitting && connected(); })) { aborted(); break; }
|
|
|
|
| 494 |
++evaluated_steps_;
|
| 495 |
}
|
| 496 |
}
|
| 497 |
+
if (result.reason != "canceled") output(calls.Push(filter.Push({}, true), true));
|
| 498 |
+
// Held call text is parsed only once generation is complete; output that
|
| 499 |
+
// does not parse is released unchanged as content.
|
| 500 |
+
if (result.reason != "canceled" && !calls.captured().empty()) {
|
| 501 |
+
auto parsed = ParseToolCalls(calls.captured(), r.tools, r.parallel_tool_calls);
|
| 502 |
+
if (!parsed.ok) output(calls.Released());
|
| 503 |
+
else {
|
| 504 |
+
result.tool_calls = std::move(parsed.calls);
|
| 505 |
+
output(parsed.trailing);
|
| 506 |
+
if (result.reason == "stop") result.reason = "tool_calls";
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
if (result.reason != "canceled" && !alive()) aborted();
|
| 510 |
bool continuation_stored = false, continuation_skipped_budget = false;
|
| 511 |
double continuation_save_ms = 0;
|
|
|
|
| 548 |
{"queue_ms", 0}, {"cached_tokens", cached_tokens}, {"warmed", true},
|
| 549 |
{"cache_status", cache_status}, {"cache_restore_ms", cache_restore_ms},
|
| 550 |
{"cache_save_ms", cache_save_ms}, {"prefix_cache_mib", prefix_cache_.bytes() / 1048576.0}};
|
| 551 |
+
result.metrics["tool_calls"] = result.tool_calls.size();
|
| 552 |
result.metrics["context_length"] = context_;
|
| 553 |
result.metrics["reuse_generated_state"] = r.reuse_generated_state;
|
| 554 |
result.metrics["generated_state_lineage"] = prefix_cache_.generated_lineage;
|
|
|
|
| 756 |
}
|
| 757 |
if (http.method != "POST" || http.path != "/v1/chat/completions")
|
| 758 |
throw Error(404, "not_found", "endpoint not found");
|
| 759 |
+
const auto request = Parse(OrderedJson::parse(http.body));
|
| 760 |
const auto id = Id(); const auto created = std::time(nullptr);
|
| 761 |
auto chunk = [&](Json delta, Json finish = nullptr) {
|
| 762 |
Json out {{"id", id}, {"object", "chat.completion.chunk"}, {"created", created}, {"model", kModel},
|
|
|
|
| 777 |
if (result.reason == "canceled") {
|
| 778 |
error(Error(409, "request_canceled", "generation canceled; partial text is not a completed reply")); return;
|
| 779 |
}
|
| 780 |
+
Json tool_calls = Json::array();
|
| 781 |
+
for (std::size_t i = 0; i < result.tool_calls.size(); ++i)
|
| 782 |
+
tool_calls.push_back({{"id", "call_" + id.substr(9) + "-" + std::to_string(i)}, {"type", "function"},
|
| 783 |
+
{"function", {{"name", result.tool_calls[i].name}, {"arguments", result.tool_calls[i].arguments.dump()}}}});
|
| 784 |
+
Json message {{"role", "assistant"}, {"content", result.text}};
|
| 785 |
+
if (!tool_calls.empty()) {
|
| 786 |
+
message["tool_calls"] = tool_calls;
|
| 787 |
+
if (result.text.empty()) message["content"] = nullptr;
|
| 788 |
+
}
|
| 789 |
if (request.stream) {
|
| 790 |
+
for (std::size_t i = 0; i < tool_calls.size(); ++i) {
|
| 791 |
+
tool_calls[i]["index"] = i;
|
| 792 |
+
chunk({{"tool_calls", Json::array({tool_calls[i]})}});
|
| 793 |
+
}
|
| 794 |
chunk(Json::object(), result.reason);
|
| 795 |
Json final {{"id", id}, {"object", "chat.completion.chunk"}, {"created", created}, {"model", kModel},
|
| 796 |
{"choices", Json::array()}, {"mindnano_metrics", result.metrics}};
|
| 797 |
if (request.include_usage) final["usage"] = result.usage;
|
| 798 |
Send(fd, "data: " + final.dump() + "\n\ndata: [DONE]\n\n");
|
| 799 |
} else Response(fd, 200, {{"id", id}, {"object", "chat.completion"}, {"created", created}, {"model", kModel},
|
| 800 |
+
{"choices", Json::array({{{"index", 0}, {"message", message},
|
| 801 |
{"finish_reason", result.reason}, {"logprobs", nullptr}}})},
|
| 802 |
{"usage", result.usage}, {"mindnano_metrics", result.metrics}});
|
| 803 |
} catch (const Error & e) { error(e); }
|
tests/chat_protocol_test.cpp
CHANGED
|
@@ -33,7 +33,7 @@ int main() {
|
|
| 33 |
"<role>HUMAN</role>你好<|role_end|><role>ASSISTANT</role>\n<think></think>你好!<|role_end|>"
|
| 34 |
"<role>HUMAN</role>继续<|role_end|><role>ASSISTANT</role>\n<think></think>", "template mismatch");
|
| 35 |
for (const auto & mutation : std::vector<Json>{
|
| 36 |
-
{{"tools",
|
| 37 |
{{"max_tokens", -1}}, {{"max_completion_tokens", 1.5}}, {{"n", 2}},
|
| 38 |
{{"model", "other"}}, {{"messages", Json::array()}}, {{"stop", ""}}, {{"cache_prompt", "false"}}}) {
|
| 39 |
auto bad = body; bad.update(mutation); bool failed = false;
|
|
|
|
| 33 |
"<role>HUMAN</role>你好<|role_end|><role>ASSISTANT</role>\n<think></think>你好!<|role_end|>"
|
| 34 |
"<role>HUMAN</role>继续<|role_end|><role>ASSISTANT</role>\n<think></think>", "template mismatch");
|
| 35 |
for (const auto & mutation : std::vector<Json>{
|
| 36 |
+
{{"tools", "read_file"}}, {{"tool_choice", "required"}}, {{"temperature", -1}}, {{"stream", "true"}},
|
| 37 |
{{"max_tokens", -1}}, {{"max_completion_tokens", 1.5}}, {{"n", 2}},
|
| 38 |
{{"model", "other"}}, {{"messages", Json::array()}}, {{"stop", ""}}, {{"cache_prompt", "false"}}}) {
|
| 39 |
auto bad = body; bad.update(mutation); bool failed = false;
|
tests/chat_tools_golden.json
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "no_tools_history",
|
| 4 |
+
"request": {
|
| 5 |
+
"model": "mindnano-ling3-tiny",
|
| 6 |
+
"messages": [
|
| 7 |
+
{
|
| 8 |
+
"role": "system",
|
| 9 |
+
"content": "请用中文回答"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"role": "user",
|
| 13 |
+
"content": "你好"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"role": "assistant",
|
| 17 |
+
"content": "你好!"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"role": "user",
|
| 21 |
+
"content": "继续"
|
| 22 |
+
}
|
| 23 |
+
],
|
| 24 |
+
"enable_thinking": false
|
| 25 |
+
},
|
| 26 |
+
"prompt": "<role>SYSTEM</role>请用中文回答\ndetailed thinking off<|role_end|><role>HUMAN</role>你好<|role_end|><role>ASSISTANT</role>\n<think></think>你好!<|role_end|><role>HUMAN</role>继续<|role_end|><role>ASSISTANT</role>\n<think></think>"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"name": "tools_no_system",
|
| 30 |
+
"request": {
|
| 31 |
+
"model": "mindnano-ling3-tiny",
|
| 32 |
+
"messages": [
|
| 33 |
+
{
|
| 34 |
+
"role": "user",
|
| 35 |
+
"content": "Reply with OK."
|
| 36 |
+
}
|
| 37 |
+
],
|
| 38 |
+
"enable_thinking": false,
|
| 39 |
+
"tools": [
|
| 40 |
+
{
|
| 41 |
+
"type": "function",
|
| 42 |
+
"function": {
|
| 43 |
+
"name": "read_file",
|
| 44 |
+
"description": "Read a file's contents.",
|
| 45 |
+
"parameters": {
|
| 46 |
+
"type": "object",
|
| 47 |
+
"properties": {
|
| 48 |
+
"path": {
|
| 49 |
+
"type": "string"
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"required": [
|
| 53 |
+
"path"
|
| 54 |
+
]
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
"prompt": "<role>SYSTEM</role># Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file's contents.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\ndetailed thinking off<|role_end|><role>HUMAN</role>Reply with OK.<|role_end|><role>ASSISTANT</role>\n<think></think>"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"name": "tools_system",
|
| 64 |
+
"request": {
|
| 65 |
+
"model": "mindnano-ling3-tiny",
|
| 66 |
+
"messages": [
|
| 67 |
+
{
|
| 68 |
+
"role": "system",
|
| 69 |
+
"content": "You are terse."
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"role": "user",
|
| 73 |
+
"content": "Use the tools to read /tmp/scratch.txt, then patch it to change 'foo' to 'bar'."
|
| 74 |
+
}
|
| 75 |
+
],
|
| 76 |
+
"enable_thinking": false,
|
| 77 |
+
"tools": [
|
| 78 |
+
{
|
| 79 |
+
"type": "function",
|
| 80 |
+
"function": {
|
| 81 |
+
"name": "read_file",
|
| 82 |
+
"description": "Read a file's contents.",
|
| 83 |
+
"parameters": {
|
| 84 |
+
"type": "object",
|
| 85 |
+
"properties": {
|
| 86 |
+
"path": {
|
| 87 |
+
"type": "string"
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
"required": [
|
| 91 |
+
"path"
|
| 92 |
+
]
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"type": "function",
|
| 98 |
+
"function": {
|
| 99 |
+
"name": "patch",
|
| 100 |
+
"description": "Apply a small text patch to a file.",
|
| 101 |
+
"parameters": {
|
| 102 |
+
"type": "object",
|
| 103 |
+
"properties": {
|
| 104 |
+
"path": {
|
| 105 |
+
"type": "string"
|
| 106 |
+
},
|
| 107 |
+
"find": {
|
| 108 |
+
"type": "string"
|
| 109 |
+
},
|
| 110 |
+
"replace": {
|
| 111 |
+
"type": "string"
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"required": [
|
| 115 |
+
"path",
|
| 116 |
+
"find",
|
| 117 |
+
"replace"
|
| 118 |
+
]
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
"prompt": "<role>SYSTEM</role>You are terse.\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file's contents.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}\n{\"type\": \"function\", \"function\": {\"name\": \"patch\", \"description\": \"Apply a small text patch to a file.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"find\": {\"type\": \"string\"}, \"replace\": {\"type\": \"string\"}}, \"required\": [\"path\", \"find\", \"replace\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\ndetailed thinking off<|role_end|><role>HUMAN</role>Use the tools to read /tmp/scratch.txt, then patch it to change 'foo' to 'bar'.<|role_end|><role>ASSISTANT</role>\n<think></think>"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"name": "tools_system_thinking_phrase",
|
| 128 |
+
"request": {
|
| 129 |
+
"model": "mindnano-ling3-tiny",
|
| 130 |
+
"messages": [
|
| 131 |
+
{
|
| 132 |
+
"role": "system",
|
| 133 |
+
"content": "detailed thinking off\nBe brief."
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"role": "user",
|
| 137 |
+
"content": "Read /tmp/a."
|
| 138 |
+
}
|
| 139 |
+
],
|
| 140 |
+
"enable_thinking": false,
|
| 141 |
+
"tools": [
|
| 142 |
+
{
|
| 143 |
+
"type": "function",
|
| 144 |
+
"function": {
|
| 145 |
+
"name": "read_file",
|
| 146 |
+
"description": "Read a file's contents.",
|
| 147 |
+
"parameters": {
|
| 148 |
+
"type": "object",
|
| 149 |
+
"properties": {
|
| 150 |
+
"path": {
|
| 151 |
+
"type": "string"
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
"required": [
|
| 155 |
+
"path"
|
| 156 |
+
]
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
]
|
| 161 |
+
},
|
| 162 |
+
"prompt": "<role>SYSTEM</role>detailed thinking off\nBe brief.\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file's contents.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\n<|role_end|><role>HUMAN</role>Read /tmp/a.<|role_end|><role>ASSISTANT</role>\n<think></think>"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"name": "tool_loop",
|
| 166 |
+
"request": {
|
| 167 |
+
"model": "mindnano-ling3-tiny",
|
| 168 |
+
"messages": [
|
| 169 |
+
{
|
| 170 |
+
"role": "system",
|
| 171 |
+
"content": "You are terse."
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"role": "user",
|
| 175 |
+
"content": "Fix /tmp/a.txt."
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"role": "assistant",
|
| 179 |
+
"content": null,
|
| 180 |
+
"tool_calls": [
|
| 181 |
+
{
|
| 182 |
+
"id": "call_1",
|
| 183 |
+
"type": "function",
|
| 184 |
+
"function": {
|
| 185 |
+
"name": "read_file",
|
| 186 |
+
"arguments": "{\"path\": \"/tmp/a.txt\"}"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"id": "call_2",
|
| 191 |
+
"type": "function",
|
| 192 |
+
"function": {
|
| 193 |
+
"name": "run",
|
| 194 |
+
"arguments": "{\"command\": \"ls -l\", \"timeout\": 30, \"env\": {\"A\": \"1\", \"B\": [1, 2.5]}, \"dry\": false}"
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
]
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"role": "tool",
|
| 201 |
+
"tool_call_id": "call_1",
|
| 202 |
+
"content": "foo\n"
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"role": "tool",
|
| 206 |
+
"tool_call_id": "call_2",
|
| 207 |
+
"content": "total 0"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"role": "assistant",
|
| 211 |
+
"content": "Read it.",
|
| 212 |
+
"tool_calls": [
|
| 213 |
+
{
|
| 214 |
+
"id": "call_3",
|
| 215 |
+
"type": "function",
|
| 216 |
+
"function": {
|
| 217 |
+
"name": "patch",
|
| 218 |
+
"arguments": "{\"path\": \"/tmp/a.txt\", \"find\": \"foo\", \"replace\": \"bar\\n\"}"
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
]
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
"role": "tool",
|
| 225 |
+
"tool_call_id": "call_3",
|
| 226 |
+
"content": "ok"
|
| 227 |
+
}
|
| 228 |
+
],
|
| 229 |
+
"enable_thinking": false,
|
| 230 |
+
"tools": [
|
| 231 |
+
{
|
| 232 |
+
"type": "function",
|
| 233 |
+
"function": {
|
| 234 |
+
"name": "read_file",
|
| 235 |
+
"description": "Read a file's contents.",
|
| 236 |
+
"parameters": {
|
| 237 |
+
"type": "object",
|
| 238 |
+
"properties": {
|
| 239 |
+
"path": {
|
| 240 |
+
"type": "string"
|
| 241 |
+
}
|
| 242 |
+
},
|
| 243 |
+
"required": [
|
| 244 |
+
"path"
|
| 245 |
+
]
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"type": "function",
|
| 251 |
+
"function": {
|
| 252 |
+
"name": "patch",
|
| 253 |
+
"description": "Apply a small text patch to a file.",
|
| 254 |
+
"parameters": {
|
| 255 |
+
"type": "object",
|
| 256 |
+
"properties": {
|
| 257 |
+
"path": {
|
| 258 |
+
"type": "string"
|
| 259 |
+
},
|
| 260 |
+
"find": {
|
| 261 |
+
"type": "string"
|
| 262 |
+
},
|
| 263 |
+
"replace": {
|
| 264 |
+
"type": "string"
|
| 265 |
+
}
|
| 266 |
+
},
|
| 267 |
+
"required": [
|
| 268 |
+
"path",
|
| 269 |
+
"find",
|
| 270 |
+
"replace"
|
| 271 |
+
]
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"type": "function",
|
| 277 |
+
"function": {
|
| 278 |
+
"name": "run",
|
| 279 |
+
"description": "运行命令 — 可设超时。",
|
| 280 |
+
"parameters": {
|
| 281 |
+
"type": "object",
|
| 282 |
+
"properties": {
|
| 283 |
+
"command": {
|
| 284 |
+
"type": "string"
|
| 285 |
+
},
|
| 286 |
+
"timeout": {
|
| 287 |
+
"type": "integer"
|
| 288 |
+
},
|
| 289 |
+
"env": {
|
| 290 |
+
"type": "object"
|
| 291 |
+
},
|
| 292 |
+
"dry": {
|
| 293 |
+
"type": "boolean"
|
| 294 |
+
}
|
| 295 |
+
},
|
| 296 |
+
"required": [
|
| 297 |
+
"command"
|
| 298 |
+
]
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
]
|
| 303 |
+
},
|
| 304 |
+
"prompt": "<role>SYSTEM</role>You are terse.\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file's contents.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}\n{\"type\": \"function\", \"function\": {\"name\": \"patch\", \"description\": \"Apply a small text patch to a file.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"find\": {\"type\": \"string\"}, \"replace\": {\"type\": \"string\"}}, \"required\": [\"path\", \"find\", \"replace\"]}}}\n{\"type\": \"function\", \"function\": {\"name\": \"run\", \"description\": \"运行命令 — 可设超时。\", \"parameters\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}, \"timeout\": {\"type\": \"integer\"}, \"env\": {\"type\": \"object\"}, \"dry\": {\"type\": \"boolean\"}}, \"required\": [\"command\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\ndetailed thinking off<|role_end|><role>HUMAN</role>Fix /tmp/a.txt.<|role_end|><role>ASSISTANT</role>\n<think></think><tool_call>read_file\n<arg_key>path</arg_key>\n<arg_value>/tmp/a.txt</arg_value>\n</tool_call>\n<tool_call>run\n<arg_key>command</arg_key>\n<arg_value>ls -l</arg_value><arg_key>timeout</arg_key>\n<arg_value>30</arg_value><arg_key>env</arg_key>\n<arg_value>{\"A\": \"1\", \"B\": [1, 2.5]}</arg_value><arg_key>dry</arg_key>\n<arg_value>false</arg_value>\n</tool_call><|role_end|><role>OBSERVATION</role>\n<tool_response>\nfoo\n\n</tool_response>\n<tool_response>\ntotal 0\n</tool_response><|role_end|><role>ASSISTANT</role>\n<think></think>Read it.\n<tool_call>patch\n<arg_key>path</arg_key>\n<arg_value>/tmp/a.txt</arg_value><arg_key>find</arg_key>\n<arg_value>foo</arg_value><arg_key>replace</arg_key>\n<arg_value>bar\n</arg_value>\n</tool_call><|role_end|><role>OBSERVATION</role>\n<tool_response>\nok\n</tool_response><|role_end|><role>ASSISTANT</role>\n<think></think>"
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"name": "thinking_tool_loop",
|
| 308 |
+
"request": {
|
| 309 |
+
"model": "mindnano-ling3-tiny",
|
| 310 |
+
"messages": [
|
| 311 |
+
{
|
| 312 |
+
"role": "user",
|
| 313 |
+
"content": "What is in /tmp/a?"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"role": "assistant",
|
| 317 |
+
"content": "",
|
| 318 |
+
"reasoning_content": "\nI should read it.\n",
|
| 319 |
+
"tool_calls": [
|
| 320 |
+
{
|
| 321 |
+
"id": "call_1",
|
| 322 |
+
"type": "function",
|
| 323 |
+
"function": {
|
| 324 |
+
"name": "read_file",
|
| 325 |
+
"arguments": "{\"path\": \"/tmp/a\"}"
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
]
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"role": "tool",
|
| 332 |
+
"tool_call_id": "call_1",
|
| 333 |
+
"content": "foo"
|
| 334 |
+
}
|
| 335 |
+
],
|
| 336 |
+
"enable_thinking": true,
|
| 337 |
+
"tools": [
|
| 338 |
+
{
|
| 339 |
+
"type": "function",
|
| 340 |
+
"function": {
|
| 341 |
+
"name": "read_file",
|
| 342 |
+
"description": "Read a file's contents.",
|
| 343 |
+
"parameters": {
|
| 344 |
+
"type": "object",
|
| 345 |
+
"properties": {
|
| 346 |
+
"path": {
|
| 347 |
+
"type": "string"
|
| 348 |
+
}
|
| 349 |
+
},
|
| 350 |
+
"required": [
|
| 351 |
+
"path"
|
| 352 |
+
]
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
]
|
| 357 |
+
},
|
| 358 |
+
"prompt": "<role>SYSTEM</role># Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file's contents.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\ndetailed thinking on<|role_end|><role>HUMAN</role>What is in /tmp/a?<|role_end|><role>ASSISTANT</role>\n<think>I should read it.</think><tool_call>read_file\n<arg_key>path</arg_key>\n<arg_value>/tmp/a</arg_value>\n</tool_call><|role_end|><role>OBSERVATION</role>\n<tool_response>\nfoo\n</tool_response><|role_end|><role>ASSISTANT</role>\n<think>"
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"name": "think_in_content",
|
| 362 |
+
"request": {
|
| 363 |
+
"model": "mindnano-ling3-tiny",
|
| 364 |
+
"messages": [
|
| 365 |
+
{
|
| 366 |
+
"role": "user",
|
| 367 |
+
"content": "Hi"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"role": "assistant",
|
| 371 |
+
"content": "<think>\nplan\n</think>\n\nHello."
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"role": "user",
|
| 375 |
+
"content": "读取 /tmp/文件 ✓"
|
| 376 |
+
}
|
| 377 |
+
],
|
| 378 |
+
"enable_thinking": true,
|
| 379 |
+
"tools": [
|
| 380 |
+
{
|
| 381 |
+
"type": "function",
|
| 382 |
+
"function": {
|
| 383 |
+
"name": "read_file",
|
| 384 |
+
"description": "Read a file's contents.",
|
| 385 |
+
"parameters": {
|
| 386 |
+
"type": "object",
|
| 387 |
+
"properties": {
|
| 388 |
+
"path": {
|
| 389 |
+
"type": "string"
|
| 390 |
+
}
|
| 391 |
+
},
|
| 392 |
+
"required": [
|
| 393 |
+
"path"
|
| 394 |
+
]
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
]
|
| 399 |
+
},
|
| 400 |
+
"prompt": "<role>SYSTEM</role># Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file's contents.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\ndetailed thinking on<|role_end|><role>HUMAN</role>Hi<|role_end|><role>ASSISTANT</role>\n<think>plan</think>Hello.<|role_end|><role>HUMAN</role>读取 /tmp/文件 ✓<|role_end|><role>ASSISTANT</role>\n<think>"
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"name": "unicode_call",
|
| 404 |
+
"request": {
|
| 405 |
+
"model": "mindnano-ling3-tiny",
|
| 406 |
+
"messages": [
|
| 407 |
+
{
|
| 408 |
+
"role": "user",
|
| 409 |
+
"content": "打招呼"
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"role": "assistant",
|
| 413 |
+
"content": null,
|
| 414 |
+
"tool_calls": [
|
| 415 |
+
{
|
| 416 |
+
"id": "call_1",
|
| 417 |
+
"type": "function",
|
| 418 |
+
"function": {
|
| 419 |
+
"name": "run",
|
| 420 |
+
"arguments": "{\"command\": \"echo 你好\"}"
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
]
|
| 424 |
+
},
|
| 425 |
+
{
|
| 426 |
+
"role": "tool",
|
| 427 |
+
"tool_call_id": "call_1",
|
| 428 |
+
"content": "你好"
|
| 429 |
+
}
|
| 430 |
+
],
|
| 431 |
+
"enable_thinking": false,
|
| 432 |
+
"tools": [
|
| 433 |
+
{
|
| 434 |
+
"type": "function",
|
| 435 |
+
"function": {
|
| 436 |
+
"name": "run",
|
| 437 |
+
"description": "运行命令 — 可设超时。",
|
| 438 |
+
"parameters": {
|
| 439 |
+
"type": "object",
|
| 440 |
+
"properties": {
|
| 441 |
+
"command": {
|
| 442 |
+
"type": "string"
|
| 443 |
+
},
|
| 444 |
+
"timeout": {
|
| 445 |
+
"type": "integer"
|
| 446 |
+
},
|
| 447 |
+
"env": {
|
| 448 |
+
"type": "object"
|
| 449 |
+
},
|
| 450 |
+
"dry": {
|
| 451 |
+
"type": "boolean"
|
| 452 |
+
}
|
| 453 |
+
},
|
| 454 |
+
"required": [
|
| 455 |
+
"command"
|
| 456 |
+
]
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
]
|
| 461 |
+
},
|
| 462 |
+
"prompt": "<role>SYSTEM</role># Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{\"type\": \"function\", \"function\": {\"name\": \"run\", \"description\": \"运行命令 — 可设超时。\", \"parameters\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}, \"timeout\": {\"type\": \"integer\"}, \"env\": {\"type\": \"object\"}, \"dry\": {\"type\": \"boolean\"}}, \"required\": [\"command\"]}}}\n</tools>\n\nIf none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out.\nIf you need to use a function, for each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>\ndetailed thinking off<|role_end|><role>HUMAN</role>打招呼<|role_end|><role>ASSISTANT</role>\n<think></think><tool_call>run\n<arg_key>command</arg_key>\n<arg_value>echo 你好</arg_value>\n</tool_call><|role_end|><role>OBSERVATION</role>\n<tool_response>\n你好\n</tool_response><|role_end|><role>ASSISTANT</role>\n<think></think>"
|
| 463 |
+
}
|
| 464 |
+
]
|
tests/chat_tools_test.cpp
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "ling3/chat_protocol.h"
|
| 2 |
+
#include <fstream>
|
| 3 |
+
#include <iostream>
|
| 4 |
+
|
| 5 |
+
using namespace ling3::chat;
|
| 6 |
+
void Check(bool condition, const std::string & message) {
|
| 7 |
+
if (!condition) throw std::runtime_error(message);
|
| 8 |
+
}
|
| 9 |
+
int main(int argc, char ** argv) {
|
| 10 |
+
if (argc != 2) throw std::runtime_error("usage: ling3-chat-tools-test tests/chat_tools_golden.json");
|
| 11 |
+
// Prompts rendered by transformers from the official chat_template.jinja
|
| 12 |
+
// (tools/render_chat_tools_golden.py); the C++ renderer must match byte for byte.
|
| 13 |
+
std::ifstream file(argv[1]);
|
| 14 |
+
const auto golden = OrderedJson::parse(file);
|
| 15 |
+
auto find = [&](const std::string & name) -> const OrderedJson & {
|
| 16 |
+
for (const auto & c : golden) if (c["name"] == name) return c;
|
| 17 |
+
throw std::runtime_error("missing golden case " + name);
|
| 18 |
+
};
|
| 19 |
+
for (const auto & c : golden)
|
| 20 |
+
Check(Parse(c["request"]).prompt == c["prompt"].get<std::string>(),
|
| 21 |
+
"template mismatch: " + c["name"].get<std::string>());
|
| 22 |
+
|
| 23 |
+
auto body = find("tool_loop")["request"];
|
| 24 |
+
const auto & tools = body["tools"];
|
| 25 |
+
const auto prompt = find("tool_loop")["prompt"].get<std::string>();
|
| 26 |
+
Check(Parse(body).tool_calls && Parse(body).parallel_tool_calls, "tools must enable call parsing");
|
| 27 |
+
auto none = body; none["tool_choice"] = "none";
|
| 28 |
+
Check(!Parse(none).tool_calls && Parse(none).prompt == prompt, "tool_choice none still renders tools");
|
| 29 |
+
auto stored = body; stored["store"] = false;
|
| 30 |
+
Check(Parse(stored).prompt == prompt, "store flag rejected");
|
| 31 |
+
auto parts = body;
|
| 32 |
+
parts["messages"][1]["content"] = OrderedJson::array({{{"type", "text"}, {"text", "Fix /tmp/a.txt."}}});
|
| 33 |
+
Check(Parse(parts).prompt == prompt, "text-part content");
|
| 34 |
+
for (const auto & [pointer, value] : std::vector<std::pair<std::string, OrderedJson>>{
|
| 35 |
+
{"/tools", "read_file"}, {"/tools/0/type", "code"}, {"/tool_choice", "required"},
|
| 36 |
+
{"/parallel_tool_calls", "yes"}, {"/store", "no"}, {"/messages/2/tool_calls/0/function/arguments", "{oops"},
|
| 37 |
+
{"/messages/2/tool_calls/0/function/arguments", "[1]"}, {"/messages/3/content", nullptr},
|
| 38 |
+
{"/messages/3/id", "x"}, {"/messages/1/content", OrderedJson::array({{{"type", "image_url"}}})}}) {
|
| 39 |
+
auto bad = body; bad[OrderedJson::json_pointer(pointer)] = value; bool failed = false;
|
| 40 |
+
try { Parse(bad); } catch (const Error &) { failed = true; }
|
| 41 |
+
Check(failed, "invalid tool request accepted: " + pointer);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Arguments are typed by the declared schema; a parsed call re-renders to
|
| 45 |
+
// the generated text, so the next turn's prompt keeps the cached prefix.
|
| 46 |
+
auto calls = ParseToolCalls("<tool_call>run\n<arg_key>command</arg_key>\n<arg_value>123</arg_value>"
|
| 47 |
+
"<arg_key>timeout</arg_key>\n<arg_value>30</arg_value><arg_key>env</arg_key>\n<arg_value>{\"A\": [1, 2.5]}"
|
| 48 |
+
"</arg_value><arg_key>dry</arg_key>\n<arg_value>false</arg_value>\n</tool_call>", tools, true);
|
| 49 |
+
Check(calls.ok && calls.calls.size() == 1 && calls.calls[0].name == "run", "call not parsed");
|
| 50 |
+
Check(calls.calls[0].arguments.dump() == R"({"command":"123","timeout":30,"env":{"A":[1,2.5]},"dry":false})",
|
| 51 |
+
"arguments not typed by schema");
|
| 52 |
+
for (const auto * name : {"tool_loop", "unicode_call", "thinking_tool_loop"})
|
| 53 |
+
for (const auto & m : find(name)["request"]["messages"]) {
|
| 54 |
+
if (!m.contains("tool_calls")) continue;
|
| 55 |
+
std::string generated;
|
| 56 |
+
for (const auto & c : m["tool_calls"])
|
| 57 |
+
generated += (generated.empty() ? "" : "\n") + RenderToolCall({c["function"]["name"].get<std::string>(),
|
| 58 |
+
OrderedJson::parse(c["function"]["arguments"].get<std::string>())});
|
| 59 |
+
Check(prompt.find(generated) != std::string::npos || std::string(name) != "tool_loop", "history render");
|
| 60 |
+
const auto parsed = ParseToolCalls(generated, find(name)["request"]["tools"], true);
|
| 61 |
+
Check(parsed.ok && parsed.calls.size() == m["tool_calls"].size(), "round trip parse");
|
| 62 |
+
std::string again;
|
| 63 |
+
for (const auto & c : parsed.calls) again += (again.empty() ? "" : "\n") + RenderToolCall(c);
|
| 64 |
+
Check(again == generated, std::string("round trip render: ") + name);
|
| 65 |
+
}
|
| 66 |
+
const std::string two = "<tool_call>read_file\n<arg_key>path</arg_key>\n<arg_value>/tmp/a</arg_value>\n</tool_call>\n"
|
| 67 |
+
"<tool_call>read_file <arg_key>path</arg_key><arg_value>/tmp/b</arg_value></tool_call>";
|
| 68 |
+
Check(ParseToolCalls(two, tools, true).calls.size() == 2, "parallel calls");
|
| 69 |
+
Check(ParseToolCalls(two, tools, false).calls.size() == 1, "parallel_tool_calls=false");
|
| 70 |
+
Check(ParseToolCalls(two, tools, true).calls[1].arguments["path"] == "/tmp/b", "whitespace-free call");
|
| 71 |
+
Check(!ParseToolCalls("<tool_call>unknown\n\n</tool_call>", tools, true).ok, "undeclared tool accepted");
|
| 72 |
+
Check(!ParseToolCalls("<tool_call>read_file\n<arg_key>path</arg_key>\n<arg_value>/tmp", tools, true).ok,
|
| 73 |
+
"truncated call accepted");
|
| 74 |
+
Check(ParseToolCalls("<tool_call>read_file\n\n</tool_call>\nDone.", tools, true).trailing == "Done.",
|
| 75 |
+
"trailing content lost");
|
| 76 |
+
|
| 77 |
+
// Streaming: call text and the newline before it are held, never sent as content.
|
| 78 |
+
ToolCallFilter filter(true);
|
| 79 |
+
std::string shown;
|
| 80 |
+
for (const auto * piece : {"Read it.", "\n<tool", "_call>patch\n<arg_key>path</arg_key>",
|
| 81 |
+
"\n<arg_value>/tmp/a</arg_value>\n</tool_call>"})
|
| 82 |
+
shown += filter.Push(piece);
|
| 83 |
+
shown += filter.Push({}, true);
|
| 84 |
+
Check(shown == "Read it." && filter.captured().starts_with("<tool_call>patch"), "call text streamed");
|
| 85 |
+
Check(filter.Released() == "\n" + filter.captured(), "held newline lost");
|
| 86 |
+
ToolCallFilter plain(true);
|
| 87 |
+
shown = plain.Push("a <tool");
|
| 88 |
+
shown += plain.Push("box>\n");
|
| 89 |
+
shown += plain.Push({}, true);
|
| 90 |
+
Check(shown == "a <toolbox>\n" && plain.captured().empty(), "false tag prefix withheld");
|
| 91 |
+
ToolCallFilter off(false);
|
| 92 |
+
Check(off.Push("<tool_call>x") == "<tool_call>x", "disabled filter held text");
|
| 93 |
+
std::cout << "chat tools: " << golden.size() << " golden prompts, parsing, round trip, streaming passed\n";
|
| 94 |
+
}
|
tools/render_chat_tools_golden.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Render OpenAI-style requests with the official Ling-3.0 chat template.
|
| 3 |
+
|
| 4 |
+
The output is the golden file for tests/chat_tools_test.cpp: each request is
|
| 5 |
+
rendered by transformers' own Jinja renderer, so the C++ prompt builder is
|
| 6 |
+
checked against the template rather than against a reading of it. The
|
| 7 |
+
template is inclusionAI/Ling-3.0-tiny@e3a47d5b986e7141b6efd62597d598ebb392060d
|
| 8 |
+
chat_template.jinja (pass its local path). Tool-call arguments travel as JSON
|
| 9 |
+
strings in the requests, as OpenAI clients send them; the template needs them
|
| 10 |
+
as mappings, as vLLM and llama.cpp convert them.
|
| 11 |
+
"""
|
| 12 |
+
import argparse
|
| 13 |
+
import copy
|
| 14 |
+
import json
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
from transformers.utils.chat_template_utils import render_jinja_template
|
| 18 |
+
|
| 19 |
+
MODEL = "mindnano-ling3-tiny"
|
| 20 |
+
READ = {"type": "function", "function": {"name": "read_file", "description": "Read a file's contents.",
|
| 21 |
+
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}}
|
| 22 |
+
PATCH = {"type": "function", "function": {"name": "patch", "description": "Apply a small text patch to a file.",
|
| 23 |
+
"parameters": {"type": "object", "properties": {"path": {"type": "string"}, "find": {"type": "string"},
|
| 24 |
+
"replace": {"type": "string"}}, "required": ["path", "find", "replace"]}}}
|
| 25 |
+
RUN = {"type": "function", "function": {"name": "run", "description": "运行命令 — 可设超时。",
|
| 26 |
+
"parameters": {"type": "object", "properties": {"command": {"type": "string"}, "timeout": {"type": "integer"},
|
| 27 |
+
"env": {"type": "object"}, "dry": {"type": "boolean"}}, "required": ["command"]}}}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def call(id_, name, **arguments):
|
| 31 |
+
return {"id": id_, "type": "function", "function": {"name": name, "arguments": json.dumps(arguments, ensure_ascii=False)}}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
CASES = [
|
| 35 |
+
("no_tools_history", False, None, [
|
| 36 |
+
{"role": "system", "content": "请用中文回答"}, {"role": "user", "content": "你好"},
|
| 37 |
+
{"role": "assistant", "content": "你好!"}, {"role": "user", "content": "继续"}]),
|
| 38 |
+
("tools_no_system", False, [READ], [{"role": "user", "content": "Reply with OK."}]),
|
| 39 |
+
("tools_system", False, [READ, PATCH], [
|
| 40 |
+
{"role": "system", "content": "You are terse."},
|
| 41 |
+
{"role": "user", "content": "Use the tools to read /tmp/scratch.txt, then patch it to change 'foo' to 'bar'."}]),
|
| 42 |
+
("tools_system_thinking_phrase", False, [READ], [
|
| 43 |
+
{"role": "system", "content": "detailed thinking off\nBe brief."}, {"role": "user", "content": "Read /tmp/a."}]),
|
| 44 |
+
("tool_loop", False, [READ, PATCH, RUN], [
|
| 45 |
+
{"role": "system", "content": "You are terse."}, {"role": "user", "content": "Fix /tmp/a.txt."},
|
| 46 |
+
{"role": "assistant", "content": None, "tool_calls": [
|
| 47 |
+
call("call_1", "read_file", path="/tmp/a.txt"),
|
| 48 |
+
call("call_2", "run", command="ls -l", timeout=30, env={"A": "1", "B": [1, 2.5]}, dry=False)]},
|
| 49 |
+
{"role": "tool", "tool_call_id": "call_1", "content": "foo\n"},
|
| 50 |
+
{"role": "tool", "tool_call_id": "call_2", "content": "total 0"},
|
| 51 |
+
{"role": "assistant", "content": "Read it.", "tool_calls": [
|
| 52 |
+
call("call_3", "patch", path="/tmp/a.txt", find="foo", replace="bar\n")]},
|
| 53 |
+
{"role": "tool", "tool_call_id": "call_3", "content": "ok"}]),
|
| 54 |
+
("thinking_tool_loop", True, [READ], [
|
| 55 |
+
{"role": "user", "content": "What is in /tmp/a?"},
|
| 56 |
+
{"role": "assistant", "content": "", "reasoning_content": "\nI should read it.\n",
|
| 57 |
+
"tool_calls": [call("call_1", "read_file", path="/tmp/a")]},
|
| 58 |
+
{"role": "tool", "tool_call_id": "call_1", "content": "foo"}]),
|
| 59 |
+
("think_in_content", True, [READ], [
|
| 60 |
+
{"role": "user", "content": "Hi"},
|
| 61 |
+
{"role": "assistant", "content": "<think>\nplan\n</think>\n\nHello."},
|
| 62 |
+
{"role": "user", "content": "读取 /tmp/文件 ✓"}]),
|
| 63 |
+
("unicode_call", False, [RUN], [
|
| 64 |
+
{"role": "user", "content": "打招呼"},
|
| 65 |
+
{"role": "assistant", "content": None, "tool_calls": [call("call_1", "run", command="echo 你好")]},
|
| 66 |
+
{"role": "tool", "tool_call_id": "call_1", "content": "你好"}]),
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def template_messages(messages):
|
| 71 |
+
messages = copy.deepcopy(messages)
|
| 72 |
+
for message in messages:
|
| 73 |
+
for tool_call in message.get("tool_calls", []):
|
| 74 |
+
tool_call["function"]["arguments"] = json.loads(tool_call["function"]["arguments"])
|
| 75 |
+
return messages
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def main():
|
| 79 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 80 |
+
parser.add_argument("--template", type=Path, required=True)
|
| 81 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 82 |
+
args = parser.parse_args()
|
| 83 |
+
template = args.template.read_text()
|
| 84 |
+
golden = []
|
| 85 |
+
for name, thinking, tools, messages in CASES:
|
| 86 |
+
rendered, _ = render_jinja_template(conversations=[template_messages(messages)], tools=tools,
|
| 87 |
+
chat_template=template, add_generation_prompt=True,
|
| 88 |
+
enable_thinking=thinking)
|
| 89 |
+
request = {"model": MODEL, "messages": messages, "enable_thinking": thinking}
|
| 90 |
+
if tools: request["tools"] = tools
|
| 91 |
+
golden.append({"name": name, "request": request, "prompt": rendered[0]})
|
| 92 |
+
args.output.write_text(json.dumps(golden, ensure_ascii=False, indent=1) + "\n")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
main()
|