use regex::RegexBuilder;
use serde_json::Value;
use uuid::Uuid;
use super::super::ToolDefinition;
use super::config::JsonParserConfig;
use super::response::{CalledFunction, ToolCallResponse, ToolCallType};
fn extract_tool_call_blocks_v3(
input: &str,
start_tokens: &[String],
end_tokens: &[String],
) -> Vec<String> {
let mut blocks = Vec::new();
let individual_start_tokens: Vec<&String> = start_tokens
.iter()
.filter(|t| t.contains("tool_call_begin") || t.contains("tool▁call▁begin"))
.collect();
let individual_end_tokens: Vec<&String> = end_tokens
.iter()
.filter(|t| t.contains("tool_call_end") || t.contains("tool▁call▁end"))
.collect();
for start_token in individual_start_tokens.iter() {
for end_token in individual_end_tokens.iter() {
if start_token.is_empty() || end_token.is_empty() {
continue;
}
let escaped_start = regex::escape(start_token);
let escaped_end = regex::escape(end_token);
let pattern = format!(r"{}(.*?){}", escaped_start, escaped_end);
if let Ok(regex) = RegexBuilder::new(&pattern)
.dot_matches_new_line(true)
.build()
{
for capture in regex.captures_iter(input) {
if let Some(matched) = capture.get(1) {
let content = matched.as_str();
if !content.trim().is_empty() {
blocks.push(content.to_string());
}
}
}
if !blocks.is_empty() {
return blocks;
}
}
}
}
blocks
}
fn parse_single_tool_call_v3(block: &str, separator_tokens: &[String]) -> Option<(String, Value)> {
for sep_token in separator_tokens.iter() {
if sep_token.is_empty() {
continue;
}
if let Some((_type_part, function_and_args_part)) = block.split_once(sep_token) {
let (function_name_part, args_block) = function_and_args_part.split_once('\n')?;
let function_name = function_name_part.trim();
if function_name.is_empty() || function_name.contains(['{', '}', '[', ']']) {
continue;
}
let args_str = if let Some(json_start) = args_block.find("```json") {
let after_fence = &args_block[json_start + "```json".len()..];
let after_newline = after_fence
.strip_prefix("\r\n")
.or_else(|| after_fence.strip_prefix('\n'))
.unwrap_or(after_fence);
if let Some(json_end) = after_newline.find("```") {
after_newline[..json_end].trim()
} else {
after_newline.trim()
}
} else {
args_block.trim()
};
if let Ok(arguments) = serde_json::from_str::<Value>(args_str) {
return Some((function_name.to_string(), arguments));
}
let normalized = args_str
.lines()
.map(|line| line.trim_start())
.collect::<Vec<_>>()
.join(" ");
if let Ok(arguments) = serde_json::from_str::<Value>(&normalized) {
return Some((function_name.to_string(), arguments));
}
}
}
None
}
pub fn parse_tool_calls_deepseek_v3(
message: &str,
config: &JsonParserConfig,
_tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
let trimmed = message.trim();
if trimmed.is_empty() {
return Ok((vec![], Some(String::new())));
}
let has_end_token = config
.tool_call_end_tokens
.iter()
.any(|token| !token.is_empty() && trimmed.contains(token));
if !has_end_token {
return Ok((vec![], Some(trimmed.to_string())));
}
let mut tool_call_start_tokens = config.tool_call_start_tokens.clone();
tool_call_start_tokens.extend(vec!["<|tool▁call▁begin|>".to_string()]);
let mut tool_call_end_tokens = config.tool_call_end_tokens.clone();
tool_call_end_tokens.extend(vec!["<|tool▁call▁end|>".to_string()]);
let separator_tokens = &config.tool_call_separator_tokens;
if tool_call_start_tokens.is_empty() || separator_tokens.is_empty() {
return Ok((vec![], Some(trimmed.to_string())));
}
if !detect_tool_call_start_deepseek_v3(trimmed, config) {
return Ok((vec![], Some(trimmed.to_string())));
}
let wrapper_tokens: Vec<&String> = tool_call_start_tokens
.iter()
.filter(|t| t.contains("tool_calls_begin") || t.contains("tool▁calls▁begin"))
.collect();
let normal_text = if !wrapper_tokens.is_empty() {
wrapper_tokens
.iter()
.find_map(|token| {
trimmed
.find(token.as_str())
.map(|idx| trimmed[..idx].to_string())
})
.unwrap_or_else(String::new)
} else {
tool_call_start_tokens
.iter()
.filter(|token| !token.is_empty())
.find_map(|token| trimmed.find(token).map(|idx| trimmed[..idx].to_string()))
.unwrap_or_else(String::new)
};
let blocks =
extract_tool_call_blocks_v3(trimmed, &tool_call_start_tokens, &tool_call_end_tokens);
if blocks.is_empty() {
return Ok((vec![], Some(trimmed.to_string())));
}
let mut tool_calls: Vec<ToolCallResponse> = Vec::new();
for block in blocks {
if let Some((function_name, arguments)) =
parse_single_tool_call_v3(&block, separator_tokens)
{
tool_calls.push(ToolCallResponse {
id: format!("call-{}", Uuid::new_v4()),
tp: ToolCallType::Function,
function: CalledFunction {
name: function_name,
arguments: serde_json::to_string(&arguments)?,
},
});
}
}
if tool_calls.is_empty() {
return Ok((vec![], Some(trimmed.to_string())));
}
Ok((tool_calls, Some(normal_text)))
}
pub fn detect_tool_call_start_deepseek_v3(chunk: &str, config: &JsonParserConfig) -> bool {
let trimmed = chunk.trim();
if trimmed.is_empty() {
return false;
}
let has_complete_token = config
.tool_call_start_tokens
.iter()
.any(|token| !token.is_empty() && trimmed.contains(token));
if has_complete_token {
return true;
}
config.tool_call_start_tokens.iter().any(|token| {
if token.is_empty() {
return false;
}
for i in 1..=token.chars().count() {
if let Some(prefix) = token.chars().take(i).collect::<String>().get(..) {
let prefix_str = &prefix[..prefix.len()];
if trimmed == prefix_str || trimmed.ends_with(prefix_str) {
return true;
}
}
}
false
})
}
#[cfg(test)]
mod tests {
use super::super::config::ToolCallConfig;
use super::*;
fn extract_name_and_args(call: ToolCallResponse) -> (String, serde_json::Value) {
let args: serde_json::Value = serde_json::from_str(&call.function.arguments).unwrap();
(call.function.name, args)
}
#[test] fn test_parse_tool_calls_deepseek_v3_basic() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "HongKong"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Paris"}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(content, Some("".to_string()));
assert_eq!(result.len(), 2);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "HongKong");
let (name, args) = extract_name_and_args(result[1].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "Paris");
}
#[test] fn test_parse_tool_calls_deepseek_v3_with_normal_text() {
let text = r#"The following tool call retrieves weather information: <|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "New York"}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(
content,
Some("The following tool call retrieves weather information: ".to_string())
);
assert_eq!(result.len(), 1);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "New York");
}
#[test] fn test_parse_tool_calls_deepseek_v3_without_tool_call_start_token() {
let text = r#"<|tool▁call▁begin|>function宽带}{location": "HongKong"}
```json
}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(content, Some(text.to_string()));
assert_eq!(result.len(), 0);
}
#[test] fn test_parse_tool_calls_deepseek_v3_with_multi_tool_calls_with_multiple_args() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Shanghai", "units": "metric"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_weather_forecast
```json
{"location": "Shanghai", "days": 7, "units": "imperial"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_air_quality
```json
{"location": "Shanghai", "radius": 50}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(content, Some("".to_string()));
assert_eq!(result.len(), 3);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "Shanghai");
assert_eq!(args["units"], "metric");
let (name, args) = extract_name_and_args(result[1].clone());
assert_eq!(name, "get_weather_forecast");
assert_eq!(args["location"], "Shanghai");
assert_eq!(args["days"], 7);
assert_eq!(args["units"], "imperial");
let (name, args) = extract_name_and_args(result[2].clone());
assert_eq!(name, "get_air_quality");
assert_eq!(args["location"], "Shanghai");
assert_eq!(args["radius"], 50);
}
#[test] fn test_parse_tool_calls_deepseek_v3_with_invalid_json() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather}{location": "HongKong"}
```json
}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(content, Some(text.trim().to_string()));
assert_eq!(result.len(), 0);
}
#[test] fn test_parse_tool_calls_deepseek_v3_with_multi_tool_calls_with_normal_text() {
let text = r#"The following tool calls retrieve weather information: <|tool▁calls▁begin|><|tool▁call▁begin|>function宽带}{location": "HongKong"}
```json
}
```<|tool▁call▁end|><|tool▁call▁begin|>function宽带}{location": "Shanghai", "days": 7, "units": "imperial"}
```json
}
```<|tool▁call▁end|><|tool▁call▁begin|>function宽带}{location": "Shanghai", "radius": 50}
```json
}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(content, Some(text.trim().to_string()));
assert_eq!(result.len(), 0);
}
#[test] fn test_parse_tool_calls_deepseek_v3_with_multiline_json() {
let text = r#"I'll help you understand this Xiaohongshu codebase. Let me start by exploring the structure
and key files to provide you with a comprehensive
explanation.<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>TodoWrite
```json
{"todos":
[{"content": "Explore the root directory structure", "status": "in_progress", "activeForm":
"Exploring the root directory structure"}, {"content": "Examine package.json and
configuration files", "status": "pending", "activeForm": "Examining package.json and
configuration files"}, {"content": "Analyze source code structure and key modules",
"status": "pending", "activeForm": "Analyzing source code structure and key modules"},
{"content": "Identify main entry points and architectural patterns", "status": "pending",
"activeForm": "Identifying main entry points and architectural patterns"}, {"content":
"Summarize the codebase purpose and functionality", "status": "pending", "activeForm":
"Summarizing the codebase purpose and
functionality"}]}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let (tool_call_results, normal_content) =
parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
assert_eq!(tool_call_results.len(), 1);
let (name, args) = extract_name_and_args(tool_call_results[0].clone());
assert_eq!(name, "TodoWrite");
assert_eq!(tool_call_results[0].tp, ToolCallType::Function);
let todos_array = args["todos"].as_array().unwrap();
assert_eq!(todos_array.len(), 5);
assert_eq!(
todos_array[0]["content"],
"Explore the root directory structure"
);
assert_eq!(todos_array[0]["status"], "in_progress");
assert_eq!(
todos_array[0]["activeForm"],
"Exploring the root directory structure"
);
assert_eq!(
todos_array[1]["content"],
"Examine package.json and configuration files"
);
assert_eq!(todos_array[1]["status"], "pending");
assert_eq!(
todos_array[4]["content"],
"Summarize the codebase purpose and functionality"
);
assert_eq!(todos_array[4]["status"], "pending");
assert_eq!(
normal_content,
Some("I'll help you understand this Xiaohongshu codebase. Let me start by exploring the structure\n and key files to provide you with a comprehensive\n explanation.".to_string())
);
}
}
#[cfg(test)]
mod detect_parser_tests {
use super::super::config::ToolCallConfig;
use super::*;
#[test] fn test_detect_tool_call_start_deepseek_v3_chunk_with_tool_call_start_token() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function宽带}"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let result = detect_tool_call_start_deepseek_v3(text, &config);
assert!(result);
}
#[test] fn test_detect_tool_call_start_deepseek_v3_chunk_without_tool_call_start_token() {
let text = r#"<|tool▁call▁begin|>function宽带}"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let result = detect_tool_call_start_deepseek_v3(text, &config);
assert!(!result);
}
#[test] fn test_detect_tool_call_start_deepseek_v3_chunk_with_tool_call_start_token_in_middle() {
let text = r#"The following tool calls retrieve weather information: <|tool▁calls▁begin|><|tool▁call▁begin|>function宽带}"#;
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
let result = detect_tool_call_start_deepseek_v3(text, &config);
assert!(result);
}
#[test] fn test_detect_tool_call_start_deepseek_v3_partial_tokens() {
let config = match ToolCallConfig::deepseek_v3().parser_config {
super::super::config::ParserConfig::Json(cfg) => cfg,
_ => panic!("Expected JSON parser config"),
};
assert!(
detect_tool_call_start_deepseek_v3("<", &config),
"'<' should be detected as potential start"
);
assert!(
detect_tool_call_start_deepseek_v3("<|", &config),
"'<|' should be detected as potential start"
);
assert!(
detect_tool_call_start_deepseek_v3("<|tool", &config),
"'<|tool' should be detected as potential start"
);
assert!(
detect_tool_call_start_deepseek_v3("<|tool▁calls", &config),
"'<|tool▁calls' should be detected as potential start"
);
assert!(
!detect_tool_call_start_deepseek_v3("hello world", &config),
"'hello world' should not be detected"
);
assert!(
!detect_tool_call_start_deepseek_v3("xyz", &config),
"'xyz' should not be detected"
);
}
}