1use super::parser::ReActOutputParser;
8use super::prompt::{build_react_prompt, format_scratchpad};
9use crate::{AgentError, AgentOutput, AgentStep, BaseAgent};
10use async_trait::async_trait;
11use lc_core::language_models::BaseChatModel;
12use lc_core::tools::BaseTool;
13use lc_providers::ProviderError;
14use lc_schema::Message;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18pub struct ReActAgent {
24 llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
26
27 tools: Vec<Arc<dyn BaseTool>>,
29
30 parser: ReActOutputParser,
32
33 system_prompt: Option<String>,
35}
36
37impl ReActAgent {
38 pub fn new<L>(llm: L, tools: Vec<Arc<dyn BaseTool>>, system_prompt: Option<String>) -> Self
49 where
50 L: BaseChatModel + Send + Sync + 'static,
51 L::Error: Into<ProviderError>,
52 {
53 Self {
54 llm: lc_providers::wrap_chat_model(llm),
55 tools,
56 parser: ReActOutputParser::new(),
57 system_prompt,
58 }
59 }
60
61 pub fn from_arc(
63 llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
64 tools: Vec<Arc<dyn BaseTool>>,
65 system_prompt: Option<String>,
66 ) -> Self {
67 Self {
68 llm,
69 tools,
70 parser: ReActOutputParser::new(),
71 system_prompt,
72 }
73 }
74
75 fn format_tools(&self) -> String {
79 self.tools
80 .iter()
81 .map(|tool| format!("{}: {}", tool.name(), tool.description()))
82 .collect::<Vec<_>>()
83 .join("\n")
84 }
85
86 fn get_tool_names(&self) -> Vec<&str> {
88 self.tools.iter().map(|t| t.name()).collect()
89 }
90
91 fn build_prompt(
98 &self,
99 input: &str,
100 intermediate_steps: &[AgentStep],
101 history: Option<&str>,
102 ) -> String {
103 let tools_description = self.format_tools();
105 let tool_names = self.get_tool_names();
106
107 let scratchpad = format_scratchpad(intermediate_steps);
109
110 let mut prompt = build_react_prompt(&tools_description, &tool_names, input, &scratchpad);
112
113 if let Some(h) = history {
115 if !h.is_empty() {
116 prompt = format!("之前的对话历史:\n{}\n\n{}", h, prompt);
117 }
118 }
119
120 if let Some(sys) = &self.system_prompt {
122 prompt = format!("{}\n\n{}", sys, prompt);
123 }
124
125 prompt
126 }
127}
128
129#[async_trait]
130impl BaseAgent for ReActAgent {
131 async fn plan(
141 &self,
142 intermediate_steps: &[AgentStep],
143 inputs: &HashMap<String, String>,
144 ) -> Result<AgentOutput, AgentError> {
145 let input = inputs
147 .get("input")
148 .ok_or_else(|| AgentError::Other("Missing input parameter 'input'".to_string()))?;
149
150 let history = inputs.get("history").map(|s| s.as_str());
152
153 let prompt_text = self.build_prompt(input, intermediate_steps, history);
155
156 let messages = vec![Message::human(prompt_text)];
158
159 let result = self
161 .llm
162 .chat(messages, None)
163 .await
164 .map_err(|e| AgentError::Other(format!("LLM call failed: {}", e)))?;
165
166 self.parser.parse(&result.content)
168 }
169
170 fn get_allowed_tools(&self) -> Option<Vec<&str>> {
172 Some(self.get_tool_names())
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::{AgentAction, ToolInput};
180 use lc_providers::{OpenAIChat, OpenAIConfig};
181 use lc_tools::Calculator;
182
183 fn create_test_config() -> OpenAIConfig {
185 OpenAIConfig {
186 api_key: "sk-6eb65fcf5d17491ca10b984efe1f43e7".to_string(),
187 base_url:
188 "https://llm-8xo1b7o30z27y2xc.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
189 .to_string(),
190 model: "glm-5.2".to_string(),
191 temperature: Some(0.0),
192 max_tokens: Some(500),
193 top_p: None,
194 frequency_penalty: None,
195 presence_penalty: None,
196 streaming: false,
197 organization: None,
198 tools: None,
199 tool_choice: None,
200 }
201 }
202
203 #[test]
204 fn test_format_tools_description() {
205 let config = create_test_config();
206 let llm = OpenAIChat::new(config);
207 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
208 let agent = ReActAgent::new(llm, tools, None);
209
210 let desc = agent.format_tools();
211 assert!(desc.contains("calculator"));
212 }
213
214 #[test]
215 fn test_get_tool_names() {
216 let config = create_test_config();
217 let llm = OpenAIChat::new(config);
218 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
219 let agent = ReActAgent::new(llm, tools, None);
220
221 let names = agent.get_tool_names();
222 assert_eq!(names, vec!["calculator"]);
223 }
224
225 #[test]
226 fn test_build_prompt() {
227 let config = create_test_config();
228 let llm = OpenAIChat::new(config);
229 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
230 let agent = ReActAgent::new(llm, tools, None);
231
232 let prompt = agent.build_prompt("计算 2 + 2", &[], None);
233
234 assert!(prompt.contains("计算 2 + 2"));
235 assert!(prompt.contains("calculator"));
236 assert!(prompt.contains("Question:"));
237 assert!(prompt.contains("Thought:"));
238 }
239
240 #[test]
241 fn test_build_prompt_with_history() {
242 let config = create_test_config();
243 let llm = OpenAIChat::new(config);
244 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
245 let agent = ReActAgent::new(llm, tools, None);
246
247 let prompt = agent.build_prompt("计算 3 + 3", &[], Some("用户: 你好\n助手: 你好!"));
248
249 assert!(prompt.contains("之前的对话历史"));
250 assert!(prompt.contains("你好"));
251 }
252
253 #[test]
254 fn test_build_prompt_with_system_prompt() {
255 let config = create_test_config();
256 let llm = OpenAIChat::new(config);
257 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
258 let agent = ReActAgent::new(llm, tools, Some("你是一个数学助手".to_string()));
259
260 let prompt = agent.build_prompt("计算 4 + 4", &[], None);
261
262 assert!(prompt.contains("你是一个数学助手"));
263 }
264
265 #[tokio::test]
267 #[ignore = "需要真实 API 调用"]
268 async fn test_real_api_simple() {
269 let config = create_test_config();
270 let llm = OpenAIChat::new(config);
271 let tools: Vec<Arc<dyn BaseTool>> = vec![];
272 let agent = ReActAgent::new(llm, tools, None);
273
274 let mut inputs = HashMap::new();
275 inputs.insert("input".to_string(), "什么是 Rust 语言?".to_string());
276
277 let result = agent.plan(&[], &inputs).await.unwrap();
278
279 match result {
281 AgentOutput::Finish(finish) => {
282 println!("答案: {:?}", finish.return_values);
283 assert!(finish.output().is_some());
284 }
285 AgentOutput::Action(_) => {
286 println!("LLM 尝试调用工具");
287 }
288 AgentOutput::Actions(_) => {
289 println!("ReActAgent 不支持并行工具调用");
290 }
291 }
292 }
293
294 #[tokio::test]
296 #[ignore = "需要真实 API 调用"]
297 async fn test_real_api_with_calculator() {
298 let config = create_test_config();
299 let llm = OpenAIChat::new(config);
300 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
301 let agent = ReActAgent::new(llm, tools, None);
302
303 let mut inputs = HashMap::new();
304 inputs.insert("input".to_string(), "计算 37 加 48 等于多少?".to_string());
305
306 let result = agent.plan(&[], &inputs).await.unwrap();
307
308 match result {
309 AgentOutput::Action(action) => {
310 println!("动作: {}({})", action.tool, action.tool_input);
311 assert_eq!(action.tool, "calculator");
312 }
313 AgentOutput::Finish(finish) => {
314 println!("直接答案: {:?}", finish.return_values);
315 }
316 AgentOutput::Actions(_) => {
317 println!("ReActAgent 不支持并行工具调用");
318 }
319 }
320 }
321
322 #[tokio::test]
324 #[ignore = "需要真实 API 调用"]
325 async fn test_real_api_multi_step() {
326 let config = create_test_config();
327 let llm = OpenAIChat::new(config);
328 let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
329 let agent = ReActAgent::new(llm, tools, None);
330
331 let steps = vec![AgentStep::new(
333 AgentAction {
334 tool: "calculator".to_string(),
335 tool_input: ToolInput::String {
336 value: "37 + 48".to_string(),
337 },
338 log: "我需要先计算 37 + 48".to_string(),
339 },
340 "85".to_string(),
341 )];
342
343 let mut inputs = HashMap::new();
344 inputs.insert(
345 "input".to_string(),
346 "计算 (37 + 48) * 2 等于多少?".to_string(),
347 );
348
349 let result = agent.plan(&steps, &inputs).await.unwrap();
350
351 match result {
352 AgentOutput::Action(action) => {
353 println!("下一步动作: {}({})", action.tool, action.tool_input);
354 assert_eq!(action.tool, "calculator");
355 }
356 AgentOutput::Finish(finish) => {
357 println!("最终答案: {:?}", finish.return_values);
358 }
359 AgentOutput::Actions(_) => {
360 println!("ReActAgent 不支持并行工具调用");
361 }
362 }
363 }
364
365 #[tokio::test]
367 #[ignore = "需要真实 API 调用"]
368 async fn test_real_api_with_memory() {
369 let config = create_test_config();
370 let llm = OpenAIChat::new(config);
371 let tools: Vec<Arc<dyn BaseTool>> = vec![];
372 let agent = ReActAgent::new(llm, tools, None);
373
374 let history = "Human: 我叫张三\nAI: 好的,张三,我记住了。";
376
377 let mut inputs = HashMap::new();
378 inputs.insert("input".to_string(), "我叫什么名字?".to_string());
379 inputs.insert("history".to_string(), history.to_string());
380
381 let result = agent.plan(&[], &inputs).await.unwrap();
382
383 match result {
384 AgentOutput::Finish(finish) => {
385 println!("答案: {:?}", finish.return_values);
386 let output = finish.output().unwrap_or("");
387 assert!(output.contains("张三"), "应该记住用户名字");
388 }
389 AgentOutput::Action(_) => {
390 println!("LLM 尝试调用工具");
391 }
392 AgentOutput::Actions(_) => {
393 println!("ReActAgent 不支持并行工具调用");
394 }
395 }
396 }
397}