ferrous_di/
capabilities.rs

1//! Capability discovery and tool catalog functionality for agentic systems.
2//!
3//! This module provides infrastructure for discovering available tools and their
4//! capabilities at runtime. Essential for agent planners that need to discover,
5//! filter, and select appropriate tools based on requirements.
6
7use std::any::{TypeId, type_name};
8use std::collections::HashMap;
9use std::sync::Arc;
10use crate::{ServiceCollection, Key};
11
12/// Metadata about a tool's capabilities and requirements.
13///
14/// This trait should be implemented by tools that want to expose their
15/// capabilities to the agent planner. Tools can declare what they can do,
16/// what they require, and other metadata useful for selection.
17///
18/// # Examples
19///
20/// ```
21/// use ferrous_di::{ToolCapability, ServiceCollection};
22/// use std::sync::Arc;
23///
24/// struct FileSearchTool {
25///     root_dir: String,
26/// }
27///
28/// impl ToolCapability for FileSearchTool {
29///     fn name(&self) -> &str { "file_search" }
30///     fn description(&self) -> &str { "Search for files matching patterns" }
31///     fn version(&self) -> &str { "1.0.0" }
32///     
33///     fn capabilities(&self) -> Vec<&str> {
34///         vec!["file_search", "pattern_matching", "filesystem_access"]
35///     }
36///     
37///     fn requires(&self) -> Vec<&str> {
38///         vec!["filesystem_read"]
39///     }
40///     
41///     fn tags(&self) -> Vec<&str> {
42///         vec!["files", "search", "core"]
43///     }
44/// }
45/// ```
46pub trait ToolCapability: Send + Sync {
47    /// The unique name/identifier of this tool.
48    fn name(&self) -> &str;
49    
50    /// Human-readable description of what this tool does.
51    fn description(&self) -> &str;
52    
53    /// Version of this tool.
54    fn version(&self) -> &str;
55    
56    /// List of capabilities this tool provides.
57    /// 
58    /// Capabilities are strings like "web_search", "file_read", "image_generation", etc.
59    fn capabilities(&self) -> Vec<&str>;
60    
61    /// List of capabilities this tool requires to function.
62    /// 
63    /// For example, a file tool might require "filesystem_access".
64    fn requires(&self) -> Vec<&str>;
65    
66    /// Optional tags for categorization and filtering.
67    /// 
68    /// Tags like "core", "experimental", "external", "local", etc.
69    fn tags(&self) -> Vec<&str> {
70        Vec::new()
71    }
72    
73    /// Optional cost estimate for using this tool.
74    /// 
75    /// This could be monetary cost, computational cost, time cost, etc.
76    /// Units are tool-specific but should be documented.
77    fn estimated_cost(&self) -> Option<f64> {
78        None
79    }
80    
81    /// Optional reliability score (0.0 to 1.0).
82    /// 
83    /// How reliable/stable is this tool? 1.0 = always works, 0.0 = very unreliable.
84    fn reliability(&self) -> Option<f64> {
85        None
86    }
87}
88
89/// Capability requirement for tool selection.
90///
91/// Used by planners to specify what capabilities they need when requesting
92/// tool recommendations.
93#[derive(Debug, Clone)]
94pub struct CapabilityRequirement {
95    /// The capability name that must be provided.
96    pub capability: String,
97    /// Whether this capability is required (vs nice-to-have).
98    pub required: bool,
99    /// Minimum version requirement (if applicable).
100    pub min_version: Option<String>,
101    /// Maximum acceptable cost for this capability.
102    pub max_cost: Option<f64>,
103    /// Minimum acceptable reliability for this capability.
104    pub min_reliability: Option<f64>,
105}
106
107impl CapabilityRequirement {
108    /// Creates a required capability.
109    pub fn required(capability: impl Into<String>) -> Self {
110        Self {
111            capability: capability.into(),
112            required: true,
113            min_version: None,
114            max_cost: None,
115            min_reliability: None,
116        }
117    }
118    
119    /// Creates an optional capability.
120    pub fn optional(capability: impl Into<String>) -> Self {
121        Self {
122            capability: capability.into(),
123            required: false,
124            min_version: None,
125            max_cost: None,
126            min_reliability: None,
127        }
128    }
129    
130    /// Sets minimum version requirement.
131    pub fn min_version(mut self, version: impl Into<String>) -> Self {
132        self.min_version = Some(version.into());
133        self
134    }
135    
136    /// Sets maximum acceptable cost.
137    pub fn max_cost(mut self, cost: f64) -> Self {
138        self.max_cost = Some(cost);
139        self
140    }
141    
142    /// Sets minimum acceptable reliability.
143    pub fn min_reliability(mut self, reliability: f64) -> Self {
144        self.min_reliability = Some(reliability);
145        self
146    }
147}
148
149/// Tool selection criteria for capability-based tool discovery.
150#[derive(Debug, Default)]
151pub struct ToolSelectionCriteria {
152    /// Required and optional capabilities.
153    pub capabilities: Vec<CapabilityRequirement>,
154    /// Tags that tools must have.
155    pub required_tags: Vec<String>,
156    /// Tags that tools should not have.
157    pub excluded_tags: Vec<String>,
158    /// Maximum total cost across all selected tools.
159    pub max_total_cost: Option<f64>,
160    /// Minimum average reliability across selected tools.
161    pub min_average_reliability: Option<f64>,
162    /// Maximum number of tools to return.
163    pub limit: Option<usize>,
164}
165
166impl ToolSelectionCriteria {
167    /// Creates new selection criteria.
168    pub fn new() -> Self {
169        Self::default()
170    }
171    
172    /// Adds a required capability.
173    pub fn require(mut self, capability: impl Into<String>) -> Self {
174        self.capabilities.push(CapabilityRequirement::required(capability));
175        self
176    }
177
178    /// Adds a required capability with cost constraint.
179    pub fn require_with_cost(mut self, capability: impl Into<String>, max_cost: f64) -> Self {
180        self.capabilities.push(CapabilityRequirement::required(capability).max_cost(max_cost));
181        self
182    }
183    
184    /// Adds an optional capability.
185    pub fn prefer(mut self, capability: impl Into<String>) -> Self {
186        self.capabilities.push(CapabilityRequirement::optional(capability));
187        self
188    }
189    
190    /// Adds a required tag.
191    pub fn require_tag(mut self, tag: impl Into<String>) -> Self {
192        self.required_tags.push(tag.into());
193        self
194    }
195    
196    /// Adds an excluded tag.
197    pub fn exclude_tag(mut self, tag: impl Into<String>) -> Self {
198        self.excluded_tags.push(tag.into());
199        self
200    }
201    
202    /// Sets maximum total cost limit.
203    pub fn max_cost(mut self, cost: f64) -> Self {
204        self.max_total_cost = Some(cost);
205        self
206    }
207    
208    /// Sets minimum reliability requirement.
209    pub fn min_reliability(mut self, reliability: f64) -> Self {
210        self.min_average_reliability = Some(reliability);
211        self
212    }
213    
214    /// Sets maximum number of tools to return.
215    pub fn limit(mut self, count: usize) -> Self {
216        self.limit = Some(count);
217        self
218    }
219}
220
221/// Information about a discovered tool.
222#[derive(Debug, Clone)]
223pub struct ToolInfo {
224    /// The service key for resolving this tool.
225    pub key: Key,
226    /// Tool name.
227    pub name: String,
228    /// Tool description.
229    pub description: String,
230    /// Tool version.
231    pub version: String,
232    /// Capabilities provided.
233    pub capabilities: Vec<String>,
234    /// Capabilities required.
235    pub requires: Vec<String>,
236    /// Tool tags.
237    pub tags: Vec<String>,
238    /// Estimated cost.
239    pub estimated_cost: Option<f64>,
240    /// Reliability score.
241    pub reliability: Option<f64>,
242}
243
244impl ToolInfo {
245    /// Checks if this tool satisfies a capability requirement.
246    pub fn satisfies(&self, req: &CapabilityRequirement) -> bool {
247        // Must provide the capability
248        if !self.capabilities.contains(&req.capability) {
249            return false;
250        }
251        
252        // Check cost constraint
253        if let Some(max_cost) = req.max_cost {
254            if let Some(cost) = self.estimated_cost {
255                if cost > max_cost {
256                    return false;
257                }
258            }
259        }
260        
261        // Check reliability constraint
262        if let Some(min_reliability) = req.min_reliability {
263            if let Some(reliability) = self.reliability {
264                if reliability < min_reliability {
265                    return false;
266                }
267            } else {
268                // No reliability info - assume it doesn't meet requirement
269                return false;
270            }
271        }
272        
273        // Version checking would go here if we implemented semver parsing
274        // For now, skip version checks
275        
276        true
277    }
278}
279
280/// Tool discovery result.
281#[derive(Debug)]
282pub struct ToolDiscoveryResult {
283    /// Tools that match all required criteria.
284    pub matching_tools: Vec<ToolInfo>,
285    /// Tools that match some optional criteria.
286    pub partial_matches: Vec<ToolInfo>,
287    /// Required capabilities that couldn't be satisfied.
288    pub unsatisfied_requirements: Vec<String>,
289}
290
291/// Registry of available tools and their capabilities.
292pub(crate) struct CapabilityRegistry {
293    /// Map from service keys to tool capability info.
294    tools: HashMap<Key, ToolInfo>,
295}
296
297impl CapabilityRegistry {
298    /// Creates a new empty capability registry.
299    pub(crate) fn new() -> Self {
300        Self {
301            tools: HashMap::new(),
302        }
303    }
304    
305    /// Registers a tool's capabilities.
306    pub(crate) fn register_tool<T: ?Sized + ToolCapability + 'static>(&mut self, key: Key, tool: &T) {
307        let info = ToolInfo {
308            key: key.clone(),
309            name: tool.name().to_string(),
310            description: tool.description().to_string(),
311            version: tool.version().to_string(),
312            capabilities: tool.capabilities().into_iter().map(|s| s.to_string()).collect(),
313            requires: tool.requires().into_iter().map(|s| s.to_string()).collect(),
314            tags: tool.tags().into_iter().map(|s| s.to_string()).collect(),
315            estimated_cost: tool.estimated_cost(),
316            reliability: tool.reliability(),
317        };
318        
319        self.tools.insert(key, info);
320    }
321    
322    /// Discovers tools based on selection criteria.
323    pub(crate) fn discover(&self, criteria: &ToolSelectionCriteria) -> ToolDiscoveryResult {
324        let mut matching_tools = Vec::new();
325        let mut partial_matches = Vec::new();
326        let mut unsatisfied_requirements = Vec::new();
327        
328        // Find required capabilities that no tool satisfies
329        for req in &criteria.capabilities {
330            if req.required {
331                let satisfied = self.tools.values().any(|tool| tool.satisfies(req));
332                if !satisfied {
333                    unsatisfied_requirements.push(req.capability.clone());
334                }
335            }
336        }
337        
338        // Score each tool
339        for tool in self.tools.values() {
340            let mut score = self.score_tool(tool, criteria);
341            
342            // Check required tags
343            let has_required_tags = criteria.required_tags.iter()
344                .all(|tag| tool.tags.contains(tag));
345            
346            // Check excluded tags
347            let has_excluded_tags = criteria.excluded_tags.iter()
348                .any(|tag| tool.tags.contains(tag));
349            
350            if !has_required_tags || has_excluded_tags {
351                score = 0.0; // Disqualify
352            }
353            
354            if score > 0.5 {
355                matching_tools.push(tool.clone());
356            } else if score > 0.0 {
357                partial_matches.push(tool.clone());
358            }
359        }
360        
361        // Sort by score (would need to store scores, simplified for now)
362        matching_tools.sort_by(|a, b| a.name.cmp(&b.name));
363        partial_matches.sort_by(|a, b| a.name.cmp(&b.name));
364        
365        // Apply limit
366        if let Some(limit) = criteria.limit {
367            matching_tools.truncate(limit);
368        }
369        
370        ToolDiscoveryResult {
371            matching_tools,
372            partial_matches,
373            unsatisfied_requirements,
374        }
375    }
376    
377    /// Scores a tool against the selection criteria (0.0 to 1.0).
378    fn score_tool(&self, tool: &ToolInfo, criteria: &ToolSelectionCriteria) -> f64 {
379        let mut score = 0.0;
380        let mut max_score = 0.0;
381        
382        // Score based on capability satisfaction
383        for req in &criteria.capabilities {
384            max_score += if req.required { 1.0 } else { 0.5 };
385            
386            if tool.satisfies(req) {
387                score += if req.required { 1.0 } else { 0.5 };
388            } else if req.required {
389                // Failed required capability - disqualify
390                return 0.0;
391            }
392        }
393        
394        if max_score == 0.0 {
395            return 1.0; // No capability requirements, all tools qualify
396        }
397        
398        score / max_score
399    }
400    
401    /// Gets all registered tools.
402    pub(crate) fn all_tools(&self) -> Vec<&ToolInfo> {
403        self.tools.values().collect()
404    }
405    
406    /// Gets tool info by key.
407    pub(crate) fn get_tool(&self, key: &Key) -> Option<&ToolInfo> {
408        self.tools.get(key)
409    }
410}
411
412impl ServiceCollection {
413    /// Registers a service as a tool with capabilities.
414    ///
415    /// This combines service registration with capability metadata registration,
416    /// making the tool discoverable through the capability system.
417    ///
418    /// # Examples
419    ///
420    /// ```
421    /// use ferrous_di::{ServiceCollection, ToolCapability};
422    /// use std::sync::Arc;
423    ///
424    /// struct WebSearchTool {
425    ///     api_key: String,
426    /// }
427    ///
428    /// impl ToolCapability for WebSearchTool {
429    ///     fn name(&self) -> &str { "web_search" }
430    ///     fn description(&self) -> &str { "Search the web for information" }
431    ///     fn version(&self) -> &str { "2.1.0" }
432    ///     fn capabilities(&self) -> Vec<&str> { vec!["web_search", "information_retrieval"] }
433    ///     fn requires(&self) -> Vec<&str> { vec!["internet_access", "api_key"] }
434    ///     fn tags(&self) -> Vec<&str> { vec!["external", "search", "web"] }
435    ///     fn estimated_cost(&self) -> Option<f64> { Some(0.001) } // $0.001 per query
436    ///     fn reliability(&self) -> Option<f64> { Some(0.95) } // 95% reliable
437    /// }
438    ///
439    /// let mut services = ServiceCollection::new();
440    /// let tool = WebSearchTool {
441    ///     api_key: "secret-key".to_string(),
442    /// };
443    /// services.add_tool_singleton(tool);
444    /// ```
445    pub fn add_tool_singleton<T>(&mut self, tool: T) -> &mut Self
446    where
447        T: ToolCapability + Send + Sync + 'static,
448    {
449        // Register capabilities first
450        let key = Key::Type(TypeId::of::<T>(), type_name::<T>());
451        self.capabilities.register_tool(key.clone(), &tool);
452        
453        // Then register as regular singleton
454        self.add_singleton(tool);
455        self
456    }
457    
458    /// Registers a trait as a tool with capabilities.
459    ///
460    /// # Examples
461    ///
462    /// ```
463    /// use ferrous_di::{ServiceCollection, ToolCapability};
464    /// use std::sync::Arc;
465    ///
466    /// trait SearchTool: ToolCapability + Send + Sync {
467    ///     fn search(&self, query: &str) -> Vec<String>;
468    /// }
469    ///
470    /// struct GoogleSearchTool;
471    ///
472    /// impl ToolCapability for GoogleSearchTool {
473    ///     fn name(&self) -> &str { "google_search" }
474    ///     fn description(&self) -> &str { "Search using Google" }
475    ///     fn version(&self) -> &str { "1.0.0" }
476    ///     fn capabilities(&self) -> Vec<&str> { vec!["web_search"] }
477    ///     fn requires(&self) -> Vec<&str> { vec!["internet"] }
478    /// }
479    ///
480    /// impl SearchTool for GoogleSearchTool {
481    ///     fn search(&self, query: &str) -> Vec<String> {
482    ///         // Implementation here
483    ///         vec![format!("Results for: {}", query)]
484    ///     }
485    /// }
486    ///
487    /// let mut services = ServiceCollection::new();
488    /// let tool = Arc::new(GoogleSearchTool);
489    /// services.add_tool_trait::<dyn SearchTool>(tool);
490    /// ```
491    pub fn add_tool_trait<T>(&mut self, tool: Arc<T>) -> &mut Self
492    where
493        T: ?Sized + ToolCapability + Send + Sync + 'static,
494    {
495        // Register capabilities first
496        let key = Key::Trait(type_name::<T>());
497        self.capabilities.register_tool(key.clone(), tool.as_ref());
498        
499        // Then register as trait
500        self.add_singleton_trait::<T>(tool);
501        self
502    }
503}
504
505// Implementation is in provider/mod.rs to access inner struct