#!/usr/bin/env python3
# SMOLAGENTS 1.19 FIX - Must be imported before anything else
from final_fix import apply_final_fix
from browser_agent_fix import validate_listing_url_for_nyc
# NEW: Import fixed address extraction (prioritizes mapaddress and structured data)
from fixed_address_extraction import apply_fixed_extraction
# Apply all fixes at startup
apply_final_fix()
apply_fixed_extraction()
import gradio as gr
import json
import pandas as pd
import re
from datetime import datetime, timezone
from typing import Dict, List, Any, Optional
from agent_setup import initialize_caseworker_agent
from tools import final_answer
import ast
# Import our new utilities and constants
from utils import log_tool_action, current_timestamp, parse_observation_data
from constants import StageEvent, RiskLevel, Borough, VoucherType
from browser_agent import BrowserAgent
from violation_checker_agent import ViolationCheckerAgent
# Import V0's enhanced email handling
from email_handler import EmailTemplateHandler, enhanced_classify_message, enhanced_handle_email_request
# --- Internationalization Setup ---
i18n_dict = {
"en": {
"app_title": "🏠 NYC Voucher Housing Navigator",
"app_subtitle": "Your personal AI Caseworker for finding voucher-friendly housing with building safety insights.",
"language_selector": "Language / Idioma / 语言 / ভাষা",
"conversation_label": "Conversation with VoucherBot",
"message_label": "Your Message",
"message_placeholder": "Start by telling me your voucher type, required bedrooms, and max rent...",
"preferences_title": "🎛️ Search Preferences",
"strict_mode_label": "Strict Mode (Only show buildings with 0 violations)",
"borough_label": "Preferred Borough",
"max_rent_label": "Maximum Rent",
"listings_label": "Matching Listings",
"status_label": "Status",
"status_ready": "Ready to search...",
"no_listings": "I don't have any listings to show you right now. Please search for apartments first!",
"no_listings_title": "📋 No Current Listings",
"invalid_listing": "I only have {count} listings available. Please ask for a listing between 1 and {count}.",
"invalid_listing_title": "❌ Invalid Listing Number",
"showing_listings": "Showing {count} listings",
"strict_applied": "🔒 Strict mode applied: {count} listings with 0 violations",
"strict_applied_title": "🔒 Filtering Applied",
"results_found": "✅ Found {count} voucher-friendly listings with safety information!",
"results_title": "✅ Results Ready",
"no_safe_listings": "No listings meet your safety criteria. Try disabling strict mode to see all available options.",
"no_safe_title": "⚠️ No Safe Listings",
"search_error": "❌ Search error: {error}",
"search_error_title": "❌ Search Error",
"error_occurred": "I apologize, but I encountered an error: {error}",
"error_title": "❌ Error",
"general_response_title": "💬 General Response",
"conversation_mode": "Conversation mode",
"no_criteria": "No listings meet criteria",
"what_if_analysis": "What-if analysis",
"what_if_error_title": "❌ What-If Error",
"error_what_if": "I encountered an error processing your what-if scenario: {error}",
"error_listings_available": "Error - {count} listings available",
"error_what_if_processing": "Error in what-if processing",
"error_conversation": "Error in conversation",
"col_address": "Address",
"col_price": "Price",
"col_risk_level": "Risk Level",
"col_violations": "Violations",
"col_last_inspection": "Last Inspection",
"col_link": "Link",
"col_summary": "Summary",
"link_not_available": "No link available",
"intro_greeting": """👋 **Hi there! I'm Navi, your personal NYC Housing Navigator!**
I'm here to help you find safe, affordable, and voucher-friendly housing in New York City. I understand that finding the right home can feel overwhelming, but you don't have to do this alone - I'm here to guide you every step of the way! 😊
**Here's how I can help you:**
• 🏠 **Find voucher-friendly apartments** that accept your specific voucher type
• 🏢 **Check building safety** and provide violation reports for peace of mind
• 🚇 **Show nearby subway stations** and transit accessibility
• 🏫 **Find nearby schools** for families with children
• 📧 **Draft professional emails** to landlords and property managers
• 💡 **Answer questions** about voucher programs, neighborhoods, and housing rights
**To get started, just tell me:**
• What type of voucher do you have? (Section 8, CityFHEPS, HASA, etc.)
• How many bedrooms do you need? 🛏️
• What's your maximum rent budget? 💰
• Do you have a preferred borough? 🗽
I'm patient, kind, and here to support you through this journey. Let's find you a wonderful place to call home! ✨🏡"""
},
"es": {
"app_title": "🏠 Navegador de Vivienda con Voucher de NYC",
"app_subtitle": "Tu trabajador social personal de IA para encontrar vivienda que acepta vouchers con información de seguridad del edificio.",
"language_selector": "Idioma / Language / 语言 / ভাষা",
"conversation_label": "Conversación con VoucherBot",
"message_label": "Tu Mensaje",
"message_placeholder": "Comienza diciéndome tu tipo de voucher, habitaciones requeridas y renta máxima...",
"preferences_title": "🎛️ Preferencias de Búsqueda",
"strict_mode_label": "Modo Estricto (Solo mostrar edificios con 0 violaciones)",
"borough_label": "Distrito Preferido",
"max_rent_label": "Renta Máxima",
"listings_label": "Listados Coincidentes",
"status_label": "Estado",
"status_ready": "Listo para buscar...",
"no_listings": "No tengo listados para mostrarte ahora. ¡Por favor busca apartamentos primero!",
"no_listings_title": "📋 Sin Listados Actuales",
"invalid_listing": "Solo tengo {count} listados disponibles. Por favor pide un listado entre 1 y {count}.",
"invalid_listing_title": "❌ Número de Listado Inválido",
"showing_listings": "Mostrando {count} listados",
"strict_applied": "🔒 Modo estricto aplicado: {count} listados con 0 violaciones",
"strict_applied_title": "🔒 Filtro Aplicado",
"results_found": "✅ ¡Encontrado {count} listados que aceptan vouchers con información de seguridad!",
"results_title": "✅ Resultados Listos",
"no_safe_listings": "Ningún listado cumple tus criterios de seguridad. Intenta desactivar el modo estricto para ver todas las opciones disponibles.",
"no_safe_title": "⚠️ Sin Listados Seguros",
"search_error": "❌ Error de búsqueda: {error}",
"search_error_title": "❌ Error de Búsqueda",
"error_occurred": "Me disculpo, pero encontré un error: {error}",
"error_title": "❌ Error",
"general_response_title": "💬 Respuesta General",
"conversation_mode": "Modo conversación",
"no_criteria": "Ningún listado cumple criterios",
"what_if_analysis": "Análisis de qué pasaría si",
"what_if_error_title": "❌ Error de Qué Pasaría Si",
"error_what_if": "Encontré un error procesando tu escenario de qué pasaría si: {error}",
"error_listings_available": "Error - {count} listados disponibles",
"error_what_if_processing": "Error en procesamiento de qué pasaría si",
"error_conversation": "Error en conversación",
"col_address": "Dirección",
"col_price": "Precio",
"col_risk_level": "Nivel de Riesgo",
"col_violations": "Violaciones",
"col_last_inspection": "Última Inspección",
"col_link": "Enlace",
"col_summary": "Resumen",
"link_not_available": "Sin enlace disponible",
"intro_greeting": """👋 **¡Hola! Soy Navi, tu Navegadora Personal de Vivienda de NYC!**
Estoy aquí para ayudarte a encontrar vivienda segura, asequible y que acepta vouchers en la Ciudad de Nueva York. Entiendo que encontrar el hogar perfecto puede sentirse abrumador, pero no tienes que hacerlo solo - ¡estoy aquí para guiarte en cada paso del camino! 😊
**Así es como puedo ayudarte:**
• 🏠 **Encontrar apartamentos que aceptan vouchers** que acepten tu tipo específico de voucher
• 🏢 **Verificar la seguridad del edificio** y proporcionar reportes de violaciones para tu tranquilidad
• 🚇 **Mostrar estaciones de metro cercanas** y accesibilidad de transporte
• 🏫 **Encontrar escuelas cercanas** para familias con niños
• 📧 **Redactar emails profesionales** a propietarios y administradores de propiedades
• 💡 **Responder preguntas** sobre programas de vouchers, vecindarios y derechos de vivienda
**Para comenzar, solo dime:**
• ¿Qué tipo de voucher tienes? (Section 8, CityFHEPS, HASA, etc.)
• ¿Cuántas habitaciones necesitas? 🛏️
• ¿Cuál es tu presupuesto máximo de renta? 💰
• ¿Tienes un distrito preferido? 🗽
Soy paciente, amable y estoy aquí para apoyarte en este viaje. ¡Encontremos un lugar maravilloso al que puedas llamar hogar! ✨🏡"""
},
"zh": {
"app_title": "🏠 纽约市住房券导航器",
"app_subtitle": "您的个人AI社工,帮助您找到接受住房券的房屋,并提供建筑安全信息。",
"language_selector": "语言 / Language / Idioma / ভাষা",
"conversation_label": "与VoucherBot对话",
"message_label": "您的消息",
"message_placeholder": "请先告诉我您的住房券类型、所需卧室数量和最高租金...",
"preferences_title": "🎛️ 搜索偏好",
"strict_mode_label": "严格模式(仅显示0违规的建筑)",
"borough_label": "首选区域",
"max_rent_label": "最高租金",
"listings_label": "匹配房源",
"status_label": "状态",
"status_ready": "准备搜索...",
"no_listings": "我现在没有房源可以显示给您。请先搜索公寓!",
"no_listings_title": "📋 当前无房源",
"invalid_listing": "我只有{count}个可用房源。请询问1到{count}之间的房源。",
"invalid_listing_title": "❌ 无效房源号码",
"showing_listings": "显示{count}个房源",
"strict_applied": "🔒 严格模式已应用:{count}个0违规房源",
"strict_applied_title": "🔒 已应用过滤",
"results_found": "✅ 找到{count}个接受住房券的房源,包含安全信息!",
"results_title": "✅ 结果准备就绪",
"no_safe_listings": "没有房源符合您的安全标准。尝试禁用严格模式以查看所有可用选项。",
"no_safe_title": "⚠️ 无安全房源",
"search_error": "❌ 搜索错误:{error}",
"search_error_title": "❌ 搜索错误",
"error_occurred": "抱歉,我遇到了一个错误:{error}",
"error_title": "❌ 错误",
"general_response_title": "💬 一般回复",
"conversation_mode": "对话模式",
"no_criteria": "没有房源符合条件",
"what_if_analysis": "假设分析",
"what_if_error_title": "❌ 假设错误",
"error_what_if": "处理您的假设场景时遇到错误:{error}",
"error_listings_available": "错误 - {count}个房源可用",
"error_what_if_processing": "假设处理错误",
"error_conversation": "对话错误",
"col_address": "地址",
"col_price": "价格",
"col_risk_level": "风险级别",
"col_violations": "违规",
"col_last_inspection": "最后检查",
"col_link": "链接",
"col_summary": "摘要",
"link_not_available": "无可用链接",
"intro_greeting": """👋 **您好!我是Navi,您的个人纽约市住房导航员!**
我在这里帮助您在纽约市找到安全、经济实惠且接受住房券的住房。我理解找到合适的家可能让人感到不知所措,但您不必独自面对这一切 - 我会在每一步中指导您!😊
**我可以为您提供以下帮助:**
• 🏠 **寻找接受住房券的公寓** - 找到接受您特定类型住房券的房源
• 🏢 **检查建筑安全** - 提供违规报告和安全评估,让您安心
• 🚇 **显示附近的地铁站** - 提供交通便利性和可达性信息
• 🏫 **寻找附近的学校** - 为有孩子的家庭提供学校信息
• 📧 **起草专业邮件** - 帮您给房东和物业管理员写邮件
• 💡 **回答问题** - 关于住房券项目、社区特点和住房权利的各种问题
**开始使用时,请告诉我:**
• 您有什么类型的住房券?(Section 8联邦住房券、CityFHEPS城市住房援助、HASA艾滋病服务券等)
• 您需要多少间卧室?🛏️
• 您的最高租金预算是多少?💰
• 您有首选的行政区吗?(布朗克斯、布鲁克林、曼哈顿、皇后区、史坦顿岛) 🗽
我很有耐心、善良,会在整个找房过程中支持您。让我们一起为您找到一个可以称之为家的美好地方!我了解纽约市的住房市场和各种住房券项目,会帮您找到既安全又符合预算的理想住所。✨🏡"""
},
"bn": {
"app_title": "🏠 NYC ভাউচার হাউজিং নেভিগেটর",
"app_subtitle": "ভাউচার-বান্ধব আবাসন খোঁজার জন্য আপনার ব্যক্তিগত AI কেসওয়ার্কার, বিল্ডিং নিরাপত্তা তথ্যসহ।",
"language_selector": "ভাষা / Language / Idioma / 语言",
"conversation_label": "VoucherBot এর সাথে কথোপকথন",
"message_label": "আপনার বার্তা",
"message_placeholder": "আপনার ভাউচারের ধরন, প্রয়োজনীয় বেডরুম এবং সর্বোচ্চ ভাড়া বলে শুরু করুন...",
"preferences_title": "🎛️ অনুসন্ধান পছন্দ",
"strict_mode_label": "কঠোর মোড (শুধুমাত্র ০ লঙ্ঘনের বিল্ডিং দেখান)",
"borough_label": "পছন্দের বরো",
"max_rent_label": "সর্বোচ্চ ভাড়া",
"listings_label": "মিলে যাওয়া তালিকা",
"status_label": "অবস্থা",
"status_ready": "অনুসন্ধানের জন্য প্রস্তুত...",
"no_listings": "এই মুহূর্তে আপনাকে দেখানোর মতো কোন তালিকা নেই। প্রথমে অ্যাপার্টমেন্ট অনুসন্ধান করুন!",
"no_listings_title": "📋 বর্তমান তালিকা নেই",
"invalid_listing": "আমার কাছে শুধুমাত্র {count}টি তালিকা উপলব্ধ। অনুগ্রহ করে ১ থেকে {count} এর মধ্যে একটি তালিকা চান।",
"invalid_listing_title": "❌ অবৈধ তালিকা নম্বর",
"showing_listings": "{count}টি তালিকা দেখাচ্ছে",
"strict_applied": "🔒 কঠোর মোড প্রয়োগ করা হয়েছে: ০ লঙ্ঘনের {count}টি তালিকা",
"strict_applied_title": "🔒 ফিল্টার প্রয়োগ করা হয়েছে",
"results_found": "✅ নিরাপত্তা তথ্যসহ {count}টি ভাউচার-বান্ধব তালিকা পাওয়া গেছে!",
"results_title": "✅ ফলাফল প্রস্তুত",
"no_safe_listings": "কোন তালিকা আপনার নিরাপত্তা মানদণ্ড পূরণ করে না। সমস্ত উপলব্ধ বিকল্প দেখতে কঠোর মোড নিষ্ক্রিয় করার চেষ্টা করুন।",
"no_safe_title": "⚠️ কোন নিরাপদ তালিকা নেই",
"search_error": "❌ অনুসন্ধান ত্রুটি: {error}",
"search_error_title": "❌ অনুসন্ধান ত্রুটি",
"error_occurred": "আমি দুঃখিত, কিন্তু আমি একটি ত্রুটির সম্মুখীন হয়েছি: {error}",
"error_title": "❌ ত্রুটি",
"general_response_title": "💬 সাধারণ উত্তর",
"conversation_mode": "কথোপকথন মোড",
"no_criteria": "কোন তালিকা মানদণ্ড পূরণ করে না",
"what_if_analysis": "যদি-তাহলে বিশ্লেষণ",
"what_if_error_title": "❌ যদি-তাহলে ত্রুটি",
"error_what_if": "আপনার যদি-তাহলে পরিস্থিতি প্রক্রিয়া করতে আমি ত্রুটির সম্মুখীন হয়েছি: {error}",
"error_listings_available": "ত্রুটি - {count}টি তালিকা উপলব্ধ",
"error_what_if_processing": "যদি-তাহলে প্রক্রিয়াকরণে ত্রুটি",
"error_conversation": "কথোপকথনে ত্রুটি",
"col_address": "ঠিকানা",
"col_price": "দাম",
"col_risk_level": "ঝুঁকির স্তর",
"col_violations": "লঙ্ঘন",
"col_last_inspection": "শেষ পরিদর্শন",
"col_link": "লিংক",
"col_summary": "সারাংশ",
"link_not_available": "কোন লিংক উপলব্ধ নেই",
"intro_greeting": """👋 **নমস্কার! আমি নবি, আপনার ব্যক্তিগত NYC হাউজিং নেভিগেটর!**
আমি এখানে আছি নিউইয়র্ক সিটিতে আপনাকে নিরাপদ, সাশ্রয়ী এবং ভাউচার-বান্ধব আবাসন খুঁজে পেতে সাহায্য করার জন্য। আমি বুঝি যে সঠিক বাড়ি খোঁজা অভিভূতকর মনে হতে পারে, কিন্তু আপনাকে একা এটি করতে হবে না - আমি প্রতিটি পদক্ষেপে আপনাকে গাইড করার জন্য এখানে আছি! 😊
**আমি যেভাবে আপনাকে সাহায্য করতে পারি:**
• 🏠 **ভাউচার-বান্ধব অ্যাপার্টমেন্ট খুঁজুন** যা আপনার নির্দিষ্ট ভাউচার ধরন গ্রহণ করে
• 🏢 **বিল্ডিং নিরাপত্তা পরীক্ষা করুন** এবং মানসিক শান্তির জন্য লঙ্ঘনের রিপোর্ট প্রদান করুন
• 🚇 **নিকটবর্তী সাবওয়ে স্টেশন দেখান** এবং ট্রানজিট অ্যাক্সেসিবলিটি
• 🏫 **নিকটবর্তী স্কুল খুঁজুন** শিশুদের সাথে পরিবারের জন্য
• 📧 **পেশাদার ইমেইল খসড়া করুন** বাড়িওয়ালা এবং সম্পত্তি ব্যবস্থাপকদের কাছে
• 💡 **প্রশ্নের উত্তর দিন** ভাউচার প্রোগ্রাম, পাড়া এবং আবাসন অধিকার সম্পর্কে
**শুরু করতে, শুধু আমাকে বলুন:**
• আপনার কি ধরনের ভাউচার আছে? (Section 8, CityFHEPS, HASA, ইত্যাদি)
• আপনার কতটি বেডরুম প্রয়োজন? 🛏️
• আপনার সর্বোচ্চ ভাড়ার বাজেট কত? 💰
• আপনার কি কোন পছন্দের বরো আছে? 🗽
আমি ধৈর্যশীল, দয়ালু, এবং এই যাত্রায় আপনাকে সমর্থন করার জন্য এখানে আছি। আসুন আপনার জন্য একটি চমৎকার জায়গা খুঁজে পাই যাকে আপনি বাড়ি বলতে পারেন! ✨🏡"""
}
}
# Create the I18n instance with keyword arguments for each language
i18n = gr.I18n(
en=i18n_dict["en"],
es=i18n_dict["es"],
zh=i18n_dict["zh"],
bn=i18n_dict["bn"]
)
# --- Initialize Agents and State Management ---
print("Initializing VoucherBot Agents...")
caseworker_agent = initialize_caseworker_agent()
browser_agent = BrowserAgent()
violation_agent = ViolationCheckerAgent()
print("Agents Initialized. Ready for requests.")
# --- State Management Functions ---
def create_initial_state() -> Dict:
"""Create initial app state."""
return {
"listings": [],
"current_listing": None, # Track the currently discussed listing
"current_listing_index": None, # Track the index of the current listing
"preferences": {
"borough": "",
"max_rent": 4000,
"min_bedrooms": 1,
"voucher_type": "",
"strict_mode": False,
"language": "en" # Add language to preferences
},
"favorites": []
}
def update_app_state(current_state: Dict, updates: Dict) -> Dict:
"""Update app state with new data."""
new_state = current_state.copy()
for key, value in updates.items():
if key == "preferences" and isinstance(value, dict):
new_state["preferences"].update(value)
else:
new_state[key] = value
return new_state
def filter_listings_strict_mode(listings: List[Dict], strict: bool = False) -> List[Dict]:
"""Filter listings based on strict mode (no violations)."""
if not strict:
return listings
return [
listing for listing in listings
if listing.get("building_violations", 0) == 0
]
def create_chat_message_with_metadata(content: str, title: str,
duration: Optional[float] = None,
parent_id: Optional[str] = None) -> Dict:
"""Create a ChatMessage with metadata for better UX."""
metadata = {
"title": title,
"timestamp": current_timestamp()
}
if duration is not None:
metadata["duration"] = duration
if parent_id is not None:
metadata["parent_id"] = parent_id
return {
"role": "assistant",
"content": content,
"metadata": metadata
}
def detect_context_dependent_question(message: str) -> bool:
"""Detect if the message is asking about something in the current context (like 'which lines?')"""
message_lower = message.lower().strip()
# Short questions that likely refer to current context
context_patterns = [
r'^which\s+(lines?|train|subway)', # "which lines", "which line", "which train"
r'^what\s+(lines?|train|subway)', # "what lines", "what line", "what train"
r'^how\s+(far|close|near)', # "how far", "how close", "how near"
r'^(lines?|train|subway)$', # just "lines", "line", "train", "subway"
r'^what\s+about', # "what about..."
r'^tell\s+me\s+about', # "tell me about..."
r'^more\s+(info|details)', # "more info", "more details"
r'^(distance|walk|walking)', # "distance", "walk", "walking"
r'^any\s+other', # "any other..."
r'^is\s+it\s+(near|close|far)', # "is it near", "is it close", "is it far"
# Add patterns for subway and school proximity questions
r'nearest\s+(subway|train|school)', # "nearest subway", "nearest school", "nearest train"
r'closest\s+(subway|train|school)', # "closest subway", "closest school", "closest train"
r'what\'?s\s+the\s+(nearest|closest)\s+(subway|train|school)', # "what's the nearest/closest subway"
r'where\s+is\s+the\s+(nearest|closest)\s+(subway|train|school)', # "where is the nearest/closest subway"
r'how\s+far\s+is\s+the\s+(subway|train|school)', # "how far is the subway"
r'(subway|train|school)\s+(distance|proximity)', # "subway distance", "school proximity"
r'^(subway|train|school)\?$', # just "subway?", "school?"
r'^closest\s+(subway|train|school)\?$', # "closest subway?", "closest school?"
]
# Check if message matches context-dependent patterns
import re
for pattern in context_patterns:
if re.match(pattern, message_lower):
return True
# Also check for very short questions (likely context-dependent)
words = message_lower.split()
if len(words) <= 3 and any(word in ['which', 'what', 'how', 'where', 'lines', 'train', 'subway'] for word in words):
return True
return False
def detect_language_from_message(message: str) -> str:
"""Detect language from user message using simple keyword matching."""
message_lower = message.lower()
# Spanish keywords
spanish_keywords = [
'hola', 'apartamento', 'vivienda', 'casa', 'alquiler', 'renta', 'busco',
'necesito', 'ayuda', 'donde', 'como', 'que', 'soy', 'tengo', 'quiero',
'habitacion', 'habitaciones', 'dormitorio', 'precio', 'costo', 'dinero',
'section', 'cityFHEPS', 'voucher', 'bronx', 'brooklyn', 'manhattan',
'queens', 'gracias', 'por favor', 'dime', 'dame', 'encuentro'
]
# Chinese keywords (simplified)
chinese_keywords = [
'你好', '公寓', '住房', '房屋', '租金', '寻找', '需要', '帮助', '在哪里',
'怎么', '什么', '我', '有', '要', '房间', '卧室', '价格', '钱',
'住房券', '布朗克斯', '布鲁克林', '曼哈顿', '皇后区', '谢谢', '请',
'告诉', '给我', '找到'
]
# Bengali keywords
bengali_keywords = [
'নমস্কার', 'অ্যাপার্টমেন্ট', 'বাড়ি', 'ভাড়া', 'খুঁজছি', 'প্রয়োজন',
'সাহায্য', 'কোথায়', 'কিভাবে', 'কি', 'আমি', 'আছে', 'চাই',
'রুম', 'বেডরুম', 'দাম', 'টাকা', 'ভাউচার', 'ব্রঙ্কস', 'ব্রুকলিন',
'ম্যানহাটান', 'কুইন্স', 'ধন্যবাদ', 'দয়া করে', 'বলুন', 'দিন', 'খুঁজে'
]
# Count matches for each language
spanish_count = sum(1 for keyword in spanish_keywords if keyword in message_lower)
chinese_count = sum(1 for keyword in chinese_keywords if keyword in message)
bengali_count = sum(1 for keyword in bengali_keywords if keyword in message)
# Return language with highest count (minimum 2 matches required)
if spanish_count >= 2:
return "es"
elif chinese_count >= 2:
return "zh"
elif bengali_count >= 2:
return "bn"
else:
return "en" # Default to English
# Define the theme using Origin
theme = gr.themes.Origin(
primary_hue="indigo",
secondary_hue="indigo",
neutral_hue="teal",
)
# --- Gradio UI Definition ---
with gr.Blocks(theme=theme) as demo:
gr.Markdown(f"# {i18n('app_title')}")
gr.Markdown(i18n("app_subtitle"))
# Initialize app state
app_state = gr.State(create_initial_state())
# Controls at the top: Language selector and Dark/Light mode toggle
with gr.Row():
language_dropdown = gr.Dropdown(
label=i18n("language_selector"),
choices=[("English", "en"), ("Español", "es"), ("中文", "zh"), ("বাংলা", "bn")],
value="en",
allow_custom_value=False,
scale=2
)
dark_mode_toggle = gr.Checkbox(
label="🌙 Dark Mode",
value=False,
scale=1
)
# Create initial greeting message for Navi
def create_initial_greeting(language="en"):
greeting_message = {
"role": "assistant",
"content": i18n_dict[language]["intro_greeting"]
}
return [greeting_message]
# Chat Section (Full Width) - Initialize with greeting
chatbot = gr.Chatbot(
label=i18n("conversation_label"),
height=600,
type="messages",
value=create_initial_greeting() # Add initial greeting
)
msg = gr.Textbox(
label=i18n("message_label"),
placeholder=i18n("message_placeholder")
)
# Preferences and Status Row (Compact)
with gr.Row():
with gr.Column(scale=2):
with gr.Group():
gr.Markdown(f"### {i18n('preferences_title')}")
strict_mode_toggle = gr.Checkbox(
label=i18n("strict_mode_label"),
value=False
)
with gr.Column(scale=3):
progress_info = gr.Textbox(
label=i18n("status_label"),
value=i18n("status_ready"),
interactive=False,
visible=True
)
# Results Display (Full Width)
results_df = gr.DataFrame(
value=pd.DataFrame(),
label=i18n("listings_label"),
interactive=False,
row_count=(10, "dynamic"),
wrap=True,
visible=False,
datatype=["number", "str", "str", "str", "number", "str", "str", "str"] # #, Address, Price, Risk, Violations, Inspection, Link, Summary
)
# Using V0's enhanced classification - now imported from email_handler.py
def handle_listing_question(message: str, history: list, state: Dict):
"""Handle questions about existing listings."""
listings = state.get("listings", [])
if not listings:
no_listings_msg = create_chat_message_with_metadata(
"I don't have any listings to show you yet. Please search for apartments first!",
"📋 No Listings Available"
)
history.append(no_listings_msg)
return (history, gr.update(), gr.update(value="No search criteria set"), state)
message_lower = message.lower()
# Parse which listing they're asking about
listing_index = None
if "first" in message_lower or "1st" in message_lower or "#1" in message_lower:
listing_index = 0
elif "second" in message_lower or "2nd" in message_lower or "#2" in message_lower:
listing_index = 1
elif "third" in message_lower or "3rd" in message_lower or "#3" in message_lower:
listing_index = 2
elif "last" in message_lower:
listing_index = len(listings) - 1
else:
# Try to extract number
numbers = re.findall(r'\d+', message_lower)
if numbers:
try:
listing_index = int(numbers[0]) - 1 # Convert to 0-based index
except:
pass
# Default to first listing if no specific index found
if listing_index is None:
listing_index = 0
# Validate index
if listing_index < 0 or listing_index >= len(listings):
invalid_msg = create_chat_message_with_metadata(
f"I only have {len(listings)} listings available. Please ask about a listing number between 1 and {len(listings)}.",
"❌ Invalid Listing Number"
)
history.append(invalid_msg)
# Preserve the current DataFrame
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), state)
# Get the requested listing
listing = listings[listing_index]
listing_num = listing_index + 1
# Create detailed response
address = listing.get("address") or listing.get("title", "N/A")
price = listing.get("price", "N/A")
url = listing.get("url", "No link available")
risk_level = listing.get("risk_level", "❓")
violations = listing.get("building_violations", 0)
response_text = f"""
**Listing #{listing_num} Details:**
🏠 **Address:** {address}
💰 **Price:** {price}
{risk_level} **Safety Level:** {violations} violations
🔗 **Link:** {url}
You can copy and paste this link into your browser to view the full listing with photos and contact information!
**Would you like to know more about this listing? I can help you with:**
1. 🚇 See the nearest subway/transit options
2. 🏫 See nearby schools
3. 📧 Draft an email to inquire about this listing
4. 🏠 View another listing
Just let me know what information you'd like to see!
""".strip()
listing_response_msg = create_chat_message_with_metadata(
response_text,
f"🏠 Listing #{listing_num} Details"
)
history.append(listing_response_msg)
# Update state to track current listing context
updated_state = update_app_state(state, {
"current_listing": listing,
"current_listing_index": listing_index
})
# Preserve the current DataFrame
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), updated_state)
def handle_chat_message(message: str, history: list, current_state: Dict,
strict_mode: bool):
"""Enhanced chat handler with new agent workflow and state management."""
# CRITICAL DEBUG: Log everything at the entry point
print(f"🚨 CHAT HANDLER CALLED:")
print(f" Message: '{message}'")
print(f" Strict mode: {strict_mode}")
log_tool_action("GradioApp", "user_message_received", {
"message": message,
"timestamp": current_timestamp()
})
# Detect language from user message
detected_language = detect_language_from_message(message)
current_language = current_state.get("preferences", {}).get("language", "en")
# Check if language has changed based on user input
language_changed = False
if detected_language != current_language and detected_language != "en":
# Language changed - update state and greeting
current_language = detected_language
language_changed = True
print(f"🌍 Language detected: {detected_language}")
# Add user message to history
history.append({"role": "user", "content": message})
# Update preferences in state (including detected language)
new_state = update_app_state(current_state, {
"preferences": {
"strict_mode": strict_mode,
"language": current_language
}
})
# If language changed, update the greeting message
if language_changed and len(history) > 1: # Don't replace if this is the first user message
# Find and replace the greeting (first assistant message)
for i, msg in enumerate(history):
if msg["role"] == "assistant" and "I'm Navi" in msg["content"] or "Soy Navi" in msg["content"] or "我是Navi" in msg["content"] or "আমি নবি" in msg["content"]:
# Replace with new language greeting
new_greeting = create_initial_greeting(current_language)
history[i] = new_greeting[0]
break
try:
# Use V0's enhanced classification
message_type = enhanced_classify_message(message, new_state)
if message_type == "email_request":
# Call V0's enhanced email handler
enhanced_result = enhanced_handle_email_request(message, history, new_state)
# Return with state preservation
return (enhanced_result[0], enhanced_result[1],
gr.update(value="Email template generated"), new_state)
elif message_type == "what_if_scenario":
print(f"🔄 CALLING handle_what_if_scenario")
return handle_what_if_scenario(message, history, new_state, strict_mode)
elif message_type == "new_search":
print(f"🏠 CALLING handle_housing_search")
return handle_housing_search(message, history, new_state, strict_mode)
elif message_type == "listing_question":
print(f"📋 CALLING handle_listing_question")
return handle_listing_question(message, history, new_state)
else:
print(f"💬 CALLING handle_general_conversation")
# Handle general conversation with caseworker agent
return handle_general_conversation(message, history, new_state)
except Exception as e:
log_tool_action("GradioApp", "error", {
"error": str(e),
"message": message
})
error_msg = create_chat_message_with_metadata(
f"I apologize, but I encountered an error: {str(e)}",
"❌ Error"
)
history.append(error_msg)
return (history, gr.update(value=pd.DataFrame(), visible=False),
gr.update(value="Error occurred"), new_state)
def handle_housing_search(message: str, history: list, state: Dict,
strict_mode: bool):
"""Handle housing search requests with the new agent workflow."""
search_id = f"search_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"
# Extract borough from message if mentioned
message_lower = message.lower()
detected_borough = None
borough_map = {
"bronx": "bronx",
"brooklyn": "brooklyn",
"manhattan": "manhattan",
"queens": "queens",
"staten island": "staten_island"
}
for borough_name, borough_code in borough_map.items():
if borough_name in message_lower:
detected_borough = borough_code
break
# Use detected borough from message
if detected_borough:
target_borough = detected_borough
print(f"🎯 Using detected borough from message: {detected_borough}")
else:
target_borough = None
print(f"🌍 No borough specified - will search all boroughs")
# Debug logging to see what's happening
log_tool_action("GradioApp", "borough_detection", {
"message": message,
"detected_borough": detected_borough,
"final_target_borough": target_borough
})
# Update search message based on target
if target_borough:
search_text = f"🔍 Searching for voucher-friendly listings in {target_borough.title()}..."
print(f"🎯 BOROUGH FILTER ACTIVE: Searching only {target_borough.upper()}")
else:
search_text = "🔍 Searching for voucher-friendly listings across NYC..."
print(f"🌍 NO BOROUGH FILTER: Searching all NYC boroughs")
search_msg = create_chat_message_with_metadata(
search_text,
"🔍 Searching Listings",
parent_id=search_id
)
history.append(search_msg)
try:
# Use BrowserAgent to search for listings
log_tool_action("GradioApp", "browser_search_started", {
"borough": target_borough,
"detected_from_message": detected_borough,
"message": message
})
search_query = "Section 8"
# Debug: Log exactly what we're passing to browser agent
boroughs_param = target_borough if target_borough else ""
print(f"📡 Calling browser_agent.forward with boroughs='{boroughs_param}'")
log_tool_action("GradioApp", "browser_agent_call", {
"query": search_query,
"boroughs_param": boroughs_param,
"target_borough": target_borough,
"detected_borough": detected_borough
})
browser_result = browser_agent.forward(
query=search_query,
boroughs=boroughs_param
)
browser_data = json.loads(browser_result)
if browser_data.get("status") != "success":
error_msg = create_chat_message_with_metadata(
f"❌ Search failed: {browser_data.get('error', 'Unknown error')}",
"❌ Search Failed"
)
history.append(error_msg)
return (history, gr.update(), gr.update(value="Search failed"), state)
listings = browser_data["data"]["listings"]
search_duration = browser_data["data"]["metadata"]["duration"]
# Update search completion message
search_complete_msg = create_chat_message_with_metadata(
f"✅ Found {len(listings)} potential listings",
"🔍 Search Complete",
duration=search_duration,
parent_id=search_id
)
history.append(search_complete_msg)
if not listings:
no_results_msg = create_chat_message_with_metadata(
"I couldn't find any voucher-friendly listings matching your criteria. Try adjusting your search parameters.",
"📋 No Results"
)
history.append(no_results_msg)
return (history, gr.update(), gr.update(value="No listings found"), state)
# Stage 2: Checking Violations
violation_msg = create_chat_message_with_metadata(
f"🏢 Checking building safety for {len(listings)} listings...",
"🏢 Checking Violations",
parent_id=search_id
)
history.append(violation_msg)
# Enrich listings with violation data
enriched_listings = []
for i, listing in enumerate(listings):
address = listing.get("address") or listing.get("title", "")
if not address:
continue
violation_result = violation_agent.forward(address)
violation_data = json.loads(violation_result)
if violation_data.get("status") == "success":
enriched_listing = {
**listing,
"building_violations": violation_data["data"]["violations"],
"risk_level": violation_data["data"]["risk_level"],
"last_inspection": violation_data["data"]["last_inspection"],
"violation_summary": violation_data["data"]["summary"]
}
else:
# Add default violation data if check failed
enriched_listing = {
**listing,
"building_violations": 0,
"risk_level": RiskLevel.UNKNOWN.value,
"last_inspection": "N/A",
"violation_summary": "Could not verify"
}
enriched_listings.append(enriched_listing)
# Stage 3: Apply strict mode filtering
if strict_mode:
filtered_listings = filter_listings_strict_mode(enriched_listings, strict=True)
filter_msg = create_chat_message_with_metadata(
f"✅ Applied strict mode filter - {len(filtered_listings)} safe listings found",
"✅ Strict Mode Applied"
)
history.append(filter_msg)
else:
filtered_listings = enriched_listings
# Update state with listings and clear current listing context (new search)
updated_state = update_app_state(state, {
"listings": filtered_listings,
"current_listing": None,
"current_listing_index": None
})
# Create DataFrame for display
if filtered_listings:
df = create_listings_dataframe(filtered_listings)
results_msg = create_chat_message_with_metadata(
f"🎉 Found {len(filtered_listings)} voucher-friendly listings for you!",
"✅ Search Results"
)
history.append(results_msg)
return (history, gr.update(value=df, visible=True),
gr.update(value=f"Showing {len(filtered_listings)} listings"),
updated_state)
else:
no_safe_msg = create_chat_message_with_metadata(
"No safe listings found with current criteria. Try adjusting your filters.",
"📋 No Safe Listings"
)
history.append(no_safe_msg)
return (history, gr.update(visible=False),
gr.update(value="No listings match criteria"),
updated_state)
except Exception as e:
error_msg = create_chat_message_with_metadata(
f"Search failed with error: {str(e)}",
"❌ Search Error"
)
history.append(error_msg)
return (history, gr.update(), gr.update(value="Search error occurred"), state)
def handle_what_if_scenario(message: str, history: list, state: Dict, strict_mode: bool):
"""Handle what-if scenarios where users want to modify search parameters"""
try:
from what_if_handler import process_what_if_scenario
# Process the what-if scenario
updated_history, updated_state = process_what_if_scenario(message, history, state)
# If changes were applied, execute a new search with the modified parameters
if "last_what_if_changes" in updated_state:
new_prefs = updated_state["preferences"]
target_borough = new_prefs.get("borough", "")
# Create a search message that includes the borough for detection
search_message = f"Search with modified parameters: {updated_state['last_what_if_changes']}"
if target_borough:
search_message += f" in {target_borough}"
# Execute search with modified parameters
return handle_housing_search(
search_message,
updated_history,
updated_state,
strict_mode
)
# If no changes were made, just return the updated history
listings = updated_state.get("listings", [])
if listings:
current_df = create_listings_dataframe(listings)
return (updated_history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), updated_state)
else:
return (updated_history, gr.update(), gr.update(value="What-if analysis complete"), updated_state)
except Exception as e:
log_tool_action("GradioApp", "what_if_error", {
"error": str(e),
"message": message
})
error_msg = create_chat_message_with_metadata(
f"What-if scenario error: {str(e)}",
"❌ What-if Error"
)
history.append(error_msg)
# Preserve existing state
listings = state.get("listings", [])
if listings:
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Error occurred - {len(listings)} listings available"), state)
else:
return (history, gr.update(), gr.update(value="Error processing what-if scenario"), state)
def handle_listing_follow_up(message: str, history: list, state: Dict):
"""Handle specific follow-up actions for the current listing using enriched data."""
current_listing = state.get("current_listing")
current_listing_index = state.get("current_listing_index")
if not current_listing:
# No current listing context - pass to general conversation
return None
message_lower = message.lower().strip()
listing_num = (current_listing_index or 0) + 1
address = current_listing.get("address") or current_listing.get("title", "N/A")
# Check for subway/transit request
subway_patterns = [
r'subway', r'transit', r'train', r'nearest.*subway', r'closest.*subway',
r'see.*subway', r'show.*subway', r'subway.*options', r'transit.*options'
]
# Check for school request
school_patterns = [
r'school', r'nearest.*school', r'closest.*school', r'see.*school',
r'show.*school', r'school.*nearby', r'nearby.*school'
]
# Check for another listing request
another_listing_patterns = [
r'another.*listing', r'different.*listing', r'next.*listing', r'other.*listing',
r'view.*another', r'see.*another', r'show.*another', r'view.*different'
]
import re
# Handle subway/transit request
if any(re.search(pattern, message_lower) for pattern in subway_patterns):
return handle_subway_info_request(current_listing, listing_num, history, state)
# Handle school request
elif any(re.search(pattern, message_lower) for pattern in school_patterns):
return handle_school_info_request(current_listing, listing_num, history, state)
# Handle another listing request
elif any(re.search(pattern, message_lower) for pattern in another_listing_patterns):
return handle_another_listing_request(history, state)
# If no specific follow-up detected, return None to pass to general conversation
return None
def handle_subway_info_request(listing: Dict, listing_num: int, history: list, state: Dict):
"""Handle subway/transit information request for current listing."""
address = listing.get("address") or listing.get("title", "N/A")
# Check if we have enriched subway data
subway_access = listing.get("subway_access")
if subway_access and subway_access.get("nearest_station"):
station_name = subway_access.get("nearest_station", "Unknown")
lines = subway_access.get("subway_lines", "N/A")
distance = subway_access.get("distance_miles", 0)
is_accessible = subway_access.get("is_accessible", False)
entrance_type = subway_access.get("entrance_type", "Unknown")
accessibility_text = "♿ Wheelchair accessible" if is_accessible else f"⚠️ Not wheelchair accessible ({entrance_type} entrance)"
walking_time = round(distance * 20) if distance else "N/A" # 20 minutes per mile at 3 mph
response_text = f"""
🚇 **Nearest Subway Information for Listing #{listing_num}:**
**Station:** {station_name}
**Lines:** {lines}
**Distance:** {distance:.2f} miles (about {walking_time} minute walk)
**Accessibility:** {accessibility_text}
Would you like to:
1. 🏫 See nearby schools for this listing?
2. 📧 Draft an email to inquire about this listing?
3. 🏠 View another listing?
""".strip()
else:
# No enriched data available - provide helpful message
response_text = f"""
🚇 **Subway Information for Listing #{listing_num}:**
I don't have detailed subway information for this specific listing yet. However, I can help you find this information!
**Address:** {address}
You can:
- Check the MTA website or app for nearby stations
- Use Google Maps to find transit options
- Ask me to search for subway information using the address
Would you like to:
1. 🏫 See nearby schools for this listing?
2. 📧 Draft an email to inquire about this listing?
3. 🏠 View another listing?
""".strip()
subway_msg = create_chat_message_with_metadata(
response_text,
f"🚇 Subway Info - Listing #{listing_num}"
)
history.append(subway_msg)
# Preserve existing DataFrame
listings = state.get("listings", [])
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), state)
def handle_school_info_request(listing: Dict, listing_num: int, history: list, state: Dict):
"""Handle school information request for current listing."""
address = listing.get("address") or listing.get("title", "N/A")
# Check if we have enriched school data
school_access = listing.get("school_access")
if school_access and school_access.get("nearby_schools"):
schools = school_access.get("nearby_schools", [])
if schools:
response_text = f"🏫 **Nearby Schools for Listing #{listing_num}:**\n\n"
for i, school in enumerate(schools[:3], 1): # Show top 3 schools
name = school.get("school_name", "Unknown School")
school_type = school.get("school_type", "Unknown")
grades = school.get("grades", "N/A")
distance = school.get("distance_miles", 0)
walking_time = school.get("walking_time_minutes", "N/A")
school_address = school.get("address", "N/A")
response_text += f"""
{i}. **{name}**
- Type: {school_type}
- Grades: {grades}
- Distance: {distance:.2f} miles ({walking_time} minute walk)
- Address: {school_address}
"""
response_text += f"""
Would you like to:
1. 🚇 See the nearest subway/transit options?
2. 📧 Draft an email to inquire about this listing?
3. 🏠 View another listing?
""".strip()
else:
response_text = f"""
🏫 **Schools Information for Listing #{listing_num}:**
No school data is currently available for this listing.
**Address:** {address}
You can research schools in the area using:
- NYC School Finder website
- GreatSchools.org
- Local Department of Education resources
Would you like to:
1. 🚇 See the nearest subway/transit options?
2. 📧 Draft an email to inquire about this listing?
3. 🏠 View another listing?
""".strip()
else:
# No enriched data available
response_text = f"""
🏫 **Schools Information for Listing #{listing_num}:**
I don't have detailed school information for this specific listing yet.
**Address:** {address}
You can research schools in the area using:
- NYC School Finder website
- GreatSchools.org
- Local Department of Education resources
Would you like to:
1. 🚇 See the nearest subway/transit options?
2. 📧 Draft an email to inquire about this listing?
3. 🏠 View another listing?
""".strip()
school_msg = create_chat_message_with_metadata(
response_text,
f"🏫 School Info - Listing #{listing_num}"
)
history.append(school_msg)
# Preserve existing DataFrame
listings = state.get("listings", [])
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), state)
def handle_another_listing_request(history: list, state: Dict):
"""Handle request to view another listing."""
listings = state.get("listings", [])
current_listing_index = state.get("current_listing_index", 0)
if not listings:
no_listings_msg = create_chat_message_with_metadata(
"I don't have any other listings to show you. Please search for apartments first!",
"📋 No Listings Available"
)
history.append(no_listings_msg)
return (history, gr.update(), gr.update(value="No listings available"), state)
if len(listings) == 1:
only_one_msg = create_chat_message_with_metadata(
"I only have one listing available right now. Try searching for more apartments to see additional options!",
"📋 Only One Listing"
)
history.append(only_one_msg)
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), state)
# Show next listing (cycle through)
next_index = (current_listing_index + 1) % len(listings)
next_listing = listings[next_index]
next_listing_num = next_index + 1
# Create response for next listing
address = next_listing.get("address") or next_listing.get("title", "N/A")
price = next_listing.get("price", "N/A")
url = next_listing.get("url", "No link available")
risk_level = next_listing.get("risk_level", "❓")
violations = next_listing.get("building_violations", 0)
response_text = f"""
**Listing #{next_listing_num} Details:**
🏠 **Address:** {address}
💰 **Price:** {price}
{risk_level} **Safety Level:** {violations} violations
🔗 **Link:** {url}
You can copy and paste this link into your browser to view the full listing with photos and contact information!
**Would you like to know more about this listing? I can help you with:**
1. 🚇 See the nearest subway/transit options
2. 🏫 See nearby schools
3. 📧 Draft an email to inquire about this listing
4. 🏠 View another listing
Just let me know what information you'd like to see!
""".strip()
next_listing_msg = create_chat_message_with_metadata(
response_text,
f"🏠 Listing #{next_listing_num} Details"
)
history.append(next_listing_msg)
# Update state to track new current listing
updated_state = update_app_state(state, {
"current_listing": next_listing,
"current_listing_index": next_index
})
# Preserve existing DataFrame
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), updated_state)
def handle_general_conversation(message: str, history: list, state: Dict):
"""Handle general conversation using the caseworker agent with listing context."""
try:
# First check if this is a specific follow-up action we can handle directly
follow_up_result = handle_listing_follow_up(message, history, state)
if follow_up_result:
return follow_up_result
# Get the current language from state
current_language = state.get("preferences", {}).get("language", "en")
# Check if this is a context-dependent question and we have a current listing
is_context_dependent = detect_context_dependent_question(message)
current_listing = state.get("current_listing")
current_listing_index = state.get("current_listing_index")
# Enhance the message with context if needed
enhanced_message = message
if is_context_dependent and current_listing:
listing_num = (current_listing_index or 0) + 1
address = current_listing.get("address") or current_listing.get("title", "N/A")
# Add context to the message for the agent
enhanced_message = f"""
User is asking about Listing #{listing_num}: {address}
Current listing details:
- Address: {address}
- Price: {current_listing.get("price", "N/A")}
- Violations: {current_listing.get("building_violations", 0)}
- Risk Level: {current_listing.get("risk_level", "❓")}
User's question: {message}
Please answer their question specifically about this listing. If they're asking about subway lines or transit, use the geocoding and subway tools to get specific information about this address.
""".strip()
# Add language context to the message
language_context = f"""
IMPORTANT: The user's preferred language is '{current_language}'. Please respond in this language:
- en = English
- es = Spanish
- zh = Chinese (Simplified)
- bn = Bengali
User message: {enhanced_message}
""".strip()
agent_output = caseworker_agent.run(language_context, reset=False)
response_text = str(agent_output)
general_msg = create_chat_message_with_metadata(
response_text,
"💬 General Response"
)
history.append(general_msg)
# Preserve existing DataFrame if we have listings
listings = state.get("listings", [])
if listings:
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Showing {len(listings)} listings"), state)
else:
return (history, gr.update(), gr.update(value="Conversation mode"), state)
except Exception as e:
error_msg = create_chat_message_with_metadata(
f"I apologize, but I encountered an error: {str(e)}",
"❌ Error"
)
history.append(error_msg)
# Preserve existing DataFrame even on error
listings = state.get("listings", [])
if listings:
current_df = create_listings_dataframe(listings)
return (history, gr.update(value=current_df, visible=True),
gr.update(value=f"Error occurred - {len(listings)} listings still available"), state)
else:
return (history, gr.update(), gr.update(value="Error in conversation"), state)
def create_listings_dataframe(listings: List[Dict]) -> pd.DataFrame:
"""Create a formatted DataFrame from listings data."""
df_data = []
for i, listing in enumerate(listings, 1): # Start enumeration at 1
# Get the address from either 'address' or 'title' field
address = listing.get("address") or listing.get("title", "N/A")
# Get the URL for the listing
url = listing.get("url", "No link available")
df_data.append({
"#": i, # Add the listing number
"Address": address,
"Price": listing.get("price", "N/A"),
"Risk Level": listing.get("risk_level", "❓"),
"Violations": listing.get("building_violations", 0),
"Last Inspection": listing.get("last_inspection", "N/A"),
"Link": url,
"Summary": listing.get("violation_summary", "")[:50] + "..." if len(listing.get("violation_summary", "")) > 50 else listing.get("violation_summary", "")
})
return pd.DataFrame(df_data)
# Wire up the submit action with state management
msg.submit(
handle_chat_message,
[msg, chatbot, app_state, strict_mode_toggle],
[chatbot, results_df, progress_info, app_state]
)
# Add a secondary submit to clear the input box for better UX
msg.submit(lambda: "", [], [msg])
# Language change handler
def change_language(language, current_state, current_history):
"""Handle language change with greeting update."""
# Update the language in state
new_state = update_app_state(current_state, {
"preferences": {"language": language}
})
# Create new greeting in the selected language
new_greeting = create_initial_greeting(language)
# Replace the first message (greeting) if it exists, otherwise add it
if current_history and len(current_history) > 0 and current_history[0]["role"] == "assistant":
updated_history = [new_greeting[0]] + current_history[1:]
else:
updated_history = new_greeting + current_history
return updated_history, new_state
# Update preferences when controls change
def update_preferences(strict, current_state):
"""Update preferences in state when UI controls change."""
return update_app_state(current_state, {
"preferences": {
"strict_mode": strict
}
})
strict_mode_toggle.change(
update_preferences,
[strict_mode_toggle, app_state],
[app_state]
)
# Language change event
language_dropdown.change(
change_language,
[language_dropdown, app_state, chatbot],
[chatbot, app_state]
)
# Dark mode toggle functionality
def toggle_dark_mode(is_dark_mode):
"""Toggle between dark and light mode"""
if is_dark_mode:
return gr.HTML("""
""")
else:
return gr.HTML("""
""")
# Hidden HTML component for dark mode script injection
dark_mode_script = gr.HTML(visible=False)
dark_mode_toggle.change(
toggle_dark_mode,
[dark_mode_toggle],
[dark_mode_script]
)
if __name__ == "__main__":
demo.launch(i18n=i18n)