atomr_agents_skill/
lib.rs1use async_trait::async_trait;
5use atomr_agents_core::{AgentContext, MemoryNamespace, Result, SkillId, TokenBudget, ToolId};
6use atomr_agents_strategy::{SkillRef, SkillStrategy};
7use semver::Version;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Skill {
12 pub id: SkillId,
13 pub name: String,
14 pub instruction_fragment: String,
15 #[serde(default)]
16 pub tool_overlay: Vec<ToolId>,
17 #[serde(default)]
18 pub memory_namespace: Option<MemoryNamespace>,
19 #[serde(default)]
21 pub keywords: Vec<String>,
22 #[serde(default = "default_priority")]
23 pub priority: u8,
24}
25
26fn default_priority() -> u8 {
27 5
28}
29
30#[derive(Clone)]
31pub struct SkillSet {
32 pub id: String,
33 pub version: Version,
34 pub skills: Vec<Skill>,
35}
36
37impl SkillSet {
38 pub fn new(id: impl Into<String>, version: Version, skills: Vec<Skill>) -> Self {
39 Self {
40 id: id.into(),
41 version,
42 skills,
43 }
44 }
45}
46
47pub struct StaticSkillStrategy {
49 skills: Vec<Skill>,
50}
51
52impl StaticSkillStrategy {
53 pub fn new(skills: Vec<Skill>) -> Self {
54 Self { skills }
55 }
56}
57
58#[async_trait]
59impl SkillStrategy for StaticSkillStrategy {
60 async fn applicable(&self, _ctx: &AgentContext, _budget: &mut TokenBudget) -> Result<Vec<SkillRef>> {
61 Ok(self
62 .skills
63 .iter()
64 .map(|s| SkillRef {
65 id: s.id.clone(),
66 name: s.name.clone(),
67 priority: s.priority,
68 })
69 .collect())
70 }
71}
72
73pub struct KeywordSkillStrategy {
75 skills: Vec<Skill>,
76}
77
78impl KeywordSkillStrategy {
79 pub fn new(skills: Vec<Skill>) -> Self {
80 Self { skills }
81 }
82}
83
84#[async_trait]
85impl SkillStrategy for KeywordSkillStrategy {
86 async fn applicable(&self, ctx: &AgentContext, _budget: &mut TokenBudget) -> Result<Vec<SkillRef>> {
87 let needle = ctx.turn.user.to_lowercase();
88 let mut out: Vec<SkillRef> = self
89 .skills
90 .iter()
91 .filter(|s| s.keywords.iter().any(|k| needle.contains(&k.to_lowercase())))
92 .map(|s| SkillRef {
93 id: s.id.clone(),
94 name: s.name.clone(),
95 priority: s.priority,
96 })
97 .collect();
98 out.sort_by_key(|s| std::cmp::Reverse(s.priority));
99 Ok(out)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use atomr_agents_core::{AgentId, TurnInput};
107
108 fn ctx(text: &str) -> AgentContext {
109 AgentContext::for_agent(
110 AgentId::from("a-1"),
111 TurnInput {
112 user: text.into(),
113 history: vec![],
114 },
115 )
116 }
117
118 #[tokio::test]
119 async fn keyword_picks_matching_skills() {
120 let s1 = Skill {
121 id: SkillId::from("rag"),
122 name: "RAG".into(),
123 instruction_fragment: "use the index".into(),
124 tool_overlay: vec![],
125 memory_namespace: None,
126 keywords: vec!["search".into(), "lookup".into()],
127 priority: 7,
128 };
129 let s2 = Skill {
130 id: SkillId::from("math"),
131 name: "Math".into(),
132 instruction_fragment: "use the calculator".into(),
133 tool_overlay: vec![],
134 memory_namespace: None,
135 keywords: vec!["compute".into()],
136 priority: 3,
137 };
138 let strat = KeywordSkillStrategy::new(vec![s1, s2]);
139 let mut b = TokenBudget::new(1000);
140 let out = strat
141 .applicable(&ctx("please search for x"), &mut b)
142 .await
143 .unwrap();
144 assert_eq!(out.len(), 1);
145 assert_eq!(out[0].name, "RAG");
146 }
147}