Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- src/agent/insights_generator.py +62 -38
- src/agent/unified_analyzer.py +134 -131
- src/ui/gradio_app.py +66 -57
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:
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import json
|
|
@@ -14,7 +17,10 @@ class InsightsGenerator:
|
|
| 14 |
"""
|
| 15 |
Generates actionable insights for different restaurant roles.
|
| 16 |
|
| 17 |
-
UPDATED:
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 125 |
-
3.
|
| 126 |
-
4.
|
| 127 |
-
5.
|
|
|
|
|
|
|
| 128 |
|
| 129 |
OUTPUT FORMAT (JSON):
|
| 130 |
{{
|
| 131 |
"summary": "2-3 sentence executive summary covering overall kitchen performance",
|
| 132 |
"strengths": [
|
| 133 |
-
"Specific strength 1
|
| 134 |
-
"Specific strength 2
|
| 135 |
-
"Specific strength 3
|
| 136 |
-
"Specific strength 4
|
| 137 |
-
"Specific strength 5
|
| 138 |
],
|
| 139 |
"concerns": [
|
| 140 |
-
"Specific concern 1 with
|
| 141 |
-
"Specific concern 2 with
|
| 142 |
-
"Specific concern 3 with
|
| 143 |
],
|
| 144 |
"recommendations": [
|
| 145 |
{{
|
| 146 |
"priority": "high",
|
| 147 |
-
"action": "Specific action to
|
| 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.
|
| 221 |
-
3.
|
| 222 |
-
4.
|
| 223 |
-
5.
|
|
|
|
|
|
|
| 224 |
|
| 225 |
OUTPUT FORMAT (JSON):
|
| 226 |
{{
|
| 227 |
"summary": "2-3 sentence executive summary covering overall operations",
|
| 228 |
"strengths": [
|
| 229 |
-
"Specific operational strength 1 with
|
| 230 |
-
"Specific operational strength 2 with
|
| 231 |
-
"Specific operational strength 3 with
|
| 232 |
-
"Specific operational strength 4 with
|
| 233 |
-
"Specific operational strength 5 with
|
| 234 |
],
|
| 235 |
"concerns": [
|
| 236 |
-
"Specific operational concern 1 with
|
| 237 |
-
"Specific operational concern 2 with
|
| 238 |
-
"Specific operational concern 3 with
|
| 239 |
],
|
| 240 |
"recommendations": [
|
| 241 |
{{
|
| 242 |
"priority": "high",
|
| 243 |
-
"action": "Specific action to
|
| 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 |
-
|
| 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 |
-
#
|
| 306 |
-
indicator = "π’" if sentiment > 0.
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
"
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 67 |
for item in batch_result.get('food_items', []):
|
| 68 |
-
name = item
|
|
|
|
|
|
|
| 69 |
if name in all_food_items:
|
| 70 |
-
all_food_items[name]['mention_count'] += item
|
| 71 |
all_food_items[name]['related_reviews'].extend(item.get('related_reviews', []))
|
|
|
|
| 72 |
old_sent = all_food_items[name]['sentiment']
|
| 73 |
-
new_sent = item
|
| 74 |
-
all_food_items[name]['
|
|
|
|
|
|
|
| 75 |
else:
|
| 76 |
all_food_items[name] = item
|
| 77 |
|
| 78 |
# Merge drinks
|
| 79 |
-
for
|
| 80 |
-
name =
|
|
|
|
|
|
|
| 81 |
if name in all_drinks:
|
| 82 |
-
all_drinks[name]['mention_count'] +=
|
| 83 |
-
all_drinks[name]['related_reviews'].extend(
|
| 84 |
old_sent = all_drinks[name]['sentiment']
|
| 85 |
-
new_sent =
|
| 86 |
-
all_drinks[name]['
|
|
|
|
|
|
|
| 87 |
else:
|
| 88 |
-
all_drinks[name] =
|
| 89 |
|
| 90 |
# Merge aspects
|
| 91 |
for aspect in batch_result.get('aspects', []):
|
| 92 |
-
name = aspect
|
|
|
|
|
|
|
| 93 |
if name in all_aspects:
|
| 94 |
-
all_aspects[name]['mention_count'] += aspect
|
| 95 |
all_aspects[name]['related_reviews'].extend(aspect.get('related_reviews', []))
|
| 96 |
old_sent = all_aspects[name]['sentiment']
|
| 97 |
-
new_sent = aspect
|
| 98 |
-
all_aspects[name]['
|
|
|
|
|
|
|
| 99 |
else:
|
| 100 |
all_aspects[name] = aspect
|
| 101 |
-
|
| 102 |
except Exception as e:
|
| 103 |
-
print(f" β οΈ
|
| 104 |
continue
|
| 105 |
|
| 106 |
-
# Convert to lists and sort
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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(
|
| 115 |
|
| 116 |
return {
|
| 117 |
"menu_analysis": {
|
| 118 |
-
"food_items":
|
| 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
|
|
|
|
| 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,
|
| 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" β οΈ
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 9 |
-
1.
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 <
|
| 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.
|
| 382 |
-
good = len([i for i in all_items if 0
|
| 383 |
-
|
| 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 |
-
|
|
| 394 |
-
|
|
| 395 |
-
|
|
| 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.
|
| 415 |
-
neutral = len([a for a in aspect_list if
|
| 416 |
-
weaknesses = len([a for a in aspect_list if a.get('sentiment', 0) <
|
| 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 |
-
|
|
| 426 |
-
| π‘ Neutral | {neutral} |
|
| 427 |
-
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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.
|
| 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
|
| 570 |
-
detail += f"Customers
|
| 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"β οΈ **
|
| 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 |
-
|
|
|
|
| 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.
|
| 623 |
detail += f"**{clean_name.title()}** is a major strength! Maintain current standards and use in marketing."
|
| 624 |
-
elif sentiment > 0
|
| 625 |
-
detail += f"**{clean_name.title()}**
|
| 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()}**
|
| 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 |
-
|
| 803 |
-
|
| 804 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 841 |
-
|
| 842 |
-
|
| 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 |
-
['
|
| 850 |
-
['
|
| 851 |
-
['
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 1053 |
positive_reviews.append(text[:180])
|
| 1054 |
-
elif review['sentiment'] <
|
| 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 |
|