riazmo commited on
Commit
868df9f
Β·
verified Β·
1 Parent(s): 1da16aa

Update langgraph_nodes.py

Browse files
Files changed (1) hide show
  1. langgraph_nodes.py +152 -174
langgraph_nodes.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- LangGraph Nodes - LAZY LOADING VERSION
3
- Initializes HF client when needed, not at module import
4
  """
5
 
6
  import os
@@ -19,7 +19,6 @@ from langgraph_state import ReviewState, BatchState
19
  from database_enhanced import EnhancedDatabase
20
 
21
  # FIXED: Don't initialize client at module import
22
- # Initialize LAZILY when first needed
23
  _hf_client = None
24
 
25
  def get_hf_client():
@@ -33,7 +32,6 @@ def get_hf_client():
33
  HF_TOKEN = os.getenv("HUGGINGFACE_API_KEY")
34
 
35
  if not HF_TOKEN or HF_TOKEN.strip() == "":
36
- # No token available
37
  return None
38
 
39
  # Initialize client with token
@@ -42,7 +40,7 @@ def get_hf_client():
42
  return _hf_client
43
 
44
 
45
- # Initialize sentiment models (singleton) - load once
46
  _sentiment_models_loaded = False
47
  _best_tokenizer = None
48
  _best_model = None
@@ -63,11 +61,11 @@ def load_sentiment_models():
63
  _best_model = AutoModelForSequenceClassification.from_pretrained("cardiffnlp/twitter-roberta-base-sentiment-latest")
64
  _best_model.eval()
65
 
66
- # Alternate Model - FIXED: Proper loading
67
  _alt_tokenizer = AutoTokenizer.from_pretrained("finiteautomata/bertweet-base-sentiment-analysis")
68
  _alt_model = AutoModelForSequenceClassification.from_pretrained(
69
  "finiteautomata/bertweet-base-sentiment-analysis",
70
- torch_dtype=torch.float32 # FIXED: Explicit dtype to avoid meta tensors
71
  )
72
  _alt_model.eval()
73
 
@@ -76,13 +74,12 @@ def load_sentiment_models():
76
 
77
 
78
  # ============================================================================
79
- # STAGE 1: CLASSIFICATION NODE (Parallel LLM1 + LLM2)
80
  # ============================================================================
81
 
82
  def llm1_classify(review: Dict[str, Any]) -> Dict[str, Any]:
83
  """LLM1: Type, Department, Priority classification"""
84
 
85
- # FIXED: Get client lazily
86
  hf_client = get_hf_client()
87
 
88
  if hf_client is None:
@@ -98,67 +95,59 @@ def llm1_classify(review: Dict[str, Any]) -> Dict[str, Any]:
98
  review_text = review.get('review_text', '')
99
  rating = review.get('rating', 3)
100
 
101
- prompt = f"""You are an expert at classifying customer reviews for theme park and attraction apps.
 
102
 
103
- REVIEW:
104
- Rating: {rating}/5
105
- Text: {review_text}
106
-
107
- Classify this review across these dimensions:
108
-
109
- 1. TYPE (choose ONE):
110
- - complaint: Customer reports a problem
111
- - praise: Customer expresses satisfaction
112
- - suggestion: Customer proposes improvement
113
- - question: Customer asks about something
114
- - bug_report: Technical issue described
115
-
116
- 2. DEPARTMENT (choose ONE):
117
- - engineering: Technical issues, bugs, crashes
118
- - ux: Design, usability, interface issues
119
- - support: Customer service, help needed
120
- - business: Pricing, policies, marketing
121
-
122
- 3. PRIORITY (choose ONE):
123
- - critical: Service down, major blocker
124
- - high: Significant problem affecting use
125
- - medium: Inconvenience but not blocking
126
- - low: Minor issue or suggestion
127
-
128
- 4. CONFIDENCE (0.0-1.0): How confident are you?
129
 
 
 
 
 
130
  5. REASONING: Brief one-sentence explanation
131
 
