Skip to main content

bamboo_agent/agent/tools/guide/
mod.rs

1//! Tool guide system for enhanced LLM prompts
2//!
3//! This module provides a comprehensive system for generating enhanced tool usage
4//! guidelines that help LLMs understand when and how to use different tools effectively.
5//!
6//! # Components
7//!
8//! - `ToolGuide` trait: Interface for defining tool usage guides
9//! - `ToolGuideSpec`: Serializable guide specification
10//! - `ToolExample`: Usage examples for tools
11//! - `ToolCategory`: Categorization system for tools
12//! - `EnhancedPromptBuilder`: Generates formatted prompt sections
13//!
14//! # Example
15//!
16//! ```no_run
17//! use bamboo_agent::tools::guide::context::GuideBuildContext;
18//! use bamboo_agent::tools::guide::EnhancedPromptBuilder;
19//! use bamboo_agent::tools::ToolRegistry;
20//!
21//! let registry = ToolRegistry::new();
22//! let schemas = registry.list_tools();
23//! let context = GuideBuildContext::default();
24//!
25//! let prompt = EnhancedPromptBuilder::build(Some(&registry), &schemas, &context);
26//! println!("{}", prompt);
27//! ```
28
29use std::collections::{BTreeMap, BTreeSet};
30
31use crate::agent::core::tools::ToolSchema;
32use serde::{Deserialize, Serialize};
33
34pub mod builtin_guides;
35pub mod context;
36
37use builtin_guides::builtin_tool_guide;
38use context::{GuideBuildContext, GuideLanguage};
39
40use crate::agent::tools::tools::ToolRegistry;
41
42/// Represents a usage example for a tool
43///
44/// Each example demonstrates a specific scenario with parameters
45/// and an explanation of the expected outcome.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct ToolExample {
48    /// Description of the scenario/use case
49    pub scenario: String,
50
51    /// Example parameters in JSON format
52    pub parameters: serde_json::Value,
53
54    /// Explanation of what this example does and why
55    pub explanation: String,
56}
57
58impl ToolExample {
59    /// Creates a new tool example
60    ///
61    /// # Arguments
62    ///
63    /// * `scenario` - Description of the use case
64    /// * `parameters` - JSON parameters for the example
65    /// * `explanation` - What this example accomplishes
66    pub fn new(
67        scenario: impl Into<String>,
68        parameters: serde_json::Value,
69        explanation: impl Into<String>,
70    ) -> Self {
71        Self {
72            scenario: scenario.into(),
73            parameters,
74            explanation: explanation.into(),
75        }
76    }
77}
78
79/// Categories for organizing tools
80///
81/// Tools are grouped into logical categories to help LLMs understand
82/// their purpose and choose the right tool for each task.
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
84pub enum ToolCategory {
85    /// Tools for reading files and understanding code
86    FileReading,
87
88    /// Tools for creating and modifying files
89    FileWriting,
90
91    /// Tools for searching code and text
92    CodeSearch,
93
94    /// Tools for running shell commands
95    CommandExecution,
96
97    /// Tools for Git operations
98    GitOperations,
99
100    /// Tools for managing tasks and workflows
101    TaskManagement,
102
103    /// Tools for interacting with the user
104    UserInteraction,
105}
106
107impl ToolCategory {
108    /// Order for presenting categories in prompts
109    const ORDER: [ToolCategory; 7] = [
110        ToolCategory::FileReading,
111        ToolCategory::FileWriting,
112        ToolCategory::CodeSearch,
113        ToolCategory::CommandExecution,
114        ToolCategory::GitOperations,
115        ToolCategory::TaskManagement,
116        ToolCategory::UserInteraction,
117    ];
118
119    /// Returns categories in their standard presentation order
120    pub fn ordered() -> &'static [ToolCategory] {
121        &Self::ORDER
122    }
123
124    /// Returns the display title for this category in the specified language
125    fn title(self, language: GuideLanguage) -> &'static str {
126        match (self, language) {
127            (ToolCategory::FileReading, GuideLanguage::Chinese) => "File Reading Tools",
128            (ToolCategory::FileWriting, GuideLanguage::Chinese) => "File Writing Tools",
129            (ToolCategory::CodeSearch, GuideLanguage::Chinese) => "Code Search Tools",
130            (ToolCategory::CommandExecution, GuideLanguage::Chinese) => "Command Execution Tools",
131            (ToolCategory::GitOperations, GuideLanguage::Chinese) => "Git Tools",
132            (ToolCategory::TaskManagement, GuideLanguage::Chinese) => "Task Management Tools",
133            (ToolCategory::UserInteraction, GuideLanguage::Chinese) => "User Interaction Tools",
134            (ToolCategory::FileReading, GuideLanguage::English) => "File Reading Tools",
135            (ToolCategory::FileWriting, GuideLanguage::English) => "File Writing Tools",
136            (ToolCategory::CodeSearch, GuideLanguage::English) => "Code Search Tools",
137            (ToolCategory::CommandExecution, GuideLanguage::English) => "Command Tools",
138            (ToolCategory::GitOperations, GuideLanguage::English) => "Git Tools",
139            (ToolCategory::TaskManagement, GuideLanguage::English) => "Task Management Tools",
140            (ToolCategory::UserInteraction, GuideLanguage::English) => "User Interaction Tools",
141        }
142    }
143
144    /// Returns the description for this category in the specified language
145    fn description(self, language: GuideLanguage) -> &'static str {
146        match (self, language) {
147            (ToolCategory::FileReading, GuideLanguage::Chinese) => {
148                "Use these to understand existing files, directory structure, and metadata."
149            }
150            (ToolCategory::FileWriting, GuideLanguage::Chinese) => {
151                "Use these to create files or make content modifications."
152            }
153            (ToolCategory::CodeSearch, GuideLanguage::Chinese) => {
154                "Use these to locate definitions, references, and key text."
155            }
156            (ToolCategory::CommandExecution, GuideLanguage::Chinese) => {
157                "Use these to run commands, confirm or switch working directories."
158            }
159            (ToolCategory::GitOperations, GuideLanguage::Chinese) => {
160                "Use these to view repository status and code differences."
161            }
162            (ToolCategory::TaskManagement, GuideLanguage::Chinese) => {
163                "Use these to break down tasks and track execution progress."
164            }
165            (ToolCategory::UserInteraction, GuideLanguage::Chinese) => {
166                "Use this to confirm uncertain matters with the user."
167            }
168            (ToolCategory::FileReading, GuideLanguage::English) => {
169                "Use these to inspect existing files and structure."
170            }
171            (ToolCategory::FileWriting, GuideLanguage::English) => {
172                "Use these to create files and apply edits."
173            }
174            (ToolCategory::CodeSearch, GuideLanguage::English) => {
175                "Use these to find symbols, references, and patterns."
176            }
177            (ToolCategory::CommandExecution, GuideLanguage::English) => {
178                "Use these for shell commands and workspace context."
179            }
180            (ToolCategory::GitOperations, GuideLanguage::English) => {
181                "Use these to inspect repository status and diffs."
182            }
183            (ToolCategory::TaskManagement, GuideLanguage::English) => {
184                "Use these for planning and progress tracking."
185            }
186            (ToolCategory::UserInteraction, GuideLanguage::English) => {
187                "Use this when user clarification is required."
188            }
189        }
190    }
191}
192
193/// Trait for defining tool usage guides
194///
195/// Implement this trait to provide contextual guidance for tools,
196/// helping LLMs understand when and how to use them effectively.
197///
198/// # Required Methods
199///
200/// - `tool_name`: Unique identifier for the tool
201/// - `when_to_use`: Guidance on appropriate use cases
202/// - `when_not_to_use`: Scenarios where the tool should be avoided
203/// - `examples`: Concrete usage examples
204/// - `related_tools`: Other tools that work well with this one
205/// - `category`: Logical grouping for the tool
206pub trait ToolGuide: Send + Sync {
207    /// Returns the tool's unique name
208    fn tool_name(&self) -> &str;
209
210    /// Returns guidance on when this tool should be used
211    fn when_to_use(&self) -> &str;
212
213    /// Returns guidance on when this tool should NOT be used
214    fn when_not_to_use(&self) -> &str;
215
216    /// Returns usage examples for this tool
217    fn examples(&self) -> Vec<ToolExample>;
218
219    /// Returns names of related tools that complement this one
220    fn related_tools(&self) -> Vec<&str>;
221
222    /// Returns the category this tool belongs to
223    fn category(&self) -> ToolCategory;
224}
225
226/// Serializable specification for a tool guide
227///
228/// This struct can be loaded from JSON or YAML files and implements
229/// the `ToolGuide` trait for use in the prompt building system.
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
231pub struct ToolGuideSpec {
232    /// Unique tool identifier
233    pub tool_name: String,
234
235    /// When to use this tool
236    pub when_to_use: String,
237
238    /// When NOT to use this tool
239    pub when_not_to_use: String,
240
241    /// Usage examples
242    pub examples: Vec<ToolExample>,
243
244    /// Related tool names
245    pub related_tools: Vec<String>,
246
247    /// Tool category
248    pub category: ToolCategory,
249}
250
251impl ToolGuideSpec {
252    /// Creates a spec from a `ToolGuide` implementation
253    ///
254    /// # Arguments
255    ///
256    /// * `guide` - Reference to any type implementing `ToolGuide`
257    pub fn from_guide(guide: &dyn ToolGuide) -> Self {
258        Self {
259            tool_name: guide.tool_name().to_string(),
260            when_to_use: guide.when_to_use().to_string(),
261            when_not_to_use: guide.when_not_to_use().to_string(),
262            examples: guide.examples(),
263            related_tools: guide
264                .related_tools()
265                .into_iter()
266                .map(str::to_string)
267                .collect(),
268            category: guide.category(),
269        }
270    }
271
272    /// Parses a guide spec from a JSON string
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the JSON is malformed or missing required fields
277    pub fn from_json_str(raw: &str) -> Result<Self, serde_json::Error> {
278        serde_json::from_str(raw)
279    }
280
281    /// Parses a guide spec from a YAML string
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if the YAML is malformed or missing required fields
286    pub fn from_yaml_str(raw: &str) -> Result<Self, serde_yaml::Error> {
287        serde_yaml::from_str(raw)
288    }
289}
290
291impl ToolGuide for ToolGuideSpec {
292    fn tool_name(&self) -> &str {
293        &self.tool_name
294    }
295
296    fn when_to_use(&self) -> &str {
297        &self.when_to_use
298    }
299
300    fn when_not_to_use(&self) -> &str {
301        &self.when_not_to_use
302    }
303
304    fn examples(&self) -> Vec<ToolExample> {
305        self.examples.clone()
306    }
307
308    fn related_tools(&self) -> Vec<&str> {
309        self.related_tools.iter().map(String::as_str).collect()
310    }
311
312    fn category(&self) -> ToolCategory {
313        self.category
314    }
315}
316
317/// Builds enhanced prompt sections with tool usage guidelines
318///
319/// This builder constructs formatted markdown sections containing:
320/// - Categorized tool guides with examples
321/// - Best practices for tool usage
322/// - Schema-only fallback for tools without guides
323///
324/// # Output Format
325///
326/// The generated prompts follow this structure:
327///
328/// ```markdown
329/// ## Tool Usage Guidelines
330///
331/// ### File Reading Tools
332/// Use these to inspect existing files and structure.
333///
334/// **read_file**
335/// - When to use: Read file contents when you need to understand code
336/// - When NOT to use: When you only need to check if a file exists
337/// - Example: {"path": "/src/main.rs"} -> Reads the main source file
338/// - Related tools: list_directory, search_files
339///
340/// ### Best Practices
341/// 1. Always verify file paths before reading
342/// 2. Use appropriate tools for the task
343/// ```
344pub struct EnhancedPromptBuilder;
345
346impl EnhancedPromptBuilder {
347    /// Builds an enhanced prompt section for available tools
348    ///
349    /// This method looks up guides for all provided schemas and generates
350    /// a formatted markdown section with usage guidelines.
351    ///
352    /// # Arguments
353    ///
354    /// * `registry` - Optional tool registry with registered guides
355    /// * `available_schemas` - List of tool schemas to document
356    /// * `context` - Build context (language, max examples, etc.)
357    ///
358    /// # Returns
359    ///
360    /// Formatted markdown string with tool usage guidelines
361    pub fn build(
362        registry: Option<&ToolRegistry>,
363        available_schemas: &[ToolSchema],
364        context: &GuideBuildContext,
365    ) -> String {
366        let mut tool_names: Vec<String> = available_schemas
367            .iter()
368            .map(|schema| schema.function.name.clone())
369            .collect();
370        tool_names.sort();
371        tool_names.dedup();
372
373        Self::build_for_tools(registry, &tool_names, available_schemas, context)
374    }
375
376    /// Builds an enhanced prompt for a specific set of tools
377    ///
378    /// This method allows specifying exactly which tools to include,
379    /// even if they're not in the available schemas.
380    ///
381    /// # Arguments
382    ///
383    /// * `registry` - Optional tool registry with registered guides
384    /// * `tool_names` - Specific tools to document
385    /// * `fallback_schemas` - Schemas for tools without guides
386    /// * `context` - Build context (language, max examples, etc.)
387    ///
388    /// # Returns
389    ///
390    /// Formatted markdown string with tool usage guidelines
391    pub fn build_for_tools(
392        registry: Option<&ToolRegistry>,
393        tool_names: &[String],
394        fallback_schemas: &[ToolSchema],
395        context: &GuideBuildContext,
396    ) -> String {
397        let guides = Self::collect_guides(registry, tool_names);
398
399        if guides.is_empty() {
400            return Self::render_schema_only_section(fallback_schemas, context, true);
401        }
402
403        let mut output = String::from("## Tool Usage Guidelines\n");
404        let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
405
406        for guide in &guides {
407            grouped.entry(guide.category).or_default().push(guide);
408        }
409
410        for guides in grouped.values_mut() {
411            guides.sort_by(|left, right| left.tool_name.cmp(&right.tool_name));
412        }
413
414        for category in ToolCategory::ordered() {
415            let Some(category_guides) = grouped.get(category) else {
416                continue;
417            };
418
419            output.push_str(&format!("\n### {}\n", category.title(context.language)));
420            output.push_str(category.description(context.language));
421            output.push('\n');
422
423            for guide in category_guides {
424                output.push_str(&format!("\n**{}**\n", guide.tool_name));
425                output.push_str(&format!(
426                    "- {}: {}\n",
427                    when_to_use_label(context.language),
428                    guide.when_to_use
429                ));
430                output.push_str(&format!(
431                    "- {}: {}\n",
432                    when_not_to_use_label(context.language),
433                    guide.when_not_to_use
434                ));
435
436                for example in guide.examples.iter().take(context.max_examples_per_tool) {
437                    let params = serde_json::to_string(&example.parameters)
438                        .unwrap_or_else(|_| "{}".to_string());
439                    output.push_str(&format!(
440                        "- {}: {}\n  -> {}\n",
441                        example_label(context.language),
442                        params,
443                        example.explanation
444                    ));
445                }
446
447                if !guide.related_tools.is_empty() {
448                    output.push_str(&format!(
449                        "- {}: {}\n",
450                        related_tools_label(context.language),
451                        guide.related_tools.join(", ")
452                    ));
453                }
454            }
455        }
456
457        let guided_names: BTreeSet<&str> = guides
458            .iter()
459            .map(|guide| guide.tool_name.as_str())
460            .collect();
461        let unguided_schemas: Vec<ToolSchema> = fallback_schemas
462            .iter()
463            .filter(|schema| !guided_names.contains(schema.function.name.as_str()))
464            .cloned()
465            .collect();
466
467        if !unguided_schemas.is_empty() {
468            output.push('\n');
469            output.push_str(&Self::render_schema_only_section(
470                &unguided_schemas,
471                context,
472                false,
473            ));
474        }
475
476        if context.include_best_practices {
477            output.push_str(&format!(
478                "\n### {}\n",
479                best_practices_title(context.language)
480            ));
481            for (index, rule) in context.best_practices().iter().enumerate() {
482                output.push_str(&format!("{}. {}\n", index + 1, rule));
483            }
484        }
485
486        output
487    }
488
489    /// Collects guides for the specified tools from registry and built-in guides
490    fn collect_guides(
491        registry: Option<&ToolRegistry>,
492        tool_names: &[String],
493    ) -> Vec<ToolGuideSpec> {
494        let mut seen = BTreeSet::new();
495        let mut guides = Vec::new();
496
497        for raw_name in tool_names {
498            let name = raw_name.trim();
499            if name.is_empty() || !seen.insert(name.to_string()) {
500                continue;
501            }
502
503            let guide = registry
504                .and_then(|registry| registry.get_guide(name))
505                .or_else(|| builtin_tool_guide(name));
506
507            if let Some(guide) = guide {
508                guides.push(ToolGuideSpec::from_guide(guide.as_ref()));
509            }
510        }
511
512        guides.sort_by(|left, right| left.tool_name.cmp(&right.tool_name));
513        guides
514    }
515
516    /// Renders a section listing tools by schema only (no detailed guides)
517    fn render_schema_only_section(
518        schemas: &[ToolSchema],
519        context: &GuideBuildContext,
520        include_header: bool,
521    ) -> String {
522        if schemas.is_empty() {
523            return String::new();
524        }
525
526        let mut output = String::new();
527        if include_header {
528            output.push_str("## Tool Usage Guidelines\n");
529        }
530
531        output.push_str(&format!("\n### {}\n", schema_only_title(context.language)));
532        output.push_str(schema_only_description(context.language));
533        output.push('\n');
534
535        let mut sorted = schemas.to_vec();
536        sorted.sort_by(|left, right| left.function.name.cmp(&right.function.name));
537
538        for schema in sorted {
539            output.push_str(&format!(
540                "- `{}`: {}\n",
541                schema.function.name, schema.function.description
542            ));
543        }
544
545        output
546    }
547}
548
549// Helper functions for internationalized labels
550
551/// Returns the "When to use" label in the appropriate language
552fn when_to_use_label(language: GuideLanguage) -> &'static str {
553    match language {
554        GuideLanguage::Chinese => "When to use",
555        GuideLanguage::English => "When to use",
556    }
557}
558
559/// Returns the "When NOT to use" label in the appropriate language
560fn when_not_to_use_label(language: GuideLanguage) -> &'static str {
561    match language {
562        GuideLanguage::Chinese => "When NOT to use",
563        GuideLanguage::English => "When NOT to use",
564    }
565}
566
567/// Returns the "Example" label in the appropriate language
568fn example_label(language: GuideLanguage) -> &'static str {
569    match language {
570        GuideLanguage::Chinese => "Example",
571        GuideLanguage::English => "Example",
572    }
573}
574
575/// Returns the "Related tools" label in the appropriate language
576fn related_tools_label(language: GuideLanguage) -> &'static str {
577    match language {
578        GuideLanguage::Chinese => "Related tools",
579        GuideLanguage::English => "Related tools",
580    }
581}
582
583/// Returns the "Best Practices" title in the appropriate language
584fn best_practices_title(language: GuideLanguage) -> &'static str {
585    match language {
586        GuideLanguage::Chinese => "Best Practices",
587        GuideLanguage::English => "Best Practices",
588    }
589}
590
591/// Returns the "Additional Tools (Schema Only)" title in the appropriate language
592fn schema_only_title(language: GuideLanguage) -> &'static str {
593    match language {
594        GuideLanguage::Chinese => "Additional Tools (Schema Only)",
595        GuideLanguage::English => "Additional Tools (Schema Only)",
596    }
597}
598
599/// Returns the description for schema-only tools in the appropriate language
600fn schema_only_description(language: GuideLanguage) -> &'static str {
601    match language {
602        GuideLanguage::Chinese => "No detailed guide is available for these tools; rely on schema.",
603        GuideLanguage::English => "No detailed guide is available for these tools; rely on schema.",
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use serde_json::json;
610
611    use crate::agent::core::tools::{FunctionSchema, ToolSchema};
612
613    use crate::agent::tools::tools::{ReadFileTool, ToolRegistry};
614
615    use super::{context::GuideBuildContext, context::GuideLanguage, EnhancedPromptBuilder};
616
617    #[test]
618    fn build_renders_builtin_guides() {
619        let registry = ToolRegistry::new();
620        registry.register(ReadFileTool::new()).unwrap();
621
622        let schemas = registry.list_tools();
623        let prompt =
624            EnhancedPromptBuilder::build(Some(&registry), &schemas, &GuideBuildContext::default());
625
626        assert!(prompt.contains("## Tool Usage Guidelines"));
627        assert!(prompt.contains("**read_file**"));
628    }
629
630    #[test]
631    fn build_falls_back_to_schema_without_guides() {
632        let schema = ToolSchema {
633            schema_type: "function".to_string(),
634            function: FunctionSchema {
635                name: "dynamic_tool".to_string(),
636                description: "A runtime tool".to_string(),
637                parameters: json!({ "type": "object", "properties": {} }),
638            },
639        };
640        let context = GuideBuildContext {
641            language: GuideLanguage::English,
642            ..GuideBuildContext::default()
643        };
644
645        let prompt = EnhancedPromptBuilder::build(None, &[schema], &context);
646
647        assert!(prompt.contains("Additional Tools (Schema Only)"));
648        assert!(prompt.contains("dynamic_tool"));
649    }
650}