TushP commited on
Commit
7dab48f
Β·
verified Β·
1 Parent(s): dfae130

Upload folder using huggingface_hub

Browse files
src/agent/insights_generator.py CHANGED
@@ -2,7 +2,10 @@
2
  Restaurant Insights Generator - EXPANDED VERSION
3
  Generates role-specific insights for Chef and Manager personas.
4
 
5
- UPDATED: Now uses TOP 20 items/aspects instead of TOP 10 for more comprehensive insights.
 
 
 
6
  """
7
 
8
  import json
@@ -14,7 +17,10 @@ class InsightsGenerator:
14
  """
15
  Generates actionable insights for different restaurant roles.
16
 
17
- UPDATED: Expanded to use top 20 menu items and aspects for better recommendations.
 
 
 
18
  """
19
 
20
  def __init__(self, client, model: str = "claude-sonnet-4-20250514"):
@@ -109,6 +115,11 @@ MENU PERFORMANCE (Top items by customer mentions):
109
  FOOD-RELATED ASPECTS:
110
  {aspect_summary}
111
 
 
 
 
 
 
112
  YOUR TASK:
113
  Generate actionable insights specifically for the HEAD CHEF. Focus on:
114
  - Food quality and taste
@@ -121,30 +132,32 @@ Generate actionable insights specifically for the HEAD CHEF. Focus on:
121
 
122
  CRITICAL RULES:
123
  1. Focus ONLY on food/kitchen topics
124
- 2. Be specific with evidence from reviews
125
- 3. Make recommendations actionable
126
- 4. Reference specific menu items by name
127
- 5. Output ONLY valid JSON, no other text
 
 
128
 
129
  OUTPUT FORMAT (JSON):
130
  {{
131
  "summary": "2-3 sentence executive summary covering overall kitchen performance",
132
  "strengths": [
133
- "Specific strength 1 with menu item example",
134
- "Specific strength 2 with menu item example",
135
- "Specific strength 3 with menu item example",
136
- "Specific strength 4 with menu item example",
137
- "Specific strength 5 with menu item example"
138
  ],
139
  "concerns": [
140
- "Specific concern 1 with evidence",
141
- "Specific concern 2 with evidence",
142
- "Specific concern 3 with evidence"
143
  ],
144
  "recommendations": [
145
  {{
146
  "priority": "high",
147
- "action": "Specific action to take",
148
  "reason": "Why this matters based on review data",
149
  "evidence": "Supporting data from reviews"
150
  }},
@@ -176,14 +189,15 @@ OUTPUT FORMAT (JSON):
176
  }}
177
 
178
  IMPORTANT:
179
- - Provide at least 5 strengths and 5 recommendations
 
180
  - Reference actual menu items from the data above
181
  - Ensure all JSON is properly formatted with no trailing commas
182
 
183
  Generate chef insights:"""
184
 
185
  return prompt
