Skip to main content

lc_agents/react/
parser.rs

1// src/agents/react/parser.rs
2//! ReAct output parser
3//!
4//! Parses the LLM's ReAct-format output.
5
6use crate::{AgentAction, AgentError, AgentFinish, AgentOutput, ToolInput};
7use regex::Regex;
8
9/// ReAct output parser
10///
11/// Parsed format:
12/// ```text
13/// Thought: thought content
14/// Action: tool name
15/// Action Input: tool input
16/// ```
17/// or
18/// ```text
19/// Thought: thought content
20/// Final Answer: final answer
21/// ```
22pub struct ReActOutputParser {
23    /// Action regex
24    action_regex: Regex,
25    /// Final Answer marker
26    final_answer_marker: &'static str,
27}
28
29impl ReActOutputParser {
30    /// Creates a new parser
31    pub fn new() -> Self {
32        Self {
33            // Matches: Action: xxx\nAction Input: yyy
34            action_regex: Regex::new(r"Action\s*:\s*(.*?)\s*\nAction\s*Input\s*:\s*(.*?)(?:\n|$)")
35                .expect("Invalid regex"),
36            final_answer_marker: "Final Answer:",
37        }
38    }
39
40    /// Parses LLM output
41    ///
42    /// # Parameters
43    /// * `text` - the LLM's output text
44    ///
45    /// # Returns
46    /// * `AgentOutput::Action` - the action to execute (Action takes priority over Final Answer)
47    /// * `AgentOutput::Finish` - the final answer (content after the last occurrence)
48    pub fn parse(&self, text: &str) -> Result<AgentOutput, AgentError> {
49        let text = text.trim();
50
51        // F6: try Action first — if there is an Action, it wins. The model may
52        // mention "Final Answer:" in its Thought (explaining the format / giving
53        // an example) but then actually call a tool; the old logic treated any
54        // `contains` hit as the end and would skip the following Action.
55        if let Some(action) = self.parse_action(text)? {
56            return Ok(AgentOutput::Action(action));
57        }
58
59        // No Action: check Final Answer, take the content after the last occurrence.
60        if text.contains(self.final_answer_marker) {
61            return self.parse_final_answer(text);
62        }
63
64        // Unparseable
65        Err(AgentError::OutputParsingError(format!(
66            "failed to parse output. Use one of the following formats:\n\
67             Thought: <your reasoning>\n\
68             Action: <tool name>\n\
69             Action Input: <tool input>\n\n\
70             or\n\n\
71             Thought: <your reasoning>\n\
72             Final Answer: <final answer>\n\n\
73             Actual output: {}",
74            text
75        )))
76    }
77
78    /// Parses the Final Answer
79    fn parse_final_answer(&self, text: &str) -> Result<AgentOutput, AgentError> {
80        let parts: Vec<&str> = text.split(self.final_answer_marker).collect();
81
82        if parts.len() < 2 {
83            return Err(AgentError::OutputParsingError(
84                "missing content after Final Answer".to_string(),
85            ));
86        }
87
88        // F6: take the content after the last occurrence, not the first (the
89        // model may reference the marker several times mid-output; the real
90        // answer is at the end).
91        let answer = parts.last().unwrap_or(&"").trim().to_string();
92
93        Ok(AgentOutput::Finish(AgentFinish::new(
94            answer,
95            text.to_string(),
96        )))
97    }
98
99    /// Parses an Action
100    fn parse_action(&self, text: &str) -> Result<Option<AgentAction>, AgentError> {
101        if let Some(caps) = self.action_regex.captures(text) {
102            let tool = caps
103                .get(1)
104                .map(|m| m.as_str().trim().to_string())
105                .ok_or_else(|| AgentError::OutputParsingError("missing Action".to_string()))?;
106
107            let tool_input_str = caps
108                .get(2)
109                .map(|m| m.as_str().trim().to_string())
110                .ok_or_else(|| {
111                    AgentError::OutputParsingError("missing Action Input".to_string())
112                })?;
113
114            // Parse the tool input
115            let tool_input = self.parse_tool_input(&tool_input_str);
116
117            return Ok(Some(AgentAction {
118                tool,
119                tool_input,
120                log: text.to_string(),
121            }));
122        }
123
124        Ok(None)
125    }
126
127    /// Parses a tool input
128    fn parse_tool_input(&self, input: &str) -> ToolInput {
129        let input = input.trim();
130
131        // Try to parse as JSON
132        if input.starts_with('{') || input.starts_with('[') {
133            if let Ok(value) = serde_json::from_str(input) {
134                return ToolInput::Object { value };
135            }
136        }
137
138        // Strip surrounding quotes
139        let cleaned = input.trim_matches('"').trim_matches('\'');
140
141        ToolInput::String {
142            value: cleaned.to_string(),
143        }
144    }
145}
146
147impl Default for ReActOutputParser {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_parse_action() {
159        let parser = ReActOutputParser::new();
160
161        let text = r#"Thought: 我需要计算这个表达式
162Action: calculator
163Action Input: {"expression": "2 + 3"}"#;
164
165        let result = parser.parse(text).unwrap();
166
167        match result {
168            AgentOutput::Action(action) => {
169                assert_eq!(action.tool, "calculator");
170            }
171            _ => panic!("期望 Action"),
172        }
173    }
174
175    #[test]
176    fn test_parse_final_answer() {
177        let parser = ReActOutputParser::new();
178
179        let text = r#"Thought: 我已经知道答案了
180Final Answer: 答案是 42"#;
181
182        let result = parser.parse(text).unwrap();
183
184        match result {
185            AgentOutput::Finish(finish) => {
186                assert_eq!(finish.output(), Some("答案是 42"));
187            }
188            _ => panic!("期望 Finish"),
189        }
190    }
191
192    #[test]
193    fn test_parse_string_input() {
194        let parser = ReActOutputParser::new();
195
196        let text = r#"Thought: 需要查询天气
197Action: weather
198Action Input: 北京"#;
199
200        let result = parser.parse(text).unwrap();
201
202        match result {
203            AgentOutput::Action(action) => {
204                assert_eq!(action.tool, "weather");
205                match action.tool_input {
206                    ToolInput::String { value: s } => assert_eq!(s, "北京"),
207                    _ => panic!("期望 String 输入"),
208                }
209            }
210            _ => panic!("期望 Action"),
211        }
212    }
213
214    #[test]
215    fn test_parse_error() {
216        let parser = ReActOutputParser::new();
217
218        let text = "这是无效的输出";
219
220        let result = parser.parse(text);
221        assert!(result.is_err());
222    }
223
224    #[test]
225    fn test_action_preferred_when_final_answer_mentioned_in_thought() {
226        // F6: the Thought mentions "Final Answer:" (explaining the format) but a
227        // real Action follows — must parse as Action, not misjudge it as the end.
228        let parser = ReActOutputParser::new();
229
230        let text = r#"Thought: 用户要算数,不能用 Final Answer: 直接回答,需要调工具
231Action: calculator
232Action Input: {"expression": "2 + 3"}"#;
233
234        let result = parser.parse(text).unwrap();
235
236        match result {
237            AgentOutput::Action(action) => assert_eq!(action.tool, "calculator"),
238            _ => panic!("期望 Action,而不是被 'Final Answer:' 字样误判收尾"),
239        }
240    }
241
242    #[test]
243    fn test_final_answer_takes_last_occurrence() {
244        // F6: multiple Final Answer occurrences → take the content after the last one.
245        let parser = ReActOutputParser::new();
246
247        let text = r#"Thought: 先给个草稿
248Final Answer: 草稿答案
249Final Answer: 正式答案是 42"#;
250
251        let result = parser.parse(text).unwrap();
252
253        match result {
254            AgentOutput::Finish(finish) => {
255                assert_eq!(finish.output(), Some("正式答案是 42"));
256            }
257            _ => panic!("期望 Finish"),
258        }
259    }
260}