File size: 4,330 Bytes
d90101d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | use std::sync::Arc;
use anyhow::{Context, anyhow};
use forge_app::SkillFetchService;
use forge_domain::Skill;
use tokio::sync::OnceCell;
/// Loads specialized skills for specific task types. ALWAYS check the
/// available_skills list when a user request matches a skill's description or
/// trigger conditions. Skills provide domain-specific workflows and must be
/// invoked BEFORE attempting the task directly. Only invoke skills listed in
/// available_skills. Do not invoke a skill that is already active.
pub struct ForgeSkillFetch<R> {
repository: Arc<R>,
cache: OnceCell<Vec<Skill>>,
}
impl<R> ForgeSkillFetch<R> {
/// Creates a new skill fetch tool
pub fn new(repository: Arc<R>) -> Self {
Self { repository, cache: OnceCell::new() }
}
}
#[async_trait::async_trait]
impl<R: forge_domain::SkillRepository> SkillFetchService for ForgeSkillFetch<R> {
async fn fetch_skill(&self, skill_name: String) -> anyhow::Result<Skill> {
// Load skills from cache or repository
let skills = self.get_or_load_skills().await?;
// Find the requested skill
skills
.iter()
.find(|skill| skill.name == skill_name)
.cloned()
.ok_or_else(|| {
anyhow!("Skill '{skill_name}' not found. Please check the available skills list.")
})
}
async fn list_skills(&self) -> anyhow::Result<Vec<Skill>> {
self.get_or_load_skills().await.cloned()
}
}
impl<R: forge_domain::SkillRepository> ForgeSkillFetch<R> {
/// Gets skills from cache or loads them from repository if not cached
async fn get_or_load_skills(&self) -> anyhow::Result<&Vec<Skill>> {
self.cache
.get_or_try_init(|| async {
self.repository
.load_skills()
.await
.context("Failed to load skills")
})
.await
}
}
#[cfg(test)]
mod tests {
use forge_domain::Skill;
use pretty_assertions::assert_eq;
use super::*;
struct MockSkillRepository {
skills: Vec<Skill>,
}
#[async_trait::async_trait]
impl forge_domain::SkillRepository for MockSkillRepository {
async fn load_skills(&self) -> anyhow::Result<Vec<Skill>> {
Ok(self.skills.clone())
}
}
#[tokio::test]
async fn test_fetch_skill_found() {
// Fixture
let skills = vec![
Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"),
Skill::new("xlsx", "Handle Excel files", "Excel handling skill")
.path("/skills/xlsx.md"),
];
let repo = MockSkillRepository { skills: skills.clone() };
let fetch_service = ForgeSkillFetch::new(Arc::new(repo));
// Act
let actual = fetch_service.fetch_skill("pdf".to_string()).await;
// Assert
assert!(actual.is_ok());
let expected =
Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md");
assert_eq!(actual.unwrap(), expected);
}
#[tokio::test]
async fn test_fetch_skill_not_found() {
// Fixture
let skills = vec![
Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"),
];
let repo = MockSkillRepository { skills };
let fetch_service = ForgeSkillFetch::new(Arc::new(repo));
// Act
let actual = fetch_service.fetch_skill("unknown".to_string()).await;
// Assert
assert!(actual.is_err());
let error = actual.unwrap_err().to_string();
assert!(error.contains("Skill 'unknown' not found"));
}
#[tokio::test]
async fn test_list_skills() {
// Fixture
let expected = vec![
Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"),
Skill::new("xlsx", "Handle Excel files", "Excel handling skill")
.path("/skills/xlsx.md"),
];
let repo = MockSkillRepository { skills: expected.clone() };
let fetch_service = ForgeSkillFetch::new(Arc::new(repo));
// Act
let actual = fetch_service.list_skills().await.unwrap();
// Assert
assert_eq!(actual, expected);
}
}
|