File size: 15,235 Bytes
bb9baa9
41fda39
 
bb9baa9
7dab48f
 
 
 
bb9baa9
 
 
 
41fda39
bb9baa9
 
 
 
41fda39
 
7dab48f
 
 
 
bb9baa9
 
41fda39
bb9baa9
 
 
 
 
41fda39
bb9baa9
 
 
 
 
41fda39
bb9baa9
 
41fda39
bb9baa9
 
41fda39
bb9baa9
 
41fda39
 
bb9baa9
 
 
41fda39
bb9baa9
 
41fda39
 
 
 
 
bb9baa9
 
 
 
 
 
 
41fda39
bb9baa9
41fda39
 
bb9baa9
41fda39
 
 
bb9baa9
41fda39
bb9baa9
41fda39
bb9baa9
 
41fda39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb9baa9
 
 
 
 
41fda39
 
 
 
bb9baa9
 
 
41fda39
bb9baa9
 
 
 
 
7dab48f
 
 
 
 
bb9baa9
 
 
 
 
 
 
 
 
 
 
 
7dab48f
 
 
 
 
 
bb9baa9
 
 
41fda39
 
7dab48f
 
 
 
 
41fda39
 
7dab48f
 
 
41fda39
bb9baa9
 
 
7dab48f
41fda39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb9baa9
 
 
 
 
 
41fda39
7dab48f
 
41fda39
 
bb9baa9
 
 
 
7dab48f
bb9baa9
 
 
 
 
41fda39
 
 
 
 
 
bb9baa9
 
 
41fda39
bb9baa9
 
41fda39
 
 
7dab48f
 
 
 
 
bb9baa9
 
 
 
 
 
 
 
 
 
 
 
 
7dab48f
 
 
 
 
 
bb9baa9
 
 
41fda39
 
7dab48f
 
 
 
 
41fda39
 
7dab48f
 
 
41fda39
bb9baa9
 
 
7dab48f
41fda39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb9baa9
 
 
 
 
 
41fda39
7dab48f
 
41fda39
 
bb9baa9
 
 
 
7dab48f
41fda39
 
 
 
 
 
 
 
 
7dab48f
41fda39
bb9baa9
41fda39
 
bb9baa9
 
 
 
41fda39
bb9baa9
 
 
7dab48f
 
41fda39
bb9baa9
 
41fda39
bb9baa9
 
 
7dab48f
 
41fda39
 
 
 
 
 
bb9baa9
 
 
41fda39
 
 
 
 
 
 
 
 
7dab48f
41fda39
bb9baa9
 
 
 
 
41fda39
 
bb9baa9
 
41fda39
 
 
bb9baa9
 
41fda39
bb9baa9
 
41fda39
 
bb9baa9
 
 
7dab48f
 
41fda39
 
 
 
 
bb9baa9
 
 
 
41fda39
bb9baa9
41fda39
 
 
bb9baa9
 
41fda39
 
 
 
bb9baa9
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
"""
Restaurant Insights Generator - EXPANDED VERSION
Generates role-specific insights for Chef and Manager personas.

UPDATED v3: 
- New sentiment scale (>= 0.6 positive, 0-0.59 neutral, < 0 negative)
- Clearer guidance on strengths vs concerns
- Top 20 items/aspects for comprehensive insights
"""

import json
import re
from typing import Any, Dict, Optional


