Skip to content

Latest commit

 

History

History
153 lines (120 loc) · 8.77 KB

File metadata and controls

153 lines (120 loc) · 8.77 KB

Project Context & Instructions

Overview

Core library for map-based applications with LLM-powered data analysis. Interactive MapLibre GL JS map with an agentic chatbot for natural language data queries and map control. Individual apps (different datasets, branding, deployments) import this library from the CDN and provide their own configuration.

Key Technologies

  • Frontend: MapLibre GL JS, PMTiles (vectors), COG + TiTiler (rasters), ES modules
  • Data: STAC catalog → unified dataset records with visual + parquet assets
  • Analytics: SQL queries via MCP (Model Context Protocol) to DuckDB on H3-indexed parquet
  • LLM: OpenAI-compatible Chat Completions API (multiple models via proxy)
  • Deployment: App JS/CSS is loaded from jsDelivr CDN. Every downstream app pins a version tag or commit SHA@main is no longer used in production or demos. Downstream apps only need a static HTML file — see the geo-agent-template repo.

Architecture (app/ modules)

  • main.js — Bootstrap: loads config, initializes catalog → map → tools → agent → UI
  • dataset-catalog.js — Fetches STAC collections, builds unified records
  • map-manager.js — Creates MapLibre map, manages layers/filters/styles
  • map-tools.js — the local tools the LLM agent can call (map control, styling, legends, geocoding)
  • tool-registry.js — Unified dispatch for local + remote (MCP) tools
  • mcp-client.js — MCP transport wrapper (connect once, lazy reconnect)
  • agent.js — LLM orchestration loop (agentic tool-use cycle)
  • chat-ui.js — Chat UI with collapsible tool-call blocks

Design decisions

Settled architectural calls are recorded in docs/design/ so they aren't re-litigated. Read the relevant note before reopening one of these questions.

  • docs/design/tool-call-parsing.md — why tool-call dialect recovery (parseEmbeddedToolCalls / looksLikeAttemptedToolCall in app/agent.js) lives in the harness and is kept, not moved to the server/proxy or replaced by a library.

Configuration

  • app/layers-input.json — Static config: STAC catalog URL, collection IDs, map view
  • config.json — Generated at deploy time by k8s (LLM models + API keys from secrets)
  • Both are merged by main.js at startup; runtime config overrides static config
  • Full configuration reference: docs/guide/configuration.md (the VitePress site is the canonical user docs)

Asset formats in layers-input.json

Each collection's assets array supports three formats:

1. Simple string — include a STAC asset by ID, using defaults:

"assets": ["cpad-holdings-pmtiles"]

2. Object — override display name, style, filters, etc.:

"assets": [
  {
    "id": "irrecoverable-total-2018-cog",
    "display_name": "Irrecoverable Carbon (2018)"
  }
]

3. Versioned — multiple STAC assets behind one logical layer, selectable via dropdown:

"assets": [
  {
    "id": "watersheds",
    "display_name": "Watersheds",
    "versions": [
      { "label": "L3 – Major Basins",   "asset_id": "hydrobasins_level_03" },
      { "label": "L4",                   "asset_id": "hydrobasins_level_04" },
      { "label": "L5",                   "asset_id": "hydrobasins_level_05" },
      { "label": "L6 – Sub-catchments",  "asset_id": "hydrobasins_level_06" }
    ],
    "default_version": "L6 – Sub-catchments"
  }
]

When versions is present, the layer panel shows one checkbox + a <select> dropdown. Switching versions swaps the visible map asset without adding/removing layer entries. The default_version matches by label (falls back to first entry if not found). All per-asset config options (default_style, default_filter, colormap, etc.) apply to every version uniformly.

Works for both PMTiles (vector) and COG (raster) assets. All versions must share the same layer type.

Git workflow — branch protection

The main branch is protected: direct pushes are rejected. All changes must go through a pull request.

