vex_adversarial/
shadow.rs1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5use vex_core::{Agent, AgentConfig};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ShadowConfig {
10 pub challenge_intensity: f64,
12 pub fact_check: bool,
14 pub logic_check: bool,
16}
17
18impl Default for ShadowConfig {
19 fn default() -> Self {
20 Self {
21 challenge_intensity: 0.7,
22 fact_check: true,
23 logic_check: true,
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
30pub struct ShadowAgent {
31 pub agent: Agent,
33 pub config: ShadowConfig,
35 pub blue_agent_id: Uuid,
37}
38
39impl ShadowAgent {
40 pub fn new(blue_agent: &Agent, config: ShadowConfig) -> Self {
42 let agent_config = AgentConfig {
43 name: format!("{}_shadow", blue_agent.config.name),
44 role: format!(
45 "You are a critical challenger. Your job is to find flaws, \
46 inconsistencies, and potential errors in the following claim. \
47 Challenge intensity: {:.0}%",
48 config.challenge_intensity * 100.0
49 ),
50 max_depth: 0, spawn_shadow: false, };
53
54 let mut agent = blue_agent.spawn_child(agent_config);
55 agent.shadow_id = None;
56
57 Self {
58 agent,
59 config,
60 blue_agent_id: blue_agent.id,
61 }
62 }
63
64 pub fn challenge_prompt(&self, claim: &str) -> String {
66 let mut challenge_types = Vec::new();
67
68 if self.config.fact_check {
69 challenge_types.push("factual accuracy");
70 }
71 if self.config.logic_check {
72 challenge_types.push("logical consistency");
73 }
74
75 let detected_issues = self.detect_areas_of_interest(claim);
77
78 let issue_guidance = if detected_issues.is_empty() {
80 String::from("Look for hidden assumptions, unstated premises, and edge cases.")
81 } else {
82 format!(
83 "Pay special attention to these potential issues: {}",
84 detected_issues.join("; ")
85 )
86 };
87
88 format!(
89 "Critically analyze the following claim for {}:\n\n\
90 \"{}\"\n\n\
91 {}\n\n\
92 For each issue found:
93 1. State the specific problem
94 2. Explain why it matters
95 3. Suggest how it could be verified or corrected
96
97 If any issues are found, start your response with the marker: [CHALLENGE]
98 If no issues are found, start your response with the marker: [CLEAN]
99
100 If [CLEAN], explain what makes the claim robust.",
101 challenge_types.join(" and "),
102 claim,
103 issue_guidance
104 )
105 }
106
107 pub fn detect_areas_of_interest(&self, claim: &str) -> Vec<String> {
111 let mut areas_of_interest = Vec::new();
112 let claim_lower = claim.to_lowercase();
113
114 if claim_lower.contains("always")
116 || claim_lower.contains("never")
117 || claim_lower.contains("all ")
118 || claim_lower.contains("none ")
119 {
120 areas_of_interest.push("Over-generalization: Verify if universal claims 'always'/'never' hold true for all edge cases.".to_string());
121 }
122
123 if claim_lower.contains("many")
125 || claim_lower.contains("some")
126 || claim_lower.contains("significant")
127 {
128 areas_of_interest.push("Quantification: Assess if vague terms like 'many' or 'significant' hide a lack of specific data.".to_string());
129 }
130
131 if claim_lower.contains("because") || claim_lower.contains("therefore") {
133 areas_of_interest.push("Causality: Scrutinize the link between the premise and the conclusion for logical leaps.".to_string());
134 }
135
136 if claim_lower.contains("%") || claim_lower.contains("percent") {
138 areas_of_interest.push("Statistics: Determine if percentages are sourced or if they are illustrative placeholders.".to_string());
139 }
140
141 let certainty_markers = ["obvious", "clearly", "undeniable", "proven", "fact"];
143 for word in certainty_markers {
144 if claim_lower.contains(word) {
145 areas_of_interest.push(format!("Certainty Bias: The use of '{}' may indicate a claim that assumes its own conclusion.", word));
146 break;
147 }
148 }
149
150 let avg_words_per_sentence =
152 claim.split_whitespace().count() / claim.matches('.').count().max(1);
153 if avg_words_per_sentence > 30 {
154 areas_of_interest.push("Complexity: The high sentence length may obscure specific errors or contradictions.".to_string());
155 }
156
157 areas_of_interest
158 }
159
160 #[doc(hidden)]
162 pub fn detect_issues(&self, claim: &str) -> Vec<String> {
163 self.detect_areas_of_interest(claim)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_shadow_creation() {
173 let blue = Agent::new(AgentConfig::default());
174 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
175
176 assert_eq!(shadow.blue_agent_id, blue.id);
177 assert!(!shadow.agent.config.spawn_shadow);
178 }
179
180 #[test]
181 fn test_detect_issues_universal_claims() {
182 let blue = Agent::new(AgentConfig::default());
183 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
184
185 let issues = shadow.detect_issues("This method always works without fail.");
186 assert!(issues.iter().any(|i| i.contains("Over-generalization")));
187 }
188
189 #[test]
190 fn test_detect_issues_statistics() {
191 let blue = Agent::new(AgentConfig::default());
192 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
193
194 let issues = shadow.detect_issues("Studies show 90% of users prefer this approach.");
195 assert!(issues.iter().any(|i| i.contains("Statistics")));
196 }
197
198 #[test]
199 fn test_detect_issues_loaded_language() {
200 let blue = Agent::new(AgentConfig::default());
201 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
202
203 let issues = shadow.detect_issues("It is obvious that the solution is correct.");
204 assert!(issues.iter().any(|i| i.contains("Certainty Bias")));
205 }
206
207 #[test]
208 fn test_detect_issues_clean_claim() {
209 let blue = Agent::new(AgentConfig::default());
210 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
211
212 let issues = shadow.detect_issues("The API returns a 200 status code.");
214 assert!(issues.len() <= 2);
216 }
217}