1const REFUSAL_PREFIXES: &[&str] = &[
25 "i cannot ",
26 "i can't help with ",
27 "i can't assist with ",
28 "i'm not able to ",
29 "i am not able to ",
30 "i'm unable to ",
31 "i am unable to ",
32 "i must decline",
33 "i won't assist",
34 "i will not assist",
35 "i won't help",
36 "i will not help",
37 "sorry, i cannot",
38 "sorry, but i can't",
39 "sorry, but i cannot",
40 "i'm sorry, but i can't",
41 "i'm sorry, but i cannot",
42 "i apologize, but i can't",
43 "i apologize, but i cannot",
44 "as an ai, i cannot",
45 "as an ai, i can't",
46 "as an ai language model, i cannot",
47 "as an ai language model, i can't",
48];
49
50pub fn detect(content: &str) -> Option<String> {
55 if std::env::var("ATLAS_DISABLE_REFUSAL_DETECTION").as_deref() == Ok("1") {
56 return None;
57 }
58 let trimmed = content.trim_start();
59 if trimmed.is_empty() {
60 return None;
61 }
62 let head: String = trimmed
65 .chars()
66 .take(48)
67 .flat_map(|c| c.to_lowercase())
68 .collect();
69 let matched = REFUSAL_PREFIXES.iter().any(|p| head.starts_with(p));
70 if !matched {
71 return None;
72 }
73 let end_idx = trimmed
76 .char_indices()
77 .take(512)
78 .find(|(_, c)| matches!(c, '.' | '?' | '!'))
79 .map(|(i, c)| i + c.len_utf8());
80 let sentence = match end_idx {
81 Some(i) => &trimmed[..i],
82 None => trimmed
83 .split_once('\n')
84 .map(|(line, _)| line)
85 .unwrap_or(trimmed),
86 };
87 Some(sentence.trim().to_string())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::sync::Mutex;
94
95 static ENV_LOCK: Mutex<()> = Mutex::new(());
100
101 #[test]
102 fn matches_canonical_refusal() {
103 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
104 let r = detect("I cannot help with that request. Here is why…").unwrap();
105 assert_eq!(r, "I cannot help with that request.");
106 }
107
108 #[test]
109 fn matches_with_leading_whitespace() {
110 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
111 let r = detect(" I'm sorry, but I can't assist with weapons design.").unwrap();
112 assert_eq!(r, "I'm sorry, but I can't assist with weapons design.");
113 }
114
115 #[test]
116 fn mixed_case_matches() {
117 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
118 assert!(detect("As AN ai, I cannot provide that.").is_some());
119 }
120
121 #[test]
122 fn non_refusal_returns_none() {
123 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
124 assert!(detect("Sure, here's how to do that.").is_none());
125 assert!(detect("").is_none());
126 assert!(detect("I can do that for you.").is_none());
127 }
128
129 #[test]
130 fn kill_switch_returns_none() {
131 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
132 unsafe {
134 std::env::set_var("ATLAS_DISABLE_REFUSAL_DETECTION", "1");
135 }
136 let got = detect("I cannot help with that.");
137 unsafe {
139 std::env::remove_var("ATLAS_DISABLE_REFUSAL_DETECTION");
140 }
141 assert!(got.is_none());
142 }
143
144 #[test]
145 fn no_terminator_falls_back_to_line() {
146 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
147 let r = detect("I cannot answer that\nnext paragraph").unwrap();
148 assert_eq!(r, "I cannot answer that");
149 }
150}