fix: Critical - Gemini model name (gemini-3-flash-preview → gemini-2.5-flash) + null guard on final_fairness

#19
brain/app/services/gemini_explain_node.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Gemini 1.5 Flash Explainability Node for LangGraph Integration.
3
- Generates personalized Tamil/English explanations using LLM.
4
  """
5
 
6
  import os
@@ -12,7 +12,7 @@ from app.schemas.allocation_state import AllocationState
12
 
13
  async def gemini_explain_node(state: AllocationState) -> Dict[str, Any]:
14
  """
15
- LangGraph Node: Gemini 1.5 Flash personalized explanations.
16
 
17
  Generates natural language explanations in Tamil/English based on
18
  driver context, recovery status, EV considerations, and fairness metrics.
@@ -35,13 +35,19 @@ async def gemini_explain_node(state: AllocationState) -> Dict[str, Any]:
35
  # LangChain Google GenAI not installed
36
  return {}
37
 
38
- # Initialize Gemini 3 Flash Preview
39
- llm = ChatGoogleGenerativeAI(
40
- model="gemini-3-flash-preview",
41
- google_api_key=api_key,
42
- temperature=0.2, # Consistent tone
43
- max_tokens=100, # Keep explanations concise (<50 words)
44
- )
 
 
 
 
 
 
45
 
46
  # Rich prompt template with Tamil/English support
