riazmo commited on
Commit
61dc3b2
Β·
verified Β·
1 Parent(s): 11542ea

Update langgraph_nodes.py

Browse files
Files changed (1) hide show
  1. langgraph_nodes.py +178 -40
langgraph_nodes.py CHANGED
@@ -1,7 +1,6 @@
1
  """
2
- LangGraph Nodes
3
- All node functions for the review processing graph
4
- Implements parallel execution where possible
5
  """
6
 
7
  import os
@@ -19,9 +18,17 @@ warnings.filterwarnings('ignore')
19
  from langgraph_state import ReviewState, BatchState
20
  from database_enhanced import EnhancedDatabase
21
 
22
- # Initialize HF client (singleton)
23
  HF_TOKEN = os.getenv("HUGGINGFACE_API_KEY")
24
- hf_client = InferenceClient(token=HF_TOKEN) if HF_TOKEN else None
 
 
 
 
 
 
 
 
25
 
26
  # Initialize sentiment models (singleton) - load once
27
  _sentiment_models_loaded = False
@@ -59,6 +66,20 @@ def load_sentiment_models():
59
 
60
  def llm1_classify(review: Dict[str, Any]) -> Dict[str, Any]:
61
  """LLM1: Type, Department, Priority classification"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  review_text = review.get('review_text', '')
63
  rating = review.get('rating', 3)
64
 
@@ -103,6 +124,9 @@ Respond ONLY in valid JSON format:
103
  }}"""
104
 
105
  try:
 
 
 
106
  response = hf_client.text_generation(
107
  prompt,
108
  model="Qwen/Qwen2.5-72B-Instruct",
@@ -110,6 +134,8 @@ Respond ONLY in valid JSON format:
110
  temperature=0.1
111
  )
112
 
 
 
113
  # Clean and parse JSON
114
  response_clean = response.strip()
115
  if response_clean.startswith('```'):
@@ -120,21 +146,41 @@ Respond ONLY in valid JSON format:
120
 
121
  result = json.loads(response_clean)
122
  result['model'] = 'Qwen/Qwen2.5-72B-Instruct'
 
 
123
  return result
124
 
125
  except Exception as e:
 
 
 
 
 
126
  return {
127
  'type': 'unknown',
128
  'department': 'unknown',
129
  'priority': 'medium',
130
  'confidence': 0.0,
131
- 'reasoning': f'Error: {str(e)}',
132
  'model': 'Qwen/Qwen2.5-72B-Instruct'
133
  }
134
 
135
 
136
  def llm2_analyze(review: Dict[str, Any]) -> Dict[str, Any]:
137
  """LLM2: User type, Emotion, Context analysis"""
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  review_text = review.get('review_text', '')
139
  rating = review.get('rating', 3)
140
 
@@ -176,6 +222,9 @@ Respond ONLY in valid JSON format:
176
  }}"""
177
 
178
  try:
 
 
 
179
  response = hf_client.text_generation(
180
  prompt,
181
  model="mistralai/Mistral-7B-Instruct-v0.3",
@@ -183,6 +232,8 @@ Respond ONLY in valid JSON format:
183
  temperature=0.1
184
  )
185
 
 
 
186
  # Clean and parse JSON
187
  response_clean = response.strip()
188
  if response_clean.startswith('```'):
@@ -193,21 +244,40 @@ Respond ONLY in valid JSON format:
193
 
194
  result = json.loads(response_clean)
195
  result['model'] = 'mistralai/Mistral-7B-Instruct-v0.3'
 
 
196
  return result
197
 
198
  except Exception as e:
 
 
 
 
 
199
  return {
200
  'user_type': 'unknown',
201
  'emotion': 'unknown',
202
  'context': 'unknown',
203
  'confidence': 0.0,
204
- 'reasoning': f'Error: {str(e)}',
205
  'model': 'mistralai/Mistral-7B-Instruct-v0.3'
206
  }
207
 
208
 
209
  def manager_synthesize(llm1_result: Dict, llm2_result: Dict, review: Dict) -> Dict[str, Any]:
210
  """Manager: Synthesize LLM1 and LLM2 results"""
 
 
 
 
 
 
 
 
 
 
 
 
211
  review_text = review.get('review_text', '')
212
  rating = review.get('rating', 3)
213
 
@@ -234,22 +304,22 @@ Respond ONLY in valid JSON format:
234
  "final_type": "from llm1 or adjusted",
235
  "final_department": "from llm1 or adjusted",
236
  "final_priority": "from llm1 or adjusted",
237
- "final_user_type": "from llm2 or adjusted",
238
- "final_emotion": "from llm2 or adjusted",
239
- "confidence": 0.0-1.0,
240
- "reasoning": "synthesis explanation",
241
- "conflicts_found": "any conflicts or 'none'"
242
  }}"""
