use async_trait::async_trait;
use super::{AgentHook, StreamAction};
pub struct ContentFilterHook {
sensitive_words: Vec<String>,
placeholder: String,
drop_token: bool,
}
impl ContentFilterHook {
pub fn new(sensitive_words: Vec<String>) -> Self {
Self {
sensitive_words,
placeholder: "[REDACTED]".to_string(),
drop_token: false,
}
}
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn with_drop_token(mut self, drop: bool) -> Self {
self.drop_token = drop;
self
}
fn contains_sensitive(&self, text: &str) -> bool {
let text_lower = text.to_lowercase();
self.sensitive_words
.iter()
.any(|word| text_lower.contains(&word.to_lowercase()))
}
fn replace_sensitive(&self, text: &str) -> String {
let mut result = text.to_string();
for word in &self.sensitive_words {
let lower = text.to_lowercase();
let mut start = 0;
while let Some(pos) = lower[start..].find(&word.to_lowercase()) {
let actual_pos = start + pos;
let end = actual_pos + word.len();
result = format!("{}{}{}", &result[..actual_pos], self.placeholder, &result[end..]);
start = actual_pos + self.placeholder.len();
let new_lower = result.to_lowercase();
if start >= new_lower.len() {
break;
}
if !new_lower[start..].contains(&word.to_lowercase()) {
break;
}
}
}
result
}
}
#[async_trait]
impl AgentHook for ContentFilterHook {
fn on_stream_chunk(&self, chunk: &str) -> StreamAction {
if !self.contains_sensitive(chunk) {
return StreamAction::Forward(chunk.to_string());
}
if self.drop_token {
StreamAction::Filter
} else {
StreamAction::Replace(self.replace_sensitive(chunk))
}
}
}