""" AR-House — Location Intelligence ================================= Página Streamlit para análisis de ubicación con score 0-100. """ from __future__ import annotations import sys from pathlib import Path import streamlit as st _ROOT = Path(__file__).resolve().parent.parent if str(_ROOT) not in sys.path: sys.path.insert(0, str(_ROOT)) from location_agent import run_location_agent from location_agent.db import list_reports, get_report from location_agent.utils.ollama_client import is_available as ollama_ok # ── Config ──────────────────────────────────────────────────────────────────── st.set_page_config( page_title="AR-House · Location Intelligence", page_icon="🏠", layout="wide", ) st.markdown(""" """, unsafe_allow_html=True) # ── Helpers ────────────────────────────────────────────────────────────────── def score_badge(score: int) -> str: cls = "score-high" if score >= 75 else ("score-medium" if score >= 50 else "score-low") return f'
{score}/100
' def score_color(score: int) -> str: return "green" if score >= 75 else ("orange" if score >= 50 else "red") def render_section_data(key: str, sec: dict) -> None: """Renderiza los datos clave de cada sección.""" if key == "crime": st.metric("Crímenes (30 días)", sec.get("total_crimes_30d", "N/A")) st.metric("Crimen violento", "Sí ⚠️" if sec.get("has_violent") else "No ✅") types = sec.get("crime_types", {}) if types: st.markdown("**Tipos de crimen:**") for t, n in list(types.items())[:6]: st.markdown(f"• {t}: {n}") elif key == "property": mlp = sec.get("median_list_price") ppsf = sec.get("price_per_sqft") dom = sec.get("days_on_market") inv = sec.get("inventory") if mlp: st.metric("Precio mediano listado", f"${mlp:,}") if ppsf: st.metric("Precio por sqft", f"${ppsf:,}") if dom: st.metric("Días en mercado (prom.)", dom) if inv: st.metric("Inventario activo", inv) cav = sec.get("county_assessed_value") if cav: st.metric("Valor tasado condado", f"${cav:,}") elif key == "schools": avg = sec.get("avg_rating") if avg: st.metric("Rating promedio", f"{avg}/10") for level, label in [("best_elementary", "Mejor Primaria"), ("best_middle", "Mejor Middle"), ("best_high", "Mejor High School")]: school = sec.get(level) if school: st.markdown( f"**{label}:** {school.get('name', 'N/A')} " f"({school.get('rating', '?')}/10)" ) schools = sec.get("schools", [])[:6] if schools: st.table({ "Escuela": [s.get("name", "")[:30] for s in schools], "Nivel": [s.get("level", "") for s in schools], "Rating": [str(s.get("rating", "N/A")) for s in schools], }) elif key == "amenities": ws = sec.get("walk_score") if ws is not None: st.metric("Walk Score estimado", f"{ws}/100") nearest = sec.get("nearest", {}) for cat, label in [("supermarket", "🛒 Supermercado"), ("hospital", "🏥 Hospital"), ("restaurant", "🍽️ Restaurante"), ("park", "🌳 Parque")]: item = nearest.get(cat) if item: st.markdown( f"**{label}:** {item.get('name', 'N/A')} " f"— {item.get('dist_miles', '?')} mi" ) elif key == "demographics": inc = sec.get("median_household_income") age = sec.get("median_age") unemp = sec.get("unemployment_rate") edu = sec.get("education_bachelors_pct") pop = sec.get("total_population") if inc: st.metric("Ingreso mediano del hogar", f"${inc:,}/año") if age: st.metric("Edad mediana", f"{age} años") if unemp is not None: st.metric("Tasa de desempleo", f"{unemp}%") if edu is not None: st.metric("Con título universitario", f"{edu}%") if pop: st.metric("Población", f"{pop:,}") eth = sec.get("ethnicity", {}) if eth: st.markdown("**Distribución étnica:**") for k, v in eth.items(): label = k.replace("_pct", "").replace("_", " ").title() st.markdown(f"• {label}: {v}%") elif key == "maritime": st.metric("Astilleros", len(sec.get("shipyards", []))) st.metric("Marinas", len(sec.get("marinas_with_jobs", []))) st.metric("Empleadores portuarios",len(sec.get("maritime_employers", []))) bls = sec.get("bls_employment", {}) if bls.get("latest_employment"): st.metric("Empleo marítimo estatal", f"{bls['latest_employment']}K ({bls.get('year','')})") elif key == "lifestyle": nm = sec.get("nearest_marina") nb = sec.get("nearest_beach") if nm: st.markdown( f"**⛵ Marina más cercana:** {nm.get('name','N/A')} " f"({nm.get('dist_miles','?')} mi)" ) if nb: st.markdown( f"**🏖️ Playa más cercana:** {nb.get('name','N/A')} " f"({nb.get('dist_miles','?')} mi)" ) st.markdown(f"**🌊 Acceso oceánico:** {'Sí ✅' if sec.get('ocean_access') else 'No'}") st.markdown(f"**🚤 Boat ramps en 10 mi:** {len(sec.get('boat_ramps', []))}") for m in sec.get("marinas", [])[:5]: st.markdown(f"• {m.get('name','N/A')} — {m.get('dist_miles','?')} mi") # ── UI principal ────────────────────────────────────────────────────────────── st.title("🏠 Location Intelligence") st.caption( "Investiga cualquier dirección en EE.UU. — " "score 0-100 para decisiones de inversión inmobiliaria" ) if not ollama_ok(): st.warning( "⚠️ Ollama no está activo — el análisis narrativo estará limitado. " "Activa Ollama para obtener síntesis completa." ) # ── Input + Historial ───────────────────────────────────────────────────────── col_input, col_history = st.columns([3, 1]) with col_input: st.subheader("🔍 Investigar Ubicación") search_mode = st.radio( "Modo de búsqueda", ["🏠 Dirección exacta", "🏙️ Ciudad / Área"], horizontal=True, label_visibility="collapsed", ) is_city_mode = search_mode == "🏙️ Ciudad / Área" if is_city_mode: address_input = st.text_input( "Ciudad o área", placeholder="Ej: Palm Beach, FL | Daytona Beach, FL | Key West, FL", help="Escribe la ciudad y el estado. El análisis usará el centro del área.", ) if address_input.strip() and "," not in address_input: st.caption("💡 Agrega el estado para mejores resultados — ej: Miami, FL") else: address_input = st.text_input( "Dirección completa", placeholder="Ej: 1234 W 49th St, Hialeah FL 33012", help="Incluye ciudad, estado y ZIP para mejores resultados", ) run_btn = st.button( "🚀 Analizar Ubicación", type="primary", disabled=not address_input.strip(), ) with col_history: st.subheader("📋 Historial") try: history = list_reports(limit=15) if history: options = { f"#{r['id']} — {r['address'][:32]}… ({r['score_general']}/100)": r["id"] for r in history } sel = st.selectbox( "Análisis anteriores", ["— Nuevo análisis —"] + list(options.keys()), ) if sel != "— Nuevo análisis —" and st.button("📂 Cargar"): st.session_state["loaded_report"] = get_report(options[sel]) st.session_state.pop("current_report", None) else: st.caption("Sin análisis previos") except Exception as e: st.caption(f"BD no disponible: {e}") # ── Ejecutar agente ─────────────────────────────────────────────────────────── if run_btn and address_input.strip(): status_box = st.empty() progress = st.progress(0) log_lines: list[str] = [] PROGRESS_KEYWORDS = [ "geocodificando", "sub-agentes", "crime", "property", "schools", "amenities", "demographics", "maritime", "lifestyle", "score", "ollama", "guardado", "pdf", "completo", ] def status_cb(msg: str) -> None: log_lines.append(msg) status_box.markdown("\n".join(f"`{l}`" for l in log_lines[-8:])) hits = sum( 1 for m in log_lines if any(k in m.lower() for k in PROGRESS_KEYWORDS) ) progress.progress(min(95, hits * 7)) with st.spinner("Analizando ubicación..."): try: result = run_location_agent(address_input.strip(), status_cb=status_cb) st.session_state["current_report"] = result st.session_state.pop("loaded_report", None) progress.progress(100) status_box.empty() except Exception as e: st.error(f"❌ Error: {e}") result = None # ── Mostrar reporte ─────────────────────────────────────────────────────────── report = ( st.session_state.get("current_report") or st.session_state.get("loaded_report") ) if not report: st.info("👆 Ingresa una dirección arriba y presiona **Investigar Ubicación**") st.stop() if report.get("error"): st.error(f"❌ {report['error']}") st.stop() overall = report.get("overall_score", 0) sections = report.get("sections", {}) scores = report.get("scores", {}) st.divider() # Cabecera con score general hdr1, hdr2, hdr3 = st.columns([2, 1, 1]) with hdr1: st.markdown(f"### 📍 {report.get('address','')}") st.caption(f"Análisis: {report.get('analysis_date','')}") with hdr2: st.markdown(score_badge(overall), unsafe_allow_html=True) st.markdown("

