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