Spaces:
Sleeping
Sleeping
| from final_fix import apply_final_fix | |
| from browser_agent_fix import validate_listing_url_for_nyc | |
| from fixed_address_extraction import apply_fixed_extraction | |
| 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 | |
| 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 | |
| from email_handler import EmailTemplateHandler, enhanced_classify_message, enhanced_handle_email_request | |
| from shortlist_utils import ( | |
| add_to_shortlist, remove_from_shortlist, get_shortlist, | |
| is_shortlisted, get_shortlist_summary, get_shortlisted_ids | |
| ) | |
| 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", | |
| "col_shortlist": "Shortlist", | |
| "link_not_available": "No link available", | |
| "shortlist_save": "➕", | |
| "shortlist_saved": "✅", | |
| "shortlist_empty": "Your shortlist is empty. Save some listings to get started!", | |
| "shortlist_title": "Your Shortlist", | |
| "shortlist_added": "Added to shortlist", | |
| "shortlist_removed": "Removed from shortlist", | |
| "shortlist_cleared": "Shortlist cleared", | |
| "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! 😊 | |
| **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? 🗽""" | |
| }, | |
| "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", | |
| "col_shortlist": "Lista Favorita", | |
| "link_not_available": "Sin enlace disponible", | |
| "shortlist_save": "➕", | |
| "shortlist_saved": "✅", | |
| "shortlist_empty": "Tu lista favorita está vacía. ¡Guarda algunos listados para comenzar!", | |
| "shortlist_title": "Tu Lista Favorita", | |
| "shortlist_added": "Agregado a lista favorita", | |
| "shortlist_removed": "Removido de lista favorita", | |
| "shortlist_cleared": "Lista favorita limpiada", | |
| "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": "摘要", | |
| "col_shortlist": "收藏清单", | |
| "link_not_available": "无可用链接", | |
| "shortlist_save": "➕", | |
| "shortlist_saved": "✅", | |
| "shortlist_empty": "您的收藏清单为空。保存一些房源开始吧!", | |
| "shortlist_title": "您的收藏清单", | |
| "shortlist_added": "已添加到收藏清单", | |
| "shortlist_removed": "已从收藏清单移除", | |
| "shortlist_cleared": "收藏清单已清空", | |
| "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": "সারাংশ", | |
| "col_shortlist": "পছন্দের তালিকা", | |
| "link_not_available": "কোন লিংক উপলব্ধ নেই", | |
| "shortlist_save": "➕", | |
| "shortlist_saved": "✅", | |
| "shortlist_empty": "আপনার পছন্দের তালিকা খালি। শুরু করতে কিছু তালিকা সংরক্ষণ করুন!", | |
| "shortlist_title": "আপনার পছন্দের তালিকা", | |
| "shortlist_added": "পছন্দের তালিকায় যোগ করা হয়েছে", | |
| "shortlist_removed": "পছন্দের তালিকা থেকে সরানো হয়েছে", | |
| "shortlist_cleared": "পছন্দের তালিকা পরিষ্কার করা হয়েছে", | |
| "intro_greeting": """👋 **নমস্কার! আমি নবি, আপনার ব্যক্তিগত NYC হাউজিং নেভিগেটর!** | |
| আমি এখানে আছি নিউইয়র্ক সিটিতে আপনাকে নিরাপদ, সাশ্রয়ী এবং ভাউচার-বান্ধব আবাসন খুঁজে পেতে সাহায্য করার জন্য। আমি বুঝি যে সঠিক বাড়ি খোঁজা অভিভূতকর মনে হতে পারে, কিন্তু আপনাকে একা এটি করতে হবে না - আমি প্রতিটি পদক্ষেপে আপনাকে গাইড করার জন্য এখানে আছি! 😊 | |
| **আমি যেভাবে আপনাকে সাহায্য করতে পারি:** | |
| • 🏠 **ভাউচার-বান্ধব অ্যাপার্টমেন্ট খুঁজুন** যা আপনার নির্দিষ্ট ভাউচার ধরন গ্রহণ করে | |
| • 🏢 **বিল্ডিং নিরাপত্তা পরীক্ষা করুন** এবং মানসিক শান্তির জন্য লঙ্ঘনের রিপোর্ট প্রদান করুন | |
| • 🚇 **নিকটবর্তী সাবওয়ে স্টেশন দেখান** এবং ট্রানজিট অ্যাক্সেসিবলিটি | |
| • 🏫 **নিকটবর্তী স্কুল খুঁজুন** শিশুদের সাথে পরিবারের জন্য | |
| • 📧 **পেশাদার ইমেইল খসড়া করুন** বাড়িওয়ালা এবং সম্পত্তি ব্যবস্থাপকদের কাছে | |
| • 💡 **প্রশ্নের উত্তর দিন** ভাউচার প্রোগ্রাম, পাড়া এবং আবাসন অধিকার সম্পর্কে | |
| **শুরু করতে, শুধু আমাকে বলুন:** | |
| • আপনার কি ধরনের ভাউচার আছে? (Section 8, CityFHEPS, HASA, ইত্যাদি) | |
| • আপনার কতটি বেডরুম প্রয়োজন? 🛏️ | |
| • আপনার সর্বোচ্চ ভাড়ার বাজেট কত? 💰 | |
| • আপনার কি কোন পছন্দের বরো আছে? 🗽 | |
| আমি ধৈর্যশীল, দয়ালু, এবং এই যাত্রায় আপনাকে সমর্থন করার জন্য এখানে আছি। আসুন আপনার জন্য একটি চমৎকার জায়গা খুঁজে পাই যাকে আপনি বাড়ি বলতে পারেন! ✨🏡""" | |
| } | |
| } | |
| i18n = gr.I18n( | |
| en=i18n_dict["en"], | |
| es=i18n_dict["es"], | |
| zh=i18n_dict["zh"], | |
| bn=i18n_dict["bn"] | |
| ) | |
| print("Initializing VoucherBot Agents...") | |
| caseworker_agent = initialize_caseworker_agent() | |
| browser_agent = BrowserAgent() | |
| violation_agent = ViolationCheckerAgent() | |
| print("Agents Initialized. Ready for requests.") | |
| 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 | |
| }, | |
| "shortlist": [] # Changed from favorites to shortlist | |
| } | |
| 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 | |
| theme = gr.themes.Origin( | |
| primary_hue="indigo", | |
| secondary_hue="indigo", | |
| neutral_hue="teal", | |
| ) | |
| with gr.Blocks(theme=theme, css=""" | |
| /* Material Design-inspired styles - Two-Column Layout */ | |
| body, .gr-root { | |
| font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; | |
| color: #222; | |
| background: #f5f5f7; | |
| } | |
| /* Header spanning both columns */ | |
| .app-header { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| padding: 1.5rem; | |
| background: linear-gradient(135deg, #00695c 0%, #004d40 100%); | |
| border-radius: 12px; | |
| color: white; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.15); | |
| } | |
| .app-title { | |
| font-size: 2.5rem; | |
| margin-bottom: 0.5rem; | |
| font-weight: 700; | |
| color: white; | |
| letter-spacing: 0.5px; | |
| text-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| .app-subtitle { | |
| font-size: 1.2rem; | |
| color: rgba(255,255,255,0.9); | |
| margin-bottom: 0; | |
| font-weight: 400; | |
| } | |
| /* Header controls */ | |
| .header-controls { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .header-controls button { | |
| background: rgba(255,255,255,0.2); | |
| border: 1px solid rgba(255,255,255,0.3); | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 6px; | |
| font-size: 0.9rem; | |
| } | |
| .header-controls button:hover { | |
| background: rgba(255,255,255,0.3); | |
| } | |
| /* Two-column layout */ | |
| .main-layout { | |
| display: flex; | |
| gap: 2rem; | |
| min-height: 70vh; | |
| } | |
| .chat-column { | |
| flex: 1; | |
| max-width: 50%; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .info-column { | |
| flex: 1; | |
| max-width: 50%; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Onboarding/Help Section */ | |
| .onboarding-box { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| border-left: 4px solid #00695c; | |
| } | |
| .onboarding-title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: #00695c; | |
| margin-bottom: 0.5rem; | |
| } | |
| .onboarding-text { | |
| color: #666; | |
| line-height: 1.5; | |
| margin-bottom: 1rem; | |
| } | |
| /* Suggested Prompts */ | |
| .suggested-prompts { | |
| margin-bottom: 1rem; | |
| } | |
| .prompt-chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .prompt-chip { | |
| background: #e8eaf6; | |
| color: #6200ea; | |
| border: 1px solid #6200ea; | |
| border-radius: 20px; | |
| padding: 0.5rem 1rem; | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .prompt-chip:hover { | |
| background: #6200ea; | |
| color: white; | |
| transform: translateY(-1px); | |
| box-shadow: 0 2px 8px rgba(98,0,234,0.2); | |
| } | |
| /* Chat area styling */ | |
| .gr-chatbot { | |
| flex: 1; | |
| margin-bottom: 1rem; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| position: relative; | |
| } | |
| /* Simple fix for green blocks - just target the specific elements causing issues */ | |
| .gr-chatbot .prose::marker, | |
| .gr-chatbot .prose li::marker, | |
| .gr-chatbot .prose ul::marker, | |
| .gr-chatbot .prose ol::marker { | |
| color: transparent !important; | |
| content: '' !important; | |
| } | |
| .gr-chatbot .prose li::before, | |
| .gr-chatbot .prose ul li::before, | |
| .gr-chatbot .prose ol li::before { | |
| background: transparent !important; | |
| color: transparent !important; | |
| content: '' !important; | |
| display: none !important; | |
| } | |
| /* Make trash/delete button smaller and positioned correctly */ | |
| .gr-chatbot button[aria-label*="Delete"], | |
| .gr-chatbot button[aria-label*="Clear"], | |
| .gr-chatbot .gr-button[title*="Delete"], | |
| .gr-chatbot .gr-button[title*="Clear"] { | |
| width: 28px !important; | |
| height: 28px !important; | |
| min-width: 28px !important; | |
| min-height: 28px !important; | |
| padding: 4px !important; | |
| font-size: 0.75rem !important; | |
| position: absolute !important; | |
| top: 8px !important; | |
| right: 8px !important; | |
| z-index: 10 !important; | |
| border-radius: 50% !important; | |
| background: rgba(0,105,92,0.8) !important; | |
| } | |
| .gr-chatbot button[aria-label*="Delete"]:hover, | |
| .gr-chatbot button[aria-label*="Clear"]:hover, | |
| .gr-chatbot .gr-button[title*="Delete"]:hover, | |
| .gr-chatbot .gr-button[title*="Clear"]:hover { | |
| background: rgba(0,77,64,0.9) !important; | |
| transform: scale(1.05) !important; | |
| } | |
| /* Input area */ | |
| .chat-input-area { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| margin-bottom: 1rem; | |
| } | |
| /* Toggles section */ | |
| .toggles-section { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| } | |
| .toggle-title { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 0.5rem; | |
| } | |
| /* Right column - Info panel */ | |
| .results-header { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| text-align: center; | |
| font-weight: 600; | |
| color: #00695c; | |
| } | |
| .results-dataframe { | |
| flex: 1; | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| margin-bottom: 1rem; | |
| } | |
| .status-panel { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: 0 4px 16px rgba(0,105,92,0.08); | |
| } | |
| /* Buttons - Enhanced Material Design */ | |
| button, .gr-button { | |
| background: #00695c; | |
| color: #fff; | |
| border-radius: 6px; | |
| box-shadow: 0 4px 12px rgba(0,105,92,0.15); | |
| font-weight: 600; | |
| font-size: 1rem; | |
| padding: 0.75em 1.5em; | |
| min-height: 44px; | |
| position: relative; | |
| overflow: hidden; | |
| transition: all 0.2s; | |
| border: none; | |
| } | |
| button:hover, .gr-button:hover { | |
| background: #004d40; | |
| box-shadow: 0 6px 20px rgba(0,105,92,0.2); | |
| transform: translateY(-1px); | |
| } | |
| button:active, .gr-button:active { | |
| transform: translateY(0); | |
| } | |
| /* Inputs - Enhanced styling */ | |
| input, textarea, .gr-textbox input, .gr-textbox textarea { | |
| border: 2px solid #e0e0e0; | |
| border-radius: 8px; | |
| padding: 12px 16px; | |
| font-size: 1rem; | |
| background: #fff; | |
| transition: all 0.2s; | |
| } | |
| input:focus, textarea:focus, .gr-textbox input:focus, .gr-textbox textarea:focus { | |
| border-color: #00695c; | |
| box-shadow: 0 0 0 3px rgba(0,105,92,0.1); | |
| outline: none; | |
| } | |
| /* DataFrame styling */ | |
| .gr-dataframe { | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); | |
| } | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .main-layout { | |
| flex-direction: column; | |
| } | |
| .chat-column, .info-column { | |
| max-width: 100%; | |
| } | |
| .header-controls { | |
| position: relative; | |
| margin-top: 1rem; | |
| } | |
| .prompt-chips { | |
| flex-direction: column; | |
| } | |
| } | |
| /* Dark mode button - Compact styling */ | |
| .dark-mode-btn { | |
| width: 36px !important; | |
| height: 36px !important; | |
| min-width: 36px !important; | |
| min-height: 36px !important; | |
| padding: 6px !important; | |
| font-size: 1rem !important; | |
| border-radius: 50% !important; | |
| background: rgba(0,105,92,0.1) !important; | |
| border: 1px solid rgba(0,105,92,0.3) !important; | |
| color: #00695c !important; | |
| box-shadow: 0 2px 6px rgba(0,105,92,0.1) !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .dark-mode-btn:hover { | |
| background: rgba(0,105,92,0.2) !important; | |
| transform: scale(1.05) !important; | |
| box-shadow: 0 3px 8px rgba(0,105,92,0.2) !important; | |
| } | |
| /* Dark mode adaptations */ | |
| .dark { | |
| background-color: #111827 !important; | |
| } | |
| .dark .app-title { color: #f9fafb !important; } | |
| .dark .app-subtitle { color: #d1d5db !important; } | |
| .dark .gradio-container { background-color: #1f2937 !important; } | |
| .dark .onboarding-box, .dark .chat-input-area, .dark .toggles-section, | |
| .dark .results-header, .dark .results-dataframe, .dark .status-panel { | |
| background: #374151 !important; | |
| color: #f3f4f6 !important; | |
| } | |
| .dark .dark-mode-btn { | |
| background: rgba(255,255,255,0.1) !important; | |
| border: 1px solid rgba(255,255,255,0.2) !important; | |
| color: #f3f4f6 !important; | |
| } | |
| .dark .dark-mode-btn:hover { | |
| background: rgba(255,255,255,0.2) !important; | |
| } | |
| """) as demo: | |
| # Header Section | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML(""" | |
| <div class="app-header"> | |
| <h1 class="app-title">🏠 NYC Voucher Housing Navigator</h1> | |
| <p class="app-subtitle">Find safe, voucher-friendly housing in NYC with AI assistance</p> | |
| </div> | |
| """) | |
| # Header controls row | |
| with gr.Row(): | |
| with gr.Column(scale=8): | |
| pass # Spacer | |
| with gr.Column(scale=1): | |
| dark_mode_toggle = gr.Button("🌙", size="sm", elem_classes=["dark-mode-btn"]) | |
| # Initialize app state | |
| app_state = gr.State(create_initial_state()) | |
| # 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] | |
| # Main two-column layout | |
| with gr.Row(elem_classes=["main-layout"]): | |
| # LEFT COLUMN: Chat Panel | |
| with gr.Column(elem_classes=["chat-column"]): | |
| # Language dropdown above chat | |
| language_dropdown = gr.Dropdown( | |
| label="Language / Idioma / 语言 / ভাষা", | |
| choices=[("English", "en"), ("Español", "es"), ("中文", "zh"), ("বাংলা", "bn")], | |
| value="en", | |
| container=True | |
| ) | |
| # Chat Section | |
| chatbot = gr.Chatbot( | |
| label="💬 Conversation", | |
| height=400, | |
| type="messages", | |
| value=create_initial_greeting(), | |
| elem_classes=["gr-chatbot"] | |
| ) | |
| # Chat Input Area | |
| with gr.Column(elem_classes=["chat-input-area"]): | |
| msg = gr.Textbox( | |
| label="Your Message", | |
| placeholder="Type your request, like '2 bedroom in Queens under $2500'...", | |
| lines=2, | |
| container=False | |
| ) | |
| send_btn = gr.Button("Send Message", variant="primary") | |
| # Placeholder state for strict mode (UI removed) | |
| strict_mode_toggle = gr.State(False) | |
| # RIGHT COLUMN: Aggregated Information Panel | |
| with gr.Column(elem_classes=["info-column"]): | |
| # Results Header/Status | |
| progress_info = gr.HTML( | |
| value='<div class="results-header">🏠 Ready to search for listings...</div>', | |
| elem_classes=["results-header"] | |
| ) | |
| # DataFrame Section | |
| with gr.Column(elem_classes=["results-dataframe"]): | |
| results_df = gr.DataFrame( | |
| value=pd.DataFrame(), | |
| label="📋 Found Listings", | |
| interactive=True, # Make interactive for shortlist functionality | |
| row_count=(10, "dynamic"), | |
| wrap=True, | |
| visible=False, | |
| datatype=["number", "str", "str", "str", "number", "str", "str", "str", "str"] # Added shortlist column | |
| ) | |
| # Shortlist Panel | |
| with gr.Column(elem_classes=["status-panel"]): | |
| def create_initial_shortlist_display(): | |
| return """ | |
| <div style="text-align: center; color: #666;"> | |
| <h4>📌 Your Shortlist (0 saved)</h4> | |
| <p>Click ➕ in the listings table to save properties to your shortlist.<br/> | |
| Use chat commands like "show my shortlist" to manage saved listings.</p> | |
| <hr style="margin: 1rem 0; border: 1px solid #eee;"> | |
| <div style="color: #999; font-style: italic;">No saved listings yet</div> | |
| </div> | |
| """ | |
| shortlist_display = gr.HTML( | |
| value=create_initial_shortlist_display(), | |
| elem_id="shortlist-display" | |
| ) | |
| # Add all the handler functions before wiring up events | |
| def update_shortlist_display(state: Dict) -> str: | |
| """Create HTML for the shortlist display panel.""" | |
| shortlist = get_shortlist(state) | |
| count = len(shortlist) | |
| if count == 0: | |
| return """ | |
| <div style="text-align: center; color: #666;"> | |
| <h4>📌 Your Shortlist (0 saved)</h4> | |
| <p>Click ➕ in the listings table to save properties to your shortlist.<br/> | |
| Use chat commands like "show my shortlist" to manage saved listings.</p> | |
| <hr style="margin: 1rem 0; border: 1px solid #eee;"> | |
| <div style="color: #999; font-style: italic;">No saved listings yet</div> | |
| </div> | |
| """ | |
| # Create HTML for shortlist items | |
| items_html = "" | |
| for i, item in enumerate(shortlist[:5], 1): # Show top 5 | |
| priority_badge = "" | |
| if item.get("priority"): | |
| priority_badge = f'<span style="background: #ff9800; color: white; padding: 2px 6px; border-radius: 10px; font-size: 0.8em;">⭐ {item["priority"]}</span>' | |
| items_html += f""" | |
| <div style="margin: 0.5rem 0; padding: 0.5rem; background: #f9f9f9; border-radius: 6px; text-align: left;"> | |
| <div style="font-weight: 600; font-size: 0.9em;">{item['address'][:40]}{'...' if len(item['address']) > 40 else ''}</div> | |
| <div style="color: #666; font-size: 0.8em;">{item['price']} • {item['risk_level']}</div> | |
| {priority_badge} | |
| </div> | |
| """ | |
| if count > 5: | |
| items_html += f'<div style="color: #999; font-style: italic; text-align: center;">... and {count - 5} more</div>' | |
| return f""" | |
| <div style="color: #666;"> | |
| <h4 style="text-align: center;">📌 Your Shortlist ({count} saved)</h4> | |
| <p style="text-align: center; font-size: 0.9em;">Click ➕/✅ in the table or use chat commands</p> | |
| <hr style="margin: 1rem 0; border: 1px solid #eee;"> | |
| {items_html} | |
| </div> | |
| """ | |
| def handle_shortlist_click(evt: gr.SelectData, state: Dict): | |
| """Handle shortlist button clicks in the DataFrame.""" | |
| try: | |
| # Only handle clicks on the Shortlist column (last column) | |
| if evt.index[1] != 8: # Shortlist column is index 8 (0-based) | |
| return gr.update(), gr.update(), gr.update(), state | |
| listings = state.get("listings", []) | |
| if not listings or evt.index[0] >= len(listings): | |
| return gr.update(), gr.update(), gr.update(), state | |
| listing = listings[evt.index[0]] | |
| # Toggle shortlist status | |
| if is_shortlisted(listing, state): | |
| # Remove from shortlist | |
| listing_id = str(listing.get("id", listing.get("address", ""))) | |
| updated_state, message = remove_from_shortlist(listing_id, state) | |
| else: | |
| # Add to shortlist | |
| updated_state, message = add_to_shortlist(listing, state) | |
| # Update DataFrame display | |
| df = create_listings_dataframe(listings, updated_state) | |
| # Update progress info with shortlist count | |
| shortlist_count = len(updated_state.get('shortlist', [])) | |
| status_text = f"Showing {len(listings)} listings ({shortlist_count} in shortlist)" | |
| # Update shortlist display | |
| shortlist_html = update_shortlist_display(updated_state) | |
| return gr.update(value=df), gr.update(value=status_text), gr.update(value=shortlist_html), updated_state | |
| except Exception as e: | |
| print(f"Error in handle_shortlist_click: {e}") | |
| return gr.update(), gr.update(), gr.update(), state | |
| def handle_shortlist_command(message: str, history: list, state: Dict): | |
| """Handle shortlist-related chat commands.""" | |
| message_lower = message.lower() | |
| listings = state.get("listings", []) | |
| # Show shortlist command | |
| if "show shortlist" in message_lower or "view shortlist" in message_lower or "my shortlist" in message_lower: | |
| shortlist_summary = get_shortlist_summary(state) | |
| shortlist_msg = create_chat_message_with_metadata( | |
| shortlist_summary, | |
| "📋 Your Shortlist" | |
| ) | |
| history.append(shortlist_msg) | |
| # Update DataFrame and shortlist display | |
| if listings: | |
| current_df = create_listings_dataframe(listings, state) | |
| shortlist_count = len(state.get("shortlist", [])) | |
| status_text = f"Showing {len(listings)} listings ({shortlist_count} in shortlist)" | |
| return (history, gr.update(value=current_df, visible=True), | |
| gr.update(value=status_text), state) | |
| else: | |
| return (history, gr.update(), gr.update(value="Shortlist displayed"), state) | |
| # Save listing command (e.g., "save listing 2", "add listing 3 to shortlist") | |
| save_patterns = ["save listing", "add listing", "shortlist listing"] | |
| if any(pattern in message_lower for pattern in save_patterns): | |
| # Extract listing number | |
| import re | |
| numbers = re.findall(r'\d+', message_lower) | |
| if numbers and listings: | |
| try: | |
| listing_index = int(numbers[0]) - 1 # Convert to 0-based index | |
| if 0 <= listing_index < len(listings): | |
| listing = listings[listing_index] | |
| updated_state, status_message = add_to_shortlist(listing, state) | |
| success_msg = create_chat_message_with_metadata( | |
| status_message, | |
| "📌 Shortlist Updated" | |
| ) | |
| history.append(success_msg) | |
| # Update DataFrame and shortlist display | |
| current_df = create_listings_dataframe(listings, updated_state) | |
| shortlist_count = len(updated_state.get("shortlist", [])) | |
| status_text = f"Showing {len(listings)} listings ({shortlist_count} in shortlist)" | |
| return (history, gr.update(value=current_df, visible=True), | |
| gr.update(value=status_text), updated_state) | |
| else: | |
| error_msg = create_chat_message_with_metadata( | |
| f"❌ Invalid listing number. Please specify a number between 1 and {len(listings)}.", | |
| "❌ Error" | |
| ) | |
| history.append(error_msg) | |
| except ValueError: | |
| error_msg = create_chat_message_with_metadata( | |
| "❌ Please specify a valid listing number (e.g., 'save listing 2').", | |
| "❌ Error" | |
| ) | |
| history.append(error_msg) | |
| else: | |
| if not listings: | |
| error_msg = create_chat_message_with_metadata( | |
| "❌ No listings available to save. Please search for apartments first.", | |
| "❌ No Listings" | |
| ) | |
| else: | |
| error_msg = create_chat_message_with_metadata( | |
| "❌ Please specify which listing to save (e.g., 'save listing 2').", | |
| "❌ Missing Number" | |
| ) | |
| history.append(error_msg) | |
| # Clear shortlist command | |
| elif "clear shortlist" in message_lower or "empty shortlist" in message_lower: | |
| from shortlist_utils import clear_shortlist | |
| updated_state, status_message = clear_shortlist(state) | |
| clear_msg = create_chat_message_with_metadata( | |
| status_message, | |
| "📋 Shortlist Cleared" | |
| ) | |
| history.append(clear_msg) | |
| # Update DataFrame and shortlist display | |
| if listings: | |
| current_df = create_listings_dataframe(listings, updated_state) | |
| status_text = f"Showing {len(listings)} listings (shortlist cleared)" | |
| return (history, gr.update(value=current_df, visible=True), | |
| gr.update(value=status_text), updated_state) | |
| else: | |
| return (history, gr.update(), gr.update(value="Shortlist cleared"), updated_state) | |
| # Default: preserve current state | |
| if listings: | |
| current_df = create_listings_dataframe(listings, state) | |
| shortlist_count = len(state.get("shortlist", [])) | |
| status_text = f"Showing {len(listings)} listings ({shortlist_count} in shortlist)" | |
| return (history, gr.update(value=current_df, visible=True), | |
| gr.update(value=status_text), state) | |
| else: | |
| return (history, gr.update(), gr.update(value="Shortlist command processed"), state) | |
| 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, state) | |
| 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, updated_state) | |
| 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 | |
| } | |
| }) | |
| 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 == "shortlist_command": | |
| print(f"📌 CALLING handle_shortlist_command") | |
| return handle_shortlist_command(message, history, new_state) | |
| 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") | |
| # 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 | |
| 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}'") | |
| 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) | |
| # Apply strict mode filtering if enabled | |
| filtered_listings = filter_listings_strict_mode(listings, strict_mode) | |
| # Update state with listings | |
| 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, updated_state) | |
| 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_general_conversation(message: str, history: list, state: Dict): | |
| """Handle general conversation using the caseworker agent.""" | |
| try: | |
| current_language = state.get("preferences", {}).get("language", "en") | |
| # Enhanced message context | |
| enhanced_message = f""" | |
| User message: {message} | |
| Context: This is a conversation with someone looking for voucher-friendly housing in NYC. | |
| Be helpful, empathetic, and knowledgeable about housing, NYC neighborhoods, and voucher programs. | |
| If they ask about specific listings, let them know they need to search first. | |
| """.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, state) | |
| 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, state) | |
| 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], app_state: Dict = None) -> pd.DataFrame: | |
| """Create a formatted DataFrame from listings data with shortlist status.""" | |
| df_data = [] | |
| # Get shortlisted IDs for quick lookup | |
| shortlisted_ids = set() | |
| if app_state: | |
| shortlisted_ids = get_shortlisted_ids(app_state) | |
| 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") | |
| # Check if listing is shortlisted | |
| listing_id = str(listing.get("id", address)) | |
| shortlist_status = "✅" if listing_id in shortlisted_ids else "➕" | |
| 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", ""), | |
| "Shortlist": shortlist_status | |
| }) | |
| return pd.DataFrame(df_data) | |
| # Wire up the submit action with state management | |
| send_btn.click( | |
| 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 | |
| send_btn.click(lambda: "", [], [msg]) | |
| # Wire up Enter key submission | |
| msg.submit( | |
| handle_chat_message, | |
| [msg, chatbot, app_state, strict_mode_toggle], | |
| [chatbot, results_df, progress_info, app_state] | |
| ) | |
| msg.submit(lambda: "", [], [msg]) | |
| # Wire up DataFrame shortlist click handler | |
| results_df.select( | |
| handle_shortlist_click, | |
| [app_state], | |
| [results_df, progress_info, shortlist_display, app_state] | |
| ) | |
| # 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 | |
| # Language change event | |
| language_dropdown.change( | |
| change_language, | |
| [language_dropdown, app_state, chatbot], | |
| [chatbot, app_state] | |
| ) | |
| # Dark mode toggle using the correct JavaScript approach | |
| dark_mode_toggle.click( | |
| fn=None, | |
| js=""" | |
| () => { | |
| document.body.classList.toggle('dark'); | |
| } | |
| """ | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(i18n=i18n) |