243
 
244
  try:
 
 
 
245
  response = hf_client.text_generation(
246
  prompt,
247
- model="meta-llama/Llama-3.1-8B-Instruct",
248
- max_new_tokens=250,
249
  temperature=0.1
250
  )
251
 
252
- # Clean and parse JSON
 
253
  response_clean = response.strip()
254
  if response_clean.startswith('```'):
255
  response_clean = response_clean.split('```')[1]
@@ -258,21 +328,21 @@ Respond ONLY in valid JSON format:
258
  response_clean = response_clean.strip()
259
 
260
  result = json.loads(response_clean)
261
- result['model'] = 'meta-llama/Llama-3.1-8B-Instruct'
 
 
262
  return result
263
 
264
  except Exception as e:
265
- # Fallback to LLM1 results
 
 
266
  return {
267
  'final_type': llm1_result.get('type', 'unknown'),
268
  'final_department': llm1_result.get('department', 'unknown'),
269
  'final_priority': llm1_result.get('priority', 'medium'),
270
- 'final_user_type': llm2_result.get('user_type', 'unknown'),
271
- 'final_emotion': llm2_result.get('emotion', 'unknown'),
272
- 'confidence': 0.5,
273
- 'reasoning': f'Manager error, used LLM1 results: {str(e)}',
274
- 'conflicts_found': 'error',
275
- 'model': 'meta-llama/Llama-3.1-8B-Instruct'
276
  }
277
 
278
 
@@ -285,35 +355,42 @@ def stage1_classification_node(state: ReviewState) -> Dict[str, Any]:
285
  print(f" ⏳ STAGE 1: Classification (Parallel LLM1 + LLM2)...")
286
 
287
  start_time = time.time()
 
288
 
289
  # PARALLEL EXECUTION: LLM1 and LLM2 run simultaneously
290
  with ThreadPoolExecutor(max_workers=2) as executor:
291
- future1 = executor.submit(llm1_classify, state['review'])
292
- future2 = executor.submit(llm2_analyze, state['review'])
293
 
294
- llm1_result = future1.result()
295
- llm2_result = future2.result()
296
 
297
- print(f" βœ… LLM1: {llm1_result.get('type')} β†’ {llm1_result.get('department')} (Priority: {llm1_result.get('priority')})")
298
- print(f" βœ… LLM2: {llm2_result.get('user_type')}, {llm2_result.get('emotion')}")
299
 
300
- # Manager synthesizes sequentially (needs both results)
301
  print(f" πŸ€– Manager synthesizing...")
302
- manager_result = manager_synthesize(llm1_result, llm2_result, state['review'])
303
 
304
  stage1_time = time.time() - start_time
305
  print(f" βœ… Stage 1 complete ({stage1_time:.2f}s)")
306
 
307
- # Update state
 
 
 
 
 
308
  return {
309
  "llm1_result": llm1_result,
310
  "llm2_result": llm2_result,
311
  "manager_result": manager_result,
312
- "classification_type": manager_result.get('final_type'),
313
- "department": manager_result.get('final_department'),
314
- "priority": manager_result.get('final_priority'),
315
- "user_type": manager_result.get('final_user_type'),
316
- "emotion": manager_result.get('final_emotion'),
 
317
  "stage1_completed": True,
318
  "stage1_time": stage1_time,
319
  "errors": state.get('errors', [])
@@ -321,11 +398,11 @@ def stage1_classification_node(state: ReviewState) -> Dict[str, Any]:
321
 
322
 
323
  # ============================================================================
324
- # STAGE 2: SENTIMENT NODE (Parallel Best + Alternate)
325
  # ============================================================================
326
 
327
  def analyze_best_sentiment(text: str) -> Dict[str, Any]:
328
- """Best Model: Twitter-RoBERTa"""
329
  load_sentiment_models()
330
 
331
  try:
@@ -348,6 +425,7 @@ def analyze_best_sentiment(text: str) -> Dict[str, Any]:
348
  'model': 'twitter-roberta-base-sentiment-latest'
349
  }
350
  except Exception as e:
 
