"""Gradio UI for Tuxedo Link cat adoption application.""" import os import gradio as gr import pandas as pd from dotenv import load_dotenv from typing import List, Optional, Tuple import logging import re from datetime import datetime # Import models - these are lightweight from models.cats import CatProfile, CatMatch, AdoptionAlert from utils.config import is_production # Load environment load_dotenv() # Initialize framework based on mode framework = None profile_agent = None if not is_production(): # LOCAL MODE: Import and initialize heavy components from cat_adoption_framework import TuxedoLinkFramework from agents.profile_agent import ProfileAgent framework = TuxedoLinkFramework() profile_agent = ProfileAgent() print("โ Running in LOCAL mode - using local components") else: # PRODUCTION MODE: Don't import heavy components - use Modal API print("โ Running in PRODUCTION mode - using Modal API") # Global state for current search results current_matches: List[CatMatch] = [] current_profile: Optional[CatProfile] = None # Configure logging to suppress verbose output logging.getLogger().setLevel(logging.WARNING) def extract_profile_from_text(user_input: str, use_cache: bool = False) -> tuple: """ Extract structured profile from user's natural language input. Args: user_input: User's description of desired cat use_cache: Whether to use cached data for search Returns: Tuple of (chat_history, results_html, profile_json) """ global current_matches, current_profile try: # Handle empty input - use placeholder text if not user_input or user_input.strip() == "": user_input = "I'm looking for a friendly, playful kitten in NYC that's good with children" # Extract profile using LLM # Using messages format for Gradio chatbot chat_history = [ {"role": "user", "content": user_input}, {"role": "assistant", "content": "๐ Analyzing your preferences..."} ] # Extract profile (Modal or local) if is_production(): # PRODUCTION: Call Modal API import modal # Look up deployed function - correct API! extract_profile_func = modal.Function.from_name("tuxedo-link-api", "extract_profile") print("[INFO] Calling Modal API to extract profile...") profile_result = extract_profile_func.remote(user_input) if not profile_result["success"]: return chat_history, "
โ Error extracting profile
", "{}" profile = CatProfile(**profile_result["profile"]) else: # LOCAL: Use local agent conversation = [{"role": "user", "content": user_input}] profile = profile_agent.extract_profile(conversation) current_profile = profile # Perform search response_msg = f"โ Got it! Searching for:\n\n" + \ f"๐ Location: {profile.user_location or 'Not specified'}\n" + \ f"๐ Distance: {profile.max_distance or 100} miles\n" + \ f"๐จ Colors: {', '.join(profile.color_preferences) if profile.color_preferences else 'Any'}\n" + \ f"๐ญ Personality: {profile.personality_description or 'Any'}\n" + \ f"๐ Age: {', '.join(profile.age_range) if profile.age_range else 'Any'}\n" + \ f"๐ถ Good with children: {'Yes' if profile.good_with_children else 'Not required'}\n" + \ f"๐ Good with dogs: {'Yes' if profile.good_with_dogs else 'Not required'}\n" + \ f"๐ฑ Good with cats: {'Yes' if profile.good_with_cats else 'Not required'}\n\n" + \ f"Searching..." chat_history[1]["content"] = response_msg # Run search (Modal or local) if is_production(): # PRODUCTION: Call Modal API import modal # Look up deployed function search_cats_func = modal.Function.from_name("tuxedo-link-api", "search_cats") print("[INFO] Calling Modal API to search cats...") search_result = search_cats_func.remote(profile.model_dump(), use_cache=use_cache) if not search_result["success"]: error_msg = search_result.get('error', 'Unknown error') chat_history.append({"role": "assistant", "content": f"โ Search error: {error_msg}"}) return chat_history, "๐ฟ Search failed. Please try again.
", profile.json() # Reconstruct matches from Modal response from models.cats import Cat current_matches = [ CatMatch( cat=Cat(**m["cat"]), match_score=m["match_score"], vector_similarity=m["vector_similarity"], attribute_match_score=m["attribute_match_score"], explanation=m["explanation"], matching_attributes=m.get("matching_attributes", []), missing_attributes=m.get("missing_attributes", []) ) for m in search_result["matches"] ] else: # LOCAL: Use local framework result = framework.search(profile, use_cache=use_cache) current_matches = result.matches # Build results HTML if current_matches: chat_history[1]["content"] += f"\n\nโจ Found {len(current_matches)} great matches!" results_html = build_results_grid(current_matches) else: chat_history[1]["content"] += "\n\n๐ฟ No matches found. Try broadening your search criteria." results_html = "No matches found
" # Profile JSON for display profile_json = profile.model_dump_json(indent=2) return chat_history, results_html, profile_json except Exception as e: error_msg = f"โ Error: {str(e)}" print(f"[ERROR] Search failed: {e}") import traceback traceback.print_exc() return [ {"role": "user", "content": user_input}, {"role": "assistant", "content": error_msg} ], "Error occurred
", "{}" def build_results_grid(matches: List[CatMatch]) -> str: """Build HTML grid of cat results.""" html = "Enter your preferences above to start searching
") # Profile display (collapsible) with gr.Accordion("๐ Extracted Profile (for debugging)", open=False): profile_display = gr.JSON(label="Profile Data") # Wire up events submit_btn.click( fn=extract_profile_from_text, inputs=[user_input, use_cache_checkbox], outputs=[chatbot, results_html, profile_display] ) user_input.submit( fn=extract_profile_from_text, inputs=[user_input, use_cache_checkbox], outputs=[chatbot, results_html, profile_display] ) clear_btn.click( fn=lambda: ([], "Enter your preferences above to start searching
", ""), outputs=[chatbot, results_html, profile_display] ) # Example buttons examples = [ "I want a friendly family cat in zip code 10001, good with children and dogs", "Looking for a playful young kitten near New York City", "I need a calm, affectionate adult cat that likes to cuddle", "Show me cats good with children in the NYC area" ] for btn, example in zip(example_btns, examples): btn.click( fn=search_with_examples, inputs=[gr.State(example), use_cache_checkbox], outputs=[chatbot, results_html, profile_display] ) def build_alerts_tab() -> None: """Build the alerts management tab for scheduling email notifications.""" with gr.Column(): gr.Markdown("# ๐ Manage Alerts") gr.Markdown("Save your search and get notified when new matching cats are available!") # Instructions gr.Markdown(""" ### How it works: 1. **Search** for cats using your preferred criteria in the Search tab 2. **Enter your email** below and choose notification frequency 3. **Save Alert** to start receiving notifications You'll be notified when new cats matching your preferences become available! """) # Save Alert Section gr.Markdown("### ๐พ Save Current Search as Alert") with gr.Row(): with gr.Column(scale=2): email_input = gr.Textbox( label="Email Address", placeholder="your@email.com", info="Where should we send notifications?" ) with gr.Column(scale=1): frequency_dropdown = gr.Dropdown( label="Notification Frequency", choices=["Immediately", "Daily", "Weekly"], value="Daily", info="How often to check for new matches" ) with gr.Row(): save_btn = gr.Button("๐พ Save Alert", variant="primary", scale=2) profile_display = gr.JSON( label="Current Search Profile", value={}, visible=False, scale=1 ) save_status = gr.Markdown("") gr.Markdown("---") # Manage Alerts Section gr.Markdown("### ๐ Your Saved Alerts") with gr.Row(): with gr.Column(scale=2): email_filter_input = gr.Textbox( label="Filter by Email (optional)", placeholder="your@email.com" ) with gr.Column(scale=1): refresh_btn = gr.Button("๐ Refresh", size="sm") alerts_table = gr.Dataframe( value=[], # Start empty - load on demand to avoid blocking UI startup headers=["ID", "Email", "Frequency", "Location", "Preferences", "Last Sent", "Status"], datatype=["number", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True ) # Alert Actions gr.Markdown("### โ๏ธ Manage Alert") with gr.Row(): alert_id_input = gr.Textbox( label="Alert ID", placeholder="Enter Alert ID from table above", scale=2 ) with gr.Column(scale=3): with gr.Row(): toggle_btn = gr.Button("๐ Toggle Active/Inactive", size="sm") delete_btn = gr.Button("๐๏ธ Delete Alert", variant="stop", size="sm") action_status = gr.Markdown("") # Wire up events save_btn.click( fn=save_alert, inputs=[email_input, frequency_dropdown, profile_display], outputs=[save_status, alerts_table] ) refresh_btn.click( fn=load_alerts, inputs=[email_filter_input], outputs=[alerts_table] ) email_filter_input.submit( fn=load_alerts, inputs=[email_filter_input], outputs=[alerts_table] ) toggle_btn.click( fn=toggle_alert_status, inputs=[alert_id_input, email_filter_input], outputs=[action_status, alerts_table] ) delete_btn.click( fn=delete_alert, inputs=[alert_id_input, email_filter_input], outputs=[action_status, alerts_table] ) def build_about_tab() -> None: """Build the about tab with Kyra's story and application info.""" with gr.Column(): gr.Markdown("# ๐ฉ About Tuxedo Link") gr.Markdown(""" ## In Loving Memory of Kyra ๐ฑ This application is dedicated to **Kyra**, a beloved companion who brought joy, comfort, and unconditional love to our lives. Kyra was more than just a catโ he was family, a friend, and a constant source of happiness. ### The Inspiration Kyra Link was created to help others find their perfect feline companion, just as Kyra found his way into our hearts. Every cat deserves a loving home, and every person deserves the companionship of a wonderful cat like Kyra. ### The Technology This application uses AI and machine learning to match prospective adopters with their ideal cat: - **Natural Language Processing**: Understand your preferences in plain English - **Semantic Search**: Find cats based on personality, not just keywords - **Multi-Source Aggregation**: Search across multiple adoption platforms - **Smart Deduplication**: Remove duplicate listings using AI - **Image Recognition**: Match cats visually using computer vision - **Hybrid Matching**: Combine semantic understanding with structured filters ### Features โ **Multi-Platform Search**: Petfinder, RescueGroups โ **AI-Powered Matching**: Semantic search with vector embeddings โ **Smart Deduplication**: Name, description, and image similarity โ **Personality Matching**: Find cats that match your lifestyle โ **Location-Based**: Search near you with customizable radius ### Technical Stack - **Frontend**: Gradio - **Backend**: Python with Modal serverless - **LLMs**: OpenAI GPT-4 for profile extraction - **Vector DB**: ChromaDB with SentenceTransformers - **Image AI**: CLIP for visual similarity - **APIs**: Petfinder, RescueGroups, SendGrid - **Database**: SQLite for caching and user management ### Open Source Tuxedo Link is open source and built as part of the Andela LLM Engineering bootcamp. Contributions and improvements are welcome! ### Acknowledgments - **Petfinder**: For their comprehensive pet adoption API - **RescueGroups**: For connecting rescues with adopters - **Andela**: For the LLM Engineering bootcamp - **Kyra**: For inspiring this project and bringing so much joy ๐ --- *"In memory of Kyra, who taught us that home is wherever your cat is."* ๐พ **May every cat find their perfect home** ๐พ """) # Add Kyra's picture with gr.Row(): with gr.Column(): gr.Image( value="assets/Kyra.png", label="Kyra - Forever in our hearts ๐", show_label=True, container=True, width=400, height=400, show_download_button=False, show_share_button=False, interactive=False ) def create_app() -> gr.Blocks: """ Create and configure the Gradio application. Returns: Configured Gradio Blocks application """ with gr.Blocks( title="Tuxedo Link - Find Your Perfect Cat", theme=gr.themes.Soft() ) as app: gr.Markdown("""AI-Powered Cat Adoption Search