Download app.py from chelleboyer/reachy_mini_retail_assistant: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/spaces/chelleboyer/reachy_mini_retail_assistant/resolve/main/app.py
- Command line
-
hf download hf://spaces/chelleboyer/reachy_mini_retail_assistant/app.py
-
curl -L -o app.py https://huggingface.co/spaces/chelleboyer/reachy_mini_retail_assistant/resolve/main/app.py
17.5 kB
| """Gradio Demo: Reachy Mini Retail Assistant | |
| Demonstrates conversational product search with backend telemetry. | |
| HuggingFace Spaces ready version. | |
| """ | |
| import gradio as gr | |
| import os | |
| from datetime import datetime | |
| from pathlib import Path | |
| # Load environment variables from parent .env file (local dev only) | |
| from dotenv import load_dotenv | |
| env_path = Path(__file__).parent.parent / "reachy_edge" / ".env" | |
| if env_path.exists(): | |
| load_dotenv(env_path) | |
| print(f"[DEBUG] Loaded .env from {env_path}") | |
| else: | |
| print(f"[DEBUG] No .env file found at {env_path} - using environment variables") | |
| # Local imports (no sys.path hacks needed) | |
| from cache.l2_cache import ProductCache | |
| from models import Product | |
| # Try to import HuggingFace | |
| try: | |
| from huggingface_hub import InferenceClient | |
| HF_AVAILABLE = True | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| if HF_TOKEN: | |
| # Initialize client without model - specify per request | |
| hf_client = InferenceClient(token=HF_TOKEN) | |
| print(f"[DEBUG] ✅ HuggingFace client configured with token (length: {len(HF_TOKEN)})") | |
| print(f"[DEBUG] Will use model: meta-llama/Llama-3.3-70B-Instruct (free tier)") | |
| else: | |
| HF_AVAILABLE = False | |
| hf_client = None | |
| print(f"[DEBUG] ⚠️ No HF_TOKEN found - will use mock responses") | |
| except ImportError: | |
| HF_AVAILABLE = False | |
| hf_client = None | |
| print("[DEBUG] huggingface-hub not available") | |
| # Initialize cache (regular ProductCache - no thread-safety needed for demo) | |
| cache = ProductCache(db_path="./data/cache.db") | |
| cache.initialize() | |
| # Interaction history for telemetry | |
| interaction_history = [] | |
| # Mock LLM responses (fallback if no OpenAI API key) | |
| RESPONSE_TEMPLATES = { | |
| "diesel": "I can help you with diesel! We have premium diesel fuel available at Fuel Islands 1-4. Would you like to know about pricing or DEF fluid as well?", | |
| "fuel": "We have several fuel options available. Are you looking for diesel, gasoline, or something specific like DEF fluid?", | |
| "shower": "Our shower facilities are available! We offer 30-minute shower credits for $15. They're located at the Service Desk - nice and clean!", | |
| "food": "Hungry? We've got fresh hot food at the counter - pizza, burgers, chicken tenders, and breakfast sandwiches. What sounds good?", | |
| "coffee": "Need some fuel for yourself? We've got fresh truck stop coffee at the coffee station - large cups are just $2.49!", | |
| "cb radio": "Looking for a CB radio? We carry the Cobra 29 LX - it's a solid choice at $129.99. Find it in Aisle 4 (Electronics section).", | |
| "safety": "Safety first! We've got high-visibility vests, LED road flares, flashlights, and emergency kits in Aisle 5. What do you need?", | |
| "deal": "Great question! While I don't have today's specific deals loaded, I can show you our best everyday prices. Popular items include our coffee ($2.49), hot dogs ($2.99), and bulk snacks. Diesel fuel is competitively priced - check our sign out front for today's rate!", | |
| "special": "Great question! While I don't have today's specific deals loaded, I can show you our best everyday prices. Popular items include our coffee ($2.49), hot dogs ($2.99), and bulk snacks. Diesel fuel is competitively priced - check our sign out front for today's rate!", | |
| "cheap": "Looking for value? Our best deals are coffee at $2.49, hot dogs at $2.99, and we have great bulk pricing on snacks and energy drinks. What are you interested in?", | |
| "parking": "We offer overnight parking for $20 - secure and well-lit. Reserve your spot at the Service Desk!", | |
| "wash": "Our full-service truck wash is $125. Gets your rig looking like new! Find us at the Truck Wash Bay.", | |
| "tired": "Need a break? We've got showers ($15), hot coffee ($2.49), and energy drinks to keep you going. Or grab some overnight parking ($20) if you need rest!", | |
| } | |
| def get_mock_response(query: str, results: list = None) -> str: | |
| """Generate conversational response based on query and search results.""" | |
| query_lower = query.lower() | |
| # Check templates first | |
| for keyword, response in RESPONSE_TEMPLATES.items(): | |
| if keyword in query_lower: | |
| return response | |
| # Smart fallback based on results | |
| if results and len(results) > 0: | |
| top_result = results[0] | |
| return f"I found some great options! The top match is **{top_result.name}** for ${top_result.price:.2f} - you'll find it at {top_result.location}. Check out the products below!" | |
| # Last resort | |
| return "I'm searching our inventory for you! Let me show you what we have..." | |
| def get_llm_response(query: str, results: list) -> tuple[str, str]: | |
| """Generate response using HuggingFace. | |
| Returns: (response_text, source_indicator) | |
| """ | |
| if not HF_AVAILABLE or not hf_client: | |
| print(f"[DEBUG] ⚠️ HF unavailable - using mock response instead") | |
| return get_mock_response(query, results), "🔴 MOCK (no API token)" | |
| print(f"[DEBUG] 🚀 CALLING HF INFERENCE API for query: {query}") | |
| print(f"[DEBUG] Model: meta-llama/Llama-3.3-70B-Instruct") | |
| try: | |
| # Build context about products found | |
| if results: | |
| products_context = "I found these items in our truck stop:\n" | |
| for i, p in enumerate(results[:3], 1): | |
| # Check if it's a service or product | |
| is_service = p.category == "Services" | |
| item_type = "service" if is_service else "product" | |
| products_context += f"{i}. {p.name} (${p.price:.2f}) - {item_type} available at {p.location}\n" | |
| else: | |
| products_context = "I searched our inventory but didn't find any matching items." | |
| # Use chat_completion for conversational models | |
| messages = [ | |
| {"role": "user", "content": f"""You are Reachy, a helpful truck stop assistant. | |
| Customer asks: '{query}' | |
| {products_context} | |
| Respond naturally in 2-3 sentences: | |
| - If services found (like showers, parking, truck wash), say "we offer" or "we have" (not "carry") | |
| - If products found (like fuel, snacks, CB radios), mention them by name | |
| - If nothing found, politely say we don't have it and suggest checking with staff | |
| - Be conversational and helpful"""} | |
| ] | |
| # Call HuggingFace chat_completion API with Meta Llama 3.3 70B (free tier, reliable) | |
| response = hf_client.chat_completion( | |
| messages=messages, | |
| model="meta-llama/Llama-3.3-70B-Instruct", | |
| max_tokens=100, | |
| temperature=0.7 | |
| ) | |
| llm_response = response.choices[0].message.content.strip() | |
| print(f"[DEBUG] ✅ LLAMA SUCCESS! Response length: {len(llm_response)} chars") | |
| print(f"[DEBUG] Response preview: {llm_response[:100]}...") | |
| return llm_response, "🟢 Meta Llama 3.3 70B Instruct" | |
| except Exception as e: | |
| print(f"[ERROR] ❌ NVIDIA NEMOTRON API FAILED: {type(e).__name__}: {e}") | |
| print(f"[ERROR] Falling back to mock response") | |
| return get_mock_response(query, results), f"🔴 MOCK (API error: {type(e).__name__})" | |
| def format_product_card(product: Product) -> str: | |
| """Format product as HTML card.""" | |
| # Category emoji mapping | |
| emoji_map = { | |
| "Fuel & Fluids": "⛽", | |
| "Trucker Supplies": "🚛", | |
| "Electronics": "📻", | |
| "Energy & Snacks": "☕", | |
| "Hot Food & Beverages": "🍕", | |
| "Services": "🚿", | |
| "Safety & Lighting": "⚠️", | |
| "Convenience": "🛒" | |
| } | |
| emoji = emoji_map.get(product.category, "📦") | |
| relevance_bar = "" | |
| if product.relevance_score: | |
| score_pct = min(100, product.relevance_score * 10) # Scale for display | |
| relevance_bar = f'<div style="background: #e0e0e0; border-radius: 4px; height: 6px; margin-top: 8px;"><div style="background: #4CAF50; width: {score_pct}%; height: 100%; border-radius: 4px;"></div></div>' | |
| return f""" | |
| <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 8px 0; background: white;"> | |
| <div style="display: flex; align-items: start; gap: 12px;"> | |
| <div style="font-size: 48px;">{emoji}</div> | |
| <div style="flex: 1;"> | |
| <h3 style="margin: 0 0 8px 0; color: #333;">{product.name}</h3> | |
| <p style="margin: 4px 0; color: #666; font-size: 14px;"><strong>SKU:</strong> {product.sku}</p> | |
| <p style="margin: 4px 0; color: #666; font-size: 14px;"><strong>Category:</strong> {product.category}</p> | |
| <p style="margin: 4px 0; color: #666; font-size: 14px;"><strong>Location:</strong> {product.location}</p> | |
| <p style="margin: 8px 0 4px 0; color: #2196F3; font-size: 24px; font-weight: bold;">${product.price:.2f}</p> | |
| {relevance_bar} | |
| {f'<p style="margin: 4px 0; color: #888; font-size: 12px;">Relevance: {product.relevance_score:.2f}</p>' if product.relevance_score else ''} | |
| </div> | |
| </div> | |
| <p style="margin: 12px 0 0 0; color: #666; font-size: 14px; font-style: italic;">{product.description}</p> | |
| </div> | |
| """ | |
| def format_backend_activity(query: str, results: list, response_time_ms: float) -> str: | |
| """Format backend telemetry as HTML.""" | |
| timestamp = datetime.now().strftime("%H:%M:%S") | |
| activity = f""" | |
| <div style="font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px;"> | |
| <div style="color: #4EC9B0; margin-bottom: 12px;">🔍 [{timestamp}] NEW QUERY RECEIVED</div> | |
| <div style="margin-left: 20px; margin-bottom: 12px;"> | |
| <div style="color: #9CDCFE;">query:</div> | |
| <div style="margin-left: 20px; color: #CE9178;">"{query}"</div> | |
| </div> | |
| <div style="color: #4EC9B0; margin-bottom: 8px;">💾 L2 CACHE (SQLite FTS5)</div> | |
| <div style="margin-left: 20px; margin-bottom: 12px;"> | |
| <div style="color: #6A9955;">// Executing full-text search with BM25 ranking...</div> | |
| <div style="color: #C586C0;">cache</div><span style="color: #D4D4D4;">.search_products(</span><span style="color: #CE9178;">"{query}"</span><span style="color: #D4D4D4;">, max_results=5)</span> | |
| <div style="color: #4FC1FF; margin-top: 4px;">✓ Found {len(results)} products in {response_time_ms:.2f}ms</div> | |
| </div> | |
| <div style="color: #4EC9B0; margin-bottom: 8px;">📊 INTERACTION LOGGED</div> | |
| <div style="margin-left: 20px; margin-bottom: 12px;"> | |
| <div style="color: #6A9955;">// Recording customer interaction for analytics...</div> | |
| <div style="color: #D4D4D4;">{{ </div> | |
| <div style="margin-left: 20px;"> | |
| <span style="color: #9CDCFE;">"timestamp"</span><span style="color: #D4D4D4;">: </span><span style="color: #CE9178;">"{timestamp}"</span><span style="color: #D4D4D4;">,</span><br> | |
| <span style="color: #9CDCFE;">"query"</span><span style="color: #D4D4D4;">: </span><span style="color: #CE9178;">"{query}"</span><span style="color: #D4D4D4;">,</span><br> | |
| <span style="color: #9CDCFE;">"results_count"</span><span style="color: #D4D4D4;">: </span><span style="color: #B5CEA8;">{len(results)}</span><span style="color: #D4D4D4;">,</span><br> | |
| <span style="color: #9CDCFE;">"response_time_ms"</span><span style="color: #D4D4D4;">: </span><span style="color: #B5CEA8;">{response_time_ms:.2f}</span> | |
| </div> | |
| <div style="color: #D4D4D4;">}}</div> | |
| <div style="color: #4FC1FF; margin-top: 4px;">✓ Logged to analytics database</div> | |
| </div> | |
| <div style="color: #4EC9B0; margin-bottom: 8px;">🧠 LEARNING INSIGHTS</div> | |
| <div style="margin-left: 20px;"> | |
| <div style="color: #6A9955;">// Updating search patterns for future optimization...</div> | |
| <div style="color: #4FC1FF;">• Customer interest in: {results[0].category if results else 'N/A'}</div> | |
| <div style="color: #4FC1FF;">• Popular query terms indexed</div> | |
| <div style="color: #4FC1FF;">• Response quality will improve over time</div> | |
| </div> | |
| </div> | |
| """ | |
| return activity | |
| def chat(message: str, history: list) -> tuple: | |
| """Process customer query and return response with products and telemetry.""" | |
| import time | |
| start_time = time.time() | |
| # Search products | |
| print(f"[DEBUG] Searching for: {message}") | |
| results = cache.search_products(message, max_results=5) | |
| print(f"[DEBUG] Found {len(results)} results") | |
| if results: | |
| print(f"[DEBUG] First result: {results[0].name}") | |
| # Calculate response time | |
| response_time_ms = (time.time() - start_time) * 1000 | |
| # Generate conversational response using LLM (falls back to mock if no API key) | |
| print(f"\n{'='*60}") | |
| print(f"[QUERY] {message}") | |
| response, source_indicator = get_llm_response(message, results) | |
| print(f"[RESPONSE] {response[:80]}...") | |
| print(f"[SOURCE] {source_indicator}") | |
| print(f"{'='*60}\n") | |
| # Add source indicator at top | |
| source_badge_color = "#4CAF50" if "🟢" in source_indicator else "#FF9800" | |
| source_html = f"<div style='background: {source_badge_color}; color: white; padding: 8px 16px; border-radius: 4px; margin-bottom: 16px; font-weight: bold; display: inline-block;'>{source_indicator}</div>" | |
| # Format products as HTML cards | |
| products_html = source_html + "<div style='margin-top: 16px;'>" | |
| if results: | |
| products_html += "<h3 style='color: #333; margin-bottom: 12px;'>📦 Products Found:</h3>" | |
| for product in results: | |
| products_html += format_product_card(product) | |
| else: | |
| products_html += "<p style='color: #999; font-style: italic;'>No products found matching your query.</p>" | |
| products_html += "</div>" | |
| # Format backend activity | |
| backend_html = format_backend_activity(message, results, response_time_ms) | |
| # Log interaction | |
| interaction_history.append({ | |
| "timestamp": datetime.now().isoformat(), | |
| "query": message, | |
| "results_count": len(results), | |
| "response_time_ms": response_time_ms | |
| }) | |
| # Build conversation history in Gradio 6.0 format (list of messages with role/content) | |
| history = history or [] | |
| history.append({"role": "user", "content": message}) | |
| history.append({"role": "assistant", "content": response}) | |
| return history, products_html, backend_html | |
| def clear_chat(): | |
| """Clear chat history.""" | |
| return [], "", "" | |
| # Build Gradio interface | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(""" | |
| # 🤖 Reachy Mini: Retail Assistant Demo | |
| **Interaction Loop Demonstration:** | |
| - Customer asks questions in natural language | |
| - Reachy searches the product database (SQLite FTS5 + BM25 ranking) | |
| - **Meta Llama 3.3 70B Instruct** generates conversational responses (via HuggingFace API) | |
| - Visual product cards + backend telemetry | |
| - All interactions logged for continuous improvement | |
| *Try asking: "Where can I get diesel?", "I need a CB radio", "Do you have grape jelly?", "What's on sale?"* | |
| """) | |
| with gr.Row(): | |
| # Left column: Chat interface | |
| with gr.Column(scale=2): | |
| chatbot = gr.Chatbot( | |
| label="💬 Reachy Conversation", | |
| height=400, | |
| show_label=True | |
| ) | |
| with gr.Row(): | |
| msg = gr.Textbox( | |
| label="Ask Reachy...", | |
| placeholder="Where can I get diesel fuel?", | |
| show_label=False, | |
| scale=4 | |
| ) | |
| submit = gr.Button("Send", variant="primary", scale=1) | |
| clear = gr.Button("Clear Chat", variant="secondary") | |
| products_display = gr.HTML(label="Products") | |
| # Right column: Backend telemetry | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 🔧 Backend Activity") | |
| backend_display = gr.HTML() | |
| # Event handlers | |
| submit.click( | |
| chat, | |
| inputs=[msg, chatbot], | |
| outputs=[chatbot, products_display, backend_display] | |
| ).then( | |
| lambda: "", | |
| outputs=[msg] | |
| ) | |
| msg.submit( | |
| chat, | |
| inputs=[msg, chatbot], | |
| outputs=[chatbot, products_display, backend_display] | |
| ).then( | |
| lambda: "", | |
| outputs=[msg] | |
| ) | |
| clear.click( | |
| clear_chat, | |
| outputs=[chatbot, products_display, backend_display] | |
| ) | |
| # Example queries | |
| gr.Examples( | |
| examples=[ | |
| ["Where can I get diesel fuel?"], | |
| ["I need a CB radio"], | |
| ["Where are the showers?"], | |
| ["I'm hungry, what do you have?"], | |
| ["Do you sell safety vests?"], | |
| ["Do you have grape jelly?"], | |
| ], | |
| inputs=[msg] | |
| ) | |
| if __name__ == "__main__": | |
| # Load sample products if cache is empty | |
| from data.sample_products import load_sample_data | |
| # Clear database to avoid duplicates on restart | |
| try: | |
| cache._get_connection().execute("DELETE FROM products_fts") | |
| cache._get_connection().commit() | |
| except: | |
| pass | |
| print("Loading sample products...") | |
| load_sample_data(cache) | |
| print("✓ 44 products loaded") | |
| demo.launch() | |