systemprompt_agent/repository/content/
skill.rs1use 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}