Module: DecisionAgent::Replay

Defined in:
lib/decision_agent/replay/replay.rb

Class Method Summary collapse

Class Method Details

.build_replay_evaluators(evaluations) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/decision_agent/replay/replay.rb', line 74

def self.build_replay_evaluators(evaluations)
  evaluations.map do |evaluation|
    Evaluators::StaticEvaluator.new(
      decision: evaluation.decision,
      weight: evaluation.weight,
      reason: evaluation.reason,
      name: evaluation.evaluator_name,
      metadata: evaluation.
    )
  end
end

.instantiate_scoring_strategy(payload) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/decision_agent/replay/replay.rb', line 86

def self.instantiate_scoring_strategy(payload)
  strategy_name = payload[:scoring_strategy] || payload["scoring_strategy"]

  return Scoring::WeightedAverage.new unless strategy_name

  case strategy_name
  when /WeightedAverage/
    Scoring::WeightedAverage.new
  when /MaxWeight/
    Scoring::MaxWeight.new
  when /Consensus/
    Scoring::Consensus.new
  when /Threshold/
    Scoring::Threshold.new
  else
    Scoring::WeightedAverage.new
  end
end

.log_differences(original_decision:, original_confidence:, replayed_decision:, replayed_confidence:) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/decision_agent/replay/replay.rb', line 124

def self.log_differences(original_decision:, original_confidence:, replayed_decision:, replayed_confidence:)
  differences = []

  differences << "Decision changed: #{original_decision} -> #{replayed_decision}" if original_decision.to_s != replayed_decision.to_s

  conf_diff = (original_confidence.to_f - replayed_confidence.to_f).abs
  differences << "Confidence changed: #{original_confidence} -> #{replayed_confidence}" if conf_diff > 0.0001

  if differences.any?
    warn "[DecisionAgent::Replay] Non-strict mode differences detected:"
    differences.each { |diff| warn "  - #{diff}" }
  end

  differences
end

.parse_evaluations(payload) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/decision_agent/replay/replay.rb', line 56

def self.parse_evaluations(payload)
  evals = payload[:evaluations] || payload["evaluations"]

  evals.map do |eval_data|
    if eval_data.is_a?(Evaluation)
      eval_data
    else
      Evaluation.new(
        decision: eval_data[:decision] || eval_data["decision"],
        weight: eval_data[:weight] || eval_data["weight"],
        reason: eval_data[:reason] || eval_data["reason"],
        evaluator_name: eval_data[:evaluator_name] || eval_data["evaluator_name"],
        metadata: eval_data[:metadata] || eval_data["metadata"] || {}
      )
    end
  end
end

.run(audit_payload, strict: true) ⇒ Object

Raises:

  • (ArgumentError)


7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/decision_agent/replay/replay.rb', line 7

def self.run(audit_payload, strict: true)
  raise ArgumentError, "audit_payload cannot be nil" if audit_payload.nil?

  validate_payload!(audit_payload)

  context = Context.new(audit_payload[:context] || audit_payload["context"])
  feedback = audit_payload[:feedback] || audit_payload["feedback"] || {}

  original_evaluations = parse_evaluations(audit_payload)
  original_decision = audit_payload[:decision] || audit_payload["decision"]
  original_confidence = audit_payload[:confidence] || audit_payload["confidence"]

  scoring_strategy = instantiate_scoring_strategy(audit_payload)

  agent = Agent.new(
    evaluators: build_replay_evaluators(original_evaluations),
    scoring_strategy: scoring_strategy,
    audit_adapter: Audit::NullAdapter.new
  )

  replayed_result = agent.decide(context: context, feedback: feedback)

  if strict
    validate_strict_match!(
      original_decision: original_decision,
      original_confidence: original_confidence,
      replayed_decision: replayed_result.decision,
      replayed_confidence: replayed_result.confidence
    )
  else
    log_differences(
      original_decision: original_decision,
      original_confidence: original_confidence,
      replayed_decision: replayed_result.decision,
      replayed_confidence: replayed_result.confidence
    )
  end

  replayed_result
end

.validate_payload!(payload) ⇒ Object



48
49
50
51
52
53
54
# File 'lib/decision_agent/replay/replay.rb', line 48

def self.validate_payload!(payload)
  required_keys = %w[context evaluations decision confidence]

  required_keys.each do |key|
    raise InvalidRuleDslError, "Audit payload missing required key: #{key}" unless payload.key?(key) || payload.key?(key.to_sym)
  end
end

.validate_strict_match!(original_decision:, original_confidence:, replayed_decision:, replayed_confidence:) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/decision_agent/replay/replay.rb', line 105

def self.validate_strict_match!(original_decision:, original_confidence:, replayed_decision:, replayed_confidence:)
  differences = []

  if original_decision.to_s != replayed_decision.to_s
    differences << "decision mismatch (expected: #{original_decision}, got: #{replayed_decision})"
  end

  conf_diff = (original_confidence.to_f - replayed_confidence.to_f).abs
  differences << "confidence mismatch (expected: #{original_confidence}, got: #{replayed_confidence})" if conf_diff > 0.0001

  return unless differences.any?

  raise ReplayMismatchError.new(
    expected: { decision: original_decision, confidence: original_confidence },
    actual: { decision: replayed_decision, confidence: replayed_confidence },
    differences: differences
  )
end