restaurant-intelligence-agent / src /agent /insights_generator.py
TushP's picture
Upload folder using huggingface_hub
7dab48f verified
"""
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"
}
]
}