TushP commited on
Commit
41fda39
·
verified ·
1 Parent(s): 1764bcf

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. src/agent/insights_generator.py +206 -94
  2. src/ui/gradio_app.py +362 -123
src/agent/insights_generator.py CHANGED
@@ -1,62 +1,56 @@
1
  """
2
- Insight Generation Module
 
3
 
4
- Generates actionable, role-specific insights from restaurant review analysis.
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 role-specific insights from analysis results.
 
 
20
  """
21
 
22
- def __init__(self, client: Anthropic, model: str):
23
  """
24
  Initialize the insights generator.
25
 
26
  Args:
27
  client: Anthropic client instance
28
- model: Claude model to use
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 = 'the restaurant'
38
  ) -> Dict[str, Any]:
39
  """
40
- Generate role-specific insights.
41
 
42
  Args:
43
- analysis_data: Results from analysis
44
- role: Target role ('chef' or 'manager')
45
  restaurant_name: Name of the restaurant
46
 
47
  Returns:
48
- Dictionary with summary, strengths, concerns, recommendations
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
- # Extract insights
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 = json.loads(insights_text)
78
 
79
- # Validate structure
80
- if not all(key in insights for key in ['summary', 'strengths', 'concerns', 'recommendations']):
81
- print(f"⚠️ Incomplete insights structure, using fallback")
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" Error generating insights: {e}")
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
- Build prompt for chef-focused insights.
101
- """
102
- # Prepare summary of analysis data
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. Output ONLY valid JSON, no other text
 
129
 
130
  OUTPUT FORMAT (JSON):
131
  {{
132
- "summary": "2-3 sentence executive summary",
133
- "strengths": ["Specific strength 1", "Specific strength 2", "Specific strength 3"],
134
- "concerns": ["Specific concern 1", "Specific concern 2"],
 
 
 
 
 
 
 
 
 
 
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: Ensure all JSON is properly formatted with no trailing commas.
 
 
 
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
- Build prompt for manager-focused insights.
158
- """
159
- # Prepare summary of analysis data
160
- aspect_summary = self._summarize_aspect_data(analysis_data, focus='operations')
 
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. Output ONLY valid JSON, no other text
 
183
 
184
  OUTPUT FORMAT (JSON):