351
  return {
352
  'sentiment': 'NEUTRAL',
353
  'confidence': 0.0,
@@ -383,6 +461,7 @@ def analyze_alt_sentiment(text: str) -> Dict[str, Any]:
383
  'model': 'bertweet-base-sentiment-analysis'
384
  }
385
  except Exception as e:
 
386
  return {
387
  'sentiment': 'NEUTRAL',
388
  'confidence': 0.0,
@@ -455,6 +534,12 @@ def stage2_sentiment_node(state: ReviewState) -> Dict[str, Any]:
455
  stage2_time = time.time() - start_time
456
  print(f" βœ… Stage 2 complete ({stage2_time:.2f}s)")
457
 
 
 
 
 
 
 
458
  return {
459
  "best_sentiment_result": best_result,
460
  "alt_sentiment_result": alt_result,
@@ -480,6 +565,43 @@ def stage3_finalization_node(state: ReviewState) -> Dict[str, Any]:
480
 
481
  start_time = time.time()
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  review_text = state['review_text']
484
  rating = state['rating']
485
 
@@ -520,6 +642,9 @@ Respond ONLY in valid JSON format:
520
  }}"""
521
 
522
  try:
 
 
 
523
  response = hf_client.text_generation(
524
  prompt,
525
  model="meta-llama/Llama-3.1-70B-Instruct",
@@ -527,6 +652,8 @@ Respond ONLY in valid JSON format:
527
  temperature=0.1
528
  )
529
 
 
 
530
  response_clean = response.strip()
531
  if response_clean.startswith('```'):
532
  response_clean = response_clean.split('```')[1]
@@ -538,6 +665,11 @@ Respond ONLY in valid JSON format:
538
  result['model'] = 'meta-llama/Llama-3.1-70B-Instruct'
539
 
540
  except Exception as e:
 
 
 
 
 
541
  result = {
542
  'final_sentiment': state.get('sentiment', 'NEUTRAL'),
543
  'confidence': state.get('sentiment_confidence', 0.5),
@@ -558,6 +690,12 @@ Respond ONLY in valid JSON format:
558
  # Calculate total time
559
  total_time = state.get('stage1_time', 0) + state.get('stage2_time', 0) + stage3_time
560
 
 
 
 
 
 
 
561
  return {
562
  "final_result": result,
563
  "final_sentiment": result['final_sentiment'],
@@ -580,4 +718,4 @@ if __name__ == "__main__":
580
  print(" Nodes available:")
581
  print(" - stage1_classification_node (parallel LLM1+LLM2)")
582
  print(" - stage2_sentiment_node (parallel Best+Alt)")
583
- print(" - stage3_finalization_node (LLM3)")
 
1
  """
2
+ LangGraph Nodes - FIXED VERSION
3
+ Better error handling and debugging for API calls
 
4
  """
5
 
6
  import os
 
18
  from langgraph_state import ReviewState, BatchState
19
  from database_enhanced import EnhancedDatabase
20
 
21
+ # FIXED: Initialize HF client with better error handling
22
  HF_TOKEN = os.getenv("HUGGINGFACE_API_KEY")
23
+
24
+ # Check if token exists
25
+ if not HF_TOKEN or HF_TOKEN.strip() == "":
26
+ print("❌ WARNING: HUGGINGFACE_API_KEY not set!")
27
+ print(" API calls will fail. Please set your API key.")
28
+ hf_client = None
29
+ else:
30
+ print(f"βœ… HF Token found: {HF_TOKEN[:8]}...")
31
+ hf_client = InferenceClient(token=HF_TOKEN)
32
 
33
  # Initialize sentiment models (singleton) - load once
34
  _sentiment_models_loaded = False
 
66
 
67
  def llm1_classify(review: Dict[str, Any]) -> Dict[str, Any]:
68
  """LLM1: Type, Department, Priority classification"""
69
+
70
+ # FIXED: Check if client exists
71
+ if hf_client is None:
72
+ print("❌ ERROR: HuggingFace client not initialized!")
73
+ print(" Make sure HUGGINGFACE_API_KEY environment variable is set")
74
+ return {
75
+ 'type': 'unknown',
76
+ 'department': 'unknown',
77
+ 'priority': 'medium',
78
+ 'confidence': 0.0,
79
+ 'reasoning': 'HuggingFace API key not set',
80
+ 'model': 'Qwen/Qwen2.5-72B-Instruct'
81
+ }
82
+
83
  review_text = review.get('review_text', '')
84
  rating = review.get('rating', 3)
85
 
 
124
  }}"""