132
  Respond ONLY in valid JSON format:
133
- {{
134
  "type": "complaint/praise/suggestion/question/bug_report",
135
  "department": "engineering/ux/support/business",
136
  "priority": "critical/high/medium/low",
137
  "confidence": 0.0-1.0,
138
  "reasoning": "brief explanation"
139
- }}"""
 
 
 
 
 
 
140
 
141
  try:
142
  print(f" πŸ” Calling Qwen API...")
143
 
144
- response = hf_client.text_generation(
145
- prompt,
 
 
 
 
146
  model="Qwen/Qwen2.5-72B-Instruct",
147
- max_new_tokens=200,
148
  temperature=0.1
149
  )
150
 
151
- print(f" βœ… Got response ({len(response)} chars)")
 
 
152
 
153
  # Clean and parse JSON
154
- response_clean = response.strip()
155
- if response_clean.startswith('```'):
156
- response_clean = response_clean.split('```')[1]
157
- if response_clean.startswith('json'):
158
- response_clean = response_clean[4:]
159
- response_clean = response_clean.strip()
160
-
161
- result = json.loads(response_clean)
162
  result['model'] = 'Qwen/Qwen2.5-72B-Instruct'
163
 
164
  print(f" βœ… Parsed: {result['type']} β†’ {result['department']}")
@@ -180,7 +169,6 @@ Respond ONLY in valid JSON format:
180
  def llm2_analyze(review: Dict[str, Any]) -> Dict[str, Any]:
181
  """LLM2: User type, Emotion, Context analysis"""
182
 
183
- # FIXED: Get client lazily
184
  hf_client = get_hf_client()
185
 
186
  if hf_client is None:
@@ -196,64 +184,57 @@ def llm2_analyze(review: Dict[str, Any]) -> Dict[str, Any]:
196
  review_text = review.get('review_text', '')
197
  rating = review.get('rating', 3)
198
 
199
- prompt = f"""You are an expert at understanding customer psychology and emotional context.
200
-
201
- REVIEW:
202
- Rating: {rating}/5
203
- Text: {review_text}
204
-
205
- Analyze the user and emotional context:
206
-
207
- 1. USER_TYPE (choose ONE):
208
- - new_user: First-time or new user
209
- - regular_user: Returning customer
210
- - power_user: Heavy user, tech-savvy
211
- - churning_user: Considering leaving
212
-
213
- 2. EMOTION (choose ONE):
214
- - anger: Angry, hostile tone
215
- - frustration: Frustrated but not angry
216
- - joy: Happy, satisfied
217
- - satisfaction: Content, pleased
218
- - disappointment: Let down, sad
219
- - confusion: Unclear, needs help
220
-
221
- 3. CONTEXT (brief): What is the underlying issue? 1-2 words
222
 
223
- 4. CONFIDENCE (0.0-1.0): How confident are you?
224
-
225
- 5. REASONING: Brief one-sentence explanation
 
 
 
226
 
227
  Respond ONLY in valid JSON format:
228
- {{
229
  "user_type": "new_user/regular_user/power_user/churning_user",
230
  "emotion": "anger/frustration/joy/satisfaction/disappointment/confusion",
231
  "context": "brief context",
232
  "confidence": 0.0-1.0,
233
  "reasoning": "brief explanation"
234
- }}"""
 
 
 
 
 
 
235
 
236
  try:
237
  print(f" πŸ” Calling Mistral API...")
238
 
239
- response = hf_client.text_generation(
240
- prompt,
 
 
 
 
241
  model="mistralai/Mistral-7B-Instruct-v0.3",
242
- max_new_tokens=200,
243
  temperature=0.1
244
  )
245
 
246
- print(f" βœ… Got response ({len(response)} chars)")
 
247
 
248
  # Clean and parse JSON
249
- response_clean = response.strip()
250
- if response_clean.startswith('```'):
251
- response_clean = response_clean.split('```')[1]
252
- if response_clean.startswith('json'):
253
- response_clean = response_clean[4:]
254
- response_clean = response_clean.strip()
255
-
256
- result = json.loads(response_clean)
257
  result['model'] = 'mistralai/Mistral-7B-Instruct-v0.3'
258
 
259
  print(f" βœ… Parsed: {result['user_type']}, {result['emotion']}")
@@ -275,7 +256,6 @@ Respond ONLY in valid JSON format:
275
  def manager_synthesize(llm1_result: Dict, llm2_result: Dict, review: Dict) -> Dict[str, Any]:
276
  """Manager: Synthesize LLM1 and LLM2 results"""
277
 
278
- # FIXED: Get client lazily
279
  hf_client = get_hf_client()
280
 
281
  if hf_client is None:
@@ -290,52 +270,60 @@ def manager_synthesize(llm1_result: Dict, llm2_result: Dict, review: Dict) -> Di
290
  review_text = review.get('review_text', '')
291
  rating = review.get('rating', 3)
292
 
293
- prompt = f"""You are a synthesis manager evaluating two AI analyses of the same review.
294
-
295
- REVIEW:
296
- Rating: {rating}/5
297
- Text: {review_text}
298
-
299
- LLM1 ANALYSIS (Type/Dept/Priority):
300
- {json.dumps(llm1_result, indent=2)}
301
-
302
- LLM2 ANALYSIS (User/Emotion/Context):
303
- {json.dumps(llm2_result, indent=2)}
304
 
305
  Your task:
306
  1. Validate both analyses
307
- 2. Resolve any conflicts
308
  3. Make final classification decision
309
  4. Provide synthesis reasoning
310
 
311
  Respond ONLY in valid JSON format:
312
- {{
313
  "final_type": "from llm1 or adjusted",
314
  "final_department": "from llm1 or adjusted",
315
  "final_priority": "from llm1 or adjusted",
316
- "synthesis_reasoning": "brief explanation of synthesis"
317
- }}"""
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
  try:
320
  print(f" πŸ” Calling Llama Manager API...")
321
 
322
- response = hf_client.text_generation(
323
- prompt,
 
 
 
 
324
  model="meta-llama/Llama-3.3-70B-Instruct",
325
- max_new_tokens=200,
326
  temperature=0.1
327
  )
328
 
329
- print(f" βœ… Got response ({len(response)} chars)")
 
330
 
331
- response_clean = response.strip()
332
- if response_clean.startswith('```'):
333
- response_clean = response_clean.split('```')[1]
334
- if response_clean.startswith('json'):
335
- response_clean = response_clean[4:]
336
- response_clean = response_clean.strip()
337
 
338
- result = json.loads(response_clean)
339
  result['model'] = 'meta-llama/Llama-3.3-70B-Instruct'
340
 
341
  print(f" βœ… Manager decision: {result['final_type']} β†’ {result['final_department']}")
@@ -354,17 +342,14 @@ Respond ONLY in valid JSON format:
354
 
355
 
356
  def stage1_classification_node(state: ReviewState) -> Dict[str, Any]:
357
- """
358
- Stage 1 Node: Classification with PARALLEL execution
359
- Runs LLM1 and LLM2 in parallel, then Manager synthesizes
360
- """
361
  print(f"\n πŸ“ Review ID: {state['review_id']}")
362
  print(f" ⏳ STAGE 1: Classification (Parallel LLM1 + LLM2)...")
363
 
364
  start_time = time.time()
365
  review_dict = dict(state)
366
 
367
- # PARALLEL EXECUTION: LLM1 and LLM2 run simultaneously
368
  with ThreadPoolExecutor(max_workers=2) as executor:
369
  future_llm1 = executor.submit(llm1_classify, review_dict)
370
  future_llm2 = executor.submit(llm2_analyze, review_dict)
@@ -375,7 +360,7 @@ def stage1_classification_node(state: ReviewState) -> Dict[str, Any]:
375
  print(f" βœ… LLM1: {llm1_result['type']} β†’ {llm1_result['department']} (Priority: {llm1_result['priority']})")
376
  print(f" βœ… LLM2: {llm2_result['user_type']}, {llm2_result['emotion']}")
377
 
378
- # Manager synthesizes results
379
  print(f" πŸ€– Manager synthesizing...")
380
  manager_result = manager_synthesize(llm1_result, llm2_result, review_dict)
381
 
@@ -439,7 +424,7 @@ def analyze_best_sentiment(text: str) -> Dict[str, Any]:
439
 
440
 
441
  def analyze_alt_sentiment(text: str) -> Dict[str, Any]:
442
- """Alternate Model: BERTweet - FIXED version"""
443
  load_sentiment_models()
444
 
445
  try:
@@ -447,16 +432,7 @@ def analyze_alt_sentiment(text: str) -> Dict[str, Any]:
447
 
448
  with torch.no_grad():
449
  outputs = _alt_model(**inputs)
450
- logits = outputs.logits
451
-
452
- # FIXED: Check if logits are on meta device
453
- if logits.device.type == 'meta':
454
- print("⚠️ Warning: Model on meta device, moving to CPU")
455
- _alt_model.to('cpu')
456
- outputs = _alt_model(**inputs)
457
- logits = outputs.logits
458
-
459
- probs = torch.nn.functional.softmax(logits, dim=-1)
460
  prediction = torch.argmax(probs, dim=-1).item()
461
  confidence = probs[0][prediction].item()
462
 
@@ -515,16 +491,13 @@ def sentiment_layer(best_result: Dict, alt_result: Dict) -> Dict[str, Any]:
515
 
516
 
517
  def stage2_sentiment_node(state: ReviewState) -> Dict[str, Any]:
518
- """
519
- Stage 2 Node: Sentiment with PARALLEL execution
520
- Runs Best and Alternate models in parallel, then combines
521
- """
522
  print(f"\n ⏳ STAGE 2: Sentiment Analysis (Parallel Best + Alternate)...")
523
 
524
  start_time = time.time()
525
  review_text = state['review_text']
526
 
527
- # PARALLEL EXECUTION: Best and Alternate models run simultaneously
528
  with ThreadPoolExecutor(max_workers=2) as executor:
529
  future_best = executor.submit(analyze_best_sentiment, review_text)
530
  future_alt = executor.submit(analyze_alt_sentiment, review_text)
@@ -562,14 +535,11 @@ def stage2_sentiment_node(state: ReviewState) -> Dict[str, Any]:
562
  # ============================================================================
563
 
564
  def stage3_finalization_node(state: ReviewState) -> Dict[str, Any]:
565
- """
566
- Stage 3 Node: Final synthesis with LLM3 (Llama 70B)
567
- """
568
  print(f"\n ⏳ STAGE 3: Finalization (LLM3)...")
569
 
570
  start_time = time.time()
571
 
572
- # FIXED: Get client lazily
573
  hf_client = get_hf_client()
574
 
575
  if hf_client is None:
@@ -608,9 +578,28 @@ def stage3_finalization_node(state: ReviewState) -> Dict[str, Any]:
608
  review_text = state['review_text']
609
  rating = state['rating']
610
 
611
- prompt = f"""You are a final decision-making AI analyzing customer feedback for a theme park/attraction app.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
 
613
- REVIEW DATA:
614
  Rating: {rating}/5
615
  Text: {review_text}
616
 
@@ -626,44 +615,33 @@ STAGE 2 SENTIMENT:
626
  - Alternate: {state['alt_sentiment_result'].get('sentiment')} ({state['alt_sentiment_result'].get('confidence'):.2f})
627
  - Agreement: {state.get('sentiment_agreement')}
628
 
