# Project export: Robust Agent

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: A multi-agent econometric robustness checker on Agentverse: it pulls housing data, estimates a causal relationship, and stress-tests whether the result actually holds up.
- Devpost: https://devpost.com/software/triple-l
- GitHub: https://github.com/leonatientran/AIHackathon
- Demo: https://agentverse.ai/agents/details/agent1qv20xqzrp5tu9n4kg0xzyv53y76kve3suucqwdwa574pn006s7s22p2hmkr/profile
- Team: 2 GitHub contributor(s) — Leona (6 commits), Pedro (2 commits)

## Devpost submission (written by the team)

### Inspiration

Anyone can run a regression and report a coefficient. The hard part that separates a real finding from a spurious one is robustness: does the result survive different specifications, samples, and controls, or did it only appear because of a confounder? In econometrics this is usually a manual, tedious process: re-run the model ten different ways, eyeball whether the coefficient moves, argue about it. We wanted to see if a team of autonomous agents could automate that discipline end to end : fetch real data, estimate an effect, and then attack it from every angle a skeptical reviewer would. We framed it around a concrete question every renter and buyer cares about: how strongly do home values track rents across US metros? — the price–rent elasticity β in $$\log(\text{value}) = \alpha + \beta \,\log(\text{rent}) + \varepsilon.$$

### What it does

The system is three agents on Agentverse, all discoverable and chat-invocable through ASI:One: Housing Robustness Agent . On request it: fetches a real multi-metro panel from Zillow Research (ZHVI home values + ZORI rents, no API key), estimates the baseline price–rent elasticity (pooled OLS with heteroskedasticity-robust standard errors), and runs a robustness battery: metro fixed effects (the key omitted-variable-bias check), classical vs robust inference, outlier trimming, first-half/second-half time splits, a drop-one-metro jackknife, and a permutation placebo test. It returns a verdict and a 0–100 robustness score. Housing Robustness Agent . On request it: fetches a real multi-metro panel from Zillow Research (ZHVI home values + ZORI rents, no API key), estimates the baseline price–rent elasticity (pooled OLS with heteroskedasticity-robust standard errors), and runs a robustness battery: metro fixed effects (the key omitted-variable-bias check), classical vs robust inference, outlier trimming, first-half/second-half time splits, a drop-one-metro jackknife, and a permutation placebo test. It returns a verdict and a 0–100 robustness score. San Jose Housing Agent is a focused single-metro service that returns the current San Jose market snapshot (typical home value and rent, with month-over-month and year-over-year trends) from live Zillow feeds. San Jose Housing Agent is a focused single-metro service that returns the current San Jose market snapshot (typical home value and rent, with month-over-month and year-over-year trends) from live Zillow feeds. Housing Intelligence Orchestrator . A user asks it one question; it fans the query out to both agents, collects their replies, and returns a single combined report. Housing Intelligence Orchestrator . A user asks it one question; it fans the query out to both agents, collects their replies, and returns a single combined report. Result On live Zillow data (10 metros, ~1,370 monthly observations, 2015–2026), the system found a price–rent elasticity of β = 1.49. Critically, it survived metro fixed effects (dropping only to 1.36) meaning the relationship isn't just an artifact of expensive metros being expensive in both rent and price. It held across time splits and the drop-one-metro jackknife, and the permutation placebo collapsed to ≈ 0 (p = 0.000). Verdict: ROBUST, score 99.7 / 100. To prove the checker isn't a rubber stamp, we tested it on a deliberately confounded null effect — it correctly flagged the collapse under fixed effects and scored it 33 / 100 (NOT ROBUST). A 65-point separation between a real effect and a confounded one.

### How we built it

