Mahault commited on
Commit
89bc1b1
·
1 Parent(s): a663d0f

Integrate emotional inference into EFE action selection

Browse files

Thread Circumplex POMDP outputs (valence beliefs, prediction error)
into coaching readiness, lambda_epist, and EFE action selection so
the agent responds to emotional state rather than just keywords.

src/mindsphere/core/action_dispatcher.py CHANGED
@@ -48,12 +48,14 @@ def compute_lambda_epist(
48
  timestep: int,
49
  tom_reliability: float,
50
  beliefs: Optional[Dict[str, np.ndarray]] = None,
 
51
  ) -> float:
52
  """
53
  Compute the epistemic drive weight, balancing exploration vs exploitation.
54
 
55
  Starts high (explore the user) and decays toward pragmatic (propose actions).
56
- Boosted when ToM reliability is low or beliefs are uncertain.
 
57
  """
58
  # Phase-dependent base value
59
  phase_base = {
@@ -78,7 +80,13 @@ def compute_lambda_epist(
78
  # Reliability discount: boost epistemic when ToM is unreliable
79
  reliability_factor = 1.0 + max(0.0, 0.5 - tom_reliability)
80
 
81
- result = phase_base * temporal_factor * uncertainty_factor * reliability_factor
 
 
 
 
 
 
82
  return result
83
 
84
 
@@ -95,6 +103,8 @@ def select_coaching_action(
95
  target_skill: Optional[str] = None,
96
  current_intervention=None,
97
  beta: float = 4.0,
 
 
98
  ) -> Tuple[int, str, Dict]:
99
  """
100
  Full EFE-driven action selection with empathy blending.
@@ -110,6 +120,8 @@ def select_coaching_action(
110
  target_skill: Currently targeted skill (optional)
111
  current_intervention: Current intervention dict (optional)
112
  beta: Inverse temperature for softmax
 
 
113
 
114
  Returns:
115
  (action_idx, action_name, efe_info_dict)
@@ -117,7 +129,8 @@ def select_coaching_action(
117
  valid = VALID_ACTIONS.get(phase, [A_ASK_FREE, A_PROPOSE])
118
 
119
  lambda_epist = compute_lambda_epist(
120
- phase, timestep, tom_reliability, beliefs
 
121
  )
122
 
123
  # Determine which factors are relevant
@@ -171,6 +184,29 @@ def select_coaching_action(
171
  action_probs = blended_probs
172
  efe_values = blended_values
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  action_name = ACTION_NAMES[action_idx]
175
 
176
  info = {
@@ -185,6 +221,8 @@ def select_coaching_action(
185
  },
186
  "lambda_epist": round(lambda_epist, 3),
187
  "phase": phase,
 
 
188
  }
189
 
190
  logger.info(
 
48
  timestep: int,
49
  tom_reliability: float,
50
  beliefs: Optional[Dict[str, np.ndarray]] = None,
51
+ emotion_prediction_error: float = 0.0,
52
  ) -> float:
53
  """
54
  Compute the epistemic drive weight, balancing exploration vs exploitation.
55
 
56
  Starts high (explore the user) and decays toward pragmatic (propose actions).
57
+ Boosted when ToM reliability is low, beliefs are uncertain, or emotional
58
+ prediction error is high (model is wrong about user's emotional state).
59
  """
60
  # Phase-dependent base value
61
  phase_base = {
 
80
  # Reliability discount: boost epistemic when ToM is unreliable
81
  reliability_factor = 1.0 + max(0.0, 0.5 - tom_reliability)
82
 
83
+ # Emotion prediction error boost: when our emotional model is wrong,
84
+ # increase exploration (ask questions, don't push interventions)
85
+ emotion_factor = 1.0
86
+ if emotion_prediction_error > 0.2:
87
+ emotion_factor = 1.0 + 0.8 * min(emotion_prediction_error, 1.0)
88
+
89
+ result = phase_base * temporal_factor * uncertainty_factor * reliability_factor * emotion_factor
90
  return result
91
 
92
 
 
103
  target_skill: Optional[str] = None,
104
  current_intervention=None,
105
  beta: float = 4.0,
106
+ emotion_prediction_error: float = 0.0,
107
+ emotion_valence_belief: Optional[np.ndarray] = None,
108
  ) -> Tuple[int, str, Dict]:
109
  """
110
  Full EFE-driven action selection with empathy blending.
 
120
  target_skill: Currently targeted skill (optional)
121
  current_intervention: Current intervention dict (optional)
122
  beta: Inverse temperature for softmax
123
+ emotion_prediction_error: Magnitude of emotional prediction error [0, ~1.4]
124
+ emotion_valence_belief: 5-element belief over valence states (optional)
125
 
126
  Returns:
127
  (action_idx, action_name, efe_info_dict)
 
129
  valid = VALID_ACTIONS.get(phase, [A_ASK_FREE, A_PROPOSE])
130
 
131
  lambda_epist = compute_lambda_epist(
132
+ phase, timestep, tom_reliability, beliefs,
133
+ emotion_prediction_error=emotion_prediction_error,
134
  )
135
 
136
  # Determine which factors are relevant
 
184
  action_probs = blended_probs
185
  efe_values = blended_values
186
 
187
+ # Step 3: Emotional valence penalty on intervention actions
188
+ # When the user is in a negative emotional state, penalize pushing
189
+ # coaching interventions — prefer softer actions (ask, safety_check)
190
+ valence_negative_mass = None
191
+ if emotion_valence_belief is not None and len(emotion_valence_belief) >= 5:
192
+ valence_negative_mass = float(emotion_valence_belief[0] + emotion_valence_belief[1])
193
+ if valence_negative_mass > 0.4:
194
+ penalty = 0.5 * valence_negative_mass
195
+ for i, v in enumerate(valid):
196
+ if v in intervention_actions:
197
+ efe_values[i] += penalty # Higher G = worse action
198
+ # Re-select with penalty applied
199
+ from .utils import softmax as _softmax
200
+ q_values = -efe_values
201
+ penalized_probs = _softmax(q_values, temperature=1.0 / max(beta, 0.01))
202
+ best_idx = int(np.argmax(penalized_probs))
203
+ action_idx = valid[best_idx]
204
+ action_probs = penalized_probs
205
+ logger.info(
206
+ f"[EFE] Emotional valence penalty applied (neg_mass={valence_negative_mass:.2f}, "
207
+ f"penalty={penalty:.2f})"
208
+ )
209
+
210
  action_name = ACTION_NAMES[action_idx]
211
 
212
  info = {
 
221
  },
222
  "lambda_epist": round(lambda_epist, 3),
223
  "phase": phase,
224
+ "emotion_prediction_error": round(emotion_prediction_error, 3),
225
+ "valence_negative_mass": round(valence_negative_mass, 3) if valence_negative_mass is not None else None,
226
  }
227
 
228
  logger.info(
src/mindsphere/core/agent.py CHANGED
@@ -254,14 +254,15 @@ class CoachingAgent:
254
  logger.warning(f"[LLM] Empty response — falling back to template (phase={self.phase})")
255
  return result
256
 
257
- def _assess_cognitive_load(self, user_message: str = "") -> Dict[str, Any]:
258
  """
259
- Infer cognitive load from ToM dimensions + conversation signals.
260
 
261
  Uses:
262
  - ToM overwhelm_threshold (low = easily overwhelmed)
263
  - Recent sentiment signals from conversation
264
  - Message length and engagement patterns
 
265
  """
266
  # ToM-based assessment
267
  tom_type = self.tom.get_user_type_summary()
@@ -321,6 +322,27 @@ class CoachingAgent:
321
  if len(user_message.strip()) < 10 and user_message.strip():
322
  signals.append("low_effort")
323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  # Track recent sentiments
325
  self._recent_sentiments.append(
326
  "disengaged" if "disengaged" in signals
@@ -344,6 +366,9 @@ class CoachingAgent:
344
  elif "off_topic" in signals or "deflection" in signals:
345
  load_level = "redirected"
346
  coaching_readiness = "not_ready"
 
 
 
347
  elif "engaged" in signals:
348
  load_level = "optimal"
349
  coaching_readiness = "ready"
@@ -965,8 +990,8 @@ class CoachingAgent:
965
  # Run emotional inference
966
  emotional_data = self._run_emotional_inference(user_text)
967
 
968
- # Assess cognitive load / intent
969
- cog_load = self._assess_cognitive_load(user_text)
970
 
971
  # Update cognitive model
972
  self._update_user_model_from_text(user_text)
@@ -999,6 +1024,9 @@ class CoachingAgent:
999
  return result
1000
 
1001
  # === EFE-driven action selection ===
 
 
 
1002
  action_idx, action_name, efe_info = select_coaching_action(
1003
  beliefs=self.beliefs,
1004
  model=self.model,
@@ -1009,6 +1037,8 @@ class CoachingAgent:
1009
  tom_filter=self.tom,
1010
  target_skill=self.target_skill,
1011
  current_intervention=self.current_intervention,
 
 
1012
  )
1013
 
1014
  # Only transition when EFE strongly favors it AND we've had some discussion
@@ -1350,7 +1380,7 @@ class CoachingAgent:
1350
  "another step", "next step", "what else",
1351
  "give me something", "another exercise", "what now",
1352
  ])
1353
- cog_load = self._assess_cognitive_load(user_text)
1354
  if wants_more_action and cog_load["coaching_readiness"] != "not_ready":
1355
  self._track_conversation("user", user_text)
1356
  result = self._propose_next_coaching_step()
@@ -1382,6 +1412,9 @@ class CoachingAgent:
1382
  }
1383
 
1384
  # === EFE-driven action selection ===
 
 
 
1385
  action_idx, action_name, efe_info = select_coaching_action(
1386
  beliefs=self.beliefs,
1387
  model=self.model,
@@ -1392,6 +1425,8 @@ class CoachingAgent:
1392
  tom_filter=self.tom,
1393
  target_skill=self.target_skill,
1394
  current_intervention=self.current_intervention,
 
 
1395
  )
1396
 
1397
  # Dispatch based on EFE-selected action
 
254
  logger.warning(f"[LLM] Empty response — falling back to template (phase={self.phase})")
255
  return result
256
 
257
+ def _assess_cognitive_load(self, user_message: str = "", emotional_data: Optional[Dict] = None) -> Dict[str, Any]:
258
  """
259
+ Infer cognitive load from ToM dimensions + conversation signals + emotional state.
260
 
261
  Uses:
262
  - ToM overwhelm_threshold (low = easily overwhelmed)
263
  - Recent sentiment signals from conversation
264
  - Message length and engagement patterns
265
+ - Emotional valence beliefs and prediction error from Circumplex POMDP
266
  """
267
  # ToM-based assessment
268
  tom_type = self.tom.get_user_type_summary()
 
322
  if len(user_message.strip()) < 10 and user_message.strip():
323
  signals.append("low_effort")
324
 
325
+ # === Emotional state integration (from Circumplex POMDP) ===
326
+ if emotional_data:
327
+ beliefs = emotional_data.get("emotional_beliefs", {})
328
+ valence_belief = beliefs.get("valence", {}).get("belief", [])
329
+
330
+ # Valence belief concentrated on negative states → emotional distress
331
+ if len(valence_belief) >= 5:
332
+ negative_mass = valence_belief[0] + valence_belief[1]
333
+ if negative_mass > 0.6:
334
+ signals.append("emotional_distress")
335
+
336
+ # High prediction error → our emotional model was wrong
337
+ error_data = emotional_data.get("error", {})
338
+ if error_data.get("magnitude", 0) > 0.5:
339
+ signals.append("emotional_surprise")
340
+
341
+ # Current valence strongly negative
342
+ current = emotional_data.get("current_emotion", {})
343
+ if current and current.get("valence", 0) < -0.3:
344
+ signals.append("low_valence")
345
+
346
  # Track recent sentiments
347
  self._recent_sentiments.append(
348
  "disengaged" if "disengaged" in signals
 
366
  elif "off_topic" in signals or "deflection" in signals:
367
  load_level = "redirected"
368
  coaching_readiness = "not_ready"
369
+ elif "emotional_distress" in signals or "low_valence" in signals:
370
+ load_level = "emotionally_vulnerable"
371
+ coaching_readiness = "not_ready"
372
  elif "engaged" in signals:
373
  load_level = "optimal"
374
  coaching_readiness = "ready"
 
990
  # Run emotional inference
991
  emotional_data = self._run_emotional_inference(user_text)
992
 
993
+ # Assess cognitive load / intent (with emotional data)
994
+ cog_load = self._assess_cognitive_load(user_text, emotional_data=emotional_data)
995
 
996
  # Update cognitive model
997
  self._update_user_model_from_text(user_text)
 
1024
  return result
1025
 
1026
  # === EFE-driven action selection ===
1027
+ emotion_error_mag = emotional_data.get("error", {}).get("magnitude", 0.0)
1028
+ valence_belief = self.emotion.belief_valence
1029
+
1030
  action_idx, action_name, efe_info = select_coaching_action(
1031
  beliefs=self.beliefs,
1032
  model=self.model,
 
1037
  tom_filter=self.tom,
1038
  target_skill=self.target_skill,
1039
  current_intervention=self.current_intervention,
1040
+ emotion_prediction_error=emotion_error_mag,
1041
+ emotion_valence_belief=valence_belief,
1042
  )
1043
 
1044
  # Only transition when EFE strongly favors it AND we've had some discussion
 
1380
  "another step", "next step", "what else",
1381
  "give me something", "another exercise", "what now",
1382
  ])
1383
+ cog_load = self._assess_cognitive_load(user_text, emotional_data=emotional_data)
1384
  if wants_more_action and cog_load["coaching_readiness"] != "not_ready":
1385
  self._track_conversation("user", user_text)
1386
  result = self._propose_next_coaching_step()
 
1412
  }
1413
 
1414
  # === EFE-driven action selection ===
1415
+ emotion_error_mag = emotional_data.get("error", {}).get("magnitude", 0.0)
1416
+ valence_belief = self.emotion.belief_valence
1417
+
1418
  action_idx, action_name, efe_info = select_coaching_action(
1419
  beliefs=self.beliefs,
1420
  model=self.model,
 
1425
  tom_filter=self.tom,
1426
  target_skill=self.target_skill,
1427
  current_intervention=self.current_intervention,
1428
+ emotion_prediction_error=emotion_error_mag,
1429
+ emotion_valence_belief=valence_belief,
1430
  )
1431
 
1432
  # Dispatch based on EFE-selected action