125
 
126
  try:
127
+ # FIXED: Better error logging
128
+ print(f" πŸ” Calling Qwen API...")
129
+
130
  response = hf_client.text_generation(
131
  prompt,
132
  model="Qwen/Qwen2.5-72B-Instruct",
 
134
  temperature=0.1
135
  )
136
 
137
+ print(f" βœ… Got response ({len(response)} chars)")
138
+
139
  # Clean and parse JSON
140
  response_clean = response.strip()
141
  if response_clean.startswith('```'):
 
146
 
147
  result = json.loads(response_clean)
148
  result['model'] = 'Qwen/Qwen2.5-72B-Instruct'
149
+
150
+ print(f" βœ… Parsed: {result['type']} β†’ {result['department']}")
151
  return result
152
 
153
  except Exception as e:
154
+ # FIXED: Show the actual error
155
+ print(f"❌ LLM1 ERROR: {type(e).__name__}: {str(e)}")
156
+ import traceback
157
+ traceback.print_exc()
158
+
159
  return {
160
  'type': 'unknown',
161
  'department': 'unknown',
162
  'priority': 'medium',
163
  'confidence': 0.0,
164
+ 'reasoning': f'API Error: {str(e)}',
165
  'model': 'Qwen/Qwen2.5-72B-Instruct'
166
  }
167
 
168
 
169
  def llm2_analyze(review: Dict[str, Any]) -> Dict[str, Any]:
170
  """LLM2: User type, Emotion, Context analysis"""
171
+
172
+ # FIXED: Check if client exists
173
+ if hf_client is None:
174
+ print("❌ ERROR: HuggingFace client not initialized!")
175
+ return {
176
+ 'user_type': 'unknown',
177
+ 'emotion': 'unknown',
178
+ 'context': 'unknown',
179
+ 'confidence': 0.0,
180
+ 'reasoning': 'HuggingFace API key not set',
181
+ 'model': 'mistralai/Mistral-7B-Instruct-v0.3'
182
+ }
183
+
184
  review_text = review.get('review_text', '')
185
  rating = review.get('rating', 3)
186
 
 
222
  }}"""
223
 
224
  try:
225
+ # FIXED: Better error logging
226
+ print(f" πŸ” Calling Mistral API...")
227
+
228
  response = hf_client.text_generation(
229
  prompt,
230
  model="mistralai/Mistral-7B-Instruct-v0.3",
 
232
  temperature=0.1
233
  )
234
 
235
+ print(f" βœ… Got response ({len(response)} chars)")
236
+
237
  # Clean and parse JSON
238
  response_clean = response.strip()
239
  if response_clean.startswith('```'):
 
244
 
245
  result = json.loads(response_clean)
246
  result['model'] = 'mistralai/Mistral-7B-Instruct-v0.3'
247
+
248
+ print(f" βœ… Parsed: {result['user_type']}, {result['emotion']}")
249
  return result
250
 
251
  except Exception as e:
252
+ # FIXED: Show the actual error
253
+ print(f"❌ LLM2 ERROR: {type(e).__name__}: {str(e)}")
254
+ import traceback
255
+ traceback.print_exc()
256
+
257
  return {
258
  'user_type': 'unknown',
259
  'emotion': 'unknown',
260
  'context': 'unknown',
261
  'confidence': 0.0,
262
+ 'reasoning': f'API Error: {str(e)}',
263
  'model': 'mistralai/Mistral-7B-Instruct-v0.3'
264
  }
265
 
266
 
267
  def manager_synthesize(llm1_result: Dict, llm2_result: Dict, review: Dict) -> Dict[str, Any]:
268
  """Manager: Synthesize LLM1 and LLM2 results"""
269
+
270
+ # FIXED: Check if client exists
271
+ if hf_client is None:
272
+ print("❌ ERROR: HuggingFace client not initialized!")
273
+ return {
274
+ 'final_type': llm1_result.get('type', 'unknown'),
275
+ 'final_department': llm1_result.get('department', 'unknown'),
276
+ 'final_priority': llm1_result.get('priority', 'medium'),
277
+ 'synthesis_reasoning': 'HuggingFace API key not set',
278
+ 'model': 'meta-llama/Llama-3.3-70B-Instruct'
279
+ }
280
+
281
  review_text = review.get('review_text', '')
282
  rating = review.get('rating', 3)
283
 
 
304
  "final_type": "from llm1 or adjusted",
305
  "final_department": "from llm1 or adjusted",
306
  "final_priority": "from llm1 or adjusted",
307
+ "synthesis_reasoning": "brief explanation of synthesis"
 
 
 
 
308
  }}"""