We used Claude (Anthropic) as a pair-programming and design partner: drafting the agent code and econometric, working through Agentverse's deployment constraints, and debugging the live hosted agents. Architecture decisions, the choice of research question and robustness checks, and validation against ground truth were directed and verified by us. Agent framework: Fetch.ai uAgents, deployed as hosted agents on Agentverse, communicating over the ASI:One chat protocol. Orchestration: a coordinator agent using inter-agent messaging plus Agent Storage to hold per-session state (hosted agents are stateless, so pending replies are tracked in storage). Econometrics: built with NumPy; OLS, HC1 robust standard errors, within-group demeaning for fixed effects, jackknife, and a permutation test. No black-box library; every number is auditable. The HC1 robust covariance is $$\widehat{\text{Var}}(\hat\beta) = \frac{n}{n-k}\,(X^\top X)^{-1} \Big(\textstyle\sum_i \hat{e}_i^2\, x_i x_i^\top\Big)(X^\top X)^{-1}.$$ Data: Zillow Research public CSV feeds (ZHVI, ZORI), fetched live at request time. Local demo: a uAgents Bureau runs the three-stage pipeline (data → estimation → robustness) in one process for a reproducible laptop demo.

### Challenges we ran into

Hosted-environment constraints. Agentverse hosts one agent per project, bans Bureau, and resets globals between calls. We re-architected the local three-agent pipeline into a self-contained hosted agent for the heavy compute, and moved orchestration state into Agent Storage. Data labeling mismatches. Our first deploy matched zero metros, Zillow labels the row "San Jose, CA," not the full Census metro name. We made the matching resilient. Making the score honest. An early scoring rule rewarded a confounded estimate because most specifications agreed on the biased number. We made the fixed-effects result a hard gate, so omitted-variable bias is penalized the way a real reviewer would penalize it. Python 3.14. uAgents doesn't yet support 3.14; tracking that down and pinning to 3.12 cost real time. Accomplishments we're proud of A robustness checker that demonstrably distinguishes a real effect from a confounded one Econometrics (fixed effects, robust SEs, permutation inference) running live inside a hosted agent on actual market data. A working multi-agent architecture where a coordinator delegates to and aggregates specialist agents.

### What we learned

Multi-agent design on a hosted, stateless platform is a different discipline from local orchestration. The value of a robustness tool is entirely in the checks, not the headline estimate; the fixed-effects gate is what makes it trustworthy. A clean, dependency-light numerical core is worth more than a heavy library when you need every step to be transparent and portable.

### What's next

Move from associational elasticity to a causal design or an event study around a rate change or policy date, with the same battery applied to the treatment effect. Add more research questions and metros, and let users specify their own question in natural language. Surface the spec curve visually so users see the coefficient's stability at a glance. Built with Python · Fetch.ai uAgents · Agentverse · ASI:One chat protocol · NumPy · Zillow Research data · Git/GitHub· Claude LLM Try it Orchestrator: agent1qv20xqzrp5tu9n4kg0xzyv53y76kve3suucqwdwa574pn006s7s22p2hmkr Housing Robustness Agent: agent1qvu8g4e5v2c76fwm90zax7varr7j2cjhxxlapureta9rsty8nqmwcdytn6n San Jose Housing Agent: agent1qtpqu2gamrv43fgnnqgy30lxk5awhwr6hn0lrggfksxhdcyqpk7qjlnt99j Code: https://github.com/leonatientran/AIHackathon

## README (from the GitHub repository)

# Multi-Agent Robustness Checker for Causal Estimates

A multi-agent system that takes an econometric estimate from real housing-market
data and **stress-tests how robust it is**. The headline question:

> *What is the price–rent elasticity of home values across US metros?*

The system estimates that elasticity, then re-runs it under a battery of
perturbations to ask: does the coefficient survive fixed effects, sub-samples,
outlier handling, and placebo tests — or was it an artifact of confounding?

## The pipeline

```
 data_agent  ──EstimationRequest──▶  estimation_agent  ──BaselineEstimate──▶  robustness_agent
 (real Zillow         (panel)            (baseline OLS)      (coef + panel)         (battery + verdict)
  panel)
```

| Agent | Role |
|-------|------|
| `data_agent.py` | Fetches a real multi-metro panel from Zillow Research (ZHVI home values + ZORI rents), builds `log(value)` / `log(rent)` series, sends it on. *(Recode of the old `zillow_agent.py`.)* |
| `estimation_agent.py` | Runs the **baseline** spec: pooled OLS of log home value on log rent, with HC1 robust SEs. Forwards the point estimate. |
| `robustness_agent.py` | Runs the **robustness battery** and reports a verdict + score. *(Recode of the old `valuator_agent.py`.)* |

