Skip to main content

bamboo_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//! ```rust,ignore
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::exposure::{canonical_tool_name, is_core_tool};
32use bamboo_agent_core::ToolSchema;
33use serde::{Deserialize, Serialize};
34
35pub mod builtin_guides;
36pub mod context;
37
38use builtin_guides::builtin_tool_guide;
39use context::{GuideBuildContext, GuideLanguage};
40
41use crate::tools::ToolRegistry;
42
43/// Represents a usage example for a tool
44///
45/// Each example demonstrates a specific scenario with parameters
46/// and an explanation of the expected outcome.
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ToolExample {
49    /// Description of the scenario/use case
50    pub scenario: String,
51
52    /// Example parameters in JSON format
53    pub parameters: serde_json::Value,
54
55    /// Explanation of what this example does and why
56    pub explanation: String,
57}
58
59impl ToolExample {
60    /// Creates a new tool example
61    ///
62    /// # Arguments
63    ///
64    /// * `scenario` - Description of the use case
65    /// * `parameters` - JSON parameters for the example
66    /// * `explanation` - What this example accomplishes
67    pub fn new(
68        scenario: impl Into<String>,
69        parameters: serde_json::Value,
70        explanation: impl Into<String>,
71    ) -> Self {
72        Self {
73            scenario: scenario.into(),
74            parameters,
75            explanation: explanation.into(),
76        }
77    }
78}
79
80/// Categories for organizing tools
81///
82/// Tools are grouped into logical categories to help LLMs understand
83/// their purpose and choose the right tool for each task.
84#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
85pub enum ToolCategory {
86    /// Tools for reading files and understanding code
87    FileReading,
88
89    /// Tools for creating and modifying files
90    FileWriting,
91
92    /// Tools for searching code and text
93    CodeSearch,
94
95    /// Tools for running shell commands
96    CommandExecution,
97
98    /// Tools for Git operations
99    GitOperations,
100
101    /// Tools for managing tasks and workflows
102    TaskManagement,
103
104    /// Tools for interacting with the user
105    UserInteraction,
106}
107
108impl ToolCategory {
109    /// Order for presenting categories in prompts
110    const ORDER: [ToolCategory; 7] = [
111        ToolCategory::FileReading,
112        ToolCategory::FileWriting,
113        ToolCategory::CodeSearch,
114        ToolCategory::CommandExecution,
115        ToolCategory::GitOperations,
116        ToolCategory::TaskManagement,
117        ToolCategory::UserInteraction,
118    ];
119
120    /// Returns categories in their standard presentation order
121    pub fn ordered() -> &'static [ToolCategory] {
122        &Self::ORDER
123    }
124
125    /// Returns the display title for this category in the specified language
126    fn title(self, language: GuideLanguage) -> &'static str {
127        match (self, language) {
128            (ToolCategory::FileReading, GuideLanguage::Chinese) => "File Reading Tools",
129            (ToolCategory::FileWriting, GuideLanguage::Chinese) => "File Writing Tools",
130            (ToolCategory::CodeSearch, GuideLanguage::Chinese) => "Code Search Tools",
131            (ToolCategory::CommandExecution, GuideLanguage::Chinese) => "Command Execution Tools",
132            (ToolCategory::GitOperations, GuideLanguage::Chinese) => "Git Tools",
133            (ToolCategory::TaskManagement, GuideLanguage::Chinese) => "Task Management Tools",
134            (ToolCategory::UserInteraction, GuideLanguage::Chinese) => "User Interaction Tools",
135            (ToolCategory::FileReading, GuideLanguage::English) => "File Reading Tools",
136            (ToolCategory::FileWriting, GuideLanguage::English) => "File Writing Tools",
137            (ToolCategory::CodeSearch, GuideLanguage::English) => "Code Search Tools",
138            (ToolCategory::CommandExecution, GuideLanguage::English) => "Command Tools",
139            (ToolCategory::GitOperations, GuideLanguage::English) => "Git Tools",
140            (ToolCategory::TaskManagement, GuideLanguage::English) => "Task Management Tools",
141            (ToolCategory::UserInteraction, GuideLanguage::English) => "User Interaction Tools",
142        }
143    }
144
145    /// Returns the description for this category in the specified language
146    fn description(self, language: GuideLanguage) -> &'static str {
147        match (self, language) {
148            (ToolCategory::FileReading, GuideLanguage::Chinese) => {
149                "Use these to understand existing files, directory structure, and metadata."
150            }
151            (ToolCategory::FileWriting, GuideLanguage::Chinese) => {
152                "Use these to create files or make content modifications."
153            }
154            (ToolCategory::CodeSearch, GuideLanguage::Chinese) => {
155                "Use these to locate definitions, references, and key text."
156            }
157            (ToolCategory::CommandExecution, GuideLanguage::Chinese) => {
158                "Use these to run commands, confirm or switch working directories."
159            }
160            (ToolCategory::GitOperations, GuideLanguage::Chinese) => {
161                "Use these to view repository status and code differences."
162            }
163            (ToolCategory::TaskManagement, GuideLanguage::Chinese) => {
164                "Use these to break down tasks and track execution progress."
165            }
166            (ToolCategory::UserInteraction, GuideLanguage::Chinese) => {
167                "Use this to confirm uncertain matters with the user."
168            }
169            (ToolCategory::FileReading, GuideLanguage::English) => {
170                "Use these to inspect existing files and structure."
171            }
172            (ToolCategory::FileWriting, GuideLanguage::English) => {
173                "Use these to create files and apply edits."
174            }
175            (ToolCategory::CodeSearch, GuideLanguage::English) => {
176                "Use these to find symbols, references, and patterns."
177            }
178            (ToolCategory::CommandExecution, GuideLanguage::English) => {
179                "Use these for shell commands and workspace context."
180            }
181            (ToolCategory::GitOperations, GuideLanguage::English) => {
182                "Use these to inspect repository status and diffs."
183            }
184            (ToolCategory::TaskManagement, GuideLanguage::English) => {
185                "Use these for planning and progress tracking."
186            }
187            (ToolCategory::UserInteraction, GuideLanguage::English) => {
188                "Use this when user clarification is required."
189            }
190        }
191    }
192}
193
194/// Trait for defining tool usage guides
195///
196/// Implement this trait to provide contextual guidance for tools,
197/// helping LLMs understand when and how to use them effectively.
198///
199/// # Required Methods
200///
201/// - `tool_name`: Unique identifier for the tool
202/// - `when_to_use`: Guidance on appropriate use cases
203/// - `when_not_to_use`: Scenarios where the tool should be avoided
204/// - `examples`: Concrete usage examples
205/// - `related_tools`: Other tools that work well with this one
206/// - `category`: Logical grouping for the tool
207pub trait ToolGuide: Send + Sync {
208    /// Returns the tool's unique name
209    fn tool_name(&self) -> &str;
210
211    /// Returns guidance on when this tool should be used
212    fn when_to_use(&self) -> &str;
213
214    /// Returns guidance on when this tool should NOT be used
215    fn when_not_to_use(&self) -> &str;
216
217    /// Returns usage examples for this tool
218    fn examples(&self) -> Vec<ToolExample>;
219
220    /// Returns names of related tools that complement this one
221    fn related_tools(&self) -> Vec<&str>;
222
223    /// Returns the category this tool belongs to
224    fn category(&self) -> ToolCategory;
225}
226
227/// Serializable specification for a tool guide
228///
229/// This struct can be loaded from JSON or YAML files and implements
230/// the `ToolGuide` trait for use in the prompt building system.
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
232pub struct ToolGuideSpec {
233    /// Unique tool identifier
234    pub tool_name: String,
235
236    /// When to use this tool
237    pub when_to_use: String,
238
239    /// When NOT to use this tool
240    pub when_not_to_use: String,
241
242    /// Usage examples
243    pub examples: Vec<ToolExample>,
244
245    /// Related tool names
246    pub related_tools: Vec<String>,
247
248    /// Tool category
249    pub category: ToolCategory,
250}
251
252impl ToolGuideSpec {
253    /// Creates a spec from a `ToolGuide` implementation
254    ///
255    /// # Arguments
256    ///
257    /// * `guide` - Reference to any type implementing `ToolGuide`
258    pub fn from_guide(guide: &dyn ToolGuide) -> Self {
259        Self {
260            tool_name: guide.tool_name().to_string(),
261            when_to_use: guide.when_to_use().to_string(),
262            when_not_to_use: guide.when_not_to_use().to_string(),
263            examples: guide.examples(),
264            related_tools: guide
265                .related_tools()
266                .into_iter()
267                .map(str::to_string)
268                .collect(),
269            category: guide.category(),
270        }
271    }
272
273    /// Parses a guide spec from a JSON string
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if the JSON is malformed or missing required fields
278    pub fn from_json_str(raw: &str) -> Result<Self, serde_json::Error> {
279        serde_json::from_str(raw)
280    }
281
282    /// Parses a guide spec from a YAML string
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if the YAML is malformed or missing required fields
287    pub fn from_yaml_str(raw: &str) -> Result<Self, serde_yaml::Error> {
288        serde_yaml::from_str(raw)
289    }
290}
291
292impl ToolGuide for ToolGuideSpec {
293    fn tool_name(&self) -> &str {
294        &self.tool_name
295    }
296
297    fn when_to_use(&self) -> &str {
298        &self.when_to_use
299    }
300
301    fn when_not_to_use(&self) -> &str {
302        &self.when_not_to_use
303    }
304
305    fn examples(&self) -> Vec<ToolExample> {
306        self.examples.clone()
307    }
308
309    fn related_tools(&self) -> Vec<&str> {
310        self.related_tools.iter().map(String::as_str).collect()
311    }
312
313    fn category(&self) -> ToolCategory {
314        self.category
315    }
316}
317
318/// Builds enhanced prompt sections with tool usage guidelines
319///
320/// This builder constructs formatted markdown sections containing:
321/// - Categorized tool guides with examples
322/// - Best practices for tool usage
323/// - Schema-only fallback for tools without guides
324///
325/// # Output Format
326///
327/// The generated prompts follow this structure:
328///
329/// ```markdown
330/// ## Tool Usage Guidelines
331///
332/// ### File Reading Tools
333/// Use these to inspect existing files and structure.
334///
335/// **read_file**
336/// - When to use: Read file contents when you need to understand code
337/// - When NOT to use: When you only need to check if a file exists
338/// - Example: {"path": "/src/main.rs"} -> Reads the main source file
339/// - Related tools: list_directory, search_files
340///
341/// ### Best Practices
342/// 1. Always verify file paths before reading
343/// 2. Use appropriate tools for the task
344/// ```
345pub struct EnhancedPromptBuilder;
346
347impl EnhancedPromptBuilder {
348    /// Builds an enhanced prompt section for available tools
349    ///
350    /// This method looks up guides for all provided schemas and generates
351    /// a formatted markdown section with usage guidelines.
352    ///
353    /// # Arguments
354    ///
355    /// * `registry` - Optional tool registry with registered guides
356    /// * `available_schemas` - List of tool schemas to document
357    /// * `context` - Build context (language, max examples, etc.)
358    ///
359    /// # Returns
360    ///
361    /// Formatted markdown string with tool usage guidelines
362    pub fn build(
363        registry: Option<&ToolRegistry>,
364        available_schemas: &[ToolSchema],
365        context: &GuideBuildContext,
366    ) -> String {
367        let mut tool_names: Vec<String> = available_schemas
368            .iter()
369            .map(|schema| schema.function.name.clone())
370            .collect();
371        tool_names.sort();
372        tool_names.dedup();
373
374        Self::build_for_tools(registry, &tool_names, available_schemas, context)
375    }
376
377    /// Builds an enhanced prompt for a specific set of tools
378    ///
379    /// This method allows specifying exactly which tools to include,
380    /// even if they're not in the available schemas.
381    ///
382    /// # Arguments
383    ///
384    /// * `registry` - Optional tool registry with registered guides
385    /// * `tool_names` - Specific tools to document
386    /// * `fallback_schemas` - Schemas for tools without guides
387    /// * `context` - Build context (language, max examples, etc.)
388    ///
389    /// # Returns
390    ///
391    /// Formatted markdown string with tool usage guidelines
392    pub fn build_for_tools(
393        registry: Option<&ToolRegistry>,
394        tool_names: &[String],
395        fallback_schemas: &[ToolSchema],
396        context: &GuideBuildContext,
397    ) -> String {
398        let guides = Self::collect_guides(registry, tool_names);
399
400        // Split into core, activated-discoverable, and inactive-discoverable.
401        let activated = &context.activated_discoverable_tools;
402        let mut core_guides: Vec<ToolGuideSpec> = Vec::new();
403        let mut activated_discoverable: Vec<ToolGuideSpec> = Vec::new();
404        let mut inactive_discoverable: Vec<ToolGuideSpec> = Vec::new();
405
406        for guide in guides {
407            let canonical = canonical_tool_name(&guide.tool_name);
408            if is_core_tool(&canonical) {
409                core_guides.push(guide);
410            } else if activated.contains(&canonical) {
411                activated_discoverable.push(guide);
412            } else {
413                inactive_discoverable.push(guide);
414            }
415        }
416
417        let mut output = String::from("## Tool Usage Guidelines\n");
418        let mut rendered_any = false;
419
420        // Render core tools (always full detail).
421        if !core_guides.is_empty() {
422            rendered_any = true;
423            let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
424            for guide in &core_guides {
425                grouped.entry(guide.category).or_default().push(guide);
426            }
427
428            for guides in grouped.values_mut() {
429                guides.sort_by_key(|g| g.tool_name.clone());
430            }
431
432            for category in ToolCategory::ordered() {
433                let Some(category_guides) = grouped.get(category) else {
434                    continue;
435                };
436
437                output.push_str(&format!("\n### {}\n", category.title(context.language)));
438                output.push_str(category.description(context.language));
439                output.push('\n');
440
441                for guide in category_guides {
442                    Self::render_full_guide(&mut output, guide, context);
443                }
444            }
445        }
446
447        // Render activated discoverable tools with full detail.
448        if !activated_discoverable.is_empty() {
449            rendered_any = true;
450            let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
451            for guide in &activated_discoverable {
452                grouped.entry(guide.category).or_default().push(guide);
453            }
454
455            for guides in grouped.values_mut() {
456                guides.sort_by_key(|g| g.tool_name.clone());
457            }
458
459            output.push_str(&format!(
460                "\n### {}\n",
461                activated_discoverable_title(context.language)
462            ));
463            output.push_str(activated_discoverable_description(context.language));
464            output.push('\n');
465
466            for category in ToolCategory::ordered() {
467                let Some(category_guides) = grouped.get(category) else {
468                    continue;
469                };
470
471                output.push_str(&format!("\n#### {}\n", category.title(context.language)));
472                output.push_str(category.description(context.language));
473                output.push('\n');
474
475                for guide in category_guides {
476                    Self::render_full_guide(&mut output, guide, context);
477                }
478            }
479        }
480
481        // Render inactive discoverable tools with short summaries.
482        if !inactive_discoverable.is_empty() {
483            rendered_any = true;
484            output.push('\n');
485            output.push_str(&Self::render_discoverable_section(
486                &inactive_discoverable,
487                context,
488            ));
489        }
490
491        let guided_names: BTreeSet<String> = core_guides
492            .iter()
493            .chain(activated_discoverable.iter())
494            .chain(inactive_discoverable.iter())
495            .map(|guide| guide.tool_name.clone())
496            .collect();
497        let unguided_schemas: Vec<ToolSchema> = fallback_schemas
498            .iter()
499            .filter(|schema| !guided_names.contains(schema.function.name.as_str()))
500            .cloned()
501            .collect();
502
503        if !unguided_schemas.is_empty() {
504            rendered_any = true;
505            output.push('\n');
506            output.push_str(&Self::render_schema_only_section(
507                &unguided_schemas,
508                context,
509                false,
510            ));
511        }
512
513        if !rendered_any {
514            return Self::render_schema_only_section(fallback_schemas, context, true);
515        }
516
517        if context.include_best_practices {
518            output.push_str(&format!(
519                "\n### {}\n",
520                best_practices_title(context.language)
521            ));
522            for (index, rule) in context.best_practices().iter().enumerate() {
523                output.push_str(&format!("{}. {}\n", index + 1, rule));
524            }
525        }
526
527        output
528    }
529
530    fn render_full_guide(output: &mut String, guide: &ToolGuideSpec, context: &GuideBuildContext) {
531        output.push_str(&format!("\n**{}**\n", guide.tool_name));
532        output.push_str(&format!(
533            "- {}: {}\n",
534            when_to_use_label(context.language),
535            guide.when_to_use
536        ));
537        output.push_str(&format!(
538            "- {}: {}\n",
539            when_not_to_use_label(context.language),
540            guide.when_not_to_use
541        ));
542
543        for example in guide.examples.iter().take(context.max_examples_per_tool) {
544            let params =
545                serde_json::to_string(&example.parameters).unwrap_or_else(|_| "{}".to_string());
546            output.push_str(&format!(
547                "- {}: {}\n  -> {}\n",
548                example_label(context.language),
549                params,
550                example.explanation
551            ));
552        }
553
554        if !guide.related_tools.is_empty() {
555            output.push_str(&format!(
556                "- {}: {}\n",
557                related_tools_label(context.language),
558                guide.related_tools.join(", ")
559            ));
560        }
561    }
562
563    /// Collects guides for the specified tools from registry and built-in guides
564    fn collect_guides(
565        registry: Option<&ToolRegistry>,
566        tool_names: &[String],
567    ) -> Vec<ToolGuideSpec> {
568        let mut seen = BTreeSet::new();
569        let mut guides = Vec::new();
570
571        for raw_name in tool_names {
572            let name = raw_name.trim();
573            if name.is_empty() || !seen.insert(name.to_string()) {
574                continue;
575            }
576
577            let guide = registry
578                .and_then(|registry| registry.get_guide(name))
579                .or_else(|| builtin_tool_guide(name));
580
581            if let Some(guide) = guide {
582                guides.push(ToolGuideSpec::from_guide(guide.as_ref()));
583            }
584        }
585
586        guides.sort_by_key(|g| g.tool_name.clone());
587        guides
588    }
589
590    /// Renders a compact summary for discoverable tools.
591    fn render_discoverable_section(
592        guides: &[ToolGuideSpec],
593        context: &GuideBuildContext,
594    ) -> String {
595        if guides.is_empty() {
596            return String::new();
597        }
598
599        let mut sorted = guides.to_vec();
600        sorted.sort_by_key(|g| g.tool_name.clone());
601
602        let mut output = String::new();
603        output.push_str(&format!(
604            "### {}\n",
605            discoverable_tools_title(context.language)
606        ));
607        output.push_str(discoverable_tools_description(context.language));
608        output.push('\n');
609        for guide in sorted {
610            output.push_str(&format!("- `{}`: {}\n", guide.tool_name, guide.when_to_use));
611        }
612        output
613    }
614
615    /// Renders a section listing tools by schema only (no detailed guides)
616    fn render_schema_only_section(
617        schemas: &[ToolSchema],
618        context: &GuideBuildContext,
619        include_header: bool,
620    ) -> String {
621        if schemas.is_empty() {
622            return String::new();
623        }
624
625        let mut output = String::new();
626        if include_header {
627            output.push_str("## Tool Usage Guidelines\n");
628        }
629
630        output.push_str(&format!("\n### {}\n", schema_only_title(context.language)));
631        output.push_str(schema_only_description(context.language));
632        output.push('\n');
633
634        let mut sorted = schemas.to_vec();
635        sorted.sort_by_key(|s| s.function.name.clone());
636
637        for schema in sorted {
638            output.push_str(&format!(
639                "- `{}`: {}\n",
640                schema.function.name, schema.function.description
641            ));
642        }
643
644        output
645    }
646}
647
648// Helper functions for internationalized labels
649
650/// Returns the "When to use" label in the appropriate language
651fn when_to_use_label(language: GuideLanguage) -> &'static str {
652    match language {
653        GuideLanguage::Chinese => "When to use",
654        GuideLanguage::English => "When to use",
655    }
656}
657
658/// Returns the "When NOT to use" label in the appropriate language
659fn when_not_to_use_label(language: GuideLanguage) -> &'static str {
660    match language {
661        GuideLanguage::Chinese => "When NOT to use",
662        GuideLanguage::English => "When NOT to use",
663    }
664}
665
666/// Returns the "Example" label in the appropriate language
667fn example_label(language: GuideLanguage) -> &'static str {
668    match language {
669        GuideLanguage::Chinese => "Example",
670        GuideLanguage::English => "Example",
671    }
672}
673
674/// Returns the "Related tools" label in the appropriate language
675fn related_tools_label(language: GuideLanguage) -> &'static str {
676    match language {
677        GuideLanguage::Chinese => "Related tools",
678        GuideLanguage::English => "Related tools",
679    }
680}
681
682/// Returns the "Best Practices" title in the appropriate language
683fn best_practices_title(language: GuideLanguage) -> &'static str {
684    match language {
685        GuideLanguage::Chinese => "Best Practices",
686        GuideLanguage::English => "Best Practices",
687    }
688}
689
690/// Returns the discoverable-tools section title in the appropriate language
691fn discoverable_tools_title(language: GuideLanguage) -> &'static str {
692    match language {
693        GuideLanguage::Chinese => "Discoverable Tools",
694        GuideLanguage::English => "Discoverable Tools",
695    }
696}
697
698/// Returns the discoverable-tools section description in the appropriate language
699fn discoverable_tools_description(language: GuideLanguage) -> &'static str {
700    match language {
701        GuideLanguage::Chinese => {
702            "These lower-frequency tools are available but not fully expanded by default to save context. Activate them when needed for full parameter details and examples."
703        }
704        GuideLanguage::English => {
705            "These lower-frequency tools are available but not fully expanded by default to save context. Activate them when needed for full parameter details and examples."
706        }
707    }
708}
709
710/// Returns the activated-discoverable section title in the appropriate language
711fn activated_discoverable_title(language: GuideLanguage) -> &'static str {
712    match language {
713        GuideLanguage::Chinese => "Activated Discoverable Tools",
714        GuideLanguage::English => "Activated Discoverable Tools",
715    }
716}
717
718/// Returns the activated-discoverable section description in the appropriate language
719fn activated_discoverable_description(language: GuideLanguage) -> &'static str {
720    match language {
721        GuideLanguage::Chinese => {
722            "These discoverable tools are currently activated and available with full detail."
723        }
724        GuideLanguage::English => {
725            "These discoverable tools are currently activated and available with full detail."
726        }
727    }
728}
729
730/// Returns the "Additional Tools (Schema Only)" title in the appropriate language
731fn schema_only_title(language: GuideLanguage) -> &'static str {
732    match language {
733        GuideLanguage::Chinese => "Additional Tools (Schema Only)",
734        GuideLanguage::English => "Additional Tools (Schema Only)",
735    }
736}
737
738/// Returns the description for schema-only tools in the appropriate language
739fn schema_only_description(language: GuideLanguage) -> &'static str {
740    match language {
741        GuideLanguage::Chinese => "No detailed guide is available for these tools; rely on schema.",
742        GuideLanguage::English => "No detailed guide is available for these tools; rely on schema.",
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use std::collections::{BTreeMap, BTreeSet};
749
750    use serde_json::json;
751
752    use crate::{
753        tools::{ReadTool, ToolRegistry},
754        BuiltinToolExecutor,
755    };
756    use bamboo_agent_core::{FunctionSchema, ToolExecutor, ToolSchema};
757
758    use super::{
759        context::GuideBuildContext, context::GuideLanguage, EnhancedPromptBuilder, ToolCategory,
760        ToolGuideSpec,
761    };
762
763    fn render_legacy_full_prompt(schemas: &[ToolSchema], context: &GuideBuildContext) -> String {
764        let tool_names: Vec<String> = schemas
765            .iter()
766            .map(|schema| schema.function.name.clone())
767            .collect();
768        let guides = EnhancedPromptBuilder::collect_guides(None, &tool_names);
769
770        if guides.is_empty() {
771            return EnhancedPromptBuilder::render_schema_only_section(schemas, context, true);
772        }
773
774        let mut output = String::from("## Tool Usage Guidelines\n");
775        let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
776
777        for guide in &guides {
778            grouped.entry(guide.category).or_default().push(guide);
779        }
780
781        for guides in grouped.values_mut() {
782            guides.sort_by_key(|g| g.tool_name.clone());
783        }
784
785        for category in ToolCategory::ordered() {
786            let Some(category_guides) = grouped.get(category) else {
787                continue;
788            };
789
790            output.push_str(&format!("\n### {}\n", category.title(context.language)));
791            output.push_str(category.description(context.language));
792            output.push('\n');
793
794            for guide in category_guides {
795                output.push_str(&format!("\n**{}**\n", guide.tool_name));
796                output.push_str(&format!(
797                    "- {}: {}\n",
798                    super::when_to_use_label(context.language),
799                    guide.when_to_use
800                ));
801                output.push_str(&format!(
802                    "- {}: {}\n",
803                    super::when_not_to_use_label(context.language),
804                    guide.when_not_to_use
805                ));
806
807                for example in guide.examples.iter().take(context.max_examples_per_tool) {
808                    let params = serde_json::to_string(&example.parameters)
809                        .unwrap_or_else(|_| "{}".to_string());
810                    output.push_str(&format!(
811                        "- {}: {}\n  -> {}\n",
812                        super::example_label(context.language),
813                        params,
814                        example.explanation
815                    ));
816                }
817
818                if !guide.related_tools.is_empty() {
819                    output.push_str(&format!(
820                        "- {}: {}\n",
821                        super::related_tools_label(context.language),
822                        guide.related_tools.join(", ")
823                    ));
824                }
825            }
826        }
827
828        let guided_names: BTreeSet<&str> = guides
829            .iter()
830            .map(|guide| guide.tool_name.as_str())
831            .collect();
832        let unguided_schemas: Vec<ToolSchema> = schemas
833            .iter()
834            .filter(|schema| !guided_names.contains(schema.function.name.as_str()))
835            .cloned()
836            .collect();
837
838        if !unguided_schemas.is_empty() {
839            output.push('\n');
840            output.push_str(&EnhancedPromptBuilder::render_schema_only_section(
841                &unguided_schemas,
842                context,
843                false,
844            ));
845        }
846
847        if context.include_best_practices {
848            output.push_str(&format!(
849                "\n### {}\n",
850                super::best_practices_title(context.language)
851            ));
852            for (index, rule) in context.best_practices().iter().enumerate() {
853                output.push_str(&format!("{}. {}\n", index + 1, rule));
854            }
855        }
856
857        output
858    }
859
860    #[test]
861    fn build_renders_builtin_guides() {
862        let registry = ToolRegistry::new();
863        registry.register(ReadTool::new()).unwrap();
864
865        let schemas = registry.list_tools();
866        let prompt =
867            EnhancedPromptBuilder::build(Some(&registry), &schemas, &GuideBuildContext::default());
868
869        assert!(prompt.contains("## Tool Usage Guidelines"));
870        assert!(prompt.contains("**Read**"));
871    }
872
873    #[test]
874    fn build_falls_back_to_schema_without_guides() {
875        let schema = ToolSchema {
876            schema_type: "function".to_string(),
877            function: FunctionSchema {
878                name: "dynamic_tool".to_string(),
879                description: "A runtime tool".to_string(),
880                parameters: json!({ "type": "object", "properties": {} }),
881            },
882        };
883        let context = GuideBuildContext {
884            language: GuideLanguage::English,
885            ..GuideBuildContext::default()
886        };
887
888        let prompt = EnhancedPromptBuilder::build(None, &[schema], &context);
889
890        assert!(prompt.contains("Additional Tools (Schema Only)"));
891        assert!(prompt.contains("dynamic_tool"));
892    }
893
894    #[test]
895    fn build_summarizes_discoverable_tools() {
896        let registry = ToolRegistry::new();
897        registry.register(ReadTool::new()).unwrap();
898        registry.register(crate::tools::SleepTool::new()).unwrap();
899
900        let schemas = registry.list_tools();
901        let prompt =
902            EnhancedPromptBuilder::build(Some(&registry), &schemas, &GuideBuildContext::default());
903
904        assert!(prompt.contains("### Discoverable Tools"));
905        assert!(prompt.contains("`Sleep`"));
906        assert!(!prompt.contains("**Sleep**"));
907    }
908
909    #[test]
910    fn build_reduces_prompt_length_vs_legacy_full_guides_for_builtin_surface() {
911        let executor = BuiltinToolExecutor::new();
912        let schemas = executor.list_tools();
913        let context = GuideBuildContext::default();
914
915        let legacy = render_legacy_full_prompt(&schemas, &context);
916        let current = EnhancedPromptBuilder::build(None, &schemas, &context);
917
918        assert!(current.len() < legacy.len());
919        let saved = legacy.len() - current.len();
920        let saved_ratio = saved as f64 / legacy.len() as f64;
921        eprintln!(
922            "guide_length_metrics: legacy={}, current={}, saved={}, saved_ratio={:.3}",
923            legacy.len(),
924            current.len(),
925            saved,
926            saved_ratio,
927        );
928        assert!(
929            saved > 0,
930            "expected prompt savings for summarized discoverable tools"
931        );
932    }
933
934    #[test]
935    fn build_shows_activated_discoverable_tools_with_full_detail() {
936        let registry = ToolRegistry::new();
937        registry.register(crate::tools::SleepTool::new()).unwrap();
938
939        let schemas = registry.list_tools();
940        let mut context = GuideBuildContext::default();
941        context
942            .activated_discoverable_tools
943            .insert("Sleep".to_string());
944
945        let prompt = EnhancedPromptBuilder::build(Some(&registry), &schemas, &context);
946
947        assert!(
948            prompt.contains("### Activated Discoverable Tools"),
949            "activated discoverable section should appear"
950        );
951        assert!(
952            prompt.contains("**Sleep**"),
953            "activated Sleep should show full guide with bold name"
954        );
955        assert!(
956            prompt.contains("When to use"),
957            "activated Sleep should include when_to_use"
958        );
959        assert!(
960            prompt.contains("When NOT to use"),
961            "activated Sleep should include when_not_to_use"
962        );
963        assert!(
964            !prompt.contains("### Discoverable Tools"),
965            "inactive discoverable section should not appear when all discoverable tools are activated"
966        );
967    }
968
969    #[test]
970    fn build_shows_inactive_discoverable_tools_with_short_summary() {
971        let registry = ToolRegistry::new();
972        registry.register(crate::tools::SleepTool::new()).unwrap();
973
974        let schemas = registry.list_tools();
975        let context = GuideBuildContext::default();
976
977        let prompt = EnhancedPromptBuilder::build(Some(&registry), &schemas, &context);
978
979        assert!(
980            prompt.contains("### Discoverable Tools"),
981            "inactive discoverable section should appear"
982        );
983        assert!(
984            prompt.contains("`Sleep`"),
985            "inactive Sleep should show as short summary"
986        );
987        assert!(
988            !prompt.contains("**Sleep**"),
989            "inactive Sleep should NOT show full guide with bold name"
990        );
991        assert!(
992            !prompt.contains("Activated Discoverable Tools"),
993            "activated section should not appear when no discoverable tools are activated"
994        );
995    }
996
997    #[test]
998    fn build_separates_core_and_discoverable_tools_correctly() {
999        let registry = ToolRegistry::new();
1000        registry.register(ReadTool::new()).unwrap();
1001        registry.register(crate::tools::SleepTool::new()).unwrap();
1002
1003        let schemas = registry.list_tools();
1004        let mut context = GuideBuildContext::default();
1005        context
1006            .activated_discoverable_tools
1007            .insert("Sleep".to_string());
1008
1009        let prompt = EnhancedPromptBuilder::build(Some(&registry), &schemas, &context);
1010
1011        // Core tool (Read) should appear in its category section with full detail
1012        assert!(
1013            prompt.contains("### File Reading Tools"),
1014            "core tools should appear in category section"
1015        );
1016        assert!(
1017            prompt.contains("**Read**"),
1018            "core Read should show full guide"
1019        );
1020
1021        // Activated discoverable should appear in activated section
1022        assert!(
1023            prompt.contains("### Activated Discoverable Tools"),
1024            "activated discoverable section should appear"
1025        );
1026        assert!(
1027            prompt.contains("**Sleep**"),
1028            "activated Sleep should show full guide"
1029        );
1030    }
1031}