309
 
310
  try:
311
+ # FIXED: Better error logging
312
+ print(f" πŸ” Calling Llama Manager API...")
313
+
314
  response = hf_client.text_generation(
315
  prompt,
316
+ model="meta-llama/Llama-3.3-70B-Instruct",
317
+ max_new_tokens=200,
318
  temperature=0.1
319
  )
320
 
321
+ print(f" βœ… Got response ({len(response)} chars)")
322
+
323
  response_clean = response.strip()
324
  if response_clean.startswith('```'):
325
  response_clean = response_clean.split('```')[1]
 
328
  response_clean = response_clean.strip()
329
 
330
  result = json.loads(response_clean)
331
+ result['model'] = 'meta-llama/Llama-3.3-70B-Instruct'
332
+
333
+ print(f" βœ… Manager decision: {result['final_type']} β†’ {result['final_department']}")
334
  return result
335
 
336
  except Exception as e:
337
+ # FIXED: Show the actual error
338
+ print(f"❌ MANAGER ERROR: {type(e).__name__}: {str(e)}")
339
+
340
  return {
341
  'final_type': llm1_result.get('type', 'unknown'),
342
  'final_department': llm1_result.get('department', 'unknown'),
343
  'final_priority': llm1_result.get('priority', 'medium'),
344
+ 'synthesis_reasoning': f'Manager error: {str(e)}',
345
+ 'model': 'meta-llama/Llama-3.3-70B-Instruct'
 
 
 
 
346
  }
347
 
348
 
 
355
  print(f" ⏳ STAGE 1: Classification (Parallel LLM1 + LLM2)...")
356
 
357
  start_time = time.time()
358
+ review_dict = dict(state)
359
 
360
  # PARALLEL EXECUTION: LLM1 and LLM2 run simultaneously
361
  with ThreadPoolExecutor(max_workers=2) as executor:
362
+ future_llm1 = executor.submit(llm1_classify, review_dict)
363
+ future_llm2 = executor.submit(llm2_analyze, review_dict)
364
 
365
+ llm1_result = future_llm1.result()
366
+ llm2_result = future_llm2.result()
367
 
368
+ print(f" βœ… LLM1: {llm1_result['type']} β†’ {llm1_result['department']} (Priority: {llm1_result['priority']})")
369
+ print(f" βœ… LLM2: {llm2_result['user_type']}, {llm2_result['emotion']}")
370
 
371
+ # Manager synthesizes results
372
  print(f" πŸ€– Manager synthesizing...")
373
+ manager_result = manager_synthesize(llm1_result, llm2_result, review_dict)
374
 
375
  stage1_time = time.time() - start_time
376
  print(f" βœ… Stage 1 complete ({stage1_time:.2f}s)")
377
 
378
+ # Save to database
379
+ db = EnhancedDatabase()
380
+ db.connect()
381
+ db.save_stage1_results(state['review_id'], llm1_result, llm2_result, manager_result)
382
+ db.close()
383
+
384
  return {
385
  "llm1_result": llm1_result,
386
  "llm2_result": llm2_result,
387
  "manager_result": manager_result,
388
+ "classification_type": manager_result['final_type'],
389
+ "department": manager_result['final_department'],
390
+ "priority": manager_result['final_priority'],
391
+ "user_type": llm2_result['user_type'],
392
+ "emotion": llm2_result['emotion'],
393
+ "context": llm2_result.get('context', ''),
394
  "stage1_completed": True,
395
  "stage1_time": stage1_time,
396
  "errors": state.get('errors', [])
 
398
 
399
 
400
  # ============================================================================
401
+ # STAGE 2: SENTIMENT ANALYSIS
402
  # ============================================================================
403
 
404
  def analyze_best_sentiment(text: str) -> Dict[str, Any]:
405
+ """Best Model: Twitter-BERT"""
406
  load_sentiment_models()
407
 
408
  try:
 
425
  'model': 'twitter-roberta-base-sentiment-latest'
426
  }
427
  except Exception as e:
428
+ print(f"❌ Best sentiment ERROR: {e}")
429
  return {
430
  'sentiment': 'NEUTRAL',
431
  'confidence': 0.0,
 
461
  'model': 'bertweet-base-sentiment-analysis'
462
  }