Supporting modules:
- `econ.py` — pure-numpy OLS (classical + HC1 robust SEs), fixed-effects demeaning, and the robustness battery. No statsmodels dependency.
- `panel.py` — builds the Zillow panel across metros.
- `models.py` — shared uAgents message models.

## The robustness battery

For the coefficient of interest, the checker runs:

1. **Baseline** — pooled OLS, robust SEs.
2. **Metro fixed effects** — within-metro demeaning. *The key omitted-variable-bias check: if the coefficient collapses or flips here, the baseline was confounded.*
3. **Classical vs robust SEs** — does inference change?
4. **Outlier trimming** — drop 1st/99th percentile of rent.
5. **Time-window split** — first half vs second half of the sample.
6. **Jackknife by metro** — drop each metro once; report the spread.
7. **Permutation placebo** — shuffle rent; a real effect should collapse toward zero (reports a permutation p-value).

It then produces a **robustness score (0–100)** that gates hard on the
fixed-effects result, plus sign stability, significance consistency, coefficient
spread, and the placebo. Score ≥ 75 → ROBUST, 50–74 → FRAGILE, < 50 → NOT ROBUST.

## Running it

One-process demo (recommended — no address copy-pasting):

```bash
pip install -r requirements.txt
python bureau.py
```

Or run the three agents in separate terminals (`data_agent.py`,
`estimation_agent.py`, `robustness_agent.py`), pasting each printed address into
the upstream agent's `*_ADDRESS` constant.

## Data sources

Zillow Research public CSVs (no API key required), updated monthly:
- **ZHVI** — Zillow Home Value Index (typical home value, 35th–65th pct)
- **ZORI** — Zillow Observed Rent Index (typical asking rent)

## Notes

- ZHVI is a *typical value*, not a median sale price.
- The price–rent elasticity here is an associational baseline; the project's
  contribution is the **robustness machinery** around it, which is what reveals
  whether such an estimate would survive scrutiny.
- A separate **San Jose Housing Research Agent** (deployed on Agentverse) serves
  live single-metro snapshots over the ASI:One chat protocol.


## Detected evidence (automated analysis)

Indexed codebase: 13 recognized source files, 37 KB.
- Anthropic (technology) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (15 of 15)

```
.gitignore
bureau.py
chat_proto.py
data_agent.py
econ.py
estimation_agent.py
housing_research.py
market_research_agent.py
models.py
panel.py
README_market_research_agent.md
README.md
report.py
requirements.txt
robustness_agent.py
```

### Dependencies

- requirements.txt: anthropic, numpy, requests, uagents, uagents-core

### Recent commits (newest first)

- Merge branch 'main' of https://github.com/leonatientran/AIHackathon
- Fix import-time agent construction; split out report formatter
- Update README.md
- Retire property-scoring agents (replaced by robustness pipeline)
- Merge: keep pipeline models, retire property-scoring models
- Add robustness checker and SJ housing agent
- Add the two agents
- first commit

## Key source files (fetched from GitHub, selected and truncated for size)

### README_market_research_agent.md

```markdown
# San Jose Housing Research Agent

A [uAgent](https://fetch.ai/docs) that researches the **San Jose–Sunnyvale–Santa Clara, CA** housing market using Zillow Research's public data. Chat with it directly through ASI:One / the Agentverse chat UI, or query it from another agent in this project.

No API key required — it pulls Zillow's public CSV feeds.

**Live agent address:** `agent1qtpqu2gamrv43fgnnqgy30lxk5awhwr6hn0lrggfksxhdcyqpk7qjlnt99j`

---

## What it does

When it receives any message, it fetches the latest data and returns a snapshot of the San Jose metro market:

- **Typical home value (ZHVI)** — Zillow Home Value Index, the typical value for homes in the 35th–65th percentile range
- **Typical observed rent (ZORI)** — Zillow Observed Rent Index, the typical asking rent
- **Month-over-month change** for each
- **Year-over-year change** for each

It refreshes and logs a snapshot on startup and once a day.

### Example output

```
San Jose-Sunnyvale-Santa Clara, CA — Housing Market Snapshot