Score General

", unsafe_allow_html=True) with hdr3: pdf_path = report.get("pdf_path") if pdf_path and Path(pdf_path).exists(): with open(pdf_path, "rb") as f: st.download_button( "📄 Descargar PDF", data=f.read(), file_name=Path(pdf_path).name, mime="application/pdf", ) st.divider() # Gráfico de barras por dimensión try: import plotly.graph_objects as go SCORE_LABELS = { "crime": "Seguridad", "property": "Inmobiliario", "schools": "Escuelas", "amenities": "Amenities", "demographics": "Demografía", "maritime": "Marítimo", "lifestyle": "Lifestyle", } cats = list(SCORE_LABELS.values()) vals = [scores.get(k, 0) for k in SCORE_LABELS] fig = go.Figure(go.Bar( x=cats, y=vals, marker_color=[ "#1a7a1a" if v >= 75 else ("#e6a817" if v >= 50 else "#b83232") for v in vals ], text=[f"{v}" for v in vals], textposition="outside", )) fig.update_layout( title="Scores por Dimensión", yaxis=dict(range=[0, 115], title="Score / 100"), height=320, margin=dict(t=40, b=20), plot_bgcolor="rgba(0,0,0,0)", ) st.plotly_chart(fig, use_container_width=True) except ImportError: cols = st.columns(7) for i, (k, lbl) in enumerate(SCORE_LABELS.items()): cols[i].metric(lbl, f"{scores.get(k,0)}/100") st.divider() # Resumen ejecutivo exec_sec = sections.get("executive", {}) with st.expander("📋 Resumen Ejecutivo", expanded=True): if exec_sec.get("summary"): st.write(exec_sec["summary"]) col_s, col_w = st.columns(2) with col_s: st.markdown("**✅ Fortalezas**") for s in exec_sec.get("strengths", []): st.markdown(f"• {s}") with col_w: st.markdown("**⚠️ Riesgos**") for w in exec_sec.get("weaknesses", []): st.markdown(f"• {w}") # Secciones detalladas SECTION_ORDER = [ ("crime", "🔒 Criminalidad"), ("property", "🏘️ Mercado Inmobiliario"), ("schools", "🎓 Escuelas"), ("amenities", "🛒 Amenities y Walkability"), ("demographics", "👥 Demografía"), ("maritime", "⚓ Mercado Laboral Marítimo"), ("lifestyle", "⛵ Estilo de Vida Náutico"), ] for key, label in SECTION_ORDER: sec = sections.get(key, {}) sec_score = sec.get("score", 0) clr = score_color(sec_score) with st.expander(f"{label} — :{clr}[**{sec_score}/100**]"): col_data, col_narr = st.columns(2) with col_data: render_section_data(key, sec) if sec.get("errors"): for err in sec["errors"]: st.caption(f"⚠️ {err}") if sec.get("sources"): st.caption(f"Fuentes: {', '.join(sec['sources'])}") with col_narr: if sec.get("narrative"): st.markdown("**Análisis:**") st.write(sec["narrative"]) else: st.caption("Análisis narrativo no disponible (Ollama inactivo)")