Skip to main content

systemprompt_models/services/
mod.rs

1pub 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}