Source: Zillow Research (ZHVI home values, ZORI observed rents)

Region matched: San Jose, CA

Typical Home Value (ZHVI) as of 2026-05-31: $1,610,466
  Month-over-month change: -0.76%
  Year-over-year change: -1.35%

Typical Observed Rent (ZORI) as of 2026-05-31: $3,625
  Month-over-month change: +1.19%
  Year-over-year change: +5.51%

Note: Zillow updates these files mid-month; ZHVI reflects the 35th-65th
percentile of home values, not a simple median sale price.
```

*(Values change with each monthly Zillow release.)*

---

## How to use it

### 1. Chat with it (ASI:One / Agentverse chat UI)

Send any message — every message triggers a fresh pull:

> **You:** How's the San Jose housing market?
>
> **Agent:** *(returns the snapshot above)*

### 2. Query it from another agent

This is how it slots into the multi-agent pipeline. Send a `HousingQuery`, get back a `HousingReport`:

```python
from uagents import Agent, Context
from models import HousingQuery, HousingReport

HOUSING_AGENT = "agent1qtpqu2gamrv43fgnnqgy30lxk5awhwr6hn0lrggfksxhdcyqpk7qjlnt99j"

my_agent = Agent()

@my_agent.on_event("startup")
async def ask(ctx: Context):
    await ctx.send(HOUSING_AGENT, HousingQuery())

@my_agent.on_message(model=HousingReport)
async def handle(ctx: Context, sender: str, msg: HousingReport):
    ctx.logger.info(msg.report)

my_agent.run()
```

An orchestrator can fan out to this agent and combine its metro-level market context with the per-property analysis from `zillow_agent.py` / `valuator_agent.py`.

---

## Files

| File | Purpose |
|------|---------|
| `market_research_agent.py` | Main entrypoint — startup logging, daily refresh, structured query handling, chat protocol |
| `chat_proto.py` | ASI:One / Agentverse chat protocol wiring |
| `housing_research.py` | Fetches Zillow ZHVI/ZORI CSVs and builds the summary |

The `HousingQuery` / `HousingReport` models live in the shared `models.py`.

---

## Data sources

- **ZHVI** (home values): Zillow Home Value
[truncated — 832 more characters]
```

### requirements.txt

```
uagents
uagents-core
requests
anthropic
numpy

```

### models.py

```python
from uagents import Model


# ── Robustness-checker pipeline models ──────────────────────────────────────
class EstimationRequest(Model):
    """data_agent -> estimation_agent: the panel plus the spec to estimate."""
    panel_json: str
    question: str


class BaselineEstimate(Model):
    """estimation_agent -> robustness_agent: baseline coefficient + the panel."""
    panel_json: str
    question: str
    coefficient: float
    std_error: float
    spec: str


class RobustnessReport(Model):
    """robustness_agent -> coordinator/log: the verdict."""
    question: str
    baseline_coef: float
    robustness_score: float
    fe_survives: bool
    sign_consistent: bool
    specs_json: str
    summary_text: str

```

### report.py

```python
"""
report.py — Formats a robustness report as readable text.

Kept separate from any agent module so it can be imported without constructing
a uAgent (which would otherwise try to grab an event loop at import time).
"""


def format_report(question, baseline, specs, summary) -> str:
    lines = [
        "ROBUSTNESS CHECK",
        f"Question: {question}",
        f"Baseline coefficient: {baseline:.3f}",
        "",
        "Specifications:",
    ]
    for s in specs:
        flag = "ok " if s["significant"] else "ns "
        lines.append(f"  [{flag}] {s['spec']:<22} coef={s['coef']:+.3f}  ({s['note']})")
    lines += [
        "",
        f"Fixed-effects survives: {summary['fe_survives']}",
        f"Sign consistent across specs: {summary['sign_consistent']}",
        f"Significant in {summary['significant_share']*100:.0f}% of specs",
        f"Coefficient range: [{summary['coef_min']:.3f}, {summary['coef_max']:.3f}]",
        f"Permutation placebo p-value: {summary['permutation_p']:.3f}",
        f"ROBUSTNESS SCORE: {summary['robustness_score']}/100",
    ]
    verdict = (
        "ROBUST" if summary["robustness_score"] >= 75
        else "FRAGILE" if summary["robustness_score"] >= 50
        else "NOT ROBUST (likely confounded)"
    )
    lines.append(f"VERDICT: {verdict}")
    return "\n".join(lines)

```

