1use crate::auth::middleware::AuthUser;
6use crate::db::turso::UserAgent;
7use crate::types::{AppError, Result};
8use crate::utils::toon_config::ToonAgentConfig;
9use crate::AppState;
10use axum::{
11 extract::{Path, Query, State},
12 http::{header, StatusCode},
13 response::{IntoResponse, Response},
14 Json,
15};
16use chrono::Utc;
17use serde::{Deserialize, Serialize};
18use toon_format::{decode_default, encode_default};
19use utoipa::{IntoParams, ToSchema};
20
21#[derive(Debug, Deserialize, ToSchema)]
25pub struct CreateAgentRequest {
26 pub name: String,
28 #[serde(default)]
30 pub display_name: Option<String>,
31 #[serde(default)]
33 pub description: Option<String>,
34 pub model: String,
36 #[serde(default)]
38 pub system_prompt: Option<String>,
39 #[serde(default)]
41 pub tools: Vec<String>,
42 #[serde(default = "default_max_tool_iterations")]
44 pub max_tool_iterations: i32,
45 #[serde(default)]
47 pub parallel_tools: bool,
48 #[serde(default)]
50 pub is_public: bool,
51}
52
53fn default_max_tool_iterations() -> i32 {
54 10
55}
56
57#[derive(Debug, Serialize, ToSchema)]
59pub struct CreateAgentResponse {
60 pub id: String,
62 pub name: String,
64 pub display_name: Option<String>,
66 pub created_at: i64,
68 pub api_endpoint: String,
70 pub toon_preview: String,
72}
73
74#[derive(Debug, Serialize, ToSchema)]
76pub struct AgentResponse {
77 pub id: String,
79 pub name: String,
81 pub display_name: Option<String>,
83 pub description: Option<String>,
85 pub model: String,
87 pub system_prompt: Option<String>,
89 pub tools: Vec<String>,
91 pub max_tool_iterations: i32,
93 pub parallel_tools: bool,
95 pub is_public: bool,
97 pub usage_count: i32,
99 pub average_rating: Option<f32>,
101 pub created_at: i64,
103 pub updated_at: i64,
105 pub source: String,
107}
108
109#[derive(Debug, Deserialize, Default, ToSchema, IntoParams)]
111pub struct GetAgentQuery {
112 #[serde(default)]
114 pub format: Option<String>,
115}
116
117#[derive(Debug, Deserialize, Default, ToSchema, IntoParams)]
119pub struct ListAgentsQuery {
120 #[serde(default)]
122 pub include_public: bool,
123 #[serde(default = "default_limit")]
125 pub limit: u32,
126 #[serde(default)]
128 pub offset: u32,
129}
130
131fn default_limit() -> u32 {
132 50
133}
134
135#[derive(Debug, Deserialize, ToSchema)]
137pub struct UpdateAgentRequest {
138 #[serde(default)]
140 pub display_name: Option<String>,
141 #[serde(default)]
143 pub description: Option<String>,
144 #[serde(default)]
146 pub model: Option<String>,
147 #[serde(default)]
149 pub system_prompt: Option<String>,
150 #[serde(default)]
152 pub tools: Option<Vec<String>>,
153 #[serde(default)]
155 pub max_tool_iterations: Option<i32>,
156 #[serde(default)]
158 pub parallel_tools: Option<bool>,
159 #[serde(default)]
161 pub is_public: Option<bool>,
162}
163
164#[utoipa::path(
170 post,
171 path = "/api/user/agents",
172 request_body = CreateAgentRequest,
173 responses(
174 (status = 200, description = "Agent created successfully", body = CreateAgentResponse),
175 (status = 400, description = "Invalid input"),
176 (status = 401, description = "Unauthorized"),
177 (status = 409, description = "Agent already exists")
178 ),
179 tag = "user_agents",
180 security(("bearer" = []))
181)]
182pub async fn create_agent(
183 State(state): State<AppState>,
184 user: AuthUser,
185 Json(req): Json<CreateAgentRequest>,
186) -> Result<Json<CreateAgentResponse>> {
187 if !is_valid_agent_name(&req.name) {
189 return Err(AppError::InvalidInput(
190 "Agent name must be alphanumeric with hyphens and underscores only".to_string(),
191 ));
192 }
193
194 if state
196 .turso
197 .get_user_agent_by_name(&user.0.sub, &req.name)
198 .await?
199 .is_some()
200 {
201 return Err(AppError::InvalidInput(format!(
202 "Agent '{}' already exists",
203 req.name
204 )));
205 }
206
207 let model_exists = state.dynamic_config.model(&req.model).is_some()
209 || state
210 .config_manager
211 .config()
212 .get_model(&req.model)
213 .is_some();
214
215 if !model_exists {
216 return Err(AppError::InvalidInput(format!(
217 "Model '{}' not found. Available models: {:?}",
218 req.model,
219 state.dynamic_config.model_names()
220 )));
221 }
222
223 for tool in &req.tools {
225 let tool_exists = state.dynamic_config.tool(tool).is_some()
226 || state.config_manager.config().get_tool(tool).is_some();
227
228 if !tool_exists {
229 return Err(AppError::InvalidInput(format!(
230 "Tool '{}' not found. Available tools: {:?}",
231 tool,
232 state.dynamic_config.tool_names()
233 )));
234 }
235 }
236
237 let agent_config = ToonAgentConfig {
239 name: req.name.clone(),
240 model: req.model.clone(),
241 system_prompt: req.system_prompt.clone(),
242 tools: req.tools.clone(),
243 max_tool_iterations: req.max_tool_iterations as usize,
244 parallel_tools: req.parallel_tools,
245 extra: std::collections::HashMap::new(),
246 };
247
248 let toon_preview = encode_default(&agent_config)
250 .map_err(|e| AppError::Internal(format!("TOON encode error: {}", e)))?;
251
252 let id = uuid::Uuid::new_v4().to_string();
253 let now = Utc::now().timestamp();
254
255 let agent = UserAgent {
256 id: id.clone(),
257 user_id: user.0.sub.clone(),
258 name: req.name.clone(),
259 display_name: req.display_name.clone(),
260 description: req.description,
261 model: req.model,
262 system_prompt: req.system_prompt,
263 tools: serde_json::to_string(&req.tools).unwrap_or_else(|_| "[]".to_string()),
264 max_tool_iterations: req.max_tool_iterations,
265 parallel_tools: req.parallel_tools,
266 extra: "{}".to_string(),
267 is_public: req.is_public,
268 usage_count: 0,
269 rating_sum: 0,
270 rating_count: 0,
271 created_at: now,
272 updated_at: now,
273 };
274
275 state.turso.create_user_agent(&agent).await?;
276
277 Ok(Json(CreateAgentResponse {
278 id,
279 name: req.name.clone(),
280 display_name: req.display_name,
281 created_at: now,
282 api_endpoint: format!("/api/user/agents/{}/chat", req.name),
283 toon_preview,
284 }))
285}
286
287#[utoipa::path(
292 post,
293 path = "/api/user/agents/import",
294 request_body(content = String, content_type = "text/x-toon"),
295 responses(
296 (status = 200, description = "Agent imported successfully", body = CreateAgentResponse),
297 (status = 400, description = "Invalid TOON format"),
298 (status = 401, description = "Unauthorized")
299 ),
300 tag = "user_agents",
301 security(("bearer" = []))
302)]
303pub async fn import_agent_toon(
304 State(state): State<AppState>,
305 user: AuthUser,
306 body: String,
307) -> Result<Json<CreateAgentResponse>> {
308 let agent_config: ToonAgentConfig = decode_default(&body)
310 .map_err(|e| AppError::InvalidInput(format!("Invalid TOON format: {}", e)))?;
311
312 let req = CreateAgentRequest {
314 name: agent_config.name,
315 display_name: None,
316 description: None,
317 model: agent_config.model,
318 system_prompt: agent_config.system_prompt,
319 tools: agent_config.tools,
320 max_tool_iterations: agent_config.max_tool_iterations as i32,
321 parallel_tools: agent_config.parallel_tools,
322 is_public: false,
323 };
324
325 create_agent(State(state), user, Json(req)).await
326}
327
328#[utoipa::path(
332 get,
333 path = "/api/user/agents/{name}",
334 params(
335 ("name" = String, Path, description = "Agent name"),
336 GetAgentQuery
337 ),
338 responses(
339 (status = 200, description = "Agent details", body = AgentResponse),
340 (status = 200, description = "Agent in TOON format", content_type = "text/x-toon"),
341 (status = 401, description = "Unauthorized"),
342 (status = 404, description = "Agent not found")
343 ),
344 tag = "user_agents",
345 security(("bearer" = []))
346)]
347pub async fn get_agent(
348 State(state): State<AppState>,
349 Path(name): Path<String>,
350 Query(params): Query<GetAgentQuery>,
351 user: AuthUser,
352) -> Result<Response> {
353 let (agent, source) = resolve_agent(&state, &user.0.sub, &name).await?;
355
356 match params.format.as_deref() {
357 Some("toon") => {
358 let config = ToonAgentConfig {
360 name: agent.name.clone(),
361 model: agent.model.clone(),
362 system_prompt: agent.system_prompt.clone(),
363 tools: agent.tools_vec(),
364 max_tool_iterations: agent.max_tool_iterations as usize,
365 parallel_tools: agent.parallel_tools,
366 extra: std::collections::HashMap::new(),
367 };
368
369 let toon = encode_default(&config)
370 .map_err(|e| AppError::Internal(format!("TOON encode error: {}", e)))?;
371
372 Ok(([(header::CONTENT_TYPE, "text/x-toon")], toon).into_response())
373 }
374 _ => {
375 let tools = agent.tools_vec();
376 let avg_rating = agent.average_rating();
377 Ok(Json(AgentResponse {
378 id: agent.id,
379 name: agent.name,
380 display_name: agent.display_name,
381 description: agent.description,
382 model: agent.model,
383 system_prompt: agent.system_prompt,
384 tools,
385 max_tool_iterations: agent.max_tool_iterations,
386 parallel_tools: agent.parallel_tools,
387 is_public: agent.is_public,
388 usage_count: agent.usage_count,
389 average_rating: avg_rating,
390 created_at: agent.created_at,
391 updated_at: agent.updated_at,
392 source,
393 })
394 .into_response())
395 }
396 }
397}
398
399#[utoipa::path(
403 get,
404 path = "/api/user/agents",
405 params(ListAgentsQuery),
406 responses(
407 (status = 200, description = "List of agents", body = Vec<AgentResponse>),
408 (status = 401, description = "Unauthorized")
409 ),
410 tag = "user_agents",
411 security(("bearer" = []))
412)]
413pub async fn list_agents(
414 State(state): State<AppState>,
415 Query(params): Query<ListAgentsQuery>,
416 user: AuthUser,
417) -> Result<Json<Vec<AgentResponse>>> {
418 let mut agents = Vec::new();
419
420 let user_agents = state.turso.list_user_agents(&user.0.sub).await?;
422 for agent in user_agents {
423 agents.push(user_agent_to_response(agent, "user".to_string()));
424 }
425
426 if params.include_public {
428 let public_agents = state
429 .turso
430 .list_public_agents(params.limit, params.offset)
431 .await?;
432
433 for agent in public_agents {
434 if agents.iter().any(|a| a.name == agent.name) {
436 continue;
437 }
438
439 agents.push(user_agent_to_response(agent, "community".to_string()));
440 }
441 }
442
443 Ok(Json(agents))
444}
445
446#[utoipa::path(
450 put,
451 path = "/api/user/agents/{name}",
452 params(
453 ("name" = String, Path, description = "Agent name")
454 ),
455 request_body = UpdateAgentRequest,
456 responses(
457 (status = 200, description = "Agent updated successfully", body = AgentResponse),
458 (status = 400, description = "Invalid input"),
459 (status = 401, description = "Unauthorized"),
460 (status = 403, description = "Forbidden - not owner"),
461 (status = 404, description = "Agent not found")
462 ),
463 tag = "user_agents",
464 security(("bearer" = []))
465)]
466pub async fn update_agent(
467 State(state): State<AppState>,
468 Path(name): Path<String>,
469 user: AuthUser,
470 Json(req): Json<UpdateAgentRequest>,
471) -> Result<Json<AgentResponse>> {
472 let mut agent = state
474 .turso
475 .get_user_agent_by_name(&user.0.sub, &name)
476 .await?
477 .ok_or_else(|| AppError::NotFound(format!("Agent '{}' not found", name)))?;
478
479 if agent.user_id != user.0.sub {
481 return Err(AppError::Auth(
482 "You can only update your own agents".to_string(),
483 ));
484 }
485
486 if let Some(display_name) = req.display_name {
488 agent.display_name = Some(display_name);
489 }
490 if let Some(description) = req.description {
491 agent.description = Some(description);
492 }
493 if let Some(model) = req.model {
494 let model_exists = state.dynamic_config.model(&model).is_some()
496 || state.config_manager.config().get_model(&model).is_some();
497 if !model_exists {
498 return Err(AppError::InvalidInput(format!(
499 "Model '{}' not found",
500 model
501 )));
502 }
503 agent.model = model;
504 }
505 if let Some(system_prompt) = req.system_prompt {
506 agent.system_prompt = Some(system_prompt);
507 }
508 if let Some(tools) = req.tools {
509 for tool in &tools {
511 let tool_exists = state.dynamic_config.tool(tool).is_some()
512 || state.config_manager.config().get_tool(tool).is_some();
513 if !tool_exists {
514 return Err(AppError::InvalidInput(format!("Tool '{}' not found", tool)));
515 }
516 }
517 agent.set_tools(tools);
518 }
519 if let Some(max_tool_iterations) = req.max_tool_iterations {
520 agent.max_tool_iterations = max_tool_iterations;
521 }
522 if let Some(parallel_tools) = req.parallel_tools {
523 agent.parallel_tools = parallel_tools;
524 }
525 if let Some(is_public) = req.is_public {
526 agent.is_public = is_public;
527 }
528
529 agent.updated_at = Utc::now().timestamp();
530
531 state.turso.update_user_agent(&agent).await?;
532
533 Ok(Json(user_agent_to_response(agent, "user".to_string())))
534}
535
536#[utoipa::path(
540 delete,
541 path = "/api/user/agents/{name}",
542 params(
543 ("name" = String, Path, description = "Agent name")
544 ),
545 responses(
546 (status = 204, description = "Agent deleted successfully"),
547 (status = 401, description = "Unauthorized"),
548 (status = 404, description = "Agent not found")
549 ),
550 tag = "user_agents",
551 security(("bearer" = []))
552)]
553pub async fn delete_agent(
554 State(state): State<AppState>,
555 Path(name): Path<String>,
556 user: AuthUser,
557) -> Result<StatusCode> {
558 let agent = state
560 .turso
561 .get_user_agent_by_name(&user.0.sub, &name)
562 .await?
563 .ok_or_else(|| AppError::NotFound(format!("Agent '{}' not found", name)))?;
564
565 let deleted = state
567 .turso
568 .delete_user_agent(&agent.id, &user.0.sub)
569 .await?;
570
571 if deleted {
572 Ok(StatusCode::NO_CONTENT)
573 } else {
574 Err(AppError::NotFound(format!("Agent '{}' not found", name)))
575 }
576}
577
578#[utoipa::path(
582 get,
583 path = "/api/user/agents/{name}/export",
584 params(
585 ("name" = String, Path, description = "Agent name")
586 ),
587 responses(
588 (status = 200, description = "Agent exported as TOON file", content_type = "text/x-toon"),
589 (status = 401, description = "Unauthorized"),
590 (status = 404, description = "Agent not found")
591 ),
592 tag = "user_agents",
593 security(("bearer" = []))
594)]
595pub async fn export_agent_toon(
596 State(state): State<AppState>,
597 Path(name): Path<String>,
598 user: AuthUser,
599) -> Result<Response> {
600 let (agent, _) = resolve_agent(&state, &user.0.sub, &name).await?;
601
602 let tools = agent.tools_vec();
603 let filename = format!("{}.toon", agent.name);
604 let config = ToonAgentConfig {
605 name: agent.name,
606 model: agent.model,
607 system_prompt: agent.system_prompt,
608 tools,
609 max_tool_iterations: agent.max_tool_iterations as usize,
610 parallel_tools: agent.parallel_tools,
611 extra: std::collections::HashMap::new(),
612 };
613
614 let toon = encode_default(&config)
615 .map_err(|e| AppError::Internal(format!("TOON encode error: {}", e)))?;
616
617 Ok((
618 [
619 (header::CONTENT_TYPE, "text/x-toon"),
620 (
621 header::CONTENT_DISPOSITION,
622 &format!("attachment; filename=\"{}\"", filename),
623 ),
624 ],
625 toon,
626 )
627 .into_response())
628}
629
630fn is_valid_agent_name(name: &str) -> bool {
634 !name.is_empty()
635 && name.len() <= 64
636 && name
637 .chars()
638 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
639}
640
641fn user_agent_to_response(agent: UserAgent, source: String) -> AgentResponse {
643 let tools = agent.tools_vec();
644 let avg_rating = agent.average_rating();
645 AgentResponse {
646 id: agent.id,
647 name: agent.name,
648 display_name: agent.display_name,
649 description: agent.description,
650 model: agent.model,
651 system_prompt: agent.system_prompt,
652 tools,
653 max_tool_iterations: agent.max_tool_iterations,
654 parallel_tools: agent.parallel_tools,
655 is_public: agent.is_public,
656 usage_count: agent.usage_count,
657 average_rating: avg_rating,
658 created_at: agent.created_at,
659 updated_at: agent.updated_at,
660 source,
661 }
662}
663
664pub async fn resolve_agent(
667 state: &AppState,
668 user_id: &str,
669 name: &str,
670) -> Result<(UserAgent, String)> {
671 if let Some(agent) = state.turso.get_user_agent_by_name(user_id, name).await? {
673 return Ok((agent, "user".to_string()));
674 }
675
676 if let Some(agent) = state.turso.get_public_agent_by_name(name).await? {
678 return Ok((agent, "community".to_string()));
679 }
680
681 if let Some(config) = state.dynamic_config.agent(name) {
683 let agent = UserAgent {
685 id: format!("system-{}", name),
686 user_id: "system".to_string(),
687 name: config.name,
688 display_name: None,
689 description: None,
690 model: config.model,
691 system_prompt: config.system_prompt,
692 tools: serde_json::to_string(&config.tools).unwrap_or_else(|_| "[]".to_string()),
693 max_tool_iterations: config.max_tool_iterations as i32,
694 parallel_tools: config.parallel_tools,
695 extra: "{}".to_string(),
696 is_public: true, usage_count: 0,
698 rating_sum: 0,
699 rating_count: 0,
700 created_at: 0,
701 updated_at: 0,
702 };
703 return Ok((agent, "system".to_string()));
704 }
705
706 if let Some(config) = state.config_manager.config().get_agent(name) {
708 let agent = UserAgent {
709 id: format!("system-{}", name),
710 user_id: "system".to_string(),
711 name: name.to_string(),
712 display_name: None,
713 description: None,
714 model: config.model.clone(),
715 system_prompt: config.system_prompt.clone(),
716 tools: serde_json::to_string(&config.tools).unwrap_or_else(|_| "[]".to_string()),
717 max_tool_iterations: config.max_tool_iterations as i32,
718 parallel_tools: config.parallel_tools,
719 extra: "{}".to_string(),
720 is_public: true,
721 usage_count: 0,
722 rating_sum: 0,
723 rating_count: 0,
724 created_at: 0,
725 updated_at: 0,
726 };
727 return Ok((agent, "system".to_string()));
728 }
729
730 Err(AppError::NotFound(format!("Agent '{}' not found", name)))
731}