Skip to main content

converge_knowledge/agentic/
skills.rs

1//! Skill Library - Consolidated Successful Patterns
2//!
3//! Implements a skill library where agents can:
4//! 1. Consolidate successful action patterns into reusable skills
5//! 2. Retrieve relevant skills when facing similar tasks
6//! 3. Build up a repertoire of proven approaches
7//!
8//! Based on the "Voyager" paper's skill library concept.
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14/// A learned skill that can be reused.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Skill {
17    /// Unique identifier.
18    pub id: Uuid,
19
20    /// Skill category/domain.
21    pub category: String,
22
23    /// Human-readable name.
24    pub name: String,
25
26    /// Description of what this skill does.
27    pub description: String,
28
29    /// Component patterns that make up this skill.
30    pub patterns: Vec<SkillPattern>,
31
32    /// Preconditions for using this skill.
33    pub preconditions: Vec<String>,
34
35    /// What this skill produces/achieves.
36    pub postconditions: Vec<String>,
37
38    /// Success rate when applied (0.0 to 1.0).
39    pub success_rate: f32,
40
41    /// How many times this skill has been used.
42    pub usage_count: u64,
43
44    /// When this skill was created.
45    pub created_at: DateTime<Utc>,
46
47    /// When this skill was last used.
48    pub last_used: DateTime<Utc>,
49
50    /// Tags for categorization.
51    pub tags: Vec<String>,
52}
53
54impl Skill {
55    /// Create a new skill.
56    pub fn new(
57        category: impl Into<String>,
58        name: impl Into<String>,
59        patterns: Vec<SkillPattern>,
60    ) -> Self {
61        let now = Utc::now();
62        Self {
63            id: Uuid::new_v4(),
64            category: category.into(),
65            name: name.into(),
66            description: String::new(),
67            patterns,
68            preconditions: Vec::new(),
69            postconditions: Vec::new(),
70            success_rate: 1.0,
71            usage_count: 0,
72            created_at: now,
73            last_used: now,
74            tags: Vec::new(),
75        }
76    }
77
78    /// Add a description.
79    pub fn with_description(mut self, description: impl Into<String>) -> Self {
80        self.description = description.into();
81        self
82    }
83
84    /// Add preconditions.
85    pub fn with_preconditions(mut self, preconditions: Vec<String>) -> Self {
86        self.preconditions = preconditions;
87        self
88    }
89
90    /// Add postconditions.
91    pub fn with_postconditions(mut self, postconditions: Vec<String>) -> Self {
92        self.postconditions = postconditions;
93        self
94    }
95
96    /// Set success rate.
97    pub fn with_success_rate(mut self, rate: f32) -> Self {
98        self.success_rate = rate.clamp(0.0, 1.0);
99        self
100    }
101
102    /// Set usage count.
103    pub fn with_usage_count(mut self, count: u64) -> Self {
104        self.usage_count = count;
105        self
106    }
107
108    /// Add tags.
109    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
110        self.tags = tags;
111        self
112    }
113
114    /// Record a usage of this skill.
115    pub fn record_usage(&mut self, succeeded: bool) {
116        self.usage_count += 1;
117        self.last_used = Utc::now();
118
119        // Update success rate with exponential moving average
120        let alpha = 0.1;
121        let outcome = if succeeded { 1.0 } else { 0.0 };
122        self.success_rate = alpha * outcome + (1.0 - alpha) * self.success_rate;
123    }
124
125    /// Get the executable code/template.
126    pub fn to_code(&self) -> String {
127        self.patterns
128            .iter()
129            .map(|p| format!("// {}\n{}", p.name, p.template))
130            .collect::<Vec<_>>()
131            .join("\n\n")
132    }
133}
134
135/// A component pattern within a skill.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct SkillPattern {
138    /// Pattern name.
139    pub name: String,
140
141    /// Code template or instruction.
142    pub template: String,
143
144    /// Parameters that can be filled in.
145    pub parameters: Vec<PatternParameter>,
146
147    /// Example usage.
148    pub example: Option<String>,
149}
150
151impl SkillPattern {
152    /// Create a new pattern.
153    pub fn new(name: impl Into<String>, template: impl Into<String>) -> Self {
154        Self {
155            name: name.into(),
156            template: template.into(),
157            parameters: Vec::new(),
158            example: None,
159        }
160    }
161
162    /// Add parameters.
163    pub fn with_parameters(mut self, params: Vec<PatternParameter>) -> Self {
164        self.parameters = params;
165        self
166    }
167
168    /// Add example.
169    pub fn with_example(mut self, example: impl Into<String>) -> Self {
170        self.example = Some(example.into());
171        self
172    }
173}
174
175/// A parameter in a skill pattern.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct PatternParameter {
178    /// Parameter name.
179    pub name: String,
180
181    /// Parameter type.
182    pub param_type: String,
183
184    /// Description.
185    pub description: String,
186
187    /// Default value if any.
188    pub default: Option<String>,
189}
190
191/// Library of learned skills.
192pub struct SkillLibrary {
193    skills: Vec<Skill>,
194}
195
196impl SkillLibrary {
197    /// Create a new skill library.
198    pub fn new() -> Self {
199        Self { skills: Vec::new() }
200    }
201
202    /// Add a skill to the library.
203    pub fn add_skill(&mut self, skill: Skill) {
204        // Check if similar skill exists (by name and category)
205        if let Some(existing) = self
206            .skills
207            .iter_mut()
208            .find(|s| s.name == skill.name && s.category == skill.category)
209        {
210            // Merge: keep the one with higher success rate
211            if skill.success_rate > existing.success_rate {
212                *existing = skill;
213            }
214        } else {
215            self.skills.push(skill);
216        }
217    }
218
219    /// Find skills by category.
220    pub fn find_by_category(&self, category: &str) -> Vec<&Skill> {
221        self.skills
222            .iter()
223            .filter(|s| s.category == category)
224            .collect()
225    }
226
227    /// Find skills by tag.
228    pub fn find_by_tag(&self, tag: &str) -> Vec<&Skill> {
229        self.skills
230            .iter()
231            .filter(|s| s.tags.iter().any(|t| t == tag))
232            .collect()
233    }
234
235    /// Find skills matching a description (keyword search).
236    pub fn search(&self, query: &str, limit: usize) -> Vec<&Skill> {
237        let query_lower = query.to_lowercase();
238        let keywords: Vec<&str> = query_lower.split_whitespace().collect();
239
240        let mut scored: Vec<(f32, &Skill)> = self
241            .skills
242            .iter()
243            .map(|s| {
244                let name_lower = s.name.to_lowercase();
245                let desc_lower = s.description.to_lowercase();
246                let cat_lower = s.category.to_lowercase();
247
248                let score: f32 = keywords
249                    .iter()
250                    .map(|k| {
251                        let mut kw_score = 0.0;
252                        if name_lower.contains(k) {
253                            kw_score += 2.0;
254                        }
255                        if desc_lower.contains(k) {
256                            kw_score += 1.0;
257                        }
258                        if cat_lower.contains(k) {
259                            kw_score += 1.5;
260                        }
261                        kw_score
262                    })
263                    .sum();
264
265                // Boost by success rate and usage
266                let adjusted = score
267                    * (0.5 + s.success_rate)
268                    * (1.0 + (s.usage_count as f32).ln().max(0.0) * 0.1);
269
270                (adjusted, s)
271            })
272            .filter(|(score, _)| *score > 0.0)
273            .collect();
274
275        scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
276
277        scored.into_iter().take(limit).map(|(_, s)| s).collect()
278    }
279
280    /// Get most used skills.
281    pub fn most_used(&self, limit: usize) -> Vec<&Skill> {
282        let mut skills: Vec<_> = self.skills.iter().collect();
283        skills.sort_by(|a, b| b.usage_count.cmp(&a.usage_count));
284        skills.into_iter().take(limit).collect()
285    }
286
287    /// Get highest success rate skills.
288    pub fn most_reliable(&self, limit: usize) -> Vec<&Skill> {
289        let mut skills: Vec<_> = self.skills.iter().filter(|s| s.usage_count >= 3).collect();
290        skills.sort_by(|a, b| {
291            b.success_rate
292                .partial_cmp(&a.success_rate)
293                .unwrap_or(std::cmp::Ordering::Equal)
294        });
295        skills.into_iter().take(limit).collect()
296    }
297
298    /// Total skill count.
299    pub fn len(&self) -> usize {
300        self.skills.len()
301    }
302
303    /// Check if empty.
304    pub fn is_empty(&self) -> bool {
305        self.skills.is_empty()
306    }
307}
308
309impl Default for SkillLibrary {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    /// Test: Creating and using a skill.
320    ///
321    /// What happens:
322    /// 1. Create a skill with code patterns
323    /// 2. Add preconditions (when to use)
324    /// 3. Add postconditions (what it achieves)
325    /// 4. Track usage statistics
326    #[test]
327    fn test_skill_creation() {
328        let skill = Skill::new(
329            "error_handling",
330            "Rust Result Pattern",
331            vec![
332                SkillPattern::new(
333                    "define_error",
334                    "#[derive(Debug, thiserror::Error)]\nenum AppError { ... }",
335                ),
336                SkillPattern::new(
337                    "use_result",
338                    "fn process() -> Result<Output, AppError> { ... }",
339                ),
340                SkillPattern::new("propagate", "let value = operation()?;"),
341            ],
342        )
343        .with_description("Standard Rust error handling with custom error types")
344        .with_preconditions(vec![
345            "Function can fail".to_string(),
346            "Need to propagate errors".to_string(),
347        ])
348        .with_postconditions(vec![
349            "Errors are properly typed".to_string(),
350            "Caller can handle or propagate".to_string(),
351        ])
352        .with_tags(vec!["rust".to_string(), "errors".to_string()]);
353
354        assert_eq!(skill.patterns.len(), 3);
355        assert_eq!(skill.preconditions.len(), 2);
356        assert_eq!(skill.postconditions.len(), 2);
357
358        // Get executable code
359        let code = skill.to_code();
360        assert!(code.contains("define_error"));
361        assert!(code.contains("Result<Output, AppError>"));
362    }
363
364    /// Test: Skill library search.
365    ///
366    /// What happens:
367    /// 1. Add multiple skills to library
368    /// 2. Search by keyword
369    /// 3. Results ranked by relevance and success rate
370    #[test]
371    fn test_skill_search() {
372        let mut library = SkillLibrary::new();
373
374        library.add_skill(
375            Skill::new("testing", "Unit Test Pattern", vec![])
376                .with_description("Writing unit tests with assertions")
377                .with_success_rate(0.9)
378                .with_usage_count(50),
379        );
380
381        library.add_skill(
382            Skill::new("testing", "Integration Test Pattern", vec![])
383                .with_description("Testing API endpoints end-to-end")
384                .with_success_rate(0.85)
385                .with_usage_count(20),
386        );
387
388        library.add_skill(
389            Skill::new("deployment", "Docker Build Pattern", vec![])
390                .with_description("Building Docker containers")
391                .with_success_rate(0.95)
392                .with_usage_count(30),
393        );
394
395        // Search for testing skills
396        let results = library.search("test assertions", 5);
397        assert!(!results.is_empty());
398        assert_eq!(results[0].name, "Unit Test Pattern");
399
400        // Search by category
401        let testing = library.find_by_category("testing");
402        assert_eq!(testing.len(), 2);
403    }
404
405    /// Test: Usage tracking and success rate.
406    ///
407    /// What happens:
408    /// 1. Use a skill multiple times
409    /// 2. Track success/failure outcomes
410    /// 3. Success rate adjusts with exponential moving average
411    #[test]
412    fn test_usage_tracking() {
413        let mut skill = Skill::new("math", "Division Pattern", vec![]).with_success_rate(1.0);
414
415        // Use successfully 3 times
416        skill.record_usage(true);
417        skill.record_usage(true);
418        skill.record_usage(true);
419        assert!(skill.success_rate > 0.95);
420
421        // One failure
422        skill.record_usage(false);
423        assert!(skill.success_rate < 1.0);
424        assert!(skill.success_rate > 0.8);
425
426        assert_eq!(skill.usage_count, 4);
427    }
428}