185
  {{
186
- "summary": "2-3 sentence executive summary",
187
- "strengths": ["Specific strength 1", "Specific strength 2", "Specific strength 3"],
188
- "concerns": ["Specific concern 1", "Specific concern 2"],
 
 
 
 
 
 
 
 
 
 
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: Ensure all JSON is properly formatted with no trailing commas.
 
 
 
200
 
201
  Generate manager insights:"""
202
 
203
  return prompt
204
 
205
- def _summarize_menu_data(self, analysis_data: Dict[str, Any]) -> str:
206
- """Summarize menu analysis for prompts."""
 
 
 
 
 
 
 
 
 
207
  menu_data = analysis_data.get('menu_analysis', {})
208
- food_items = menu_data.get('food_items', [])[:10] # Top 10
209
- drinks = menu_data.get('drinks', [])[:5] # Top 5
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
- summary.append(f" - {item.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
 
 
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
- summary.append(f" - {drink.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
 
 
 
 
 
 
226
 
227
  return '\n'.join(summary) if summary else "No menu data available"
228
 
229
- def _summarize_aspect_data(self, analysis_data: Dict[str, Any], focus: str = 'all') -> str:
230
- """Summarize aspect analysis for prompts."""
 
 
 
 
 
 
 
 
 
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', 'portion']
 
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', 'clean']
 
 
240
  aspects = [a for a in aspects if any(kw in a.get('name', '').lower() for kw in ops_keywords)]
241
 
242
- aspects = aspects[:10] # Top 10
243
 
244
  summary = []
 
 
245
  for aspect in aspects:
246
  sentiment = aspect.get('sentiment', 0)
247
  mentions = aspect.get('mention_count', 0)
248
- summary.append(f" - {aspect.get('name', 'unknown')}: sentiment {sentiment:+.2f}, {mentions} mentions")
 
 
 
 
 
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 data available for review"],
259
- "concerns": ["Insight generation encountered an error"],
260
  "recommendations": [
261
  {
262
- "priority": "high",
263
- "action": "Retry insight generation",
264
- "reason": "Complete analysis requires insights",
265
- "evidence": "System error"
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
- """Generate PDF report from analysis state - FIXED VERSION."""
 
 
 
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.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
 
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} at {output_path}")
641
 
642
- doc = SimpleDocTemplate(output_path, pagesize=letter,
643
- rightMargin=0.75*inch, leftMargin=0.75*inch,
644
- topMargin=0.75*inch, bottomMargin=0.75*inch)
 
 
 
 
 
645
 
646
  styles = getSampleStyleSheet()
647
 
648
  # Custom styles
649
- title_style = ParagraphStyle('CustomTitle', parent=styles['Heading1'],
650
- fontSize=24, textColor=colors.HexColor('#2196F3'),
651
- alignment=TA_CENTER, spaceAfter=20)
652
 
653
- heading_style = ParagraphStyle('CustomHeading', parent=styles['Heading2'],
654
- fontSize=14, textColor=colors.HexColor('#1f2937'),
655
- spaceBefore=15, spaceAfter=10)
656
 
657
- body_style = ParagraphStyle('CustomBody', parent=styles['Normal'],
658
- fontSize=10, textColor=colors.HexColor('#374151'),
659
- spaceAfter=8)
660
 
661
- elements = []
 
 
662
 
663
- # Title
664
- elements.append(Paragraph("🍽️ Restaurant Intelligence Report", title_style))
665
- elements.append(Paragraph(restaurant_name, ParagraphStyle('RestName', parent=styles['Heading2'],
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
- # Stats
673
- menu = state.get('menu_analysis', {})
674
- aspects = state.get('aspect_analysis', {})
675
- raw_reviews = state.get('raw_reviews', [])
676
- source = state.get('source', 'unknown')
677
 
678
- food_items = menu.get('food_items', [])
679
- drinks = menu.get('drinks', [])
680
- aspect_list = aspects.get('aspects', [])
681
 
682
- elements.append(Paragraph("📊 Executive Summary", heading_style))
 
 
683
 
684
- stats_data = [
685
- ['Metric', 'Value'],
686
- ['Source Platform', source.replace('_', ' ').title()],
687
- ['Reviews Analyzed', str(len(raw_reviews))],
688
- ['Menu Items Found', f"{len(food_items) + len(drinks)} ({len(food_items)} food, {len(drinks)} drinks)"],
689
- ['Aspects Analyzed', str(len(aspect_list))],
690
- ]
 
 
 
691
 
692
- stats_table = Table(stats_data, colWidths=[2.5*inch, 3*inch])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  stats_table.setStyle(TableStyle([
694
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2196F3')),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
696
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
697
  ('FONTSIZE', (0, 0), (-1, -1), 10),
698
- ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
699
- ('TOPPADDING', (0, 0), (-1, 0), 10),
700
- ('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#f3f4f6')),
701
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e5e7eb')),
702
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
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(stats_table)
708
- elements.append(Spacer(1, 20))
709
-
710
- # Top Menu Items
711
- if food_items or drinks:
712
- elements.append(Paragraph("🍽️ Top Menu Items (by mentions)", heading_style))
713
- all_menu = food_items + drinks
714
- sorted_menu = sorted(all_menu, key=lambda x: x.get('mention_count', 0), reverse=True)[:10]
 
 
 
 
 
715
 
716
- menu_data = [['Item', 'Sentiment', 'Mentions', 'Status']]
717
- for item in sorted_menu:
 
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.8*inch, 1.2*inch])
728
  menu_table.setStyle(TableStyle([
729
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#10b981')),
730
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
731
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
732
  ('FONTSIZE', (0, 0), (-1, -1), 9),
733
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
 
734
  ('TOPPADDING', (0, 0), (-1, -1), 6),
735
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f9fafb')]),
736
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e5e7eb')),
737
- ('ALIGN', (1, 0), (-1, -1), 'CENTER'),
738
  ]))
739
  elements.append(menu_table)
740
- elements.append(Spacer(1, 20))
 
 
 
 
741
 
742
- # Aspects
743
  if aspect_list:
744
- elements.append(Paragraph("📊 Customer Experience Aspects", heading_style))
745
- sorted_aspects = sorted(aspect_list, key=lambda x: x.get('mention_count', 0), reverse=True)[:10]
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.8*inch, 1.2*inch])
759
  aspect_table.setStyle(TableStyle([
760
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f59e0b')),
761
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
762
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
763
  ('FONTSIZE', (0, 0), (-1, -1), 9),
764
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
 
765
  ('TOPPADDING', (0, 0), (-1, -1), 6),
766
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f9fafb')]),
767
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e5e7eb')),
768
- ('ALIGN', (1, 0), (-1, -1), 'CENTER'),
769
  ]))
770
  elements.append(aspect_table)
771
- elements.append(Spacer(1, 20))
772
 
773
- # Insights
774
- insights = state.get('insights', {})
 
775
 
776
- chef_insights = insights.get('chef', {})
777
- if chef_insights:
778
- elements.append(Paragraph("🍳 Chef Insights", heading_style))
779
- if chef_insights.get('summary'):
780
- elements.append(Paragraph(f"<b>Summary:</b> {chef_insights['summary']}", body_style))
781
- if chef_insights.get('strengths'):
782
- strengths = chef_insights['strengths']
 
 
783
  if isinstance(strengths, list):
784
- for s in strengths[:3]:
785
  text = s.get('action', str(s)) if isinstance(s, dict) else str(s)
786
- elements.append(Paragraph(f" {text}", body_style))
787
- elements.append(Spacer(1, 10))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
 
789
- manager_insights = insights.get('manager', {})
790
- if manager_insights:
791
- elements.append(Paragraph("📊 Manager Insights", heading_style))
792
- if manager_insights.get('summary'):
793
- elements.append(Paragraph(f"<b>Summary:</b> {manager_insights['summary']}", body_style))
794
- if manager_insights.get('recommendations'):
795
- recs = manager_insights['recommendations']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  if isinstance(recs, list):
797
- for r in recs[:3]:
798
  if isinstance(r, dict):
799
- priority = r.get('priority', '')
800
  action = r.get('action', str(r))
801
- elements.append(Paragraph(f"[{priority.upper()}] {action}", body_style))
 
802
  else:
803
- elements.append(Paragraph(f"• {r}", body_style))
 
 
 
 
804
 
805
- # Footer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
  elements.append(Spacer(1, 30))
807
- elements.append(Paragraph("Generated by Restaurant Intelligence Agent | Powered by Claude AI",
808
- ParagraphStyle('Footer', parent=styles['Normal'],
809
- fontSize=8, textColor=colors.grey, alignment=TA_CENTER)))
 
 
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: