mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 23:05:08 +00:00
Open-source Agent OS built in Rust. - 14 crates, 1,767+ tests, zero clippy warnings - 7 autonomous Hands (Clip, Lead, Collector, Predictor, Researcher, Twitter, Browser) - 16 security systems (WASM sandbox, Merkle audit trail, taint tracking, Ed25519 signing, SSRF protection, secret zeroization, HMAC-SHA256 mutual auth, and more) - 30 pre-built agents across 4 performance tiers - 40 channel adapters (Telegram, Discord, Slack, WhatsApp, Teams, and 35 more) - 38 built-in tools + MCP client/server + A2A protocol - 26 LLM providers with intelligent routing and cost tracking - 60+ bundled skills with FangHub marketplace - Tauri 2.0 native desktop app - 140+ REST/WS/SSE API endpoints with Alpine.js dashboard - OpenAI-compatible /v1/chat/completions endpoint - One-command install, production-ready
161 lines
4.7 KiB
Rust
161 lines
4.7 KiB
Rust
//! Streaming duplicate detection.
|
|
//!
|
|
//! Detects when the LLM repeats text that was already sent (e.g., repeating
|
|
//! tool output verbatim). Uses exact + normalized matching with a sliding window.
|
|
|
|
/// Minimum text length to consider for deduplication.
|
|
const MIN_DEDUP_LENGTH: usize = 10;
|
|
|
|
/// Number of recent chunks to keep in the dedup window.
|
|
const DEDUP_WINDOW: usize = 50;
|
|
|
|
/// Streaming duplicate detector.
|
|
pub struct StreamDedup {
|
|
/// Recent chunks (exact text).
|
|
recent_chunks: Vec<String>,
|
|
/// Recent chunks (normalized: lowercased, whitespace-collapsed).
|
|
recent_normalized: Vec<String>,
|
|
}
|
|
|
|
impl StreamDedup {
|
|
/// Create a new dedup detector.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
recent_chunks: Vec::with_capacity(DEDUP_WINDOW),
|
|
recent_normalized: Vec::with_capacity(DEDUP_WINDOW),
|
|
}
|
|
}
|
|
|
|
/// Check if text is a duplicate of recently sent content.
|
|
///
|
|
/// Returns `true` if the text matches (exact or normalized) any
|
|
/// recent chunk. Skips very short texts.
|
|
pub fn is_duplicate(&self, text: &str) -> bool {
|
|
if text.len() < MIN_DEDUP_LENGTH {
|
|
return false;
|
|
}
|
|
|
|
// Exact match
|
|
if self.recent_chunks.iter().any(|c| c == text) {
|
|
return true;
|
|
}
|
|
|
|
// Normalized match
|
|
let normalized = normalize(text);
|
|
self.recent_normalized.iter().any(|c| c == &normalized)
|
|
}
|
|
|
|
/// Record text that was successfully sent to the client.
|
|
pub fn record_sent(&mut self, text: &str) {
|
|
if text.len() < MIN_DEDUP_LENGTH {
|
|
return;
|
|
}
|
|
|
|
// Evict oldest if at capacity
|
|
if self.recent_chunks.len() >= DEDUP_WINDOW {
|
|
self.recent_chunks.remove(0);
|
|
self.recent_normalized.remove(0);
|
|
}
|
|
|
|
self.recent_chunks.push(text.to_string());
|
|
self.recent_normalized.push(normalize(text));
|
|
}
|
|
|
|
/// Clear the dedup window.
|
|
pub fn clear(&mut self) {
|
|
self.recent_chunks.clear();
|
|
self.recent_normalized.clear();
|
|
}
|
|
}
|
|
|
|
impl Default for StreamDedup {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Normalize text for fuzzy matching: lowercase + collapse whitespace.
|
|
fn normalize(text: &str) -> String {
|
|
let mut result = String::with_capacity(text.len());
|
|
let mut last_was_space = false;
|
|
|
|
for ch in text.chars() {
|
|
if ch.is_whitespace() {
|
|
if !last_was_space {
|
|
result.push(' ');
|
|
last_was_space = true;
|
|
}
|
|
} else {
|
|
result.push(ch.to_lowercase().next().unwrap_or(ch));
|
|
last_was_space = false;
|
|
}
|
|
}
|
|
|
|
result.trim().to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_exact_match_detected() {
|
|
let mut dedup = StreamDedup::new();
|
|
dedup.record_sent("This is a test chunk of text that was sent.");
|
|
assert!(dedup.is_duplicate("This is a test chunk of text that was sent."));
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalized_match_detected() {
|
|
let mut dedup = StreamDedup::new();
|
|
dedup.record_sent("This is a test chunk");
|
|
// Same text but different whitespace/case
|
|
assert!(dedup.is_duplicate("this is a test chunk"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_short_text_skipped() {
|
|
let mut dedup = StreamDedup::new();
|
|
dedup.record_sent("short");
|
|
assert!(!dedup.is_duplicate("short"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_window_rollover() {
|
|
let mut dedup = StreamDedup::new();
|
|
// Fill the window
|
|
for i in 0..DEDUP_WINDOW {
|
|
dedup.record_sent(&format!("chunk number {} is here", i));
|
|
}
|
|
// Add one more — should evict the oldest
|
|
dedup.record_sent("new chunk that is quite long");
|
|
// Oldest should no longer be detected
|
|
assert!(!dedup.is_duplicate("chunk number 0 is here"));
|
|
// Newest should be detected
|
|
assert!(dedup.is_duplicate("new chunk that is quite long"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_false_positives() {
|
|
let mut dedup = StreamDedup::new();
|
|
dedup.record_sent("The quick brown fox jumps over the lazy dog");
|
|
assert!(!dedup.is_duplicate("A completely different sentence here"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_clear() {
|
|
let mut dedup = StreamDedup::new();
|
|
dedup.record_sent("This is test content here");
|
|
assert!(dedup.is_duplicate("This is test content here"));
|
|
dedup.clear();
|
|
assert!(!dedup.is_duplicate("This is test content here"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize() {
|
|
assert_eq!(normalize("Hello World"), "hello world");
|
|
assert_eq!(normalize(" spaced out "), "spaced out");
|
|
assert_eq!(normalize("UPPER case"), "upper case");
|
|
}
|
|
}
|