1use crate::client::{LLMClient, LLMConfig, LLMResponse, UniversalLLMClient};
2use crate::types::Message;
3use ceylon_core::action::ToolInvoker;
4use ceylon_core::agent::AgentContext;
5use ceylon_core::error::Result;
6use ceylon_core::memory::Memory;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use tokio::sync::Mutex;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ReActConfig {
17 pub enabled: bool,
19
20 pub max_iterations: usize,
22
23 pub thought_prefix: String,
25
26 pub action_prefix: String,
28
29 pub observation_prefix: String,
31
32 pub finish_action: String,
34
35 pub stop_sequences: Vec<String>,
37}
38
39impl Default for ReActConfig {
40 fn default() -> Self {
41 Self {
42 enabled: false,
43 max_iterations: 10,
44 thought_prefix: "Thought:".to_string(),
45 action_prefix: "Action:".to_string(),
46 observation_prefix: "Observation:".to_string(),
47 finish_action: "finish".to_string(),
48 stop_sequences: vec!["Observation:".to_string()],
49 }
50 }
51}
52
53impl ReActConfig {
54 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn enabled(mut self) -> Self {
61 self.enabled = true;
62 self
63 }
64
65 pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
67 self.max_iterations = max_iterations;
68 self
69 }
70
71 pub fn with_thought_prefix(mut self, prefix: impl Into<String>) -> Self {
73 self.thought_prefix = prefix.into();
74 self
75 }
76
77 pub fn with_action_prefix(mut self, prefix: impl Into<String>) -> Self {
79 self.action_prefix = prefix.into();
80 self
81 }
82
83 pub fn with_observation_prefix(mut self, prefix: impl Into<String>) -> Self {
85 self.observation_prefix = prefix.into();
86 self
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ReActStep {
93 pub iteration: usize,
95
96 pub thought: String,
98
99 pub action: Option<String>,
101
102 pub action_input: Option<String>,
104
105 pub observation: Option<String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ReActResult {
112 pub answer: String,
114
115 pub steps: Vec<ReActStep>,
117
118 pub iterations: usize,
120
121 pub finish_reason: FinishReason,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127pub enum FinishReason {
128 Success,
130
131 MaxIterations,
133
134 Error(String),
136}
137
138pub struct ReActEngine {
140 config: ReActConfig,
141 tool_invoker: Option<Arc<Mutex<ToolInvoker>>>,
142}
143
144impl ReActEngine {
145 pub fn new(config: ReActConfig, tool_invoker: Option<Arc<Mutex<ToolInvoker>>>) -> Self {
147 Self {
148 config,
149 tool_invoker,
150 }
151 }
152
153 pub async fn execute(
155 &self,
156 query: String,
157 llm_client: &UniversalLLMClient,
158 _llm_config: &LLMConfig,
159 _memory: Option<&Arc<dyn Memory>>,
160 ctx: &mut AgentContext,
161 ) -> Result<ReActResult> {
162 let mut steps = Vec::new();
163 let mut conversation_history = self.build_initial_messages(&query);
164
165 for iteration in 1..=self.config.max_iterations {
166 let prompt = self.build_iteration_prompt(&query, &steps);
168
169 conversation_history.push(Message {
171 role: "user".to_string(),
172 content: prompt,
173 });
174
175 let response: LLMResponse<String> = llm_client
177 .complete::<LLMResponse<String>, String>(&conversation_history, &[])
178 .await
179 .map_err(|e| ceylon_core::error::Error::MeshError(e))?;
180
181 let step = self.parse_step(iteration, &response.content)?;
183
184 if let Some(ref action_name) = step.action {
186 if action_name == &self.config.finish_action {
187 let answer = step
189 .action_input
190 .clone()
191 .unwrap_or_else(|| step.thought.clone());
192
193 steps.push(step);
194
195 return Ok(ReActResult {
196 answer,
197 steps,
198 iterations: iteration,
199 finish_reason: FinishReason::Success,
200 });
201 }
202 }
203
204 let mut step_with_observation = step.clone();
206
207 if let (Some(ref action_name), Some(ref action_input)) =
208 (&step.action, &step.action_input)
209 {
210 let observation = self.execute_action(action_name, action_input, ctx).await?;
211 step_with_observation.observation = Some(observation.clone());
212
213 conversation_history.push(Message {
215 role: "assistant".to_string(),
216 content: response.content.clone(),
217 });
218
219 conversation_history.push(Message {
220 role: "user".to_string(),
221 content: format!("{} {}", self.config.observation_prefix, observation),
222 });
223 } else {
224 conversation_history.push(Message {
226 role: "assistant".to_string(),
227 content: response.content,
228 });
229 }
230
231 steps.push(step_with_observation);
232 }
233
234 let last_thought = steps.last().map(|s| s.thought.clone()).unwrap_or_default();
236
237 Ok(ReActResult {
238 answer: last_thought,
239 steps,
240 iterations: self.config.max_iterations,
241 finish_reason: FinishReason::MaxIterations,
242 })
243 }
244
245 fn build_initial_messages(&self, _query: &str) -> Vec<Message> {
247 let system_prompt = self.build_react_system_prompt();
248
249 vec![Message {
250 role: "system".to_string(),
251 content: system_prompt,
252 }]
253 }
254
255 fn build_react_system_prompt(&self) -> String {
257 format!(
258 "You are a ReAct (Reason + Act) agent. You solve problems by alternating between reasoning and taking actions.\n\n\
259 On each turn, you must:\n\
260 1. Output your reasoning prefixed with '{}'\n\
261 2. Decide on an action prefixed with '{}'\n\
262 3. Wait for an observation prefixed with '{}'\n\n\
263 Format:\n\
264 {} [your reasoning about the problem]\n\
265 {} action_name[action_input]\n\n\
266 Example:\n\
267 {} I need to find information about X\n\
268 {} search[X]\n\n\
269 When you have the final answer, use:\n\
270 {} {}[your final answer]\n\n\
271 Available actions will be provided with each query.",
272 self.config.thought_prefix,
273 self.config.action_prefix,
274 self.config.observation_prefix,
275 self.config.thought_prefix,
276 self.config.action_prefix,
277 self.config.thought_prefix,
278 self.config.action_prefix,
279 self.config.action_prefix,
280 self.config.finish_action
281 )
282 }
283
284 fn build_iteration_prompt(&self, query: &str, steps: &[ReActStep]) -> String {
286 if steps.is_empty() {
287 format!("Question: {}\n\nBegin reasoning:", query)
289 } else {
290 "Continue reasoning:".to_string()
292 }
293 }
294
295 fn parse_step(&self, iteration: usize, output: &str) -> Result<ReActStep> {
297 let mut thought = String::new();
298 let mut action: Option<String> = None;
299 let mut action_input: Option<String> = None;
300
301 for line in output.lines() {
303 let line = line.trim();
304
305 if line.starts_with(&self.config.thought_prefix) {
306 thought = line
307 .strip_prefix(&self.config.thought_prefix)
308 .unwrap_or("")
309 .trim()
310 .to_string();
311 } else if line.starts_with(&self.config.action_prefix) {
312 let action_str = line
314 .strip_prefix(&self.config.action_prefix)
315 .unwrap_or("")
316 .trim();
317
318 if let Some(bracket_pos) = action_str.find('[') {
319 let name = action_str[..bracket_pos].trim().to_string();
320 let input = action_str[bracket_pos + 1..]
321 .trim_end_matches(']')
322 .trim()
323 .to_string();
324
325 action = Some(name);
326 action_input = Some(input);
327 } else {
328 action = Some(action_str.to_string());
330 }
331 }
332 }
333
334 Ok(ReActStep {
335 iteration,
336 thought,
337 action,
338 action_input,
339 observation: None,
340 })
341 }
342
343 async fn execute_action(
345 &self,
346 action_name: &str,
347 action_input: &str,
348 ctx: &mut AgentContext,
349 ) -> Result<String> {
350 if let Some(ref tool_invoker) = self.tool_invoker {
351 let invoker = tool_invoker.lock().await;
352
353 let input_value: serde_json::Value = serde_json::from_str(action_input)
355 .unwrap_or_else(|_| serde_json::json!({"input": action_input}));
356
357 let result = invoker.invoke(action_name, ctx, input_value).await?;
358
359 Ok(result.to_string())
360 } else {
361 Ok(format!(
362 "No tool invoker available to execute action: {}",
363 action_name
364 ))
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_react_config_default() {
375 let config = ReActConfig::default();
376 assert_eq!(config.enabled, false);
377 assert_eq!(config.max_iterations, 10);
378 assert_eq!(config.thought_prefix, "Thought:");
379 assert_eq!(config.action_prefix, "Action:");
380 }
381
382 #[test]
383 fn test_react_config_builder() {
384 let config = ReActConfig::new()
385 .enabled()
386 .with_max_iterations(15)
387 .with_thought_prefix("Think:");
388
389 assert_eq!(config.enabled, true);
390 assert_eq!(config.max_iterations, 15);
391 assert_eq!(config.thought_prefix, "Think:");
392 }
393
394 #[test]
395 fn test_parse_step() {
396 let config = ReActConfig::default();
397 let engine = ReActEngine::new(config, None);
398
399 let output = "Thought: I need to search for information\nAction: search[test query]";
400 let step = engine.parse_step(1, output).unwrap();
401
402 assert_eq!(step.iteration, 1);
403 assert_eq!(step.thought, "I need to search for information");
404 assert_eq!(step.action, Some("search".to_string()));
405 assert_eq!(step.action_input, Some("test query".to_string()));
406 }
407}