629
- YOUR TASK:
630
- 1. Review all data from both stages
631
- 2. Make FINAL sentiment decision
632
- 3. Provide comprehensive reasoning
633
- 4. Generate action recommendation
634
- 5. Flag if human review needed
635
-
636
- Respond ONLY in valid JSON format:
637
- {{
638
- "final_sentiment": "POSITIVE/NEGATIVE/NEUTRAL",
639
- "confidence": 0.0-1.0,
640
- "reasoning": "Comprehensive explanation",
641
- "validation_notes": "Does classification match sentiment?",
642
- "conflicts_found": "any conflicts or 'none'",
643
- "action_recommendation": "Specific action",
644
- "needs_human_review": true/false
645
- }}"""
646
 
647
  try:
648
  print(f" πŸ” Calling Llama 70B API...")
649
 
650
- response = hf_client.text_generation(
651
- prompt,
 
 
 
 
652
  model="meta-llama/Llama-3.1-70B-Instruct",
653
- max_new_tokens=400,
654
  temperature=0.1
655
  )
656
 
657
- print(f" βœ… Got response ({len(response)} chars)")
 
658
 
659
- response_clean = response.strip()
660
- if response_clean.startswith('```'):
661
- response_clean = response_clean.split('```')[1]
662
- if response_clean.startswith('json'):
663
- response_clean = response_clean[4:]
664
- response_clean = response_clean.strip()
665
 
666
- result = json.loads(response_clean)
667
  result['model'] = 'meta-llama/Llama-3.1-70B-Instruct'
668
 
669
  except Exception as e:
 
1
  """
2
+ LangGraph Nodes - FINAL WORKING VERSION
3
+ Uses chat_completion() API format + Lazy loading + Fixed alt sentiment
4
  """
5
 
6
  import os
 
19
  from database_enhanced import EnhancedDatabase
20
 
21
  # FIXED: Don't initialize client at module import
 
22
  _hf_client = None
23
 
24
  def get_hf_client():
 
32
  HF_TOKEN = os.getenv("HUGGINGFACE_API_KEY")
33
 
34
  if not HF_TOKEN or HF_TOKEN.strip() == "":
 
35
  return None
36
 
37
  # Initialize client with token
 
40
  return _hf_client
41
 
42
 
43
+ # Initialize sentiment models (singleton)
44
  _sentiment_models_loaded = False
45
  _best_tokenizer = None
46
  _best_model = None
 
61
  _best_model = AutoModelForSequenceClassification.from_pretrained("cardiffnlp/twitter-roberta-base-sentiment-latest")
62
  _best_model.eval()
63
 
64
+ # Alternate Model - FIXED: Load with low_cpu_mem_usage to avoid meta tensors
65
  _alt_tokenizer = AutoTokenizer.from_pretrained("finiteautomata/bertweet-base-sentiment-analysis")
66
  _alt_model = AutoModelForSequenceClassification.from_pretrained(
67
  "finiteautomata/bertweet-base-sentiment-analysis",
68
+ low_cpu_mem_usage=False # FIXED: Don't use meta device
69
  )
70
  _alt_model.eval()
71
 
 
74
 
75
 
76
  # ============================================================================
77
+ # STAGE 1: CLASSIFICATION NODE
78
  # ============================================================================
79
 
80
  def llm1_classify(review: Dict[str, Any]) -> Dict[str, Any]:
81
  """LLM1: Type, Department, Priority classification"""
82
 
 
83
  hf_client = get_hf_client()
84
 
85
  if hf_client is None:
 
95
  review_text = review.get('review_text', '')
96
  rating = review.get('rating', 3)
97
 
98
+ # FIXED: Use chat format with system + user messages
99
+ system_prompt = """You are an expert at classifying customer reviews for theme park and attraction apps.
100
 
101
+ Classify reviews across these dimensions:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ 1. TYPE: complaint, praise, suggestion, question, or bug_report
104
+ 2. DEPARTMENT: engineering, ux, support, or business
105
+ 3. PRIORITY: critical, high, medium, or low
106
+ 4. CONFIDENCE: 0.0-1.0
107
  5. REASONING: Brief one-sentence explanation
108
 
109
  Respond ONLY in valid JSON format:
