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_issues(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_issues(&self, claim: &str) -> Vec<String> {
110 let mut areas_of_interest = Vec::new();
111 let claim_lower = claim.to_lowercase();
112
113 if claim_lower.contains("always")
115 || claim_lower.contains("never")
116 || claim_lower.contains("all ")
117 || claim_lower.contains("none ")
118 {
119 areas_of_interest.push("Over-generalization: Verify if universal claims 'always'/'never' hold true for all edge cases.".to_string());
120 }
121
122 if claim_lower.contains("many")
124 || claim_lower.contains("some")
125 || claim_lower.contains("significant")
126 {
127 areas_of_interest.push("Quantification: Assess if vague terms like 'many' or 'significant' hide a lack of specific data.".to_string());
128 }
129
130 if claim_lower.contains("because") || claim_lower.contains("therefore") {
132 areas_of_interest.push("Causality: Scrutinize the link between the premise and the conclusion for logical leaps.".to_string());
133 }
134
135 if claim_lower.contains("%") || claim_lower.contains("percent") {
137 areas_of_interest.push("Statistics: Determine if percentages are sourced or if they are illustrative placeholders.".to_string());
138 }
139
140 let certainty_markers = ["obvious", "clearly", "undeniable", "proven", "fact"];
142 for word in certainty_markers {
143 if claim_lower.contains(word) {
144 areas_of_interest.push(format!("Certainty Bias: The use of '{}' may indicate a claim that assumes its own conclusion.", word));
145 break;
146 }
147 }
148
149 let avg_words_per_sentence =
151 claim.split_whitespace().count() / claim.matches('.').count().max(1);
152 if avg_words_per_sentence > 30 {
153 areas_of_interest.push("Complexity: The high sentence length may obscure specific errors or contradictions.".to_string());
154 }
155
156 areas_of_interest
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_shadow_creation() {
166 let blue = Agent::new(AgentConfig::default());
167 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
168
169 assert_eq!(shadow.blue_agent_id, blue.id);
170 assert!(!shadow.agent.config.spawn_shadow);
171 }
172
173 #[test]
174 fn test_detect_issues_universal_claims() {
175 let blue = Agent::new(AgentConfig::default());
176 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
177
178 let issues = shadow.detect_issues("This method always works without fail.");
179 assert!(issues.iter().any(|i| i.contains("Over-generalization")));
180 }
181
182 #[test]
183 fn test_detect_issues_statistics() {
184 let blue = Agent::new(AgentConfig::default());
185 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
186
187 let issues = shadow.detect_issues("Studies show 90% of users prefer this approach.");
188 assert!(issues.iter().any(|i| i.contains("Statistics")));
189 }
190
191 #[test]
192 fn test_detect_issues_loaded_language() {
193 let blue = Agent::new(AgentConfig::default());
194 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
195
196 let issues = shadow.detect_issues("It is obvious that the solution is correct.");
197 assert!(issues.iter().any(|i| i.contains("Certainty Bias")));
198 }
199
200 #[test]
201 fn test_detect_issues_clean_claim() {
202 let blue = Agent::new(AgentConfig::default());
203 let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
204
205 let issues = shadow.detect_issues("The API returns a 200 status code.");
207 assert!(issues.len() <= 2);
209 }
210}