Skip to main content

systemprompt_agent/repository/content/
skill.rs

1use crate::models::{Skill, SkillRow};
2use anyhow::{Context, Result};
3use sqlx::PgPool;
4use std::sync::Arc;
5use systemprompt_database::DatabaseProvider;
6use systemprompt_identifiers::{CategoryId, SkillId, SourceId};
7
8#[derive(Debug)]
9pub struct SkillRepository {
10    db: Arc<dyn DatabaseProvider>,
11}
12
13impl SkillRepository {
14    pub fn new(db: Arc<dyn DatabaseProvider>) -> Self {
15        Self { db }
16    }
17
18    fn get_pg_pool(&self) -> Result<Arc<PgPool>> {
19        self.db
20            .get_postgres_pool()
21            .context("PostgreSQL pool not available")
22    }
23
24    pub async fn create(&self, skill: &Skill) -> Result<()> {
25        let pool = self.get_pg_pool()?;
26        let skill_id_str = skill.skill_id.as_str();
27        let category_id = skill.category_id.as_ref().map(|c| c.to_string());
28        let source_id_str = skill.source_id.as_str();
29
30        sqlx::query!(
31            "INSERT INTO agent_skills (skill_id, file_path, name, description, instructions,
32             enabled, tags, category_id, source_id)
33             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
34            skill_id_str,
35            skill.file_path,
36            skill.name,
37            skill.description,
38            skill.instructions,
39            skill.enabled,
40            &skill.tags[..],
41            category_id,
42            source_id_str
43        )
44        .execute(pool.as_ref())
45        .await
46        .context(format!("Failed to create skill: {}", skill.name))?;
47
48        Ok(())
49    }
50
51    pub async fn get_by_skill_id(&self, skill_id: &SkillId) -> Result<Option<Skill>> {
52        let pool = self.get_pg_pool()?;
53        let skill_id_str = skill_id.as_str();
54
55        let row = sqlx::query_as!(
56            SkillRow,
57            r#"SELECT
58                skill_id as "skill_id!: SkillId",
59                file_path as "file_path!",
60                name as "name!",
61                description as "description!",
62                instructions as "instructions!",
63                enabled as "enabled!",
64                tags,
65                category_id as "category_id?: CategoryId",
66                source_id as "source_id!: SourceId",
67                created_at as "created_at!",
68                updated_at as "updated_at!"
69            FROM agent_skills WHERE skill_id = $1"#,
70            skill_id_str
71        )
72        .fetch_optional(pool.as_ref())
73        .await
74        .context(format!("Failed to get skill by id: {skill_id}"))?;
75
76        row.map(skill_from_row).transpose()
77    }
78
79    pub async fn get_by_file_path(&self, file_path: &str) -> Result<Option<Skill>> {
80        let pool = self.get_pg_pool()?;
81
82        let row = sqlx::query_as!(
83            SkillRow,
84            r#"SELECT
85                skill_id as "skill_id!: SkillId",
86                file_path as "file_path!",
87                name as "name!",
88                description as "description!",
89                instructions as "instructions!",
90                enabled as "enabled!",
91                tags,
92                category_id as "category_id?: CategoryId",
93                source_id as "source_id!: SourceId",
94                created_at as "created_at!",
95                updated_at as "updated_at!"
96            FROM agent_skills WHERE file_path = $1"#,
97            file_path
98        )
99        .fetch_optional(pool.as_ref())
100        .await
101        .context(format!("Failed to get skill by file path: {file_path}"))?;
102
103        row.map(skill_from_row).transpose()
104    }
105
106    pub async fn list_enabled(&self) -> Result<Vec<Skill>> {
107        let pool = self.get_pg_pool()?;
108
109        let rows = sqlx::query_as!(
110            SkillRow,
111            r#"SELECT
112                skill_id as "skill_id!: SkillId",
113                file_path as "file_path!",
114                name as "name!",
115                description as "description!",
116                instructions as "instructions!",
117                enabled as "enabled!",
118                tags,
119                category_id as "category_id?: CategoryId",
120                source_id as "source_id!: SourceId",
121                created_at as "created_at!",
122                updated_at as "updated_at!"
123            FROM agent_skills WHERE enabled = true ORDER BY name ASC"#
124        )
125        .fetch_all(pool.as_ref())
126        .await
127        .context("Failed to list enabled skills")?;
128
129        rows.into_iter()
130            .map(skill_from_row)
131            .collect::<Result<Vec<_>>>()
132    }
133
134    pub async fn list_all(&self) -> Result<Vec<Skill>> {
135        let pool = self.get_pg_pool()?;
136
137        let rows = sqlx::query_as!(
138            SkillRow,
139            r#"SELECT
140                skill_id as "skill_id!: SkillId",
141                file_path as "file_path!",
142                name as "name!",
143                description as "description!",
144                instructions as "instructions!",
145                enabled as "enabled!",
146                tags,
147                category_id as "category_id?: CategoryId",
148                source_id as "source_id!: SourceId",
149                created_at as "created_at!",
150                updated_at as "updated_at!"
151            FROM agent_skills ORDER BY name ASC"#
152        )
153        .fetch_all(pool.as_ref())
154        .await
155        .context("Failed to list all skills")?;
156
157        rows.into_iter()
158            .map(skill_from_row)
159            .collect::<Result<Vec<_>>>()
160    }
161
162    pub async fn update(&self, skill_id: &SkillId, skill: &Skill) -> Result<()> {
163        let pool = self.get_pg_pool()?;
164        let skill_id_str = skill_id.as_str();
165
166        sqlx::query!(
167            "UPDATE agent_skills SET name = $1, description = $2, instructions = $3, enabled = $4,
168             tags = $5, updated_at = CURRENT_TIMESTAMP
169             WHERE skill_id = $6",
170            skill.name,
171            skill.description,
172            skill.instructions,
173            skill.enabled,
174            &skill.tags[..],
175            skill_id_str
176        )
177        .execute(pool.as_ref())
178        .await
179        .context(format!("Failed to update skill: {}", skill.name))?;
180
181        Ok(())
182    }
183}
184
185fn skill_from_row(row: SkillRow) -> Result<Skill> {
186    Ok(Skill {
187        skill_id: row.skill_id,
188        file_path: row.file_path,
189        name: row.name,
190        description: row.description,
191        instructions: row.instructions,
192        enabled: row.enabled,
193        tags: row.tags.unwrap_or_else(Vec::new),
194        category_id: row.category_id,
195        source_id: row.source_id,
196        created_at: row.created_at,
197        updated_at: row.updated_at,
198    })
199}