110
+ {
111
  "type": "complaint/praise/suggestion/question/bug_report",
112
  "department": "engineering/ux/support/business",
113
  "priority": "critical/high/medium/low",
114
  "confidence": 0.0-1.0,
115
  "reasoning": "brief explanation"
116
+ }"""
117
+
118
+ user_prompt = f"""REVIEW:
119
+ Rating: {rating}/5
120
+ Text: {review_text}
121
+
122
+ Classify this review:"""
123
 
124
  try:
125
  print(f" πŸ” Calling Qwen API...")
126
 
127
+ # FIXED: Use chat_completion instead of text_generation
128
+ response = hf_client.chat_completion(
129
+ messages=[
130
+ {"role": "system", "content": system_prompt},
131
+ {"role": "user", "content": user_prompt}
132
+ ],
133
  model="Qwen/Qwen2.5-72B-Instruct",
134
+ max_tokens=200,
135
  temperature=0.1
136
  )
137
 
138
+ # Extract content from chat response
139
+ content = response.choices[0].message.content
140
+ print(f" βœ… Got response ({len(content)} chars)")
141
 
142
  # Clean and parse JSON
143
+ content_clean = content.strip()
144
+ if content_clean.startswith('```'):
145
+ content_clean = content_clean.split('```')[1]
146
+ if content_clean.startswith('json'):
147
+ content_clean = content_clean[4:]
148
+ content_clean = content_clean.strip()
149
+
150
+ result = json.loads(content_clean)
151
  result['model'] = 'Qwen/Qwen2.5-72B-Instruct'
152
 
153
  print(f" βœ… Parsed: {result['type']} β†’ {result['department']}")
 
169
  def llm2_analyze(review: Dict[str, Any]) -> Dict[str, Any]:
170
  """LLM2: User type, Emotion, Context analysis"""
171
 
 
172
  hf_client = get_hf_client()
173
 
174
  if hf_client is None:
 
184
  review_text = review.get('review_text', '')
185
  rating = review.get('rating', 3)
186
 
187
+ # FIXED: Use chat format
188
+ system_prompt = """You are an expert at understanding customer psychology and emotional context.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ Analyze reviews for:
191
+ 1. USER_TYPE: new_user, regular_user, power_user, or churning_user
192
+ 2. EMOTION: anger, frustration, joy, satisfaction, disappointment, or confusion
193
+ 3. CONTEXT: Brief context (1-2 words)
194
+ 4. CONFIDENCE: 0.0-1.0
195
+ 5. REASONING: Brief explanation
196
 
197
  Respond ONLY in valid JSON format:
198
+ {
199
  "user_type": "new_user/regular_user/power_user/churning_user",
200
  "emotion": "anger/frustration/joy/satisfaction/disappointment/confusion",
201
  "context": "brief context",
202
  "confidence": 0.0-1.0,
203
  "reasoning": "brief explanation"
204
+ }"""
205
+
206
+ user_prompt = f"""REVIEW:
207
+ Rating: {rating}/5
208
+ Text: {review_text}
209
+
210
+ Analyze this review:"""
211
 
212
  try:
213
  print(f" πŸ” Calling Mistral API...")
214
 
215
+ # FIXED: Use chat_completion
216
+ response = hf_client.chat_completion(
217
+ messages=[
218
+ {"role": "system", "content": system_prompt},
219
+ {"role": "user", "content": user_prompt}
220
+ ],
221
  model="mistralai/Mistral-7B-Instruct-v0.3",
222
+ max_tokens=200,
223
  temperature=0.1
224
  )
225
 
226
+ content = response.choices[0].message.content
227
+ print(f" βœ… Got response ({len(content)} chars)")
228
 
229
  # Clean and parse JSON
230
+ content_clean = content.strip()
231
+ if content_clean.startswith('```'):
232
+ content_clean = content_clean.split('```')[1]
233
+ if content_clean.startswith('json'):
234
+ content_clean = content_clean[4:]
235
+ content_clean = content_clean.strip()
236
+
237
+ result = json.loads(content_clean)
238
  result['model'] = 'mistralai/Mistral-7B-Instruct-v0.3'
239
 
240
  print(f" βœ… Parsed: {result['user_type']}, {result['emotion']}")
 
256
  def manager_synthesize(llm1_result: Dict, llm2_result: Dict, review: Dict) -> Dict[str, Any]:
257
  """Manager: Synthesize LLM1 and LLM2 results"""
258
 
 
259
  hf_client = get_hf_client()
260
 
261
  if hf_client is None:
 
270
  review_text = review.get('review_text', '')
271
  rating = review.get('rating', 3)
272
 
273
+ # FIXED: Use chat format
274
+ system_prompt = """You are a synthesis manager evaluating two AI analyses.
 
 
 
 
 
 
 
 
 
