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};
19
20#[derive(Debug, Deserialize)]
24pub struct CreateAgentRequest {
25 pub name: String,
26 #[serde(default)]
27 pub display_name: Option<String>,
28 #[serde(default)]
29 pub description: Option<String>,
30 pub model: String,
31 #[serde(default)]
32 pub system_prompt: Option<String>,
33 #[serde(default)]
34 pub tools: Vec<String>,
35 #[serde(default = "default_max_tool_iterations")]
36 pub max_tool_iterations: i32,
37 #[serde(default)]
38 pub parallel_tools: bool,
39 #[serde(default)]
40 pub is_public: bool,
41}
42
43fn default_max_tool_iterations() -> i32 {
44 10
45}
46
47#[derive(Debug, Serialize)]
49pub struct CreateAgentResponse {
50 pub id: String,
51 pub name: String,
52 pub display_name: Option<String>,
53 pub created_at: i64,
54 pub api_endpoint: String,
55 pub toon_preview: String,
57}
58
59#[derive(Debug, Serialize)]
61pub struct AgentResponse {
62 pub id: String,
63 pub name: String,
64 pub display_name: Option<String>,
65 pub description: Option<String>,
66 pub model: String,
67 pub system_prompt: Option<String>,
68 pub tools: Vec<String>,
69 pub max_tool_iterations: i32,
70 pub parallel_tools: bool,
71 pub is_public: bool,
72 pub usage_count: i32,
73 pub average_rating: Option<f32>,
74 pub created_at: i64,
75 pub updated_at: i64,
76 pub source: String,
78}
79
80#[derive(Debug, Deserialize, Default)]
82pub struct GetAgentQuery {
83 #[serde(default)]
85 pub format: Option<String>,
86}
87
88#[derive(Debug, Deserialize, Default)]
90pub struct ListAgentsQuery {
91 #[serde(default)]
93 pub include_public: bool,
94 #[serde(default = "default_limit")]
96 pub limit: u32,
97 #[serde(default)]
99 pub offset: u32,
100}
101
102fn default_limit() -> u32 {
103 50
104}
105
106#[derive(Debug, Deserialize)]
108pub struct UpdateAgentRequest {
109 #[serde(default)]
110 pub display_name: Option<String>,
111 #[serde(default)]
112 pub description: Option<String>,
113 #[serde(default)]
114 pub model: Option<String>,
115 #[serde(default)]
116 pub system_prompt: Option<String>,
117 #[serde(default)]
118 pub tools: Option<Vec<String>>,
119 #[serde(default)]
120 pub max_tool_iterations: Option<i32>,
121 #[serde(default)]
122 pub parallel_tools: Option<bool>,
123 #[serde(default)]
124 pub is_public: Option<bool>,
125}
126
127pub async fn create_agent(
133 State(state): State<AppState>,
134 user: AuthUser,
135 Json(req): Json<CreateAgentRequest>,
136) -> Result<Json<CreateAgentResponse>> {
137 if !is_valid_agent_name(&req.name) {
139 return Err(AppError::InvalidInput(
140 "Agent name must be alphanumeric with hyphens and underscores only".to_string(),
141 ));
142 }
143
144 if state
146 .turso
147 .get_user_agent_by_name(&user.0.sub, &req.name)
148 .await?
149 .is_some()
150 {
151 return Err(AppError::InvalidInput(format!(
152 "Agent '{}' already exists",
153 req.name
154 )));
155 }
156
157 let model_exists = state.dynamic_config.model(&req.model).is_some()
159 || state
160 .config_manager
161 .config()
162 .get_model(&req.model)
163 .is_some();
164
165 if !model_exists {
166 return Err(AppError::InvalidInput(format!(
167 "Model '{}' not found. Available models: {:?}",
168 req.model,
169 state.dynamic_config.model_names()
170 )));
171 }
172
173 for tool in &req.tools {
175 let tool_exists = state.dynamic_config.tool(tool).is_some()
176 || state.config_manager.config().get_tool(tool).is_some();
177
178 if !tool_exists {
179 return Err(AppError::InvalidInput(format!(
180 "Tool '{}' not found. Available tools: {:?}",
181 tool,
182 state.dynamic_config.tool_names()
183 )));
184 }
185 }
186
187 let agent_config = ToonAgentConfig {
189 name: req.name.clone(),
190 model: req.model.clone(),
191 system_prompt: req.system_prompt.clone(),
192 tools: req.tools.clone(),
193 max_tool_iterations: req.max_tool_iterations as usize,
194 parallel_tools: req.parallel_tools,
195 extra: std::collections::HashMap::new(),
196 };
197
198 let toon_preview = encode_default(&agent_config)
200 .map_err(|e| AppError::Internal(format!("TOON encode error: {}", e)))?;
201
202 let id = uuid::Uuid::new_v4().to_string();
203 let now = Utc::now().timestamp();
204
205 let agent = UserAgent {
206 id: id.clone(),
207 user_id: user.0.sub.clone(),
208 name: req.name.clone(),
209 display_name: req.display_name.clone(),
210 description: req.description,
211 model: req.model,
212 system_prompt: req.system_prompt,
213 tools: serde_json::to_string(&req.tools).unwrap_or_else(|_| "[]".to_string()),
214 max_tool_iterations: req.max_tool_iterations,
215 parallel_tools: req.parallel_tools,
216 extra: "{}".to_string(),
217 is_public: req.is_public,
218 usage_count: 0,
219 rating_sum: 0,
220 rating_count: 0,
221 created_at: now,
222 updated_at: now,
223 };
224
225 state.turso.create_user_agent(&agent).await?;
226
227 Ok(Json(CreateAgentResponse {
228 id,
229 name: req.name.clone(),
230 display_name: req.display_name,
231 created_at: now,
232 api_endpoint: format!("/api/user/agents/{}/chat", req.name),
233 toon_preview,
234 }))
235}
236
237pub async fn import_agent_toon(
242 State(state): State<AppState>,
243 user: AuthUser,
244 body: String,
245) -> Result<Json<CreateAgentResponse>> {
246 let agent_config: ToonAgentConfig = decode_default(&body)
248 .map_err(|e| AppError::InvalidInput(format!("Invalid TOON format: {}", e)))?;
249
250 let req = CreateAgentRequest {
252 name: agent_config.name,
253 display_name: None,
254 description: None,
255 model: agent_config.model,
256 system_prompt: agent_config.system_prompt,
257 tools: agent_config.tools,
258 max_tool_iterations: agent_config.max_tool_iterations as i32,
259 parallel_tools: agent_config.parallel_tools,
260 is_public: false,
261 };
262
263 create_agent(State(state), user, Json(req)).await
264}
265
266pub async fn get_agent(
270 State(state): State<AppState>,
271 Path(name): Path<String>,
272 Query(params): Query<GetAgentQuery>,
273 user: AuthUser,
274) -> Result<Response> {
275 let (agent, source) = resolve_agent(&state, &user.0.sub, &name).await?;
277
278 match params.format.as_deref() {
279 Some("toon") => {
280 let config = ToonAgentConfig {
282 name: agent.name.clone(),
283 model: agent.model.clone(),
284 system_prompt: agent.system_prompt.clone(),
285 tools: agent.tools_vec(),
286 max_tool_iterations: agent.max_tool_iterations as usize,
287 parallel_tools: agent.parallel_tools,
288 extra: std::collections::HashMap::new(),
289 };
290
291 let toon = encode_default(&config)
292 .map_err(|e| AppError::Internal(format!("TOON encode error: {}", e)))?;
293
294 Ok(([(header::CONTENT_TYPE, "text/x-toon")], toon).into_response())
295 }
296 _ => {
297 let tools = agent.tools_vec();
298 let avg_rating = agent.average_rating();
299 Ok(Json(AgentResponse {
300 id: agent.id,
301 name: agent.name,
302 display_name: agent.display_name,
303 description: agent.description,
304 model: agent.model,
305 system_prompt: agent.system_prompt,
306 tools,
307 max_tool_iterations: agent.max_tool_iterations,
308 parallel_tools: agent.parallel_tools,
309 is_public: agent.is_public,
310 usage_count: agent.usage_count,
311 average_rating: avg_rating,
312 created_at: agent.created_at,
313 updated_at: agent.updated_at,
314 source,
315 })
316 .into_response())
317 }
318 }
319}
320
321pub async fn list_agents(
325 State(state): State<AppState>,
326 Query(params): Query<ListAgentsQuery>,
327 user: AuthUser,
328) -> Result<Json<Vec<AgentResponse>>> {
329 let mut agents = Vec::new();
330
331 let user_agents = state.turso.list_user_agents(&user.0.sub).await?;
333 for agent in user_agents {
334 agents.push(user_agent_to_response(agent, "user".to_string()));
335 }
336
337 if params.include_public {
339 let public_agents = state
340 .turso
341 .list_public_agents(params.limit, params.offset)
342 .await?;
343
344 for agent in public_agents {
345 if agents.iter().any(|a| a.name == agent.name) {
347 continue;
348 }
349
350 agents.push(user_agent_to_response(agent, "community".to_string()));
351 }
352 }
353
354 Ok(Json(agents))
355}
356
357pub async fn update_agent(
361 State(state): State<AppState>,
362 Path(name): Path<String>,
363 user: AuthUser,
364 Json(req): Json<UpdateAgentRequest>,
365) -> Result<Json<AgentResponse>> {
366 let mut agent = state
368 .turso
369 .get_user_agent_by_name(&user.0.sub, &name)
370 .await?
371 .ok_or_else(|| AppError::NotFound(format!("Agent '{}' not found", name)))?;
372
373 if agent.user_id != user.0.sub {
375 return Err(AppError::Auth(
376 "You can only update your own agents".to_string(),
377 ));
378 }
379
380 if let Some(display_name) = req.display_name {
382 agent.display_name = Some(display_name);
383 }
384 if let Some(description) = req.description {
385 agent.description = Some(description);
386 }
387 if let Some(model) = req.model {
388 let model_exists = state.dynamic_config.model(&model).is_some()
390 || state.config_manager.config().get_model(&model).is_some();
391 if !model_exists {
392 return Err(AppError::InvalidInput(format!(
393 "Model '{}' not found",
394 model
395 )));
396 }
397 agent.model = model;
398 }
399 if let Some(system_prompt) = req.system_prompt {
400 agent.system_prompt = Some(system_prompt);
401 }
402 if let Some(tools) = req.tools {
403 for tool in &tools {
405 let tool_exists = state.dynamic_config.tool(tool).is_some()
406 || state.config_manager.config().get_tool(tool).is_some();
407 if !tool_exists {
408 return Err(AppError::InvalidInput(format!("Tool '{}' not found", tool)));
409 }
410 }
411 agent.set_tools(tools);
412 }
413 if let Some(max_tool_iterations) = req.max_tool_iterations {
414 agent.max_tool_iterations = max_tool_iterations;
415 }
416 if let Some(parallel_tools) = req.parallel_tools {
417 agent.parallel_tools = parallel_tools;
418 }
419 if let Some(is_public) = req.is_public {
420 agent.is_public = is_public;
421 }
422
423 agent.updated_at = Utc::now().timestamp();
424
425 state.turso.update_user_agent(&agent).await?;
426
427 Ok(Json(user_agent_to_response(agent, "user".to_string())))
428}
429
430pub async fn delete_agent(
434 State(state): State<AppState>,
435 Path(name): Path<String>,
436 user: AuthUser,
437) -> Result<StatusCode> {
438 let agent = state
440 .turso
441 .get_user_agent_by_name(&user.0.sub, &name)
442 .await?
443 .ok_or_else(|| AppError::NotFound(format!("Agent '{}' not found", name)))?;
444
445 let deleted = state
447 .turso
448 .delete_user_agent(&agent.id, &user.0.sub)
449 .await?;
450
451 if deleted {
452 Ok(StatusCode::NO_CONTENT)
453 } else {
454 Err(AppError::NotFound(format!("Agent '{}' not found", name)))
455 }
456}
457
458pub async fn export_agent_toon(
462 State(state): State<AppState>,
463 Path(name): Path<String>,
464 user: AuthUser,
465) -> Result<Response> {
466 let (agent, _) = resolve_agent(&state, &user.0.sub, &name).await?;
467
468 let tools = agent.tools_vec();
469 let filename = format!("{}.toon", agent.name);
470 let config = ToonAgentConfig {
471 name: agent.name,
472 model: agent.model,
473 system_prompt: agent.system_prompt,
474 tools,
475 max_tool_iterations: agent.max_tool_iterations as usize,
476 parallel_tools: agent.parallel_tools,
477 extra: std::collections::HashMap::new(),
478 };
479
480 let toon = encode_default(&config)
481 .map_err(|e| AppError::Internal(format!("TOON encode error: {}", e)))?;
482
483 Ok((
484 [
485 (header::CONTENT_TYPE, "text/x-toon"),
486 (
487 header::CONTENT_DISPOSITION,
488 &format!("attachment; filename=\"{}\"", filename),
489 ),
490 ],
491 toon,
492 )
493 .into_response())
494}
495
496fn is_valid_agent_name(name: &str) -> bool {
500 !name.is_empty()
501 && name.len() <= 64
502 && name
503 .chars()
504 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
505}
506
507fn user_agent_to_response(agent: UserAgent, source: String) -> AgentResponse {
509 let tools = agent.tools_vec();
510 let avg_rating = agent.average_rating();
511 AgentResponse {
512 id: agent.id,
513 name: agent.name,
514 display_name: agent.display_name,
515 description: agent.description,
516 model: agent.model,
517 system_prompt: agent.system_prompt,
518 tools,
519 max_tool_iterations: agent.max_tool_iterations,
520 parallel_tools: agent.parallel_tools,
521 is_public: agent.is_public,
522 usage_count: agent.usage_count,
523 average_rating: avg_rating,
524 created_at: agent.created_at,
525 updated_at: agent.updated_at,
526 source,
527 }
528}
529
530pub async fn resolve_agent(
533 state: &AppState,
534 user_id: &str,
535 name: &str,
536) -> Result<(UserAgent, String)> {
537 if let Some(agent) = state.turso.get_user_agent_by_name(user_id, name).await? {
539 return Ok((agent, "user".to_string()));
540 }
541
542 if let Some(agent) = state.turso.get_public_agent_by_name(name).await? {
544 return Ok((agent, "community".to_string()));
545 }
546
547 if let Some(config) = state.dynamic_config.agent(name) {
549 let agent = UserAgent {
551 id: format!("system-{}", name),
552 user_id: "system".to_string(),
553 name: config.name,
554 display_name: None,
555 description: None,
556 model: config.model,
557 system_prompt: config.system_prompt,
558 tools: serde_json::to_string(&config.tools).unwrap_or_else(|_| "[]".to_string()),
559 max_tool_iterations: config.max_tool_iterations as i32,
560 parallel_tools: config.parallel_tools,
561 extra: "{}".to_string(),
562 is_public: true, usage_count: 0,
564 rating_sum: 0,
565 rating_count: 0,
566 created_at: 0,
567 updated_at: 0,
568 };
569 return Ok((agent, "system".to_string()));
570 }
571
572 if let Some(config) = state.config_manager.config().get_agent(name) {
574 let agent = UserAgent {
575 id: format!("system-{}", name),
576 user_id: "system".to_string(),
577 name: name.to_string(),
578 display_name: None,
579 description: None,
580 model: config.model.clone(),
581 system_prompt: config.system_prompt.clone(),
582 tools: serde_json::to_string(&config.tools).unwrap_or_else(|_| "[]".to_string()),
583 max_tool_iterations: config.max_tool_iterations as i32,
584 parallel_tools: config.parallel_tools,
585 extra: "{}".to_string(),
586 is_public: true,
587 usage_count: 0,
588 rating_sum: 0,
589 rating_count: 0,
590 created_at: 0,
591 updated_at: 0,
592 };
593 return Ok((agent, "system".to_string()));
594 }
595
596 Err(AppError::NotFound(format!("Agent '{}' not found", name)))
597}