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