### data_agent.py

```python
"""
data_agent.py — Stage 1 of the robustness checker (recode of zillow_agent).

Fetches a real multi-metro housing panel from Zillow Research and sends it to
the estimation agent to kick off the pipeline:

    data_agent  --EstimationRequest-->  estimation_agent  --BaselineEstimate-->  robustness_agent

Run locally. Paste the estimation agent's printed address into ESTIMATION_ADDRESS.
"""

import asyncio

from uagents import Agent, Context

from models import EstimationRequest
from panel import build_panel, panel_to_json

# Printed by estimation_agent.py on startup. (Not needed if you run via bureau.py.)
ESTIMATION_ADDRESS = "agent1..."

QUESTION = "What is the price-rent elasticity of home values across US metros?"

data_agent = Agent(
    name="data_agent",
    port=8000,
    seed="robustness_data_agent_seed_v1",
    endpoint=["http://127.0.0.1:8000/submit"],
)


@data_agent.on_event("startup")
async def kickoff(ctx: Context):
    await asyncio.sleep(2)
    ctx.logger.info("Fetching Zillow ZHVI + ZORI panel across metros...")
    try:
        panel = build_panel()
    except Exception as e:  # noqa: BLE001
        ctx.logger.exception(f"Failed to build panel: {e}")
        return

    meta = panel["meta"]
    ctx.logger.info(
        f"Panel built: {meta['n']} obs across {len(meta['metros'])} metros "
        f"({meta['date_min']} to {meta['date_max']})."
    )
    await ctx.send(
        ESTIMATION_ADDRESS,
        EstimationRequest(panel_json=panel_to_json(panel), question=QUESTION),
    )
    ctx.logger.info("Sent panel to estimation agent.")


if __name__ == "__main__":
    data_agent.run()

```

### estimation_agent.py

```python
"""
estimation_agent.py — Stage 2 of the robustness checker.

Receives the panel, runs the BASELINE specification (pooled OLS of log home
value on log rent), and forwards the point estimate plus the panel to the
robustness agent.

Run locally. Paste the robustness agent's printed address into ROBUSTNESS_ADDRESS.
"""

from uagents import Agent, Context

from models import EstimationRequest, BaselineEstimate
from panel import panel_from_json, panel_arrays
from econ import simple_slope

ROBUSTNESS_ADDRESS = "agent1..."

estimation_agent = Agent(
    name="estimation_agent",
    port=8001,
    seed="robustness_estimation_agent_seed_v1",
    endpoint=["http://127.0.0.1:8001/submit"],
)

print(f"\n--> ESTIMATION agent address (paste into data_agent): {estimation_agent.address}\n")


@estimation_agent.on_message(model=EstimationRequest)
async def estimate(ctx: Context, sender: str, msg: EstimationRequest):
    ctx.logger.info(f"Received estimation request: {msg.question}")
    panel = panel_from_json(msg.panel_json)
    y, x, groups, period, metros = panel_arrays(panel)

    # Baseline spec: pooled OLS, log(value) ~ log(rent), HC1 robust SE.
    beta, se = simple_slope(y, x, robust=True)
    ctx.logger.info(f"Baseline price-rent elasticity: {beta:.3f} (SE {se:.3f})")

    await ctx.send(
        ROBUSTNESS_ADDRESS,
        BaselineEstimate(
            panel_json=msg.panel_json,
            question=msg.question,
            coefficient=float(beta),
            std_error=float(se),
            spec="pooled_ols_log_value_on_log_rent_HC1",
        ),
    )
    ctx.logger.info("Forwarded baseline estimate to robustness agent.")


if __name__ == "__main__":
    estimation_agent.run()

```

