systemprompt_models/services/agent_config/
disk.rs1use serde::Deserialize;
5use systemprompt_identifiers::AgentId;
6
7use super::card::{AgentCardConfig, AgentMetadataConfig, OAuthConfig, default_true};
8use super::{AgentConfig, DEFAULT_AGENT_SYSTEM_PROMPT_FILE};
9use crate::errors::ConfigValidationError;
10
11fn default_version() -> String {
12 "1.0.0".to_string()
13}
14
15#[derive(Debug, Clone, Deserialize)]
16pub struct DiskAgentConfig {
17 #[serde(default)]
18 pub id: Option<AgentId>,
19 pub name: String,
20 pub display_name: String,
21 pub description: String,
22 #[serde(default = "default_version")]
23 pub version: String,
24 #[serde(default = "default_true")]
25 pub enabled: bool,
26 pub port: u16,
27 #[serde(default)]
28 pub endpoint: Option<String>,
29 #[serde(default)]
30 pub dev_only: bool,
31 #[serde(default)]
32 pub is_primary: bool,
33 #[serde(default)]
34 pub default: bool,
35 #[serde(default)]
36 pub system_prompt_file: Option<String>,
37 #[serde(default)]
38 pub tags: Vec<String>,
39 #[serde(default)]
40 pub category: Option<String>,
41 #[serde(default)]
42 pub mcp_servers: Vec<String>,
43 #[serde(default)]
44 pub skills: Vec<String>,
45 #[serde(default)]
46 pub provider: Option<String>,
47 #[serde(default)]
48 pub model: Option<String>,
49 pub card: AgentCardConfig,
50 #[serde(default)]
51 pub oauth: OAuthConfig,
52}
53
54impl DiskAgentConfig {
55 #[must_use]
56 pub fn system_prompt_file(&self) -> &str {
57 self.system_prompt_file
58 .as_deref()
59 .filter(|s| !s.is_empty())
60 .unwrap_or(DEFAULT_AGENT_SYSTEM_PROMPT_FILE)
61 }
62
63 #[must_use]
64 pub fn to_agent_config(&self, base_url: &str, system_prompt: Option<String>) -> AgentConfig {
65 let endpoint = self.endpoint.clone().unwrap_or_else(|| {
66 format!(
67 "{}/api/v1/agents/{}",
68 base_url.trim_end_matches('/'),
69 self.name
70 )
71 });
72
73 let card_name = self
74 .card
75 .name
76 .clone()
77 .unwrap_or_else(|| self.display_name.clone());
78
79 AgentConfig {
80 name: self.name.clone(),
81 port: self.port,
82 endpoint,
83 tags: self.tags.clone(),
84 enabled: self.enabled,
85 dev_only: self.dev_only,
86 is_primary: self.is_primary,
87 default: self.default,
88 card: AgentCardConfig {
89 name: Some(card_name),
90 ..self.card.clone()
91 },
92 metadata: AgentMetadataConfig {
93 system_prompt,
94 mcp_servers: self.mcp_servers.clone(),
95 skills: self.skills.clone(),
96 provider: self.provider.clone(),
97 model: self.model.clone(),
98 ..Default::default()
99 },
100 oauth: self.oauth.clone(),
101 }
102 }
103
104 pub fn validate(&self, dir_name: &str) -> Result<(), ConfigValidationError> {
105 if let Some(id) = &self.id
106 && id.as_str() != dir_name
107 {
108 return Err(ConfigValidationError::invalid_field(format!(
109 "Agent config id '{id}' does not match directory name '{dir_name}'"
110 )));
111 }
112
113 if !self
114 .name
115 .chars()
116 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
117 {
118 return Err(ConfigValidationError::invalid_field(format!(
119 "Agent name '{}' must be lowercase alphanumeric with underscores only",
120 self.name
121 )));
122 }
123
124 if self.name.len() < 3 || self.name.len() > 50 {
125 return Err(ConfigValidationError::invalid_field(format!(
126 "Agent name '{}' must be between 3 and 50 characters",
127 self.name
128 )));
129 }
130
131 if self.port == 0 {
132 return Err(ConfigValidationError::invalid_field(format!(
133 "Agent '{}' has invalid port {}",
134 self.name, self.port
135 )));
136 }
137
138 if self.display_name.is_empty() {
139 return Err(ConfigValidationError::required(format!(
140 "Agent '{}' display_name must not be empty",
141 self.name
142 )));
143 }
144
145 Ok(())
146 }
147}