463
  except Exception as e:
464
+ print(f"❌ Alt sentiment ERROR: {e}")
465
  return {
466
  'sentiment': 'NEUTRAL',
467
  'confidence': 0.0,
 
534
  stage2_time = time.time() - start_time
535
  print(f" βœ… Stage 2 complete ({stage2_time:.2f}s)")
536
 
537
+ # Save to database
538
+ db = EnhancedDatabase()
539
+ db.connect()
540
+ db.save_stage2_results(state['review_id'], best_result, alt_result, layer_result)
541
+ db.close()
542
+
543
  return {
544
  "best_sentiment_result": best_result,
545
  "alt_sentiment_result": alt_result,
 
565
 
566
  start_time = time.time()
567
 
568
+ # FIXED: Check if client exists
569
+ if hf_client is None:
570
+ print("❌ ERROR: HuggingFace client not initialized!")
571
+ print(" Skipping Stage 3 (requires API key)")
572
+
573
+ result = {
574
+ 'final_sentiment': state.get('sentiment', 'NEUTRAL'),
575
+ 'confidence': state.get('sentiment_confidence', 0.0),
576
+ 'reasoning': 'Stage 3 skipped - HuggingFace API key not set',
577
+ 'validation_notes': 'API key missing',
578
+ 'conflicts_found': 'none',
579
+ 'action_recommendation': f"Route to {state.get('department', 'support')}",
580
+ 'needs_human_review': True,
581
+ 'model': 'meta-llama/Llama-3.1-70B-Instruct'
582
+ }
583
+
584
+ stage3_time = 0.00
585
+ print(f" βœ… Final: {result['final_sentiment']} ({result.get('confidence', 0):.3f})")
586
+ print(f" πŸ“‹ Needs Review: {result.get('needs_human_review', False)}")
587
+ print(f" βœ… Stage 3 complete ({stage3_time:.2f}s)")
588
+
589
+ return {
590
+ "final_result": result,
591
+ "final_sentiment": result['final_sentiment'],
592
+ "final_confidence": result['confidence'],
593
+ "reasoning": result['reasoning'],
594
+ "action_recommendation": result['action_recommendation'],
595
+ "conflicts_found": result['conflicts_found'],
596
+ "validation_notes": result['validation_notes'],
597
+ "needs_human_review": result['needs_human_review'],
598
+ "stage3_completed": True,
599
+ "stage3_time": stage3_time,
600
+ "total_time": state.get('stage1_time', 0) + state.get('stage2_time', 0),
601
+ "processing_completed_at": datetime.now().isoformat(),
602
+ "errors": state.get('errors', [])
603
+ }
604
+
605
  review_text = state['review_text']
606
  rating = state['rating']
607
 
 
642
  }}"""
643
 
644
  try:
645
+ # FIXED: Better error logging
646
+ print(f" πŸ” Calling Llama 70B API...")
647
+
648
  response = hf_client.text_generation(
649
  prompt,
650
  model="meta-llama/Llama-3.1-70B-Instruct",
 
652
  temperature=0.1
653
  )
654
 
655
+ print(f" βœ… Got response ({len(response)} chars)")
656
+
657
  response_clean = response.strip()
658
  if response_clean.startswith('```'):
659
  response_clean = response_clean.split('```')[1]
 
665
  result['model'] = 'meta-llama/Llama-3.1-70B-Instruct'
666
 
667
  except Exception as e:
668
+ # FIXED: Show the actual error
669
+ print(f"❌ STAGE 3 ERROR: {type(e).__name__}: {str(e)}")
670
+ import traceback
671
+ traceback.print_exc()
672
+
673
  result = {
674
  'final_sentiment': state.get('sentiment', 'NEUTRAL'),
675
  'confidence': state.get('sentiment_confidence', 0.5),
 
690
  # Calculate total time
691
  total_time = state.get('stage1_time', 0) + state.get('stage2_time', 0) + stage3_time
692
 
693
+ # Save to database
694
+ db = EnhancedDatabase()
695
+ db.connect()
696
+ db.save_stage3_results(state['review_id'], result)
697
+ db.close()
698
+
699
  return {
700
  "final_result": result,
701
  "final_sentiment": result['final_sentiment'],
 
718
  print(" Nodes available:")
719
  print(" - stage1_classification_node (parallel LLM1+LLM2)")
720
  print(" - stage2_sentiment_node (parallel Best+Alt)")
721
+ print(" - stage3_finalization_node (LLM3)")