"""
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)")