275
 
276
  Your task:
277
  1. Validate both analyses
278
+ 2. Resolve conflicts
279
  3. Make final classification decision
280
  4. Provide synthesis reasoning
281
 
282
  Respond ONLY in valid JSON format:
283
+ {
284
  "final_type": "from llm1 or adjusted",
285
  "final_department": "from llm1 or adjusted",
286
  "final_priority": "from llm1 or adjusted",
287
+ "synthesis_reasoning": "brief explanation"
288
+ }"""
289
+
290
+ user_prompt = f"""REVIEW:
291
+ Rating: {rating}/5
292
+ Text: {review_text}
293
+
294
+ LLM1 ANALYSIS (Type/Dept/Priority):
295
+ {json.dumps(llm1_result, indent=2)}
296
+
297
+ LLM2 ANALYSIS (User/Emotion/Context):
298
+ {json.dumps(llm2_result, indent=2)}
299
+
300
+ Synthesize these analyses:"""
301
 
302
  try:
303
  print(f" πŸ” Calling Llama Manager API...")
304
 
305
+ # FIXED: Use chat_completion
306
+ response = hf_client.chat_completion(
307
+ messages=[
308
+ {"role": "system", "content": system_prompt},
309
+ {"role": "user", "content": user_prompt}
310
+ ],
311
  model="meta-llama/Llama-3.3-70B-Instruct",
312
+ max_tokens=200,
313
  temperature=0.1
314
  )
315
 
316
+ content = response.choices[0].message.content
317
+ print(f" βœ… Got response ({len(content)} chars)")
318
 
319
+ content_clean = content.strip()
320
+ if content_clean.startswith('```'):
321
+ content_clean = content_clean.split('```')[1]
322
+ if content_clean.startswith('json'):
323
+ content_clean = content_clean[4:]
324
+ content_clean = content_clean.strip()
325
 
326
+ result = json.loads(content_clean)
327
  result['model'] = 'meta-llama/Llama-3.3-70B-Instruct'
328
 
329
  print(f" βœ… Manager decision: {result['final_type']} β†’ {result['final_department']}")
 
342
 
343
 
344
  def stage1_classification_node(state: ReviewState) -> Dict[str, Any]:
345
+ """Stage 1 Node: Classification with PARALLEL execution"""
 
 
 
346
  print(f"\n πŸ“ Review ID: {state['review_id']}")
347
  print(f" ⏳ STAGE 1: Classification (Parallel LLM1 + LLM2)...")
348
 
349
  start_time = time.time()
350
  review_dict = dict(state)
351
 
352
+ # PARALLEL EXECUTION
353
  with ThreadPoolExecutor(max_workers=2) as executor:
354
  future_llm1 = executor.submit(llm1_classify, review_dict)
355
  future_llm2 = executor.submit(llm2_analyze, review_dict)
 
360
  print(f" βœ… LLM1: {llm1_result['type']} β†’ {llm1_result['department']} (Priority: {llm1_result['priority']})")
361
  print(f" βœ… LLM2: {llm2_result['user_type']}, {llm2_result['emotion']}")
362
 
363
+ # Manager synthesizes
364
  print(f" πŸ€– Manager synthesizing...")
365
  manager_result = manager_synthesize(llm1_result, llm2_result, review_dict)
366
 
 
424
 
425
 
426
  def analyze_alt_sentiment(text: str) -> Dict[str, Any]:
427
+ """Alternate Model: BERTweet - FIXED"""
428
  load_sentiment_models()
429
 
430
  try:
 
432
 
433
  with torch.no_grad():
434
  outputs = _alt_model(**inputs)
435
+ probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
 
 
 
 
 
 
 
 
 
436
  prediction = torch.argmax(probs, dim=-1).item()
437
  confidence = probs[0][prediction].item()
438
 
 
491
 
492
 
493
  def stage2_sentiment_node(state: ReviewState) -> Dict[str, Any]:
494
+ """Stage 2 Node: Sentiment with PARALLEL execution"""
 
 
 
495
  print(f"\n ⏳ STAGE 2: Sentiment Analysis (Parallel Best + Alternate)...")
496
 
497
  start_time = time.time()
498
  review_text = state['review_text']
499
 
500
+ # PARALLEL EXECUTION
501
  with ThreadPoolExecutor(max_workers=2) as executor:
502
  future_best = executor.submit(analyze_best_sentiment, review_text)
503
  future_alt = executor.submit(analyze_alt_sentiment, review_text)
 
535
  # ============================================================================
536
 
537
  def stage3_finalization_node(state: ReviewState) -> Dict[str, Any]:
538
+ """Stage 3 Node: Final synthesis with LLM3"""
 
 
539
  print(f"\n ⏳ STAGE 3: Finalization (LLM3)...")
540
 
541
  start_time = time.time()
542
 
 
543
  hf_client = get_hf_client()
544
 
545
  if hf_client is None:
 
578
  review_text = state['review_text']
579
  rating = state['rating']
580
 
581
+ # FIXED: Use chat format
582
+ system_prompt = """You are a final decision-making AI analyzing customer feedback for a theme park/attraction app.
583
+
584
+ Your task:
585
+ 1. Review all data from previous stages
586
+ 2. Make FINAL sentiment decision
587
+ 3. Provide comprehensive reasoning
588
+ 4. Generate action recommendation
589
+ 5. Flag if human review needed
590
+
591
+ Respond ONLY in valid JSON format:
592
+ {
593
+ "final_sentiment": "POSITIVE/NEGATIVE/NEUTRAL",
594
+ "confidence": 0.0-1.0,
595
+ "reasoning": "Comprehensive explanation",
596
+ "validation_notes": "Does classification match sentiment?",
597
+ "conflicts_found": "any conflicts or 'none'",
598
+ "action_recommendation": "Specific action",
599
+ "needs_human_review": true/false
600
+ }"""
601
 
602
+ user_prompt = f"""REVIEW DATA:
603
  Rating: {rating}/5
