Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- src/agent/insights_generator.py +206 -94
- src/ui/gradio_app.py +362 -123
src/agent/insights_generator.py
CHANGED
|
@@ -1,62 +1,56 @@
|
|
| 1 |
"""
|
| 2 |
-
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
Stakeholder-specific outputs:
|
| 7 |
-
- Chef: Food quality, menu items, presentation, taste
|
| 8 |
-
- Manager: Service, operations, staffing, customer experience
|
| 9 |
"""
|
| 10 |
|
| 11 |
-
from typing import Dict, Any
|
| 12 |
-
from anthropic import Anthropic
|
| 13 |
import json
|
| 14 |
import re
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
class InsightsGenerator:
|
| 18 |
"""
|
| 19 |
-
Generates
|
|
|
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
-
def __init__(self, client
|
| 23 |
"""
|
| 24 |
Initialize the insights generator.
|
| 25 |
|
| 26 |
Args:
|
| 27 |
client: Anthropic client instance
|
| 28 |
-
model:
|
| 29 |
"""
|
| 30 |
self.client = client
|
| 31 |
self.model = model
|
| 32 |
|
| 33 |
def generate_insights(
|
| 34 |
-
self,
|
| 35 |
analysis_data: Dict[str, Any],
|
| 36 |
role: str = 'chef',
|
| 37 |
-
restaurant_name: str =
|
| 38 |
) -> Dict[str, Any]:
|
| 39 |
"""
|
| 40 |
-
Generate role-specific insights.
|
| 41 |
|
| 42 |
Args:
|
| 43 |
-
analysis_data:
|
| 44 |
-
role:
|
| 45 |
restaurant_name: Name of the restaurant
|
| 46 |
|
| 47 |
Returns:
|
| 48 |
-
|
| 49 |
"""
|
| 50 |
-
# Build the prompt based on role
|
| 51 |
-
if role.lower() == 'chef':
|
| 52 |
-
prompt = self._build_chef_prompt(analysis_data, restaurant_name)
|
| 53 |
-
elif role.lower() == 'manager':
|
| 54 |
-
prompt = self._build_manager_prompt(analysis_data, restaurant_name)
|
| 55 |
-
else:
|
| 56 |
-
raise ValueError(f"Unknown role: {role}. Must be 'chef' or 'manager'")
|
| 57 |
-
|
| 58 |
-
# Call Claude to generate insights
|
| 59 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
response = self.client.messages.create(
|
| 61 |
model=self.model,
|
| 62 |
max_tokens=2000,
|
|
@@ -64,48 +58,52 @@ class InsightsGenerator:
|
|
| 64 |
messages=[{"role": "user", "content": prompt}]
|
| 65 |
)
|
| 66 |
|
| 67 |
-
|
| 68 |
-
insights_text = response.content[0].text
|
| 69 |
-
|
| 70 |
-
# Clean up response
|
| 71 |
-
insights_text = insights_text.replace('```json', '').replace('```', '').strip()
|
| 72 |
-
|
| 73 |
-
# Remove any trailing commas before closing braces/brackets
|
| 74 |
-
insights_text = re.sub(r',(\s*[}\]])', r'\1', insights_text)
|
| 75 |
|
| 76 |
-
# Parse JSON response
|
| 77 |
-
insights =
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
return self._get_fallback_insights(role)
|
| 83 |
-
|
| 84 |
-
return insights
|
| 85 |
-
|
| 86 |
-
except json.JSONDecodeError as e:
|
| 87 |
-
print(f"❌ Failed to parse insights as JSON: {e}")
|
| 88 |
-
print(f"Raw response: {insights_text[:200]}...")
|
| 89 |
-
return self._get_fallback_insights(role)
|
| 90 |
except Exception as e:
|
| 91 |
-
print(f"
|
| 92 |
return self._get_fallback_insights(role)
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def _build_chef_prompt(
|
| 95 |
self,
|
| 96 |
analysis_data: Dict[str, Any],
|
| 97 |
restaurant_name: str
|
| 98 |
) -> str:
|
| 99 |
-
"""
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
menu_summary = self._summarize_menu_data(analysis_data)
|
| 104 |
-
aspect_summary = self._summarize_aspect_data(analysis_data, focus='food')
|
| 105 |
|
| 106 |
prompt = f"""You are an expert culinary consultant analyzing customer feedback for {restaurant_name}.
|
| 107 |
|
| 108 |
-
MENU PERFORMANCE:
|
| 109 |
{menu_summary}
|
| 110 |
|
| 111 |
FOOD-RELATED ASPECTS:
|
|
@@ -125,24 +123,62 @@ CRITICAL RULES:
|
|
| 125 |
1. Focus ONLY on food/kitchen topics
|
| 126 |
2. Be specific with evidence from reviews
|
| 127 |
3. Make recommendations actionable
|
| 128 |
-
4.
|
|
|
|
| 129 |
|
| 130 |
OUTPUT FORMAT (JSON):
|
| 131 |
{{
|
| 132 |
-
"summary": "2-3 sentence executive summary",
|
| 133 |
-
"strengths": [
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
"recommendations": [
|
| 136 |
{{
|
| 137 |
"priority": "high",
|
| 138 |
"action": "Specific action to take",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
"reason": "Why this matters",
|
| 140 |
"evidence": "Supporting data"
|
| 141 |
}}
|
| 142 |
]
|
| 143 |
}}
|
| 144 |
|
| 145 |
-
IMPORTANT:
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
Generate chef insights:"""
|
| 148 |
|
|
@@ -153,17 +189,21 @@ Generate chef insights:"""
|
|
| 153 |
analysis_data: Dict[str, Any],
|
| 154 |
restaurant_name: str
|
| 155 |
) -> str:
|
| 156 |
-
"""
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
| 161 |
|
| 162 |
prompt = f"""You are an expert restaurant operations consultant analyzing customer feedback for {restaurant_name}.
|
| 163 |
|
| 164 |
-
OPERATIONAL ASPECTS:
|
| 165 |
{aspect_summary}
|
| 166 |
|
|
|
|
|
|
|
|
|
|
| 167 |
YOUR TASK:
|
| 168 |
Generate actionable insights specifically for the RESTAURANT MANAGER. Focus on:
|
| 169 |
- Service quality and speed
|
|
@@ -179,90 +219,162 @@ CRITICAL RULES:
|
|
| 179 |
1. Focus ONLY on operations/service topics
|
| 180 |
2. Be specific with evidence from reviews
|
| 181 |
3. Make recommendations actionable
|
| 182 |
-
4.
|
|
|
|
| 183 |
|
| 184 |
OUTPUT FORMAT (JSON):
|
| 185 |
{{
|
| 186 |
-
"summary": "2-3 sentence executive summary",
|
| 187 |
-
"strengths": [
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
"recommendations": [
|
| 190 |
{{
|
| 191 |
"priority": "high",
|
| 192 |
"action": "Specific action to take",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
"reason": "Why this matters",
|
| 194 |
"evidence": "Supporting data"
|
| 195 |
}}
|
| 196 |
]
|
| 197 |
}}
|
| 198 |
|
| 199 |
-
IMPORTANT:
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
Generate manager insights:"""
|
| 202 |
|
| 203 |
return prompt
|
| 204 |
|
| 205 |
-
def _summarize_menu_data(
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
menu_data = analysis_data.get('menu_analysis', {})
|
| 208 |
-
food_items = menu_data.get('food_items', [])[:
|
| 209 |
-
drinks = menu_data.get('drinks', [])[:
|
| 210 |
|
| 211 |
summary = []
|
| 212 |
|
| 213 |
if food_items:
|
| 214 |
-
summary.append("TOP FOOD ITEMS:")
|
| 215 |
for item in food_items:
|
| 216 |
sentiment = item.get('sentiment', 0)
|
| 217 |
mentions = item.get('mention_count', 0)
|
| 218 |
-
|
|
|
|
|
|
|
| 219 |
|
| 220 |
if drinks:
|
| 221 |
-
summary.append("\nTOP DRINKS:")
|
| 222 |
for drink in drinks:
|
| 223 |
sentiment = drink.get('sentiment', 0)
|
| 224 |
mentions = drink.get('mention_count', 0)
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
return '\n'.join(summary) if summary else "No menu data available"
|
| 228 |
|
| 229 |
-
def _summarize_aspect_data(
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
aspect_data = analysis_data.get('aspect_analysis', {})
|
| 232 |
aspects = aspect_data.get('aspects', [])
|
| 233 |
|
| 234 |
# Filter aspects based on focus
|
| 235 |
if focus == 'food':
|
| 236 |
-
food_keywords = ['food', 'taste', 'flavor', 'quality', 'presentation', 'freshness',
|
|
|
|
| 237 |
aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in food_keywords)]
|
| 238 |
elif focus == 'operations':
|
| 239 |
-
ops_keywords = ['service', 'staff', 'wait', 'ambience', 'atmosphere', 'value', 'price',
|
|
|
|
|
|
|
| 240 |
aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in ops_keywords)]
|
| 241 |
|
| 242 |
-
aspects = aspects[:
|
| 243 |
|
| 244 |
summary = []
|
|
|
|
|
|
|
| 245 |
for aspect in aspects:
|
| 246 |
sentiment = aspect.get('sentiment', 0)
|
| 247 |
mentions = aspect.get('mention_count', 0)
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
return '\n'.join(summary) if summary else "No aspect data available"
|
| 251 |
|
| 252 |
def _get_fallback_insights(self, role: str) -> Dict[str, Any]:
|
| 253 |
-
"""
|
| 254 |
-
Return fallback insights if generation fails.
|
| 255 |
-
"""
|
| 256 |
return {
|
| 257 |
-
"summary": f"Unable to generate {role} insights at this time.",
|
| 258 |
-
"strengths": ["Analysis
|
| 259 |
-
"concerns": ["
|
| 260 |
"recommendations": [
|
| 261 |
{
|
| 262 |
-
"priority": "
|
| 263 |
-
"action": "
|
| 264 |
-
"reason": "
|
| 265 |
-
"evidence": "
|
| 266 |
}
|
| 267 |
]
|
| 268 |
}
|
|
|
|
| 1 |
"""
|
| 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
|
| 9 |
import re
|
| 10 |
+
from typing import Any, Dict, Optional
|
| 11 |
|
| 12 |
|
| 13 |
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"):
|
| 21 |
"""
|
| 22 |
Initialize the insights generator.
|
| 23 |
|
| 24 |
Args:
|
| 25 |
client: Anthropic client instance
|
| 26 |
+
model: Model to use for generation
|
| 27 |
"""
|
| 28 |
self.client = client
|
| 29 |
self.model = model
|
| 30 |
|
| 31 |
def generate_insights(
|
| 32 |
+
self,
|
| 33 |
analysis_data: Dict[str, Any],
|
| 34 |
role: str = 'chef',
|
| 35 |
+
restaurant_name: str = "the restaurant"
|
| 36 |
) -> Dict[str, Any]:
|
| 37 |
"""
|
| 38 |
+
Generate role-specific insights from analysis data.
|
| 39 |
|
| 40 |
Args:
|
| 41 |
+
analysis_data: Complete analysis including menu and aspects
|
| 42 |
+
role: Either 'chef' or 'manager'
|
| 43 |
restaurant_name: Name of the restaurant
|
| 44 |
|
| 45 |
Returns:
|
| 46 |
+
Dict with summary, strengths, concerns, and recommendations
|
| 47 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
+
if role == 'chef':
|
| 50 |
+
prompt = self._build_chef_prompt(analysis_data, restaurant_name)
|
| 51 |
+
else:
|
| 52 |
+
prompt = self._build_manager_prompt(analysis_data, restaurant_name)
|
| 53 |
+
|
| 54 |
response = self.client.messages.create(
|
| 55 |
model=self.model,
|
| 56 |
max_tokens=2000,
|
|
|
|
| 58 |
messages=[{"role": "user", "content": prompt}]
|
| 59 |
)
|
| 60 |
|
| 61 |
+
response_text = response.content[0].text.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
# Parse JSON from response
|
| 64 |
+
insights = self._parse_json_response(response_text)
|
| 65 |
|
| 66 |
+
if insights:
|
| 67 |
+
return insights
|
| 68 |
+
else:
|
| 69 |
return self._get_fallback_insights(role)
|
| 70 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
except Exception as e:
|
| 72 |
+
print(f"[INSIGHTS] Error generating {role} insights: {e}")
|
| 73 |
return self._get_fallback_insights(role)
|
| 74 |
|
| 75 |
+
def _parse_json_response(self, text: str) -> Optional[Dict]:
|
| 76 |
+
"""Parse JSON from response, handling markdown fences."""
|
| 77 |
+
# Remove markdown code fences
|
| 78 |
+
text = re.sub(r'```json\s*', '', text)
|
| 79 |
+
text = re.sub(r'```\s*', '', text)
|
| 80 |
+
text = text.strip()
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
return json.loads(text)
|
| 84 |
+
except json.JSONDecodeError:
|
| 85 |
+
# Try to find JSON object in text
|
| 86 |
+
match = re.search(r'\{[\s\S]*\}', text)
|
| 87 |
+
if match:
|
| 88 |
+
try:
|
| 89 |
+
return json.loads(match.group())
|
| 90 |
+
except:
|
| 91 |
+
pass
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
def _build_chef_prompt(
|
| 95 |
self,
|
| 96 |
analysis_data: Dict[str, Any],
|
| 97 |
restaurant_name: str
|
| 98 |
) -> str:
|
| 99 |
+
"""Build prompt for chef-focused insights - EXPANDED to 20 items."""
|
| 100 |
+
# Get EXPANDED menu summary (20 items instead of 10)
|
| 101 |
+
menu_summary = self._summarize_menu_data(analysis_data, max_food=20, max_drinks=10)
|
| 102 |
+
aspect_summary = self._summarize_aspect_data(analysis_data, focus='food', max_aspects=15)
|
|
|
|
|
|
|
| 103 |
|
| 104 |
prompt = f"""You are an expert culinary consultant analyzing customer feedback for {restaurant_name}.
|
| 105 |
|
| 106 |
+
MENU PERFORMANCE (Top items by customer mentions):
|
| 107 |
{menu_summary}
|
| 108 |
|
| 109 |
FOOD-RELATED ASPECTS:
|
|
|
|
| 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 |
+
}},
|
| 151 |
+
{{
|
| 152 |
+
"priority": "high",
|
| 153 |
+
"action": "Another high priority action",
|
| 154 |
+
"reason": "Why this matters",
|
| 155 |
+
"evidence": "Supporting data"
|
| 156 |
+
}},
|
| 157 |
+
{{
|
| 158 |
+
"priority": "medium",
|
| 159 |
+
"action": "Medium priority action",
|
| 160 |
+
"reason": "Why this matters",
|
| 161 |
+
"evidence": "Supporting data"
|
| 162 |
+
}},
|
| 163 |
+
{{
|
| 164 |
+
"priority": "medium",
|
| 165 |
+
"action": "Another medium priority action",
|
| 166 |
+
"reason": "Why this matters",
|
| 167 |
+
"evidence": "Supporting data"
|
| 168 |
+
}},
|
| 169 |
+
{{
|
| 170 |
+
"priority": "low",
|
| 171 |
+
"action": "Lower priority improvement",
|
| 172 |
"reason": "Why this matters",
|
| 173 |
"evidence": "Supporting data"
|
| 174 |
}}
|
| 175 |
]
|
| 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 |
|
|
|
|
| 189 |
analysis_data: Dict[str, Any],
|
| 190 |
restaurant_name: str
|
| 191 |
) -> str:
|
| 192 |
+
"""Build prompt for manager-focused insights - EXPANDED to 20 aspects."""
|
| 193 |
+
# Get EXPANDED aspect summary (20 aspects instead of 10)
|
| 194 |
+
aspect_summary = self._summarize_aspect_data(analysis_data, focus='operations', max_aspects=20)
|
| 195 |
+
|
| 196 |
+
# Also include menu overview for context
|
| 197 |
+
menu_summary = self._summarize_menu_data(analysis_data, max_food=10, max_drinks=5)
|
| 198 |
|
| 199 |
prompt = f"""You are an expert restaurant operations consultant analyzing customer feedback for {restaurant_name}.
|
| 200 |
|
| 201 |
+
OPERATIONAL ASPECTS (All discovered from reviews):
|
| 202 |
{aspect_summary}
|
| 203 |
|
| 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
|
|
|
|
| 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 |
+
}},
|
| 247 |
+
{{
|
| 248 |
+
"priority": "high",
|
| 249 |
+
"action": "Another high priority action",
|
| 250 |
+
"reason": "Why this matters",
|
| 251 |
+
"evidence": "Supporting data"
|
| 252 |
+
}},
|
| 253 |
+
{{
|
| 254 |
+
"priority": "medium",
|
| 255 |
+
"action": "Medium priority action",
|
| 256 |
+
"reason": "Why this matters",
|
| 257 |
+
"evidence": "Supporting data"
|
| 258 |
+
}},
|
| 259 |
+
{{
|
| 260 |
+
"priority": "medium",
|
| 261 |
+
"action": "Another medium priority action",
|
| 262 |
+
"reason": "Why this matters",
|
| 263 |
+
"evidence": "Supporting data"
|
| 264 |
+
}},
|
| 265 |
+
{{
|
| 266 |
+
"priority": "low",
|
| 267 |
+
"action": "Lower priority improvement",
|
| 268 |
"reason": "Why this matters",
|
| 269 |
"evidence": "Supporting data"
|
| 270 |
}}
|
| 271 |
]
|
| 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],
|
| 286 |
+
max_food: int = 20,
|
| 287 |
+
max_drinks: int = 10
|
| 288 |
+
) -> str:
|
| 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]
|
| 296 |
+
drinks = menu_data.get('drinks', [])[:max_drinks]
|
| 297 |
|
| 298 |
summary = []
|
| 299 |
|
| 300 |
if food_items:
|
| 301 |
+
summary.append(f"TOP {len(food_items)} FOOD ITEMS:")
|
| 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:
|
| 310 |
+
summary.append(f"\nTOP {len(drinks)} DRINKS:")
|
| 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
|
| 318 |
+
total_food = len(menu_data.get('food_items', []))
|
| 319 |
+
total_drinks = len(menu_data.get('drinks', []))
|
| 320 |
+
summary.append(f"\n(Total: {total_food} food items, {total_drinks} drinks discovered)")
|
| 321 |
|
| 322 |
return '\n'.join(summary) if summary else "No menu data available"
|
| 323 |
|
| 324 |
+
def _summarize_aspect_data(
|
| 325 |
+
self,
|
| 326 |
+
analysis_data: Dict[str, Any],
|
| 327 |
+
focus: str = 'all',
|
| 328 |
+
max_aspects: int = 20
|
| 329 |
+
) -> str:
|
| 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', [])
|
| 337 |
|
| 338 |
# Filter aspects based on focus
|
| 339 |
if focus == 'food':
|
| 340 |
+
food_keywords = ['food', 'taste', 'flavor', 'quality', 'presentation', 'freshness',
|
| 341 |
+
'portion', 'dish', 'menu', 'ingredient', 'cook', 'season', 'texture']
|
| 342 |
aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in food_keywords)]
|
| 343 |
elif focus == 'operations':
|
| 344 |
+
ops_keywords = ['service', 'staff', 'wait', 'ambience', 'atmosphere', 'value', 'price',
|
| 345 |
+
'clean', 'reservation', 'host', 'server', 'attentive', 'friendly',
|
| 346 |
+
'noise', 'music', 'parking', 'location', 'decor', 'vibe']
|
| 347 |
aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in ops_keywords)]
|
| 348 |
|
| 349 |
+
aspects = aspects[:max_aspects]
|
| 350 |
|
| 351 |
summary = []
|
| 352 |
+
summary.append(f"ASPECTS ({len(aspects)} found):")
|
| 353 |
+
|
| 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
|
| 361 |
+
total_aspects = len(aspect_data.get('aspects', []))
|
| 362 |
+
summary.append(f"\n(Total: {total_aspects} aspects discovered from reviews)")
|
| 363 |
|
| 364 |
return '\n'.join(summary) if summary else "No aspect data available"
|
| 365 |
|
| 366 |
def _get_fallback_insights(self, role: str) -> Dict[str, Any]:
|
| 367 |
+
"""Return fallback insights if generation fails."""
|
|
|
|
|
|
|
| 368 |
return {
|
| 369 |
+
"summary": f"Unable to generate {role} insights at this time. Please try again.",
|
| 370 |
+
"strengths": ["Analysis in progress"],
|
| 371 |
+
"concerns": ["No data available"],
|
| 372 |
"recommendations": [
|
| 373 |
{
|
| 374 |
+
"priority": "medium",
|
| 375 |
+
"action": "Re-run analysis with more reviews",
|
| 376 |
+
"reason": "Insufficient data for detailed insights",
|
| 377 |
+
"evidence": "N/A"
|
| 378 |
}
|
| 379 |
]
|
| 380 |
}
|
src/ui/gradio_app.py
CHANGED
|
@@ -416,7 +416,7 @@ def translate_aspect_performance(aspects: dict, restaurant_name: str) -> str:
|
|
| 416 |
|
| 417 |
|
| 418 |
def generate_chart(items: list, title: str) -> Optional[str]:
|
| 419 |
-
"""Generate sentiment chart - top 10 by mentions."""
|
| 420 |
if not items:
|
| 421 |
return None
|
| 422 |
|
|
@@ -432,7 +432,10 @@ def generate_chart(items: list, title: str) -> Optional[str]:
|
|
| 432 |
NEUTRAL = '#f59e0b'
|
| 433 |
NEGATIVE = '#ef4444'
|
| 434 |
|
|
|
|
|
|
|
| 435 |
sorted_items = sorted(items, key=lambda x: x.get('mention_count', 0), reverse=True)[:10]
|
|
|
|
| 436 |
|
| 437 |
names = [f"{item.get('name', '?')[:18]} ({item.get('mention_count', 0)})" for item in sorted_items]
|
| 438 |
sentiments = [item.get('sentiment', 0) for item in sorted_items]
|
|
@@ -619,7 +622,10 @@ def get_aspect_detail(aspect_name: str, state: dict) -> str:
|
|
| 619 |
# ============================================================================
|
| 620 |
|
| 621 |
def generate_pdf_report(state: dict) -> Optional[str]:
|
| 622 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 623 |
if not state:
|
| 624 |
print("[PDF] No state provided")
|
| 625 |
return None
|
|
@@ -628,189 +634,422 @@ def generate_pdf_report(state: dict) -> Optional[str]:
|
|
| 628 |
from reportlab.lib.pagesizes import letter
|
| 629 |
from reportlab.lib.units import inch
|
| 630 |
from reportlab.lib import colors
|
| 631 |
-
from reportlab.
|
|
|
|
| 632 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 633 |
-
from reportlab.lib.enums import TA_CENTER
|
| 634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
restaurant_name = state.get('restaurant_name', 'Restaurant')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 637 |
safe_name = restaurant_name.lower().replace(" ", "_").replace("/", "_").replace("&", "and")[:30]
|
| 638 |
output_path = os.path.join(tempfile.gettempdir(), f"{safe_name}_report_{timestamp}.pdf")
|
| 639 |
|
| 640 |
-
print(f"[PDF] Generating report for {restaurant_name}
|
| 641 |
|
| 642 |
-
doc = SimpleDocTemplate(
|
| 643 |
-
|
| 644 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
|
| 646 |
styles = getSampleStyleSheet()
|
| 647 |
|
| 648 |
# Custom styles
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
|
| 661 |
-
|
|
|
|
|
|
|
| 662 |
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
fontSize=18, alignment=TA_CENTER)))
|
| 667 |
-
elements.append(Paragraph(f"Generated: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}",
|
| 668 |
-
ParagraphStyle('Date', parent=styles['Normal'], alignment=TA_CENTER,
|
| 669 |
-
textColor=colors.grey)))
|
| 670 |
-
elements.append(Spacer(1, 30))
|
| 671 |
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
raw_reviews = state.get('raw_reviews', [])
|
| 676 |
-
source = state.get('source', 'unknown')
|
| 677 |
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
|
| 682 |
-
|
|
|
|
|
|
|
| 683 |
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
| 691 |
|
| 692 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
stats_table.setStyle(TableStyle([
|
| 694 |
-
('BACKGROUND', (0, 0), (-1,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 696 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 697 |
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 698 |
-
('
|
| 699 |
-
('TOPPADDING', (0, 0), (-1,
|
| 700 |
-
('
|
| 701 |
-
('
|
| 702 |
-
('
|
| 703 |
-
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
| 704 |
-
('LEFTPADDING', (0, 0), (-1, -1), 10),
|
| 705 |
-
('RIGHTPADDING', (0, 0), (-1, -1), 10),
|
| 706 |
]))
|
| 707 |
-
elements.append(
|
| 708 |
-
elements.append(
|
| 709 |
-
|
| 710 |
-
#
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
|
| 716 |
-
|
| 717 |
-
|
|
|
|
| 718 |
sentiment = item.get('sentiment', 0)
|
| 719 |
status = '✓ Positive' if sentiment > 0.3 else '~ Mixed' if sentiment > -0.3 else '✗ Negative'
|
| 720 |
-
menu_data.append([
|
| 721 |
-
item.get('name', '?').title()[:25],
|
| 722 |
-
f"{sentiment:+.2f}",
|
| 723 |
-
str(item.get('mention_count', 0)),
|
| 724 |
-
status
|
| 725 |
-
])
|
| 726 |
|
| 727 |
-
menu_table = Table(menu_data, colWidths=[2*inch, 1*inch, 0.
|
| 728 |
menu_table.setStyle(TableStyle([
|
| 729 |
-
('BACKGROUND', (0, 0), (-1, 0),
|
| 730 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 731 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 732 |
('FONTSIZE', (0, 0), (-1, -1), 9),
|
| 733 |
-
('
|
|
|
|
| 734 |
('TOPPADDING', (0, 0), (-1, -1), 6),
|
| 735 |
-
('
|
| 736 |
-
('
|
| 737 |
-
('
|
| 738 |
]))
|
| 739 |
elements.append(menu_table)
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
|
| 742 |
-
# Aspects
|
| 743 |
if aspect_list:
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
aspect_data = [['Aspect', 'Sentiment', 'Mentions', 'Status']]
|
| 748 |
-
for aspect in sorted_aspects:
|
| 749 |
sentiment = aspect.get('sentiment', 0)
|
| 750 |
status = '✓ Strength' if sentiment > 0.3 else '~ Neutral' if sentiment > -0.3 else '✗ Weakness'
|
| 751 |
-
aspect_data.append([
|
| 752 |
-
aspect.get('name', '?').title()[:25],
|
| 753 |
-
f"{sentiment:+.2f}",
|
| 754 |
-
str(aspect.get('mention_count', 0)),
|
| 755 |
-
status
|
| 756 |
-
])
|
| 757 |
|
| 758 |
-
aspect_table = Table(aspect_data, colWidths=[2*inch, 1*inch, 0.
|
| 759 |
aspect_table.setStyle(TableStyle([
|
| 760 |
-
('BACKGROUND', (0, 0), (-1, 0),
|
| 761 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 762 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 763 |
('FONTSIZE', (0, 0), (-1, -1), 9),
|
| 764 |
-
('
|
|
|
|
| 765 |
('TOPPADDING', (0, 0), (-1, -1), 6),
|
| 766 |
-
('
|
| 767 |
-
('
|
| 768 |
-
('
|
| 769 |
]))
|
| 770 |
elements.append(aspect_table)
|
| 771 |
-
|
| 772 |
|
| 773 |
-
#
|
| 774 |
-
|
|
|
|
| 775 |
|
| 776 |
-
|
| 777 |
-
if
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
elements.append(Paragraph(
|
| 781 |
-
|
| 782 |
-
|
|
|
|
|
|
|
| 783 |
if isinstance(strengths, list):
|
| 784 |
-
for s in strengths[:
|
| 785 |
text = s.get('action', str(s)) if isinstance(s, dict) else str(s)
|
| 786 |
-
elements.append(Paragraph(f"
|
| 787 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
if isinstance(recs, list):
|
| 797 |
-
for r in recs[:
|
| 798 |
if isinstance(r, dict):
|
| 799 |
-
priority = r.get('priority', '')
|
| 800 |
action = r.get('action', str(r))
|
| 801 |
-
|
|
|
|
| 802 |
else:
|
| 803 |
-
elements.append(Paragraph(f"• {r}",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
elements.append(Spacer(1, 30))
|
| 807 |
-
elements.append(
|
| 808 |
-
|
| 809 |
-
|
|
|
|
|
|
|
| 810 |
|
| 811 |
# Build PDF
|
| 812 |
doc.build(elements)
|
| 813 |
-
print(f"[PDF] Successfully generated: {output_path}")
|
| 814 |
return output_path
|
| 815 |
|
| 816 |
except Exception as e:
|
|
|
|
| 416 |
|
| 417 |
|
| 418 |
def generate_chart(items: list, title: str) -> Optional[str]:
|
| 419 |
+
"""Generate sentiment chart - top 10 by mentions, highest at TOP."""
|
| 420 |
if not items:
|
| 421 |
return None
|
| 422 |
|
|
|
|
| 432 |
NEUTRAL = '#f59e0b'
|
| 433 |
NEGATIVE = '#ef4444'
|
| 434 |
|
| 435 |
+
# Sort by mention_count descending, then REVERSE for display
|
| 436 |
+
# (so highest mentions appear at TOP of horizontal bar chart)
|
| 437 |
sorted_items = sorted(items, key=lambda x: x.get('mention_count', 0), reverse=True)[:10]
|
| 438 |
+
sorted_items = sorted_items[::-1] # Reverse so highest is at top
|
| 439 |
|
| 440 |
names = [f"{item.get('name', '?')[:18]} ({item.get('mention_count', 0)})" for item in sorted_items]
|
| 441 |
sentiments = [item.get('sentiment', 0) for item in sorted_items]
|
|
|
|
| 622 |
# ============================================================================
|
| 623 |
|
| 624 |
def generate_pdf_report(state: dict) -> Optional[str]:
|
| 625 |
+
"""
|
| 626 |
+
Generate professional PDF report from analysis state.
|
| 627 |
+
Uses ReportLab with custom styling for a polished output.
|
| 628 |
+
"""
|
| 629 |
if not state:
|
| 630 |
print("[PDF] No state provided")
|
| 631 |
return None
|
|
|
|
| 634 |
from reportlab.lib.pagesizes import letter
|
| 635 |
from reportlab.lib.units import inch
|
| 636 |
from reportlab.lib import colors
|
| 637 |
+
from reportlab.lib.colors import HexColor
|
| 638 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, HRFlowable
|
| 639 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 640 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 641 |
+
|
| 642 |
+
# Color scheme
|
| 643 |
+
PRIMARY = HexColor('#2563eb')
|
| 644 |
+
PRIMARY_LIGHT = HexColor('#dbeafe')
|
| 645 |
+
POSITIVE = HexColor('#10b981')
|
| 646 |
+
POSITIVE_LIGHT = HexColor('#d1fae5')
|
| 647 |
+
WARNING = HexColor('#f59e0b')
|
| 648 |
+
WARNING_LIGHT = HexColor('#fef3c7')
|
| 649 |
+
NEGATIVE = HexColor('#ef4444')
|
| 650 |
+
NEGATIVE_LIGHT = HexColor('#fee2e2')
|
| 651 |
+
TEXT_DARK = HexColor('#1f2937')
|
| 652 |
+
TEXT_LIGHT = HexColor('#6b7280')
|
| 653 |
+
BACKGROUND = HexColor('#f9fafb')
|
| 654 |
+
BORDER = HexColor('#e5e7eb')
|
| 655 |
+
|
| 656 |
+
# Extract data
|
| 657 |
restaurant_name = state.get('restaurant_name', 'Restaurant')
|
| 658 |
+
source = state.get('source', 'unknown').replace('_', ' ').title()
|
| 659 |
+
menu = state.get('menu_analysis', {})
|
| 660 |
+
aspects = state.get('aspect_analysis', {})
|
| 661 |
+
insights = state.get('insights', {})
|
| 662 |
+
raw_reviews = state.get('raw_reviews', [])
|
| 663 |
+
|
| 664 |
+
food_items = menu.get('food_items', [])
|
| 665 |
+
drinks = menu.get('drinks', [])
|
| 666 |
+
all_menu = food_items + drinks
|
| 667 |
+
aspect_list = aspects.get('aspects', [])
|
| 668 |
+
|
| 669 |
+
# Create file
|
| 670 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 671 |
safe_name = restaurant_name.lower().replace(" ", "_").replace("/", "_").replace("&", "and")[:30]
|
| 672 |
output_path = os.path.join(tempfile.gettempdir(), f"{safe_name}_report_{timestamp}.pdf")
|
| 673 |
|
| 674 |
+
print(f"[PDF] Generating professional report for {restaurant_name}")
|
| 675 |
|
| 676 |
+
doc = SimpleDocTemplate(
|
| 677 |
+
output_path,
|
| 678 |
+
pagesize=letter,
|
| 679 |
+
rightMargin=0.75*inch,
|
| 680 |
+
leftMargin=0.75*inch,
|
| 681 |
+
topMargin=0.75*inch,
|
| 682 |
+
bottomMargin=0.75*inch
|
| 683 |
+
)
|
| 684 |
|
| 685 |
styles = getSampleStyleSheet()
|
| 686 |
|
| 687 |
# Custom styles
|
| 688 |
+
styles.add(ParagraphStyle('CoverTitle', parent=styles['Heading1'],
|
| 689 |
+
fontSize=32, textColor=PRIMARY, alignment=TA_CENTER,
|
| 690 |
+
spaceAfter=10, fontName='Helvetica-Bold'))
|
| 691 |
|
| 692 |
+
styles.add(ParagraphStyle('CoverSubtitle', parent=styles['Normal'],
|
| 693 |
+
fontSize=16, textColor=TEXT_LIGHT, alignment=TA_CENTER,
|
| 694 |
+
spaceAfter=30, fontName='Helvetica'))
|
| 695 |
|
| 696 |
+
styles.add(ParagraphStyle('CoverRestaurant', parent=styles['Heading1'],
|
| 697 |
+
fontSize=24, textColor=TEXT_DARK, alignment=TA_CENTER,
|
| 698 |
+
spaceAfter=15, fontName='Helvetica-Bold'))
|
| 699 |
|
| 700 |
+
styles.add(ParagraphStyle('SectionHeader', parent=styles['Heading1'],
|
| 701 |
+
fontSize=18, textColor=PRIMARY, spaceBefore=20,
|
| 702 |
+
spaceAfter=12, fontName='Helvetica-Bold'))
|
| 703 |
|
| 704 |
+
styles.add(ParagraphStyle('SubHeader', parent=styles['Heading2'],
|
| 705 |
+
fontSize=14, textColor=TEXT_DARK, spaceBefore=15,
|
| 706 |
+
spaceAfter=8, fontName='Helvetica-Bold'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
|
| 708 |
+
styles.add(ParagraphStyle('BodyText', parent=styles['Normal'],
|
| 709 |
+
fontSize=10, textColor=TEXT_DARK, spaceAfter=8,
|
| 710 |
+
leading=14, fontName='Helvetica'))
|
|
|
|
|
|
|
| 711 |
|
| 712 |
+
styles.add(ParagraphStyle('Bullet', parent=styles['Normal'],
|
| 713 |
+
fontSize=10, textColor=TEXT_DARK, leftIndent=20,
|
| 714 |
+
spaceAfter=5, fontName='Helvetica'))
|
| 715 |
|
| 716 |
+
styles.add(ParagraphStyle('Quote', parent=styles['Normal'],
|
| 717 |
+
fontSize=10, textColor=TEXT_LIGHT, leftIndent=20,
|
| 718 |
+
rightIndent=20, spaceAfter=10, fontName='Helvetica-Oblique'))
|
| 719 |
|
| 720 |
+
styles.add(ParagraphStyle('Footer', parent=styles['Normal'],
|
| 721 |
+
fontSize=8, textColor=TEXT_LIGHT, alignment=TA_CENTER))
|
| 722 |
+
|
| 723 |
+
styles.add(ParagraphStyle('PriorityHigh', parent=styles['Normal'],
|
| 724 |
+
fontSize=10, textColor=NEGATIVE, leftIndent=20,
|
| 725 |
+
spaceAfter=5, fontName='Helvetica-Bold'))
|
| 726 |
+
|
| 727 |
+
styles.add(ParagraphStyle('PriorityMedium', parent=styles['Normal'],
|
| 728 |
+
fontSize=10, textColor=WARNING, leftIndent=20,
|
| 729 |
+
spaceAfter=5, fontName='Helvetica-Bold'))
|
| 730 |
|
| 731 |
+
styles.add(ParagraphStyle('PriorityLow', parent=styles['Normal'],
|
| 732 |
+
fontSize=10, textColor=POSITIVE, leftIndent=20,
|
| 733 |
+
spaceAfter=5, fontName='Helvetica-Bold'))
|
| 734 |
+
|
| 735 |
+
elements = []
|
| 736 |
+
|
| 737 |
+
# ==================== COVER PAGE ====================
|
| 738 |
+
elements.append(Spacer(1, 1.5*inch))
|
| 739 |
+
elements.append(Paragraph("RESTAURANT", styles['CoverTitle']))
|
| 740 |
+
elements.append(Paragraph("INTELLIGENCE REPORT", styles['CoverTitle']))
|
| 741 |
+
elements.append(Spacer(1, 0.3*inch))
|
| 742 |
+
elements.append(Paragraph("AI-Powered Customer Review Analysis", styles['CoverSubtitle']))
|
| 743 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=10, spaceAfter=10))
|
| 744 |
+
elements.append(Spacer(1, 0.5*inch))
|
| 745 |
+
elements.append(Paragraph(restaurant_name, styles['CoverRestaurant']))
|
| 746 |
+
elements.append(Spacer(1, 0.3*inch))
|
| 747 |
+
elements.append(Paragraph(f"Data Source: {source}", styles['Footer']))
|
| 748 |
+
elements.append(Spacer(1, 0.5*inch))
|
| 749 |
+
|
| 750 |
+
# Stats boxes
|
| 751 |
+
stats_data = [[
|
| 752 |
+
str(len(raw_reviews)), str(len(all_menu)), str(len(aspect_list))
|
| 753 |
+
], [
|
| 754 |
+
"Reviews", "Menu Items", "Aspects"
|
| 755 |
+
]]
|
| 756 |
+
stats_table = Table(stats_data, colWidths=[2*inch, 2*inch, 2*inch])
|
| 757 |
stats_table.setStyle(TableStyle([
|
| 758 |
+
('BACKGROUND', (0, 0), (-1, -1), BACKGROUND),
|
| 759 |
+
('TEXTCOLOR', (0, 0), (-1, 0), PRIMARY),
|
| 760 |
+
('TEXTCOLOR', (0, 1), (-1, 1), TEXT_LIGHT),
|
| 761 |
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 762 |
+
('FONTSIZE', (0, 0), (-1, 0), 24),
|
| 763 |
+
('FONTSIZE', (0, 1), (-1, 1), 10),
|
| 764 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 765 |
+
('TOPPADDING', (0, 0), (-1, -1), 15),
|
| 766 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
|
| 767 |
+
('BOX', (0, 0), (-1, -1), 1, BORDER),
|
| 768 |
+
]))
|
| 769 |
+
elements.append(stats_table)
|
| 770 |
+
elements.append(Spacer(1, 1*inch))
|
| 771 |
+
elements.append(Paragraph(f"Generated: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", styles['Footer']))
|
| 772 |
+
elements.append(Paragraph("Powered by Claude AI • Restaurant Intelligence Agent", styles['Footer']))
|
| 773 |
+
elements.append(PageBreak())
|
| 774 |
+
|
| 775 |
+
# ==================== EXECUTIVE SUMMARY ====================
|
| 776 |
+
elements.append(Paragraph("Executive Summary", styles['SectionHeader']))
|
| 777 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=5, spaceAfter=15))
|
| 778 |
+
|
| 779 |
+
# Calculate sentiment
|
| 780 |
+
all_sentiments = [item.get('sentiment', 0) for item in all_menu]
|
| 781 |
+
avg_sentiment = sum(all_sentiments) / len(all_sentiments) if all_sentiments else 0
|
| 782 |
+
|
| 783 |
+
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"
|
| 784 |
+
sent_color = POSITIVE if avg_sentiment > 0.3 else WARNING if avg_sentiment > -0.3 else NEGATIVE
|
| 785 |
+
sent_bg = POSITIVE_LIGHT if avg_sentiment > 0.3 else WARNING_LIGHT if avg_sentiment > -0.3 else NEGATIVE_LIGHT
|
| 786 |
+
|
| 787 |
+
# Sentiment box
|
| 788 |
+
sent_data = [[f"Overall Sentiment: {avg_sentiment:+.2f}", sent_label]]
|
| 789 |
+
sent_table = Table(sent_data, colWidths=[3.5*inch, 2*inch])
|
| 790 |
+
sent_table.setStyle(TableStyle([
|
| 791 |
+
('BACKGROUND', (0, 0), (-1, -1), sent_bg),
|
| 792 |
+
('TEXTCOLOR', (0, 0), (-1, -1), sent_color),
|
| 793 |
+
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
|
| 794 |
+
('FONTSIZE', (0, 0), (-1, -1), 14),
|
| 795 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 796 |
+
('TOPPADDING', (0, 0), (-1, -1), 15),
|
| 797 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
|
| 798 |
+
('BOX', (0, 0), (-1, -1), 2, sent_color),
|
| 799 |
+
]))
|
| 800 |
+
elements.append(sent_table)
|
| 801 |
+
elements.append(Spacer(1, 15))
|
| 802 |
+
|
| 803 |
+
# Key highlights
|
| 804 |
+
elements.append(Paragraph("Key Highlights", styles['SubHeader']))
|
| 805 |
+
top_items = sorted(all_menu, key=lambda x: x.get('sentiment', 0), reverse=True)[:3]
|
| 806 |
+
if top_items:
|
| 807 |
+
elements.append(Paragraph("✅ <b>Top Performing Items:</b>", styles['BodyText']))
|
| 808 |
+
for item in top_items:
|
| 809 |
+
elements.append(Paragraph(f" • {item.get('name', '?').title()} (sentiment: {item.get('sentiment', 0):+.2f})", styles['Bullet']))
|
| 810 |
+
|
| 811 |
+
concern_items = [i for i in all_menu if i.get('sentiment', 0) < -0.2]
|
| 812 |
+
if concern_items:
|
| 813 |
+
elements.append(Spacer(1, 10))
|
| 814 |
+
elements.append(Paragraph("⚠️ <b>Items Needing Attention:</b>", styles['BodyText']))
|
| 815 |
+
for item in sorted(concern_items, key=lambda x: x.get('sentiment', 0))[:3]:
|
| 816 |
+
elements.append(Paragraph(f" • {item.get('name', '?').title()} (sentiment: {item.get('sentiment', 0):+.2f})", styles['Bullet']))
|
| 817 |
+
|
| 818 |
+
elements.append(Spacer(1, 15))
|
| 819 |
+
|
| 820 |
+
# Summary stats
|
| 821 |
+
stars = len([i for i in all_menu if i.get('sentiment', 0) > 0.5])
|
| 822 |
+
good = len([i for i in all_menu if 0.2 < i.get('sentiment', 0) <= 0.5])
|
| 823 |
+
mixed = len([i for i in all_menu if -0.2 <= i.get('sentiment', 0) <= 0.2])
|
| 824 |
+
concerns = len([i for i in all_menu if i.get('sentiment', 0) < -0.2])
|
| 825 |
+
|
| 826 |
+
summary_data = [
|
| 827 |
+
['Metric', 'Value', 'Details'],
|
| 828 |
+
['Reviews Analyzed', str(len(raw_reviews)), f'From {source}'],
|
| 829 |
+
['Menu Items', str(len(all_menu)), f'{len(food_items)} food, {len(drinks)} drinks'],
|
| 830 |
+
['Customer Favorites', str(stars), 'Sentiment > 0.5'],
|
| 831 |
+
['Performing Well', str(good), 'Sentiment 0.2 - 0.5'],
|
| 832 |
+
['Mixed Reviews', str(mixed), 'Sentiment -0.2 - 0.2'],
|
| 833 |
+
['Needs Attention', str(concerns), 'Sentiment < -0.2'],
|
| 834 |
+
]
|
| 835 |
+
summary_table = Table(summary_data, colWidths=[2*inch, 1.3*inch, 2.5*inch])
|
| 836 |
+
summary_table.setStyle(TableStyle([
|
| 837 |
+
('BACKGROUND', (0, 0), (-1, 0), PRIMARY),
|
| 838 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 839 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 840 |
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 841 |
+
('ALIGN', (1, 0), (1, -1), 'CENTER'),
|
| 842 |
+
('TOPPADDING', (0, 0), (-1, -1), 8),
|
| 843 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 844 |
+
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, BACKGROUND]),
|
| 845 |
+
('GRID', (0, 0), (-1, -1), 0.5, BORDER),
|
|
|
|
|
|
|
|
|
|
| 846 |
]))
|
| 847 |
+
elements.append(summary_table)
|
| 848 |
+
elements.append(PageBreak())
|
| 849 |
+
|
| 850 |
+
# ==================== MENU ANALYSIS ====================
|
| 851 |
+
elements.append(Paragraph("Menu Performance Analysis", styles['SectionHeader']))
|
| 852 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=5, spaceAfter=15))
|
| 853 |
+
|
| 854 |
+
if all_menu:
|
| 855 |
+
elements.append(Paragraph(
|
| 856 |
+
f"Analysis of <b>{len(all_menu)}</b> menu items ({len(food_items)} food, {len(drinks)} drinks) based on {len(raw_reviews)} customer reviews.",
|
| 857 |
+
styles['BodyText']
|
| 858 |
+
))
|
| 859 |
+
elements.append(Spacer(1, 10))
|
| 860 |
|
| 861 |
+
sorted_menu = sorted(all_menu, key=lambda x: x.get('mention_count', 0), reverse=True)[:20]
|
| 862 |
+
menu_data = [['#', 'Item', 'Sentiment', 'Mentions', 'Status']]
|
| 863 |
+
for i, item in enumerate(sorted_menu, 1):
|
| 864 |
sentiment = item.get('sentiment', 0)
|
| 865 |
status = '✓ Positive' if sentiment > 0.3 else '~ Mixed' if sentiment > -0.3 else '✗ Negative'
|
| 866 |
+
menu_data.append([str(i), item.get('name', '?').title()[:22], f"{sentiment:+.2f}", str(item.get('mention_count', 0)), status])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
|
| 868 |
+
menu_table = Table(menu_data, colWidths=[0.4*inch, 2.2*inch, 1*inch, 0.9*inch, 1.1*inch])
|
| 869 |
menu_table.setStyle(TableStyle([
|
| 870 |
+
('BACKGROUND', (0, 0), (-1, 0), POSITIVE),
|
| 871 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 872 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 873 |
('FONTSIZE', (0, 0), (-1, -1), 9),
|
| 874 |
+
('ALIGN', (0, 0), (0, -1), 'CENTER'),
|
| 875 |
+
('ALIGN', (2, 0), (3, -1), 'CENTER'),
|
| 876 |
('TOPPADDING', (0, 0), (-1, -1), 6),
|
| 877 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
| 878 |
+
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, BACKGROUND]),
|
| 879 |
+
('GRID', (0, 0), (-1, -1), 0.5, BORDER),
|
| 880 |
]))
|
| 881 |
elements.append(menu_table)
|
| 882 |
+
elements.append(Spacer(1, 20))
|
| 883 |
+
|
| 884 |
+
# ==================== ASPECT ANALYSIS ====================
|
| 885 |
+
elements.append(Paragraph("Customer Experience Aspects", styles['SectionHeader']))
|
| 886 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=5, spaceAfter=15))
|
| 887 |
|
|
|
|
| 888 |
if aspect_list:
|
| 889 |
+
sorted_aspects = sorted(aspect_list, key=lambda x: x.get('mention_count', 0), reverse=True)[:20]
|
| 890 |
+
aspect_data = [['#', 'Aspect', 'Sentiment', 'Mentions', 'Status']]
|
| 891 |
+
for i, aspect in enumerate(sorted_aspects, 1):
|
|
|
|
|
|
|
| 892 |
sentiment = aspect.get('sentiment', 0)
|
| 893 |
status = '✓ Strength' if sentiment > 0.3 else '~ Neutral' if sentiment > -0.3 else '✗ Weakness'
|
| 894 |
+
aspect_data.append([str(i), aspect.get('name', '?').title()[:22], f"{sentiment:+.2f}", str(aspect.get('mention_count', 0)), status])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
|
| 896 |
+
aspect_table = Table(aspect_data, colWidths=[0.4*inch, 2.2*inch, 1*inch, 0.9*inch, 1.1*inch])
|
| 897 |
aspect_table.setStyle(TableStyle([
|
| 898 |
+
('BACKGROUND', (0, 0), (-1, 0), WARNING),
|
| 899 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 900 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 901 |
('FONTSIZE', (0, 0), (-1, -1), 9),
|
| 902 |
+
('ALIGN', (0, 0), (0, -1), 'CENTER'),
|
| 903 |
+
('ALIGN', (2, 0), (3, -1), 'CENTER'),
|
| 904 |
('TOPPADDING', (0, 0), (-1, -1), 6),
|
| 905 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
| 906 |
+
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, BACKGROUND]),
|
| 907 |
+
('GRID', (0, 0), (-1, -1), 0.5, BORDER),
|
| 908 |
]))
|
| 909 |
elements.append(aspect_table)
|
| 910 |
+
elements.append(PageBreak())
|
| 911 |
|
| 912 |
+
# ==================== CHEF INSIGHTS ====================
|
| 913 |
+
elements.append(Paragraph("🍳 Chef Insights", styles['SectionHeader']))
|
| 914 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=5, spaceAfter=15))
|
| 915 |
|
| 916 |
+
chef_data = insights.get('chef', {})
|
| 917 |
+
if chef_data:
|
| 918 |
+
if chef_data.get('summary'):
|
| 919 |
+
elements.append(Paragraph("Summary", styles['SubHeader']))
|
| 920 |
+
elements.append(Paragraph(str(chef_data['summary']), styles['BodyText']))
|
| 921 |
+
|
| 922 |
+
if chef_data.get('strengths'):
|
| 923 |
+
elements.append(Paragraph("✅ Strengths", styles['SubHeader']))
|
| 924 |
+
strengths = chef_data['strengths']
|
| 925 |
if isinstance(strengths, list):
|
| 926 |
+
for s in strengths[:8]: # Show up to 8 strengths
|
| 927 |
text = s.get('action', str(s)) if isinstance(s, dict) else str(s)
|
| 928 |
+
elements.append(Paragraph(f"• {text}", styles['Bullet']))
|
| 929 |
+
|
| 930 |
+
if chef_data.get('concerns'):
|
| 931 |
+
elements.append(Paragraph("⚠️ Areas of Concern", styles['SubHeader']))
|
| 932 |
+
concerns = chef_data['concerns']
|
| 933 |
+
if isinstance(concerns, list):
|
| 934 |
+
for c in concerns[:5]: # Show up to 5 concerns
|
| 935 |
+
text = c.get('action', str(c)) if isinstance(c, dict) else str(c)
|
| 936 |
+
elements.append(Paragraph(f"• {text}", styles['Bullet']))
|
| 937 |
+
|
| 938 |
+
if chef_data.get('recommendations'):
|
| 939 |
+
elements.append(Paragraph("💡 Recommendations", styles['SubHeader']))
|
| 940 |
+
recs = chef_data['recommendations']
|
| 941 |
+
if isinstance(recs, list):
|
| 942 |
+
for r in recs[:8]: # Show up to 8 recommendations
|
| 943 |
+
if isinstance(r, dict):
|
| 944 |
+
priority = r.get('priority', 'medium').lower()
|
| 945 |
+
action = r.get('action', str(r))
|
| 946 |
+
style_name = 'PriorityHigh' if priority == 'high' else 'PriorityMedium' if priority == 'medium' else 'PriorityLow'
|
| 947 |
+
elements.append(Paragraph(f"[{priority.upper()}] {action}", styles[style_name]))
|
| 948 |
+
else:
|
| 949 |
+
elements.append(Paragraph(f"• {r}", styles['Bullet']))
|
| 950 |
+
else:
|
| 951 |
+
elements.append(Paragraph("Chef insights will be available after full analysis.", styles['BodyText']))
|
| 952 |
|
| 953 |
+
elements.append(Spacer(1, 20))
|
| 954 |
+
|
| 955 |
+
# ==================== MANAGER INSIGHTS ====================
|
| 956 |
+
elements.append(Paragraph("📊 Manager Insights", styles['SectionHeader']))
|
| 957 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=5, spaceAfter=15))
|
| 958 |
+
|
| 959 |
+
manager_data = insights.get('manager', {})
|
| 960 |
+
if manager_data:
|
| 961 |
+
if manager_data.get('summary'):
|
| 962 |
+
elements.append(Paragraph("Summary", styles['SubHeader']))
|
| 963 |
+
elements.append(Paragraph(str(manager_data['summary']), styles['BodyText']))
|
| 964 |
+
|
| 965 |
+
if manager_data.get('strengths'):
|
| 966 |
+
elements.append(Paragraph("✅ Operational Strengths", styles['SubHeader']))
|
| 967 |
+
strengths = manager_data['strengths']
|
| 968 |
+
if isinstance(strengths, list):
|
| 969 |
+
for s in strengths[:8]: # Show up to 8 strengths
|
| 970 |
+
text = s.get('action', str(s)) if isinstance(s, dict) else str(s)
|
| 971 |
+
elements.append(Paragraph(f"• {text}", styles['Bullet']))
|
| 972 |
+
|
| 973 |
+
if manager_data.get('concerns'):
|
| 974 |
+
elements.append(Paragraph("⚠️ Operational Concerns", styles['SubHeader']))
|
| 975 |
+
concerns = manager_data['concerns']
|
| 976 |
+
if isinstance(concerns, list):
|
| 977 |
+
for c in concerns[:5]: # Show up to 5 concerns
|
| 978 |
+
text = c.get('action', str(c)) if isinstance(c, dict) else str(c)
|
| 979 |
+
elements.append(Paragraph(f"• {text}", styles['Bullet']))
|
| 980 |
+
|
| 981 |
+
if manager_data.get('recommendations'):
|
| 982 |
+
elements.append(Paragraph("💡 Action Items", styles['SubHeader']))
|
| 983 |
+
recs = manager_data['recommendations']
|
| 984 |
if isinstance(recs, list):
|
| 985 |
+
for r in recs[:8]: # Show up to 8 recommendations
|
| 986 |
if isinstance(r, dict):
|
| 987 |
+
priority = r.get('priority', 'medium').lower()
|
| 988 |
action = r.get('action', str(r))
|
| 989 |
+
style_name = 'PriorityHigh' if priority == 'high' else 'PriorityMedium' if priority == 'medium' else 'PriorityLow'
|
| 990 |
+
elements.append(Paragraph(f"[{priority.upper()}] {action}", styles[style_name]))
|
| 991 |
else:
|
| 992 |
+
elements.append(Paragraph(f"• {r}", styles['Bullet']))
|
| 993 |
+
else:
|
| 994 |
+
elements.append(Paragraph("Manager insights will be available after full analysis.", styles['BodyText']))
|
| 995 |
+
|
| 996 |
+
elements.append(PageBreak())
|
| 997 |
|
| 998 |
+
# ==================== CUSTOMER FEEDBACK HIGHLIGHTS ====================
|
| 999 |
+
elements.append(Paragraph("Customer Feedback Highlights", styles['SectionHeader']))
|
| 1000 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=5, spaceAfter=15))
|
| 1001 |
+
|
| 1002 |
+
positive_reviews = []
|
| 1003 |
+
negative_reviews = []
|
| 1004 |
+
|
| 1005 |
+
for review in raw_reviews[:50]:
|
| 1006 |
+
if isinstance(review, dict):
|
| 1007 |
+
text = review.get('text', '') or review.get('review_text', '')
|
| 1008 |
+
else:
|
| 1009 |
+
text = str(review)
|
| 1010 |
+
|
| 1011 |
+
if not text or len(text) < 30:
|
| 1012 |
+
continue
|
| 1013 |
+
|
| 1014 |
+
text_lower = text.lower()
|
| 1015 |
+
pos_words = ['amazing', 'excellent', 'fantastic', 'great', 'awesome', 'delicious', 'perfect', 'loved', 'best']
|
| 1016 |
+
neg_words = ['terrible', 'horrible', 'awful', 'bad', 'worst', 'disappointing', 'poor', 'rude']
|
| 1017 |
+
|
| 1018 |
+
pos_count = sum(1 for w in pos_words if w in text_lower)
|
| 1019 |
+
neg_count = sum(1 for w in neg_words if w in text_lower)
|
| 1020 |
+
|
| 1021 |
+
if pos_count > neg_count and len(positive_reviews) < 3:
|
| 1022 |
+
positive_reviews.append(text[:180])
|
| 1023 |
+
elif neg_count > pos_count and len(negative_reviews) < 3:
|
| 1024 |
+
negative_reviews.append(text[:180])
|
| 1025 |
+
|
| 1026 |
+
elements.append(Paragraph("✅ Positive Feedback", styles['SubHeader']))
|
| 1027 |
+
if positive_reviews:
|
| 1028 |
+
for review in positive_reviews:
|
| 1029 |
+
elements.append(Paragraph(f'"{review}..."', styles['Quote']))
|
| 1030 |
+
else:
|
| 1031 |
+
elements.append(Paragraph("Detailed positive feedback samples not available.", styles['BodyText']))
|
| 1032 |
+
|
| 1033 |
+
elements.append(Spacer(1, 15))
|
| 1034 |
+
|
| 1035 |
+
elements.append(Paragraph("⚠️ Critical Feedback", styles['SubHeader']))
|
| 1036 |
+
if negative_reviews:
|
| 1037 |
+
for review in negative_reviews:
|
| 1038 |
+
elements.append(Paragraph(f'"{review}..."', styles['Quote']))
|
| 1039 |
+
else:
|
| 1040 |
+
elements.append(Paragraph("No significant negative feedback identified. Great job!", styles['BodyText']))
|
| 1041 |
+
|
| 1042 |
+
# ==================== FOOTER ====================
|
| 1043 |
elements.append(Spacer(1, 30))
|
| 1044 |
+
elements.append(HRFlowable(width="100%", thickness=1, color=BORDER, spaceBefore=10, spaceAfter=10))
|
| 1045 |
+
elements.append(Paragraph(f"Report generated for {restaurant_name}", styles['Footer']))
|
| 1046 |
+
elements.append(Paragraph(f"Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", styles['Footer']))
|
| 1047 |
+
elements.append(Paragraph("Restaurant Intelligence Agent • Powered by Claude AI", styles['Footer']))
|
| 1048 |
+
elements.append(Paragraph("© 2025 - Built for Anthropic MCP Hackathon", styles['Footer']))
|
| 1049 |
|
| 1050 |
# Build PDF
|
| 1051 |
doc.build(elements)
|
| 1052 |
+
print(f"[PDF] Successfully generated professional report: {output_path}")
|
| 1053 |
return output_path
|
| 1054 |
|
| 1055 |
except Exception as e:
|