1use serde::Deserialize;
3
4use super::SensorModality;
5use super::agent::McpServerEntry;
6
7#[derive(Debug, Clone, Deserialize)]
9pub struct SensorConfig {
10 #[serde(default = "super::default_true")]
12 pub enabled: bool,
13 #[serde(default)]
15 pub routing: Option<SensorRoutingConfig>,
16 #[serde(default)]
18 pub salience: Option<SalienceConfig>,
19 #[serde(default)]
21 pub token_budget: Option<TokenBudgetConfig>,
22 #[serde(default)]
24 pub stories: Option<StoryCorrelationConfig>,
25 #[serde(default)]
27 pub sources: Vec<SensorSourceConfig>,
28}
29
30#[derive(Debug, Clone, Deserialize)]
32pub struct SensorRoutingConfig {
33 #[serde(default = "default_triage_model")]
35 pub triage_model: String,
36 pub local_model_path: Option<String>,
38 #[serde(default = "default_confidence_threshold")]
40 pub confidence_threshold: f64,
41}
42
43fn default_triage_model() -> String {
44 "cloud_light".into()
45}
46
47fn default_confidence_threshold() -> f64 {
48 0.85
49}
50
51#[derive(Debug, Clone, Deserialize)]
53pub struct SalienceConfig {
54 #[serde(default = "default_urgency_weight")]
56 pub urgency_weight: f64,
57 #[serde(default = "default_novelty_weight")]
59 pub novelty_weight: f64,
60 #[serde(default = "default_relevance_weight")]
62 pub relevance_weight: f64,
63 #[serde(default = "default_salience_threshold")]
65 pub threshold: f64,
66}
67
68fn default_urgency_weight() -> f64 {
69 0.3
70}
71
72fn default_novelty_weight() -> f64 {
73 0.3
74}
75
76fn default_relevance_weight() -> f64 {
77 0.4
78}
79
80fn default_salience_threshold() -> f64 {
81 0.3
82}
83
84#[derive(Debug, Clone, Deserialize)]
86pub struct TokenBudgetConfig {
87 #[serde(default = "default_hourly_limit")]
89 pub hourly_limit: usize,
90 #[serde(default = "default_queue_size")]
92 pub queue_size: usize,
93}
94
95fn default_hourly_limit() -> usize {
96 100_000
97}
98
99fn default_queue_size() -> usize {
100 200
101}
102
103#[derive(Debug, Clone, Deserialize)]
105pub struct StoryCorrelationConfig {
106 #[serde(default = "default_correlation_window_hours")]
108 pub correlation_window_hours: u64,
109 #[serde(default = "default_max_events_per_story")]
111 pub max_events_per_story: usize,
112 #[serde(default = "default_stale_after_hours")]
114 pub stale_after_hours: u64,
115}
116
117fn default_correlation_window_hours() -> u64 {
118 4
119}
120
121fn default_max_events_per_story() -> usize {
122 50
123}
124
125fn default_stale_after_hours() -> u64 {
126 24
127}
128
129#[derive(Debug, Clone, Deserialize)]
131#[serde(tag = "type", rename_all = "snake_case")]
132pub enum SensorSourceConfig {
133 JmapEmail {
135 name: String,
137 server: String,
139 username: String,
141 password_env: String,
143 #[serde(default)]
145 priority_senders: Vec<String>,
146 #[serde(default)]
148 blocked_senders: Vec<String>,
149 #[serde(default = "default_email_poll_interval")]
151 poll_interval_seconds: u64,
152 },
153 Rss {
155 name: String,
157 feeds: Vec<String>,
159 #[serde(default)]
161 interest_keywords: Vec<String>,
162 #[serde(default = "default_rss_poll_interval")]
164 poll_interval_seconds: u64,
165 },
166 Image {
168 name: String,
170 watch_directory: String,
172 #[serde(default = "default_file_poll_interval")]
174 poll_interval_seconds: u64,
175 },
176 Audio {
178 name: String,
180 watch_directory: String,
182 #[serde(default = "default_whisper_model")]
184 whisper_model: String,
185 #[serde(default)]
187 known_contacts: Vec<String>,
188 #[serde(default = "default_file_poll_interval")]
190 poll_interval_seconds: u64,
191 },
192 Weather {
194 name: String,
196 api_key_env: String,
198 locations: Vec<String>,
200 #[serde(default = "default_weather_poll_interval")]
202 poll_interval_seconds: u64,
203 #[serde(default)]
205 alert_only: bool,
206 },
207 Webhook {
209 name: String,
211 path: String,
213 secret_env: Option<String>,
215 },
216 Mcp {
218 name: String,
220 server: Box<McpServerEntry>,
222 tool_name: String,
224 #[serde(default = "default_empty_object")]
226 tool_args: serde_json::Value,
227 kafka_topic: String,
229 #[serde(default = "default_mcp_modality")]
231 modality: SensorModality,
232 #[serde(default = "default_mcp_poll_interval")]
234 poll_interval_seconds: u64,
235 #[serde(default = "default_id_field")]
237 id_field: String,
238 #[serde(default)]
240 content_field: Option<String>,
241 #[serde(default)]
243 items_field: Option<String>,
244 #[serde(default)]
246 priority_senders: Vec<String>,
247 #[serde(default)]
249 blocked_senders: Vec<String>,
250 #[serde(default)]
254 enrich_tool: Option<String>,
255 #[serde(default)]
257 enrich_id_param: Option<String>,
258 #[serde(default = "default_dedup_ttl_seconds")]
260 dedup_ttl_seconds: u64,
261 },
262}
263
264fn default_dedup_ttl_seconds() -> u64 {
265 7 * 24 * 3600 }
267
268impl SensorSourceConfig {
269 pub fn name(&self) -> &str {
271 match self {
272 SensorSourceConfig::JmapEmail { name, .. }
273 | SensorSourceConfig::Rss { name, .. }
274 | SensorSourceConfig::Image { name, .. }
275 | SensorSourceConfig::Audio { name, .. }
276 | SensorSourceConfig::Weather { name, .. }
277 | SensorSourceConfig::Webhook { name, .. }
278 | SensorSourceConfig::Mcp { name, .. } => name,
279 }
280 }
281
282 pub fn sender_lists(&self) -> (&[String], &[String]) {
287 match self {
288 SensorSourceConfig::JmapEmail {
289 priority_senders,
290 blocked_senders,
291 ..
292 }
293 | SensorSourceConfig::Mcp {
294 priority_senders,
295 blocked_senders,
296 ..
297 } => (priority_senders, blocked_senders),
298 _ => (&[], &[]),
299 }
300 }
301}
302
303fn default_email_poll_interval() -> u64 {
304 60
305}
306
307fn default_rss_poll_interval() -> u64 {
308 900
309}
310
311fn default_file_poll_interval() -> u64 {
312 30
313}
314
315fn default_whisper_model() -> String {
316 "base".into()
317}
318
319fn default_weather_poll_interval() -> u64 {
320 1800
321}
322
323fn default_mcp_poll_interval() -> u64 {
324 60
325}
326
327fn default_mcp_modality() -> SensorModality {
328 SensorModality::Text
329}
330
331fn default_id_field() -> String {
332 "id".into()
333}
334
335fn default_empty_object() -> serde_json::Value {
336 serde_json::json!({})
337}