1use serde::{Deserialize, Serialize};
46use sha2::{Digest, Sha256};
47use std::time::{SystemTime, UNIX_EPOCH};
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DecisionTree {
56 pub root: DecisionNode,
58 pub rules_evaluated: Vec<RuleEvaluation>,
60 pub decision: ExplainedDecision,
62 pub tree_hash: [u8; 32],
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct DecisionNode {
69 pub node_id: String,
71 pub node_type: NodeType,
73 pub description: String,
75 pub result: NodeResult,
77 pub children: Vec<DecisionNode>,
79 pub confidence: f32,
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum NodeType {
86 Root,
88 RuleCheck(String),
90 ContextAnalysis,
92 PatternMatch,
94 ThresholdCheck,
96 Decision,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum NodeResult {
103 Passed,
105 Flagged(String),
107 Blocked(String),
109 NeedsContext,
111 Neutral,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct RuleEvaluation {
118 pub rule_id: String,
120 pub rule_text: String,
122 pub triggered: bool,
124 pub reason: String,
126 pub contribution: f32,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ExplainedDecision {
133 pub allowed: bool,
135 pub primary_reason: String,
137 pub factors: Vec<DecisionFactor>,
139 pub confidence: f32,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct DecisionFactor {
146 pub factor: String,
147 pub weight: f32,
148 pub direction: FactorDirection,
149}
150
151#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
153pub enum FactorDirection {
154 Allow,
156 Block,
158 Neutral,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ReasoningStep {
169 pub step: usize,
171 pub step_type: StepType,
173 pub input: String,
175 pub output: String,
177 pub reasoning: String,
179 pub confidence: f32,
181 pub step_hash: [u8; 32],
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub enum StepType {
188 InputParsing,
190 IntentClassification,
192 RuleLookup,
194 ContextGathering,
196 RiskAssessment,
198 DecisionSynthesis,
200 OutputGeneration,
202}
203
204pub struct ExplainabilityEngine {
210 rules: Vec<Rule>,
212 context_patterns: Vec<ContextPattern>,
214}
215
216#[derive(Debug, Clone)]
218pub struct Rule {
219 pub id: String,
220 pub text: String,
221 pub category: RuleCategory,
222 pub severity: Severity,
223}
224
225#[derive(Debug, Clone)]
227pub enum RuleCategory {
228 Safety,
229 Legal,
230 Privacy,
231 Ethics,
232 Context,
233}
234
235#[derive(Debug, Clone, Copy)]
237pub enum Severity {
238 Low,
239 Medium,
240 High,
241 Critical,
242}
243
244#[derive(Debug, Clone)]
246pub struct ContextPattern {
247 pub pattern: String,
248 pub indicates: String,
249 pub weight: f32,
250}
251
252impl ExplainabilityEngine {
253 pub fn new() -> Self {
255 let rules = vec![
256 Rule {
257 id: "R1".to_string(),
258 text: "Do no harm".to_string(),
259 category: RuleCategory::Safety,
260 severity: Severity::Critical,
261 },
262 Rule {
263 id: "R2".to_string(),
264 text: "Legal activities only".to_string(),
265 category: RuleCategory::Legal,
266 severity: Severity::High,
267 },
268 Rule {
269 id: "R3".to_string(),
270 text: "Respect privacy".to_string(),
271 category: RuleCategory::Privacy,
272 severity: Severity::High,
273 },
274 Rule {
275 id: "R4".to_string(),
276 text: "Consider context".to_string(),
277 category: RuleCategory::Context,
278 severity: Severity::Medium,
279 },
280 ];
281
282 let context_patterns = vec![
283 ContextPattern {
284 pattern: "for educational purposes".to_string(),
285 indicates: "Educational context".to_string(),
286 weight: 0.3,
287 },
288 ContextPattern {
289 pattern: "I'm a professional".to_string(),
290 indicates: "Professional context".to_string(),
291 weight: 0.2,
292 },
293 ];
294
295 ExplainabilityEngine {
296 rules,
297 context_patterns,
298 }
299 }
300
301 pub fn explain(&self, input: &str, output: Option<&str>, allowed: bool) -> ExplainabilityProof {
303 let mut steps = Vec::new();
304 let mut step_num = 1;
305
306 let input_step = self.parse_input(input, step_num);
308 steps.push(input_step);
309 step_num += 1;
310
311 let intent_step = self.classify_intent(input, step_num);
313 let intent = intent_step.output.clone();
314 steps.push(intent_step);
315 step_num += 1;
316
317 let rule_step = self.lookup_rules(&intent, step_num);
319 steps.push(rule_step);
320 step_num += 1;
321
322 let context_step = self.gather_context(input, step_num);
324 steps.push(context_step);
325 step_num += 1;
326
327 let risk_step = self.assess_risk(input, &intent, step_num);
329 steps.push(risk_step);
330 step_num += 1;
331
332 let decision_step = self.synthesize_decision(&steps, allowed, step_num);
334 steps.push(decision_step);
335
336 let tree = self.build_decision_tree(input, &steps, allowed);
338
339 let rule_evals = self.evaluate_rules(input, allowed);
341
342 let proof_hash = self.compute_proof_hash(&steps, &tree);
344
345 ExplainabilityProof {
346 input: input.to_string(),
347 output: output.map(String::from),
348 decision: tree.decision.clone(),
349 reasoning_steps: steps,
350 decision_tree: tree,
351 rule_evaluations: rule_evals,
352 proof_hash,
353 timestamp: SystemTime::now()
354 .duration_since(UNIX_EPOCH)
355 .unwrap()
356 .as_secs(),
357 }
358 }
359
360 fn parse_input(&self, input: &str, step: usize) -> ReasoningStep {
361 let words = input.split_whitespace().count();
362 let has_question = input.contains('?');
363
364 ReasoningStep {
365 step,
366 step_type: StepType::InputParsing,
367 input: input.to_string(),
368 output: format!("{} words, question: {}", words, has_question),
369 reasoning: "Parsed input to extract structure and features".to_string(),
370 confidence: 1.0,
371 step_hash: self.hash_step(step, input, "parsed"),
372 }
373 }
374
375 fn classify_intent(&self, input: &str, step: usize) -> ReasoningStep {
376 let input_lower = input.to_lowercase();
377
378 let intent = if input_lower.contains("how to") || input_lower.contains("how do") {
379 "instructional_request"
380 } else if input_lower.contains("what is") || input_lower.contains("explain") {
381 "informational_request"
382 } else if input_lower.contains("can you") || input_lower.contains("please") {
383 "task_request"
384 } else {
385 "general_query"
386 };
387
388 ReasoningStep {
389 step,
390 step_type: StepType::IntentClassification,
391 input: input.to_string(),
392 output: intent.to_string(),
393 reasoning: format!("Classified intent based on keyword analysis: {}", intent),
394 confidence: 0.85,
395 step_hash: self.hash_step(step, input, intent),
396 }
397 }
398
399 fn lookup_rules(&self, intent: &str, step: usize) -> ReasoningStep {
400 let applicable_rules: Vec<_> = self.rules.iter().map(|r| r.id.clone()).collect();
401
402 ReasoningStep {
403 step,
404 step_type: StepType::RuleLookup,
405 input: intent.to_string(),
406 output: format!("Applicable rules: {:?}", applicable_rules),
407 reasoning: "Identified all rules that may apply to this intent".to_string(),
408 confidence: 1.0,
409 step_hash: self.hash_step(step, intent, &format!("{:?}", applicable_rules)),
410 }
411 }
412
413 fn gather_context(&self, input: &str, step: usize) -> ReasoningStep {
414 let mut context_factors = Vec::new();
415
416 for pattern in &self.context_patterns {
417 if input
418 .to_lowercase()
419 .contains(&pattern.pattern.to_lowercase())
420 {
421 context_factors.push(pattern.indicates.clone());
422 }
423 }
424
425 ReasoningStep {
426 step,
427 step_type: StepType::ContextGathering,
428 input: input.to_string(),
429 output: format!("Context factors: {:?}", context_factors),
430 reasoning: "Analyzed input for contextual indicators".to_string(),
431 confidence: 0.75,
432 step_hash: self.hash_step(step, input, &format!("{:?}", context_factors)),
433 }
434 }
435
436 fn assess_risk(&self, input: &str, intent: &str, step: usize) -> ReasoningStep {
437 let input_lower = input.to_lowercase();
438
439 let risk_level = if input_lower.contains("harm")
440 || input_lower.contains("illegal")
441 || input_lower.contains("weapon")
442 {
443 "HIGH"
444 } else if input_lower.contains("hack") || input_lower.contains("bypass") {
445 "MEDIUM"
446 } else {
447 "LOW"
448 };
449
450 ReasoningStep {
451 step,
452 step_type: StepType::RiskAssessment,
453 input: format!("{}: {}", intent, input),
454 output: risk_level.to_string(),
455 reasoning: format!("Risk assessed as {} based on content analysis", risk_level),
456 confidence: 0.9,
457 step_hash: self.hash_step(step, input, risk_level),
458 }
459 }
460
461 fn synthesize_decision(
462 &self,
463 steps: &[ReasoningStep],
464 allowed: bool,
465 step: usize,
466 ) -> ReasoningStep {
467 let avg_confidence: f32 =
468 steps.iter().map(|s| s.confidence).sum::<f32>() / steps.len() as f32;
469
470 ReasoningStep {
471 step,
472 step_type: StepType::DecisionSynthesis,
473 input: format!("{} reasoning steps", steps.len()),
474 output: if allowed { "ALLOWED" } else { "BLOCKED" }.to_string(),
475 reasoning: format!(
476 "Synthesized decision from {} steps with average confidence {:.2}",
477 steps.len(),
478 avg_confidence
479 ),
480 confidence: avg_confidence,
481 step_hash: self.hash_step(
482 step,
483 &steps.len().to_string(),
484 if allowed { "allowed" } else { "blocked" },
485 ),
486 }
487 }
488
489 fn build_decision_tree(
490 &self,
491 input: &str,
492 steps: &[ReasoningStep],
493 allowed: bool,
494 ) -> DecisionTree {
495 let mut rule_nodes = Vec::new();
496
497 for rule in &self.rules {
498 let triggered = self.check_rule_triggered(input, rule);
499 rule_nodes.push(DecisionNode {
500 node_id: rule.id.clone(),
501 node_type: NodeType::RuleCheck(rule.text.clone()),
502 description: rule.text.clone(),
503 result: if triggered {
504 NodeResult::Flagged(format!("Rule {} triggered", rule.id))
505 } else {
506 NodeResult::Passed
507 },
508 children: vec![],
509 confidence: 0.9,
510 });
511 }
512
513 let root = DecisionNode {
514 node_id: "root".to_string(),
515 node_type: NodeType::Root,
516 description: format!("Analyzing: {}", &input[..input.len().min(50)]),
517 result: NodeResult::Neutral,
518 children: rule_nodes,
519 confidence: 1.0,
520 };
521
522 let decision = ExplainedDecision {
523 allowed,
524 primary_reason: if allowed {
525 "All rules passed".to_string()
526 } else {
527 "One or more rules triggered".to_string()
528 },
529 factors: vec![
530 DecisionFactor {
531 factor: "Rule evaluation".to_string(),
532 weight: 0.6,
533 direction: if allowed {
534 FactorDirection::Allow
535 } else {
536 FactorDirection::Block
537 },
538 },
539 DecisionFactor {
540 factor: "Context analysis".to_string(),
541 weight: 0.4,
542 direction: FactorDirection::Neutral,
543 },
544 ],
545 confidence: steps.iter().map(|s| s.confidence).sum::<f32>() / steps.len() as f32,
546 };
547
548 let tree_hash = self.hash_tree(&root, &decision);
549
550 DecisionTree {
551 root,
552 rules_evaluated: self.evaluate_rules(input, allowed),
553 decision,
554 tree_hash,
555 }
556 }
557
558 fn evaluate_rules(&self, input: &str, allowed: bool) -> Vec<RuleEvaluation> {
559 self.rules
560 .iter()
561 .map(|rule| {
562 let triggered = self.check_rule_triggered(input, rule);
563 RuleEvaluation {
564 rule_id: rule.id.clone(),
565 rule_text: rule.text.clone(),
566 triggered,
567 reason: if triggered {
568 "Content matched rule criteria".to_string()
569 } else {
570 "No match".to_string()
571 },
572 contribution: if triggered && !allowed { 0.5 } else { 0.0 },
573 }
574 })
575 .collect()
576 }
577
578 fn check_rule_triggered(&self, input: &str, rule: &Rule) -> bool {
579 let input_lower = input.to_lowercase();
580
581 match rule.category {
582 RuleCategory::Safety => {
583 input_lower.contains("harm")
584 || input_lower.contains("hurt")
585 || input_lower.contains("kill")
586 }
587 RuleCategory::Legal => {
588 input_lower.contains("illegal")
589 || input_lower.contains("hack")
590 || input_lower.contains("steal")
591 }
592 RuleCategory::Privacy => {
593 input_lower.contains("password") || input_lower.contains("personal data")
594 }
595 _ => false,
596 }
597 }
598
599 fn hash_step(&self, step: usize, input: &str, output: &str) -> [u8; 32] {
600 let mut hasher = Sha256::new();
601 hasher.update(b"STEP:");
602 hasher.update(step.to_le_bytes());
603 hasher.update(input.as_bytes());
604 hasher.update(output.as_bytes());
605 hasher.finalize().into()
606 }
607
608 fn hash_tree(&self, root: &DecisionNode, decision: &ExplainedDecision) -> [u8; 32] {
609 let mut hasher = Sha256::new();
610 hasher.update(b"TREE:");
611 hasher.update(root.node_id.as_bytes());
612 hasher.update([decision.allowed as u8]);
613 hasher.update(decision.primary_reason.as_bytes());
614 hasher.finalize().into()
615 }
616
617 fn compute_proof_hash(&self, steps: &[ReasoningStep], tree: &DecisionTree) -> [u8; 32] {
618 let mut hasher = Sha256::new();
619 hasher.update(b"EXPLAINABILITY_PROOF:");
620 for step in steps {
621 hasher.update(step.step_hash);
622 }
623 hasher.update(tree.tree_hash);
624 hasher.finalize().into()
625 }
626}
627
628impl Default for ExplainabilityEngine {
629 fn default() -> Self {
630 Self::new()
631 }
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct ExplainabilityProof {
641 pub input: String,
643 pub output: Option<String>,
645 pub decision: ExplainedDecision,
647 pub reasoning_steps: Vec<ReasoningStep>,
649 pub decision_tree: DecisionTree,
651 pub rule_evaluations: Vec<RuleEvaluation>,
653 pub proof_hash: [u8; 32],
655 pub timestamp: u64,
657}
658
659impl ExplainabilityProof {
660 pub fn to_human_readable(&self) -> String {
662 let mut explanation = String::new();
663
664 explanation.push_str("═══════════════════════════════════════════════════════════\n");
665 explanation.push_str(" EXPLAINABILITY PROOF \n");
666 explanation.push_str("═══════════════════════════════════════════════════════════\n\n");
667
668 explanation.push_str(&format!(
669 "DECISION: {}\n",
670 if self.decision.allowed {
671 "ALLOWED ✓"
672 } else {
673 "BLOCKED ✗"
674 }
675 ));
676 explanation.push_str(&format!(
677 "CONFIDENCE: {:.1}%\n",
678 self.decision.confidence * 100.0
679 ));
680 explanation.push_str(&format!("REASON: {}\n\n", self.decision.primary_reason));
681
682 explanation.push_str("REASONING STEPS:\n");
683 for step in &self.reasoning_steps {
684 explanation.push_str(&format!(
685 " {}. [{:?}] {} → {}\n",
686 step.step,
687 step.step_type,
688 if step.input.len() > 30 {
689 &step.input[..30]
690 } else {
691 &step.input
692 },
693 step.output
694 ));
695 }
696
697 explanation.push_str("\nRULES EVALUATED:\n");
698 for eval in &self.rule_evaluations {
699 explanation.push_str(&format!(
700 " {} {}: {} ({})\n",
701 if eval.triggered { "✗" } else { "✓" },
702 eval.rule_id,
703 eval.rule_text,
704 eval.reason
705 ));
706 }
707
708 explanation.push_str(&format!(
709 "\nPROOF HASH: 0x{}\n",
710 hex::encode(&self.proof_hash[..8])
711 ));
712
713 explanation
714 }
715}
716
717#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
726 fn test_explain_allowed() {
727 let engine = ExplainabilityEngine::new();
728 let proof = engine.explain("What is the capital of France?", Some("Paris"), true);
729
730 assert!(proof.decision.allowed);
731 assert!(!proof.reasoning_steps.is_empty());
732 }
733
734 #[test]
735 fn test_explain_blocked() {
736 let engine = ExplainabilityEngine::new();
737 let proof = engine.explain("How do I hack into a computer?", None, false);
738
739 assert!(!proof.decision.allowed);
740 assert!(proof.rule_evaluations.iter().any(|r| r.triggered));
741 }
742
743 #[test]
744 fn test_human_readable() {
745 let engine = ExplainabilityEngine::new();
746 let proof = engine.explain("Tell me a joke", Some("Why did..."), true);
747
748 let readable = proof.to_human_readable();
749 assert!(readable.contains("ALLOWED"));
750 assert!(readable.contains("PROOF HASH"));
751 }
752
753 #[test]
754 fn test_decision_tree_structure() {
755 let engine = ExplainabilityEngine::new();
756 let proof = engine.explain("Test query", None, true);
757
758 assert_eq!(proof.decision_tree.root.node_type, NodeType::Root);
759 assert!(!proof.decision_tree.root.children.is_empty());
760 }
761
762 #[test]
763 fn test_proof_hash_consistency() {
764 let engine = ExplainabilityEngine::new();
765 let proof1 = engine.explain("Same query", None, true);
766 let proof2 = engine.explain("Same query", None, true);
767
768 assert_eq!(proof1.decision.allowed, proof2.decision.allowed);
770 }
771}