1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct ToolExample {
48 pub scenario: String,
50
51 pub parameters: serde_json::Value,
53
54 pub explanation: String,
56}
57
58impl ToolExample {
59 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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
84pub enum ToolCategory {
85 FileReading,
87
88 FileWriting,
90
91 CodeSearch,
93
94 CommandExecution,
96
97 GitOperations,
99
100 TaskManagement,
102
103 UserInteraction,
105}
106
107impl ToolCategory {
108 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 pub fn ordered() -> &'static [ToolCategory] {
121 &Self::ORDER
122 }
123
124 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 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
193pub trait ToolGuide: Send + Sync {
207 fn tool_name(&self) -> &str;
209
210 fn when_to_use(&self) -> &str;
212
213 fn when_not_to_use(&self) -> &str;
215
216 fn examples(&self) -> Vec<ToolExample>;
218
219 fn related_tools(&self) -> Vec<&str>;
221
222 fn category(&self) -> ToolCategory;
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
231pub struct ToolGuideSpec {
232 pub tool_name: String,
234
235 pub when_to_use: String,
237
238 pub when_not_to_use: String,
240
241 pub examples: Vec<ToolExample>,
243
244 pub related_tools: Vec<String>,
246
247 pub category: ToolCategory,
249}
250
251impl ToolGuideSpec {
252 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 pub fn from_json_str(raw: &str) -> Result<Self, serde_json::Error> {
278 serde_json::from_str(raw)
279 }
280
281 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
317pub struct EnhancedPromptBuilder;
345
346impl EnhancedPromptBuilder {
347 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 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 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 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
549fn 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
559fn 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
567fn example_label(language: GuideLanguage) -> &'static str {
569 match language {
570 GuideLanguage::Chinese => "Example",
571 GuideLanguage::English => "Example",
572 }
573}
574
575fn related_tools_label(language: GuideLanguage) -> &'static str {
577 match language {
578 GuideLanguage::Chinese => "Related tools",
579 GuideLanguage::English => "Related tools",
580 }
581}
582
583fn best_practices_title(language: GuideLanguage) -> &'static str {
585 match language {
586 GuideLanguage::Chinese => "Best Practices",
587 GuideLanguage::English => "Best Practices",
588 }
589}
590
591fn 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
599fn 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(®istry), &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}