Committing and pushing changes:

  1. Make changes, then commit:
    git add <files>
    git commit -m "<message>"
  2. Create a feature branch and push:
    git checkout -b <branch-name>
    git push -u origin <branch-name>
  3. The push output includes a PR URL — open it to create the pull request:
    remote: Create a pull request ... by visiting:
    remote:   https://github.com/boettiger-lab/geo-agent/pull/new/<branch-name>
    
  4. After the PR is merged, always clean up:
    git checkout main
    git pull
    git branch -d <branch-name>

If the user confirms the PR has been merged and asks to "clean up" or "switch back to main", run all three cleanup commands together.

Deployment

Downstream apps pin a specific jsDelivr ref — either a release tag (@vX.Y.Z, preferred) or a commit SHA. @main is not used by any deployed app; merging a PR here does not propagate to downstream apps until those apps bump their pin. Coordinating that bump across the fleet is the job of the private geo-agent-ops repo.

Release flow:

  1. Merge feature PRs to main here.
  2. Cut a release with gh release create vX.Y.Z --target main --generate-notes (or edit notes for migration-relevant changes).
  3. geo-agent-ops opens PRs against each downstream app to bump its pin.

Because every consumer is pinned, jsdelivr.net cache purges are normally unnecessary; the only situation that needs them is a SHA-pinned demo where a force-push rewrote the SHA's content (don't do this).

Live deployments:

Downstream apps serve a static HTML file that loads the pinned app code from jsDelivr at runtime. Two cases are easy to conflate:

  • The library ships a new version here. Pinned downstream apps are unaffected and need no action — that's the whole point of pinning. Nothing to roll out.
  • A downstream app bumps its own pin. This edits the app's index.html, so the new file must be re-served. How depends on the deployment:
    • GitHub Pages (e.g. the geo-agent-template demo) — auto-rebuilds on push; nothing further needed.
    • k8s pods that git clone at pod start (initContainer, e.g. the padus deployment of geo-agent-template) — the running pod keeps serving the index.html it cloned at startup, so you must kubectl -n biodiversity rollout restart deployment/<name> for the initContainer to re-clone the new pin. No kubectl apply is needed when index.html comes from the clone rather than a ConfigMap.
    • k8s pods serving content from a ConfigMap (private configmap-based apps) — regenerate and kubectl apply the ConfigMap, then rollout restart.

Development

  • Local: cd app && python -m http.server 8000
  • Create a local app/config.json with LLM model configs for development
  • config.json is in .gitignore — never committed (contains API keys)

Testing

  • npm test runs the suite; npm run test:coverage prints a per-module coverage table.
  • Tests live in test/; CI runs them on every PR (.github/workflows/test.yml).
  • The test runner is vitest. Browser-bound modules are not in jsdom — they're left to manual verification in deployed apps.

Module coverage status

When a PR touches a covered module, expect tests to change too. When it touches an uncovered one, verify by hand on a representative downstream app.

Module Coverage What changes here should be tested
app/transcriber.js 100% test/transcriber.test.js — endpoint resolution, error paths, abort signal
app/mcp-client.js 99% test/mcp-client.test.js — connect / reconnect / callTool retry / resources / prompts
app/tool-registry.js 98% test/tool-registry.test.js — registration, dispatch, argsRewriter, schema cleaning
app/map-tools.js 98% test/map-tools.test.js — local-tool execute paths, get_schema MCP delegate
app/dataset-catalog.js 89% test/dataset-catalog.test.js — STAC parsing, layer config shape, prompt rendering
app/agent.js 48% test/agent-retry.test.js — retry / abort / timeout. The conversation loop is uncovered (tested manually).
app/main.js 0% Bootstrap — verify by loading the app locally.
app/chat-ui.js, app/map-manager.js, app/layout-manager.js, app/map-draw.js, app/h3geo.js, app/animation-manager.js, app/voice-input.js 0% Browser-bound (DOM, MapLibre, MediaRecorder). Verify visually in a deployed app — no harness yet.

Overall line coverage is reported on each PR via the coverage workflow.