Spaces:
Sleeping
A newer version of the Gradio SDK is available:
6.5.1
Gradio 6 Implementation Guide
Restaurant Intelligence Agent UI
Date: November 24, 2025 (Day 15)
Hackathon: Anthropic MCP 1st Birthday - Track 2 (Productivity)
π Table of Contents
- Overview
- Installation
- Architecture
- Implementation Steps
- Key Components
- Challenges & Solutions
- Testing
- Next Steps
π― Overview
Built a production-ready Gradio 6 web interface for the Restaurant Intelligence Agent that:
- Accepts OpenTable URLs for analysis
- Displays role-based insights (Chef vs Manager)
- Enables Q&A over customer reviews
- Provides interactive drill-down functionality
Technology Stack:
- Framework: Gradio 6.0.0
- Backend: Python 3.12
- AI: Claude Sonnet 4 (via Anthropic API)
- Scraper: Selenium + BeautifulSoup
- Analysis: Custom NLP pipeline
π¦ Installation
Step 1: Install Gradio 6
pip install gradio==6.0.0
Step 2: Verify Installation
import gradio as gr
print(gr.__version__) # Should show 6.0.0
Step 3: Install Project Dependencies
pip install anthropic selenium beautifulsoup4 pandas python-dotenv fastmcp
ποΈ Architecture
File Structure
src/
βββ ui/
β βββ __init__.py
β βββ gradio_app.py # Main Gradio interface
βββ scrapers/
β βββ opentable_scraper.py # Web scraping
βββ data_processing/
β βββ review_cleaner.py # Text preprocessing
βββ agent/
β βββ base_agent.py # Core analysis agent
β βββ unified_analyzer.py # Menu/aspect analysis
β βββ insights_generator.py # Chef/Manager insights
βββ mcp_integrations/
βββ generate_chart.py # Visualizations
βββ query_reviews.py # Q&A system (RAG)
Data Flow
User Input (URL + Review Count)
β
[Gradio Interface]
β
[OpenTable Scraper] β Raw HTML
β
[Review Processor] β Cleaned Text
β
[AI Agent] β Unified Analysis
β
[Insights Generator] β Chef + Manager Insights
β
[Visualization Generator] β Charts
β
[Gradio Display] β Interactive Results
β
[Q&A System] β User Questions
π οΈ Implementation Steps
Step 1: Create UI Directory Structure
mkdir -p src/ui
touch src/ui/__init__.py
touch src/ui/gradio_app.py
Step 2: Build Basic Gradio Interface
Key Gradio 6 Change: Theme moved from Blocks() to .launch()
import gradio as gr
# β OLD (Gradio 5)
with gr.Blocks(theme=gr.themes.Soft()) as demo:
pass
# β
NEW (Gradio 6)
with gr.Blocks() as demo:
pass
demo.launch(theme=gr.themes.Soft())
Step 3: Design Layout
Three-Tab Design:
- Chef Insights - Menu performance, food quality
- Manager Insights - Service, operations, ambience
- Ask Questions - RAG-powered Q&A
Components Used:
gr.Textbox()- URL input, progress displaygr.Dropdown()- Review count selection, drill-down menusgr.Button()- Analyze, Ask buttonsgr.Image()- Charts displaygr.Markdown()- Formatted insightsgr.State()- Context persistence (critical!)gr.Tabs()+gr.Tab()- Tabbed navigation
Step 4: Implement Progress Tracking
Used gr.Progress() with yield for real-time updates:
def analyze_restaurant_interface(url, review_count, progress=gr.Progress()):
# Phase 1: Scraping
progress(0.1, desc="π₯ Scraping reviews...")
yield (..., "π₯ Scraping reviews...", ...)
# Phase 2: Processing
progress(0.3, desc="βοΈ Processing data...")
yield (..., "βοΈ Processing data...", ...)
# Phase 3: Analysis
progress(0.8, desc="π€ Running AI analysis...")
yield (..., "π€ Running AI analysis...", ...)
# Final
progress(1.0, desc="β
Complete!")
yield (..., "β
Complete!", ...)
Step 5: Connect Backend
Imports:
from src.scrapers.opentable_scraper import scrape_opentable
from src.data_processing import process_reviews, clean_reviews_for_ai
from src.agent.base_agent import RestaurantAnalysisAgent
from src.mcp_integrations.query_reviews import query_reviews_direct
Integration:
# Scrape
result = scrape_opentable(url=url, max_reviews=review_count, headless=True)
# Process
df = process_reviews(result)
reviews = clean_reviews_for_ai(df['review_text'].tolist())
# Analyze
agent = RestaurantAnalysisAgent()
analysis = agent.analyze_restaurant(url, restaurant_name, reviews)
# Display
chef_insights = analysis['insights']['chef']
manager_insights = analysis['insights']['manager']
Step 6: Implement Drill-Down Functionality
Dynamic Dropdowns:
# Populate dropdowns after analysis
chef_dropdown_choices = [item['name'] for item in menu_items]
manager_dropdown_choices = [aspect['name'] for aspect in aspects]
# Connect change events
chef_dropdown.change(
fn=get_menu_item_summary,
inputs=chef_dropdown,
outputs=chef_summary
)
Detail Functions:
def get_menu_item_summary(item_name: str) -> str:
# Load menu_analysis.json
# Find selected item
# Return formatted summary with sentiment, mentions, reviews
pass
Step 7: Build Q&A System
Architecture:
- Index reviews after analysis
- Store in memory dictionary (keyed by restaurant name)
- Use keyword search to find relevant reviews
- Send top 50 to Claude for answer
Key Code:
# In query_reviews.py
def find_relevant_reviews(reviews, question, max_reviews=50):
# Extract keywords from question
keywords = [k for k in question.lower().split() if k not in stop_words]
# Score reviews by keyword matches
scored = [(sum(1 for k in keywords if k in r.lower()), r) for r in reviews]
scored.sort(reverse=True)
# Return top matches
return [r for score, r in scored[:max_reviews]]
Context Persistence (Critical!):
# β WRONG - Context lost between interactions
restaurant_context = gr.Textbox(visible=False)
# β
CORRECT - Context persists
restaurant_context = gr.State("")
π Key Components
1. Main Interface (create_interface())
Features:
- Clean, professional design
- Mobile-responsive layout
- Real-time progress updates
- Error handling
Code Structure:
def create_interface():
with gr.Blocks(title="Restaurant Intelligence Agent") as demo:
# Header
gr.Markdown("# π½οΈ Restaurant Intelligence Agent")
# Input Section
with gr.Row():
url_input = gr.Textbox(...)
review_count = gr.Dropdown(...)
analyze_btn = gr.Button(...)
# Progress
progress_box = gr.Textbox(...)
# Hidden state
restaurant_context = gr.State("")
# Results Tabs
with gr.Tabs():
with gr.Tab("π³ Chef Insights"):
...
with gr.Tab("π Manager Insights"):
...
with gr.Tab("π¬ Ask Questions"):
...
# Event handlers
analyze_btn.click(fn=analyze_restaurant_interface, ...)
return demo
2. Analysis Function (analyze_restaurant_interface())
Generator Pattern for Progress:
def analyze_restaurant_interface(url, review_count, progress=gr.Progress()):
try:
# Validate input
if not url or "opentable" not in url.lower():
return error_output
# Phase 1: Scrape
progress(0.1, desc="Scraping...")
yield intermediate_output
result = scrape_opentable(...)
# Phase 2: Process
progress(0.3, desc="Processing...")
yield intermediate_output
reviews = process_reviews(result)
# Phase 3: Analyze
progress(0.5, desc="Analyzing...")
yield intermediate_output
analysis = agent.analyze_restaurant(...)
# Phase 4: Format & Display
progress(1.0, desc="Complete!")
yield final_output
except Exception as e:
yield error_output
3. Insight Formatting (clean_insight_text())
Problem: Claude returns insights in various formats:
- Plain text
- Lists:
["item1", "item2"] - Dicts:
[{"priority": "high", "action": "..."}] - Mixed with quotes and brackets
Solution: Universal text cleaner
def clean_insight_text(text):
if isinstance(text, list):
# Handle list of dicts (recommendations)
if text and isinstance(text[0], dict):
return '\n\n'.join(f"β’ {item['action']}" for item in text)
# Handle simple list
return '\n\n'.join(f"β’ {item}" for item in text)
elif isinstance(text, str):
# Parse string representations
if text.startswith('[{'):
parsed = ast.literal_eval(text)
return format_list(parsed)
if text.startswith('['):
parsed = ast.literal_eval(text)
return '\n\n'.join(f"β’ {item}" for item in parsed)
# Clean quotes
return text.strip('"\'[]')
return str(text)
4. Q&A System (query_reviews.py)
Features:
- Keyword-based relevance scoring
- Searches all indexed reviews
- Returns top 50 most relevant
- Context-aware answers
Key Functions:
# Index reviews after analysis
def index_reviews_direct(restaurant_name, reviews):
REVIEW_INDEX[restaurant_name.lower()] = reviews
return f"Indexed {len(reviews)} reviews"
# Find relevant reviews
def find_relevant_reviews(reviews, question, max_reviews=50):
keywords = extract_keywords(question)
scored = score_by_keywords(reviews, keywords)
return top_n(scored, max_reviews)
# Answer question
def query_reviews_direct(restaurant_name, question):
reviews = REVIEW_INDEX.get(restaurant_name.lower())
relevant = find_relevant_reviews(reviews, question)
return ask_claude(relevant, question)
π Challenges & Solutions
Challenge 1: Gradio 6 Breaking Changes
Problem: theme= parameter in Blocks() causes error
TypeError: BlockContext.__init__() got an unexpected keyword argument 'theme'
Solution: Move theme to .launch()
# Before
with gr.Blocks(theme=gr.themes.Soft()) as demo:
pass
demo.launch()
# After
with gr.Blocks() as demo:
pass
demo.launch(theme=gr.themes.Soft())
Challenge 2: Insights Formatting Issues
Problem: Raw JSON in display
["Strength 1", "Strength 2"]
[{'priority': 'high', 'action': '...'}]
Solution: Created clean_insight_text() function
- Handles lists, dicts, strings
- Extracts 'action' from recommendation dicts
- Converts to bullet points
- Removes brackets/quotes
Challenge 3: Manager Insights Rate Limit
Problem: API rate limit (30K tokens/min) hit when generating insights
Error 429: rate_limit_error
Solution: Added 15s delay between chef and manager insights
# In base_agent.py
chef_insights = generate_insights(role='chef')
time.sleep(15) # Wait to avoid rate limit
manager_insights = generate_insights(role='manager')
Challenge 4: Q&A Context Not Persisting
Problem: Restaurant context arrives as empty string ''
DEBUG: restaurant_context = ''
Solution: Use gr.State() instead of hidden gr.Textbox()
# Before
restaurant_context = gr.Textbox(visible=False)
# After
restaurant_context = gr.State("")
Why: gr.State() is designed for persisting values between interactions, while hidden textboxes can lose state.
Challenge 5: Poor Q&A Quality
Problem: Q&A using only first 10 reviews, missing relevant content
"Reviews don't mention Brussels sprouts" (but they do!)
Solution:
- Increased to 50 reviews
- Added keyword-based filtering
- Improved Claude prompt
Result: Now finds relevant reviews from entire dataset
π§ͺ Testing
Test 1: Basic Functionality (20 reviews)
- β Scraping works
- β Analysis completes
- β Insights display
- β Charts generate
- β Q&A works
Test 2: Rate Limits (100 reviews)
- β Manager insights generate (with 15s delay)
- β No rate limit errors
- β±οΈ Total time: ~5-6 minutes
Test 3: Q&A Quality
- β Keyword search finds relevant reviews
- β Answers cite specific review numbers
- β Handles topics not in reviews gracefully
Test 4: Edge Cases
- β Invalid URL β Clear error message
- β Empty reviews β Fallback message
- β No context β "Analyze restaurant first" message
π Performance Metrics
| Reviews | Scraping | Analysis | Insights | Total | Cost |
|---|---|---|---|---|---|
| 20 | 30s | 1m | 30s | 2m | $0.20 |
| 100 | 2m | 3m | 1m | 6m | $1.20 |
| 500 | 8m | 12m | 2m | 22m* | $5.00* |
*Estimated based on scaling
π¨ UI/UX Design Decisions
1. Three-Tab Layout
Why: Separates concerns by user role
- Chef tab β Food/menu focused
- Manager tab β Operations focused
- Q&A tab β Ad-hoc questions
2. Drill-Down Dropdowns
Why: Reduces cognitive load
- Overview first (charts + summaries)
- Details on demand (select item)
3. Progress Indicators
Why: Long-running operations (5-20 minutes)
- Real-time updates every 30 seconds
- Phase descriptions (Scraping β Processing β Analyzing)
- Prevents user from thinking app is frozen
4. Error Handling
Why: Graceful degradation
- Clear error messages
- Fallback insights if generation fails
- Validation before expensive operations
π Next Steps
Immediate (Day 16)
- Deploy backend to Modal
- Create Modal API endpoints
- Update Gradio to call Modal instead of local functions
Day 17
- Create HuggingFace Space
- Deploy Gradio UI to HF Space
- Connect UI to Modal backend
- Add API key as HF Secret
Day 18-19
- Create demo video (1-5 mins)
- Polish README
- Social media post
- Final testing
- Submit before Nov 30, 11:59 PM UTC
π Code Summary
Files Created/Modified (Day 15)
src/ui/gradio_app.py (NEW - 620 lines)
- Main Gradio interface
- Progress tracking
- Event handlers
- Insight formatting
src/mcp_integrations/query_reviews.py (UPDATED)
- Added keyword-based search
- Increased max_reviews to 50
- Better prompts for Claude
src/agent/base_agent.py (UPDATED)
- Added 15s delay between insights
- Fixed state clearing
src/agent/insights_generator.py (UPDATED)
- Better error handling
- Improved prompts
src/data_processing/review_cleaner.py (CREATED)
- Text sanitization
- Token reduction
π Key Learnings
Gradio 6 Best Practices
- Use
gr.State()for persistence, not hidden textboxes - Move theme to
.launch(), notBlocks() - Use generators with
yieldfor progress updates - Wrap long operations in try-except with user-friendly errors
- Test with
share=Falselocally before deploying
AI Agent Integration
- Add delays between API calls to avoid rate limits
- Handle variable response formats from LLMs
- Provide fallback responses when generation fails
- Log extensively for debugging
- Validate responses before displaying
Q&A System Design
- Simple keyword search often beats complex embeddings for small datasets
- Normalize inputs (lowercase, strip) to avoid mismatches
- Show what's available when context missing
- Cite sources in answers for credibility
- Filter first, then send to LLM to reduce tokens
π References
β Day 15 Completion Checklist
- Install Gradio 6
- Create UI directory structure
- Build basic interface
- Implement progress tracking
- Connect backend (scraper, agent, insights)
- Add drill-down functionality
- Build Q&A system with RAG
- Fix insights formatting
- Fix rate limit issues
- Fix Q&A context persistence
- Improve Q&A quality (keyword search)
- Test with 20 reviews β
- Test with 100 reviews β
- Document implementation β
Status: β
Day 15 Complete!
Next: Day 16 - Modal Backend Deployment
Generated: November 24, 2025
Project: Restaurant Intelligence Agent
Hackathon: Anthropic MCP 1st Birthday - Track 2