1use kernel::{AxCode, AxError};
12
13use super::{Catalog, Expansion, dormant};
14use crate::tools::CallTool;
15
16pub(crate) const DESCRIBE_HITS: usize = 8;
18
19const NAME_WEIGHT: u32 = 3;
22
23impl Catalog {
24 pub fn describe(&self, asked: &str) -> Result<String, AxError> {
37 let asked = asked.trim();
38 if let Some(def) = self.tools.get(asked) {
39 let schema = serde_json::to_string(&def.input_schema).map_err(|err| {
40 AxError::failure(AxCode::InvalidArgs, "describe", asked.to_owned()).with_recovery(
41 format!(
42 "report this against runtime::catalog: a registered schema no \
43 longer prints as JSON ({err})"
44 ),
45 )
46 })?;
47 let how = if self.is_core(asked) {
48 "it is in your tool list; call it directly".to_owned()
49 } else {
50 format!(
51 "run it with `{}` and the arguments {{\"name\": \"{asked}\", \"args\": \
52 {{...}}}}",
53 CallTool::NAME
54 )
55 };
56 return Ok(format!(
57 "tool {asked}: {how}.\n{}\ninput schema: {schema}\n",
58 def.description
59 ));
60 }
61 let skill = asked.strip_prefix("skill ").unwrap_or(asked);
63 if let Some(admitted) = self.skills.get(skill) {
64 return Ok(format!(
65 "skill {skill}: {}\nOpen its whole text with `read` and the path `{skill}`.\n",
66 admitted.entry.disclosure
67 ));
68 }
69 if let Some(Expansion::Said { text }) = self.expand(asked) {
70 return Ok(text);
71 }
72 Ok(self.closest(asked))
73 }
74
75 fn closest(&self, asked: &str) -> String {
78 let words: Vec<String> = asked
79 .split(|c: char| !c.is_alphanumeric())
80 .filter(|word| !word.is_empty())
81 .map(str::to_lowercase)
82 .collect();
83 let mut ranked: Vec<(u32, String, &str)> = self
84 .tools
85 .iter()
86 .map(|(name, def)| (name.clone(), def.description.as_str()))
87 .chain(self.skills.iter().map(|(name, admitted)| {
88 (format!("skill {name}"), admitted.entry.disclosure.as_str())
89 }))
90 .map(|(label, about)| (score(&words, &label, about), label, about))
91 .filter(|(score, _, _)| *score > 0)
92 .collect();
93 ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
94 if ranked.is_empty() {
95 return format!(
96 "Nothing admitted here matches `{asked}`. The dormant index in your prompt \
97 names every capability this building admits beyond your tool list.\n"
98 );
99 }
100 let mut out: String = ranked
101 .iter()
102 .take(DESCRIBE_HITS)
103 .map(|(_, label, about)| format!("- {label}: {}\n", dormant::hint(about)))
104 .collect();
105 out.push_str("Describe one by its whole name for its guide.\n");
106 out
107 }
108}
109
110fn score(words: &[String], label: &str, about: &str) -> u32 {
113 let label = label.to_lowercase();
114 let about = about.to_lowercase();
115 words
116 .iter()
117 .map(|word| {
118 if label.contains(word.as_str()) {
119 NAME_WEIGHT
120 } else {
121 u32::from(about.contains(word.as_str()))
122 }
123 })
124 .fold(0, u32::saturating_add)
125}