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