systemprompt_agent/services/config_authoring/
edit.rs1use std::fs;
13
14use systemprompt_models::services::{AgentConfig, ServicesConfig};
15
16use super::{AgentConfigAuthoringService, ConfigAuthoringError};
17
18#[derive(Debug, Clone, Default)]
19pub struct AgentEditRequest {
20 pub enable: bool,
21 pub disable: bool,
22 pub port: Option<u16>,
23 pub endpoint: Option<String>,
24 pub dev_only: bool,
25 pub is_primary: bool,
26 pub default: bool,
27 pub display_name: Option<String>,
28 pub description: Option<String>,
29 pub version: Option<String>,
30 pub icon_url: Option<String>,
31 pub documentation_url: Option<String>,
32 pub streaming: Option<bool>,
33 pub push_notifications: Option<bool>,
34 pub state_transition_history: Option<bool>,
35 pub provider: Option<String>,
36 pub model: Option<String>,
37 pub system_prompt: Option<String>,
38 pub system_prompt_file: Option<String>,
39 pub mcp_servers: Vec<String>,
40 pub remove_mcp_servers: Vec<String>,
41 pub skills: Vec<String>,
42 pub remove_skills: Vec<String>,
43 pub set_values: Vec<String>,
44}
45
46impl AgentConfigAuthoringService {
47 pub fn apply_enabled_flags(
48 agent: &mut AgentConfig,
49 request: &AgentEditRequest,
50 changes: &mut Vec<String>,
51 ) {
52 if request.enable {
53 agent.enabled = true;
54 changes.push("enabled: true".to_owned());
55 }
56 if request.disable {
57 agent.enabled = false;
58 changes.push("enabled: false".to_owned());
59 }
60 }
61
62 pub fn apply_runtime_fields(
63 agent: &mut AgentConfig,
64 request: &AgentEditRequest,
65 changes: &mut Vec<String>,
66 ) -> Result<(), ConfigAuthoringError> {
67 if let Some(port) = request.port {
68 Self::validate_port(port)?;
69 agent.port = port;
70 changes.push(format!("port: {port}"));
71 }
72 if let Some(endpoint) = &request.endpoint {
73 agent.endpoint.clone_from(endpoint);
74 changes.push(format!("endpoint: {endpoint}"));
75 }
76 if request.dev_only {
77 agent.dev_only = true;
78 changes.push("dev_only: true".to_owned());
79 }
80 if request.is_primary {
81 agent.is_primary = true;
82 changes.push("is_primary: true".to_owned());
83 }
84 if request.default {
85 agent.default = true;
86 changes.push("default: true".to_owned());
87 }
88 Ok(())
89 }
90
91 pub fn apply_card_fields(
92 agent: &mut AgentConfig,
93 request: &AgentEditRequest,
94 changes: &mut Vec<String>,
95 ) {
96 if let Some(display_name) = &request.display_name {
97 agent.card.display_name.clone_from(display_name);
98 changes.push(format!("card.display_name: {display_name}"));
99 }
100 if let Some(description) = &request.description {
101 agent.card.description.clone_from(description);
102 changes.push(format!("card.description: {description}"));
103 }
104 if let Some(version) = &request.version {
105 agent.card.version.clone_from(version);
106 changes.push(format!("card.version: {version}"));
107 }
108 if let Some(icon_url) = &request.icon_url {
109 agent.card.icon_url = Some(icon_url.clone());
110 changes.push(format!("card.icon_url: {icon_url}"));
111 }
112 if let Some(documentation_url) = &request.documentation_url {
113 agent.card.documentation_url = Some(documentation_url.clone());
114 changes.push(format!("card.documentation_url: {documentation_url}"));
115 }
116 }
117
118 pub fn apply_capability_fields(
119 agent: &mut AgentConfig,
120 request: &AgentEditRequest,
121 changes: &mut Vec<String>,
122 ) {
123 if let Some(streaming) = request.streaming {
124 agent.card.capabilities.streaming = streaming;
125 changes.push(format!("card.capabilities.streaming: {streaming}"));
126 }
127 if let Some(push_notifications) = request.push_notifications {
128 agent.card.capabilities.push_notifications = push_notifications;
129 changes.push(format!(
130 "card.capabilities.push_notifications: {push_notifications}"
131 ));
132 }
133 if let Some(state_transition_history) = request.state_transition_history {
134 agent.card.capabilities.state_transition_history = state_transition_history;
135 changes.push(format!(
136 "card.capabilities.state_transition_history: {state_transition_history}"
137 ));
138 }
139 }
140
141 pub fn apply_metadata_fields(
142 agent: &mut AgentConfig,
143 request: &AgentEditRequest,
144 changes: &mut Vec<String>,
145 ) -> Result<(), ConfigAuthoringError> {
146 if let Some(provider) = &request.provider {
147 agent.metadata.provider = Some(provider.clone());
148 changes.push(format!("metadata.provider: {provider}"));
149 }
150 if let Some(model) = &request.model {
151 agent.metadata.model = Some(model.clone());
152 changes.push(format!("metadata.model: {model}"));
153 }
154 if let Some(file_path) = &request.system_prompt_file {
155 let content = fs::read_to_string(file_path).map_err(|source| {
156 ConfigAuthoringError::SystemPromptFile {
157 path: file_path.clone(),
158 source,
159 }
160 })?;
161 agent.metadata.system_prompt = Some(content.clone());
162 changes.push(format!(
163 "system_prompt: loaded from {} ({} chars)",
164 file_path,
165 content.len()
166 ));
167 } else if let Some(prompt) = &request.system_prompt {
168 agent.metadata.system_prompt = Some(prompt.clone());
169 changes.push(format!("system_prompt: {} chars", prompt.len()));
170 }
171 Ok(())
172 }
173
174 pub fn apply_mcp_server_changes(
175 agent: &mut AgentConfig,
176 request: &AgentEditRequest,
177 services_config: &ServicesConfig,
178 changes: &mut Vec<String>,
179 ) -> Result<Vec<String>, ConfigAuthoringError> {
180 for mcp_server in &request.mcp_servers {
181 if agent.metadata.mcp_servers.include.contains(mcp_server) {
182 continue;
183 }
184 if !services_config.mcp_servers.contains_key(mcp_server) {
185 return Err(ConfigAuthoringError::UnknownMcpServer {
186 name: mcp_server.clone(),
187 available: services_config
188 .mcp_servers
189 .keys()
190 .cloned()
191 .collect::<Vec<_>>()
192 .join(", "),
193 });
194 }
195 agent.metadata.mcp_servers.include.push(mcp_server.clone());
196 changes.push(format!("added mcp_server: {mcp_server}"));
197 }
198 let mut skipped = Vec::new();
199 for mcp_server in &request.remove_mcp_servers {
200 if let Some(pos) = agent
201 .metadata
202 .mcp_servers
203 .include
204 .iter()
205 .position(|s| s == mcp_server)
206 {
207 agent.metadata.mcp_servers.include.remove(pos);
208 changes.push(format!("removed mcp_server: {mcp_server}"));
209 } else {
210 skipped.push(mcp_server.clone());
211 }
212 }
213 Ok(skipped)
214 }
215
216 pub fn apply_skill_changes(
217 agent: &mut AgentConfig,
218 request: &AgentEditRequest,
219 changes: &mut Vec<String>,
220 ) -> Vec<String> {
221 for skill in &request.skills {
222 if !agent.metadata.skills.include.contains(skill) {
223 agent.metadata.skills.include.push(skill.clone());
224 changes.push(format!("added skill: {skill}"));
225 }
226 }
227 let mut skipped = Vec::new();
228 for skill in &request.remove_skills {
229 if let Some(pos) = agent
230 .metadata
231 .skills
232 .include
233 .iter()
234 .position(|s| s == skill)
235 {
236 let removed = agent.metadata.skills.include.remove(pos);
237 changes.push(format!("removed skill: {removed}"));
238 } else {
239 skipped.push(skill.clone());
240 }
241 }
242 skipped
243 }
244
245 pub fn apply_set_value_changes(
246 agent: &mut AgentConfig,
247 request: &AgentEditRequest,
248 changes: &mut Vec<String>,
249 ) -> Result<(), ConfigAuthoringError> {
250 for set_value in &request.set_values {
251 let Some((key, value)) = set_value.split_once('=') else {
252 return Err(ConfigAuthoringError::InvalidSetFormat(set_value.clone()));
253 };
254 apply_set_value(agent, key, value)?;
255 changes.push(format!("{key}: {value}"));
256 }
257 Ok(())
258 }
259}
260
261fn apply_set_value(
262 agent: &mut AgentConfig,
263 key: &str,
264 value: &str,
265) -> Result<(), ConfigAuthoringError> {
266 match key {
267 "card.displayName" | "card.display_name" => {
268 value.clone_into(&mut agent.card.display_name);
269 },
270 "card.description" => {
271 value.clone_into(&mut agent.card.description);
272 },
273 "card.version" => {
274 value.clone_into(&mut agent.card.version);
275 },
276 "endpoint" => {
277 value.clone_into(&mut agent.endpoint);
278 },
279 "is_primary" => {
280 agent.is_primary = parse_bool(key, value)?;
281 },
282 "default" => {
283 agent.default = parse_bool(key, value)?;
284 },
285 "dev_only" => {
286 agent.dev_only = parse_bool(key, value)?;
287 },
288 _ => {
289 return Err(ConfigAuthoringError::UnknownSetKey(key.to_owned()));
290 },
291 }
292 Ok(())
293}
294
295fn parse_bool(key: &str, value: &str) -> Result<bool, ConfigAuthoringError> {
296 value
297 .parse()
298 .map_err(|_e| ConfigAuthoringError::InvalidBoolean {
299 key: key.to_owned(),
300 value: value.to_owned(),
301 })
302}