### market_research_agent.py

```python
"""
San Jose Housing Research Agent

A uAgent that researches the San Jose-Sunnyvale-Santa Clara, CA housing
market using Zillow Research's public data (ZHVI home values, ZORI rents).

- Chat with it directly via ASI:One / the Agentverse chat UI (any message
  triggers a fresh report).
- Other agents can also send a structured HousingQuery message and get back
  a HousingReport.
- It refreshes and logs a snapshot once a day automatically.
"""

from uagents import Agent, Context, Model

from chat_proto import chat_proto
from housing_research import get_san_jose_housing_report

agent = Agent()


class HousingQuery(Model):
    pass


class HousingReport(Model):
    report: str


@agent.on_event("startup")
async def startup(ctx: Context):
    ctx.logger.info(f"San Jose Housing Research Agent started: {agent.address}")
    try:
        ctx.logger.info(get_san_jose_housing_report())
    except Exception as e:  # noqa: BLE001
        ctx.logger.exception(f"Startup data fetch failed: {e}")


@agent.on_interval(period=86400.0)  # once a day
async def daily_refresh(ctx: Context):
    try:
        ctx.logger.info("Daily refresh:\n" + get_san_jose_housing_report())
    except Exception as e:  # noqa: BLE001
        ctx.logger.exception(f"Daily refresh failed: {e}")


@agent.on_message(model=HousingQuery, replies=HousingReport)
async def handle_query(ctx: Context, sender: str, msg: HousingQuery):
    try:
        report = get_san_jose_housing_report()
    except Exception as e:  # noqa: BLE001
        report = f"Error fetching housing data: {e}"
    await ctx.send(sender, HousingReport(report=report))


agent.include(chat_proto, publish_manifest=True)

if __name__ == "__main__":
    agent.run()

```

### robustness_agent.py

```python
"""
robustness_agent.py — Stage 3 of the robustness checker (recode of valuator_agent).

Receives the baseline estimate and the panel, then re-runs the estimate under a
battery of perturbations (metro fixed effects, time-window splits, outlier
trimming, jackknife-by-metro, and a permutation placebo). Reports how stable the
coefficient is and an overall robustness score.
"""

import json

from uagents import Agent, Context

from models import BaselineEstimate, RobustnessReport
from panel import panel_from_json, panel_arrays
from econ import robustness_battery
from report import format_report

robustness_agent = Agent(
    name="robustness_agent",
    port=8002,
    seed="robustness_check_agent_seed_v1",
    endpoint=["http://127.0.0.1:8002/submit"],
)

print(f"\n--> ROBUSTNESS agent address (paste into estimation_agent): {robustness_agent.address}\n")


@robustness_agent.on_message(model=BaselineEstimate)
async def check(ctx: Context, sender: str, msg: BaselineEstimate):
    ctx.logger.info(
        f"Received baseline {msg.coefficient:.3f} for: {msg.question}. Running battery..."
    )
    panel = panel_from_json(msg.panel_json)
    y, x, groups, period, metros = panel_arrays(panel)

    specs, summary = robustness_battery(y, x, groups, time_index=period, seed=0)
    report_text = format_report(msg.question, msg.coefficient, specs, summary)
    ctx.logger.info("\n" + report_text)

    # If a coordinator is listening, this is where you'd ctx.send the report.
    _ = RobustnessReport(
        question=msg.question,
        baseline_coef=msg.coefficient,
        robustness_score=summary["robustness_score"],
        fe_survives=summary["fe_survives"],
        sign_consistent=summary["sign_consistent"],
        specs_json=json.dumps(specs),
        summary_text=report_text,
    )


if __name__ == "__main__":
    robustness_agent.run()

```

### chat_proto.py

