Live testing of all 7 languages (en/es/zh/vi/ht/ar/tl against the
Chicago Drexel test address) showed every language correctly populated
the advisor's insurance + mitigation arrays (confirming the v0.15.3
Tier-1 fix), AND advisor.tldr rendered fluent prose in every language.
But risk.plain_verdict came back EMPTY for zh and ar β the two
non-Latin-script languages. Other risk-agent fields (risk_score,
risk_level, fema_gap_explanation) populated normally.
Root cause: the field's instruction said "Second person, plain English,
~10th-grade reading level" β when the system-prompt language directive
says "write everything in Mandarin/Arabic", the model gets two
contradictory signals about what language to use for THIS particular
field and either skips it or returns an empty string. The Latin-script
languages were robust to the contradiction; the non-Latin ones weren't.
Fix:
- Drop "plain English" from the plain_verdict instruction β replace
with explicit "Write the VALUE in the user's chβ¦
v0.15.3: make non-English dossiers actually work end-to-endβ
Hand-testing the Spanish ("es") dossier on a Chicago address surfaced
two distinct failures the original "translate everything" prompt
directive caused:
(1) Empty advisor sections. Gemma 4 in Spanish mode helpfully
translated the technical enum values inside the JSON it returned β
"nfip_standard" became "nfip_estandar", bucket "drainage" became
"drenaje". The strict post-parse filter in advisor_agent.py then
silently dropped every insurance_recommendations entry (none matched
a catalog product_id) and the bucket-grouped action renderer
collapsed all mitigation_actions into the "other" bucket (or the
model emitted an empty array, same result). The dossier rendered
with "The advisor agent did not return insurance recommendations
for this address" and only the forced-passthrough city resources
in the action plan. Functionally broken Spanish.
(2) Mixed-language UI chrome. The model-generated prose translated
correctly (verdict, NRI narrative, vision findings, action descriptions
β all Spβ¦
Caught when a chip pointed to 1 World Way (Clifton A. Moore Admin
Building at LAX) and FlutIQ still produced residential-flavored
"For this propertyβ¦" insurance copy, a homeowner action plan, and a
before-you-sign checklist β none of which make sense for a commercial
property. Root cause: property type wasn't surfaced anywhere; advisor
was hardcoded residential.
Fix (Option A β honest residential-only mode, on-brand with FlutIQ's
homeowner mission):
geocoder.py β classify Nominatim hits as residential or commercial
using class/type/name. Defaults to residential unless
the signal is confident; Census fallback assumes
residential (it only resolves TIGER street addresses).
orchestrator β pass property_type through into the dossier payload.
advisor β early-return for commercial: skip the LLM call entirely
and return a graceful note explaining FlutIQ is built
for residential and pointing to β¦
Adds a local-inference path (FlutIQ Edge via Ollama / Gemma 4 e4b)
alongside the existing cloud path (FlutIQ Cloud via OpenRouter / Gemma
4 31B). Same FastAPI server, same agents, same frontend; INFERENCE_BACKEND
env var picks the LLM endpoint. Verified end-to-end on an M4 Pro 24 GB:
/api/assess for a Chicago address completes in ~190s with all 9 agents
finishing on gemma4:e4b. Frontend re-skins itself based on data-variant
so judges can tell the modes apart at a glance (blue Cloud / green Edge).
Directly addresses the hackathon brief's "local frontier intelligence"
and "privacy is non-negotiable" framing.
Reframes the dossier as an expert briefing for someone about to live,
buy, or rent at the property β value is synthesis and time, not novel
prediction:
- risk_agent gains a plain_verdict field (3-5 sentences, second-person,
~10th-grade reading: bottom line first, then most important reason,
then trend direction)
- advisor_agent gains before_you_move_in[] (3-5 due-diligeβ¦
v0.14.1: tune three system prompts for the post-NRI signal layeringβ
Three Gemma 4 system prompts hadn't been updated since v0.9 even though
we'd layered in multi-city support, building permits trajectory, NRI
multi-hazard, and dedicated satellite analysis since. The prompts were
still saying things like 'Chicago's combined sewers overwhelm at 0.67
in/hr' as if Chicago were the only supported city, and not telling the
synthesis where each new signal fits in the four-layer hierarchy.
RISK_AGENT_SYSTEM_PROMPT β biggest update:
- Explicit 4-layer signal hierarchy: Property / Neighborhood / Visual /
Regional. Tells the model to cite which layer each claim comes from.
- Drops Chicago-specific knowledge from the system prompt (it's now
delivered city-by-city via the local agent's context_blurb).
- Adds combined-vs-separated sewer literacy as a general principle.
- Adds permits-as-leading-indicator framing.
- Adds multi-hazard awareness (NRI) β concurrent risks should be
surfaced when meaningful, but FlutIQ's flood thesis still leads.
β¦
v0.14: regional_risk_agent β FEMA NRI multi-hazard at county levelβ
Broadens FlutIQ from flood-only to multi-hazard. Every county in the US
now gets a calibrated risk profile across 18 hazards (wildfire, hurricane,
tornado, earthquake, drought, heat wave, etc.) at the neighborhood level
from FEMA's National Risk Index β the canonical source.
The win that matters: NRI's data was previously gated behind FEMA's
click-through CSV download. Found the exposed ArcGIS REST endpoint
behind RAPT (Resilience Analysis and Planning Tool):
https://services.arcgis.com/XG15cJAlne2vxtgt/arcgis/rest/services/
National_Risk_Index_Counties/FeatureServer/0/query
Free, no auth, point queries, 467 fields per county, full national
coverage. Verified live across 6 geographies with very different
hazard fingerprints:
Chicago: Cold wave + Winter weather + Tornado + Inland flood
LA: Earthquake + Wildfire + Inland flood (Community
Resilience: Very Low β interesting demo signal)
Houston: Hurricane + Tornado + Inland flood + Lightning
Miami:β¦
v0.13: Tier-1 multi-city expansion β Chicago + NYC + SF + LA + Austinβ
Refactors the Chicago-only local_agent into a city-aware system driven
by a registry. Each registry entry encodes the city's Socrata dataset
IDs, field names, flood-related category codes, and a context blurb
that gets injected into the local-agent prompt so Gemma 4 reasons
about each city's actual sewer system (combined vs separated, dominant
flood mode, local hazard pattern).
Honest per-city availability β what each city's dossier actually shows:
Chicago: full β 311 (WIB/SFL) + permits with reported_cost
(e.g. $80M new medical office at 4822 S Cottage Grove)
NYC: 311 β 391 sewer-flood reports per 350 5th Ave query.
Permits intentionally deferred: DOB datasets are
fragmented (legacy is stale, modern lacks dates,
active is filtered to currently-open jobs).
SF: full β 941 sewer reports + 75 permits at $366M (Mission
test point: '595 basement reports, increaβ¦
v0.12: building permits as flood-risk leading indicator (Chicago)β
The local agent now pulls TWO Socrata signals in parallel and asks
Gemma 4 to interpret them together: 311 flood reports (the historical
symptom) AND building permits (the leading indicator of impervious-
surface change). The combined-sewer system that handles stormwater
doesn't get upgraded when neighborhoods densify, so each new permit
shifts more load onto the same shared pipes.
The killer narrative this unlocks, verified live for 4521 S Drexel Blvd:
311 reports today: 0 (clean β historically stable)
Building permits: 89 within 1km / 3y, $189M total, including a
$80M new medical office and a $7.2M renovation
of a 15-story building literally next door.
Gemma 4 compound_signal: 'Clean 311 history combined with heavy
recent densification suggests a rising risk profile; the lack of
current reports may be a lagging indicator while the infrastructure
is being stressed by new development.'
That's leading-vs-lagging-indicator reasoningβ¦
v0.11: dedicated satellite-analysis agent with bbox detectionβ
The satellite image is now its own first-class agent (parallel to
the streetview agent) instead of being passed through to the risk
analyst as a raw image with no dedicated extraction. Each vision
agent owns its own image fetch, runs a Gemma 4 vision call with
a domain-specific prompt, and returns structured findings + per-
indicator bounding boxes. The risk analyst still gets both raw
images for cross-modal reasoning.
Why this is a real upgrade over v0.10:
v0.10 sent the satellite to the risk analyst as a raw image
with a generic 'use this for impervious surface reasoning'
hint. The model would mention it in passing.
v0.11 has a dedicated satellite agent that produces structured
output BEFORE the risk synthesis: impervious_estimate_pct,
building_footprint_pct, catchment_assessment, vegetation_buffer,
drainage_features_visible, plus 5+ categorized indicators
(impervious / drainage / water_body / vegetation / building /
other) with bounding boxes. The risk analyst theβ¦
v0.10: 3-image multimodal β satellite + wider-area + street view in one Gemma 4 callβ
Risk analyst now reasons across THREE visual perspectives plus all
6 data sources in a single inference, exercising six Gemma 4
capabilities at once (vision + interleaved multimodal + reasoning
+ structured JSON + long context + graceful per-image fallback).
Pipeline:
Mapbox satellite (zoom 17, ~150m across) β
Mapbox outdoors (zoom 15, ~600m across) β
Google Street View (heading-aimed at the building) β
+ 6 data agents β risk analyst (Gemma 4 reasoning=on)
Verified end-to-end against 4521 S Drexel Blvd, Chicago. The model
named all three views in its trace ('From the satellite I can seeβ¦',
'In the street viewβ¦') and produced a visual_corroboration that
explicitly cross-referenced views vs data:
'The satellite view confirms a high percentage of impervious surface
due to the large asphalt parking lot, increasing runoff. The street
view corroborates the presence of basement-level windows, which
create a direct vulnerability to water ingress that is not reflected
β¦
v0.9: multimodal risk analyst β vision + reasoning + 6 data sources in one Gemma 4 callβ
The risk analyst now receives the Street View photo as an image_url
content part alongside the text data from every other agent, with
reasoning mode enabled. Single Gemma 4 inference pass composes:
vision + reasoning + interleaved multimodal + structured JSON +
long context.
Per Gemma 4 best practice the image goes BEFORE the text in the
user message content array.
Concretely, the chain-of-thought trace now reads like a single mind
reasoning across both modalities, e.g. for 4521 S Drexel Blvd:
'I can see basement-level windows (partially obscured by the
fence). This is a critical vulnerability for surface water
ingress. The driveway is paved (impervious) and appears to slope
slightly toward the building/parking area, which could channel
water toward the foundation.'
β¦then cross-references with the FEMA zone, 311 count, weather
forecast, and AEP math. The model is examining the patient, not
reading the nurse's notes.
Backend:
- risk_agent.py accepts streetview_image_dβ¦
v0.8: Gemma 4 bounding-box detection on Street Viewβ
The streetview agent now returns a 2D bounding box per identified
indicator using Gemma 4's native object-detection capability. The
frontend overlays the boxes on the actual image as an SVG so users
can see exactly which pixels Gemma 4 saw.
Smoke-tested against Drexel Blvd Chicago: returns
- basement-level windows [moderate] box_2d=[400, 205, 458, 242]
- sloping driveway [low] box_2d=[455, 395, 580, 552]
both in the Gemini-convention yxyx 0-1000 normalized format.
Backend (streetview_agent.py + scripts/smoke_test_bbox.py):
- System prompt now describes the bounding-box convention
explicitly (yxyx, normalized 0-1000, top-left origin) β verified
via smoke test that Gemma 4 follows the explicit instruction
reliably while being unreliable about self-describing format.
- User prompt schema adds box_2d field per indicator entry.
- Post-parse validator drops box_2d if it isn't a valid 4-tuple of
numbers in [0, 1000] with ymin<ymax and xmin<xmax. Indicator
itself iβ¦
v0.7: README rewrite + version bump for multi-language + visionβ
Significant version jump from v0.4 prototype β v0.7 beta. Two
capabilities shipped since the v0.4 README was written that
materially change what FlutIQ is:
- Multi-language dossier (es, zh, vi, ht, ar, tl) β Digital
Equity & Inclusivity prize track angle
- Street View vision agent (multimodal Gemma 4) β eliminates
the 'shallow Gemma 4 usage' critique by adding actual visual
reasoning over a property photograph
README updates:
- 'Live demo' link to kredd25-flutiq.hf.space at the top
- 'What's new in v0.7' section right after the mission
- Mermaid architecture diagram now shows 6 data agents (added
Street View) and the language-aware advisor
- Tech stack table adds multimodal row, Google Street View row,
and updates hosting to single-Space
- Project structure adds streetview_agent.py, streetview.py,
languages.py, static/, README.md, DEPLOY.md
- 'What you see' section now describes 6 dossier sections (the
new Β§03 'What we saw at the property' β¦
The single deepest Gemma 4 capability we hadn't been using. Adds a
6th data agent that fetches a Google Street View image at the
property and asks Gemma 4 (vision) to identify visible flood-risk
indicators: basement-level windows, ground-floor HVAC, drainage
infrastructure, evidence of past water damage, elevation vs street.
Backend:
- New backend/app/tools/streetview.py β Google Street View Static
API client. Metadata-first pattern (free, doesn't count against
quota): walks radii Noneβ50mβ200mβ500m looking for a real
panorama before fetching the image. Computes a bearing from the
panorama's actual location back to the geocoded address so the
camera aims AT the building (without this, default heading shows
the road, not the property β verified with a Drexel Blvd test
case where unaimed heading produced property_visible=false but
the aimed shot found a real impermeable-paving indicator).
Image returned as a data: URL ready to inline into the vision
request.
- New baβ¦
Adds a language dropdown in the chrome bar that controls the language
of all user-facing copy in the dossier. Hits the Digital Equity &
Inclusivity track, and is the strongest single demo moment for the
mission framing β a Pilsen homeowner getting their flood-risk briefing
in Spanish without having to translate jargon.
Backend:
- New backend/app/data/languages.py β registry of 7 supported codes
(en, es, zh, vi, ht, ar, tl) covering ~80% of non-English-speaking
households in US flood-prone metros (USCB ACS 2022). normalize()
collapses 'es-MX' β 'es' and unknown codes β 'en'.
- prompt_directive(code) emits the language directive injected into
the risk + advisor agent system prompts. Critical bilingual rule:
user-facing copy translates, but legal product names (NFIP flood
insurance, sewer/water-backup endorsement), URLs, phone numbers,
dollar amounts, and JSON keys stay English. Otherwise the homeowner
can't search/say the right thing to actually buy the product.
- Assessβ¦
config: strip OPENROUTER_API_KEY to defend against trailing newlineβ
HF Spaces' web UI for secrets adds a trailing \n on paste. httpx
rejects headers containing newlines as 'Illegal header value', so
all Gemma 4 calls failed in production. Stripping at read time
handles this and any future re-paste.
deploy: bundle frontend into FastAPI for single-Space deployβ
Restructures so one HF Space serves both the API and the frontend
on a single port β chosen over the spec's split (HF + Cloudflare)
because for a hackathon demo a single URL is what gets pasted into
Kaggle / the video, and the CDN advantage is meaningless for a few
judges.
Mechanics:
- index.html β backend/static/index.html (git mv, no copy)
- FastAPI mounts static/ and serves index.html at GET /. The
api/* routes are registered first so they always win.
- Dockerfile COPYs static/ alongside app/.
- Frontend API_URL resolver: same-origin by default; falls back to
http://127.0.0.1:8000 only when the page is served on a non-8000
localhost port (e.g. python -m http.server 5173). The window.
FLUTIQ_API and ?api=... overrides still work for split deploys.
Local dev simplified β single uvicorn now serves everything:
cd backend && .venv/bin/uvicorn app.main:app --reload --port 8000
open http://127.0.0.1:8000
Verified end-to-end: GET / returns 74KB of HTML (the frontend),
GETβ¦
The frontend already used "FlutIQ" in the chrome wordmark and the
repo name has always been Gemma4Good_FlutIQ; the rest of the code
(backend app title, system prompts, smoke-test headers, skill
metadata, env-var name) had drifted into "FloodIQ" from the
original spec. Standardizing on FlutIQ β "Flut" is German for
flood/tide.
What changed:
- backend/app: APP_NAME, USER_AGENT, app title, agent + prompt
module docstrings
- backend/scripts: smoke-test User-Agent + display headers
- backend/README.md: HF Spaces frontmatter
- SKILL.md: skill name in YAML frontmatter (floodiq β flutiq)
- index.html: window.FLOODIQ_API β window.FLUTIQ_API
- floodiq-skill.tar β flutiq-skill.tar (git mv)
- placeholder domain: floodiq.pages.dev β flutiq.pages.dev
- new STATUS.md β working snapshot of architecture, fixed bugs,
known limitations, and what's left before submission
Intentionally NOT renamed:
- FLOODIQ_BACKEND_SPEC.md β historical build spec, referenced in
earlier commit messages; renaming woβ¦
OpenRouter returned 502 Bad Gateway mid-assessment, killing two
agents (FEMA and 311 analyst) while three others happened to hit
the API at calmer moments. The user saw partial results.
5xx from OpenRouter typically means an upstream provider hiccup
and clears within a few seconds, so we now retry 500/502/503/504
with the same exponential backoff we already use for 429, before
surfacing a real error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
advisor: stop inventing insurance products β drive from a verified catalogβ
The advisor was generating insurance recommendations directly from
Gemma 4's training data, which produced two real inaccuracies in the
Chicago dossier:
- "NFIP Preferred Risk Policy $400-600/yr" β that product was retired
in October 2021 when Risk Rating 2.0 launched; there is no longer
a separate PRP tier.
- "Chicago DWM 312-747-7030" β wrong number. The real DWM line is
311 or 312-744-7000. (This came from the original spec; I propagated
it without verifying.)
For a tool whose mission is reducing the scariness of insurance,
making up products and phone numbers is the opposite of helpful.
Architectural fix:
- New backend/app/data/insurance_catalog.py β hand-curated list of
real, verified flood insurance products (sewer endorsement, NFIP
post-RR2.0, private flood via Neptune/Wright, parametric via
FloodFlash US/Arbol) and city-specific resources (Chicago: MWRD
rain barrels at correct $21.96, CNT RainReady, real DWM phone).
- Advisor agent now passes the catalog intoβ¦
geocoder: add US Census fallback when Nominatim returns nothingβ
Nominatim's US residential coverage is patchy β the user's
"1133 Potomac Road, Atlanta GA" returned [] from OSM despite being
a real address. The US Census Geocoder is authoritative for US
addresses (TIGER/Line) and free with no auth, so we fall back to
it whenever Nominatim has no match.
The /geographies/onelineaddress variant also returns county FIPS,
so downstream agents that key off county (archive, news location
queries) keep working. State is normalized back to full name to
match Nominatim's shape (so the local-agent's Chicago check still
fires correctly).
Verified: same address now returns
1133 POTOMAC RD, ATLANTA, GA, 30338 β 33.9337N, 84.3461W
city=Atlanta state=Georgia county=DeKalb County
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backend: full 7-agent pipeline working end-to-endβ
Adds the four remaining data agents (local/weather/news/archive),
the risk-analyst agent (Gemma 4 reasoning mode), and the advisor
agent. Orchestrator runs the 5 data agents in parallel, then risk,
then advisor, streaming agent_update SSE events along the way and
emitting the compiled dossier on complete.
Verified against 4521 S Drexel Blvd, Chicago β full assessment in
~30s, all 7 agents return structured JSON, and the risk agent's
reasoning_trace shows actual step-by-step math:
P = 1 - (1 - 0.005)^30
P = 1 - (0.995)^30
0.995^30 β 0.860
P β 0.14 (14%)
It also unit-converted the precipitation forecast (1.02 mm/hr β
0.04 in/hr) and compared to the 0.67 in/hr Chicago combined-sewer
overwhelm threshold from the system prompt before concluding low
near-term risk. Advisor wrote recs at 5th-grade level as instructed.
Spec deviations:
- archive_agent uses GDELT 24-month timespan as a Storm Events DB
proxy. NCEI Storm Events has no clean public JSON API (CSV bulk
per year only)β¦
Stands up the FastAPI backend with one agent fully wired through the
SSE pipe so the architecture is proven before we fan out the rest:
POST /api/assess β geocode β FEMA NFHL β Gemma 4 interpret
β agent_update events β complete{dossier}
Verified end-to-end against two cases:
- Houston Allen Pkwy (Zone AE) β is_sfha=true, requires_insurance=true
- Chicago S Drexel (Zone X) β gap_warning generated unprompted,
surfacing the urban-sewer-backup blind spot that is the whole pitch
Smoke-tested Gemma 4 on OpenRouter free tier (scripts/smoke_test.py):
basic completion, reasoning mode, and OpenAI-format tool calls all
pass on google/gemma-4-31b-it:free.
Spec bugs fixed while building:
- llm/client.extract_reasoning read message.reasoning_details[i].content;
the actual key is .text. Also falls back to top-level .reasoning string.
- tools/fema requested VERSION_ID, which does not exist on NFHL layer 28.
ArcGIS returned HTTP 200 with an {"error": ...} bβ¦