Skip to content

Commit 8762d44

Browse files
feat: python as a subprocess reasoning parser implementation
1 parent 7f107b6 commit 8762d44

File tree

9 files changed

+437
-55
lines changed

9 files changed

+437
-55
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/backends/vllm/src/dynamo/vllm/args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def parse_args() -> Config:
117117
"--dyn-reasoning-parser",
118118
type=str,
119119
default=None,
120-
help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss'.",
120+
help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss'. This can also be a file path to a custom Python reasoning parser implementation of the `dynamo.reasoning_parser.BaseReasoningParser` interface.",
121121
)
122122

123123
parser = AsyncEngineArgs.add_cli_args(parser)

example_python_parser.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

lib/bindings/python/examples/basic_reasoning_parser/basic_parser.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
14
from typing import Sequence, Tuple
25

36
# import from __init__.py in the same directory
47
from dynamo.reasoning_parser import BaseReasoningParser
58

9+
610
class BasicReasoningParser(BaseReasoningParser):
711
"""Base class providing two sets of interfaces: one-time and streaming incremental."""
812

@@ -17,7 +21,9 @@ def __init__(
1721
self._buffer = ""
1822
self.stripped_think_start = False
1923

20-
def detect_and_parse_reasoning(self, text: str, _token_ids: Sequence[int]) -> Tuple[str, str]:
24+
def detect_and_parse_reasoning(
25+
self, text: str, _token_ids: Sequence[int]
26+
) -> Tuple[str, str]:
2127
"""
2228
One-time parsing: Detects and parses reasoning sections in the provided text.
2329
Returns both reasoning content and normal text separately.
@@ -41,7 +47,9 @@ def detect_and_parse_reasoning(self, text: str, _token_ids: Sequence[int]) -> Tu
4147

4248
return (normal_text, reasoning_text)
4349

44-
def parse_reasoning_streaming_incremental(self, new_text: str, _token_ids: Sequence[int]) -> Tuple[str, str]:
50+
def parse_reasoning_streaming_incremental(
51+
self, new_text: str, _token_ids: Sequence[int]
52+
) -> Tuple[str, str]:
4553
"""
4654
Streaming incremental parsing for reasoning content.
4755
Handles partial reasoning tags and content.
@@ -82,7 +90,6 @@ def parse_reasoning_streaming_incremental(self, new_text: str, _token_ids: Seque
8290
# Continue with reasoning content
8391
if self._in_reasoning:
8492
if self.stream_reasoning:
85-
# Stream the content immediately
8693
self._buffer = ""
8794
return ("", current_text)
8895
else:
@@ -93,4 +100,4 @@ def parse_reasoning_streaming_incremental(self, new_text: str, _token_ids: Seque
93100
self._buffer = ""
94101
return (current_text, "")
95102

96-
return ("", "")
103+
return ("", "")

lib/bindings/python/src/dynamo/reasoning_parser/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
13

4+
from typing import Protocol, Sequence, Tuple
25

3-
from typing import Sequence, Tuple
4-
from typing import Protocol
56

67
class BaseReasoningParser(Protocol):
7-
88
def __init__(self):
99
"""Initialize the reasoning parser.
1010
1111
This method should set up any necessary internal state or configurations.
1212
1313
Signature must not change and must not take any arguments other than self.
14-
14+
1515
"""
1616
...
1717

18-
def detect_and_parse_reasoning(self, text: str, token_ids: Sequence[int]) -> Tuple[str, str]:
18+
def detect_and_parse_reasoning(
19+
self, text: str, token_ids: Sequence[int]
20+
) -> Tuple[str, str]:
1921
"""Detect and parse reasoning from the given text and token IDs.
2022
2123
Args:
@@ -28,8 +30,10 @@ def detect_and_parse_reasoning(self, text: str, token_ids: Sequence[int]) -> Tup
2830
(normal_text, reasoning_text)
2931
"""
3032
...
31-
32-
def parse_reasoning_streaming_incremental(self, text: str, token_ids: Sequence[int]) -> Tuple[str, str]:
33+
34+
def parse_reasoning_streaming_incremental(
35+
self, text: str, token_ids: Sequence[int]
36+
) -> Tuple[str, str]:
3337
"""Parse reasoning from the given text and token IDs in a streaming incremental manner.
3438
3539
Args:
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from typing import Sequence, Tuple
5+
6+
# import from __init__.py in the same directory
7+
from __init__ import BaseReasoningParser
8+
9+
10+
class BasicReasoningParser(BaseReasoningParser):
11+
"""Base class providing two sets of interfaces: one-time and streaming incremental."""
12+
13+
def __init__(
14+
self,
15+
):
16+
self.think_start_token = "<think>"
17+
self.think_end_token = "</think>"
18+
self._in_reasoning = False
19+
self.stream_reasoning = True
20+
21+
self._buffer = ""
22+
self.stripped_think_start = False
23+
24+
def detect_and_parse_reasoning(
25+
self, text: str, _token_ids: Sequence[int]
26+
) -> Tuple[str, str]:
27+
"""
28+
One-time parsing: Detects and parses reasoning sections in the provided text.
29+
Returns both reasoning content and normal text separately.
30+
"""
31+
in_reasoning = self._in_reasoning or self.think_start_token in text
32+
33+
if not in_reasoning:
34+
return (text, "")
35+
36+
# The text is considered to be in a reasoning block.
37+
processed_text = text.replace(self.think_start_token, "").strip()
38+
39+
if self.think_end_token not in processed_text:
40+
# Assume reasoning was truncated before `</think>` token
41+
return ("", processed_text)
42+
43+
# Extract reasoning content
44+
splits = processed_text.split(self.think_end_token, maxsplit=1)
45+
reasoning_text = splits[0]
46+
normal_text = splits[1].strip()
47+
48+
return (normal_text, reasoning_text)
49+
50+
def parse_reasoning_streaming_incremental(
51+
self, new_text: str, _token_ids: Sequence[int]
52+
) -> Tuple[str, str]:
53+
"""
54+
Streaming incremental parsing for reasoning content.
55+
Handles partial reasoning tags and content.
56+
57+
If stream_reasoning is False:
58+
Accumulates reasoning content until the end tag is found
59+
If stream_reasoning is True:
60+
Streams reasoning content as it arrives
61+
"""
62+
self._buffer += new_text
63+
current_text = self._buffer
64+
65+
# If the current text is a prefix of the think token, keep buffering
66+
if any(
67+
token.startswith(current_text) and token != current_text
68+
for token in [self.think_start_token, self.think_end_token]
69+
):
70+
return ("", "")
71+
72+
# Strip `<think>` token if present
73+
if not self.stripped_think_start and self.think_start_token in current_text:
74+
current_text = current_text.replace(self.think_start_token, "")
75+
self.stripped_think_start = True
76+
self._in_reasoning = True
77+
78+
# Handle end of reasoning block
79+
if self._in_reasoning and self.think_end_token in current_text:
80+
end_idx = current_text.find(self.think_end_token)
81+
82+
reasoning_text = current_text[:end_idx]
83+
84+
self._buffer = ""
85+
self._in_reasoning = False
86+
normal_text = current_text[end_idx + len(self.think_end_token) :]
87+
88+
return (normal_text, reasoning_text.rstrip())
89+
90+
# Continue with reasoning content
91+
if self._in_reasoning:
92+
if self.stream_reasoning:
93+
self._buffer = ""
94+
return ("", current_text)
95+
else:
96+
return ("", "")
97+
98+
# If we're not in a reasoning block return as normal text
99+
if not self._in_reasoning:
100+
self._buffer = ""
101+
return (current_text, "")
102+
103+
return ("", "")

lib/parsers/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ uuid = { workspace = true }
3535
regex = "1"
3636
openai-harmony = "0.0.3"
3737
lazy_static = "1.5.0"
38+
minijinja = "2.12.0"

lib/parsers/src/reasoning/mod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
mod base_parser;
55
mod deepseek_r1_parser;
66
mod gpt_oss_parser;
7+
mod python_process_parser;
78

89
// Re-export main types and functions for convenience
910
pub use base_parser::BasicReasoningParser;
1011
pub use deepseek_r1_parser::DeepseekR1ReasoningParser;
1112
pub use gpt_oss_parser::GptOssReasoningParser;
13+
pub use python_process_parser::PythonProcessParser;
1214

1315
#[derive(Debug, Clone, Default)]
1416
pub struct ParserResult {
@@ -116,16 +118,22 @@ impl ReasoningParserType {
116118
}
117119
}
118120

119-
pub fn get_reasoning_parser_from_name(name: &str) -> ReasoningParserWrapper {
120-
tracing::debug!("Selected reasoning parser: {}", name);
121-
match name.to_lowercase().as_str() {
121+
pub fn get_reasoning_parser_from_name(name_or_path: &str) -> ReasoningParserWrapper {
122+
tracing::debug!("Selected reasoning parser: {}", name_or_path);
123+
// check if name_or_path is a file path
124+
if std::path::Path::new(name_or_path).exists() {
125+
return ReasoningParserWrapper {
126+
parser: Box::new(PythonProcessParser::new(name_or_path)),
127+
};
128+
}
129+
match name_or_path.to_lowercase().as_str() {
122130
"deepseek_r1" => Self::DeepseekR1.get_reasoning_parser(),
123131
"basic" => Self::Basic.get_reasoning_parser(),
124132
"gpt_oss" => Self::GptOss.get_reasoning_parser(),
125133
_ => {
126134
tracing::warn!(
127135
"Unknown reasoning parser type '{}', falling back to Basic Reasoning Parser",
128-
name
136+
name_or_path
129137
);
130138
Self::Basic.get_reasoning_parser()
131139
}

0 commit comments

Comments
 (0)