class InsightsGenerator:
    """
    Generates actionable insights for different restaurant roles.
    
    UPDATED: 
    - New sentiment thresholds (0.6/0 instead of 0.3/-0.3)
    - Expanded to use top 20 menu items and aspects
    - Clearer mapping of sentiment to strengths/concerns
    """
    
    def __init__(self, client, model: str = "claude-sonnet-4-20250514"):
        """
        Initialize the insights generator.
        
        Args:
            client: Anthropic client instance
            model: Model to use for generation
        """
        self.client = client
        self.model = model
    
    def generate_insights(
        self, 
        analysis_data: Dict[str, Any],
        role: str = 'chef',
        restaurant_name: str = "the restaurant"
    ) -> Dict[str, Any]:
        """
        Generate role-specific insights from analysis data.
        
        Args:
            analysis_data: Complete analysis including menu and aspects
            role: Either 'chef' or 'manager'
            restaurant_name: Name of the restaurant
        
        Returns:
            Dict with summary, strengths, concerns, and recommendations
        """
        try:
            if role == 'chef':
                prompt = self._build_chef_prompt(analysis_data, restaurant_name)
            else:
                prompt = self._build_manager_prompt(analysis_data, restaurant_name)
            
            response = self.client.messages.create(
                model=self.model,
                max_tokens=2000,
                temperature=0.4,
                messages=[{"role": "user", "content": prompt}]
            )
            
            response_text = response.content[0].text.strip()
            
            # Parse JSON from response
            insights = self._parse_json_response(response_text)
            
            if insights:
                return insights
            else:
                return self._get_fallback_insights(role)
                
        except Exception as e:
            print(f"[INSIGHTS] Error generating {role} insights: {e}")
            return self._get_fallback_insights(role)
    
    def _parse_json_response(self, text: str) -> Optional[Dict]:
        """Parse JSON from response, handling markdown fences."""
        # Remove markdown code fences
        text = re.sub(r'```json\s*', '', text)
        text = re.sub(r'```\s*', '', text)
        text = text.strip()
        
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            # Try to find JSON object in text
            match = re.search(r'\{[\s\S]*\}', text)
            if match:
                try:
                    return json.loads(match.group())
                except:
                    pass
            return None
    
    def _build_chef_prompt(
        self,
        analysis_data: Dict[str, Any],
        restaurant_name: str
    ) -> str:
        """Build prompt for chef-focused insights - EXPANDED to 20 items."""
        # Get EXPANDED menu summary (20 items instead of 10)
        menu_summary = self._summarize_menu_data(analysis_data, max_food=20, max_drinks=10)
        aspect_summary = self._summarize_aspect_data(analysis_data, focus='food', max_aspects=15)
        
        prompt = f"""You are an expert culinary consultant analyzing customer feedback for {restaurant_name}.

MENU PERFORMANCE (Top items by customer mentions):
{menu_summary}

FOOD-RELATED ASPECTS:
{aspect_summary}

SENTIMENT SCALE:
- 🟒 POSITIVE (0.6 to 1.0): Customers love this - highlight as a STRENGTH
- 🟑 NEUTRAL (0.0 to 0.59): Mixed or average feedback - room for improvement
- πŸ”΄ NEGATIVE (below 0): Customers complained - flag as a CONCERN

YOUR TASK:
Generate actionable insights specifically for the HEAD CHEF. Focus on:
- Food quality and taste
- Menu items (what's working, what's not)
- Ingredient quality and freshness
- Presentation and plating
- Portion sizes
- Recipe consistency
- Kitchen execution

CRITICAL RULES:
1. Focus ONLY on food/kitchen topics
2. STRENGTHS should come from items/aspects with sentiment >= 0.6 (🟒 positive)
3. CONCERNS should come from items/aspects with sentiment < 0 (πŸ”΄ negative)
4. Be specific with evidence from reviews
5. Make recommendations actionable
6. Reference specific menu items by name
7. Output ONLY valid JSON, no other text

OUTPUT FORMAT (JSON):
{{
  "summary": "2-3 sentence executive summary covering overall kitchen performance",
  "strengths": [
    "Specific strength 1 - reference a 🟒 positive item with sentiment >= 0.6",
    "Specific strength 2 - reference a 🟒 positive item with sentiment >= 0.6",
    "Specific strength 3 - reference a 🟒 positive item with sentiment >= 0.6",
    "Specific strength 4 - reference a 🟒 positive item with sentiment >= 0.6",
    "Specific strength 5 - reference a 🟒 positive item with sentiment >= 0.6"
  ],
  "concerns": [
    "Specific concern 1 - reference a πŸ”΄ negative item with sentiment < 0",
    "Specific concern 2 - reference a πŸ”΄ negative item with sentiment < 0",
    "Specific concern 3 - reference a πŸ”΄ negative item with sentiment < 0"
  ],
  "recommendations": [
    {{
      "priority": "high",
      "action": "Specific action to fix a negative sentiment item",
      "reason": "Why this matters based on review data",
      "evidence": "Supporting data from reviews"
    }},
    {{
      "priority": "high",
      "action": "Another high priority action",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }},
    {{
      "priority": "medium",
      "action": "Medium priority action",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }},
    {{
      "priority": "medium",
      "action": "Another medium priority action",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }},
    {{
      "priority": "low",
      "action": "Lower priority improvement",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }}
  ]
}}

IMPORTANT: 
- Provide at least 5 strengths (from 🟒 items) and 5 recommendations
- If there are no negative items, focus recommendations on improving neutral items
- Reference actual menu items from the data above
- Ensure all JSON is properly formatted with no trailing commas

Generate chef insights:"""
        
        return prompt

    def _build_manager_prompt(
        self,
        analysis_data: Dict[str, Any],
        restaurant_name: str
    ) -> str:
        """Build prompt for manager-focused insights - EXPANDED to 20 aspects."""
        # Get EXPANDED aspect summary (20 aspects instead of 10)
        aspect_summary = self._summarize_aspect_data(analysis_data, focus='operations', max_aspects=20)
        
        # Also include menu overview for context
        menu_summary = self._summarize_menu_data(analysis_data, max_food=10, max_drinks=5)
        
        prompt = f"""You are an expert restaurant operations consultant analyzing customer feedback for {restaurant_name}.

OPERATIONAL ASPECTS (All discovered from reviews):
{aspect_summary}

MENU OVERVIEW (for context):
{menu_summary}

SENTIMENT SCALE:
- 🟒 POSITIVE (0.6 to 1.0): Customers love this - highlight as a STRENGTH
- 🟑 NEUTRAL (0.0 to 0.59): Mixed or average feedback - room for improvement
- πŸ”΄ NEGATIVE (below 0): Customers complained - flag as a CONCERN

YOUR TASK:
Generate actionable insights specifically for the RESTAURANT MANAGER. Focus on:
- Service quality and speed
- Staff performance and training needs
- Wait times and reservations
- Customer experience and satisfaction
- Operational efficiency
- Ambience and atmosphere
- Value for money
- Cleanliness and maintenance

CRITICAL RULES:
1. Focus ONLY on operations/service topics
2. STRENGTHS should come from aspects with sentiment >= 0.6 (🟒 positive)
3. CONCERNS should come from aspects with sentiment < 0 (πŸ”΄ negative)
4. Be specific with evidence from reviews
5. Make recommendations actionable
6. Reference specific aspects by name
7. Output ONLY valid JSON, no other text

OUTPUT FORMAT (JSON):
{{
  "summary": "2-3 sentence executive summary covering overall operations",
  "strengths": [
    "Specific operational strength 1 - reference a 🟒 positive aspect with sentiment >= 0.6",
    "Specific operational strength 2 - reference a 🟒 positive aspect with sentiment >= 0.6",
    "Specific operational strength 3 - reference a 🟒 positive aspect with sentiment >= 0.6",
    "Specific operational strength 4 - reference a 🟒 positive aspect with sentiment >= 0.6",
    "Specific operational strength 5 - reference a 🟒 positive aspect with sentiment >= 0.6"
  ],
  "concerns": [
    "Specific operational concern 1 - reference a πŸ”΄ negative aspect with sentiment < 0",
    "Specific operational concern 2 - reference a πŸ”΄ negative aspect with sentiment < 0",
    "Specific operational concern 3 - reference a πŸ”΄ negative aspect with sentiment < 0"
  ],
  "recommendations": [
    {{
      "priority": "high",
      "action": "Specific action to fix a negative sentiment aspect",
      "reason": "Why this matters based on review data",
      "evidence": "Supporting data from reviews"
    }},
    {{
      "priority": "high",
      "action": "Another high priority action",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }},
    {{
      "priority": "medium",
      "action": "Medium priority action",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }},
    {{
      "priority": "medium",
      "action": "Another medium priority action",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }},
    {{
      "priority": "low",
      "action": "Lower priority improvement",
      "reason": "Why this matters",
      "evidence": "Supporting data"
    }}
  ]
}}

IMPORTANT: 
- Provide at least 5 strengths (from 🟒 aspects) and 5 recommendations
- If there are no negative aspects, focus recommendations on improving neutral aspects
- Reference actual aspects from the data above
- Ensure all JSON is properly formatted with no trailing commas

Generate manager insights:"""
        
        return prompt

    def _summarize_menu_data(
        self, 
        analysis_data: Dict[str, Any],
        max_food: int = 20,
        max_drinks: int = 10
    ) -> str:
        """
        Summarize menu analysis for prompts.
        
        UPDATED: New sentiment thresholds (0.6/0 instead of 0.3/-0.3)
        """
        menu_data = analysis_data.get('menu_analysis', {})
        food_items = menu_data.get('food_items', [])[:max_food]
        drinks = menu_data.get('drinks', [])[:max_drinks]
        
        summary = []
        
        if food_items:
            summary.append(f"TOP {len(food_items)} FOOD ITEMS:")
            for item in food_items:
                sentiment = item.get('sentiment', 0)
                mentions = item.get('mention_count', 0)
                # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
                indicator = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
                summary.append(f"  {indicator} {item.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
        
        if drinks:
            summary.append(f"\nTOP {len(drinks)} DRINKS:")
            for drink in drinks:
                sentiment = drink.get('sentiment', 0)
                mentions = drink.get('mention_count', 0)
                # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
                indicator = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
                summary.append(f"  {indicator} {drink.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
        
        # Add overall stats
        total_food = len(menu_data.get('food_items', []))
        total_drinks = len(menu_data.get('drinks', []))
        summary.append(f"\n(Total: {total_food} food items, {total_drinks} drinks discovered)")
        
        return '\n'.join(summary) if summary else "No menu data available"
    
    def _summarize_aspect_data(
        self, 
        analysis_data: Dict[str, Any], 
        focus: str = 'all',
        max_aspects: int = 20
    ) -> str:
        """
        Summarize aspect analysis for prompts.
        
        UPDATED: New sentiment thresholds (0.6/0 instead of 0.3/-0.3)
        """
        aspect_data = analysis_data.get('aspect_analysis', {})
        aspects = aspect_data.get('aspects', [])
        
        # Filter aspects based on focus
        if focus == 'food':
            food_keywords = ['food', 'taste', 'flavor', 'quality', 'presentation', 'freshness', 
                           'portion', 'dish', 'menu', 'ingredient', 'cook', 'season', 'texture']
            aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in food_keywords)]
        elif focus == 'operations':
            ops_keywords = ['service', 'staff', 'wait', 'ambience', 'atmosphere', 'value', 'price', 
                          'clean', 'reservation', 'host', 'server', 'attentive', 'friendly', 
                          'noise', 'music', 'parking', 'location', 'decor', 'vibe']
            aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in ops_keywords)]
        
        aspects = aspects[:max_aspects]
        
        summary = []
        summary.append(f"ASPECTS ({len(aspects)} found):")
        
        for aspect in aspects:
            sentiment = aspect.get('sentiment', 0)
            mentions = aspect.get('mention_count', 0)
            # NEW thresholds: >= 0.6 positive, >= 0 neutral, < 0 negative
            indicator = "🟒" if sentiment >= 0.6 else "🟑" if sentiment >= 0 else "πŸ”΄"
            summary.append(f"  {indicator} {aspect.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
        
        # Add total count
        total_aspects = len(aspect_data.get('aspects', []))
        summary.append(f"\n(Total: {total_aspects} aspects discovered from reviews)")
        
        return '\n'.join(summary) if summary else "No aspect data available"
    
    def _get_fallback_insights(self, role: str) -> Dict[str, Any]:
        """Return fallback insights if generation fails."""
        return {
            "summary": f"Unable to generate {role} insights at this time. Please try again.",
            "strengths": ["Analysis in progress"],
            "concerns": ["No data available"],
            "recommendations": [
                {
                    "priority": "medium",
                    "action": "Re-run analysis with more reviews",
                    "reason": "Insufficient data for detailed insights",
                    "evidence": "N/A"
                }
            ]
        }