synaptic_deep/middleware/
skills.rs1use async_trait::async_trait;
2use std::sync::Arc;
3use synaptic_core::SynapticError;
4use synaptic_middleware::{AgentMiddleware, ModelRequest};
5
6use crate::backend::Backend;
7
8pub struct Skill {
10 pub name: String,
11 pub description: String,
12 pub path: String,
13}
14
15pub struct SkillsMiddleware {
20 backend: Arc<dyn Backend>,
21 skills_dir: String,
22}
23
24impl SkillsMiddleware {
25 pub fn new(backend: Arc<dyn Backend>, skills_dir: String) -> Self {
26 Self {
27 backend,
28 skills_dir,
29 }
30 }
31
32 async fn discover_skills(&self) -> Vec<Skill> {
33 let entries = match self.backend.ls(&self.skills_dir).await {
34 Ok(e) => e,
35 Err(_) => return Vec::new(),
36 };
37
38 let mut skills = Vec::new();
39 for entry in entries {
40 if !entry.is_dir {
41 continue;
42 }
43 let skill_path = format!("{}/{}/SKILL.md", self.skills_dir, entry.name);
44 if let Ok(content) = self.backend.read_file(&skill_path, 0, 50).await {
45 if let Some(skill) = parse_skill_frontmatter(&content, &skill_path) {
46 skills.push(skill);
47 }
48 }
49 }
50 skills
51 }
52}
53
54fn parse_skill_frontmatter(content: &str, path: &str) -> Option<Skill> {
56 let mut lines = content.lines();
57
58 if lines.next()?.trim() != "---" {
59 return None;
60 }
61
62 let mut name = None;
63 let mut description = None;
64
65 for line in lines {
66 let trimmed = line.trim();
67 if trimmed == "---" {
68 break;
69 }
70 if let Some(val) = trimmed.strip_prefix("name:") {
71 name = Some(val.trim().trim_matches('"').trim_matches('\'').to_string());
72 } else if let Some(val) = trimmed.strip_prefix("description:") {
73 description = Some(val.trim().trim_matches('"').trim_matches('\'').to_string());
74 }
75 }
76
77 Some(Skill {
78 name: name?,
79 description: description.unwrap_or_default(),
80 path: path.to_string(),
81 })
82}
83
84#[async_trait]
85impl AgentMiddleware for SkillsMiddleware {
86 async fn before_model(&self, request: &mut ModelRequest) -> Result<(), SynapticError> {
87 let skills = self.discover_skills().await;
88 if skills.is_empty() {
89 return Ok(());
90 }
91
92 let mut section = String::from("\n<available_skills>\n");
93 for skill in &skills {
94 section.push_str(&format!(
95 "- **{}**: {} (read `{}` for details)\n",
96 skill.name, skill.description, skill.path
97 ));
98 }
99 section.push_str("</available_skills>\n");
100
101 if let Some(ref mut prompt) = request.system_prompt {
102 prompt.push_str(§ion);
103 } else {
104 request.system_prompt = Some(section);
105 }
106 Ok(())
107 }
108}