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