186
-
187
  def _build_manager_prompt(
188
  self,
189
  analysis_data: Dict[str, Any],
@@ -204,6 +218,11 @@ OPERATIONAL ASPECTS (All discovered from reviews):
204
  MENU OVERVIEW (for context):
205
  {menu_summary}
206
 
 
 
 
 
 
207
  YOUR TASK:
208
  Generate actionable insights specifically for the RESTAURANT MANAGER. Focus on:
209
  - Service quality and speed
@@ -217,30 +236,32 @@ Generate actionable insights specifically for the RESTAURANT MANAGER. Focus on:
217
 
218
  CRITICAL RULES:
219
  1. Focus ONLY on operations/service topics
220
- 2. Be specific with evidence from reviews
221
- 3. Make recommendations actionable
222
- 4. Reference specific aspects by name
223
- 5. Output ONLY valid JSON, no other text
 
 
224
 
225
  OUTPUT FORMAT (JSON):
226
  {{
227
  "summary": "2-3 sentence executive summary covering overall operations",
228
  "strengths": [
229
- "Specific operational strength 1 with evidence",
230
- "Specific operational strength 2 with evidence",
231
- "Specific operational strength 3 with evidence",
232
- "Specific operational strength 4 with evidence",
233
- "Specific operational strength 5 with evidence"
234
  ],
235
  "concerns": [
236
- "Specific operational concern 1 with evidence",
237
- "Specific operational concern 2 with evidence",
238
- "Specific operational concern 3 with evidence"
239
  ],
240
  "recommendations": [
241
  {{
242
  "priority": "high",
243
- "action": "Specific action to take",
244
  "reason": "Why this matters based on review data",
245
  "evidence": "Supporting data from reviews"
246
  }},
@@ -272,14 +293,15 @@ OUTPUT FORMAT (JSON):
272
  }}
273
 
274
  IMPORTANT:
275
- - Provide at least 5 strengths and 5 recommendations
 
276
  - Reference actual aspects from the data above
277
  - Ensure all JSON is properly formatted with no trailing commas
278
 
279
  Generate manager insights:"""
280
 
281
  return prompt
282
-
283
  def _summarize_menu_data(
284
  self,
285
  analysis_data: Dict[str, Any],
@@ -289,7 +311,7 @@ Generate manager insights:"""
289
  """
290
  Summarize menu analysis for prompts.
291
 
292
- EXPANDED: Now uses top 20 food items and top 10 drinks (was 10/5).
293
  """
294
  menu_data = analysis_data.get('menu_analysis', {})
295
  food_items = menu_data.get('food_items', [])[:max_food]
@@ -302,8 +324,8 @@ Generate manager insights:"""
302
  for item in food_items:
303
  sentiment = item.get('sentiment', 0)
304
  mentions = item.get('mention_count', 0)
305
- # Add sentiment indicator
306
- indicator = "🟒" if sentiment > 0.3 else "🟑" if sentiment > -0.3 else "πŸ”΄"
307
  summary.append(f" {indicator} {item.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
308
 
309
  if drinks:
@@ -311,7 +333,8 @@ Generate manager insights:"""
311
  for drink in drinks:
312
  sentiment = drink.get('sentiment', 0)
313
  mentions = drink.get('mention_count', 0)
314
- indicator = "🟒" if sentiment > 0.3 else "🟑" if sentiment > -0.3 else "πŸ”΄"
 
315
  summary.append(f" {indicator} {drink.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
316
 
317
  # Add overall stats
@@ -330,7 +353,7 @@ Generate manager insights:"""
330
  """
331
  Summarize aspect analysis for prompts.
332
 
333
- EXPANDED: Now uses top 20 aspects (was 10).
334
  """
335
  aspect_data = analysis_data.get('aspect_analysis', {})
336
  aspects = aspect_data.get('aspects', [])
@@ -354,7 +377,8 @@ Generate manager insights:"""
354
  for aspect in aspects:
355
  sentiment = aspect.get('sentiment', 0)
356
  mentions = aspect.get('mention_count', 0)
357
- indicator = "🟒" if sentiment > 0.3 else "🟑" if sentiment > -0.3 else "πŸ”΄"
 
358
  summary.append(f" {indicator} {aspect.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
359
 
360
  # Add total count
 
2
  Restaurant Insights Generator - EXPANDED VERSION
3
  Generates role-specific insights for Chef and Manager personas.
4
 
5
+ UPDATED v3:
6
+ - New sentiment scale (>= 0.6 positive, 0-0.59 neutral, < 0 negative)
7
+ - Clearer guidance on strengths vs concerns
8
+ - Top 20 items/aspects for comprehensive insights
9
  """
10
 
11
  import json
 
17
  """
18
  Generates actionable insights for different restaurant roles.
19
 
20
+ UPDATED:
21
+ - New sentiment thresholds (0.6/0 instead of 0.3/-0.3)
22
+ - Expanded to use top 20 menu items and aspects
23
+ - Clearer mapping of sentiment to strengths/concerns
24
  """
25
 
26
  def __init__(self, client, model: str = "claude-sonnet-4-20250514"):
 
115
  FOOD-RELATED ASPECTS:
116
  {aspect_summary}
117
 
118
+ SENTIMENT SCALE:
119
+ - 🟒 POSITIVE (0.6 to 1.0): Customers love this - highlight as a STRENGTH
120
+ - 🟑 NEUTRAL (0.0 to 0.59): Mixed or average feedback - room for improvement
121
+ - πŸ”΄ NEGATIVE (below 0): Customers complained - flag as a CONCERN
122
+
123
  YOUR TASK:
124
  Generate actionable insights specifically for the HEAD CHEF. Focus on:
125
  - Food quality and taste
 
132
 
133
  CRITICAL RULES:
134
  1. Focus ONLY on food/kitchen topics
135
+ 2. STRENGTHS should come from items/aspects with sentiment >= 0.6 (🟒 positive)
136
+ 3. CONCERNS should come from items/aspects with sentiment < 0 (πŸ”΄ negative)
137
+ 4. Be specific with evidence from reviews
138
+ 5. Make recommendations actionable
139
+ 6. Reference specific menu items by name
140
+ 7. Output ONLY valid JSON, no other text
141
 
142
  OUTPUT FORMAT (JSON):
143
  {{
144
  "summary": "2-3 sentence executive summary covering overall kitchen performance",
145
  "strengths": [
146
+ "Specific strength 1 - reference a 🟒 positive item with sentiment >= 0.6",
147
+ "Specific strength 2 - reference a 🟒 positive item with sentiment >= 0.6",
148
+ "Specific strength 3 - reference a 🟒 positive item with sentiment >= 0.6",
149
+ "Specific strength 4 - reference a 🟒 positive item with sentiment >= 0.6",
150
+ "Specific strength 5 - reference a 🟒 positive item with sentiment >= 0.6"
151
  ],
152
  "concerns": [
153
+ "Specific concern 1 - reference a πŸ”΄ negative item with sentiment < 0",
154
+ "Specific concern 2 - reference a πŸ”΄ negative item with sentiment < 0",
155
+ "Specific concern 3 - reference a πŸ”΄ negative item with sentiment < 0"
156
  ],
157
  "recommendations": [
158
  {{
159
  "priority": "high",
160
+ "action": "Specific action to fix a negative sentiment item",
161
  "reason": "Why this matters based on review data",
162
  "evidence": "Supporting data from reviews"
163
  }},
 
189
  }}
190
 
191
  IMPORTANT:
192
+ - Provide at least 5 strengths (from 🟒 items) and 5 recommendations
193
+ - If there are no negative items, focus recommendations on improving neutral items
194
  - Reference actual menu items from the data above
195
  - Ensure all JSON is properly formatted with no trailing commas
196
 
197
  Generate chef insights:"""
198
 
199
  return prompt
200
+
201
  def _build_manager_prompt(
202
  self,
203
  analysis_data: Dict[str, Any],
 
218
  MENU OVERVIEW (for context):
219
  {menu_summary}
220
 
221
+ SENTIMENT SCALE:
222
+ - 🟒 POSITIVE (0.6 to 1.0): Customers love this - highlight as a STRENGTH
223
+ - 🟑 NEUTRAL (0.0 to 0.59): Mixed or average feedback - room for improvement
224
+ - πŸ”΄ NEGATIVE (below 0): Customers complained - flag as a CONCERN
225
+
226
  YOUR TASK:
227
  Generate actionable insights specifically for the RESTAURANT MANAGER. Focus on:
228
  - Service quality and speed
 
236
 
237
  CRITICAL RULES:
238
  1. Focus ONLY on operations/service topics
239
+ 2. STRENGTHS should come from aspects with sentiment >= 0.6 (🟒 positive)
240
+ 3. CONCERNS should come from aspects with sentiment < 0 (πŸ”΄ negative)
241
+ 4. Be specific with evidence from reviews
242
+ 5. Make recommendations actionable
243
+ 6. Reference specific aspects by name
244
+ 7. Output ONLY valid JSON, no other text
245
 
246
  OUTPUT FORMAT (JSON):
247
  {{
248
  "summary": "2-3 sentence executive summary covering overall operations",
249
  "strengths": [
250
+ "Specific operational strength 1 - reference a 🟒 positive aspect with sentiment >= 0.6",
251
+ "Specific operational strength 2 - reference a 🟒 positive aspect with sentiment >= 0.6",
252
+ "Specific operational strength 3 - reference a 🟒 positive aspect with sentiment >= 0.6",
253
+ "Specific operational strength 4 - reference a 🟒 positive aspect with sentiment >= 0.6",
254
+ "Specific operational strength 5 - reference a 🟒 positive aspect with sentiment >= 0.6"
255
  ],
256
  "concerns": [
257
+ "Specific operational concern 1 - reference a πŸ”΄ negative aspect with sentiment < 0",
258
+ "Specific operational concern 2 - reference a πŸ”΄ negative aspect with sentiment < 0",
259
+ "Specific operational concern 3 - reference a πŸ”΄ negative aspect with sentiment < 0"
260
  ],
261
  "recommendations": [
262
  {{
263
  "priority": "high",
264
+ "action": "Specific action to fix a negative sentiment aspect",
265
  "reason": "Why this matters based on review data",
266
  "evidence": "Supporting data from reviews"
267
  }},
 
293
  }}
294
 
295
  IMPORTANT:
296
+ - Provide at least 5 strengths (from 🟒 aspects) and 5 recommendations
297
+ - If there are no negative aspects, focus recommendations on improving neutral aspects
298
  - Reference actual aspects from the data above
299
  - Ensure all JSON is properly formatted with no trailing commas
300
 
301
  Generate manager insights:"""
302
 
303
  return prompt
304
+
305
  def _summarize_menu_data(
306
  self,
307
  analysis_data: Dict[str, Any],
 
311
  """
312
  Summarize menu analysis for prompts.
313
 
314
+ UPDATED: New sentiment thresholds (0.6/0 instead of 0.3/-0.3)
315
  """
316
  menu_data = analysis_data.get('menu_analysis', {})
317
  food_items = menu_data.get('food_items', [])[:max_food]
 
324
  for item in food_items:
325
  sentiment = item.get('sentiment', 0)
326
  mentions = item.get('mention_count', 0)
327
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
328
+ indicator = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
329
  summary.append(f" {indicator} {item.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
330
 
331
  if drinks:
 
333
  for drink in drinks:
334
  sentiment = drink.get('sentiment', 0)
335
  mentions = drink.get('mention_count', 0)
336
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
337
+ indicator = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
338
  summary.append(f" {indicator} {drink.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
339
 
340
  # Add overall stats
 
353
  """
354
  Summarize aspect analysis for prompts.
355
 
356
+ UPDATED: New sentiment thresholds (0.6/0 instead of 0.3/-0.3)
357
  """
358
  aspect_data = analysis_data.get('aspect_analysis', {})
359
  aspects = aspect_data.get('aspects', [])
 
377
  for aspect in aspects:
378
  sentiment = aspect.get('sentiment', 0)
379
  mentions = aspect.get('mention_count', 0)
380
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
381
+ indicator = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
382
  summary.append(f" {indicator} {aspect.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
383
 
384
  # Add total count
src/agent/unified_analyzer.py CHANGED
@@ -1,6 +1,11 @@
1
  """
2
  Unified Review Analyzer - Single-pass extraction
3
  Extracts menu items, aspects, and sentiment in ONE API call per batch
 
 
 
 
 
4
  """
5
 
6
  from typing import List, Dict, Any
@@ -23,7 +28,7 @@ class UnifiedReviewAnalyzer:
23
  - Customer aspects (service, ambience, etc.)
24
  - Sentiment for each
25
 
26
- Reduces API calls by 3x!
27
  """
28
 
29
  def __init__(self, client: Anthropic, model: str):
@@ -41,9 +46,13 @@ class UnifiedReviewAnalyzer:
41
 
42
  Returns:
43
  {
44
- "menu_items": {...},
45
- "aspects": {...},
46
- "overall_stats": {...}
 
 
 
 
47
  }
48
  """
49
  print(f"πŸš€ Unified analysis: {len(reviews)} reviews in batches of {batch_size}...")
@@ -63,65 +72,73 @@ class UnifiedReviewAnalyzer:
63
  try:
64
  batch_result = self._analyze_batch(batch, restaurant_name, start_index=i)
65
 
66
- # Merge menu items
67
  for item in batch_result.get('food_items', []):
68
- name = item['name']
 
 
69
  if name in all_food_items:
70
- all_food_items[name]['mention_count'] += item['mention_count']
71
  all_food_items[name]['related_reviews'].extend(item.get('related_reviews', []))
 
72
  old_sent = all_food_items[name]['sentiment']
73
- new_sent = item['sentiment']
74
- all_food_items[name]['sentiment'] = (old_sent + new_sent) / 2
 
 
75
  else:
76
  all_food_items[name] = item
77
 
78
  # Merge drinks
79
- for drink in batch_result.get('drinks', []):
80
- name = drink['name']
 
 
81
  if name in all_drinks:
82
- all_drinks[name]['mention_count'] += drink['mention_count']
83
- all_drinks[name]['related_reviews'].extend(drink.get('related_reviews', []))
84
  old_sent = all_drinks[name]['sentiment']
85
- new_sent = drink['sentiment']
86
- all_drinks[name]['sentiment'] = (old_sent + new_sent) / 2
 
 
87
  else:
88
- all_drinks[name] = drink
89
 
90
  # Merge aspects
91
  for aspect in batch_result.get('aspects', []):
92
- name = aspect['name']
 
 
93
  if name in all_aspects:
94
- all_aspects[name]['mention_count'] += aspect['mention_count']
95
  all_aspects[name]['related_reviews'].extend(aspect.get('related_reviews', []))
96
  old_sent = all_aspects[name]['sentiment']
97
- new_sent = aspect['sentiment']
98
- all_aspects[name]['sentiment'] = (old_sent + new_sent) / 2
 
 
99
  else:
100
  all_aspects[name] = aspect
101
-
102
  except Exception as e:
103
- print(f" ⚠️ Batch {batch_num} failed: {e}")
104
  continue
105
 
106
- # Convert to lists and sort
107
- food_items_list = sorted(list(all_food_items.values()),
108
- key=lambda x: x['mention_count'], reverse=True)
109
- drinks_list = sorted(list(all_drinks.values()),
110
- key=lambda x: x['mention_count'], reverse=True)
111
- aspects_list = sorted(list(all_aspects.values()),
112
- key=lambda x: x['mention_count'], reverse=True)
113
 
114
- print(f"βœ… Discovered: {len(food_items_list)} food + {len(drinks_list)} drinks + {len(aspects_list)} aspects")
115
 
116
  return {
117
  "menu_analysis": {
118
- "food_items": food_items_list,
119
- "drinks": drinks_list,
120
- "total_extracted": len(food_items_list) + len(drinks_list)
121
  },
122
  "aspect_analysis": {
123
- "aspects": aspects_list,
124
- "total_aspects": len(aspects_list)
125
  }
126
  }
127
 
@@ -131,14 +148,15 @@ class UnifiedReviewAnalyzer:
131
  restaurant_name: str,
132
  start_index: int = 0
133
  ) -> Dict[str, Any]:
134
- """Analyze a single batch - extract EVERYTHING in one call."""
 
135
  prompt = self._build_unified_prompt(reviews, restaurant_name, start_index)
136
 
137
  try:
138
  response = call_claude_with_retry(
139
  client=self.client,
140
  model=self.model,
141
- max_tokens=4000, # Reduced since we're not returning full text
142
  temperature=0.3,
143
  messages=[{"role": "user", "content": prompt}]
144
  )
@@ -150,7 +168,7 @@ class UnifiedReviewAnalyzer:
150
  try:
151
  data = json.loads(result_text)
152
  except json.JSONDecodeError as e:
153
- print(f" ⚠️ JSON parse error: {e}")
154
  return {"food_items": [], "drinks": [], "aspects": []}
155
 
156
  # Post-process: Add full review text back using indices
@@ -163,99 +181,13 @@ class UnifiedReviewAnalyzer:
163
  print(f"❌ Extraction error: {e}")
164
  return {"food_items": [], "drinks": [], "aspects": []}
165
 
166
- def _map_reviews_to_items(
167
- self,
168
- data: Dict[str, Any],
169
- reviews: List[str],
170
- start_index: int
171
- ) -> Dict[str, Any]:
172
- """
173
- Map review indices back to full review text.
174
-
175
- Claude returns just indices to avoid JSON breaking.
176
- We add the full text back here.
177
- """
178
- # Process food items
179
- for item in data.get('food_items', []):
180
- review_indices = item.get('related_reviews', [])
181
- if isinstance(review_indices, list) and review_indices:
182
- # If it's already in full format, skip
183
- if isinstance(review_indices[0], dict):
184
- continue
185
-
186
- # Map indices to full reviews
187
- full_reviews = []
188
- for idx in review_indices:
189
- if isinstance(idx, int) and 0 <= idx < len(reviews):
190
- full_reviews.append({
191
- "review_index": start_index + idx,
192
- "review_text": reviews[idx],
193
- "sentiment_context": reviews[idx][:200] # First 200 chars as context
194
- })
195
-
196
- item['related_reviews'] = full_reviews
197
-
198
- # Process drinks
199
- for drink in data.get('drinks', []):
200
- review_indices = drink.get('related_reviews', [])
201
- if isinstance(review_indices, list) and review_indices:
202
- if isinstance(review_indices[0], dict):
203
- continue
204
-
205
- full_reviews = []
206
- for idx in review_indices:
207
- if isinstance(idx, int) and 0 <= idx < len(reviews):
208
- full_reviews.append({
209
- "review_index": start_index + idx,
210
- "review_text": reviews[idx],
211
- "sentiment_context": reviews[idx][:200]
212
- })
213
-
214
- drink['related_reviews'] = full_reviews
215
-
216
- # Process aspects
217
- for aspect in data.get('aspects', []):
218
- review_indices = aspect.get('related_reviews', [])
219
- if isinstance(review_indices, list) and review_indices:
220
- if isinstance(review_indices[0], dict):
221
- continue
222
-
223
- full_reviews = []
224
- for idx in review_indices:
225
- if isinstance(idx, int) and 0 <= idx < len(reviews):
226
- full_reviews.append({
227
- "review_index": start_index + idx,
228
- "review_text": reviews[idx],
229
- "sentiment_context": reviews[idx][:200]
230
- })
231
-
232
- aspect['related_reviews'] = full_reviews
233
-
234
- return data
235
-
236
- def _normalize_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
237
- """Normalize all names to lowercase."""
238
- for item in data.get('food_items', []):
239
- if 'name' in item:
240
- item['name'] = item['name'].lower()
241
-
242
- for drink in data.get('drinks', []):
243
- if 'name' in drink:
244
- drink['name'] = drink['name'].lower()
245
-
246
- for aspect in data.get('aspects', []):
247
- if 'name' in aspect:
248
- aspect['name'] = aspect['name'].lower()
249
-
250
- return data
251
-
252
  def _build_unified_prompt(
253
  self,
254
  reviews: List[str],
255
  restaurant_name: str,
256
  start_index: int
257
  ) -> str:
258
- """Build unified extraction prompt."""
259
  numbered_reviews = []
260
  for i, review in enumerate(reviews):
261
  numbered_reviews.append(f"[Review {i}]: {review}")
@@ -272,20 +204,30 @@ YOUR TASK - Extract THREE things simultaneously:
272
  2. **ASPECTS** (what customers care about: service, ambience, etc.)
273
  3. **SENTIMENT** for each
274
 
 
 
 
 
 
 
 
 
 
 
275
  RULES:
276
 
277
  **MENU ITEMS:**
278
  - Specific items only: "salmon sushi", "miso soup", "sake"
279
  - Separate food from drinks
280
  - Lowercase names
281
- - Calculate sentiment per item
282
 
283
  **ASPECTS:**
284
  - What customers discuss: "service speed", "food quality", "ambience", "value"
285
  - Be specific: "service speed" not just "service"
286
  - Cuisine-specific welcome: "freshness", "authenticity", "presentation"
287
  - Lowercase names
288
- - Calculate sentiment per aspect
289
 
290
  **REVIEW LINKING:**
291
  - For EACH item/aspect, list which review NUMBERS mention it
@@ -298,7 +240,7 @@ OUTPUT (JSON) - IMPORTANT: Return ONLY review indices, NOT full text:
298
  {{
299
  "name": "salmon aburi sushi",
300
  "mention_count": 2,
301
- "sentiment": 0.9,
302
  "category": "sushi",
303
  "related_reviews": [0, 5]
304
  }}
@@ -307,7 +249,7 @@ OUTPUT (JSON) - IMPORTANT: Return ONLY review indices, NOT full text:
307
  {{
308
  "name": "sake",
309
  "mention_count": 1,
310
- "sentiment": 0.8,
311
  "category": "alcohol",
312
  "related_reviews": [3]
313
  }}
@@ -316,7 +258,7 @@ OUTPUT (JSON) - IMPORTANT: Return ONLY review indices, NOT full text:
316
  {{
317
  "name": "service speed",
318
  "mention_count": 3,
319
- "sentiment": 0.6,
320
  "description": "How quickly food arrives",
321
  "related_reviews": [1, 2, 7]
322
  }}
@@ -328,7 +270,68 @@ CRITICAL:
328
  - DO NOT include review text or quotes
329
  - This prevents JSON parsing errors and saves tokens
330
  - Output ONLY valid JSON, no other text
 
331
 
332
  Extract everything:"""
333
 
334
- return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Unified Review Analyzer - Single-pass extraction
3
  Extracts menu items, aspects, and sentiment in ONE API call per batch
4
+
5
+ UPDATED: New sentiment scale
6
+ - Positive: >= 0.6
7
+ - Neutral: 0 to 0.59
8
+ - Negative: < 0
9
  """
10
 
11
  from typing import List, Dict, Any
 
28
  - Customer aspects (service, ambience, etc.)
29
  - Sentiment for each
30
 
31
+ Reduces API calls by 3x compared to separate extraction!
32
  """
33
 
34
  def __init__(self, client: Anthropic, model: str):
 
46
 
47
  Returns:
48
  {
49
+ "menu_analysis": {
50
+ "food_items": [...],
51
+ "drinks": [...]
52
+ },
53
+ "aspect_analysis": {
54
+ "aspects": [...]
55
+ }
56
  }
57
  """
58
  print(f"πŸš€ Unified analysis: {len(reviews)} reviews in batches of {batch_size}...")
 
72
  try:
73
  batch_result = self._analyze_batch(batch, restaurant_name, start_index=i)
74
 
75
+ # Merge food items
76
  for item in batch_result.get('food_items', []):
77
+ name = item.get('name', '').lower()
78
+ if not name:
79
+ continue
80
  if name in all_food_items:
81
+ all_food_items[name]['mention_count'] += item.get('mention_count', 1)
82
  all_food_items[name]['related_reviews'].extend(item.get('related_reviews', []))
83
+ # Average sentiment
84
  old_sent = all_food_items[name]['sentiment']
85
+ new_sent = item.get('sentiment', 0)
86
+ old_count = all_food_items[name]['mention_count'] - item.get('mention_count', 1)
87
+ new_count = item.get('mention_count', 1)
88
+ all_food_items[name]['sentiment'] = (old_sent * old_count + new_sent * new_count) / (old_count + new_count)
89
  else:
90
  all_food_items[name] = item
91
 
92
  # Merge drinks
93
+ for item in batch_result.get('drinks', []):
94
+ name = item.get('name', '').lower()
95
+ if not name:
96
+ continue
97
  if name in all_drinks:
98
+ all_drinks[name]['mention_count'] += item.get('mention_count', 1)
99
+ all_drinks[name]['related_reviews'].extend(item.get('related_reviews', []))
100
  old_sent = all_drinks[name]['sentiment']
101
+ new_sent = item.get('sentiment', 0)
102
+ old_count = all_drinks[name]['mention_count'] - item.get('mention_count', 1)
103
+ new_count = item.get('mention_count', 1)
104
+ all_drinks[name]['sentiment'] = (old_sent * old_count + new_sent * new_count) / (old_count + new_count)
105
  else:
106
+ all_drinks[name] = item
107
 
108
  # Merge aspects
109
  for aspect in batch_result.get('aspects', []):
110
+ name = aspect.get('name', '').lower()
111
+ if not name:
112
+ continue
113
  if name in all_aspects:
114
+ all_aspects[name]['mention_count'] += aspect.get('mention_count', 1)
115
  all_aspects[name]['related_reviews'].extend(aspect.get('related_reviews', []))
116
  old_sent = all_aspects[name]['sentiment']
117
+ new_sent = aspect.get('sentiment', 0)
118
+ old_count = all_aspects[name]['mention_count'] - aspect.get('mention_count', 1)
119
+ new_count = aspect.get('mention_count', 1)
120
+ all_aspects[name]['sentiment'] = (old_sent * old_count + new_sent * new_count) / (old_count + new_count)
121
  else:
122
  all_aspects[name] = aspect
123
+
124
  except Exception as e:
125
+ print(f" ⚠️ Batch {batch_num} error: {e}")
126
  continue
127
 
128
+ # Convert to lists and sort by mention count
129
+ food_list = sorted(all_food_items.values(), key=lambda x: x.get('mention_count', 0), reverse=True)
130
+ drinks_list = sorted(all_drinks.values(), key=lambda x: x.get('mention_count', 0), reverse=True)
131
+ aspects_list = sorted(all_aspects.values(), key=lambda x: x.get('mention_count', 0), reverse=True)
 
 
 
132
 
133
+ print(f"βœ… Discovered: {len(food_list)} food + {len(drinks_list)} drinks + {len(aspects_list)} aspects")
134
 
135
  return {
136
  "menu_analysis": {
137
+ "food_items": food_list,
138
+ "drinks": drinks_list
 
139
  },
140
  "aspect_analysis": {
141
+ "aspects": aspects_list
 
142
  }
143
  }
144
 
 
148
  restaurant_name: str,
149
  start_index: int = 0
150
  ) -> Dict[str, Any]:
151
+ """Analyze a single batch of reviews."""
152
+
153
  prompt = self._build_unified_prompt(reviews, restaurant_name, start_index)
154
 
155
  try:
156
  response = call_claude_with_retry(
157
  client=self.client,
158
  model=self.model,
159
+ max_tokens=4000,
160
  temperature=0.3,
161
  messages=[{"role": "user", "content": prompt}]
162
  )
 
168
  try:
169
  data = json.loads(result_text)
170
  except json.JSONDecodeError as e:
171
+ print(f" ⚠️ JSON parse error: {e}")
172
  return {"food_items": [], "drinks": [], "aspects": []}
173
 
174
  # Post-process: Add full review text back using indices
 
181
  print(f"❌ Extraction error: {e}")
182
  return {"food_items": [], "drinks": [], "aspects": []}
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  def _build_unified_prompt(
185
  self,
186
  reviews: List[str],
187
  restaurant_name: str,
188
  start_index: int
189
  ) -> str:
190
+ """Build unified extraction prompt with NEW SENTIMENT SCALE."""
191
  numbered_reviews = []
192
  for i, review in enumerate(reviews):
193
  numbered_reviews.append(f"[Review {i}]: {review}")
 
204
  2. **ASPECTS** (what customers care about: service, ambience, etc.)
205
  3. **SENTIMENT** for each
206
 
207
+ SENTIMENT SCALE (IMPORTANT):
208
+ - **Positive (0.6 to 1.0):** Customer clearly enjoyed/praised this item or aspect
209
+ - **Neutral (0.0 to 0.59):** Mixed feelings, okay but not exceptional, or simply mentioned without strong opinion
210
+ - **Negative (-1.0 to -0.01):** Customer complained, criticized, or expressed disappointment
211
+
212
+ Examples:
213
+ - "The pasta was absolutely divine!" β†’ 0.85 (Positive)
214
+ - "The pasta was decent, nothing special" β†’ 0.3 (Neutral)
215
+ - "The pasta was undercooked and bland" β†’ -0.6 (Negative)
216
+
217
  RULES:
218
 
219
  **MENU ITEMS:**
220
  - Specific items only: "salmon sushi", "miso soup", "sake"
221
  - Separate food from drinks
222
  - Lowercase names
223
+ - Calculate sentiment per item using the scale above
224
 
225
  **ASPECTS:**
226
  - What customers discuss: "service speed", "food quality", "ambience", "value"
227
  - Be specific: "service speed" not just "service"
228
  - Cuisine-specific welcome: "freshness", "authenticity", "presentation"
229
  - Lowercase names
230
+ - Calculate sentiment per aspect using the scale above
231
 
232
  **REVIEW LINKING:**
233
  - For EACH item/aspect, list which review NUMBERS mention it
 
240
  {{
241
  "name": "salmon aburi sushi",
242
  "mention_count": 2,
243
+ "sentiment": 0.85,
244
  "category": "sushi",
245
  "related_reviews": [0, 5]
246
  }}
 
249
  {{
250
  "name": "sake",
251
  "mention_count": 1,
252
+ "sentiment": 0.7,
253
  "category": "alcohol",
254
  "related_reviews": [3]
255
  }}
 
258
  {{
259
  "name": "service speed",
260
  "mention_count": 3,
261
+ "sentiment": 0.65,
262
  "description": "How quickly food arrives",
263
  "related_reviews": [1, 2, 7]
264
  }}
 
270
  - DO NOT include review text or quotes
271
  - This prevents JSON parsing errors and saves tokens
272
  - Output ONLY valid JSON, no other text
273
+ - Use the sentiment scale: >= 0.6 positive, 0-0.59 neutral, < 0 negative
274
 
275
  Extract everything:"""
276
 
277
+ return prompt
278
+
279
+ def _map_reviews_to_items(
280
+ self,
281
+ data: Dict[str, Any],
282
+ reviews: List[str],
283
+ start_index: int
284
+ ) -> Dict[str, Any]:
285
+ """
286
+ Map review indices back to full review text.
287
+
288
+ Claude returns just indices to avoid JSON breaking.
289
+ We add the full text back here.
290
+ """
291
+ for item in data.get('food_items', []):
292
+ indices = item.get('related_reviews', [])
293
+ item['related_reviews'] = []
294
+ for idx in indices:
295
+ if isinstance(idx, int) and 0 <= idx < len(reviews):
296
+ item['related_reviews'].append({
297
+ 'review_index': start_index + idx,
298
+ 'review_text': reviews[idx]
299
+ })
300
+
301
+ for item in data.get('drinks', []):
302
+ indices = item.get('related_reviews', [])
303
+ item['related_reviews'] = []
304
+ for idx in indices:
305
+ if isinstance(idx, int) and 0 <= idx < len(reviews):
306
+ item['related_reviews'].append({
307
+ 'review_index': start_index + idx,
308
+ 'review_text': reviews[idx]
309
+ })
310
+
311
+ for aspect in data.get('aspects', []):
312
+ indices = aspect.get('related_reviews', [])
313
+ aspect['related_reviews'] = []
314
+ for idx in indices:
315
+ if isinstance(idx, int) and 0 <= idx < len(reviews):
316
+ aspect['related_reviews'].append({
317
+ 'review_index': start_index + idx,
318
+ 'review_text': reviews[idx]
319
+ })
320
+
321
+ return data
322
+
323
+ def _normalize_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
324
+ """Normalize names to lowercase."""
325
+ for item in data.get('food_items', []):
326
+ if 'name' in item:
327
+ item['name'] = item['name'].lower()
328
+
329
+ for drink in data.get('drinks', []):
330
+ if 'name' in drink:
331
+ drink['name'] = drink['name'].lower()
332
+
333
+ for aspect in data.get('aspects', []):
334
+ if 'name' in aspect:
335
+ aspect['name'] = aspect['name'].lower()
336
+
337
+ return data
src/ui/gradio_app.py CHANGED
@@ -5,11 +5,17 @@ Professional UI with cards, plain English summaries, polished layout
5
  Hackathon: Anthropic MCP 1st Birthday - Track 2 (Productivity)
6
  Author: Tushar Pingle
7
 
8
- VERSION 4.0 FIXES:
9
- 1. Fixed Q&A "proxies" error with Anthropic SDK
10
- 2. Fixed PDF download functionality
11
- 3. Improved summaries to be more detailed and actionable
12
- 4. Multi-platform support (OpenTable + Google Maps)
 
 
 
 
 
 
13
  """
14
 
15
  import gradio as gr
@@ -285,13 +291,13 @@ def generate_trend_insight(trend_data: List[Dict], restaurant_name: str) -> str:
285
 
286
  insight = f"**{restaurant_name}** has an average rating of **{avg_rating:.1f} stars** "
287
 
288
- if avg_sentiment > 0.3:
289
  insight += "with **positive sentiment**. "
290
  if avg_rating >= 4.0:
291
  insight += "βœ… Ratings and sentiment are aligned!"
292
  else:
293
  insight += "πŸ€” Sentiment is positive but ratings are moderate."
294
- elif avg_sentiment < -0.1:
295
  insight += "but with **concerning sentiment**. "
296
  if avg_rating >= 4.0:
297
  insight += "⚠️ **Warning:** High ratings but negative sentiment detected."
@@ -377,11 +383,10 @@ def translate_menu_performance(menu: dict, restaurant_name: str) -> str:
377
  if not all_items:
378
  return f"*No menu data available for {restaurant_name} yet.*"
379
 
380
- # Count categories
381
- stars = len([i for i in all_items if i.get('sentiment', 0) > 0.5])
382
- good = len([i for i in all_items if 0.2 < i.get('sentiment', 0) <= 0.5])
383
- mixed = len([i for i in all_items if -0.2 <= i.get('sentiment', 0) <= 0.2])
384
- concerns = len([i for i in all_items if i.get('sentiment', 0) < -0.2])
385
 
386
  # Simple summary
387
  summary = f"""### 🍽️ Menu Overview for {restaurant_name}
@@ -390,10 +395,9 @@ def translate_menu_performance(menu: dict, restaurant_name: str) -> str:
390
 
391
  | Category | Count |
392
  |----------|-------|
393
- | 🌟 Customer Favorites | {stars} |
394
- | βœ… Performing Well | {good} |
395
- | 🟑 Mixed Reviews | {mixed} |
396
- | ⚠️ Needs Attention | {concerns} |
397
 
398
  πŸ‘‡ **Select an item from the dropdown below to see detailed customer feedback.**
399
  """
@@ -410,10 +414,10 @@ def translate_aspect_performance(aspects: dict, restaurant_name: str) -> str:
410
  if not aspect_list:
411
  return f"*No aspect data available for {restaurant_name} yet.*"
412
 
413
- # Count categories
414
- strengths = len([a for a in aspect_list if a.get('sentiment', 0) > 0.3])
415
- neutral = len([a for a in aspect_list if -0.3 <= a.get('sentiment', 0) <= 0.3])
416
- weaknesses = len([a for a in aspect_list if a.get('sentiment', 0) < -0.3])
417
 
418
  # Simple summary
419
  summary = f"""### πŸ“Š Customer Experience Overview for {restaurant_name}
@@ -422,9 +426,9 @@ def translate_aspect_performance(aspects: dict, restaurant_name: str) -> str:
422
 
423
  | Category | Count |
424
  |----------|-------|
425
- | πŸ’ͺ Strengths | {strengths} |
426
- | 🟑 Neutral | {neutral} |
427
- | πŸ“‰ Needs Work | {weaknesses} |
428
 
429
  πŸ‘‡ **Select an aspect from the dropdown below to see detailed customer feedback.**
430
  """
@@ -456,7 +460,8 @@ def generate_chart(items: list, title: str) -> Optional[str]:
456
  names = [f"{item.get('name', '?')[:18]} ({item.get('mention_count', 0)})" for item in sorted_items]
457
  sentiments = [item.get('sentiment', 0) for item in sorted_items]
458
 
459
- colors = [POSITIVE if s > 0.3 else NEUTRAL if s > -0.3 else NEGATIVE for s in sentiments]
 
460
 
461
  fig, ax = plt.subplots(figsize=(10, max(5, len(names) * 0.5)))
462
  fig.patch.set_facecolor(BG_COLOR)
@@ -538,7 +543,8 @@ def get_item_detail(item_name: str, state: dict) -> str:
538
  summary = item.get('summary', '')
539
  related_reviews = item.get('related_reviews', [])
540
 
541
- emoji = "🟒" if sentiment > 0.3 else "🟑" if sentiment > -0.3 else "πŸ”΄"
 
542
 
543
  detail = f"""### {clean_name.title()}
544
 
@@ -562,16 +568,14 @@ def get_item_detail(item_name: str, state: dict) -> str:
562
  if text and len(text) > 20:
563
  detail += f"> *\"{text[:200]}{'...' if len(text) > 200 else ''}\"*\n\n"
564
 
565
- # Add actionable insight
566
  detail += "\n**🎯 Recommended Action:**\n"
567
- if sentiment > 0.5:
568
  detail += f"This is a **star performer**! Consider featuring {clean_name.title()} in promotions and training staff to recommend it."
569
- elif sentiment > 0.2:
570
- detail += f"Customers generally like {clean_name.title()}. Monitor feedback and maintain quality."
571
- elif sentiment > -0.2:
572
- detail += f"Mixed feedback on {clean_name.title()}. Review recent complaints and consider recipe adjustments."
573
  else:
574
- detail += f"⚠️ **Urgent:** {clean_name.title()} has significant negative feedback. Review preparation process and consider temporary removal while issues are addressed."
575
 
576
  return detail
577
 
@@ -593,7 +597,8 @@ def get_aspect_detail(aspect_name: str, state: dict) -> str:
593
  summary = aspect.get('summary', '')
594
  related_reviews = aspect.get('related_reviews', [])
595
 
596
- emoji = "🟒" if sentiment > 0.3 else "🟑" if sentiment > -0.3 else "πŸ”΄"
 
597
 
598
  detail = f"""### {clean_name.title()}
599
 
@@ -617,16 +622,14 @@ def get_aspect_detail(aspect_name: str, state: dict) -> str:
617
  if text and len(text) > 20:
618
  detail += f"> *\"{text[:200]}{'...' if len(text) > 200 else ''}\"*\n\n"
619
 
620
- # Add actionable insight
621
  detail += "\n**🎯 Recommended Action:**\n"
622
- if sentiment > 0.5:
623
  detail += f"**{clean_name.title()}** is a major strength! Maintain current standards and use in marketing."
624
- elif sentiment > 0.2:
625
- detail += f"**{clean_name.title()}** is performing well. Look for opportunities to make it exceptional."
626
- elif sentiment > -0.2:
627
- detail += f"**{clean_name.title()}** has mixed reviews. Identify specific pain points and address them."
628
  else:
629
- detail += f"⚠️ **Priority Issue:** **{clean_name.title()}** is hurting the business. Immediate action needed - consider staff training, process changes, or operational review."
630
 
631
  return detail
632
 
@@ -799,9 +802,10 @@ def generate_pdf_report(state: dict) -> Optional[str]:
799
  all_sentiments = [item.get('sentiment', 0) for item in all_menu]
800
  avg_sentiment = sum(all_sentiments) / len(all_sentiments) if all_sentiments else 0
801
 
802
- sent_label = "Excellent" if avg_sentiment > 0.5 else "Good" if avg_sentiment > 0.3 else "Positive" if avg_sentiment > 0 else "Mixed" if avg_sentiment > -0.3 else "Needs Attention"
803
- sent_color = POSITIVE if avg_sentiment > 0.3 else WARNING if avg_sentiment > -0.3 else NEGATIVE
804
- sent_bg = POSITIVE_LIGHT if avg_sentiment > 0.3 else WARNING_LIGHT if avg_sentiment > -0.3 else NEGATIVE_LIGHT
 
805
 
806
  # Sentiment box
807
  sent_data = [[f"Overall Sentiment: {avg_sentiment:+.2f}", sent_label]]
@@ -827,7 +831,8 @@ def generate_pdf_report(state: dict) -> Optional[str]:
827
  for item in top_items:
828
  elements.append(Paragraph(f" β€’ {item.get('name', '?').title()} (sentiment: {item.get('sentiment', 0):+.2f})", styles['RIABullet']))
829
 
830
- concern_items = [i for i in all_menu if i.get('sentiment', 0) < -0.2]
 
831
  if concern_items:
832
  elements.append(Spacer(1, 10))
833
  elements.append(Paragraph("⚠️ <b>Items Needing Attention:</b>", styles['RIABody']))
@@ -836,20 +841,18 @@ def generate_pdf_report(state: dict) -> Optional[str]:
836
 
837
  elements.append(Spacer(1, 15))
838
 
839
- # Summary stats
840
- stars = len([i for i in all_menu if i.get('sentiment', 0) > 0.5])
841
- good = len([i for i in all_menu if 0.2 < i.get('sentiment', 0) <= 0.5])
842
- mixed = len([i for i in all_menu if -0.2 <= i.get('sentiment', 0) <= 0.2])
843
- concerns = len([i for i in all_menu if i.get('sentiment', 0) < -0.2])
844
 
845
  summary_data = [
846
  ['Metric', 'Value', 'Details'],
847
  ['Reviews Analyzed', str(len(trend_data)), f'From {source}'],
848
  ['Menu Items', str(len(all_menu)), f'{len(food_items)} food, {len(drinks)} drinks'],
849
- ['Customer Favorites', str(stars), 'Sentiment > 0.5'],
850
- ['Performing Well', str(good), 'Sentiment 0.2 - 0.5'],
851
- ['Mixed Reviews', str(mixed), 'Sentiment -0.2 - 0.2'],
852
- ['Needs Attention', str(concerns), 'Sentiment < -0.2'],
853
  ]
854
  summary_table = Table(summary_data, colWidths=[2*inch, 1.3*inch, 2.5*inch])
855
  summary_table.setStyle(TableStyle([
@@ -881,7 +884,8 @@ def generate_pdf_report(state: dict) -> Optional[str]:
881
  menu_data = [['#', 'Item', 'Sentiment', 'Mentions', 'Status']]
882
  for i, item in enumerate(sorted_menu, 1):
883
  sentiment = item.get('sentiment', 0)
884
- status = 'βœ“ Positive' if sentiment > 0.3 else '~ Mixed' if sentiment > -0.3 else 'βœ— Negative'
 
885
  menu_data.append([str(i), item.get('name', '?').title()[:22], f"{sentiment:+.2f}", str(item.get('mention_count', 0)), status])
886
 
887
  menu_table = Table(menu_data, colWidths=[0.4*inch, 2.2*inch, 1*inch, 0.9*inch, 1.1*inch])
@@ -909,7 +913,8 @@ def generate_pdf_report(state: dict) -> Optional[str]:
909
  aspect_data = [['#', 'Aspect', 'Sentiment', 'Mentions', 'Status']]
910
  for i, aspect in enumerate(sorted_aspects, 1):
911
  sentiment = aspect.get('sentiment', 0)
912
- status = 'βœ“ Strength' if sentiment > 0.3 else '~ Neutral' if sentiment > -0.3 else 'βœ— Weakness'
 
913
  aspect_data.append([str(i), aspect.get('name', '?').title()[:22], f"{sentiment:+.2f}", str(aspect.get('mention_count', 0)), status])
914
 
915
  aspect_table = Table(aspect_data, colWidths=[0.4*inch, 2.2*inch, 1*inch, 0.9*inch, 1.1*inch])
@@ -1049,9 +1054,10 @@ def generate_pdf_report(state: dict) -> Optional[str]:
1049
  # Sort by sentiment to get best positive and worst negative
1050
  for review in sorted(all_related_reviews, key=lambda x: x['sentiment'], reverse=True):
1051
  text = review['text']
1052
- if review['sentiment'] > 0.2 and len(positive_reviews) < 3:
 
1053
  positive_reviews.append(text[:180])
1054
- elif review['sentiment'] < -0.2 and len(negative_reviews) < 3:
1055
  negative_reviews.append(text[:180])
1056
 
1057
  elements.append(Paragraph("βœ… Positive Feedback", styles['RIASubHeader']))
@@ -1273,6 +1279,9 @@ Instructions:
1273
  - If reviews mention specific examples, include them
1274
  - Keep your answer helpful and concise (3-5 sentences)
1275
  - If the reviews don't contain relevant information, say so honestly
 
 
 
1276
 
1277
  Answer:"""
1278
 
 
5
  Hackathon: Anthropic MCP 1st Birthday - Track 2 (Productivity)
6
  Author: Tushar Pingle
7
 
8
+ VERSION 4.1 UPDATES:
9
+ 1. NEW SENTIMENT SCALE:
10
+ - 🟒 Positive: >= 0.6 (customers clearly enjoyed/praised)
11
+ - 🟑 Neutral: 0 to 0.59 (mixed feelings, average, okay)
12
+ - πŸ”΄ Negative: < 0 (complaints, criticism, disappointment)
13
+
14
+ 2. Updated all thresholds throughout the app for consistency
15
+ 3. Improved Q&A prompt for balanced answers (pros AND cons)
16
+ 4. Fixed PDF style conflicts with RIA prefix
17
+ 5. Fixed Q&A "proxies" error with Anthropic SDK
18
+ 6. Multi-platform support (OpenTable + Google Maps)
19
  """
20
 
21
  import gradio as gr
 
291
 
292
  insight = f"**{restaurant_name}** has an average rating of **{avg_rating:.1f} stars** "
293
 
294
+ if avg_sentiment >= 0.6:
295
  insight += "with **positive sentiment**. "
296
  if avg_rating >= 4.0:
297
  insight += "βœ… Ratings and sentiment are aligned!"
298
  else:
299
  insight += "πŸ€” Sentiment is positive but ratings are moderate."
300
+ elif avg_sentiment < 0:
301
  insight += "but with **concerning sentiment**. "
302
  if avg_rating >= 4.0:
303
  insight += "⚠️ **Warning:** High ratings but negative sentiment detected."
 
383
  if not all_items:
384
  return f"*No menu data available for {restaurant_name} yet.*"
385
 
386
+ # Count categories - NEW thresholds: >= 0.6 positive, 0-0.59 neutral, < 0 negative
387
+ stars = len([i for i in all_items if i.get('sentiment', 0) >= 0.6])
388
+ good = len([i for i in all_items if 0 <= i.get('sentiment', 0) < 0.6])
389
+ concerns = len([i for i in all_items if i.get('sentiment', 0) < 0])
 
390
 
391
  # Simple summary
392
  summary = f"""### 🍽️ Menu Overview for {restaurant_name}
 
395
 
396
  | Category | Count |
397
  |----------|-------|
398
+ | 🟒 Positive (β‰₯0.6) | {stars} |
399
+ | 🟑 Neutral (0 to 0.59) | {good} |
400
+ | πŸ”΄ Negative (<0) | {concerns} |
 
401
 
402
  πŸ‘‡ **Select an item from the dropdown below to see detailed customer feedback.**
403
  """
 
414
  if not aspect_list:
415
  return f"*No aspect data available for {restaurant_name} yet.*"
416
 
417
+ # Count categories - NEW thresholds: >= 0.6 positive, 0-0.59 neutral, < 0 negative
418
+ strengths = len([a for a in aspect_list if a.get('sentiment', 0) >= 0.6])
419
+ neutral = len([a for a in aspect_list if 0 <= a.get('sentiment', 0) < 0.6])
420
+ weaknesses = len([a for a in aspect_list if a.get('sentiment', 0) < 0])
421
 
422
  # Simple summary
423
  summary = f"""### πŸ“Š Customer Experience Overview for {restaurant_name}
 
426
 
427
  | Category | Count |
428
  |----------|-------|
429
+ | 🟒 Strengths (β‰₯0.6) | {strengths} |
430
+ | 🟑 Neutral (0 to 0.59) | {neutral} |
431
+ | πŸ”΄ Weaknesses (<0) | {weaknesses} |
432
 
433
  πŸ‘‡ **Select an aspect from the dropdown below to see detailed customer feedback.**
434
  """
 
460
  names = [f"{item.get('name', '?')[:18]} ({item.get('mention_count', 0)})" for item in sorted_items]
461
  sentiments = [item.get('sentiment', 0) for item in sorted_items]
462
 
463
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
464
+ colors = [POSITIVE if s >= 0.6 else NEUTRAL if s >= 0 else NEGATIVE for s in sentiments]
465
 
466
  fig, ax = plt.subplots(figsize=(10, max(5, len(names) * 0.5)))
467
  fig.patch.set_facecolor(BG_COLOR)
 
543
  summary = item.get('summary', '')
544
  related_reviews = item.get('related_reviews', [])
545
 
546
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
547
+ emoji = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
548
 
549
  detail = f"""### {clean_name.title()}
550
 
 
568
  if text and len(text) > 20:
569
  detail += f"> *\"{text[:200]}{'...' if len(text) > 200 else ''}\"*\n\n"
570
 
571
+ # Add actionable insight - NEW thresholds
572
  detail += "\n**🎯 Recommended Action:**\n"
573
+ if sentiment >= 0.6:
574
  detail += f"This is a **star performer**! Consider featuring {clean_name.title()} in promotions and training staff to recommend it."
575
+ elif sentiment >= 0:
576
+ detail += f"Customers have neutral/mixed feelings about {clean_name.title()}. Monitor feedback and look for improvement opportunities."
 
 
577
  else:
578
+ detail += f"⚠️ **Attention Needed:** {clean_name.title()} has negative feedback. Review preparation process and address customer complaints."
579
 
580
  return detail
581
 
 
597
  summary = aspect.get('summary', '')
598
  related_reviews = aspect.get('related_reviews', [])
599
 
600
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
601
+ emoji = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
602
 
603
  detail = f"""### {clean_name.title()}
604
 
 
622
  if text and len(text) > 20:
623
  detail += f"> *\"{text[:200]}{'...' if len(text) > 200 else ''}\"*\n\n"
624
 
625
+ # Add actionable insight - NEW thresholds
626
  detail += "\n**🎯 Recommended Action:**\n"
627
+ if sentiment >= 0.6:
628
  detail += f"**{clean_name.title()}** is a major strength! Maintain current standards and use in marketing."
629
+ elif sentiment >= 0:
630
+ detail += f"**{clean_name.title()}** has neutral/mixed reviews. Identify specific areas to improve and make it exceptional."
 
 
631
  else:
632
+ detail += f"⚠️ **Priority Issue:** **{clean_name.title()}** needs attention. Address customer complaints and consider staff training or process changes."
633
 
634
  return detail
635
 
 
802
  all_sentiments = [item.get('sentiment', 0) for item in all_menu]
803
  avg_sentiment = sum(all_sentiments) / len(all_sentiments) if all_sentiments else 0
804
 
805
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
806
+ sent_label = "Excellent" if avg_sentiment >= 0.8 else "Positive" if avg_sentiment >= 0.6 else "Neutral" if avg_sentiment >= 0 else "Needs Attention"
807
+ sent_color = POSITIVE if avg_sentiment >= 0.6 else WARNING if avg_sentiment >= 0 else NEGATIVE
808
+ sent_bg = POSITIVE_LIGHT if avg_sentiment >= 0.6 else WARNING_LIGHT if avg_sentiment >= 0 else NEGATIVE_LIGHT
809
 
810
  # Sentiment box
811
  sent_data = [[f"Overall Sentiment: {avg_sentiment:+.2f}", sent_label]]
 
831
  for item in top_items:
832
  elements.append(Paragraph(f" β€’ {item.get('name', '?').title()} (sentiment: {item.get('sentiment', 0):+.2f})", styles['RIABullet']))
833
 
834
+ # NEW threshold: < 0 for concerns
835
+ concern_items = [i for i in all_menu if i.get('sentiment', 0) < 0]
836
  if concern_items:
837
  elements.append(Spacer(1, 10))
838
  elements.append(Paragraph("⚠️ <b>Items Needing Attention:</b>", styles['RIABody']))
 
841
 
842
  elements.append(Spacer(1, 15))
843
 
844
+ # Summary stats - NEW thresholds
845
+ positive = len([i for i in all_menu if i.get('sentiment', 0) >= 0.6])
846
+ neutral = len([i for i in all_menu if 0 <= i.get('sentiment', 0) < 0.6])
847
+ negative = len([i for i in all_menu if i.get('sentiment', 0) < 0])
 
848
 
849
  summary_data = [
850
  ['Metric', 'Value', 'Details'],
851
  ['Reviews Analyzed', str(len(trend_data)), f'From {source}'],
852
  ['Menu Items', str(len(all_menu)), f'{len(food_items)} food, {len(drinks)} drinks'],
853
+ ['🟒 Positive', str(positive), 'Sentiment β‰₯ 0.6'],
854
+ ['🟑 Neutral', str(neutral), 'Sentiment 0 to 0.59'],
855
+ ['πŸ”΄ Negative', str(negative), 'Sentiment < 0'],
 
856
  ]
857
  summary_table = Table(summary_data, colWidths=[2*inch, 1.3*inch, 2.5*inch])
858
  summary_table.setStyle(TableStyle([
 
884
  menu_data = [['#', 'Item', 'Sentiment', 'Mentions', 'Status']]
885
  for i, item in enumerate(sorted_menu, 1):
886
  sentiment = item.get('sentiment', 0)
887
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
888
+ status = 'βœ“ Positive' if sentiment >= 0.6 else '~ Neutral' if sentiment >= 0 else 'βœ— Negative'
889
  menu_data.append([str(i), item.get('name', '?').title()[:22], f"{sentiment:+.2f}", str(item.get('mention_count', 0)), status])
890
 
891
  menu_table = Table(menu_data, colWidths=[0.4*inch, 2.2*inch, 1*inch, 0.9*inch, 1.1*inch])
 
913
  aspect_data = [['#', 'Aspect', 'Sentiment', 'Mentions', 'Status']]
914
  for i, aspect in enumerate(sorted_aspects, 1):
915
  sentiment = aspect.get('sentiment', 0)
916
+ # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
917
+ status = 'βœ“ Strength' if sentiment >= 0.6 else '~ Neutral' if sentiment >= 0 else 'βœ— Weakness'
918
  aspect_data.append([str(i), aspect.get('name', '?').title()[:22], f"{sentiment:+.2f}", str(aspect.get('mention_count', 0)), status])
919
 
920
  aspect_table = Table(aspect_data, colWidths=[0.4*inch, 2.2*inch, 1*inch, 0.9*inch, 1.1*inch])
 
1054
  # Sort by sentiment to get best positive and worst negative
1055
  for review in sorted(all_related_reviews, key=lambda x: x['sentiment'], reverse=True):
1056
  text = review['text']
1057
+ # NEW thresholds: >= 0.6 for positive, < 0 for negative
1058
+ if review['sentiment'] >= 0.6 and len(positive_reviews) < 3:
1059
  positive_reviews.append(text[:180])
1060
+ elif review['sentiment'] < 0 and len(negative_reviews) < 3:
1061
  negative_reviews.append(text[:180])
1062
 
1063
  elements.append(Paragraph("βœ… Positive Feedback", styles['RIASubHeader']))
 
1279
  - If reviews mention specific examples, include them
1280
  - Keep your answer helpful and concise (3-5 sentences)
1281
  - If the reviews don't contain relevant information, say so honestly
1282
+ - Provide BALANCED answers - mention both pros AND cons when relevant
1283
+ - If customers have mixed opinions, acknowledge both positive and negative feedback
1284
+ - Don't oversell or undersell - be honest about what customers actually said
1285
 
1286
  Answer:"""
1287