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 listed here only in brief to save context. Their full parameter details and examples become visible once they are activated for this session (activation is handled by the app or user, not via a tool call)."
703        }
704        GuideLanguage::English => {
705            "These lower-frequency tools are available but listed here only in brief to save context. Their full parameter details and examples become visible once they are activated for this session (activation is handled by the app or user, not via a tool call)."
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}