A Google ADK / Gemini agent that lets you query OGC API — Connected Systems (CSAPI) servers in natural language. Built as a "second consumer" of the unmerged CSAPI library in camptocamp/ogc-client#136, which itself notes that a second consumer is one of the trigger conditions for unblocking the deferred SWE Common work in OS4CSAPI/ogc-client-CSAPI_2#171.
A single ADK LlmAgent powered by gemini-2.5-flash, exposing six CSAPI
query tools. The tools are Python wrappers around a thin Node CLI that
imports @camptocamp/ogc-client/csapi and uses its CSAPIQueryBuilder to
construct request URLs against a CSAPI endpoint.
Six tools:
| Tool | Purpose |
|---|---|
discover_csapi(url) |
Probe an OGC API endpoint; report CSAPI support + collection IDs |
get_collection_info(url, collection) |
Full metadata for one collection |
list_systems(url, collection, limit) |
Sensors / platforms / actuators |
list_datastreams(url, collection, limit) |
Continuous observation channels |
list_sampling_features(url, collection, limit) |
Geographic measurement sites |
query_observations(url, collection, datastream_id, time_range, limit) |
Time-series readings |
┌───────────────────────┐
│ User (adk web UI) │
└───────────┬───────────┘
│ natural-language chat
▼
┌───────────────────────────────────────────────┐
│ Gemini LlmAgent (csapi_agent) │
│ • Picks tool from user intent │
│ • Summarizes JSON results in prose │
└───────────┬───────────────────────────────────┘
│ Python function call (1 of 6)
▼
┌───────────────────────────────────────────────┐
│ csapi_tools.py — Python wrappers │
└───────────┬───────────────────────────────────┘
│ subprocess.run(node ...)
▼
┌───────────────────────────────────────────────┐
│ csapi_cli/cli.mjs — Node CLI │
│ discover | collection_info | list_systems | │
│ list_datastreams | list_sampling_features | │
│ query_observations │
└───────────┬───────────────────────────────────┘
│ import
▼
┌───────────────────────────────────────────────┐
│ @camptocamp/ogc-client/csapi (PR #136) │
│ • OgcApiEndpoint │
│ • createCSAPIBuilder → CSAPIQueryBuilder │
└───────────┬───────────────────────────────────┘
│ HTTP fetch
▼
┌───────────────────────────────────────────────┐
│ Mock CSAPI Server (FastAPI, port 8765) │
│ — or any conformant CSAPI endpoint — │
└───────────────────────────────────────────────┘
Prerequisites: Node ≥20, Python 3.14+, the BIA .venv at BIA/.venv/.
# 1. Copy your existing Google API key (one-time)
cp ../my_agent/.env ./.env
# 2. Install Node deps for the CLI (one-time; links to the patched snapshot)
cd csapi_cli && npm install && cd ..
# 3. Install Python deps for the mock (one-time)
../.venv/bin/pip install -r mock_server/requirements.txt
# 4. Start the mock server (terminal 1)
../.venv/bin/python mock_server/server.py
# 5. Start the agent (terminal 2)
cd ..
source .venv/bin/activate
adk web
# → open http://localhost:8000, pick `csapi_agent` from the dropdownThe exact prose the model produces will vary turn-to-turn, but the tool calls and underlying data are deterministic. Sample outputs below were captured against the included mock server.
You: Discover the CSAPI server at http://localhost:8765
Tool call: discover_csapi(url="http://localhost:8765")
Tool returns:
{
"url": "http://localhost:8765",
"hasConnectedSystems": true,
"collections": ["weather-stations"]
}Sample assistant reply: "The server at http://localhost:8765 supports
Connected Systems and exposes one collection: weather-stations."
You: What sensors are deployed?
Tool call: list_systems(url="http://localhost:8765", collection="weather-stations", limit=10)
Tool returns 2 features: station-001 (North Field Weather Station,
−122.4194, 37.7749) and station-002 (Coastal Buoy, −122.5, 37.8). Both
are systemKind: Sensor.
Sample assistant reply: "There are two sensor systems registered in weather-stations:
- station-001 — North Field Weather Station, located at (−122.42, 37.77).
- station-002 — Coastal Buoy, located at (−122.5, 37.8).
Both are classified as Sensor systems."
You: Show me the air temperature readings between 5am and 8am UTC on May 2nd, 2026
The agent first calls list_datastreams to map "air temperature" to a
datastream ID (temp-001), then:
Tool call: query_observations(url="http://localhost:8765", collection="weather-stations", datastream_id="temp-001", time_range="2026-05-02T05:00:00Z/2026-05-02T08:00:00Z", limit=10)
Tool returns 3 observations:
phenomenonTime |
result | uom |
|---|---|---|
| 2026-05-02T05:00:00Z | 17.5 | Cel |
| 2026-05-02T06:00:00Z | 18.0 | Cel |
| 2026-05-02T07:00:00Z | 18.5 | Cel |
Sample assistant reply: "Air temperature on station-001 climbed steadily from 17.5 °C at 05:00 UTC to 18.5 °C at 07:00 UTC on May 2nd — about 0.5 °C per hour. Three hourly readings in that window."
You: Where are the sampling locations?
Tool call: list_sampling_features(url="http://localhost:8765", collection="weather-stations", limit=10)
Tool returns 1 feature: sf-001 — North Field Site, Point at (−122.4194, 37.7749).
Sample assistant reply: "There's one sampling site: North Field Site
(sf-001), a point location at approximately (−122.42, 37.77)."
Built incrementally against the OpenSensorHub demo server cited in PR #136. Four distinct interop issues surfaced — strong evidence for the #171 trigger condition #3 ("a second consumer demonstrating need"):
| # | Where | Layer | Status |
|---|---|---|---|
| 1 | csapiCollections null-derefs when data is null instead of returning [] |
Library bug | Filed: #188 — patched locally |
| 2 | collectionsUrl only accepts rel: "data"; rejects rel: "collections" (used by OpenSensorHub) |
Library compatibility gap | Filed in #188 follow-up — patched locally |
| 3 | parseCollections only flags ogc-cs:* rels as CSAPI; ignores collection-level featureType |
Library / convention mismatch | Filed in #188 follow-up |
| 4 | getCollectionDocument follows the collection's self link blindly without validating that the returned doc has the requested id |
Library could be more defensive (server is also wrong here) | Filed in #188 follow-up |
The bug pattern in (1) is shared by 5 sibling getters (recordCollections,
featureCollections, edrCollections, vectorTileCollections,
mapTileCollections) — same one-line fix applied by symmetry would harden
all of them.
The two patches we apply locally to make the agent work end-to-end live in
the snapshot at ~/Downloads/ogc-client-CSAPI_2-phase-7/src/ogc-api/endpoint.ts
(lines 88–96 and 230–237). After patching, run npm run build in the
snapshot root and the CLI's symlink picks up the rebuilt dist/ automatically.
- Local snapshot dependency. The CLI links to a patched local snapshot
of
ogc-client-CSAPI_2rather than the unpublished npm package. PR #136 has not landed; once it does (and incorporates the four fixes above), the snapshot can be replaced withnpm install @camptocamp/ogc-client@dev. - Mock server only. The 52°North demo's
/conformanceis missing CSAPI URIs (server-side regression), and OpenSensorHub has its own bugs (issues 3 & 4 above). The mock atmock_server/server.pyis the only currently working full-loop endpoint. - No SWE Common parsing. Observations are returned with their raw
resultfield. The architecturally correct typed-extraction path is tracked at upstream issue #171; we will adopt it once shipped rather than build a heuristic in the agent layer. - One subprocess per tool call. ~1–2 s of Node startup per call. Fine for chat latency; if it becomes a bottleneck, the CLI could be promoted to a long-running HTTP service.
csapi_agent/
├── __init__.py # ADK package marker
├── agent.py # LlmAgent definition + tool registration
├── csapi_tools.py # 6 Python wrappers around the CLI
├── .env.example # template for GOOGLE_API_KEY
├── csapi_cli/
│ ├── package.json # links to local snapshot via file:
│ ├── cli.mjs # Node entry point: 6 subcommands
│ └── node_modules/ # generated by npm install
└── mock_server/
├── requirements.txt # fastapi, uvicorn
└── server.py # spec-shaped CSAPI mock on port 8765