Skip to main content

ras_agent/domain/
loop_detector.rs

1use sha2::{Digest, Sha256};
2use url::Url;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct PageFingerprint(String);
6
7impl PageFingerprint {
8    #[must_use]
9    pub fn new(url: &Url, title: &str, clickable_count: u32, text_chars: u32) -> Self {
10        let mut h = Sha256::new();
11        h.update(url.as_str().as_bytes());
12        h.update(b"|");
13        h.update(title.as_bytes());
14        h.update(b"|");
15        h.update(clickable_count.to_le_bytes());
16        h.update(b"|");
17        h.update(text_chars.to_le_bytes());
18        Self(format!("{:x}", h.finalize()))
19    }
20
21    #[must_use]
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25}
26
27#[derive(Debug, Default)]
28pub struct ActionLoopDetector {
29    history: Vec<String>,
30    pages: Vec<PageFingerprint>,
31    pub action_threshold: u32,
32    pub stagnation_threshold: u32,
33}
34
35impl ActionLoopDetector {
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            history: Vec::new(),
40            pages: Vec::new(),
41            action_threshold: 5,
42            stagnation_threshold: 5,
43        }
44    }
45
46    pub fn record_action(&mut self, action_hash: impl Into<String>) {
47        self.history.push(action_hash.into());
48        if self.history.len() > 32 {
49            self.history.remove(0);
50        }
51    }
52
53    pub fn record_page(&mut self, fingerprint: PageFingerprint) {
54        self.pages.push(fingerprint);
55        if self.pages.len() > 32 {
56            self.pages.remove(0);
57        }
58    }
59
60    #[must_use]
61    pub fn action_loop_detected(&self) -> bool {
62        let n = self.action_threshold as usize;
63        if self.history.len() < n {
64            return false;
65        }
66        let last = &self.history[self.history.len() - 1];
67        self.history.iter().rev().take(n).all(|h| h == last)
68    }
69
70    #[must_use]
71    pub fn page_stagnation_detected(&self) -> bool {
72        let n = self.stagnation_threshold as usize;
73        if self.pages.len() < n {
74            return false;
75        }
76        let last = &self.pages[self.pages.len() - 1];
77        self.pages.iter().rev().take(n).all(|p| p == last)
78    }
79}