systemprompt_models/services/
mod.rs1pub mod agent_config;
2pub mod ai;
3pub mod runtime;
4pub mod scheduler;
5pub mod settings;
6pub mod skills;
7pub mod web;
8
9pub use agent_config::*;
10pub use ai::{
11 AiConfig, AiProviderConfig, HistoryConfig, McpConfig, ModelCapabilities, ModelDefinition,
12 ModelLimits, ModelPricing, SamplingConfig, ToolModelConfig, ToolModelSettings,
13};
14pub use runtime::{RuntimeStatus, ServiceType};
15pub use scheduler::*;
16pub use settings::*;
17pub use skills::{SkillConfig, SkillsConfig};
18pub use web::{BrandingConfig, WebConfig};
19
20use crate::mcp::Deployment;
21use serde::{Deserialize, Deserializer, Serialize};
22use std::collections::HashMap;
23
24#[derive(Debug, Clone, Serialize)]
25#[serde(untagged)]
26pub enum IncludableString {
27 Inline(String),
28 Include { path: String },
29}
30
31impl<'de> Deserialize<'de> for IncludableString {
32 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33 where
34 D: Deserializer<'de>,
35 {
36 let s = String::deserialize(deserializer)?;
37 s.strip_prefix("!include ")
38 .map_or_else(
39 || Self::Inline(s.clone()),
40 |path| Self::Include {
41 path: path.trim().to_string(),
42 },
43 )
44 .pipe(Ok)
45 }
46}
47
48trait Pipe: Sized {
49 fn pipe<T>(self, f: impl FnOnce(Self) -> T) -> T {
50 f(self)
51 }
52}
53impl<T> Pipe for T {}
54
55impl IncludableString {
56 pub const fn is_include(&self) -> bool {
57 matches!(self, Self::Include { .. })
58 }
59
60 pub fn as_inline(&self) -> Option<&str> {
61 match self {
62 Self::Inline(s) => Some(s),
63 Self::Include { .. } => None,
64 }
65 }
66}
67
68impl Default for IncludableString {
69 fn default() -> Self {
70 Self::Inline(String::new())
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct PartialServicesConfig {
76 #[serde(default)]
77 pub agents: HashMap<String, AgentConfig>,
78 #[serde(default)]
79 pub mcp_servers: HashMap<String, Deployment>,
80 #[serde(default)]
81 pub scheduler: Option<SchedulerConfig>,
82 #[serde(default)]
83 pub ai: Option<AiConfig>,
84 #[serde(default)]
85 pub web: Option<WebConfig>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ServicesConfig {
90 #[serde(default)]
91 pub agents: HashMap<String, AgentConfig>,
92 #[serde(default)]
93 pub mcp_servers: HashMap<String, Deployment>,
94 #[serde(default)]
95 pub settings: Settings,
96 #[serde(default)]
97 pub scheduler: Option<SchedulerConfig>,
98 #[serde(default)]
99 pub ai: AiConfig,
100 #[serde(default)]
101 pub web: WebConfig,
102}
103
104impl ServicesConfig {
105 pub fn validate(&self) -> anyhow::Result<()> {
106 self.validate_port_conflicts()?;
107 self.validate_port_ranges()?;
108 self.validate_mcp_port_ranges()?;
109 self.validate_single_default_agent()?;
110
111 for (name, agent) in &self.agents {
112 agent.validate(name)?;
113 }
114
115 Ok(())
116 }
117
118 fn validate_port_conflicts(&self) -> anyhow::Result<()> {
119 let mut seen_ports = HashMap::new();
120
121 for (name, agent) in &self.agents {
122 if let Some(existing) = seen_ports.insert(agent.port, ("agent", name.as_str())) {
123 anyhow::bail!(
124 "Port conflict: {} used by both {} '{}' and agent '{}'",
125 agent.port,
126 existing.0,
127 existing.1,
128 name
129 );
130 }
131 }
132
133 for (name, mcp) in &self.mcp_servers {
134 if let Some(existing) = seen_ports.insert(mcp.port, ("mcp_server", name.as_str())) {
135 anyhow::bail!(
136 "Port conflict: {} used by both {} '{}' and mcp_server '{}'",
137 mcp.port,
138 existing.0,
139 existing.1,
140 name
141 );
142 }
143 }
144
145 Ok(())
146 }
147
148 fn validate_port_ranges(&self) -> anyhow::Result<()> {
149 let (min, max) = self.settings.agent_port_range;
150
151 for (name, agent) in &self.agents {
152 if agent.port < min || agent.port > max {
153 anyhow::bail!(
154 "Agent '{}' port {} is outside allowed range {}-{}",
155 name,
156 agent.port,
157 min,
158 max
159 );
160 }
161 }
162
163 Ok(())
164 }
165
166 fn validate_mcp_port_ranges(&self) -> anyhow::Result<()> {
167 let (min, max) = self.settings.mcp_port_range;
168
169 for (name, mcp) in &self.mcp_servers {
170 if mcp.port < min || mcp.port > max {
171 anyhow::bail!(
172 "MCP server '{}' port {} is outside allowed range {}-{}",
173 name,
174 mcp.port,
175 min,
176 max
177 );
178 }
179 }
180
181 Ok(())
182 }
183
184 fn validate_single_default_agent(&self) -> anyhow::Result<()> {
185 let default_agents: Vec<&str> = self
186 .agents
187 .iter()
188 .filter_map(|(name, agent)| {
189 if agent.default {
190 Some(name.as_str())
191 } else {
192 None
193 }
194 })
195 .collect();
196
197 match default_agents.len() {
198 0 | 1 => Ok(()),
199 _ => anyhow::bail!(
200 "Multiple agents marked as default: {}. Only one agent can have 'default: true'",
201 default_agents.join(", ")
202 ),
203 }
204 }
205}