47
  prompt_template = PromptTemplate.from_template("""
@@ -68,46 +74,53 @@ Generate the explanation:
68
 
69
  final_proposal = state.final_proposal or state.route_proposal_1
70
  final_fairness = state.final_fairness or state.fairness_check_1
71
- metrics = final_fairness["metrics"]
72
 
73
- updated_explanations = state.explanations.copy()
 
 
 
 
 
 
 
 
74
 
75
- for alloc in final_proposal["allocation"]:
76
  driver_id = str(alloc["driver_id"])
77
 
78
  # Get existing explanation to enhance
79
- existing = state.explanations.get(driver_id, {})
80
 
81
  # Find driver info
82
  driver = next(
83
- (d for d in state.driver_models if str(d.get("id")) == driver_id),
84
  {}
85
  )
86
 
87
  # Find route info
88
  route_id = str(alloc["route_id"])
89
  route = next(
90
- (r for r in state.route_models if str(r.get("id")) == route_id),
91
  {}
92
  )
93
 
94
  # Get driver context
95
- driver_context = state.driver_contexts.get(driver_id, {})
96
 
97
  # Determine language preference
98
  preferred_lang = driver.get("preferred_language", "en")
99
  language = "Tamil" if preferred_lang == "ta" else "English"
100
 
101
  # Check EV status
102
- is_ev = driver.get("vehicle_type") == "EV" or driver.get("is_ev", False)
103
 
104
  # Check recovery status
105
- recovery_target = state.recovery_targets.get(driver_id)
106
  is_recovery = recovery_target is not None
107
 
108
  # Build context for prompt
109
- today_effort = alloc["effort"]
110
- team_avg = metrics["avg_effort"]
111
  delta_pct = ((today_effort / team_avg) - 1) * 100 if team_avg > 0 else 0
112
 
113
  context = {
@@ -130,7 +143,7 @@ Generate the explanation:
130
  # Fairness note
131
  "fairness_note": (
132
  "✅ Team workload perfectly balanced today!"
133
- if metrics["gini_index"] < 0.25
134
  else "Team fairness optimized."
135
  ),
136
 
@@ -153,7 +166,7 @@ Generate the explanation:
153
  # Update explanation
154
  updated_explanations[driver_id] = {
155
  "driver_explanation": generated_text,
156
- "admin_explanation": f"Gemini 1.5 Flash ({language}, {len(generated_text)} chars) - {existing.get('category', 'NEAR_AVG')}",
157
  "category": existing.get("category", "NEAR_AVG"),
158
  "gemini_generated": True,
159
  }
@@ -169,13 +182,14 @@ Generate the explanation:
169
  # Create decision log entry
170
  log_entry = {
171
  "timestamp": datetime.utcnow().isoformat(),
172
- "agent_name": "GEMINI_1_5_FLASH",
173
  "step_type": "PERSONALIZED_EXPLANATIONS",
174
  "input_snapshot": {
175
- "num_drivers": len(final_proposal["allocation"]),
 
176
  "languages": list(set(
177
  d.get("preferred_language", "en")
178
- for d in state.driver_models
179
  )),
180
  },
181
  "output_snapshot": {
@@ -192,21 +206,13 @@ Generate the explanation:
192
 
193
  return {
194
  "explanations": updated_explanations,
195
- "decision_logs": state.decision_logs + [log_entry],
196
  }
197
 
198
 
199
  def template_fallback(effort: float, avg_effort: float, is_recovery: bool) -> str:
200
  """
201
  Fallback template-based explanation when Gemini is unavailable.
202
-
203
- Args:
204
- effort: Today's effort score
205
- avg_effort: Team average effort
206
- is_recovery: Whether driver is in recovery mode
207
-
208
- Returns:
209
- Simple explanation string
210
  """
211
  if is_recovery:
212
  return "Recovery route today - lighter load after a busy week. Take it easy!"
 
1
  """
2
+ Gemini Explainability Node for LangGraph Integration.
3
+ Generates personalized Tamil/English explanations using Gemini 2.5 Flash LLM.
4
  """
5
 
6
  import os
 
12
 
13
  async def gemini_explain_node(state: AllocationState) -> Dict[str, Any]:
14
  """
15
+ LangGraph Node: Gemini personalized explanations.
16
 
17
  Generates natural language explanations in Tamil/English based on
18
  driver context, recovery status, EV considerations, and fairness metrics.
 
35
  # LangChain Google GenAI not installed
36
  return {}
37
 
38
+ # Initialize Gemini - use accessible model with fallback
39
+ gemini_model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
40
+
41
+ try:
42
+ llm = ChatGoogleGenerativeAI(
43
+ model=gemini_model,
44
+ google_api_key=api_key,
45
+ temperature=0.2, # Consistent tone
46
+ max_tokens=100, # Keep explanations concise (<50 words)
47
+ )
48
+ except Exception as e:
49
+ # Model initialization failed, skip Gemini
50
+ return {}
51
 
52
  # Rich prompt template with Tamil/English support
53
  prompt_template = PromptTemplate.from_template("""
 
74
 
75
  final_proposal = state.final_proposal or state.route_proposal_1
76
  final_fairness = state.final_fairness or state.fairness_check_1
 
77
 
78
+ # Guard against None state (if workflow failed mid-way)
79
+ if not final_proposal or not final_fairness:
80
+ return {}
81
+
82
+ metrics = final_fairness.get("metrics", {})
83
+ if not metrics:
84
+ return {}
85
+
86
+ updated_explanations = state.explanations.copy() if state.explanations else {}
87
 
88
+ for alloc in final_proposal.get("allocation", []):
89
  driver_id = str(alloc["driver_id"])
90
 
91
  # Get existing explanation to enhance
92
+ existing = state.explanations.get(driver_id, {}) if state.explanations else {}
93
 
94
  # Find driver info
95
  driver = next(
96
+ (d for d in (state.driver_models or []) if str(d.get("id")) == driver_id),
97
  {}
98
  )
99
 
100
  # Find route info
101
  route_id = str(alloc["route_id"])
102
  route = next(
103
+ (r for r in (state.route_models or []) if str(r.get("id")) == route_id),
104
  {}
105
  )
106
 
107
  # Get driver context
108
+ driver_context = state.driver_contexts.get(driver_id, {}) if state.driver_contexts else {}
109
 
110
  # Determine language preference
111
  preferred_lang = driver.get("preferred_language", "en")
112
  language = "Tamil" if preferred_lang == "ta" else "English"
113
 
114
  # Check EV status
115
+ is_ev = str(driver.get("vehicle_type", "")).upper() in ("EV", "ELECTRIC") or driver.get("is_ev", False)
116
 
117
  # Check recovery status
118
+ recovery_target = state.recovery_targets.get(driver_id) if state.recovery_targets else None
119
  is_recovery = recovery_target is not None
120
 
121
  # Build context for prompt
122
+ today_effort = alloc.get("effort", 0)
123
+ team_avg = metrics.get("avg_effort", 60)
124
  delta_pct = ((today_effort / team_avg) - 1) * 100 if team_avg > 0 else 0
125
 
126
  context = {
 
143
  # Fairness note
144
  "fairness_note": (
145
  "✅ Team workload perfectly balanced today!"
146
+ if metrics.get("gini_index", 1) < 0.25
147
  else "Team fairness optimized."
148
  ),
149
 
 
166
  # Update explanation
167
  updated_explanations[driver_id] = {
168
  "driver_explanation": generated_text,
169
+ "admin_explanation": f"Gemini ({gemini_model}, {language}, {len(generated_text)} chars) - {existing.get('category', 'NEAR_AVG')}",
170
  "category": existing.get("category", "NEAR_AVG"),
171
  "gemini_generated": True,
172
  }
 
182
  # Create decision log entry
183
  log_entry = {
184
  "timestamp": datetime.utcnow().isoformat(),
185
+ "agent_name": "GEMINI_EXPLAIN",
186
  "step_type": "PERSONALIZED_EXPLANATIONS",
187
  "input_snapshot": {
188
+ "num_drivers": len(final_proposal.get("allocation", [])),
189
+ "model": gemini_model,
190
  "languages": list(set(
191
  d.get("preferred_language", "en")
192
+ for d in (state.driver_models or [])
193
  )),
194
  },
195
  "output_snapshot": {
 
206
 
207
  return {
208
  "explanations": updated_explanations,
209
+ "decision_logs": (state.decision_logs or []) + [log_entry],
210
  }
211
 
212
 
213
  def template_fallback(effort: float, avg_effort: float, is_recovery: bool) -> str:
214
  """
215
  Fallback template-based explanation when Gemini is unavailable.
 
 
 
 
 
 
 
 
216
  """
217
  if is_recovery:
218
  return "Recovery route today - lighter load after a busy week. Take it easy!"