-
-
AquaPlot: citizen-first stream health checks for OneAquaHealth
-
The volunteer identifies what's in the tray by shape, with photos from a field guide
-
A readable result: screening band and ecosystem, human and animal health findings, each with an action
-
Dashboard: sites that need attention, the 48-hour weather outlook and the trend at every site
-
Repeat visits to a site become a trend over tim
-
The key idea: the AI checks the tray photo without seeing the volunteer's answers, then asks a question
Inspiration
We were originally inspired by a drastic video on social media depicting "The Vine that Ate the South," commonly known as Kudzu. Its disastrous effects on the local ecology and general welfare of the local populations (human included), brought us to begin looking for solutions to ecological destruction by invasive species. We decided that this hackathon would be a great opportunity to use our skills to help and be helped.
The animals that live in a stream are the best evidence of its health. A flat-headed mayfly means clean, oxygen-rich water; a tray of bloodworms and sludge worms means the opposite. They are also the hardest thing for a volunteer to name, and Track 3 describes the problem exactly: citizen observations can be inconsistent and error-prone.
The obvious answer, letting an AI identify the animals, felt wrong to us. The volunteer learns nothing, nobody can tell when the AI is mistaken, and a machine's guess ends up in a record that is supposed to protect people's health. OneAquaHealth's own Citizen Science App records the channel, banks and flow of a site, but does not ask what lives in the water. We wanted to build that missing half without taking the person out of it.
What it does
A volunteer at an urban stream photographs the stream, scoops gravel from a shallow fast patch into a pale tray, and identifies what lives there from a guide organised by shape ("three tails", "a case made of sand").
A vision model looks at the same tray photo without seeing their answers, and only raises questions where it saw something different: "You marked some kind of stonefly. The model thinks this may be Mayfly (flat-headed). Count the tails." Questions are ranked by whether the answer would change the stream's reading. The person decides.
Then published indices (BMWP, or the Iberian IBMWP in Portugal and Spain, chosen from the coordinates) and a readable rule engine produce:
- a Water Framework Directive screening band, a visual pressure score, and One Health findings for the ecosystem, people and animals, each naming its rule, its evidence and an action, split between what you can do now, what your community can do, and what to ask the authority for;
- a health alert that waits for a human: evidence only the model saw is held at "concern" until a person confirms the observation it rests on; the weather either side of the visit (Open-Meteo): sewage signs after heavy rain point the authority at storm-overflow records, and in dry weather at misconnected drains;
- a dated report the citizen can send to the water authority; exports in OneAquaHealth's own formats: a FHIR R4 Bundle conforming to the project's IG (hl7-eu/oah), which passes the HL7 validator with no errors and no warnings, and the project's Citizen Science App submission in the app's own answer codes. A check at one of the project's 106 research sites carries the project's site code.
- Repeat visits to the same spot become a trend, the dashboard flags declining sites, and a 48-hour outlook re-runs every site's latest reading against the forecast, because an overflow is most likely to run when heavy rain falls where sewage has been seen. It works offline (riverbanks often have no signal) and it works with no AI at all.
How we built it
- Python and FastAPI, SQLite, and plain HTML and JavaScript with no build step, so it runs on a free tier and anyone can read it.
- Claude as the vision model, behind a fixed schema: observe.py may only return observations in a vocabulary generated from the field form, each with a confidence, and it is explicitly allowed to say "I cannot tell from this photo". Every determination lives in bioindex.py and onehealth.py, where it can be read, tested and argued with.
- One JSON file defines the field form, and generates the model's prompt, the server's validator, the questions on screen, the pressure scoring and the FHIR CodeSystem. FHIR R4 against the OneAquaHealth IG, built from source with SUSHI and validated with the official HL7 validator.
- The OneAquaHealth public API (api.enora-oah.eu) for the 106 research sites and the app's answer codes, snapshotted so the app never depends on it at run time. Open-Meteo for weather, iNaturalist for taxonomy and places, Leaflet for the map.
Challenges we ran into
Making the AI useful without letting it decide. Our first version had the model identify the animals and the person confirm. We changed the order: the person identifies, and the model is asked blind, because the model agreeing only counts as evidence if it never saw the answer. Getting a clean validation. Our first FHIR export failed the HL7 validator: invalid fullUrls, undefined extensions, empty arrays. We only found them by running the validator against the project's own profiles. Being honest about thin evidence. A tray with three animals cannot prove a stream is healthy or dead. The index caps the band a small sample can reach, and says so on screen. A demo without a stream or an API key. Judges rarely stand in a river. The sample check ships two openly licensed photos whose model reading was recorded once and is replayed, labelled as a recording everywhere it appears. We ran into problems with Claude and learned the hard way. We tried running Graphify and ended up running 12 subagents on a session with a 600k context window on Opus 5.5, blowing up both our 5-hour and weekly limits.
Accomplishments that we're proud of
- The blind second opinion: an AI that asks a volunteer the right question instead of replacing their judgement.
- A FHIR export that conforms to OneAquaHealth's own IG with zero validator errors and warnings. Every finding explains itself, with the rule behind it, its evidence and an action.
- Missing inputs don't break it. No model, no signal, no coordinates or no photo each lower the certainty, with a sentence saying why, but none of them stops the result.
What we learned
- How to use Claude.
- This includes how to prompt engineer, utilize Claude skills, manage tokens, and strategies such as prompt-looping and subagent checking.
- How to interpret raw data into tangible results. Our inspiration started by taking a report by hand, breaking down the steps, what each data point (animal, plant, steam condition) means, and ultimately thinking about how we could use AI to make this process more accurate, faster, and easier to do for beginners.
- UI design and how to utilize public libraries in tandem with Claude.
- TROUBLESHOOTING. Diving into the code and asking Claude where the bugs are, and manually going in and checking the code ourselves. Also troubleshooting the demo by running the demo and seeing where the website falls apart (buttons misplacement, not scaling properly when window size changes, no "back" buttons when you change pages)
- How to create a user-friendly application. The best example of this is how we added pictures with a "zoom in" feature to help the user identify what animals/bugs they have on their tray.
What's next for AquaPlot
- Measure the second opinion on real trays. The evaluation harness is built (EVALUATION.md); running it on labelled volunteer photos is the first step.
- National indices beyond BMWP and IBMWP (Portugal's IPtI, Italy's IBE): one score column each.
- Sending the Citizen Science App submission directly, with an app account. (If we did this, we would either train our own model or utilize Claude, since we already have rate limiting set up.)
- Mapping to the project's expert macroinvertebrate codes once they are documented.
- A pilot with a OneAquaHealth research city, comparing citizen readings with the project's own laboratory samples at the same site codes.
Built With
- anthropic
- api.enora-oah.eu
- claude
- fastapi
- fhir
- hl7
- html
- inaturalist
- javascript
- leaflet.js
- open-meteo
- open-meteo-forecast
- playwright
- pytest
- python
- render
- sqlite
Log in or sign up for Devpost to join the conversation.