Module: Legion::Gaia::Anticipation
- Extended by:
- Logging::Helper
- Defined in:
- lib/legion/gaia/anticipation.rb
Overview
H6 — prediction pre-staging with emotional modulation (§12.8).
Reads pending predictions from lex-agentic-inference (if available). Actionable predictions (confidence >= threshold) trigger pre-staged RAG retrieval, capped at 25% of total entries.
Emotional modulation: negative-valence anticipations defer when the partner's affect baseline is low (soft-guarded on lex-agentic-affect).
Entropy guard: no resolution under high identity entropy (consistent with H2). Soft-guarded throughout: returns nil when dependencies are absent.
Constant Summary collapse
- LOW_AFFECT_BASELINE =
Threshold below which the partner's affect baseline is considered "low".
0.3- PRESTAGE_CAP_RATIO =
Cap on pre-staged entries as a fraction of total RAG entries.
0.25
Class Method Summary collapse
- .actionable_threshold ⇒ Object
-
.affect_baseline_low?(identity:) ⇒ Boolean
Returns true when the partner's affect baseline is below LOW_AFFECT_BASELINE.
-
.build(identity:, context:) ⇒ Hash?
Build anticipation context for advisory.
-
.build_prestaged(context:, prediction:) ⇒ Object
Builds pre-staged RAG entries capped at 25% of total context entries.
-
.deferred?(identity:, anticipation:) ⇒ Boolean
Check if an anticipation should be deferred due to emotional state.
- .extract_valence(anticipation) ⇒ Object
-
.grade_prediction(prediction_id:, actual:, identity:) ⇒ Object
Grade a prediction as :correct, :incorrect, or :partial.
- .high_entropy?(identity:) ⇒ Boolean
- .inference_available? ⇒ Boolean
- .inference_runner ⇒ Object
- .partial_match?(predicted, actual) ⇒ Boolean
-
.resolve(identity:, prediction_id:, actual:) ⇒ Hash?
Resolve a pending anticipation against the partner's actual next input.
Class Method Details
.actionable_threshold ⇒ Object
140 141 142 143 144 145 146 |
# File 'lib/legion/gaia/anticipation.rb', line 140 def actionable_threshold settings = Legion::Gaia.settings settings&.dig(:anticipation, :actionable_threshold) || (defined?(Legion::Extensions::Agentic::Inference::Prediction::Helpers::Modes::PREDICTION_CONFIDENCE_MIN) && Legion::Extensions::Agentic::Inference::Prediction::Helpers::Modes::PREDICTION_CONFIDENCE_MIN) || 0.65 end |
.affect_baseline_low?(identity:) ⇒ Boolean
Returns true when the partner's affect baseline is below LOW_AFFECT_BASELINE.
200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/legion/gaia/anticipation.rb', line 200 def affect_baseline_low?(identity:) return false unless defined?(Legion::Extensions::Agentic::Affect::Empathy::Helpers::ModelStore) model_store = Legion::Extensions::Agentic::Affect::Empathy::Helpers::ModelStore.new model = model_store.get(identity) return false unless model.respond_to?(:emotional_state) negative_states = %i[stressed anxious frustrated overwhelmed sad] negative_states.include?(model.emotional_state) rescue StandardError => e handle_exception(e, level: :debug, operation: 'gaia.anticipation.affect_baseline_low', identity: identity) false end |
.build(identity:, context:) ⇒ Hash?
Build anticipation context for advisory.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/legion/gaia/anticipation.rb', line 37 def build(identity:, context:) return nil unless inference_available? runner = inference_runner result = runner.pending_predictions(identity: identity.to_s) predictions = Array(result[:predictions]) return nil if predictions.empty? actionable = predictions.select { |p| p[:confidence].to_f >= actionable_threshold } return nil if actionable.empty? # Pick the highest-confidence actionable prediction best = actionable.max_by { |p| p[:confidence].to_f } # Emotional modulation: defer negative-valence anticipation when affect baseline is low if deferred?(identity: identity, anticipation: best) log.info("[anticipation] deferred identity=#{identity} prediction_id=#{best[:prediction_id].to_s[0, 8]} " \ 'reason=emotional_state') return nil end pre_staged = build_prestaged(context: context, prediction: best) log.info("[anticipation] built identity=#{identity} prediction_id=#{best[:prediction_id].to_s[0, 8]} " \ "confidence=#{best[:confidence].to_f.round(3)} pre_staged=#{pre_staged.size}") { prediction_id: best[:prediction_id], content: best[:description] || best[:context].to_s, confidence: best[:confidence].to_f, pre_staged: pre_staged } rescue StandardError => e handle_exception(e, level: :warn, operation: 'gaia.anticipation.build', identity: identity) nil end |
.build_prestaged(context:, prediction:) ⇒ Object
Builds pre-staged RAG entries capped at 25% of total context entries.
149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/legion/gaia/anticipation.rb', line 149 def build_prestaged(context:, prediction:) return [] unless context.is_a?(Hash) rag_count = context[:rag_entry_count].to_i return [] if rag_count.zero? cap = [(rag_count * PRESTAGE_CAP_RATIO).ceil, 1].max prediction_context = prediction[:context] return [] unless prediction_context.is_a?(Hash) && prediction_context[:pre_stage_entries].is_a?(Array) prediction_context[:pre_stage_entries].first(cap) end |
.deferred?(identity:, anticipation:) ⇒ Boolean
Check if an anticipation should be deferred due to emotional state.
Deferred when the anticipation has negative emotional context AND the partner's current affect baseline is low (§12.8 emotional modulation).
118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/legion/gaia/anticipation.rb', line 118 def deferred?(identity:, anticipation:) return false unless anticipation.is_a?(Hash) anticipation_valence = extract_valence(anticipation) return false unless anticipation_valence < 0.0 affect_baseline_low?(identity: identity.to_s) rescue StandardError => e handle_exception(e, level: :debug, operation: 'gaia.anticipation.deferred', identity: identity) false end |
.extract_valence(anticipation) ⇒ Object
214 215 216 217 218 219 220 221 222 223 |
# File 'lib/legion/gaia/anticipation.rb', line 214 def extract_valence(anticipation) ctx = anticipation[:emotional_context] if ctx.is_a?(Hash) ctx[:valence].to_f elsif ctx.is_a?(Numeric) ctx.to_f else 0.0 end end |
.grade_prediction(prediction_id:, actual:, identity:) ⇒ Object
Grade a prediction as :correct, :incorrect, or :partial.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/legion/gaia/anticipation.rb', line 163 def grade_prediction(prediction_id:, actual:, identity:) return :incorrect unless inference_available? runner = inference_runner pred_result = runner.get_prediction(prediction_id: prediction_id, identity: identity) return :incorrect unless pred_result[:found] prediction = pred_result[:prediction] predicted = prediction[:description].to_s.downcase.strip actual_str = actual.to_s.downcase.strip return :incorrect if predicted.empty? || actual_str.empty? if actual_str.include?(predicted) || predicted.include?(actual_str) :correct elsif partial_match?(predicted, actual_str) :partial else :incorrect end rescue StandardError => e handle_exception(e, level: :debug, operation: 'gaia.anticipation.grade_prediction', prediction_id: prediction_id) :incorrect end |
.high_entropy?(identity:) ⇒ Boolean
225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/legion/gaia/anticipation.rb', line 225 def high_entropy?(identity:) return false unless defined?(Legion::Extensions::Agentic::Self::Helpers::IdentityFingerprint) runner = Object.new runner.extend(Legion::Extensions::Agentic::Self::Helpers::IdentityFingerprint) entropy = runner.identity_entropy(identity: identity) threshold = Legion::Gaia.settings&.dig(:identity, :high_entropy_threshold) || 0.7 entropy.to_f >= threshold rescue StandardError false end |
.inference_available? ⇒ Boolean
130 131 132 |
# File 'lib/legion/gaia/anticipation.rb', line 130 def inference_available? defined?(Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction) == 'constant' end |
.inference_runner ⇒ Object
134 135 136 137 138 |
# File 'lib/legion/gaia/anticipation.rb', line 134 def inference_runner runner = Object.new runner.extend(Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction) runner end |
.partial_match?(predicted, actual) ⇒ Boolean
189 190 191 192 193 194 195 196 197 |
# File 'lib/legion/gaia/anticipation.rb', line 189 def partial_match?(predicted, actual) pred_words = predicted.split(/\s+/).reject(&:empty?) actual_words = actual.split(/\s+/).reject(&:empty?) return false if pred_words.empty? || actual_words.empty? overlap = pred_words & actual_words ratio = overlap.size.to_f / [pred_words.size, actual_words.size].min ratio >= 0.5 end |
.resolve(identity:, prediction_id:, actual:) ⇒ Hash?
Resolve a pending anticipation against the partner's actual next input.
Grades the prediction as :correct, :incorrect, or :partial and records the outcome. Skipped under high identity entropy.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/legion/gaia/anticipation.rb', line 85 def resolve(identity:, prediction_id:, actual:) return nil unless inference_available? identity_str = identity.to_s if high_entropy?(identity: identity_str) log.info("[anticipation] resolve skipped identity=#{identity_str} reason=high_entropy") return { resolved: false, reason: :high_entropy, prediction_id: prediction_id } end outcome = grade_prediction(prediction_id: prediction_id, actual: actual, identity: identity_str) runner = inference_runner result = runner.resolve_prediction(prediction_id: prediction_id, outcome: outcome, actual: actual, identity: identity_str) log.info("[anticipation] resolved identity=#{identity_str} prediction_id=#{prediction_id.to_s[0, 8]} " \ "outcome=#{outcome}") result rescue StandardError => e handle_exception(e, level: :warn, operation: 'gaia.anticipation.resolve', identity: identity, prediction_id: prediction_id) nil end |