fastskill_core/http/handlers/
claude_api.rs1use crate::http::errors::{HttpError, HttpResult};
7use crate::http::handlers::{skill_storage::SkillStorage, AppState};
8use crate::http::models::*;
9use axum::{
10 extract::{Multipart, Path, Query, State},
11 Json,
12};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use validator::Validate;
16
17#[derive(Debug)]
21pub struct MultipartFile {
22 pub filename: String,
23 pub content: Vec<u8>,
24 pub content_type: Option<String>,
25}
26
27#[derive(Debug, Serialize)]
29pub struct CreateSkillVersionResponse {
30 pub created_at: String,
31 pub description: String,
32 pub directory: String,
33 pub id: String,
34 pub name: String,
35 pub skill_id: String,
36 pub r#type: String, pub version: String,
38}
39
40#[derive(Debug, Serialize)]
42pub struct ListSkillsResponse {
43 pub skills: Vec<SkillInfo>,
44 pub count: usize,
45 pub total: usize,
46}
47
48#[derive(Debug, Serialize)]
50pub struct SkillInfo {
51 pub id: String,
52 pub name: String,
53 pub description: String,
54 pub created_at: Option<String>,
55 pub updated_at: Option<String>,
56 pub latest_version: Option<String>,
57 pub source: String, }
59
60#[derive(Debug, Serialize)]
62pub struct GetSkillResponse {
63 pub id: String,
64 pub name: String,
65 pub description: String,
66 pub created_at: Option<String>,
67 pub updated_at: Option<String>,
68 pub latest_version: Option<String>,
69 pub source: String,
70}
71
72#[derive(Debug, Serialize)]
74pub struct CreateSkillResponse {
75 pub id: String,
76 pub name: String,
77 pub description: String,
78 pub created_at: String,
79 pub source: String,
80}
81
82#[derive(Debug, Deserialize)]
84pub struct ListSkillsQuery {
85 pub source: Option<String>, pub limit: Option<usize>,
87 pub offset: Option<usize>,
88}
89
90pub async fn create_skill(
92 State(state): State<AppState>,
93 Json(request): Json<CreateSkillRequest>,
94) -> HttpResult<Json<ApiResponse<CreateSkillResponse>>> {
95 request.validate().map_err(|e| {
97 HttpError::ValidationError(
98 e.field_errors()
99 .into_iter()
100 .map(|(field, errors)| {
101 (
102 field.to_string(),
103 errors
104 .iter()
105 .map(|e| e.message.clone().unwrap_or_default().to_string())
106 .collect(),
107 )
108 })
109 .collect(),
110 )
111 })?;
112
113 let skill_id = SkillStorage::generate_skill_id();
115
116 let skills_dir = state.service.config().skill_storage_path.clone();
118 let _skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
119 let created_at = chrono::Utc::now();
120
121 let response = CreateSkillResponse {
124 id: skill_id,
125 name: request.display_title.clone(),
126 description: request.description.unwrap_or_default(),
127 created_at: created_at.to_rfc3339(),
128 source: "custom".to_string(),
129 };
130
131 Ok(Json(ApiResponse::success(response)))
132}
133
134pub async fn list_skills(
136 State(state): State<AppState>,
137 Query(query): Query<ListSkillsQuery>,
138) -> HttpResult<Json<ApiResponse<ListSkillsResponse>>> {
139 let skills_dir = state.service.config().skill_storage_path.clone();
140 let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
141
142 let mut skills = skill_storage.list_skills().await?;
144
145 if let Some(source_filter) = &query.source {
147 skills.retain(|skill| {
148 let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf");
151 match source_filter.as_str() {
152 "anthropic" => is_anthropic,
153 "custom" => !is_anthropic,
154 _ => true,
155 }
156 });
157 }
158
159 let offset = query.offset.unwrap_or(0);
161 let limit = query.limit.unwrap_or(50);
162 let total = skills.len();
163 let paginated_skills = skills
164 .into_iter()
165 .skip(offset)
166 .take(limit)
167 .collect::<Vec<_>>();
168 let count = paginated_skills.len();
169
170 let skill_infos: Vec<SkillInfo> = paginated_skills
172 .into_iter()
173 .map(|skill| {
174 let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf");
175 SkillInfo {
176 id: skill.id,
177 name: skill.name,
178 description: skill.description,
179 created_at: Some(skill.created_at.to_rfc3339()),
180 updated_at: Some(skill.updated_at.to_rfc3339()),
181 latest_version: Some(skill.latest_version),
182 source: if is_anthropic {
183 "anthropic".to_string()
184 } else {
185 "custom".to_string()
186 },
187 }
188 })
189 .collect();
190
191 let response = ListSkillsResponse {
192 skills: skill_infos,
193 count,
194 total,
195 };
196
197 Ok(Json(ApiResponse::success(response)))
198}
199
200pub async fn get_skill(
202 State(state): State<AppState>,
203 Path(skill_id): Path<String>,
204) -> HttpResult<Json<ApiResponse<GetSkillResponse>>> {
205 let skills_dir = state.service.config().skill_storage_path.clone();
206 let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
207
208 let skill = skill_storage
209 .get_skill(&skill_id)
210 .await?
211 .ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?;
212
213 let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf");
214
215 let response = GetSkillResponse {
216 id: skill.id,
217 name: skill.name,
218 description: skill.description,
219 created_at: Some(skill.created_at.to_rfc3339()),
220 updated_at: Some(skill.updated_at.to_rfc3339()),
221 latest_version: Some(skill.latest_version),
222 source: if is_anthropic {
223 "anthropic".to_string()
224 } else {
225 "custom".to_string()
226 },
227 };
228
229 Ok(Json(ApiResponse::success(response)))
230}
231
232pub async fn delete_skill(
234 State(state): State<AppState>,
235 Path(skill_id): Path<String>,
236) -> HttpResult<Json<ApiResponse<serde_json::Value>>> {
237 let skills_dir = state.service.config().skill_storage_path.clone();
238 let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
239
240 skill_storage.delete_skill(&skill_id).await?;
241
242 Ok(Json(ApiResponse::success(serde_json::json!({
243 "message": "Skill deleted successfully"
244 }))))
245}
246
247pub async fn create_skill_version(
249 State(state): State<AppState>,
250 Path(skill_id): Path<String>,
251 mut multipart: Multipart,
252) -> HttpResult<Json<ApiResponse<CreateSkillVersionResponse>>> {
253 let skills_dir = state.service.config().skill_storage_path.clone();
254 let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
255
256 let mut files = HashMap::new();
258 while let Some(field) = multipart
259 .next_field()
260 .await
261 .map_err(|e| HttpError::BadRequest(format!("Failed to read multipart field: {}", e)))?
262 {
263 let name = field.name().unwrap_or("unnamed").to_string();
264 if name == "files" || name == "files[]" {
265 let filename = field.file_name().unwrap_or("unnamed").to_string();
266 let content = field.bytes().await.map_err(|e| {
267 HttpError::BadRequest(format!("Failed to read file content: {}", e))
268 })?;
269 files.insert(filename, content.to_vec());
270 }
271 }
272
273 if files.is_empty() {
274 return Err(HttpError::BadRequest("No files provided".to_string()));
275 }
276
277 let version = skill_storage.store_skill_version(&skill_id, files).await?;
279
280 let response = CreateSkillVersionResponse {
281 created_at: version.created_at.to_rfc3339(),
282 description: version.description,
283 directory: version.directory,
284 id: version.id,
285 name: version.name,
286 skill_id: version.skill_id,
287 r#type: "skill_version".to_string(),
288 version: version.version,
289 };
290
291 Ok(Json(ApiResponse::success(response)))
292}
293
294pub async fn list_skill_versions(
296 State(_state): State<AppState>,
297 Path(_skill_id): Path<String>,
298) -> HttpResult<Json<ApiResponse<SkillVersionsListResponse>>> {
299 let response = SkillVersionsListResponse {
301 versions: vec![],
302 count: 0,
303 total: 0,
304 };
305
306 Ok(Json(ApiResponse::success(response)))
307}
308
309pub async fn get_skill_version(
311 State(_state): State<AppState>,
312 Path((_skill_id, _version)): Path<(String, String)>,
313) -> HttpResult<Json<ApiResponse<SkillVersionResponse>>> {
314 Err(HttpError::NotFound(format!(
316 "Skill version not found: {}@{}",
317 _skill_id, _version
318 )))
319}
320
321pub async fn delete_skill_version(
323 State(_state): State<AppState>,
324 Path((_skill_id, _version)): Path<(String, String)>,
325) -> HttpResult<Json<ApiResponse<serde_json::Value>>> {
326 Err(HttpError::NotFound(format!(
328 "Skill version not found: {}@{}",
329 _skill_id, _version
330 )))
331}
332
333#[derive(Debug, Serialize)]
335pub struct SkillVersionsListResponse {
336 pub versions: Vec<SkillVersionInfo>,
337 pub count: usize,
338 pub total: usize,
339}
340
341#[derive(Debug, Serialize)]
342pub struct SkillVersionInfo {
343 pub id: String,
344 pub version: String,
345 pub created_at: String,
346 pub name: String,
347 pub description: String,
348}
349
350#[derive(Debug, Serialize)]
351pub struct SkillVersionResponse {
352 pub id: String,
353 pub skill_id: String,
354 pub version: String,
355 pub created_at: String,
356 pub name: String,
357 pub description: String,
358 pub directory: String,
359 pub r#type: String,
360}
361
362#[derive(Debug, Deserialize, validator::Validate)]
364pub struct CreateSkillRequest {
365 #[validate(length(min = 1, max = 64))]
366 pub display_title: String,
367 #[validate(length(max = 1024))]
368 pub description: Option<String>,
369}