ref(upstream): FULL TREE — Deep-Spark xllm (1470) + ds_vllm csrc/models (703)
Replaces cherry-picked upstream_ref with complete source trees. xllm/ — Iluvatar official C++ inference engine (15MB, 1470 files) Complete: kernels → layers → models → runtime → scheduler → api Excluded: .git, binary images, third_party submodule checkouts ds_vllm/ — Iluvatar official vllm fork (8MB, 703 files) Included: csrc/ (ALL CUDA kernels), fused_moe/, qwen3_5 model, _custom_ops Excluded: tests, benchmarks, docs, examples (not needed for reference) Critical call chains now fully traceable: MoE: moe_topk_softmax_kernels.cuh → ixformer.h → fused_moe.cpp → layer GDN: qwen3_gated_delta_net_base.cpp → qwen3_5_gated_delta_net.cpp Attention: ixformer.h → xllm_paged_attention → attention.cpp
This commit is contained in:
25
upstream_ref/xllm/tests/function_call/CMakeLists.txt
Normal file
25
upstream_ref/xllm/tests/function_call/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
include(cc_test)
|
||||
|
||||
add_subdirectory(partial_json_parser)
|
||||
|
||||
function(add_detector_test TEST_NAME)
|
||||
cc_test(
|
||||
NAME
|
||||
${TEST_NAME}
|
||||
SRCS
|
||||
${TEST_NAME}.cpp
|
||||
DEPS
|
||||
:function_call
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
endfunction()
|
||||
|
||||
add_detector_test(qwen25_detector_test)
|
||||
add_detector_test(qwen3_coder_detector_test)
|
||||
add_detector_test(kimik2_detector_test)
|
||||
add_detector_test(deepseekv3_detector_test)
|
||||
add_detector_test(glm45_detector_test)
|
||||
add_detector_test(glm47_detector_test)
|
||||
add_detector_test(deepseekv32_detector_test)
|
||||
@@ -0,0 +1,943 @@
|
||||
/* Copyright 2026 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "deepseekv32_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
class DeepSeek32DetectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
detector_ = std::make_unique<DeepSeekV32Detector>();
|
||||
|
||||
// Setup test tools
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"city",
|
||||
{{"type", "string"},
|
||||
{"description", "The city name, e.g. Beijing, Shanghai"}}},
|
||||
{"date",
|
||||
{{"type", "string"},
|
||||
{"description", "Date in YYYY-MM-DD format"}}}}},
|
||||
{"required", {"city"}}};
|
||||
|
||||
JsonFunction weather_func("get_weather",
|
||||
"Get the weather information for a given city",
|
||||
weather_params);
|
||||
weather_tool_ = JsonTool("function", weather_func);
|
||||
|
||||
nlohmann::json calculator_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"expression",
|
||||
{{"type", "string"},
|
||||
{"description", "Mathematical expression to evaluate"}}},
|
||||
{"precision",
|
||||
{{"type", "number"}, {"description", "Number of decimal places"}}}}},
|
||||
{"required", {"expression"}}};
|
||||
|
||||
JsonFunction calculator_func(
|
||||
"calculate", "Calculate mathematical expressions", calculator_params);
|
||||
calculator_tool_ = JsonTool("function", calculator_func);
|
||||
|
||||
tools_ = {weather_tool_, calculator_tool_};
|
||||
}
|
||||
|
||||
std::unique_ptr<DeepSeekV32Detector> detector_;
|
||||
JsonTool weather_tool_;
|
||||
JsonTool calculator_tool_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
// Test constructor and basic properties
|
||||
TEST_F(DeepSeek32DetectorTest, ConstructorInitializesCorrectly) {
|
||||
EXPECT_NE(detector_, nullptr);
|
||||
|
||||
// Test basic token detection
|
||||
std::string text_with_tool_call =
|
||||
"Some text "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">Beijing</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
// Test has_tool_call method
|
||||
TEST_F(DeepSeek32DetectorTest, HasToolCallDetection) {
|
||||
// Test text containing tool calls
|
||||
EXPECT_TRUE(detector_->has_tool_call("<|DSML|function_calls>"));
|
||||
EXPECT_TRUE(detector_->has_tool_call("<|DSML|invoke"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"Previous text <|DSML|function_calls>Following content"));
|
||||
EXPECT_TRUE(
|
||||
detector_->has_tool_call("<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">北京</|DSML|parameter></"
|
||||
"|DSML|invoke></|DSML|function_calls>"));
|
||||
EXPECT_TRUE(detector_->has_tool_call("{\"tool_calls\": []}"));
|
||||
|
||||
// Test text not containing tool calls
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("DSML without brackets"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("<function_calls> without DSML"));
|
||||
}
|
||||
|
||||
// Test single tool call parsing
|
||||
TEST_F(DeepSeek32DetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">北京</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"date\" "
|
||||
"string=\"true\">2024-06-27</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "北京");
|
||||
EXPECT_EQ(params["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test single tool call parsing with direct JSON format
|
||||
TEST_F(DeepSeek32DetectorTest, SingleToolCallParsingJsonFormat) {
|
||||
std::string text =
|
||||
"Please help me check the weather "
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"get_weather\">{\"city\": "
|
||||
"\"北京\", \"date\": "
|
||||
"\"2024-06-27\"}</|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "北京");
|
||||
EXPECT_EQ(params["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test multiple tool calls parsing
|
||||
TEST_F(DeepSeek32DetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather and calculate "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">上海</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"date\" "
|
||||
"string=\"true\">2024-06-27</|DSML|parameter></"
|
||||
"|DSML|invoke><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter name=\"city\" "
|
||||
"string=\"true\">北京</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"date\" "
|
||||
"string=\"true\">2024-06-27</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"Please help me check the weather and calculate");
|
||||
ASSERT_EQ(result.calls.size(), 2);
|
||||
|
||||
// Verify first tool call
|
||||
const auto& call1 = result.calls[0];
|
||||
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call1.name.has_value());
|
||||
EXPECT_EQ(call1.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
|
||||
EXPECT_EQ(params1["city"], "上海");
|
||||
EXPECT_EQ(params1["date"], "2024-06-27");
|
||||
|
||||
// Verify second tool call
|
||||
const auto& call2 = result.calls[1];
|
||||
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call2.name.has_value());
|
||||
EXPECT_EQ(call2.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
|
||||
EXPECT_EQ(params2["city"], "北京");
|
||||
EXPECT_EQ(params2["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test number type handling
|
||||
TEST_F(DeepSeek32DetectorTest, NumberTypeHandling) {
|
||||
std::string text =
|
||||
"Calculate with precision "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"calculate\"><|DSML|parameter "
|
||||
"name=\"expression\" string=\"true\">3.14 * "
|
||||
"2</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"precision\" "
|
||||
"string=\"false\">2</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Calculate with precision");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["expression"], "3.14 * 2");
|
||||
// precision should be parsed as number
|
||||
EXPECT_TRUE(params["precision"].is_number());
|
||||
EXPECT_EQ(params["precision"], 2);
|
||||
}
|
||||
|
||||
// Test empty tool call content
|
||||
TEST_F(DeepSeek32DetectorTest, EmptyToolCallContent) {
|
||||
std::string text =
|
||||
"Test empty content <|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"></|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test empty content");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params.empty());
|
||||
}
|
||||
|
||||
// Test incomplete tool call (only start tag)
|
||||
TEST_F(DeepSeek32DetectorTest, IncompleteToolCall) {
|
||||
std::string text =
|
||||
"Incomplete tool call "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\">Beijing";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Incomplete tool call");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
|
||||
}
|
||||
|
||||
// Test unknown tool name handling
|
||||
TEST_F(DeepSeek32DetectorTest, UnknownToolName) {
|
||||
std::string text =
|
||||
"Unknown tool "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"unknown_tool\"><|DSML|parameter "
|
||||
"name=\"param\" "
|
||||
"string=\"true\">value</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Unknown tool");
|
||||
// Base class will skip unknown tools, so should be 0 calls
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test case with only normal text
|
||||
TEST_F(DeepSeek32DetectorTest, OnlyNormalText) {
|
||||
std::string text = "This is a regular text without any tool calls.";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"This is a regular text without any tool calls.");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test empty string input
|
||||
TEST_F(DeepSeek32DetectorTest, EmptyStringInput) {
|
||||
std::string text = "";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test whitespace-only input
|
||||
TEST_F(DeepSeek32DetectorTest, WhitespaceOnlyInput) {
|
||||
std::string text = " \t\n\r ";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test complex nested JSON parameters
|
||||
TEST_F(DeepSeek32DetectorTest, ComplexNestedJsonParameters) {
|
||||
std::string text =
|
||||
"Complex parameter test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">Beijing</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"options\" string=\"false\">{\"include_forecast\": true, "
|
||||
"\"days\": "
|
||||
"7}</|DSML|parameter></|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Complex parameter test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
EXPECT_TRUE(params["options"]["include_forecast"]);
|
||||
EXPECT_EQ(params["options"]["days"], 7);
|
||||
}
|
||||
|
||||
// Test special characters handling
|
||||
TEST_F(DeepSeek32DetectorTest, SpecialCharactersHandling) {
|
||||
std::string text =
|
||||
"Special characters test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\">New York "
|
||||
"City</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"note\" string=\"true\">Contains "
|
||||
"symbols!@#$%^&*()_+=</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Special characters test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "New York City");
|
||||
EXPECT_EQ(params["note"], "Contains symbols!@#$%^&*()_+=");
|
||||
}
|
||||
|
||||
// Test whitespace handling in parameter values
|
||||
TEST_F(DeepSeek32DetectorTest, WhitespaceHandlingInParameterValues) {
|
||||
std::string text =
|
||||
"Whitespace test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\"> Beijing "
|
||||
"</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"date\" "
|
||||
"string=\"true\">\n\t2024-06-27\r\n</|DSML|parameter></"
|
||||
"|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Whitespace test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
// Note: whitespace trimming behavior depends on implementation
|
||||
// This test verifies the basic parsing works
|
||||
EXPECT_TRUE(params.contains("city"));
|
||||
EXPECT_TRUE(params.contains("date"));
|
||||
}
|
||||
|
||||
// ========== Streaming Tests ==========
|
||||
|
||||
// Test basic streaming parsing
|
||||
TEST_F(DeepSeek32DetectorTest, BasicStreamingParsing) {
|
||||
std::vector<std::string> chunks = {
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"get_weather\">",
|
||||
"<|DSML|parameter name=\"city\" string=\"true\">",
|
||||
"Beijing</|DSML|parameter></|DSML|invoke></|DSML|function_calls>"};
|
||||
|
||||
std::vector<StreamingParseResult> results;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
results.push_back(result);
|
||||
}
|
||||
|
||||
bool found_tool_name = false;
|
||||
bool found_arguments = false;
|
||||
|
||||
for (const auto& result : results) {
|
||||
if (!result.calls.empty()) {
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value() && call.name.value() == "get_weather") {
|
||||
found_tool_name = true;
|
||||
}
|
||||
if (!call.parameters.empty() &&
|
||||
call.parameters.find("Beijing") != std::string::npos) {
|
||||
found_arguments = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_tool_name) << "Should find tool name in streaming results";
|
||||
EXPECT_TRUE(found_arguments) << "Should find arguments in streaming results";
|
||||
}
|
||||
|
||||
// Test incremental argument streaming
|
||||
TEST_F(DeepSeek32DetectorTest, IncrementalArgumentStreaming) {
|
||||
std::vector<std::string> chunks = {
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"get_weather\">",
|
||||
"<|DSML|parameter name=\"city\" string=\"true\">",
|
||||
"Beijing",
|
||||
"</|DSML|parameter><|DSML|parameter name=\"date\" string=\"true\">",
|
||||
"2024-06-27</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>"};
|
||||
|
||||
std::string accumulated_args;
|
||||
bool tool_name_sent = false;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_name_sent = true;
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
} else {
|
||||
accumulated_args += call.parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(tool_name_sent)
|
||||
<< "Tool name should be sent when tool call is complete";
|
||||
if (!accumulated_args.empty()) {
|
||||
EXPECT_TRUE(accumulated_args.find("Beijing") != std::string::npos)
|
||||
<< "Should contain city argument";
|
||||
EXPECT_TRUE(accumulated_args.find("2024-06-27") != std::string::npos)
|
||||
<< "Should contain date argument";
|
||||
}
|
||||
}
|
||||
|
||||
// Test normal text handling during streaming
|
||||
TEST_F(DeepSeek32DetectorTest, StreamingNormalTextHandling) {
|
||||
std::vector<std::string> chunks = {
|
||||
"This is normal text before tool call. ",
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"get_weather\">",
|
||||
"<|DSML|parameter name=\"city\" "
|
||||
"string=\"true\">Tokyo</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>",
|
||||
" And this is text after."};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
bool found_tool_call = false;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
if (!result.calls.empty()) {
|
||||
found_tool_call = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_tool_call) << "Should find tool call";
|
||||
EXPECT_TRUE(accumulated_normal_text.find("This is normal text") !=
|
||||
std::string::npos)
|
||||
<< "Should preserve normal text before tool call";
|
||||
// Note: We don't expect "And this is text after" to be in
|
||||
// accumulated_normal_text
|
||||
}
|
||||
|
||||
TEST_F(DeepSeek32DetectorTest, StreamingNormalTextBuffersIncompleteUtf8Tail) {
|
||||
std::string chunk1 = "The user didn";
|
||||
chunk1.push_back(static_cast<char>(0xE2));
|
||||
chunk1.push_back(static_cast<char>(0x80));
|
||||
|
||||
std::string chunk2;
|
||||
chunk2.push_back(static_cast<char>(0x99));
|
||||
chunk2 += "t specify temperature units.";
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
for (const auto& chunk : {chunk1, chunk2}) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
EXPECT_EQ(accumulated_normal_text,
|
||||
"The user didn’t specify temperature units.");
|
||||
}
|
||||
|
||||
TEST_F(DeepSeek32DetectorTest, StreamingToolArgumentsBufferIncompleteUtf8Tail) {
|
||||
std::string chunk1 =
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"get_weather\">"
|
||||
"<|DSML|parameter name=\"city\" string=\"true\">";
|
||||
|
||||
std::string chunk2;
|
||||
chunk2.push_back(static_cast<char>(0xE5));
|
||||
chunk2.push_back(static_cast<char>(0x8C));
|
||||
|
||||
std::string chunk3;
|
||||
chunk3.push_back(static_cast<char>(0x97));
|
||||
chunk3 += "京</|DSML|parameter></|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
std::string accumulated_arguments;
|
||||
bool tool_name_sent = false;
|
||||
for (const auto& chunk : {chunk1, chunk2, chunk3}) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_name_sent = true;
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
} else {
|
||||
accumulated_arguments += call.parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(tool_name_sent);
|
||||
nlohmann::json params = nlohmann::json::parse(accumulated_arguments);
|
||||
EXPECT_EQ(params["city"], "北京");
|
||||
}
|
||||
|
||||
// Test invalid JSON in parameter values
|
||||
TEST_F(DeepSeek32DetectorTest, InvalidJsonInParameterValues) {
|
||||
std::string text =
|
||||
"Invalid JSON test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">Beijing</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"config\" string=\"false\">{invalid "
|
||||
"json}</|DSML|parameter></|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Invalid JSON test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
// Invalid JSON should be treated as string or cause parsing to fail
|
||||
// gracefully
|
||||
EXPECT_TRUE(params.contains("config"));
|
||||
}
|
||||
|
||||
// Test nested braces in JSON values
|
||||
|
||||
TEST_F(DeepSeek32DetectorTest, NestedBracesInJsonValues) {
|
||||
std::string text =
|
||||
"Nested braces test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">Beijing</|DSML|parameter><|DSML|parameter "
|
||||
"name=\"config\" string=\"false\">{\"nested\": {\"deep\": "
|
||||
"\"value\"}}</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Nested braces test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
|
||||
}
|
||||
|
||||
// Test mixed format (XML and JSON in same invoke)
|
||||
TEST_F(DeepSeek32DetectorTest, MixedFormatNotSupported) {
|
||||
// Note: This test verifies that the parser handles the format correctly
|
||||
// In practice, an invoke should use either XML or JSON, not both
|
||||
std::string text =
|
||||
"Mixed format test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\">Beijing</|DSML|parameter>{\"date\": "
|
||||
"\"2024-06-27\"}</|DSML|invoke></|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Mixed format test");
|
||||
// The parser should handle this gracefully
|
||||
ASSERT_GE(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test performance with many tool calls
|
||||
TEST_F(DeepSeek32DetectorTest, PerformanceWithManyToolCalls) {
|
||||
std::string text = "Performance test <|DSML|function_calls>";
|
||||
|
||||
// Build text containing multiple tool calls
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
text +=
|
||||
"<|DSML|invoke name=\"calculate\"><|DSML|parameter "
|
||||
"name=\"expression\" string=\"true\">" +
|
||||
std::to_string(i) + " + " + std::to_string(i + 1) +
|
||||
"</|DSML|parameter></|DSML|invoke>";
|
||||
}
|
||||
text += "</|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Performance test");
|
||||
ASSERT_EQ(result.calls.size(), 10);
|
||||
|
||||
// Verify each tool call is correctly parsed
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
const auto& call = result.calls[i];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
std::string expected_expr =
|
||||
std::to_string(i) + " + " + std::to_string(i + 1);
|
||||
EXPECT_EQ(params["expression"], expected_expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Test array parameter with string="false"
|
||||
TEST_F(DeepSeek32DetectorTest, ArrayParameterHandling) {
|
||||
std::string text =
|
||||
"Array parameter test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"cities\" string=\"false\">[\"Beijing\", \"Shanghai\", "
|
||||
"\"Guangzhou\"]</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Array parameter test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_TRUE(params["cities"].is_array());
|
||||
EXPECT_EQ(params["cities"].size(), 3);
|
||||
EXPECT_EQ(params["cities"][0], "Beijing");
|
||||
EXPECT_EQ(params["cities"][1], "Shanghai");
|
||||
EXPECT_EQ(params["cities"][2], "Guangzhou");
|
||||
}
|
||||
|
||||
// Test boolean parameter with string="false"
|
||||
TEST_F(DeepSeek32DetectorTest, BooleanParameterHandling) {
|
||||
std::string text =
|
||||
"Boolean parameter test "
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"include_forecast\" "
|
||||
"string=\"false\">true</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Boolean parameter test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_TRUE(params["include_forecast"].is_boolean());
|
||||
EXPECT_EQ(params["include_forecast"], true);
|
||||
}
|
||||
|
||||
// Test JSON format tool calls (fallback)
|
||||
TEST_F(DeepSeek32DetectorTest, JsonFormatToolCalls) {
|
||||
std::string text =
|
||||
"Some text before "
|
||||
"{\"tool_calls\": [{\"name\": \"get_weather\", \"arguments\": "
|
||||
"{\"city\": \"Beijing\", \"date\": \"2024-06-27\"}}]}";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Some text before");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
EXPECT_EQ(params["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test JSON format with function wrapper
|
||||
TEST_F(DeepSeek32DetectorTest, JsonFormatWithFunctionWrapper) {
|
||||
std::string text =
|
||||
"Text before "
|
||||
"{\"tool_calls\": [{\"function\": {\"name\": \"get_weather\", "
|
||||
"\"arguments\": \"{\\\"city\\\": \\\"Tokyo\\\"}\"}}]}";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Text before");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Tokyo");
|
||||
}
|
||||
|
||||
// Test trailing markdown language hint before DSML tool block
|
||||
TEST_F(DeepSeek32DetectorTest, StripTrailingJsonHintBeforeDsmlToolBlock) {
|
||||
std::string text =
|
||||
"Some text before tool call\n"
|
||||
"json\n"
|
||||
"<|DSML|function_calls><|DSML|invoke "
|
||||
"name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" "
|
||||
"string=\"true\">Beijing</|DSML|parameter></|DSML|invoke></"
|
||||
"|DSML|function_calls>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Some text before tool call");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
}
|
||||
|
||||
// Test trailing markdown language hint before JSON fallback tool block
|
||||
TEST_F(DeepSeek32DetectorTest, StripTrailingJsonHintBeforeJsonToolBlock) {
|
||||
std::string text =
|
||||
"Some text before tool call\n"
|
||||
"json\n"
|
||||
"{\"tool_calls\": [{\"name\": \"get_weather\", \"arguments\": "
|
||||
"{\"city\": \"Beijing\"}}]}";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Some text before tool call");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
}
|
||||
|
||||
// Test streaming with multiple tool calls
|
||||
TEST_F(DeepSeek32DetectorTest, StreamingMultipleToolCalls) {
|
||||
std::vector<std::string> chunks = {"<|DSML|function_calls>",
|
||||
"\n",
|
||||
"<|DSML|invoke",
|
||||
" name=\"get_weather\">",
|
||||
"<|DSML|parameter name=\"city\" "
|
||||
"string=\"true\">",
|
||||
"Tokyo",
|
||||
"</|DSML|parameter>",
|
||||
"</|DSML|invoke>",
|
||||
"\n",
|
||||
"<|DSML|invoke",
|
||||
" name=\"get_weather\">",
|
||||
"<|DSML|parameter name=\"city\" "
|
||||
"string=\"true\">",
|
||||
"Paris",
|
||||
"</|DSML|parameter>",
|
||||
"</|DSML|invoke>",
|
||||
"\n",
|
||||
"</|DSML|function_calls>"};
|
||||
|
||||
int tool_calls_found = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
auto stream_result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
for (const auto& call : stream_result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_calls_found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(tool_calls_found, 2)
|
||||
<< "Should find 2 tool calls in streaming mode";
|
||||
}
|
||||
|
||||
// Test streaming response with trailing empty markdown json fence before
|
||||
// tool-call chunks. The empty fence should not leak to normal_text.
|
||||
TEST_F(DeepSeek32DetectorTest,
|
||||
StreamingSkipsTrailingEmptyJsonFenceBeforeToolCall) {
|
||||
std::vector<std::string> chunks = {
|
||||
"Let me break this down.\n\nNow I'll call the tool.\n\n```json\n",
|
||||
"\n```",
|
||||
"<|DSML|invoke name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\">Boston</|DSML|parameter></"
|
||||
"|DSML|invoke>"};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
int tool_name_events = 0;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_name_events++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(tool_name_events, 1);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("```json") == std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("```") == std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("Now I'll call the tool") !=
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
// Test streaming response with trailing placeholder punctuation and fence at
|
||||
// stream end. These artifacts should be removed from flushed normal_text.
|
||||
TEST_F(DeepSeek32DetectorTest,
|
||||
StreamingFlushStripsTrailingPlaceholderAndFence) {
|
||||
std::vector<std::string> chunks = {
|
||||
"Let me break this down.\n\n"
|
||||
"The user is asking for weather info.\n\n"
|
||||
"I will call the weather tool now.\n\n"
|
||||
"、、、\n"
|
||||
"```\n"};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
}
|
||||
|
||||
auto flush_result = detector_->parse_streaming_increment("", tools_);
|
||||
if (!flush_result.normal_text.empty()) {
|
||||
accumulated_normal_text += flush_result.normal_text;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(accumulated_normal_text.find(
|
||||
"I will call the weather tool now.") != std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("、、、") == std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("```") == std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("\n\n\n") == std::string::npos);
|
||||
EXPECT_TRUE(!accumulated_normal_text.empty() &&
|
||||
accumulated_normal_text.back() != ' ');
|
||||
}
|
||||
|
||||
// Test standalone placeholder/fence chunks right before invoke start.
|
||||
// They should not leak into streamed normal_text.
|
||||
TEST_F(DeepSeek32DetectorTest,
|
||||
StreamingDropsStandalonePlaceholderChunkBeforeToolCall) {
|
||||
std::vector<std::string> chunks = {
|
||||
"I'll use the weather tool for Boston.\n\n",
|
||||
"、、、",
|
||||
"\n```",
|
||||
"<|DSML|invoke name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\">Boston</|DSML|parameter></"
|
||||
"|DSML|invoke>"};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
int tool_name_events = 0;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_name_events++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(tool_name_events, 1);
|
||||
EXPECT_TRUE(
|
||||
accumulated_normal_text.find("I'll use the weather tool for Boston.") !=
|
||||
std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("、、、") == std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("```") == std::string::npos);
|
||||
}
|
||||
|
||||
// Test that deferred trailing whitespace does not grow into large blank blocks
|
||||
// across chunks.
|
||||
TEST_F(DeepSeek32DetectorTest,
|
||||
StreamingDeferredWhitespaceDoesNotAccumulateBlankLines) {
|
||||
std::vector<std::string> chunks = {"First line.\n\n\n", "Second line."};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(accumulated_normal_text.find("First line.") != std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("Second line.") !=
|
||||
std::string::npos);
|
||||
EXPECT_TRUE(accumulated_normal_text.find("\n\n\n") == std::string::npos);
|
||||
}
|
||||
|
||||
// Test tool call without function_calls wrapper
|
||||
TEST_F(DeepSeek32DetectorTest, ToolCallWithoutWrapper) {
|
||||
std::string text =
|
||||
"Direct invoke "
|
||||
"<|DSML|invoke name=\"get_weather\"><|DSML|parameter "
|
||||
"name=\"city\" string=\"true\">Shanghai</|DSML|parameter></"
|
||||
"|DSML|invoke>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Direct invoke");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_weather");
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
@@ -0,0 +1,663 @@
|
||||
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "deepseekv3_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
class DeepSeekV3DetectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
detector_ = std::make_unique<DeepSeekV3Detector>();
|
||||
|
||||
// Setup test tools
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"location",
|
||||
{{"type", "string"},
|
||||
{"description", "The city and state, e.g. San Francisco, CA"}}},
|
||||
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
|
||||
{"required", {"location"}}};
|
||||
|
||||
JsonFunction weather_func("get_current_weather",
|
||||
"Get the current weather in a given location",
|
||||
weather_params);
|
||||
weather_tool_ = JsonTool("function", weather_func);
|
||||
|
||||
nlohmann::json calculator_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"expression",
|
||||
{{"type", "string"},
|
||||
{"description", "Mathematical expression to evaluate"}}}}},
|
||||
{"required", {"expression"}}};
|
||||
|
||||
JsonFunction calculator_func(
|
||||
"calculate", "Calculate mathematical expressions", calculator_params);
|
||||
calculator_tool_ = JsonTool("function", calculator_func);
|
||||
|
||||
tools_ = {weather_tool_, calculator_tool_};
|
||||
}
|
||||
|
||||
std::unique_ptr<DeepSeekV3Detector> detector_;
|
||||
JsonTool weather_tool_;
|
||||
JsonTool calculator_tool_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
// Test constructor and basic properties
|
||||
TEST_F(DeepSeekV3DetectorTest, ConstructorInitializesCorrectly) {
|
||||
EXPECT_NE(detector_, nullptr);
|
||||
|
||||
// Test basic token detection
|
||||
std::string text_with_tool_call =
|
||||
"Some text "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test\n`"
|
||||
"``json\n{\"name\": "
|
||||
"\"test\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
// Test has_tool_call method
|
||||
TEST_F(DeepSeekV3DetectorTest, HasToolCallDetection) {
|
||||
// Test text containing tool calls
|
||||
EXPECT_TRUE(detector_->has_tool_call("<|tool▁calls▁begin|>"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"Previous text <|tool▁calls▁begin|>Following content"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test\n`"
|
||||
"``json\n{\"name\": "
|
||||
"\"test\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>"));
|
||||
|
||||
// Test text not containing tool calls
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("tool_calls without special tokens"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("<tool_call> without unicode tokens"));
|
||||
}
|
||||
|
||||
// Test single tool call parsing
|
||||
TEST_F(DeepSeekV3DetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": \"Beijing\", \"unit\": "
|
||||
"\"celsius\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
// Test multiple tool calls parsing
|
||||
TEST_F(DeepSeekV3DetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather and calculate an expression "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": "
|
||||
"\"Shanghai\"}\n```<|tool▁call▁end|>\n<|tool▁call▁begin|>function<"
|
||||
"|tool▁sep|>calculate\n```json\n{\"expression\": \"2 + 3 * "
|
||||
"4\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"Please help me check the weather and calculate an expression");
|
||||
EXPECT_EQ(result.calls.size(), 2);
|
||||
|
||||
// Verify first tool call
|
||||
const auto& call1 = result.calls[0];
|
||||
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call1.name.has_value());
|
||||
EXPECT_EQ(call1.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
|
||||
EXPECT_EQ(params1["location"], "Shanghai");
|
||||
|
||||
// Verify second tool call
|
||||
const auto& call2 = result.calls[1];
|
||||
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call2.name.has_value());
|
||||
EXPECT_EQ(call2.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
|
||||
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
|
||||
}
|
||||
|
||||
// Test DeepSeekV3 specific format with exact tokens
|
||||
TEST_F(DeepSeekV3DetectorTest, DeepSeekV3SpecificFormat) {
|
||||
std::string text =
|
||||
"I need weather info "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": "
|
||||
"\"Tokyo\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "I need weather info");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Tokyo");
|
||||
}
|
||||
|
||||
// Test invalid JSON handling
|
||||
TEST_F(DeepSeekV3DetectorTest, InvalidJsonHandling) {
|
||||
std::string text =
|
||||
"Test invalid JSON "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": \"Beijing\", "
|
||||
"invalid_json}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test invalid JSON");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Invalid JSON should be ignored
|
||||
}
|
||||
|
||||
// Test empty tool call content
|
||||
TEST_F(DeepSeekV3DetectorTest, EmptyToolCallContent) {
|
||||
std::string text =
|
||||
"Test empty content "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>\n```"
|
||||
"json\n \t\n \n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test empty content");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
|
||||
}
|
||||
|
||||
// Test incomplete tool call (only start tag)
|
||||
TEST_F(DeepSeekV3DetectorTest, IncompleteToolCall) {
|
||||
std::string text =
|
||||
"Incomplete tool call "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": \"Beijing\"}";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Incomplete tool call");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
|
||||
}
|
||||
|
||||
// Test unknown tool name handling
|
||||
TEST_F(DeepSeekV3DetectorTest, UnknownToolName) {
|
||||
std::string text =
|
||||
"Unknown tool "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>"
|
||||
"unknown_tool\n```json\n{\"param\": "
|
||||
"\"value\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Unknown tool");
|
||||
// Base class will skip unknown tools, so should be 0 calls
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test case with only normal text
|
||||
TEST_F(DeepSeekV3DetectorTest, OnlyNormalText) {
|
||||
std::string text = "This is a regular text without any tool calls.";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"This is a regular text without any tool calls.");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test empty string input
|
||||
TEST_F(DeepSeekV3DetectorTest, EmptyStringInput) {
|
||||
std::string text = "";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test whitespace-only input
|
||||
TEST_F(DeepSeekV3DetectorTest, WhitespaceOnlyInput) {
|
||||
std::string text = " \t\n\r ";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test complex nested JSON parameters
|
||||
TEST_F(DeepSeekV3DetectorTest, ComplexNestedJsonParameters) {
|
||||
std::string text =
|
||||
"Complex parameter test "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": \"Beijing\", \"options\": "
|
||||
"{\"include_forecast\": true, \"days\": 7, \"details\": "
|
||||
"[\"temperature\", \"humidity\", "
|
||||
"\"wind\"]}}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Complex parameter test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_TRUE(params["options"]["include_forecast"]);
|
||||
EXPECT_EQ(params["options"]["days"], 7);
|
||||
EXPECT_EQ(params["options"]["details"].size(), 3);
|
||||
}
|
||||
|
||||
// Test tool call in the middle of text
|
||||
TEST_F(DeepSeekV3DetectorTest, ToolCallInMiddleOfText) {
|
||||
std::string text =
|
||||
"Previous text "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>"
|
||||
"calculate\n```json\n{\"expression\": "
|
||||
"\"1+1\"}\n```<|tool▁call▁end|><|tool▁calls▁end|> Following text";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
// Note: According to implementation, only text before tool call is preserved
|
||||
// as normal_text
|
||||
EXPECT_EQ(result.normal_text, "Previous text");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
}
|
||||
|
||||
// Test special characters handling
|
||||
TEST_F(DeepSeekV3DetectorTest, SpecialCharactersHandling) {
|
||||
std::string text =
|
||||
"Special characters test "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": \"New York City\", \"note\": "
|
||||
"\"Contains "
|
||||
"symbols!@#$%^&*()_+=\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Special characters test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "New York City");
|
||||
EXPECT_EQ(params["note"], "Contains symbols!@#$%^&*()_+=");
|
||||
}
|
||||
|
||||
// Test whitespace trimming
|
||||
TEST_F(DeepSeekV3DetectorTest, WhitespaceTrimming) {
|
||||
std::string text_with_whitespace =
|
||||
" \t\nPrevious text\r\n "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n{\"location\": "
|
||||
"\"Beijing\"}\n```<|tool▁call▁end|><|tool▁calls▁end|> \t\r\n";
|
||||
|
||||
auto result = detector_->detect_and_parse(text_with_whitespace, tools_);
|
||||
|
||||
// Verify normal text is correctly trimmed
|
||||
EXPECT_EQ(result.normal_text, "Previous text");
|
||||
|
||||
// Verify tool call is correctly parsed
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].tool_index, -1); // Base class always returns -1
|
||||
}
|
||||
|
||||
// Test regex pattern matching edge cases
|
||||
TEST_F(DeepSeekV3DetectorTest, RegexPatternEdgeCases) {
|
||||
// Test with newlines in function name (should fail)
|
||||
std::string text1 =
|
||||
"Test "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current\nweather\n```json\n{\"location\": "
|
||||
"\"Beijing\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
auto result1 = detector_->detect_and_parse(text1, tools_);
|
||||
EXPECT_EQ(result1.calls.size(), 0); // Should fail to match
|
||||
|
||||
// Test with missing json markers
|
||||
std::string text2 =
|
||||
"Test "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n{\"location\": "
|
||||
"\"Beijing\"}\n<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
auto result2 = detector_->detect_and_parse(text2, tools_);
|
||||
EXPECT_EQ(result2.calls.size(),
|
||||
0); // Should fail to match without ```json``` markers
|
||||
}
|
||||
|
||||
// Performance test: multiple tool calls
|
||||
TEST_F(DeepSeekV3DetectorTest, PerformanceWithMultipleToolCalls) {
|
||||
std::string text = "Performance test";
|
||||
|
||||
// Build text containing multiple tool calls
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
text +=
|
||||
" <|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>"
|
||||
"calculate\n```json\n{\"expression\": \"" +
|
||||
std::to_string(i) + " + " + std::to_string(i + 1) +
|
||||
"\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
}
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Performance test");
|
||||
EXPECT_EQ(result.calls.size(), 10000);
|
||||
|
||||
// Verify each tool call is correctly parsed
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
const auto& call = result.calls[i];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
std::string expected_expr =
|
||||
std::to_string(i) + " + " + std::to_string(i + 1);
|
||||
EXPECT_EQ(params["expression"], expected_expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Test error handling with malformed tokens
|
||||
TEST_F(DeepSeekV3DetectorTest, MalformedTokensHandling) {
|
||||
// Test with incomplete start token
|
||||
std::string text1 =
|
||||
"Test "
|
||||
"<|tool▁calls▁begi><|tool▁call▁begin|>function<|tool▁sep|>test\n```"
|
||||
"json\n{}\n```<|tool▁call▁end|><|tool▁calls▁end|>";
|
||||
auto result1 = detector_->detect_and_parse(text1, tools_);
|
||||
EXPECT_EQ(result1.normal_text,
|
||||
"Test "
|
||||
"<|tool▁calls▁begi><|tool▁call▁begin|>function<|tool▁sep|>"
|
||||
"test\n```json\n{}\n```<|tool▁call▁end|><|tool▁calls▁end|>");
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Test with incomplete end token
|
||||
std::string text2 =
|
||||
"Test "
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test\n`"
|
||||
"``json\n{}\n```<|tool▁call▁end|><|tool▁calls▁en|>";
|
||||
auto result2 = detector_->detect_and_parse(text2, tools_);
|
||||
EXPECT_EQ(result2.normal_text, "Test");
|
||||
EXPECT_EQ(result2.calls.size(), 0); // Should not match incomplete pattern
|
||||
}
|
||||
|
||||
// ========== Streaming Tests ==========
|
||||
|
||||
// Test basic streaming parsing
|
||||
TEST_F(DeepSeekV3DetectorTest, BasicStreamingParsing) {
|
||||
std::vector<std::string> chunks = {
|
||||
"<|tool▁calls▁begin|>",
|
||||
"<|tool▁call▁begin|>function<|tool▁sep|>get_current_weather\n```"
|
||||
"json\n",
|
||||
"{\"location\": \"Tokyo\"}",
|
||||
"\n```<|tool▁call▁end|>",
|
||||
"<|tool▁calls▁end|>"};
|
||||
|
||||
std::vector<StreamingParseResult> results;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
results.push_back(result);
|
||||
}
|
||||
|
||||
bool found_tool_name = false;
|
||||
bool found_arguments = false;
|
||||
|
||||
for (const auto& result : results) {
|
||||
if (!result.calls.empty()) {
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value() &&
|
||||
call.name.value() == "get_current_weather") {
|
||||
found_tool_name = true;
|
||||
}
|
||||
if (!call.parameters.empty() &&
|
||||
call.parameters.find("Tokyo") != std::string::npos) {
|
||||
found_arguments = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_tool_name) << "Should find tool name in streaming results";
|
||||
EXPECT_TRUE(found_arguments) << "Should find arguments in streaming results";
|
||||
}
|
||||
|
||||
// Test incremental argument streaming
|
||||
TEST_F(DeepSeekV3DetectorTest, IncrementalArgumentStreaming) {
|
||||
std::vector<std::string> chunks = {
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n",
|
||||
"{\"location\":",
|
||||
" \"San Francisco\"",
|
||||
", \"unit\": \"celsius\"",
|
||||
"}\n```<|tool▁call▁end|><|tool▁calls▁end|>"};
|
||||
|
||||
std::string accumulated_args;
|
||||
bool tool_name_sent = false;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_name_sent = true;
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
} else {
|
||||
accumulated_args += call.parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(tool_name_sent)
|
||||
<< "Tool name should be sent when tool call is complete";
|
||||
if (!accumulated_args.empty()) {
|
||||
EXPECT_TRUE(accumulated_args.find("San Francisco") != std::string::npos)
|
||||
<< "Should contain location argument";
|
||||
EXPECT_TRUE(accumulated_args.find("celsius") != std::string::npos)
|
||||
<< "Should contain unit argument";
|
||||
}
|
||||
}
|
||||
|
||||
// Test streaming with multiple tool calls
|
||||
TEST_F(DeepSeekV3DetectorTest, StreamingMultipleToolCalls) {
|
||||
std::vector<std::string> chunks = {"<|tool▁calls▁begin|>",
|
||||
"\n",
|
||||
"<|tool▁call▁begin|>",
|
||||
"function",
|
||||
"<|tool▁sep|>",
|
||||
"get_current_weather",
|
||||
"\n",
|
||||
"```",
|
||||
"json",
|
||||
"\n",
|
||||
"{",
|
||||
"\"location\"",
|
||||
":",
|
||||
" \"",
|
||||
"Tokyo",
|
||||
"\"}",
|
||||
"\n",
|
||||
"```",
|
||||
"<|tool▁call▁end|>",
|
||||
"\n",
|
||||
"<|tool▁call▁begin|>",
|
||||
"function",
|
||||
"<|tool▁sep|>",
|
||||
"get_current_weather",
|
||||
"\n",
|
||||
"```",
|
||||
"json",
|
||||
"\n",
|
||||
"{",
|
||||
"\"location\"",
|
||||
":",
|
||||
" \"",
|
||||
"Paris",
|
||||
"\",",
|
||||
" \"",
|
||||
"unit\"",
|
||||
":",
|
||||
" \"",
|
||||
"celsius",
|
||||
"\"}",
|
||||
"\n",
|
||||
"```",
|
||||
"<|tool▁call▁end|>",
|
||||
"\n",
|
||||
"<|tool▁calls▁end|>"};
|
||||
|
||||
int tool_calls_found = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
auto stream_result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
for (const auto& call : stream_result.calls) {
|
||||
if (call.name.has_value()) {
|
||||
tool_calls_found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(tool_calls_found, 2)
|
||||
<< "Should find 2 tool calls in streaming mode";
|
||||
}
|
||||
|
||||
// Test normal text handling during streaming
|
||||
TEST_F(DeepSeekV3DetectorTest, StreamingNormalTextHandling) {
|
||||
std::vector<std::string> chunks = {
|
||||
"This is normal text before tool call. ",
|
||||
"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_"
|
||||
"current_weather\n```json\n",
|
||||
"{\"location\": \"Tokyo\"}\n```<|tool▁call▁end|><|tool▁calls▁end|>",
|
||||
" And this is text after."};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
bool found_tool_call = false;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
if (!result.calls.empty()) {
|
||||
found_tool_call = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_tool_call) << "Should find tool call";
|
||||
EXPECT_TRUE(accumulated_normal_text.find("This is normal text") !=
|
||||
std::string::npos)
|
||||
<< "Should preserve normal text before tool call";
|
||||
// Note: We don't expect "And this is text after" to be in
|
||||
// accumulated_normal_text
|
||||
}
|
||||
|
||||
// Test partial token handling
|
||||
TEST_F(DeepSeekV3DetectorTest, StreamingPartialTokenHandling) {
|
||||
std::vector<std::string> chunks = {
|
||||
"<|tool▁calls▁beg", // Partial start token
|
||||
"in|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather\n```"
|
||||
"json\n",
|
||||
"{\"location\": \"Tokyo\"}\n```<|tool▁call▁e", // Partial end token
|
||||
"nd|><|tool▁calls▁end|>"};
|
||||
|
||||
bool found_tool_call = false;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
|
||||
if (!result.calls.empty()) {
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value() &&
|
||||
call.name.value() == "get_current_weather") {
|
||||
found_tool_call = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_tool_call) << "Should handle partial tokens correctly";
|
||||
}
|
||||
|
||||
// Test streaming with empty chunks
|
||||
TEST_F(DeepSeekV3DetectorTest, StreamingEmptyChunks) {
|
||||
std::vector<std::string> chunks = {
|
||||
"",
|
||||
"<|tool▁calls▁begin|>",
|
||||
"",
|
||||
"<|tool▁call▁begin|>function<|tool▁sep|>get_current_weather\n```"
|
||||
"json\n",
|
||||
"",
|
||||
"{\"location\": \"Tokyo\"}",
|
||||
"",
|
||||
"\n```<|tool▁call▁end|><|tool▁calls▁end|>",
|
||||
""};
|
||||
|
||||
bool found_tool_call = false;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = detector_->parse_streaming_increment(chunk, tools_);
|
||||
|
||||
if (!result.calls.empty()) {
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.name.has_value() &&
|
||||
call.name.value() == "get_current_weather") {
|
||||
found_tool_call = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_tool_call) << "Should handle empty chunks correctly";
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
499
upstream_ref/xllm/tests/function_call/glm45_detector_test.cpp
Normal file
499
upstream_ref/xllm/tests/function_call/glm45_detector_test.cpp
Normal file
@@ -0,0 +1,499 @@
|
||||
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "glm45_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
class Glm45DetectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
detector_ = std::make_unique<Glm45Detector>();
|
||||
|
||||
// Setup test tools
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"location",
|
||||
{{"type", "string"},
|
||||
{"description", "The city and state, e.g. San Francisco, CA"}}},
|
||||
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
|
||||
{"required", {"location"}}};
|
||||
|
||||
JsonFunction weather_func("get_current_weather",
|
||||
"Get the current weather in a given location",
|
||||
weather_params);
|
||||
weather_tool_ = JsonTool("function", weather_func);
|
||||
|
||||
nlohmann::json calculator_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"expression",
|
||||
{{"type", "string"},
|
||||
{"description", "Mathematical expression to evaluate"}}}}},
|
||||
{"required", {"expression"}}};
|
||||
|
||||
JsonFunction calculator_func(
|
||||
"calculate", "Calculate mathematical expressions", calculator_params);
|
||||
calculator_tool_ = JsonTool("function", calculator_func);
|
||||
|
||||
tools_ = {weather_tool_, calculator_tool_};
|
||||
}
|
||||
|
||||
std::unique_ptr<Glm45Detector> detector_;
|
||||
JsonTool weather_tool_;
|
||||
JsonTool calculator_tool_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
// Test constructor and basic properties
|
||||
TEST_F(Glm45DetectorTest, ConstructorInitializesCorrectly) {
|
||||
EXPECT_NE(detector_, nullptr);
|
||||
|
||||
// Test basic token detection
|
||||
std::string text_with_tool_call =
|
||||
"Some text "
|
||||
"<tool_call>test\n<arg_key>param</arg_key>\n<arg_value>value</"
|
||||
"arg_value>\n</tool_call>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
// Test has_tool_call method
|
||||
TEST_F(Glm45DetectorTest, HasToolCallDetection) {
|
||||
// Test text containing tool calls
|
||||
EXPECT_TRUE(detector_->has_tool_call("<tool_call>"));
|
||||
EXPECT_TRUE(
|
||||
detector_->has_tool_call("Previous text <tool_call>Following content"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"<tool_call>get_weather\n<arg_key>city</arg_key>\n<arg_value>Beijing</"
|
||||
"arg_value>\n</tool_call>"));
|
||||
|
||||
// Test text not containing tool calls
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("tool_call without brackets"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("<tool_call without closing"));
|
||||
}
|
||||
|
||||
// Test single tool call parsing
|
||||
TEST_F(Glm45DetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"<arg_key>unit</arg_key>\n<arg_value>celsius</arg_value>\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
// Test multiple tool calls parsing
|
||||
TEST_F(Glm45DetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather and calculate an expression "
|
||||
"<tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Shanghai</arg_value>\n</"
|
||||
"tool_call>"
|
||||
"<tool_call>calculate\n"
|
||||
"<arg_key>expression</arg_key>\n<arg_value>2 + 3 * "
|
||||
"4</arg_value>\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"Please help me check the weather and calculate an expression");
|
||||
EXPECT_EQ(result.calls.size(), 2);
|
||||
|
||||
// Verify first tool call
|
||||
const auto& call1 = result.calls[0];
|
||||
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call1.name.has_value());
|
||||
EXPECT_EQ(call1.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
|
||||
EXPECT_EQ(params1["location"], "Shanghai");
|
||||
|
||||
// Verify second tool call
|
||||
const auto& call2 = result.calls[1];
|
||||
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call2.name.has_value());
|
||||
EXPECT_EQ(call2.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
|
||||
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
|
||||
}
|
||||
|
||||
// Test GLM-4.5 specific format with Chinese characters
|
||||
TEST_F(Glm45DetectorTest, Glm45SpecificFormatWithChinese) {
|
||||
std::string text =
|
||||
"I need weather info "
|
||||
"<tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>北京</arg_value>\n"
|
||||
"<arg_key>unit</arg_key>\n<arg_value>celsius</arg_value>\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "I need weather info");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "北京");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
// Test empty tool call content
|
||||
TEST_F(Glm45DetectorTest, EmptyToolCallContent) {
|
||||
std::string text =
|
||||
"Test empty content <tool_call>test\n \t\n \n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test empty content");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
|
||||
}
|
||||
|
||||
// Test incomplete tool call (only start tag)
|
||||
TEST_F(Glm45DetectorTest, IncompleteToolCall) {
|
||||
std::string text =
|
||||
"Incomplete tool call <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Incomplete tool call");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
|
||||
}
|
||||
|
||||
// Test unknown tool name handling
|
||||
TEST_F(Glm45DetectorTest, UnknownToolName) {
|
||||
std::string text =
|
||||
"Unknown tool <tool_call>unknown_tool\n"
|
||||
"<arg_key>param</arg_key>\n<arg_value>value</arg_value>\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Unknown tool");
|
||||
// Base class will skip unknown tools, so should be 0 calls
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test case with only normal text
|
||||
TEST_F(Glm45DetectorTest, OnlyNormalText) {
|
||||
std::string text = "This is a regular text without any tool calls.";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"This is a regular text without any tool calls.");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test empty string input
|
||||
TEST_F(Glm45DetectorTest, EmptyStringInput) {
|
||||
std::string text = "";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test whitespace-only input
|
||||
TEST_F(Glm45DetectorTest, WhitespaceOnlyInput) {
|
||||
std::string text = " \t\n\r ";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test complex nested parameters (JSON values)
|
||||
TEST_F(Glm45DetectorTest, ComplexNestedJsonParameters) {
|
||||
std::string text =
|
||||
"Complex parameter test <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"<arg_key>options</arg_key>\n<arg_value>{\"include_forecast\": true, "
|
||||
"\"days\": 7}</arg_value>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Complex parameter test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_TRUE(params["options"]["include_forecast"]);
|
||||
EXPECT_EQ(params["options"]["days"], 7);
|
||||
}
|
||||
|
||||
// Test special characters handling
|
||||
TEST_F(Glm45DetectorTest, SpecialCharactersHandling) {
|
||||
std::string text =
|
||||
"Special characters test <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>New York City</arg_value>\n"
|
||||
"<arg_key>note</arg_key>\n<arg_value>Contains "
|
||||
"symbols!@#$%^&*()_+=</arg_value>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Special characters test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "New York City");
|
||||
EXPECT_EQ(params["note"], "Contains symbols!@#$%^&*()_+=");
|
||||
}
|
||||
|
||||
// Test tool call in the middle of text
|
||||
TEST_F(Glm45DetectorTest, ToolCallInMiddleOfText) {
|
||||
std::string text =
|
||||
"Previous text <tool_call>calculate\n"
|
||||
"<arg_key>expression</arg_key>\n<arg_value>1+1</arg_value>\n"
|
||||
"</tool_call> Following text";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
// Note: According to GLM-4.5 implementation, only text before tool call is
|
||||
// preserved as normal_text
|
||||
EXPECT_EQ(result.normal_text, "Previous text");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
}
|
||||
|
||||
// Test malformed arg tags handling
|
||||
TEST_F(Glm45DetectorTest, MalformedArgTagsHandling) {
|
||||
std::string text =
|
||||
"Malformed args test <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key><arg_value>Beijing</arg_value>\n" // Missing
|
||||
// newline
|
||||
"<arg_key>unit<arg_value>celsius</arg_value>\n" // Missing closing tag
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Malformed args test");
|
||||
// Should still parse what it can
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
// The malformed unit parameter should not be parsed
|
||||
EXPECT_FALSE(params.contains("unit"));
|
||||
}
|
||||
|
||||
// Test whitespace handling in arg values
|
||||
TEST_F(Glm45DetectorTest, WhitespaceHandlingInArgValues) {
|
||||
std::string text =
|
||||
"Whitespace test <tool_call>get_current_weather\n"
|
||||
"<arg_key> location </arg_key>\n<arg_value> Beijing </arg_value>\n"
|
||||
"<arg_key>\t\nunit\r\n</arg_key>\n<arg_value>\n\tcelsius\r\n</"
|
||||
"arg_value>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Whitespace test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing"); // Whitespace should be trimmed
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
// Test multiple sections (edge case)
|
||||
TEST_F(Glm45DetectorTest, MultipleSections) {
|
||||
std::string text =
|
||||
"First section <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"</tool_call> Middle text <tool_call>calculate\n"
|
||||
"<arg_key>expression</arg_key>\n<arg_value>1+1</arg_value>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
// Should extract text before first tool call
|
||||
EXPECT_EQ(result.normal_text, "First section");
|
||||
// Should parse all tool calls
|
||||
EXPECT_EQ(result.calls.size(), 2);
|
||||
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result.calls[1].name.value(), "calculate");
|
||||
}
|
||||
|
||||
// Test performance with many tool calls
|
||||
TEST_F(Glm45DetectorTest, PerformanceWithManyToolCalls) {
|
||||
std::string text = "Performance test ";
|
||||
|
||||
// Build text containing multiple tool calls
|
||||
for (int i = 0; i < 100; ++i) { // Reduced from 10000 for faster testing
|
||||
text +=
|
||||
"<tool_call>calculate\n"
|
||||
"<arg_key>expression</arg_key>\n<arg_value>" +
|
||||
std::to_string(i) + " + " + std::to_string(i + 1) +
|
||||
"</arg_value>\n</tool_call>";
|
||||
}
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Performance test");
|
||||
EXPECT_EQ(result.calls.size(), 100);
|
||||
|
||||
// Verify each tool call is correctly parsed
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
const auto& call = result.calls[i];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
std::string expected_expr =
|
||||
std::to_string(i) + " + " + std::to_string(i + 1);
|
||||
EXPECT_EQ(params["expression"], expected_expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Test edge case: nested braces in JSON values
|
||||
TEST_F(Glm45DetectorTest, NestedBracesInJsonValues) {
|
||||
std::string text =
|
||||
"Nested braces test <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"<arg_key>config</arg_key>\n<arg_value>{\"nested\": {\"deep\": "
|
||||
"\"value\"}}</arg_value>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Nested braces test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
|
||||
}
|
||||
|
||||
// Test streaming parsing functionality
|
||||
TEST_F(Glm45DetectorTest, StreamingParseBasicFunctionality) {
|
||||
std::string chunk1 = "<tool_call>get_current_weather\n";
|
||||
std::string chunk2 = "<arg_key>location</arg_key>\n<arg_value>";
|
||||
std::string chunk3 = "Beijing</arg_value>\n</tool_call>";
|
||||
|
||||
// First chunk - should buffer and wait for complete tool call
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.normal_text, "");
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - still incomplete
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.normal_text, "");
|
||||
EXPECT_EQ(result2.calls.size(), 0);
|
||||
|
||||
// Third chunk - completes the tool call
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_EQ(result3.normal_text, "");
|
||||
EXPECT_EQ(result3.calls.size(), 1);
|
||||
EXPECT_EQ(result3.calls[0].name.value(), "get_current_weather");
|
||||
}
|
||||
|
||||
// Test streaming parse with normal text
|
||||
TEST_F(Glm45DetectorTest, StreamingParseWithNormalText) {
|
||||
std::string chunk1 = "Please check the weather ";
|
||||
std::string chunk2 = "<tool_call>get_current_weather\n";
|
||||
std::string chunk3 =
|
||||
"<arg_key>location</arg_key>\n<arg_value>Tokyo</arg_value>\n</tool_call>";
|
||||
|
||||
// First chunk - normal text should be returned
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.normal_text, "Please check the weather ");
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - tool call start, should be buffered
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.normal_text, "");
|
||||
EXPECT_EQ(result2.calls.size(), 0);
|
||||
|
||||
// Third chunk - complete tool call
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_EQ(result3.normal_text, "");
|
||||
EXPECT_EQ(result3.calls.size(), 1);
|
||||
EXPECT_EQ(result3.calls[0].name.value(), "get_current_weather");
|
||||
}
|
||||
|
||||
// Test invalid JSON in arg values
|
||||
TEST_F(Glm45DetectorTest, InvalidJsonInArgValues) {
|
||||
std::string text =
|
||||
"Invalid JSON test <tool_call>get_current_weather\n"
|
||||
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"<arg_key>config</arg_key>\n<arg_value>{invalid json}</arg_value>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Invalid JSON test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["config"], "{invalid json}"); // Should be treated as string
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
767
upstream_ref/xllm/tests/function_call/glm47_detector_test.cpp
Normal file
767
upstream_ref/xllm/tests/function_call/glm47_detector_test.cpp
Normal file
@@ -0,0 +1,767 @@
|
||||
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "glm47_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
class Glm47DetectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
detector_ = std::make_unique<Glm47Detector>();
|
||||
|
||||
// Setup test tools
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"city",
|
||||
{{"type", "string"},
|
||||
{"description", "The city name, e.g. Beijing, Shanghai"}}},
|
||||
{"date",
|
||||
{{"type", "string"},
|
||||
{"description", "Date in YYYY-MM-DD format"}}}}},
|
||||
{"required", {"city"}}};
|
||||
|
||||
JsonFunction weather_func("get_weather",
|
||||
"Get the weather information for a given city",
|
||||
weather_params);
|
||||
weather_tool_ = JsonTool("function", weather_func);
|
||||
|
||||
nlohmann::json calculator_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"expression",
|
||||
{{"type", "string"},
|
||||
{"description", "Mathematical expression to evaluate"}}},
|
||||
{"precision",
|
||||
{{"type", "number"}, {"description", "Number of decimal places"}}}}},
|
||||
{"required", {"expression"}}};
|
||||
|
||||
JsonFunction calculator_func(
|
||||
"calculate", "Calculate mathematical expressions", calculator_params);
|
||||
calculator_tool_ = JsonTool("function", calculator_func);
|
||||
|
||||
tools_ = {weather_tool_, calculator_tool_};
|
||||
}
|
||||
|
||||
std::unique_ptr<Glm47Detector> detector_;
|
||||
JsonTool weather_tool_;
|
||||
JsonTool calculator_tool_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
// Test constructor and basic properties
|
||||
TEST_F(Glm47DetectorTest, ConstructorInitializesCorrectly) {
|
||||
EXPECT_NE(detector_, nullptr);
|
||||
|
||||
// Test basic token detection (GLM-4.7 compact format)
|
||||
std::string text_with_tool_call =
|
||||
"Some text "
|
||||
"<tool_call>test<arg_key>param</arg_key><arg_value>value</arg_value></"
|
||||
"tool_call>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
// Test has_tool_call method
|
||||
TEST_F(Glm47DetectorTest, HasToolCallDetection) {
|
||||
// Test text containing tool calls
|
||||
EXPECT_TRUE(detector_->has_tool_call("<tool_call>"));
|
||||
EXPECT_TRUE(
|
||||
detector_->has_tool_call("Previous text <tool_call>Following content"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</"
|
||||
"arg_value></tool_call>"));
|
||||
|
||||
// Test text not containing tool calls
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("tool_call without brackets"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("<tool_call without closing"));
|
||||
}
|
||||
|
||||
// Test single tool call parsing (GLM-4.7 compact format)
|
||||
TEST_F(Glm47DetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</arg_value>"
|
||||
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "北京");
|
||||
EXPECT_EQ(params["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test multiple tool calls parsing (GLM-4.7 format)
|
||||
TEST_F(Glm47DetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather and calculate "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>上海</arg_value>"
|
||||
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>"
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</arg_value>"
|
||||
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"Please help me check the weather and calculate");
|
||||
ASSERT_EQ(result.calls.size(), 2); // Use ASSERT to stop test if this fails
|
||||
|
||||
// Verify first tool call
|
||||
const auto& call1 = result.calls[0];
|
||||
EXPECT_EQ(call1.tool_index, -1);
|
||||
EXPECT_TRUE(call1.name.has_value());
|
||||
EXPECT_EQ(call1.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
|
||||
EXPECT_EQ(params1["city"], "上海");
|
||||
EXPECT_EQ(params1["date"], "2024-06-27");
|
||||
|
||||
// Verify second tool call
|
||||
const auto& call2 = result.calls[1];
|
||||
EXPECT_EQ(call2.tool_index, -1);
|
||||
EXPECT_TRUE(call2.name.has_value());
|
||||
EXPECT_EQ(call2.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
|
||||
EXPECT_EQ(params2["city"], "北京");
|
||||
EXPECT_EQ(params2["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test GLM-4.7 specific compact format
|
||||
TEST_F(Glm47DetectorTest, Glm47CompactFormat) {
|
||||
std::string text =
|
||||
"Weather query "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
|
||||
"arg_value></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Weather query");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
}
|
||||
|
||||
// Test number type coercion
|
||||
TEST_F(Glm47DetectorTest, NumberTypeCoercion) {
|
||||
std::string text =
|
||||
"Calculate with precision "
|
||||
"<tool_call>calculate<arg_key>expression</arg_key><arg_value>3.14 * "
|
||||
"2</arg_value>"
|
||||
"<arg_key>precision</arg_key><arg_value>2</arg_value></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Calculate with precision");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["expression"], "3.14 * 2");
|
||||
// precision should be parsed as number
|
||||
EXPECT_TRUE(params["precision"].is_number());
|
||||
EXPECT_EQ(params["precision"], 2);
|
||||
}
|
||||
|
||||
// Test empty tool call content
|
||||
TEST_F(Glm47DetectorTest, EmptyToolCallContent) {
|
||||
std::string text = "Test empty content <tool_call>test</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test empty content");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
|
||||
}
|
||||
|
||||
// Test incomplete tool call
|
||||
TEST_F(Glm47DetectorTest, IncompleteToolCall) {
|
||||
std::string text =
|
||||
"Incomplete tool call "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Incomplete tool call");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
|
||||
}
|
||||
|
||||
// Test unknown tool name handling
|
||||
TEST_F(Glm47DetectorTest, UnknownToolName) {
|
||||
std::string text =
|
||||
"Unknown tool "
|
||||
"<tool_call>unknown_tool<arg_key>param</arg_key><arg_value>value</"
|
||||
"arg_value></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Unknown tool");
|
||||
// Base class will skip unknown tools
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test case with only normal text
|
||||
TEST_F(Glm47DetectorTest, OnlyNormalText) {
|
||||
std::string text = "This is a regular text without any tool calls.";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"This is a regular text without any tool calls.");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test empty string input
|
||||
TEST_F(Glm47DetectorTest, EmptyStringInput) {
|
||||
std::string text = "";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test complex nested JSON parameters
|
||||
TEST_F(Glm47DetectorTest, ComplexNestedJsonParameters) {
|
||||
std::string text =
|
||||
"Complex parameter test "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
|
||||
"arg_value>"
|
||||
"<arg_key>options</arg_key><arg_value>{\"include_forecast\": true, "
|
||||
"\"days\": 7}</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Complex parameter test");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1);
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
EXPECT_TRUE(params["options"]["include_forecast"]);
|
||||
EXPECT_EQ(params["options"]["days"], 7);
|
||||
}
|
||||
|
||||
// Test special characters handling
|
||||
TEST_F(Glm47DetectorTest, SpecialCharactersHandling) {
|
||||
std::string text =
|
||||
"Special characters test "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>New York "
|
||||
"City</arg_value>"
|
||||
"<arg_key>note</arg_key><arg_value>Contains "
|
||||
"symbols!@#$%^&*()_+=</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Special characters test");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1);
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "New York City");
|
||||
EXPECT_EQ(params["note"], "Contains symbols!@#$%^&*()_+=");
|
||||
}
|
||||
|
||||
// Test whitespace handling in arg values
|
||||
TEST_F(Glm47DetectorTest, WhitespaceHandlingInArgValues) {
|
||||
std::string text =
|
||||
"Whitespace test "
|
||||
"<tool_call>get_weather<arg_key> city </arg_key><arg_value> Beijing "
|
||||
"</arg_value>"
|
||||
"<arg_key>\t\ndate\r\n</arg_key><arg_value>\n\t2024-06-27\r\n</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Whitespace test");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing"); // Whitespace should be trimmed
|
||||
EXPECT_EQ(params["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
// Test streaming parsing functionality
|
||||
TEST_F(Glm47DetectorTest, StreamingParseBasicFunctionality) {
|
||||
std::string chunk1 = "<tool_call>get_weather";
|
||||
std::string chunk2 = "<arg_key>city</arg_key><arg_value>";
|
||||
std::string chunk3 = "Beijing</arg_value></tool_call>";
|
||||
|
||||
// First chunk - function name not yet complete (no <arg_key> or </tool_call>)
|
||||
// Should wait for more data to avoid sending partial names
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - now we have <arg_key>, so function name is complete
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.calls.size(), 1);
|
||||
EXPECT_TRUE(result2.calls[0].name.has_value());
|
||||
EXPECT_EQ(result2.calls[0].name.value(), "get_weather");
|
||||
|
||||
// Third chunk - completes the tool call
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_GE(result3.calls.size(), 0); // Should complete the JSON
|
||||
}
|
||||
|
||||
// Test streaming parse with normal text
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithNormalText) {
|
||||
std::string chunk1 = "Please check the weather ";
|
||||
std::string chunk2 = "<tool_call>get_weather";
|
||||
std::string chunk3 =
|
||||
"<arg_key>city</arg_key><arg_value>Tokyo</arg_value></tool_call>";
|
||||
|
||||
// First chunk - normal text should be returned
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.normal_text, "Please check the weather ");
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - tool call start, but function name not yet complete
|
||||
// (no <arg_key> or </tool_call>), should wait for more data
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.calls.size(), 0);
|
||||
|
||||
// Third chunk - now we have <arg_key>, function name is complete
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
EXPECT_EQ(result3.calls[0].name.value(), "get_weather");
|
||||
}
|
||||
|
||||
// Test invalid JSON in arg values
|
||||
TEST_F(Glm47DetectorTest, InvalidJsonInArgValues) {
|
||||
std::string text =
|
||||
"Invalid JSON test "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
|
||||
"arg_value>"
|
||||
"<arg_key>config</arg_key><arg_value>{invalid json}</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Invalid JSON test");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
EXPECT_EQ(params["config"], "{invalid json}"); // Should be treated as string
|
||||
}
|
||||
|
||||
// Test nested braces in JSON values
|
||||
TEST_F(Glm47DetectorTest, NestedBracesInJsonValues) {
|
||||
std::string text =
|
||||
"Nested braces test "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
|
||||
"arg_value>"
|
||||
"<arg_key>config</arg_key><arg_value>{\"nested\": {\"deep\": "
|
||||
"\"value\"}}</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Nested braces test");
|
||||
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1);
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "Beijing");
|
||||
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
|
||||
}
|
||||
|
||||
// Test performance with many tool calls
|
||||
TEST_F(Glm47DetectorTest, PerformanceWithManyToolCalls) {
|
||||
std::string text = "Performance test ";
|
||||
|
||||
// Build text containing multiple tool calls
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
text += "<tool_call>calculate<arg_key>expression</arg_key><arg_value>" +
|
||||
std::to_string(i) + " + " + std::to_string(i + 1) +
|
||||
"</arg_value></tool_call>";
|
||||
}
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Performance test");
|
||||
ASSERT_EQ(result.calls.size(), 100); // Use ASSERT to stop test if this fails
|
||||
|
||||
// Verify each tool call is correctly parsed
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
const auto& call = result.calls[i];
|
||||
EXPECT_EQ(call.tool_index, -1);
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
std::string expected_expr =
|
||||
std::to_string(i) + " + " + std::to_string(i + 1);
|
||||
EXPECT_EQ(params["expression"], expected_expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for issue #751: std::regex stack overflow on large payloads
|
||||
// The old regex implementation with [\s\S]*? pattern caused O(n) recursion
|
||||
// depth, leading to stack overflow on inputs larger than ~46KB (depending on
|
||||
// stack size). The fix uses string::find() and substr() for O(1) stack usage.
|
||||
TEST_F(Glm47DetectorTest, LargePayloadNoStackOverflow) {
|
||||
// Create a tool that accepts large content
|
||||
nlohmann::json write_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"filename", {{"type", "string"}, {"description", "Filename"}}},
|
||||
{"content",
|
||||
{{"type", "string"}, {"description", "Content to write"}}}}},
|
||||
{"required", {"filename", "content"}}};
|
||||
|
||||
JsonFunction write_func("write_file", "Write content to file", write_params);
|
||||
JsonTool write_tool("function", write_func);
|
||||
std::vector<JsonTool> tools = {write_tool};
|
||||
|
||||
// Test with 50KB payload (larger than the ~46KB that caused stack overflow)
|
||||
std::string large_content(50000, 'A');
|
||||
std::string text =
|
||||
"Test "
|
||||
"<tool_call>write_file"
|
||||
"<arg_key>filename</arg_key><arg_value>test.txt</arg_value>"
|
||||
"<arg_key>content</arg_key><arg_value>" +
|
||||
large_content +
|
||||
"</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
// This would crash with stack overflow before the fix
|
||||
auto result = detector_->detect_and_parse(text, tools);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "write_file");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["filename"], "test.txt");
|
||||
EXPECT_EQ(params["content"].get<std::string>().size(), 50000);
|
||||
}
|
||||
|
||||
// Test with Chinese content to validate UTF-8 handling with large payloads
|
||||
TEST_F(Glm47DetectorTest, LargeChinesePayloadNoStackOverflow) {
|
||||
nlohmann::json write_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"filename", {{"type", "string"}}}, {"content", {{"type", "string"}}}}},
|
||||
{"required", {"filename", "content"}}};
|
||||
|
||||
JsonFunction write_func("write_file", "Write content to file", write_params);
|
||||
JsonTool write_tool("function", write_func);
|
||||
std::vector<JsonTool> tools = {write_tool};
|
||||
|
||||
// Generate ~50KB of Chinese content (each char is 3 bytes in UTF-8)
|
||||
std::string chinese_char = "测"; // 3 bytes in UTF-8
|
||||
std::string large_content;
|
||||
large_content.reserve(50000);
|
||||
for (int i = 0; i < 16667; ++i) { // 16667 * 3 ≈ 50KB
|
||||
large_content += chinese_char;
|
||||
}
|
||||
|
||||
std::string text =
|
||||
"<tool_call>write_file"
|
||||
"<arg_key>filename</arg_key><arg_value>中文.txt</arg_value>"
|
||||
"<arg_key>content</arg_key><arg_value>" +
|
||||
large_content +
|
||||
"</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools);
|
||||
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.name.value(), "write_file");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["filename"], "中文.txt");
|
||||
// Verify content length (each Chinese char is 3 bytes)
|
||||
EXPECT_GE(params["content"].get<std::string>().size(), 49000);
|
||||
}
|
||||
|
||||
// Test streaming with large payload
|
||||
TEST_F(Glm47DetectorTest, StreamingLargePayloadNoStackOverflow) {
|
||||
nlohmann::json write_params = {
|
||||
{"type", "object"},
|
||||
{"properties", {{"content", {{"type", "string"}}}}},
|
||||
{"required", {"content"}}};
|
||||
|
||||
JsonFunction write_func("write_file", "Write content", write_params);
|
||||
JsonTool write_tool("function", write_func);
|
||||
std::vector<JsonTool> tools = {write_tool};
|
||||
|
||||
// Build a large tool call incrementally (simulating streaming)
|
||||
std::string large_content(20000, 'X'); // 20KB for streaming test
|
||||
std::string full_text =
|
||||
"<tool_call>write_file"
|
||||
"<arg_key>content</arg_key><arg_value>" +
|
||||
large_content +
|
||||
"</arg_value>"
|
||||
"</tool_call>";
|
||||
|
||||
// Simulate streaming by sending chunks
|
||||
// Create fresh detector for streaming test
|
||||
auto streaming_detector = std::make_unique<Glm47Detector>();
|
||||
std::vector<StreamingParseResult> results;
|
||||
|
||||
size_t chunk_size = 1000; // 1KB chunks
|
||||
for (size_t i = 0; i < full_text.size(); i += chunk_size) {
|
||||
std::string chunk = full_text.substr(i, chunk_size);
|
||||
auto result = streaming_detector->parse_streaming_increment(chunk, tools);
|
||||
results.push_back(result);
|
||||
}
|
||||
|
||||
// Verify we got the function name and completed without crash
|
||||
bool found_name = false;
|
||||
bool found_args = false;
|
||||
for (const auto& r : results) {
|
||||
for (const auto& call : r.calls) {
|
||||
if (call.name.has_value() && call.name.value() == "write_file") {
|
||||
found_name = true;
|
||||
}
|
||||
if (!call.parameters.empty()) {
|
||||
found_args = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_name);
|
||||
EXPECT_TRUE(found_args);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTF-8 Streaming Tests
|
||||
// =============================================================================
|
||||
// These tests verify that multi-byte UTF-8 characters are handled correctly
|
||||
// when split across streaming chunks. The fix buffers incomplete UTF-8
|
||||
// sequences until the next chunk completes them.
|
||||
|
||||
// Test streaming with Chinese characters split across chunks
|
||||
// "北京" = 0xE5 0x8C 0x97 (北) + 0xE4 0xBA 0xAC (京)
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithChineseCharactersSplit) {
|
||||
// Chunk 1: Function name and key, start of value
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
|
||||
|
||||
// Chunk 2: First 2 bytes of "北" (0xE5 0x8C) - incomplete 3-byte sequence
|
||||
std::string chunk2 = "\xE5\x8C";
|
||||
|
||||
// Chunk 3: Last byte of "北" (0x97) + first 2 bytes of "京" (0xE4 0xBA)
|
||||
std::string chunk3 = "\x97\xE4\xBA";
|
||||
|
||||
// Chunk 4: Last byte of "京" (0xAC) + closing tags
|
||||
std::string chunk4 = "\xAC</arg_value></tool_call>";
|
||||
|
||||
// Process each chunk - should not crash with UTF-8 errors
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
// Function name is returned when <arg_key> is seen
|
||||
EXPECT_GE(result1.calls.size(), 1);
|
||||
if (result1.calls.size() > 0) {
|
||||
EXPECT_TRUE(result1.calls[0].name.has_value());
|
||||
EXPECT_EQ(result1.calls[0].name.value(), "get_weather");
|
||||
}
|
||||
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
// Should buffer incomplete UTF-8, not crash
|
||||
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
// Should complete first character and buffer second incomplete
|
||||
|
||||
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
|
||||
// Should complete the tool call
|
||||
EXPECT_GE(result4.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test with single Chinese character split (simpler case)
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithSingleChineseCharacterSplit) {
|
||||
// "北" = 0xE5 0x8C 0x97 (3-byte UTF-8)
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
|
||||
std::string chunk2 = "\xE5\x8C"; // First 2 bytes (incomplete)
|
||||
std::string chunk3 = "\x97</arg_value></tool_call>"; // Last byte + closing
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
// Should not crash with "invalid UTF-8 byte at index 1: 0xE5"
|
||||
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test with 4-byte UTF-8 emoji split
|
||||
// Emoji "😊" = 0xF0 0x9F 0x98 0x8A (4-byte UTF-8)
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithEmojiSplit) {
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Tokyo ";
|
||||
std::string chunk2 = "\xF0\x9F"; // First 2 bytes of emoji (incomplete)
|
||||
std::string chunk3 =
|
||||
"\x98\x8A</arg_value></tool_call>"; // Last 2 bytes + closing
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
// Should buffer incomplete 4-byte sequence
|
||||
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test with complete UTF-8 characters (no splitting needed)
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithCompleteUtf8) {
|
||||
// Complete Chinese characters in single chunks
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
|
||||
std::string chunk2 = "北京"; // Complete UTF-8 characters
|
||||
std::string chunk3 = "</arg_value></tool_call>";
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
// Function name is returned when <arg_key> is seen
|
||||
EXPECT_GE(result1.calls.size(), 1);
|
||||
if (result1.calls.size() > 0) {
|
||||
EXPECT_TRUE(result1.calls[0].name.has_value());
|
||||
EXPECT_EQ(result1.calls[0].name.value(), "get_weather");
|
||||
}
|
||||
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test with mixed UTF-8 and ASCII
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithMixedUtf8AndAscii) {
|
||||
// Mix of ASCII and Chinese with UTF-8 split at boundary
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>City: ";
|
||||
std::string chunk2 = "\xE5"; // First byte of "北" (incomplete)
|
||||
std::string chunk3 =
|
||||
"\x8C\x97京</arg_value></tool_call>"; // Rest of "北" + complete "京"
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
// Should buffer the single incomplete byte
|
||||
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test with Japanese characters (also 3-byte UTF-8)
|
||||
// "東京" = 0xE6 0x9D 0xB1 (東) + 0xE4 0xBA 0xAC (京)
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithJapaneseCharactersSplit) {
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
|
||||
std::string chunk2 = "\xE6\x9D"; // First 2 bytes of "東" (incomplete)
|
||||
std::string chunk3 = "\xB1\xE4\xBA\xAC</arg_value></tool_call>"; // Rest
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test with Korean characters (also 3-byte UTF-8)
|
||||
// "서울" = 0xEC 0x84 0x9C (서) + 0xEC 0x9A 0xB8 (울)
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithKoreanCharactersSplit) {
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
|
||||
std::string chunk2 = "\xEC\x84"; // First 2 bytes of "서" (incomplete)
|
||||
std::string chunk3 = "\x9C\xEC\x9A\xB8</arg_value></tool_call>"; // Rest
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
|
||||
EXPECT_GE(result3.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test UTF-8 boundary: one byte at a time for 3-byte char
|
||||
TEST_F(Glm47DetectorTest, StreamingParseWithUtf8OneByteAtATime) {
|
||||
// Split "北" (0xE5 0x8C 0x97) one byte at a time
|
||||
std::string chunk1 =
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
|
||||
std::string chunk2 = "\xE5"; // First byte
|
||||
std::string chunk3 = "\x8C"; // Second byte
|
||||
std::string chunk4 = "\x97</arg_value></tool_call>"; // Third byte + closing
|
||||
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
|
||||
|
||||
EXPECT_GE(result4.calls.size(), 1);
|
||||
}
|
||||
|
||||
// Test UTF-8 in non-streaming (full parse) mode still works
|
||||
TEST_F(Glm47DetectorTest, NonStreamingParseWithUtf8) {
|
||||
std::string text =
|
||||
"Query for city "
|
||||
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</arg_value>"
|
||||
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Query for city");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["city"], "北京");
|
||||
EXPECT_EQ(params["date"], "2024-06-27");
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
641
upstream_ref/xllm/tests/function_call/kimik2_detector_test.cpp
Normal file
641
upstream_ref/xllm/tests/function_call/kimik2_detector_test.cpp
Normal file
@@ -0,0 +1,641 @@
|
||||
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "kimik2_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
class KimiK2DetectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
detector_ = std::make_unique<KimiK2Detector>();
|
||||
|
||||
// Setup test tools
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"location",
|
||||
{{"type", "string"},
|
||||
{"description", "The city and state, e.g. San Francisco, CA"}}},
|
||||
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
|
||||
{"required", {"location"}}};
|
||||
|
||||
JsonFunction weather_func("get_current_weather",
|
||||
"Get the current weather in a given location",
|
||||
weather_params);
|
||||
weather_tool_ = JsonTool("function", weather_func);
|
||||
|
||||
nlohmann::json calculator_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"expression",
|
||||
{{"type", "string"},
|
||||
{"description", "Mathematical expression to evaluate"}}}}},
|
||||
{"required", {"expression"}}};
|
||||
|
||||
JsonFunction calculator_func(
|
||||
"calculate", "Calculate mathematical expressions", calculator_params);
|
||||
calculator_tool_ = JsonTool("function", calculator_func);
|
||||
|
||||
tools_ = {weather_tool_, calculator_tool_};
|
||||
}
|
||||
|
||||
std::unique_ptr<KimiK2Detector> detector_;
|
||||
JsonTool weather_tool_;
|
||||
JsonTool calculator_tool_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
// Test constructor and basic properties
|
||||
TEST_F(KimiK2DetectorTest, ConstructorInitializesCorrectly) {
|
||||
EXPECT_NE(detector_, nullptr);
|
||||
|
||||
// Test basic token detection
|
||||
std::string text_with_tool_call =
|
||||
"Some text "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.test:0 "
|
||||
"<|tool_call_argument_begin|>{\"param\": "
|
||||
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
// Test has_tool_call method
|
||||
TEST_F(KimiK2DetectorTest, HasToolCallDetection) {
|
||||
// Test text containing tool calls
|
||||
EXPECT_TRUE(detector_->has_tool_call("<|tool_calls_section_begin|>"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"Previous text <|tool_calls_section_begin|>Following content"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.test:0 "
|
||||
"<|tool_call_argument_begin|>{\"param\": "
|
||||
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>"));
|
||||
|
||||
// Test text not containing tool calls
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
|
||||
EXPECT_FALSE(
|
||||
detector_->has_tool_call("tool_calls_section_begin without brackets"));
|
||||
EXPECT_FALSE(
|
||||
detector_->has_tool_call("<tool_calls_section_begin without pipes"));
|
||||
}
|
||||
|
||||
// Test trim_whitespace method (indirectly tested through public interface)
|
||||
TEST_F(KimiK2DetectorTest, TrimWhitespaceHandling) {
|
||||
std::string text_with_whitespace =
|
||||
" \t\nPrevious text\r\n "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": "
|
||||
"\"Beijing\"}<|tool_call_end|><|tool_calls_section_end|> \t\r\n";
|
||||
|
||||
auto result = detector_->detect_and_parse(text_with_whitespace, tools_);
|
||||
|
||||
// Verify normal text is correctly extracted
|
||||
EXPECT_EQ(result.normal_text, " \t\nPrevious text\r\n ");
|
||||
|
||||
// Verify tool call is correctly parsed
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].tool_index, 0);
|
||||
}
|
||||
|
||||
// Test single tool call parsing
|
||||
TEST_F(KimiK2DetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
|
||||
"\"unit\": \"celsius\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather ");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
// Test multiple tool calls parsing
|
||||
TEST_F(KimiK2DetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather and calculate an expression "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": "
|
||||
"\"Shanghai\"}<|tool_call_end|><|tool_call_begin|>functions.calculate:1 "
|
||||
"<|tool_call_argument_begin|>{\"expression\": \"2 + 3 * "
|
||||
"4\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"Please help me check the weather and calculate an expression ");
|
||||
EXPECT_EQ(result.calls.size(), 2);
|
||||
|
||||
// Verify first tool call
|
||||
const auto& call1 = result.calls[0];
|
||||
EXPECT_EQ(call1.tool_index, 0);
|
||||
EXPECT_TRUE(call1.name.has_value());
|
||||
EXPECT_EQ(call1.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
|
||||
EXPECT_EQ(params1["location"], "Shanghai");
|
||||
|
||||
// Verify second tool call
|
||||
const auto& call2 = result.calls[1];
|
||||
EXPECT_EQ(call2.tool_index, 1);
|
||||
EXPECT_TRUE(call2.name.has_value());
|
||||
EXPECT_EQ(call2.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
|
||||
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
|
||||
}
|
||||
|
||||
// Test invalid JSON handling
|
||||
TEST_F(KimiK2DetectorTest, InvalidJsonHandling) {
|
||||
std::string text =
|
||||
"Test invalid JSON "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
|
||||
"invalid_json}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test invalid JSON ");
|
||||
// KimiK2 detector should still parse the call even with invalid JSON, leaving
|
||||
// JSON validation to higher levels
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
|
||||
}
|
||||
|
||||
// Test empty tool call content
|
||||
TEST_F(KimiK2DetectorTest, EmptyToolCallContent) {
|
||||
std::string text =
|
||||
"Test empty content "
|
||||
"<|tool_calls_section_begin|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test empty content ");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
|
||||
}
|
||||
|
||||
// Test incomplete tool call (only start tag)
|
||||
TEST_F(KimiK2DetectorTest, IncompleteToolCall) {
|
||||
std::string text =
|
||||
"Incomplete tool call "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Incomplete tool call ");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
|
||||
}
|
||||
|
||||
// Test unknown tool name handling
|
||||
TEST_F(KimiK2DetectorTest, UnknownToolName) {
|
||||
std::string text =
|
||||
"Unknown tool "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.unknown_tool:0 "
|
||||
"<|tool_call_argument_begin|>{\"param\": "
|
||||
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Unknown tool ");
|
||||
// KimiK2 detector should parse the call regardless of whether the tool is
|
||||
// known
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].name.value(), "unknown_tool");
|
||||
}
|
||||
|
||||
// Test case with only normal text
|
||||
TEST_F(KimiK2DetectorTest, OnlyNormalText) {
|
||||
std::string text = "This is a regular text without any tool calls.";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"This is a regular text without any tool calls.");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test empty string input
|
||||
TEST_F(KimiK2DetectorTest, EmptyStringInput) {
|
||||
std::string text = "";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test whitespace-only input
|
||||
TEST_F(KimiK2DetectorTest, WhitespaceOnlyInput) {
|
||||
std::string text = " \t\n\r ";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, " \t\n\r ");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test complex nested JSON parameters
|
||||
TEST_F(KimiK2DetectorTest, ComplexNestedJsonParameters) {
|
||||
std::string text =
|
||||
"Complex parameter test "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
|
||||
"\"options\": {\"include_forecast\": true, \"days\": 7, \"details\": "
|
||||
"[\"temperature\", \"humidity\", "
|
||||
"\"wind\"]}}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Complex parameter test ");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_TRUE(params["options"]["include_forecast"]);
|
||||
EXPECT_EQ(params["options"]["days"], 7);
|
||||
EXPECT_EQ(params["options"]["details"].size(), 3);
|
||||
}
|
||||
|
||||
// Test tool call in the middle of text
|
||||
TEST_F(KimiK2DetectorTest, ToolCallInMiddleOfText) {
|
||||
std::string text =
|
||||
"Previous text "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.calculate:0 "
|
||||
"<|tool_call_argument_begin|>{\"expression\": "
|
||||
"\"1+1\"}<|tool_call_end|><|tool_calls_section_end|> Following text";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
// Note: According to KimiK2 implementation, only text before tool call
|
||||
// section is preserved as normal_text
|
||||
EXPECT_EQ(result.normal_text, "Previous text ");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
}
|
||||
|
||||
// Test special characters handling
|
||||
TEST_F(KimiK2DetectorTest, SpecialCharactersHandling) {
|
||||
std::string text =
|
||||
"Special characters test "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": \"New York City\", "
|
||||
"\"note\": \"Contains "
|
||||
"symbols!@#$%^&*()_+=\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Special characters test ");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "New York City");
|
||||
EXPECT_EQ(params["note"], "Contains symbols!@#$%^&*()_+=");
|
||||
}
|
||||
|
||||
// Test function name extraction
|
||||
TEST_F(KimiK2DetectorTest, FunctionNameExtraction) {
|
||||
std::string text =
|
||||
"Function name test "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.my_custom_"
|
||||
"function:5 <|tool_call_argument_begin|>{\"param\": "
|
||||
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Function name test ");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 5); // Should extract the index correctly
|
||||
EXPECT_EQ(call.name.value(), "my_custom_function");
|
||||
}
|
||||
|
||||
// Test malformed function ID handling
|
||||
TEST_F(KimiK2DetectorTest, MalformedFunctionIdHandling) {
|
||||
std::string text =
|
||||
"Malformed ID test "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>invalid_format "
|
||||
"<|tool_call_argument_begin|>{\"param\": "
|
||||
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Malformed ID test ");
|
||||
// Malformed format that doesn't match regex should result in no calls
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test malformed function ID that matches regex but has invalid format
|
||||
TEST_F(KimiK2DetectorTest, MalformedButMatchingFunctionId) {
|
||||
std::string text =
|
||||
"Malformed but matching test "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>invalid.format:0 "
|
||||
"<|tool_call_argument_begin|>{\"param\": "
|
||||
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Malformed but matching test ");
|
||||
// Should parse but with empty function name due to missing "functions."
|
||||
// prefix
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), ""); // Empty function name for malformed ID
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
}
|
||||
|
||||
// Test multiple sections (edge case)
|
||||
TEST_F(KimiK2DetectorTest, MultipleSections) {
|
||||
std::string text =
|
||||
"First section "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": "
|
||||
"\"Beijing\"}<|tool_call_end|><|tool_calls_section_end|> Middle text "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.calculate:1 "
|
||||
"<|tool_call_argument_begin|>{\"expression\": "
|
||||
"\"1+1\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
// Should extract text before first section
|
||||
EXPECT_EQ(result.normal_text, "First section ");
|
||||
// Should parse all tool calls from all sections
|
||||
EXPECT_EQ(result.calls.size(), 2);
|
||||
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result.calls[1].name.value(), "calculate");
|
||||
}
|
||||
|
||||
// Performance test: many tool calls
|
||||
TEST_F(KimiK2DetectorTest, PerformanceWithManyToolCalls) {
|
||||
std::string text = "Performance test <|tool_calls_section_begin|>";
|
||||
|
||||
// Build text containing multiple tool calls
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
text += "<|tool_call_begin|>functions.calculate:" + std::to_string(i) +
|
||||
" <|tool_call_argument_begin|>{\"expression\": \"" +
|
||||
std::to_string(i) + " + " + std::to_string(i + 1) +
|
||||
"\"}<|tool_call_end|>";
|
||||
}
|
||||
text += "<|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Performance test ");
|
||||
EXPECT_EQ(result.calls.size(), 10000);
|
||||
|
||||
// Verify each tool call is correctly parsed
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
const auto& call = result.calls[i];
|
||||
EXPECT_EQ(call.tool_index, i);
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
std::string expected_expr =
|
||||
std::to_string(i) + " + " + std::to_string(i + 1);
|
||||
EXPECT_EQ(params["expression"], expected_expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Test edge case: nested braces in JSON
|
||||
TEST_F(KimiK2DetectorTest, NestedBracesInJson) {
|
||||
std::string text =
|
||||
"Nested braces test "
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
|
||||
"\"config\": {\"nested\": {\"deep\": "
|
||||
"\"value\"}}}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Nested braces test ");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
|
||||
}
|
||||
|
||||
// Test streaming parsing functionality
|
||||
TEST_F(KimiK2DetectorTest, StreamingParseBasicFunctionality) {
|
||||
std::string chunk1 =
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>";
|
||||
std::string chunk2 = "{\"location\": ";
|
||||
std::string chunk3 =
|
||||
"\"Beijing\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
// First chunk - no calls returned yet, waiting for arguments to start
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.normal_text, "");
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - function name sent with empty parameters when arguments
|
||||
// start
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.normal_text, "");
|
||||
EXPECT_EQ(result2.calls.size(), 1);
|
||||
EXPECT_TRUE(result2.calls[0].name.has_value());
|
||||
EXPECT_EQ(result2.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result2.calls[0].parameters, "");
|
||||
|
||||
// Third chunk - incremental arguments sent
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_EQ(result3.normal_text, "");
|
||||
EXPECT_EQ(result3.calls.size(), 1);
|
||||
EXPECT_FALSE(result3.calls[0].name.has_value());
|
||||
EXPECT_EQ(result3.calls[0].parameters, "{\"location\": \"Beijing\"}");
|
||||
}
|
||||
|
||||
TEST_F(KimiK2DetectorTest, StreamingParseWithNormalText) {
|
||||
// Test streaming parsing with normal text before tool call
|
||||
std::string chunk1 = "Please check the weather ";
|
||||
std::string chunk2 =
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>";
|
||||
std::string chunk3 =
|
||||
"{\"location\": \"Tokyo\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
// First chunk - normal text
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.normal_text, "Please check the weather ");
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - tool call start (no calls returned yet, waiting for
|
||||
// arguments)
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.normal_text, "");
|
||||
EXPECT_EQ(result2.calls.size(), 0);
|
||||
|
||||
// Third chunk - complete arguments (function name sent with empty parameters
|
||||
// first)
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_EQ(result3.normal_text, "");
|
||||
EXPECT_EQ(result3.calls.size(), 1);
|
||||
EXPECT_TRUE(result3.calls[0].name.has_value());
|
||||
EXPECT_EQ(result3.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result3.calls[0].parameters, "");
|
||||
}
|
||||
|
||||
TEST_F(KimiK2DetectorTest, StreamingParseMultipleToolCalls) {
|
||||
// Test streaming parsing with multiple tool calls
|
||||
std::string chunk1 =
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>";
|
||||
std::string chunk2 = "{\"location\": \"Beijing\"}<|tool_call_end|>";
|
||||
std::string chunk3 =
|
||||
"<|tool_call_begin|>functions.calculate:1 <|tool_call_argument_begin|>";
|
||||
std::string chunk4 =
|
||||
"{\"expression\": \"2+3\"}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
// First chunk - no calls returned yet, waiting for arguments
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - first tool call completes, function name sent with empty
|
||||
// parameters then arguments are sent separately
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.calls.size(), 1);
|
||||
EXPECT_TRUE(result2.calls[0].name.has_value());
|
||||
EXPECT_EQ(result2.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result2.calls[0].parameters, "");
|
||||
|
||||
// Third chunk - second tool call starts, first tool's arguments are sent
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_EQ(result3.calls.size(), 1);
|
||||
EXPECT_FALSE(
|
||||
result3.calls[0].name.has_value()); // Only parameters for first tool
|
||||
EXPECT_EQ(result3.calls[0].parameters, "{\"location\": \"Beijing\"}");
|
||||
|
||||
// Fourth chunk - second tool call completes
|
||||
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
|
||||
EXPECT_EQ(result4.calls.size(), 1);
|
||||
EXPECT_TRUE(result4.calls[0].name.has_value());
|
||||
EXPECT_EQ(result4.calls[0].name.value(), "calculate");
|
||||
EXPECT_EQ(result4.calls[0].parameters, "");
|
||||
}
|
||||
|
||||
TEST_F(KimiK2DetectorTest, StreamingParseIncrementalArguments) {
|
||||
// Test streaming parsing with arguments arriving in small chunks
|
||||
std::string chunk1 =
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>";
|
||||
std::string chunk2 = "{";
|
||||
std::string chunk3 = "\"location\":";
|
||||
std::string chunk4 = " \"Shanghai\",";
|
||||
std::string chunk5 = " \"unit\": \"celsius\"";
|
||||
std::string chunk6 = "}<|tool_call_end|><|tool_calls_section_end|>";
|
||||
|
||||
// First chunk - no calls returned yet, waiting for arguments
|
||||
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
|
||||
EXPECT_EQ(result1.calls.size(), 0);
|
||||
|
||||
// Second chunk - function name sent with empty parameters when arguments
|
||||
// start
|
||||
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
|
||||
EXPECT_EQ(result2.calls.size(), 1);
|
||||
EXPECT_TRUE(result2.calls[0].name.has_value());
|
||||
EXPECT_EQ(result2.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result2.calls[0].parameters, "");
|
||||
|
||||
// Incremental argument chunks - each sends the incremental part
|
||||
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
|
||||
EXPECT_EQ(result3.calls.size(), 1);
|
||||
EXPECT_FALSE(result3.calls[0].name.has_value());
|
||||
EXPECT_EQ(result3.calls[0].parameters, "{\"location\":");
|
||||
|
||||
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
|
||||
EXPECT_EQ(result4.calls.size(), 1);
|
||||
EXPECT_FALSE(result4.calls[0].name.has_value());
|
||||
EXPECT_EQ(result4.calls[0].parameters, " \"Shanghai\",");
|
||||
|
||||
auto result5 = detector_->parse_streaming_increment(chunk5, tools_);
|
||||
EXPECT_EQ(result5.calls.size(), 1);
|
||||
EXPECT_FALSE(result5.calls[0].name.has_value());
|
||||
EXPECT_EQ(result5.calls[0].parameters, " \"unit\": \"celsius\"");
|
||||
|
||||
// Complete arguments - final chunk
|
||||
auto result6 = detector_->parse_streaming_increment(chunk6, tools_);
|
||||
EXPECT_EQ(result6.calls.size(), 1);
|
||||
EXPECT_FALSE(result6.calls[0].name.has_value());
|
||||
EXPECT_EQ(result6.calls[0].parameters, "}");
|
||||
}
|
||||
|
||||
TEST_F(KimiK2DetectorTest, StreamingParseStateReset) {
|
||||
// Test that streaming state is properly reset between different parsing
|
||||
// sessions
|
||||
|
||||
// First complete tool call
|
||||
std::string text1 =
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
|
||||
"weather:0 <|tool_call_argument_begin|>{\"location\": "
|
||||
"\"Beijing\"}<|tool_call_end|>";
|
||||
auto result1 = detector_->parse_streaming_increment(text1, tools_);
|
||||
|
||||
// Create a new detector to simulate fresh state
|
||||
auto fresh_detector = std::make_unique<KimiK2Detector>();
|
||||
|
||||
// Second complete tool call with fresh detector
|
||||
std::string text2 =
|
||||
"<|tool_calls_section_begin|><|tool_call_begin|>functions.calculate:0 "
|
||||
"<|tool_call_argument_begin|>{\"expression\": \"1+1\"}<|tool_call_end|>";
|
||||
auto result2 = fresh_detector->parse_streaming_increment(text2, tools_);
|
||||
|
||||
EXPECT_EQ(result2.calls.size(), 1);
|
||||
EXPECT_EQ(result2.calls[0].name.value(), "calculate");
|
||||
EXPECT_EQ(result2.calls[0].tool_index,
|
||||
0); // Should start from 0 with fresh state
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
@@ -0,0 +1,13 @@
|
||||
include(cc_test)
|
||||
|
||||
cc_test(
|
||||
NAME
|
||||
partial_json_parser_test
|
||||
SRCS
|
||||
test_examples.cpp
|
||||
test_property_based.cpp
|
||||
DEPS
|
||||
:partial_json_parser
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "partial_json_parser/options.h"
|
||||
#include "partial_json_parser/parser.h"
|
||||
|
||||
using namespace partial_json_parser;
|
||||
|
||||
// Test string parsing
|
||||
TEST(PartialJsonParserTest, TestStr) {
|
||||
// Test incomplete string with STR option
|
||||
EXPECT_EQ(parse_malformed_string("\"", STR), "\"\"");
|
||||
|
||||
// Test incomplete string without STR option should throw
|
||||
EXPECT_THROW(parse_malformed_string("\"", static_cast<TypeOptions>(~STR)),
|
||||
MalformedJSONException);
|
||||
|
||||
// Test escaped backslash
|
||||
EXPECT_EQ(parse_malformed_string("\"\\\\", STR), "\"\\\\\"");
|
||||
|
||||
// Test unicode escape sequences
|
||||
EXPECT_EQ(parse_malformed_string("\"\\\\u", STR), "\"\\\\u\"");
|
||||
EXPECT_EQ(parse_malformed_string("\"\\\\U\\\\u", STR), "\"\\\\U\\\\u\"");
|
||||
}
|
||||
|
||||
// Test array parsing
|
||||
TEST(PartialJsonParserTest, TestArr) {
|
||||
// Test incomplete array with ARR option only
|
||||
EXPECT_EQ(parse_malformed_string("[\"", ARR), "[]");
|
||||
|
||||
// Test incomplete array with both ARR and STR options
|
||||
EXPECT_EQ(parse_malformed_string("[\"", static_cast<TypeOptions>(ARR | STR)),
|
||||
"[\"\"]");
|
||||
|
||||
// Test various incomplete arrays without proper options should throw
|
||||
EXPECT_THROW(parse_malformed_string("[", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("[\"", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("[\"\",", STR), MalformedJSONException);
|
||||
}
|
||||
|
||||
// Test object parsing
|
||||
TEST(PartialJsonParserTest, TestObj) {
|
||||
// Test incomplete object with OBJ option only
|
||||
EXPECT_EQ(parse_malformed_string("{\"\": \"", OBJ), "{}");
|
||||
|
||||
// Test incomplete object with both OBJ and STR options
|
||||
EXPECT_EQ(
|
||||
parse_malformed_string("{\"\": \"", static_cast<TypeOptions>(OBJ | STR)),
|
||||
"{\"\": \"\"}");
|
||||
|
||||
// Test various incomplete objects without proper options should throw
|
||||
EXPECT_THROW(parse_malformed_string("{", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("{\"", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("{\"\"]", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("{\"\":", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("{\"\":\"", STR), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("{\"\":\"\"]", STR),
|
||||
MalformedJSONException);
|
||||
}
|
||||
|
||||
// Test singleton values
|
||||
TEST(PartialJsonParserTest, TestSingletons) {
|
||||
// Test null
|
||||
EXPECT_EQ(parse_malformed_string("n", NULL_TYPE), "null");
|
||||
EXPECT_THROW(
|
||||
parse_malformed_string("n", static_cast<TypeOptions>(~NULL_TYPE)),
|
||||
MalformedJSONException);
|
||||
|
||||
// Test boolean true
|
||||
EXPECT_EQ(parse_malformed_string("t", BOOL), "true");
|
||||
EXPECT_THROW(parse_malformed_string("t", static_cast<TypeOptions>(~BOOL)),
|
||||
MalformedJSONException);
|
||||
|
||||
// Test boolean false
|
||||
EXPECT_EQ(parse_malformed_string("f", BOOL), "false");
|
||||
EXPECT_THROW(parse_malformed_string("f", static_cast<TypeOptions>(~BOOL)),
|
||||
MalformedJSONException);
|
||||
|
||||
// Test Infinity
|
||||
EXPECT_EQ(parse_malformed_string("I", INF), "Infinity");
|
||||
EXPECT_THROW(
|
||||
parse_malformed_string("I", static_cast<TypeOptions>(~INFINITY_TYPE)),
|
||||
MalformedJSONException);
|
||||
|
||||
// Test negative Infinity
|
||||
EXPECT_EQ(parse_malformed_string("-I", INF), "-Infinity");
|
||||
EXPECT_THROW(
|
||||
parse_malformed_string("-I", static_cast<TypeOptions>(~NEG_INFINITY)),
|
||||
MalformedJSONException);
|
||||
|
||||
// Test NaN
|
||||
EXPECT_EQ(parse_malformed_string("N", NAN_TYPE), "NaN");
|
||||
EXPECT_THROW(parse_malformed_string("N", static_cast<TypeOptions>(~NAN_TYPE)),
|
||||
MalformedJSONException);
|
||||
}
|
||||
|
||||
// Test number parsing
|
||||
TEST(PartialJsonParserTest, TestNum) {
|
||||
// Test complete numbers (should work without NUM option)
|
||||
EXPECT_EQ(parse_malformed_string("0", static_cast<TypeOptions>(~NUM)), "0");
|
||||
EXPECT_EQ(parse_malformed_string("-1.25e+4", static_cast<TypeOptions>(~NUM)),
|
||||
"-1.25e+4");
|
||||
|
||||
// Test incomplete numbers (need NUM option)
|
||||
EXPECT_EQ(parse_malformed_string("-1.25e+", NUM), "-1.25");
|
||||
EXPECT_EQ(parse_malformed_string("-1.25e", NUM), "-1.25");
|
||||
}
|
||||
|
||||
// Test error cases
|
||||
TEST(PartialJsonParserTest, TestError) {
|
||||
// Test unexpected characters
|
||||
EXPECT_THROW(parse_malformed_string("a", ALL), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("{0", ALL), MalformedJSONException);
|
||||
EXPECT_THROW(parse_malformed_string("--", ALL), MalformedJSONException);
|
||||
}
|
||||
|
||||
// Test basic functionality
|
||||
TEST(PartialJsonParserTest, TestBasicFunctionality) {
|
||||
// Test basic incomplete JSON structures
|
||||
EXPECT_EQ(parse_malformed_string("[", ALL), "[]");
|
||||
EXPECT_EQ(parse_malformed_string("[0.", ALL), "[0]");
|
||||
EXPECT_EQ(parse_malformed_string("{\"key\": ", ALL), "{}");
|
||||
EXPECT_EQ(parse_malformed_string("t", ALL), "true");
|
||||
|
||||
// Test with restricted options
|
||||
EXPECT_EQ(parse_malformed_string("[1", static_cast<TypeOptions>(~NUM)), "[]");
|
||||
EXPECT_EQ(parse_malformed_string("1", static_cast<TypeOptions>(~NUM)), "1");
|
||||
|
||||
// Test error case
|
||||
EXPECT_THROW(parse_malformed_string("-", ALL), MalformedJSONException);
|
||||
}
|
||||
|
||||
// Test complex nested structures
|
||||
TEST(PartialJsonParserTest, TestComplexStructures) {
|
||||
// Test nested array with incomplete string
|
||||
std::string result1 =
|
||||
parse_malformed_string("[\"a\", \"b", static_cast<TypeOptions>(~STR));
|
||||
EXPECT_EQ(result1, "[\"a\"]");
|
||||
|
||||
// Test nested object with incomplete values
|
||||
std::string result2 =
|
||||
parse_malformed_string("{\"key1\": 123, \"key2\": \"val", ALL);
|
||||
EXPECT_EQ(result2, "{\"key1\": 123, \"key2\": \"val\"}");
|
||||
|
||||
// Test deeply nested structure
|
||||
std::string result3 =
|
||||
parse_malformed_string("{\"arr\": [1, 2, {\"nested\":", ALL);
|
||||
EXPECT_EQ(result3, "{\"arr\": [1, 2, {}]}");
|
||||
}
|
||||
|
||||
// Test edge cases
|
||||
TEST(PartialJsonParserTest, TestEdgeCases) {
|
||||
// Test empty string
|
||||
EXPECT_THROW(parse_malformed_string("", ALL), MalformedJSONException);
|
||||
|
||||
// Test whitespace only
|
||||
EXPECT_THROW(parse_malformed_string(" ", ALL), MalformedJSONException);
|
||||
|
||||
// Test single characters
|
||||
EXPECT_EQ(parse_malformed_string("n", NULL_TYPE), "null");
|
||||
EXPECT_EQ(parse_malformed_string("t", BOOL), "true");
|
||||
EXPECT_EQ(parse_malformed_string("f", BOOL), "false");
|
||||
|
||||
// Test numbers with trailing operators
|
||||
EXPECT_EQ(parse_malformed_string("123.", NUM), "123");
|
||||
EXPECT_EQ(parse_malformed_string("123e", NUM), "123");
|
||||
EXPECT_EQ(parse_malformed_string("123e+", NUM), "123");
|
||||
EXPECT_EQ(parse_malformed_string("123e-", NUM), "123");
|
||||
}
|
||||
|
||||
// Test format parameter
|
||||
TEST(PartialJsonParserTest, TestFormatParameter) {
|
||||
std::string result1 = parse_malformed_string("{\"foo\":\"bar", ALL, false);
|
||||
EXPECT_EQ(result1, "{\"foo\":\"bar\"}");
|
||||
|
||||
std::string result2 = parse_malformed_string("{\"foo\":\"bar", ALL, true);
|
||||
EXPECT_EQ(result2, "{\n \"foo\": \"bar\"\n}");
|
||||
}
|
||||
|
||||
// Test Go example
|
||||
TEST(PartialJsonParserTest, TestGoExample) {
|
||||
// Test the example: `{"foo":"bar`
|
||||
std::string result = parse_malformed_string("{\"foo\":\"bar", ALL);
|
||||
EXPECT_EQ(result, "{\"foo\":\"bar\"}");
|
||||
|
||||
// Test the array example: `["a",{"a":123`
|
||||
std::string result2 = parse_malformed_string(
|
||||
"[\"a\",{\"a\":123", static_cast<TypeOptions>(NUM | ARR | OBJ));
|
||||
EXPECT_EQ(result2, "[\"a\",{\"a\":123}]");
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "partial_json_parser/options.h"
|
||||
#include "partial_json_parser/parser.h"
|
||||
|
||||
using namespace partial_json_parser;
|
||||
using json = nlohmann::json;
|
||||
|
||||
// JSON value generator class
|
||||
class JsonGenerator {
|
||||
private:
|
||||
std::mt19937 rng;
|
||||
std::uniform_int_distribution<int> int_dist;
|
||||
std::uniform_real_distribution<double> float_dist;
|
||||
std::uniform_int_distribution<int> bool_dist;
|
||||
std::uniform_int_distribution<int> type_dist;
|
||||
std::uniform_int_distribution<int> size_dist;
|
||||
std::uniform_int_distribution<int> char_dist;
|
||||
|
||||
public:
|
||||
JsonGenerator(unsigned seed = std::random_device{}())
|
||||
: rng(seed),
|
||||
int_dist(-1000, 1000),
|
||||
float_dist(-1000.0, 1000.0),
|
||||
bool_dist(0, 1),
|
||||
type_dist(
|
||||
0,
|
||||
5), // 6 basic types: null, bool, int, float, string, array, object
|
||||
size_dist(0, 5), // max collection size
|
||||
char_dist(32, 126) // printable ASCII
|
||||
{}
|
||||
|
||||
json generateJson(int depth = 0, int maxDepth = 3) {
|
||||
if (depth >= maxDepth) {
|
||||
// Generate only primitive types at max depth
|
||||
int type = type_dist(rng) % 4; // null, bool, int, float, string
|
||||
return generatePrimitive(type);
|
||||
}
|
||||
|
||||
int type = type_dist(rng);
|
||||
switch (type) {
|
||||
case 0:
|
||||
return json(nullptr); // null
|
||||
case 1:
|
||||
return json(bool_dist(rng) == 1); // bool
|
||||
case 2:
|
||||
return json(int_dist(rng)); // int
|
||||
case 3:
|
||||
return json(float_dist(rng)); // float
|
||||
case 4:
|
||||
return generateString(); // string
|
||||
case 5:
|
||||
return generateArray(depth, maxDepth); // array
|
||||
default:
|
||||
return generateObject(depth, maxDepth); // object
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
json generatePrimitive(int type) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return json(nullptr); // null
|
||||
case 1:
|
||||
return json(bool_dist(rng) == 1); // bool
|
||||
case 2:
|
||||
return json(int_dist(rng)); // int
|
||||
case 3:
|
||||
return json(float_dist(rng)); // float
|
||||
default:
|
||||
return generateString(); // string
|
||||
}
|
||||
}
|
||||
|
||||
json generateString() {
|
||||
int length = size_dist(rng);
|
||||
std::string str;
|
||||
for (int i = 0; i < length; ++i) {
|
||||
char c = static_cast<char>(char_dist(rng));
|
||||
// Avoid problematic characters for JSON
|
||||
if (c == '"' || c == '\\' || c < 32) {
|
||||
c = 'a' + (i % 26);
|
||||
}
|
||||
str += c;
|
||||
}
|
||||
return json(str);
|
||||
}
|
||||
|
||||
json generateArray(int depth, int maxDepth) {
|
||||
json array = json::array();
|
||||
int size = size_dist(rng);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
array.push_back(generateJson(depth + 1, maxDepth));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
json generateObject(int depth, int maxDepth) {
|
||||
json object = json::object();
|
||||
int size = size_dist(rng);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
std::string key = "key" + std::to_string(i);
|
||||
object[key] = generateJson(depth + 1, maxDepth);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to convert json to string
|
||||
std::string jsonToString(const json& value) { return value.dump(); }
|
||||
|
||||
// Helper function to parse JSON string back to json
|
||||
json parse_json_string(const std::string& json_str) {
|
||||
try {
|
||||
return json::parse(json_str);
|
||||
} catch (const json::parse_error& e) {
|
||||
throw std::runtime_error("Failed to parse JSON: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
// Test class for property-based testing of partial JSON parser
|
||||
class PartialJsonParserPropertyTest : public ::testing::Test {
|
||||
protected:
|
||||
JsonGenerator generator;
|
||||
static const int FINE_JSON_EXAMPLES =
|
||||
100; // Reduced from 333 for faster testing
|
||||
static const int PARTIAL_JSON_EXAMPLES =
|
||||
100; // Reduced from 333 for faster testing
|
||||
|
||||
void SetUp() override { generator = JsonGenerator(); }
|
||||
};
|
||||
|
||||
// Test that complete JSON strings are parsed correctly
|
||||
TEST_F(PartialJsonParserPropertyTest, TestFineJson) {
|
||||
for (int i = 0; i < FINE_JSON_EXAMPLES; ++i) {
|
||||
json originalJson = generator.generateJson();
|
||||
std::string json_string = jsonToString(originalJson);
|
||||
|
||||
try {
|
||||
// Parse with our parser
|
||||
std::string result = parse_malformed_string(json_string, ALL, false);
|
||||
|
||||
// Parse both original and result with standard JSON parser
|
||||
json original_parsed = parse_json_string(json_string);
|
||||
json result_parsed = parse_json_string(result);
|
||||
|
||||
// They should be equivalent
|
||||
EXPECT_EQ(original_parsed, result_parsed)
|
||||
<< "Original: " << json_string << "\nResult: " << result;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// If our parser fails, the original should also be invalid JSON
|
||||
// This is acceptable for some edge cases
|
||||
GTEST_SKIP() << "Skipping invalid JSON: " << json_string
|
||||
<< " Error: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that partial JSON strings can be completed
|
||||
TEST_F(PartialJsonParserPropertyTest, TestPartialJson) {
|
||||
for (int i = 0; i < PARTIAL_JSON_EXAMPLES; ++i) {
|
||||
json originalJson = generator.generateJson();
|
||||
std::string json_string = jsonToString(originalJson);
|
||||
|
||||
if (json_string.empty()) continue;
|
||||
|
||||
// Test various prefixes of the JSON string
|
||||
int step = std::max(1, static_cast<int>(json_string.length()) / 10);
|
||||
for (size_t pos = 1; pos < json_string.length(); pos += step) {
|
||||
std::string partial_json = json_string.substr(0, pos);
|
||||
|
||||
// Skip if starts with '-' (known problematic case)
|
||||
if (partial_json[0] == '-' && partial_json.length() == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string result = parse_malformed_string(partial_json, ALL, false);
|
||||
|
||||
// The result should be valid JSON
|
||||
json result_parsed = parse_json_string(result);
|
||||
|
||||
// Basic sanity check: result should not be empty
|
||||
EXPECT_FALSE(result.empty())
|
||||
<< "Empty result for partial JSON: " << partial_json;
|
||||
|
||||
} catch (const MalformedJSONException& e) {
|
||||
// Some partial JSONs are expected to fail
|
||||
// This is acceptable behavior
|
||||
continue;
|
||||
} catch (const std::exception& e) {
|
||||
FAIL() << "Unexpected exception for partial JSON: " << partial_json
|
||||
<< " Error: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test specific edge cases that caused issues in Go version
|
||||
TEST_F(PartialJsonParserPropertyTest, TestKnownEdgeCases) {
|
||||
// Test cases from Go test files that should fail
|
||||
std::vector<std::string> shouldFail = {
|
||||
"{0", // Invalid object key
|
||||
"--", // Invalid number
|
||||
"a", // Invalid character
|
||||
"", // Empty string
|
||||
" " // Whitespace only
|
||||
};
|
||||
|
||||
for (const auto& test_case : shouldFail) {
|
||||
EXPECT_THROW(parse_malformed_string(test_case, ALL, false),
|
||||
MalformedJSONException)
|
||||
<< "Expected failure for: " << test_case;
|
||||
}
|
||||
}
|
||||
|
||||
// Test specific cases that should succeed
|
||||
TEST_F(PartialJsonParserPropertyTest, TestKnownSuccessCases) {
|
||||
struct TestCase {
|
||||
std::string input;
|
||||
std::string expected;
|
||||
TypeOptions options;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
{"[", "[]", ALL},
|
||||
{"[0.", "[0]", ALL},
|
||||
{"{\"key\": ", "{}", ALL},
|
||||
{"t", "true", ALL},
|
||||
{"\"", "\"\"", STR},
|
||||
{"[\"", "[\"\"]", static_cast<TypeOptions>(ARR | STR)},
|
||||
{"{\"foo\":\"bar", "{\"foo\":\"bar\"}", ALL}};
|
||||
|
||||
for (const auto& test_case : test_cases) {
|
||||
try {
|
||||
std::string result =
|
||||
parse_malformed_string(test_case.input, test_case.options, false);
|
||||
EXPECT_EQ(result, test_case.expected)
|
||||
<< "Input: " << test_case.input << " Expected: " << test_case.expected
|
||||
<< " Got: " << result;
|
||||
} catch (const std::exception& e) {
|
||||
FAIL() << "Unexpected exception for input: " << test_case.input
|
||||
<< " Error: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test option restrictions
|
||||
TEST_F(PartialJsonParserPropertyTest, TestOptionRestrictions) {
|
||||
struct TestCase {
|
||||
std::string input;
|
||||
TypeOptions allowedOptions;
|
||||
bool shouldSucceed;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
{"\"", STR, true},
|
||||
{"\"", static_cast<TypeOptions>(~STR), false},
|
||||
{"[", ARR, true},
|
||||
{"[", STR, false},
|
||||
{"{", OBJ, true},
|
||||
{"{", STR, false},
|
||||
{"t", BOOL, true},
|
||||
{"t", static_cast<TypeOptions>(~BOOL), false},
|
||||
{"n", NULL_TYPE, true},
|
||||
{"n", static_cast<TypeOptions>(~NULL_TYPE), false}};
|
||||
|
||||
for (const auto& test_case : test_cases) {
|
||||
if (test_case.shouldSucceed) {
|
||||
EXPECT_NO_THROW(parse_malformed_string(
|
||||
test_case.input, test_case.allowedOptions, false))
|
||||
<< "Expected success for: " << test_case.input
|
||||
<< " with options: " << test_case.allowedOptions;
|
||||
} else {
|
||||
EXPECT_THROW(parse_malformed_string(
|
||||
test_case.input, test_case.allowedOptions, false),
|
||||
MalformedJSONException)
|
||||
<< "Expected failure for: " << test_case.input
|
||||
<< " with options: " << test_case.allowedOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Performance test - ensure reasonable performance on large inputs
|
||||
TEST_F(PartialJsonParserPropertyTest, TestPerformance) {
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
json json = generator.generateJson(0, 2); // Smaller depth for performance
|
||||
std::string json_string = jsonToString(json);
|
||||
|
||||
if (!json_string.empty()) {
|
||||
try {
|
||||
parse_malformed_string(json_string, ALL, false);
|
||||
} catch (const MalformedJSONException&) {
|
||||
// Expected for some cases
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// Should complete 1000 operations in reasonable time (less than 10 seconds)
|
||||
EXPECT_LT(duration.count(), 10000)
|
||||
<< "Performance test took too long: " << duration.count() << "ms";
|
||||
}
|
||||
|
||||
// Test consistency between different option combinations
|
||||
TEST_F(PartialJsonParserPropertyTest, TestOptionConsistency) {
|
||||
std::vector<std::string> testInputs = {
|
||||
"[1,2,3", "{\"a\":1,\"b\":", "\"hello", "123.", "true", "null"};
|
||||
|
||||
for (const auto& input : testInputs) {
|
||||
// Test that ALL option works when individual options work
|
||||
bool individualSuccess = false;
|
||||
std::string individual_result;
|
||||
|
||||
std::vector<TypeOptions> individualOptions = {
|
||||
STR, NUM, ARR, OBJ, NULL_TYPE, BOOL};
|
||||
|
||||
for (auto option : individualOptions) {
|
||||
try {
|
||||
individual_result = parse_malformed_string(input, option, false);
|
||||
individualSuccess = true;
|
||||
break;
|
||||
} catch (const MalformedJSONException&) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (individualSuccess) {
|
||||
// ALL option should also succeed
|
||||
EXPECT_NO_THROW({
|
||||
std::string all_result = parse_malformed_string(input, ALL, false);
|
||||
// Results might differ, but both should be valid
|
||||
EXPECT_FALSE(all_result.empty());
|
||||
}) << "ALL option failed when individual option succeeded for: "
|
||||
<< input;
|
||||
}
|
||||
}
|
||||
}
|
||||
545
upstream_ref/xllm/tests/function_call/qwen25_detector_test.cpp
Normal file
545
upstream_ref/xllm/tests/function_call/qwen25_detector_test.cpp
Normal file
@@ -0,0 +1,545 @@
|
||||
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "qwen25_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core_types.h"
|
||||
#include "function_call_parser.h"
|
||||
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
class Qwen25TestBase : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"location",
|
||||
{{"type", "string"},
|
||||
{"description", "The city and state, e.g. San Francisco, CA"}}},
|
||||
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
|
||||
{"required", {"location"}}};
|
||||
|
||||
JsonFunction weather_func("get_current_weather",
|
||||
"Get the current weather in a given location",
|
||||
weather_params);
|
||||
weather_tool_ = JsonTool("function", weather_func);
|
||||
|
||||
nlohmann::json calculator_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"expression",
|
||||
{{"type", "string"},
|
||||
{"description", "Mathematical expression to evaluate"}}}}},
|
||||
{"required", {"expression"}}};
|
||||
|
||||
JsonFunction calculator_func(
|
||||
"calculate", "Calculate mathematical expressions", calculator_params);
|
||||
calculator_tool_ = JsonTool("function", calculator_func);
|
||||
|
||||
tools_ = {weather_tool_, calculator_tool_};
|
||||
}
|
||||
|
||||
JsonTool weather_tool_;
|
||||
JsonTool calculator_tool_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
class Qwen25DetectorTest : public Qwen25TestBase {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
Qwen25TestBase::SetUp();
|
||||
detector_ = std::make_unique<Qwen25Detector>();
|
||||
}
|
||||
|
||||
std::unique_ptr<Qwen25Detector> detector_;
|
||||
};
|
||||
|
||||
class Qwen25StreamingTest : public Qwen25TestBase {
|
||||
protected:
|
||||
void SetUp() override { Qwen25TestBase::SetUp(); }
|
||||
};
|
||||
|
||||
// Test constructor and basic properties
|
||||
TEST_F(Qwen25DetectorTest, ConstructorInitializesCorrectly) {
|
||||
EXPECT_NE(detector_, nullptr);
|
||||
|
||||
// Test basic token detection
|
||||
std::string text_with_tool_call =
|
||||
"Some text <tool_call>\n{\"name\": \"test\"}\n</tool_call>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
// Test has_tool_call method
|
||||
TEST_F(Qwen25DetectorTest, HasToolCallDetection) {
|
||||
// Test text containing tool calls
|
||||
EXPECT_TRUE(detector_->has_tool_call("<tool_call>\n"));
|
||||
EXPECT_TRUE(
|
||||
detector_->has_tool_call("Previous text <tool_call>\nFollowing content"));
|
||||
EXPECT_TRUE(detector_->has_tool_call(
|
||||
"<tool_call>\n{\"name\": \"test\"}\n</tool_call>"));
|
||||
|
||||
// Test text not containing tool calls
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("tool_call without brackets"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("<tool_call without newline"));
|
||||
}
|
||||
|
||||
// Test trim_whitespace method (indirectly tested through public interface)
|
||||
TEST_F(Qwen25DetectorTest, TrimWhitespaceHandling) {
|
||||
std::string text_with_whitespace =
|
||||
" \t\nPrevious text\r\n <tool_call>\n {\"name\": "
|
||||
"\"get_current_weather\", \"arguments\": {\"location\": \"Beijing\"}} "
|
||||
"\n</tool_call> \t\r\n";
|
||||
|
||||
auto result = detector_->detect_and_parse(text_with_whitespace, tools_);
|
||||
|
||||
// Verify normal text is correctly trimmed
|
||||
EXPECT_EQ(result.normal_text, "Previous text");
|
||||
|
||||
// Verify tool call is correctly parsed
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].tool_index, -1); // Base class always returns -1
|
||||
}
|
||||
|
||||
// Test single tool call parsing
|
||||
TEST_F(Qwen25DetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather <tool_call>\n{\"name\": "
|
||||
"\"get_current_weather\", \"arguments\": {\"location\": \"Beijing\", "
|
||||
"\"unit\": \"celsius\"}}\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Please help me check the weather");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
// Verify parameter JSON
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
// Test multiple tool calls parsing
|
||||
TEST_F(Qwen25DetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"Please help me check the weather and calculate an expression "
|
||||
"<tool_call>\n{\"name\": \"get_current_weather\", \"arguments\": "
|
||||
"{\"location\": \"Shanghai\"}}\n</tool_call>\n<tool_call>\n{\"name\": "
|
||||
"\"calculate\", \"arguments\": {\"expression\": \"2 + 3 * "
|
||||
"4\"}}\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"Please help me check the weather and calculate an expression");
|
||||
EXPECT_EQ(result.calls.size(), 2);
|
||||
|
||||
// Verify first tool call
|
||||
const auto& call1 = result.calls[0];
|
||||
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call1.name.has_value());
|
||||
EXPECT_EQ(call1.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
|
||||
EXPECT_EQ(params1["location"], "Shanghai");
|
||||
|
||||
// Verify second tool call
|
||||
const auto& call2 = result.calls[1];
|
||||
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_TRUE(call2.name.has_value());
|
||||
EXPECT_EQ(call2.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
|
||||
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
|
||||
}
|
||||
|
||||
// Test invalid JSON handling
|
||||
TEST_F(Qwen25DetectorTest, InvalidJsonHandling) {
|
||||
std::string text =
|
||||
"Test invalid JSON <tool_call>\n{\"name\": \"get_current_weather\", "
|
||||
"\"arguments\": {\"location\": \"Beijing\", invalid_json}}\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test invalid JSON");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Invalid JSON should be ignored
|
||||
}
|
||||
|
||||
// Test empty tool call content
|
||||
TEST_F(Qwen25DetectorTest, EmptyToolCallContent) {
|
||||
std::string text = "Test empty content <tool_call>\n \t\n \n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Test empty content");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
|
||||
}
|
||||
|
||||
// Test incomplete tool call (only start tag)
|
||||
TEST_F(Qwen25DetectorTest, IncompleteToolCall) {
|
||||
std::string text =
|
||||
"Incomplete tool call <tool_call>\n{\"name\": \"get_current_weather\"";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Incomplete tool call");
|
||||
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
|
||||
}
|
||||
|
||||
// Test unknown tool name handling
|
||||
TEST_F(Qwen25DetectorTest, UnknownToolName) {
|
||||
std::string text =
|
||||
"Unknown tool <tool_call>\n{\"name\": \"unknown_tool\", \"arguments\": "
|
||||
"{\"param\": \"value\"}}\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Unknown tool");
|
||||
// Base class will skip unknown tools, so should be 0 calls
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test case with only normal text
|
||||
TEST_F(Qwen25DetectorTest, OnlyNormalText) {
|
||||
std::string text = "This is a regular text without any tool calls.";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text,
|
||||
"This is a regular text without any tool calls.");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test empty string input
|
||||
TEST_F(Qwen25DetectorTest, EmptyStringInput) {
|
||||
std::string text = "";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
EXPECT_FALSE(result.has_calls());
|
||||
}
|
||||
|
||||
// Test whitespace-only input
|
||||
TEST_F(Qwen25DetectorTest, WhitespaceOnlyInput) {
|
||||
std::string text = " \t\n\r ";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "");
|
||||
EXPECT_EQ(result.calls.size(), 0);
|
||||
}
|
||||
|
||||
// Test complex nested JSON parameters
|
||||
TEST_F(Qwen25DetectorTest, ComplexNestedJsonParameters) {
|
||||
std::string text =
|
||||
"Complex parameter test <tool_call>\n{\"name\": \"get_current_weather\", "
|
||||
"\"arguments\": {\"location\": \"Beijing\", \"options\": "
|
||||
"{\"include_forecast\": true, \"days\": 7, \"details\": "
|
||||
"[\"temperature\", \"humidity\", \"wind\"]}}}\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Complex parameter test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Beijing");
|
||||
EXPECT_TRUE(params["options"]["include_forecast"]);
|
||||
EXPECT_EQ(params["options"]["days"], 7);
|
||||
EXPECT_EQ(params["options"]["details"].size(), 3);
|
||||
}
|
||||
|
||||
// Test tool call in the middle of text
|
||||
TEST_F(Qwen25DetectorTest, ToolCallInMiddleOfText) {
|
||||
std::string text =
|
||||
"Previous text <tool_call>\n{\"name\": \"calculate\", \"arguments\": "
|
||||
"{\"expression\": \"1+1\"}}\n</tool_call> Following text";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
// Note: According to implementation, only text before tool call is preserved
|
||||
// as normal_text
|
||||
EXPECT_EQ(result.normal_text, "Previous text");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
}
|
||||
|
||||
// Test special characters handling
|
||||
TEST_F(Qwen25DetectorTest, SpecialCharactersHandling) {
|
||||
std::string text =
|
||||
"Special characters test <tool_call>\n{\"name\": "
|
||||
"\"get_current_weather\", \"arguments\": {\"location\": \"New York "
|
||||
"City\", \"note\": \"Contains symbols!@#$%^&*()_+=\"}}\n</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Special characters test");
|
||||
EXPECT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "New York City");
|
||||
EXPECT_EQ(params["note"], "Contains symbols!@#$%^&*()_+=");
|
||||
}
|
||||
|
||||
// Performance test: many tool calls
|
||||
TEST_F(Qwen25DetectorTest, PerformanceWithManyToolCalls) {
|
||||
std::string text = "Performance test";
|
||||
|
||||
// Build text containing multiple tool calls
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
text +=
|
||||
" <tool_call>\n{\"name\": \"calculate\", \"arguments\": "
|
||||
"{\"expression\": \"" +
|
||||
std::to_string(i) + " + " + std::to_string(i + 1) +
|
||||
"\"}}\n</tool_call>";
|
||||
}
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, "Performance test");
|
||||
EXPECT_EQ(result.calls.size(), 10000);
|
||||
|
||||
// Verify each tool call is correctly parsed
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
const auto& call = result.calls[i];
|
||||
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
|
||||
EXPECT_EQ(call.name.value(), "calculate");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
std::string expected_expr =
|
||||
std::to_string(i) + " + " + std::to_string(i + 1);
|
||||
EXPECT_EQ(params["expression"], expected_expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Test basic streaming functionality
|
||||
TEST_F(Qwen25StreamingTest, BasicStreamingParsing) {
|
||||
FunctionCallParser parser(tools_, "qwen25");
|
||||
|
||||
// Simulate streaming chunks
|
||||
std::vector<std::string> chunks = {
|
||||
"I need to check the weather ",
|
||||
"<tool_call>\n",
|
||||
"{\"name\": \"get_current_weather\", ",
|
||||
"\"arguments\": {\"location\": \"Beijing\", ",
|
||||
"\"unit\": \"celsius\"}}\n",
|
||||
"</tool_call>"};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
std::vector<ToolCallItem> accumulated_calls;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = parser.parse_streaming_increment(chunk);
|
||||
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
for (const auto& call : result.calls) {
|
||||
accumulated_calls.push_back(call);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify results
|
||||
EXPECT_EQ(accumulated_normal_text, "I need to check the weather ");
|
||||
EXPECT_GT(accumulated_calls.size(), 0);
|
||||
|
||||
// Find the complete tool call
|
||||
bool found_complete_call = false;
|
||||
for (const auto& call : accumulated_calls) {
|
||||
if (call.name.has_value() && call.name.value() == "get_current_weather") {
|
||||
found_complete_call = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found_complete_call);
|
||||
}
|
||||
|
||||
// Test multiple tool calls streaming
|
||||
TEST_F(Qwen25StreamingTest, MultipleToolCallsStreaming) {
|
||||
FunctionCallParser parser(tools_, "qwen25");
|
||||
|
||||
// Simulate realistic token-level streaming chunks with multiple tool calls
|
||||
std::vector<std::string> chunks = {"Let",
|
||||
" me",
|
||||
" help",
|
||||
" you",
|
||||
" with",
|
||||
" weather",
|
||||
" and",
|
||||
" calculation",
|
||||
" ",
|
||||
"<tool_call>",
|
||||
"\n",
|
||||
"{",
|
||||
"\"name\"",
|
||||
":",
|
||||
" \"",
|
||||
"get_current_weather",
|
||||
"\",",
|
||||
" ",
|
||||
"\"arguments\"",
|
||||
":",
|
||||
" {",
|
||||
"\"location\"",
|
||||
":",
|
||||
" \"",
|
||||
"Shanghai",
|
||||
"\"}}\n",
|
||||
"</tool_call>",
|
||||
"\n",
|
||||
"<tool_call>",
|
||||
"\n",
|
||||
"{",
|
||||
"\"name\"",
|
||||
":",
|
||||
" \"",
|
||||
"calculate",
|
||||
"\",",
|
||||
" ",
|
||||
"\"arguments\"",
|
||||
":",
|
||||
" {",
|
||||
"\"expression\"",
|
||||
":",
|
||||
" \"",
|
||||
"2",
|
||||
" +",
|
||||
" ",
|
||||
"3",
|
||||
"\"}}\n",
|
||||
"</tool_call>"};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
std::vector<ToolCallItem> accumulated_calls;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = parser.parse_streaming_increment(chunk);
|
||||
// std::cerr << "buffer_: " << (*parser.detector_).buffer_ << std::endl;
|
||||
// std::cerr << " -> Normal text: " << result.normal_text << std::endl;
|
||||
// std::cerr << " -> Calls count: " << result.calls.size() << std::endl;
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
for (const auto& call : result.calls) {
|
||||
accumulated_calls.push_back(call);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify results
|
||||
EXPECT_EQ(accumulated_normal_text,
|
||||
"Let me help you with weather and calculation ");
|
||||
EXPECT_GT(accumulated_calls.size(), 0);
|
||||
|
||||
// Check for both tool calls
|
||||
bool found_weather = false;
|
||||
bool found_calculator = false;
|
||||
|
||||
for (const auto& call : accumulated_calls) {
|
||||
if (call.name.has_value()) {
|
||||
if (call.name.value() == "get_current_weather") {
|
||||
found_weather = true;
|
||||
} else if (call.name.value() == "calculate") {
|
||||
found_calculator = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_weather);
|
||||
EXPECT_TRUE(found_calculator);
|
||||
}
|
||||
|
||||
// Test partial token handling
|
||||
TEST_F(Qwen25StreamingTest, PartialTokenHandling) {
|
||||
FunctionCallParser parser(tools_, "qwen25");
|
||||
|
||||
// Simulate realistic partial tokens being streamed - testing edge cases where
|
||||
// tokens are split
|
||||
std::vector<std::string> chunks = {"Testing",
|
||||
" partial",
|
||||
" tokens",
|
||||
" ",
|
||||
"<tool_call>",
|
||||
"\n",
|
||||
"{",
|
||||
"\"name\"",
|
||||
":",
|
||||
" \"",
|
||||
"get_current_weather",
|
||||
"\",",
|
||||
" ",
|
||||
"\"arguments\"",
|
||||
":",
|
||||
" {",
|
||||
"\"location\"",
|
||||
":",
|
||||
" \"",
|
||||
"Tokyo",
|
||||
"\"}}",
|
||||
"\n",
|
||||
"</tool_call>"};
|
||||
|
||||
std::string accumulated_normal_text;
|
||||
std::vector<ToolCallItem> accumulated_calls;
|
||||
|
||||
for (const auto& chunk : chunks) {
|
||||
auto result = parser.parse_streaming_increment(chunk);
|
||||
|
||||
if (!result.normal_text.empty()) {
|
||||
accumulated_normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
for (const auto& call : result.calls) {
|
||||
accumulated_calls.push_back(call);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify results
|
||||
EXPECT_EQ(accumulated_normal_text, "Testing partial tokens ");
|
||||
EXPECT_GT(accumulated_calls.size(), 0);
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
@@ -0,0 +1,718 @@
|
||||
/* Copyright 2026 The xLLM Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://github.com/jd-opensource/xllm/blob/main/LICENSE
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==============================================================================*/
|
||||
|
||||
#include "qwen3_coder_detector.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core_types.h"
|
||||
#include "function_call_parser.h"
|
||||
|
||||
namespace xllm {
|
||||
namespace function_call {
|
||||
|
||||
namespace {
|
||||
|
||||
struct StreamCallAccumulator {
|
||||
std::string name;
|
||||
std::string parameters;
|
||||
};
|
||||
|
||||
void merge_stream_result(const StreamingParseResult& result,
|
||||
std::string* normal_text,
|
||||
std::vector<StreamCallAccumulator>* calls) {
|
||||
if (normal_text != nullptr && !result.normal_text.empty()) {
|
||||
*normal_text += result.normal_text;
|
||||
}
|
||||
|
||||
if (calls == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& call : result.calls) {
|
||||
if (call.tool_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (calls->size() <= static_cast<size_t>(call.tool_index)) {
|
||||
calls->push_back(StreamCallAccumulator());
|
||||
}
|
||||
|
||||
auto& acc = (*calls)[call.tool_index];
|
||||
if (call.name.has_value()) {
|
||||
acc.name = call.name.value();
|
||||
}
|
||||
if (!call.parameters.empty()) {
|
||||
acc.parameters += call.parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class Qwen3CoderDetectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
detector_ = std::make_unique<Qwen3CoderDetector>();
|
||||
|
||||
nlohmann::json weather_params = {
|
||||
{"type", "object"},
|
||||
{"properties",
|
||||
{{"location", {{"type", "string"}}},
|
||||
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}},
|
||||
{"days", {{"type", "integer"}}},
|
||||
{"temperature", {{"type", "number"}}},
|
||||
{"metadata", {{"type", "object"}}}}}};
|
||||
tools_.emplace_back(
|
||||
"function",
|
||||
JsonFunction(
|
||||
"get_current_weather", "Get weather info", weather_params));
|
||||
|
||||
nlohmann::json sql_params = {{"type", "object"},
|
||||
{"properties",
|
||||
{{"query", {{"type", "string"}}},
|
||||
{"dry_run", {{"type", "boolean"}}}}}};
|
||||
tools_.emplace_back("function",
|
||||
JsonFunction("sql_interpreter", "Run SQL", sql_params));
|
||||
|
||||
nlohmann::json todo_params = {
|
||||
{"type", "object"}, {"properties", {{"todos", {{"type", "array"}}}}}};
|
||||
tools_.emplace_back(
|
||||
"function", JsonFunction("TodoWrite", "Write TODO items", todo_params));
|
||||
}
|
||||
|
||||
std::unique_ptr<Qwen3CoderDetector> detector_;
|
||||
std::vector<JsonTool> tools_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Basic behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, ConstructorInitializesCorrectly) {
|
||||
ASSERT_NE(detector_, nullptr);
|
||||
|
||||
std::string text_with_tool_call =
|
||||
"Some text <tool_call><function=test></function></tool_call>";
|
||||
std::string text_without_tool_call =
|
||||
"Just normal text without any tool calls";
|
||||
|
||||
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
|
||||
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, HasToolCallDetection) {
|
||||
EXPECT_TRUE(detector_->has_tool_call("<tool_call>"));
|
||||
EXPECT_TRUE(
|
||||
detector_->has_tool_call("prefix <tool_call> middle </tool_call>"));
|
||||
|
||||
EXPECT_FALSE(detector_->has_tool_call(""));
|
||||
EXPECT_FALSE(detector_->has_tool_call("regular text only"));
|
||||
EXPECT_FALSE(detector_->has_tool_call("<function=get_current_weather>"));
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, PlainTextOnly) {
|
||||
std::string text = "This is plain text without any tool calls.";
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
|
||||
EXPECT_EQ(result.normal_text, text);
|
||||
EXPECT_TRUE(result.calls.empty());
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, SingleToolCallParsing) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>Boston</parameter>\n"
|
||||
"<parameter=unit>celsius</parameter>\n"
|
||||
"<parameter=days>3</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
const auto& call = result.calls[0];
|
||||
EXPECT_EQ(call.tool_index, 0);
|
||||
ASSERT_TRUE(call.name.has_value());
|
||||
EXPECT_EQ(call.name.value(), "get_current_weather");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(call.parameters);
|
||||
EXPECT_EQ(params["location"], "Boston");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
EXPECT_EQ(params["days"], 3);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, SingleToolCallWithTextPrefix) {
|
||||
std::string text =
|
||||
"Let me check this for you.\n\n"
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>New York</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
EXPECT_EQ(result.normal_text, "Let me check this for you.\n\n");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
ASSERT_TRUE(result.calls[0].name.has_value());
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, MultipleToolCallsParsing) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>New York</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>\n"
|
||||
"<tool_call>\n"
|
||||
"<function=sql_interpreter>\n"
|
||||
"<parameter=query>SELECT * FROM users</parameter>\n"
|
||||
"<parameter=dry_run>True</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 2);
|
||||
|
||||
ASSERT_TRUE(result.calls[0].name.has_value());
|
||||
ASSERT_TRUE(result.calls[1].name.has_value());
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
|
||||
EXPECT_EQ(result.calls[1].name.value(), "sql_interpreter");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_EQ(params1["location"], "New York");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(result.calls[1].parameters);
|
||||
EXPECT_EQ(params2["query"], "SELECT * FROM users");
|
||||
EXPECT_EQ(params2["dry_run"], true);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, MultipleFunctionsInOneToolCallBlock) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>Paris</parameter>\n"
|
||||
"</function>\n"
|
||||
"<function=sql_interpreter>\n"
|
||||
"<parameter=query>SELECT 1</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 2);
|
||||
|
||||
EXPECT_EQ(result.calls[0].name.value_or(""), "get_current_weather");
|
||||
EXPECT_EQ(result.calls[1].name.value_or(""), "sql_interpreter");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, ParseWithoutToolCallWrapperFallback) {
|
||||
std::string text =
|
||||
"Prefix text\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>Tokyo</parameter>\n"
|
||||
"</function>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
EXPECT_EQ(result.normal_text, "Prefix text\n");
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
ASSERT_TRUE(result.calls[0].name.has_value());
|
||||
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Tokyo");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, UnknownToolIsStillParsedLikeSgLang) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=unknown_tool>\n"
|
||||
"<parameter=x>42</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
EXPECT_EQ(result.calls[0].name.value_or(""), "unknown_tool");
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
// Unknown schema defaults to string type.
|
||||
EXPECT_EQ(params["x"], "42");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, EmptyParameterValue) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location></parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_EQ(params["location"], "");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, SpecialCharactersInParameterValue) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=sql_interpreter>\n"
|
||||
"<parameter=query>SELECT * FROM users WHERE name = 'John \"Doe\"'</"
|
||||
"parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_NE(params["query"].get<std::string>().find("John"), std::string::npos);
|
||||
EXPECT_NE(params["query"].get<std::string>().find("Doe"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, IncompleteToolCallDoesNotCrash) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>London";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
EXPECT_GE(result.calls.size(), 0U);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Type conversion behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, IntegerParameterConversion) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Tokyo</parameter>"
|
||||
"<parameter=days>5</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["days"].is_number_integer());
|
||||
EXPECT_EQ(params["days"], 5);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, InvalidIntegerFallsBackToString) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Tokyo</parameter>"
|
||||
"<parameter=days>five</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["days"].is_string());
|
||||
EXPECT_EQ(params["days"], "five");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, NumberParameterConversion) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Tokyo</parameter>"
|
||||
"<parameter=temperature>12.5</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["temperature"].is_number_float());
|
||||
EXPECT_DOUBLE_EQ(params["temperature"], 12.5);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, NumberIntegerLikeStringConvertedToInt) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Tokyo</parameter>"
|
||||
"<parameter=temperature>12</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["temperature"].is_number_integer());
|
||||
EXPECT_EQ(params["temperature"], 12);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, BooleanParameterConversion) {
|
||||
std::string text =
|
||||
"<tool_call><function=sql_interpreter>"
|
||||
"<parameter=query>SELECT 1</parameter>"
|
||||
"<parameter=dry_run>True</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["dry_run"].is_boolean());
|
||||
EXPECT_EQ(params["dry_run"], true);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, InvalidBooleanFallsBackToFalse) {
|
||||
std::string text =
|
||||
"<tool_call><function=sql_interpreter>"
|
||||
"<parameter=query>SELECT 1</parameter>"
|
||||
"<parameter=dry_run>not_bool</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["dry_run"].is_boolean());
|
||||
EXPECT_EQ(params["dry_run"], false);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, NullValueConversion) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>null</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["location"].is_null());
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, ObjectAndArrayConversion) {
|
||||
std::string text =
|
||||
"<tool_call>\n"
|
||||
"<function=get_current_weather>\n"
|
||||
"<parameter=location>Beijing</parameter>\n"
|
||||
"<parameter=metadata>{\"source\":\"api\",\"retry\":1}</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>\n"
|
||||
"<tool_call>\n"
|
||||
"<function=TodoWrite>\n"
|
||||
"<parameter=todos>[{\"content\":\"A\",\"status\":\"pending\"}]</"
|
||||
"parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 2);
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params1["metadata"].is_object());
|
||||
EXPECT_EQ(params1["metadata"]["source"], "api");
|
||||
|
||||
nlohmann::json params2 = nlohmann::json::parse(result.calls[1].parameters);
|
||||
EXPECT_TRUE(params2["todos"].is_array());
|
||||
EXPECT_EQ(params2["todos"][0]["content"], "A");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, InvalidObjectFallsBackToString) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Beijing</parameter>"
|
||||
"<parameter=metadata>{invalid_json}</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["metadata"].is_string());
|
||||
EXPECT_EQ(params["metadata"], "{invalid_json}");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, UnknownParameterFallsBackToRawString) {
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Berlin</parameter>"
|
||||
"<parameter=unexpected_field>123</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
ASSERT_EQ(result.calls.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
|
||||
EXPECT_TRUE(params["unexpected_field"].is_string());
|
||||
EXPECT_EQ(params["unexpected_field"], "123");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Streaming behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingSingleToolCall) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {"<tool_call>",
|
||||
"<function=get_current_weather>",
|
||||
"<parameter=location>",
|
||||
"Boston",
|
||||
"</parameter>",
|
||||
"<parameter=unit>celsius</parameter>",
|
||||
"<parameter=days>3</parameter>",
|
||||
"</function>",
|
||||
"</tool_call>"};
|
||||
|
||||
std::string normal_text;
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(
|
||||
parser.parse_streaming_increment(chunk), &normal_text, &acc);
|
||||
}
|
||||
|
||||
EXPECT_TRUE(normal_text.empty());
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
EXPECT_EQ(acc[0].name, "get_current_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Boston");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
EXPECT_EQ(params["days"], 3);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingMultipleToolCalls) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {
|
||||
"<tool_call><function=get_current_weather><parameter=location>Paris</"
|
||||
"parameter></function></tool_call>",
|
||||
"<tool_call><function=sql_interpreter><parameter=query>SELECT 1</"
|
||||
"parameter><parameter=dry_run>false</parameter></function></tool_call>"};
|
||||
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
|
||||
}
|
||||
|
||||
ASSERT_EQ(acc.size(), 2);
|
||||
EXPECT_EQ(acc[0].name, "get_current_weather");
|
||||
EXPECT_EQ(acc[1].name, "sql_interpreter");
|
||||
|
||||
nlohmann::json params0 = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params0["location"], "Paris");
|
||||
|
||||
nlohmann::json params1 = nlohmann::json::parse(acc[1].parameters);
|
||||
EXPECT_EQ(params1["query"], "SELECT 1");
|
||||
EXPECT_EQ(params1["dry_run"], false);
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingTextAndToolCall) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {"Let me ",
|
||||
"help you.\n\n",
|
||||
"<tool_call>",
|
||||
"<function=get_current_weather>",
|
||||
"<parameter=location>Paris</parameter>",
|
||||
"</function>",
|
||||
"</tool_call>"};
|
||||
|
||||
std::string normal_text;
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(
|
||||
parser.parse_streaming_increment(chunk), &normal_text, &acc);
|
||||
}
|
||||
|
||||
EXPECT_EQ(normal_text, "Let me help you.\n\n");
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
EXPECT_EQ(acc[0].name, "get_current_weather");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingParameterEndedByNextParameter) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::string chunk =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Boston"
|
||||
"<parameter=unit>celsius</parameter>"
|
||||
"</function></tool_call>";
|
||||
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
|
||||
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Boston");
|
||||
EXPECT_EQ(params["unit"], "celsius");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingParameterEndedByFunctionEnd) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::string chunk =
|
||||
"<tool_call><function=get_current_weather>"
|
||||
"<parameter=location>Boston"
|
||||
"</function></tool_call>";
|
||||
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
|
||||
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Boston");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest,
|
||||
StreamingFunctionWithoutParametersEmitsEmptyJson) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::string chunk =
|
||||
"<tool_call><function=sql_interpreter></function></tool_call>";
|
||||
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
|
||||
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
EXPECT_EQ(acc[0].name, "sql_interpreter");
|
||||
EXPECT_EQ(acc[0].parameters, "{}");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingIgnoresTextInsideToolCallRegion) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {"before ",
|
||||
"<tool_call>\n",
|
||||
"THIS_SHOULD_BE_IGNORED",
|
||||
"<function=get_current_weather>",
|
||||
"<parameter=location>Rome</parameter>",
|
||||
"</function>",
|
||||
"</tool_call>",
|
||||
" after"};
|
||||
|
||||
std::string normal_text;
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(
|
||||
parser.parse_streaming_increment(chunk), &normal_text, &acc);
|
||||
}
|
||||
|
||||
EXPECT_EQ(normal_text, "before after");
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Rome");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest,
|
||||
StreamingKeepsLiteralAngleBracketOutsideToolCall) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {"2 < 3 and ", "5 > 4"};
|
||||
|
||||
std::string normal_text;
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(
|
||||
parser.parse_streaming_increment(chunk), &normal_text, &acc);
|
||||
}
|
||||
|
||||
EXPECT_EQ(normal_text, "2 < 3 and 5 > 4");
|
||||
EXPECT_TRUE(acc.empty());
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingPartialTagWaitsForMoreData) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
|
||||
auto result1 = parser.parse_streaming_increment("prefix <tool_ca");
|
||||
EXPECT_EQ(result1.normal_text, "prefix ");
|
||||
EXPECT_TRUE(result1.calls.empty());
|
||||
|
||||
auto result2 = parser.parse_streaming_increment(
|
||||
"ll><function=get_current_weather><parameter=location>Paris</parameter>"
|
||||
"</function></tool_call>");
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
merge_stream_result(result2, nullptr, &acc);
|
||||
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
EXPECT_EQ(acc[0].name, "get_current_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Paris");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest,
|
||||
StreamingUnknownTagInsideToolCallDoesNotBreakParsing) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {
|
||||
"<tool_call><unknown>abc</unknown><function=get_current_weather>",
|
||||
"<parameter=location>Madrid</parameter></function></tool_call>"};
|
||||
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
|
||||
}
|
||||
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
EXPECT_EQ(acc[0].name, "get_current_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Madrid");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest,
|
||||
StreamingTextAfterToolCallIsReturnedAsNormalText) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::vector<std::string> chunks = {
|
||||
"<tool_call><function=get_current_weather><parameter=location>Seoul</"
|
||||
"parameter></function></tool_call>",
|
||||
" done"};
|
||||
|
||||
std::string normal_text;
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (const auto& chunk : chunks) {
|
||||
merge_stream_result(
|
||||
parser.parse_streaming_increment(chunk), &normal_text, &acc);
|
||||
}
|
||||
|
||||
EXPECT_EQ(normal_text, " done");
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Seoul");
|
||||
}
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, StreamingCharacterByCharacter) {
|
||||
FunctionCallParser parser(tools_, "qwen3_coder");
|
||||
std::string text =
|
||||
"<tool_call><function=get_current_weather><parameter=location>Tokyo</"
|
||||
"parameter><parameter=days>2</parameter></function></tool_call>";
|
||||
|
||||
std::vector<StreamCallAccumulator> acc;
|
||||
for (char ch : text) {
|
||||
std::string chunk(1, ch);
|
||||
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
|
||||
}
|
||||
|
||||
ASSERT_EQ(acc.size(), 1);
|
||||
EXPECT_EQ(acc[0].name, "get_current_weather");
|
||||
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
|
||||
EXPECT_EQ(params["location"], "Tokyo");
|
||||
EXPECT_EQ(params["days"], 2);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Robustness / scale
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(Qwen3CoderDetectorTest, PerformanceWithManyToolCalls) {
|
||||
std::string text = "Performance test ";
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
text += "<tool_call><function=sql_interpreter><parameter=query>SELECT " +
|
||||
std::to_string(i) + "</parameter></function></tool_call>";
|
||||
}
|
||||
|
||||
auto result = detector_->detect_and_parse(text, tools_);
|
||||
EXPECT_EQ(result.normal_text, "Performance test ");
|
||||
ASSERT_EQ(result.calls.size(), 300);
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
EXPECT_EQ(result.calls[i].name.value_or(""), "sql_interpreter");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace function_call
|
||||
} // namespace xllm
|
||||
Reference in New Issue
Block a user