#![allow(missing_docs)]
use serde::Deserialize;
use super::SensorModality;
use super::agent::McpServerEntry;
#[derive(Debug, Clone, Deserialize)]
pub struct SensorConfig {
#[serde(default = "super::default_true")]
pub enabled: bool,
#[serde(default)]
pub routing: Option<SensorRoutingConfig>,
#[serde(default)]
pub salience: Option<SalienceConfig>,
#[serde(default)]
pub token_budget: Option<TokenBudgetConfig>,
#[serde(default)]
pub stories: Option<StoryCorrelationConfig>,
#[serde(default)]
pub sources: Vec<SensorSourceConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SensorRoutingConfig {
#[serde(default = "default_triage_model")]
pub triage_model: String,
pub local_model_path: Option<String>,
#[serde(default = "default_confidence_threshold")]
pub confidence_threshold: f64,
}
fn default_triage_model() -> String {
"cloud_light".into()
}
fn default_confidence_threshold() -> f64 {
0.85
}
#[derive(Debug, Clone, Deserialize)]
pub struct SalienceConfig {
#[serde(default = "default_urgency_weight")]
pub urgency_weight: f64,
#[serde(default = "default_novelty_weight")]
pub novelty_weight: f64,
#[serde(default = "default_relevance_weight")]
pub relevance_weight: f64,
#[serde(default = "default_salience_threshold")]
pub threshold: f64,
}
fn default_urgency_weight() -> f64 {
0.3
}
fn default_novelty_weight() -> f64 {
0.3
}
fn default_relevance_weight() -> f64 {
0.4
}
fn default_salience_threshold() -> f64 {
0.3
}
#[derive(Debug, Clone, Deserialize)]
pub struct TokenBudgetConfig {
#[serde(default = "default_hourly_limit")]
pub hourly_limit: usize,
#[serde(default = "default_queue_size")]
pub queue_size: usize,
}
fn default_hourly_limit() -> usize {
100_000
}
fn default_queue_size() -> usize {
200
}
#[derive(Debug, Clone, Deserialize)]
pub struct StoryCorrelationConfig {
#[serde(default = "default_correlation_window_hours")]
pub correlation_window_hours: u64,
#[serde(default = "default_max_events_per_story")]
pub max_events_per_story: usize,
#[serde(default = "default_stale_after_hours")]
pub stale_after_hours: u64,
}
fn default_correlation_window_hours() -> u64 {
4
}
fn default_max_events_per_story() -> usize {
50
}
fn default_stale_after_hours() -> u64 {
24
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SensorSourceConfig {
JmapEmail {
name: String,
server: String,
username: String,
password_env: String,
#[serde(default)]
priority_senders: Vec<String>,
#[serde(default)]
blocked_senders: Vec<String>,
#[serde(default = "default_email_poll_interval")]
poll_interval_seconds: u64,
},
Rss {
name: String,
feeds: Vec<String>,
#[serde(default)]
interest_keywords: Vec<String>,
#[serde(default = "default_rss_poll_interval")]
poll_interval_seconds: u64,
},
Image {
name: String,
watch_directory: String,
#[serde(default = "default_file_poll_interval")]
poll_interval_seconds: u64,
},
Audio {
name: String,
watch_directory: String,
#[serde(default = "default_whisper_model")]
whisper_model: String,
#[serde(default)]
known_contacts: Vec<String>,
#[serde(default = "default_file_poll_interval")]
poll_interval_seconds: u64,
},
Weather {
name: String,
api_key_env: String,
locations: Vec<String>,
#[serde(default = "default_weather_poll_interval")]
poll_interval_seconds: u64,
#[serde(default)]
alert_only: bool,
},
Webhook {
name: String,
path: String,
secret_env: Option<String>,
},
Mcp {
name: String,
server: Box<McpServerEntry>,
tool_name: String,
#[serde(default = "default_empty_object")]
tool_args: serde_json::Value,
kafka_topic: String,
#[serde(default = "default_mcp_modality")]
modality: SensorModality,
#[serde(default = "default_mcp_poll_interval")]
poll_interval_seconds: u64,
#[serde(default = "default_id_field")]
id_field: String,
#[serde(default)]
content_field: Option<String>,
#[serde(default)]
items_field: Option<String>,
#[serde(default)]
priority_senders: Vec<String>,
#[serde(default)]
blocked_senders: Vec<String>,
#[serde(default)]
enrich_tool: Option<String>,
#[serde(default)]
enrich_id_param: Option<String>,
#[serde(default = "default_dedup_ttl_seconds")]
dedup_ttl_seconds: u64,
},
}
fn default_dedup_ttl_seconds() -> u64 {
7 * 24 * 3600 }
impl SensorSourceConfig {
pub fn name(&self) -> &str {
match self {
SensorSourceConfig::JmapEmail { name, .. }
| SensorSourceConfig::Rss { name, .. }
| SensorSourceConfig::Image { name, .. }
| SensorSourceConfig::Audio { name, .. }
| SensorSourceConfig::Weather { name, .. }
| SensorSourceConfig::Webhook { name, .. }
| SensorSourceConfig::Mcp { name, .. } => name,
}
}
pub fn sender_lists(&self) -> (&[String], &[String]) {
match self {
SensorSourceConfig::JmapEmail {
priority_senders,
blocked_senders,
..
}
| SensorSourceConfig::Mcp {
priority_senders,
blocked_senders,
..
} => (priority_senders, blocked_senders),
_ => (&[], &[]),
}
}
}
fn default_email_poll_interval() -> u64 {
60
}
fn default_rss_poll_interval() -> u64 {
900
}
fn default_file_poll_interval() -> u64 {
30
}
fn default_whisper_model() -> String {
"base".into()
}
fn default_weather_poll_interval() -> u64 {
1800
}
fn default_mcp_poll_interval() -> u64 {
60
}
fn default_mcp_modality() -> SensorModality {
SensorModality::Text
}
fn default_id_field() -> String {
"id".into()
}
fn default_empty_object() -> serde_json::Value {
serde_json::json!({})
}