604
  Text: {review_text}
605
 
 
615
  - Alternate: {state['alt_sentiment_result'].get('sentiment')} ({state['alt_sentiment_result'].get('confidence'):.2f})
616
  - Agreement: {state.get('sentiment_agreement')}
617
 
618
+ Make your final decision:"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
  try:
621
  print(f" πŸ” Calling Llama 70B API...")
622
 
623
+ # FIXED: Use chat_completion
624
+ response = hf_client.chat_completion(
625
+ messages=[
626
+ {"role": "system", "content": system_prompt},
627
+ {"role": "user", "content": user_prompt}
628
+ ],
629
  model="meta-llama/Llama-3.1-70B-Instruct",
630
+ max_tokens=400,
631
  temperature=0.1
632
  )
633
 
634
+ content = response.choices[0].message.content
635
+ print(f" βœ… Got response ({len(content)} chars)")
636
 
637
+ content_clean = content.strip()
638
+ if content_clean.startswith('```'):
639
+ content_clean = content_clean.split('```')[1]
640
+ if content_clean.startswith('json'):
641
+ content_clean = content_clean[4:]
642
+ content_clean = content_clean.strip()
643
 
644
+ result = json.loads(content_clean)
645
  result['model'] = 'meta-llama/Llama-3.1-70B-Instruct'
646
 
647
  except Exception as e: