gemini_adk_rs/planners/
plan_re_act.rs1use async_trait::async_trait;
7
8use super::{Planner, PlannerError};
9use crate::llm::LlmRequest;
10
11const TAG_PLANNING: &str = "/*PLANNING*/";
13const TAG_REPLANNING: &str = "/*REPLANNING*/";
14const TAG_REASONING: &str = "/*REASONING*/";
15const TAG_ACTION: &str = "/*ACTION*/";
16const TAG_FINAL_ANSWER: &str = "/*FINAL_ANSWER*/";
17
18#[derive(Debug, Clone)]
24pub struct PlanReActPlanner {
25 include_tool_instructions: bool,
27}
28
29impl PlanReActPlanner {
30 pub fn new() -> Self {
32 Self {
33 include_tool_instructions: true,
34 }
35 }
36
37 pub fn with_tool_instructions(mut self, include: bool) -> Self {
39 self.include_tool_instructions = include;
40 self
41 }
42}
43
44impl Default for PlanReActPlanner {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50#[async_trait]
51impl Planner for PlanReActPlanner {
52 fn build_planning_instruction(
53 &self,
54 _request: &LlmRequest,
55 ) -> Result<Option<String>, PlannerError> {
56 let mut instruction = format!(
57 r#"For every turn, you must follow the format below and use these exact tags to organize your output:
58
591. {TAG_PLANNING} — Create a natural language plan for how to approach the query. Plans should be:
60 - Coherent and cover all aspects of the query
61 - Decomposed into numbered steps
62 - Aware of available tools and their capabilities
63
642. {TAG_REASONING} — For each step in your plan, explain your reasoning before taking action.
65
663. {TAG_ACTION} — Execute one step at a time using available tools when needed.
67
684. {TAG_REPLANNING} — If new information changes your approach, create an updated plan.
69
705. {TAG_FINAL_ANSWER} — After completing all steps, provide the final answer."#
71 );
72
73 if self.include_tool_instructions {
74 instruction.push_str(
75 "\n\nWhen using tools:\n\
76 - Only use tools that are available to you\n\
77 - Write self-contained tool calls\n\
78 - Prefer using information from previous tool results over making redundant calls",
79 );
80 }
81
82 Ok(Some(instruction))
83 }
84
85 fn process_planning_response(
86 &self,
87 response_text: &str,
88 ) -> Result<Option<String>, PlannerError> {
89 let mut filtered = String::new();
91 let mut in_planning = false;
92 let mut in_reasoning = false;
93
94 for line in response_text.lines() {
95 let trimmed = line.trim();
96
97 if trimmed.contains(TAG_PLANNING) || trimmed.contains(TAG_REPLANNING) {
98 in_planning = true;
99 in_reasoning = false;
100 continue;
101 }
102 if trimmed.contains(TAG_REASONING) {
103 in_reasoning = true;
104 in_planning = false;
105 continue;
106 }
107 if trimmed.contains(TAG_ACTION) || trimmed.contains(TAG_FINAL_ANSWER) {
108 in_planning = false;
109 in_reasoning = false;
110 if !filtered.is_empty() {
112 filtered.push('\n');
113 }
114 filtered.push_str(line);
115 continue;
116 }
117
118 if in_planning || in_reasoning {
119 if !filtered.is_empty() {
121 filtered.push('\n');
122 }
123 filtered.push_str(line);
124 } else {
125 if !filtered.is_empty() {
127 filtered.push('\n');
128 }
129 filtered.push_str(line);
130 }
131 }
132
133 if filtered.trim().is_empty() || filtered == response_text {
134 Ok(None)
135 } else {
136 Ok(Some(filtered))
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn builds_instruction_with_tags() {
147 let planner = PlanReActPlanner::new();
148 let request = LlmRequest::default();
149 let instruction = planner.build_planning_instruction(&request).unwrap();
150 let text = instruction.unwrap();
151 assert!(text.contains(TAG_PLANNING));
152 assert!(text.contains(TAG_REASONING));
153 assert!(text.contains(TAG_ACTION));
154 assert!(text.contains(TAG_FINAL_ANSWER));
155 }
156
157 #[test]
158 fn instruction_includes_tool_guidance() {
159 let planner = PlanReActPlanner::new().with_tool_instructions(true);
160 let request = LlmRequest::default();
161 let text = planner
162 .build_planning_instruction(&request)
163 .unwrap()
164 .unwrap();
165 assert!(text.contains("Only use tools"));
166 }
167
168 #[test]
169 fn instruction_without_tool_guidance() {
170 let planner = PlanReActPlanner::new().with_tool_instructions(false);
171 let request = LlmRequest::default();
172 let text = planner
173 .build_planning_instruction(&request)
174 .unwrap()
175 .unwrap();
176 assert!(!text.contains("Only use tools"));
177 }
178
179 #[test]
180 fn process_passthrough_plain_text() {
181 let planner = PlanReActPlanner::new();
182 let result = planner
183 .process_planning_response("Just a plain response")
184 .unwrap();
185 assert!(result.is_none());
186 }
187
188 #[test]
189 fn process_filters_tagged_response() {
190 let planner = PlanReActPlanner::new();
191 let response = format!(
192 "{TAG_PLANNING}\nStep 1: Search\nStep 2: Summarize\n{TAG_ACTION}\nSearching...\n{TAG_FINAL_ANSWER}\nThe answer is 42."
193 );
194 let result = planner.process_planning_response(&response).unwrap();
195 assert!(result.is_some());
197 }
198}