179 lines
5.0 KiB
Rust
179 lines
5.0 KiB
Rust
|
|
// Qwen3 specific reasoning parser.
|
||
|
|
// This parser starts with in_reasoning=false, requiring an explicit
|
||
|
|
// start token to enter reasoning mode.
|
||
|
|
|
||
|
|
use crate::reasoning_parser::parsers::BaseReasoningParser;
|
||
|
|
use crate::reasoning_parser::traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
|
||
|
|
|
||
|
|
/// Qwen3 reasoning parser.
|
||
|
|
///
|
||
|
|
/// This parser requires explicit <think> tokens to enter reasoning mode
|
||
|
|
/// (in_reasoning=false initially).
|
||
|
|
pub struct Qwen3Parser {
|
||
|
|
base: BaseReasoningParser,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Qwen3Parser {
|
||
|
|
/// Create a new Qwen3 parser.
|
||
|
|
pub fn new() -> Self {
|
||
|
|
let config = ParserConfig {
|
||
|
|
think_start_token: "<think>".to_string(),
|
||
|
|
think_end_token: "</think>".to_string(),
|
||
|
|
stream_reasoning: true,
|
||
|
|
max_buffer_size: 65536,
|
||
|
|
initial_in_reasoning: false, // Requires explicit start token
|
||
|
|
};
|
||
|
|
|
||
|
|
Self {
|
||
|
|
base: BaseReasoningParser::new(config).with_model_type("qwen3".to_string()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for Qwen3Parser {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self::new()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ReasoningParser for Qwen3Parser {
|
||
|
|
fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
|
||
|
|
self.base.detect_and_parse_reasoning(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_reasoning_streaming_incremental(
|
||
|
|
&mut self,
|
||
|
|
text: &str,
|
||
|
|
) -> Result<ParserResult, ParseError> {
|
||
|
|
self.base.parse_reasoning_streaming_incremental(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn reset(&mut self) {
|
||
|
|
self.base.reset()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn model_type(&self) -> &str {
|
||
|
|
self.base.model_type()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// QwenThinking parser - variant that assumes reasoning from start.
|
||
|
|
///
|
||
|
|
/// This is for qwen*thinking models that behave like DeepSeek-R1.
|
||
|
|
pub struct QwenThinkingParser {
|
||
|
|
base: BaseReasoningParser,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl QwenThinkingParser {
|
||
|
|
/// Create a new QwenThinking parser.
|
||
|
|
pub fn new() -> Self {
|
||
|
|
let config = ParserConfig {
|
||
|
|
think_start_token: "<think>".to_string(),
|
||
|
|
think_end_token: "</think>".to_string(),
|
||
|
|
stream_reasoning: true,
|
||
|
|
max_buffer_size: 65536,
|
||
|
|
initial_in_reasoning: true, // Assumes reasoning from start
|
||
|
|
};
|
||
|
|
|
||
|
|
Self {
|
||
|
|
base: BaseReasoningParser::new(config).with_model_type("qwen_thinking".to_string()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for QwenThinkingParser {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self::new()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ReasoningParser for QwenThinkingParser {
|
||
|
|
fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
|
||
|
|
self.base.detect_and_parse_reasoning(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_reasoning_streaming_incremental(
|
||
|
|
&mut self,
|
||
|
|
text: &str,
|
||
|
|
) -> Result<ParserResult, ParseError> {
|
||
|
|
self.base.parse_reasoning_streaming_incremental(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn reset(&mut self) {
|
||
|
|
self.base.reset()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn model_type(&self) -> &str {
|
||
|
|
self.base.model_type()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_qwen3_initial_state() {
|
||
|
|
let mut parser = Qwen3Parser::new();
|
||
|
|
|
||
|
|
// Should NOT treat text as reasoning without start token
|
||
|
|
let result = parser
|
||
|
|
.detect_and_parse_reasoning("This is normal content")
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(result.normal_text, "This is normal content");
|
||
|
|
assert_eq!(result.reasoning_text, "");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_qwen3_with_tokens() {
|
||
|
|
let mut parser = Qwen3Parser::new();
|
||
|
|
|
||
|
|
// Should extract reasoning with proper tokens
|
||
|
|
let result = parser
|
||
|
|
.detect_and_parse_reasoning("<think>reasoning</think>answer")
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(result.normal_text, "answer");
|
||
|
|
assert_eq!(result.reasoning_text, "reasoning");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_qwen_thinking_initial_state() {
|
||
|
|
let mut parser = QwenThinkingParser::new();
|
||
|
|
|
||
|
|
// Should treat text as reasoning even without start token
|
||
|
|
let result = parser
|
||
|
|
.detect_and_parse_reasoning("This is reasoning content")
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(result.normal_text, "");
|
||
|
|
assert_eq!(result.reasoning_text, "This is reasoning content");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_qwen3_streaming() {
|
||
|
|
let mut parser = Qwen3Parser::new();
|
||
|
|
|
||
|
|
// First chunk - normal text (no start token yet)
|
||
|
|
let result1 = parser
|
||
|
|
.parse_reasoning_streaming_incremental("normal text ")
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(result1.normal_text, "normal text ");
|
||
|
|
assert_eq!(result1.reasoning_text, "");
|
||
|
|
|
||
|
|
// Second chunk - enters reasoning
|
||
|
|
let result2 = parser
|
||
|
|
.parse_reasoning_streaming_incremental("<think>reasoning")
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(result2.normal_text, "");
|
||
|
|
assert_eq!(result2.reasoning_text, "reasoning");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_model_types() {
|
||
|
|
let qwen3 = Qwen3Parser::new();
|
||
|
|
assert_eq!(qwen3.model_type(), "qwen3");
|
||
|
|
|
||
|
|
let qwen_thinking = QwenThinkingParser::new();
|
||
|
|
assert_eq!(qwen_thinking.model_type(), "qwen_thinking");
|
||
|
|
}
|
||
|
|
}
|