feat(channel): add interrupt_on_new_message support for Mattermost
This commit is contained in:
parent
c1095de1ac
commit
2aaf916185
@ -291,6 +291,7 @@ const OPENRC_RESTART_ARGS: [&str; 2] = ["zeroclaw", "restart"];
|
||||
struct InterruptOnNewMessageConfig {
|
||||
telegram: bool,
|
||||
slack: bool,
|
||||
mattermost: bool,
|
||||
}
|
||||
|
||||
impl InterruptOnNewMessageConfig {
|
||||
@ -298,6 +299,7 @@ impl InterruptOnNewMessageConfig {
|
||||
match channel {
|
||||
"telegram" => self.telegram,
|
||||
"slack" => self.slack,
|
||||
"mattermost" => self.mattermost,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -4383,6 +4385,11 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||
.slack
|
||||
.as_ref()
|
||||
.is_some_and(|sl| sl.interrupt_on_new_message);
|
||||
let interrupt_on_new_message_mattermost = config
|
||||
.channels_config
|
||||
.mattermost
|
||||
.as_ref()
|
||||
.is_some_and(|mm| mm.interrupt_on_new_message);
|
||||
|
||||
let runtime_ctx = Arc::new(ChannelRuntimeContext {
|
||||
channels_by_name,
|
||||
@ -4411,6 +4418,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: interrupt_on_new_message,
|
||||
slack: interrupt_on_new_message_slack,
|
||||
mattermost: interrupt_on_new_message_mattermost,
|
||||
},
|
||||
multimodal: config.multimodal.clone(),
|
||||
hooks: if config.hooks.enabled {
|
||||
@ -4727,6 +4735,7 @@ mod tests {
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -4839,6 +4848,7 @@ mod tests {
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -4907,6 +4917,7 @@ mod tests {
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -4994,6 +5005,7 @@ mod tests {
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -5502,6 +5514,80 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_channel_message_executes_tool_calls_instead_of_sending_raw_json() {
|
||||
let channel_impl = Arc::new(RecordingChannel::default());
|
||||
let channel: Arc<dyn Channel> = channel_impl.clone();
|
||||
|
||||
let mut channels_by_name = HashMap::new();
|
||||
channels_by_name.insert(channel.name().to_string(), channel);
|
||||
|
||||
let runtime_ctx = Arc::new(ChannelRuntimeContext {
|
||||
channels_by_name: Arc::new(channels_by_name),
|
||||
provider: Arc::new(ToolCallingProvider),
|
||||
default_provider: Arc::new("test-provider".to_string()),
|
||||
memory: Arc::new(NoopMemory),
|
||||
tools_registry: Arc::new(vec![Box::new(MockPriceTool)]),
|
||||
observer: Arc::new(NoopObserver),
|
||||
system_prompt: Arc::new("test-system-prompt".to_string()),
|
||||
model: Arc::new("test-model".to_string()),
|
||||
temperature: 0.0,
|
||||
auto_save_memory: false,
|
||||
max_tool_iterations: 10,
|
||||
min_relevance_score: 0.0,
|
||||
conversation_histories: Arc::new(Mutex::new(HashMap::new())),
|
||||
provider_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
route_overrides: Arc::new(Mutex::new(HashMap::new())),
|
||||
api_key: None,
|
||||
api_url: None,
|
||||
reliability: Arc::new(crate::config::ReliabilityConfig::default()),
|
||||
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
|
||||
workspace_dir: Arc::new(std::env::temp_dir()),
|
||||
message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS,
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
non_cli_excluded_tools: Arc::new(Vec::new()),
|
||||
tool_call_dedup_exempt: Arc::new(Vec::new()),
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
model_routes: Arc::new(Vec::new()),
|
||||
query_classification: crate::config::QueryClassificationConfig::default(),
|
||||
ack_reactions: true,
|
||||
show_tool_calls: true,
|
||||
session_store: None,
|
||||
approval_manager: Arc::new(ApprovalManager::for_non_interactive(
|
||||
&crate::config::AutonomyConfig::default(),
|
||||
)),
|
||||
activated_tools: None,
|
||||
});
|
||||
|
||||
process_channel_message(
|
||||
runtime_ctx,
|
||||
traits::ChannelMessage {
|
||||
id: "msg-1".to_string(),
|
||||
sender: "alice".to_string(),
|
||||
reply_target: "chat-42".to_string(),
|
||||
content: "What is the BTC price now?".to_string(),
|
||||
channel: "test-channel".to_string(),
|
||||
timestamp: 1,
|
||||
thread_ts: None,
|
||||
},
|
||||
CancellationToken::new(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let sent_messages = channel_impl.sent_messages.lock().await;
|
||||
assert!(!sent_messages.is_empty());
|
||||
let reply = sent_messages.last().unwrap();
|
||||
assert!(reply.starts_with("chat-42:"));
|
||||
assert!(reply.contains("BTC is currently around"));
|
||||
assert!(!reply.contains("\"tool_calls\""));
|
||||
assert!(!reply.contains("mock_price"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_channel_message_telegram_does_not_persist_tool_summary_prefix() {
|
||||
let channel_impl = Arc::new(TelegramRecordingChannel::default());
|
||||
let channel: Arc<dyn Channel> = channel_impl.clone();
|
||||
|
||||
@ -5535,6 +5621,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
non_cli_excluded_tools: Arc::new(Vec::new()),
|
||||
autonomy_level: AutonomyLevel::default(),
|
||||
@ -5625,6 +5712,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -5700,6 +5788,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -5785,6 +5874,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -5891,6 +5981,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -5978,6 +6069,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6080,6 +6172,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6167,6 +6260,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6244,6 +6338,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6432,6 +6527,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6528,6 +6624,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: true,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6639,6 +6736,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: true,
|
||||
mattermost: false,
|
||||
},
|
||||
ack_reactions: true,
|
||||
show_tool_calls: true,
|
||||
@ -6747,6 +6845,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: true,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6837,6 +6936,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -6912,6 +7012,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -7663,6 +7764,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -7952,6 +8054,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -8053,6 +8156,7 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -8383,6 +8487,7 @@ This is an example JSON object for profile settings."#;
|
||||
allowed_users: vec![],
|
||||
thread_replies: Some(true),
|
||||
mention_only: Some(false),
|
||||
interrupt_on_new_message: false,
|
||||
});
|
||||
|
||||
let channels = collect_configured_channels(&config, "test");
|
||||
@ -8618,6 +8723,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -8700,6 +8806,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -8856,6 +8963,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -8962,6 +9070,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -9060,6 +9169,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -9178,6 +9288,7 @@ This is an example JSON object for profile settings."#;
|
||||
interrupt_on_new_message: InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
},
|
||||
multimodal: crate::config::MultimodalConfig::default(),
|
||||
hooks: None,
|
||||
@ -9299,4 +9410,24 @@ This is an example JSON object for profile settings."#;
|
||||
assert!(!is_stop_command("/stopwatch"));
|
||||
assert!(!is_stop_command("/stop-all"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_on_new_message_enabled_for_mattermost_when_true() {
|
||||
let cfg = InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: true,
|
||||
};
|
||||
assert!(cfg.enabled_for_channel("mattermost"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_on_new_message_disabled_for_mattermost_by_default() {
|
||||
let cfg = InterruptOnNewMessageConfig {
|
||||
telegram: false,
|
||||
slack: false,
|
||||
mattermost: false,
|
||||
};
|
||||
assert!(!cfg.enabled_for_channel("mattermost"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -4731,6 +4731,10 @@ pub struct MattermostConfig {
|
||||
/// Other messages in the channel are silently ignored.
|
||||
#[serde(default)]
|
||||
pub mention_only: Option<bool>,
|
||||
/// When true, a newer Mattermost message from the same sender in the same channel
|
||||
/// cancels the in-flight request and starts a fresh response with preserved history.
|
||||
#[serde(default)]
|
||||
pub interrupt_on_new_message: bool,
|
||||
}
|
||||
|
||||
impl ChannelConfig for MattermostConfig {
|
||||
@ -9713,6 +9717,21 @@ channel_id = "C123"
|
||||
assert_eq!(parsed.channel_id.as_deref(), Some("C123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn mattermost_config_default_interrupt_on_new_message_is_false() {
|
||||
let json = r#"{"url":"https://mm.example.com","bot_token":"tok"}"#;
|
||||
let parsed: MattermostConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(!parsed.interrupt_on_new_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn mattermost_config_deserializes_interrupt_on_new_message_true() {
|
||||
let json =
|
||||
r#"{"url":"https://mm.example.com","bot_token":"tok","interrupt_on_new_message":true}"#;
|
||||
let parsed: MattermostConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(parsed.interrupt_on_new_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn webhook_config_with_secret() {
|
||||
let json = r#"{"port":8080,"secret":"my-secret-key"}"#;
|
||||
|
||||
@ -671,6 +671,7 @@ mod tests {
|
||||
allowed_users: vec!["*".into()],
|
||||
thread_replies: Some(true),
|
||||
mention_only: Some(false),
|
||||
interrupt_on_new_message: false,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
|
||||
@ -7371,6 +7371,7 @@ mod tests {
|
||||
allowed_users: vec!["*".into()],
|
||||
thread_replies: Some(true),
|
||||
mention_only: Some(false),
|
||||
interrupt_on_new_message: false,
|
||||
});
|
||||
assert!(has_launchable_channels(&channels));
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user