```python
"""
ASI:One / Agentverse-compatible chat protocol for the San Jose Housing Research Agent.

Any text message the agent receives triggers a fresh pull of the San Jose
housing snapshot (ZHVI + ZORI), which is sent back as the reply.
"""

from datetime import datetime
from uuid import uuid4

from uagents import Context, Protocol
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    EndSessionContent,
    StartSessionContent,
    TextContent,
    chat_protocol_spec,
)

from housing_research import get_san_jose_housing_report

chat_proto = Protocol(spec=chat_protocol_spec)


def _text_chat(text: str, end_session: bool = True) -> ChatMessage:
    content = [TextContent(type="text", text=text)]
    if end_session:
        content.append(EndSessionContent(type="end-session"))
    return ChatMessage(timestamp=datetime.utcnow(), msg_id=uuid4(), content=content)


@chat_proto.on_message(ChatMessage)
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
    # Always acknowledge receipt first.
    await ctx.send(
        sender,
        ChatAcknowledgement(timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id),
    )

    for item in msg.content:
        if isinstance(item, StartSessionContent):
            ctx.logger.info(f"Chat session started with {sender}")
            continue

        if isinstance(item, TextContent):
            ctx.logger.info(f"Housing query from {sender}: {item.text}")
            try:
                report = get_san_jose_housing_report()
            except Exception as e:  # noqa: BLE001
                report = f"Sorry, I couldn't pull San Jose housing data right now ({e})."
            await ctx.send(sender, _text_chat(report))


@chat_proto.on_message(ChatAcknowledgement)
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
    # Not tracking read receipts for this agent.
    pass

```

### bureau.py

```python
"""
bureau.py — Run the whole robustness-checker pipeline in ONE process.

For demos this is far easier than launching three terminals and pasting
addresses. The Bureau wires the agents together on a shared event loop; we
resolve their addresses programmatically so no manual copy-paste is needed.

    python bureau.py
"""

import asyncio

from uagents import Bureau, Agent, Context

from models import EstimationRequest, BaselineEstimate, RobustnessReport
from panel import build_panel, panel_to_json, panel_from_json, panel_arrays
from econ import simple_slope, robustness_battery
from report import format_report

# --- define the three agents (no network endpoints needed inside a Bureau) ---
data_agent = Agent(name="data_agent", seed="robustness_data_agent_seed_v1")
estimation_agent = Agent(name="estimation_agent", seed="robustness_estimation_agent_seed_v1")
robustness_agent = Agent(name="robustness_agent", seed="robustness_check_agent_seed_v1")

QUESTION = "What is the price-rent elasticity of home values across US metros?"


@data_agent.on_event("startup")
async def kickoff(ctx: Context):
    await asyncio.sleep(1)
    ctx.logger.info("Fetching Zillow ZHVI + ZORI panel...")
    panel = build_panel()
    meta = panel["meta"]
    ctx.logger.info(
        f"Panel: {meta['n']} obs, {len(meta['metros'])} metros, "
        f"{meta['date_min']}..{meta['date_max']}"
    )
    await ctx.send(
        estimation_agent.address,
        EstimationRequest(panel_json=panel_to_json(panel), question=QUESTION),
    )


@estimation_agent.on_message(model=EstimationRequest)
async def estimate(ctx: Context, sender: str, msg: EstimationRequest):
    y, x, groups, period, metros = panel_arrays(panel_from_json(msg.panel_json))
    beta, se = simple_slope(y, x, robust=True)
    ctx.logger.info(f"Baseline elasticity = {beta:.3f} (SE {se:.3f})")
    await ctx.send(
        robustness_agent.address,
        BaselineEstimate(
            panel_json=msg.panel_json,
            question=msg.question,
            coefficient=float(beta),
            std_error=float(se),
            spec="pooled_ols_log_value_on_log_rent_HC1",
        ),
    )


@robustness_agent.on_message(model=BaselineEstimate)
async def check(ctx: Context, sender: str, msg: BaselineEstimate):
    y, x, groups, period, metros = panel_arrays(panel_from_json(msg.panel_json))
    specs, summary = robustness_battery(y, x, groups, time_index=period, seed=0)
    ctx.logger.info("\n" + format_report(msg.question, msg.coefficient, specs, summary))


bureau = Bureau()
bureau.add(data_agent)
bureau.add(estimation_agent)
bureau.add(robustness_agent)

if __name__ == "__main__":
    bureau.run()

```

[3 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]