Skip to content

feat: add display_map tool for point maps on OpenStreetMap#1206

Open
tckaros wants to merge 1 commit into
getnao:mainfrom
tckaros:feat/display-map-tool
Open

feat: add display_map tool for point maps on OpenStreetMap#1206
tckaros wants to merge 1 commit into
getnao:mainfrom
tckaros:feat/display-map-tool

Conversation

@tckaros

@tckaros tckaros commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

A new display_map agent tool that plots geographic point data from execute_sql results on an interactive MapLibre GL map, using OpenFreeMap (OSM-based vector tiles) as the default basemap.

This is the proof of concept behind #1205 — it's fully working, but it's mainly here to make that discussion concrete. If maps are a direction you want, I fully expect remarks on the approach and I'm happy to rework it.

Capture d’écran 2026-07-16 à 22 53 20

How it works

  • Shared schema (apps/shared/src/tools/display-map.ts): query_id reference, lat/lng column keys, optional popup label_key/tooltip_keys, map_type enum ready for a future heatmap mode.
  • Shared geo logic (apps/shared/src/map-points.ts): one implementation used by both backend validation and frontend rendering — case-insensitive column resolution (exact match first), rejection of null/blank coordinates before Number() coercion (no fabricated (0,0) points), Web Mercator latitude limit, antimeridian-aware bounds.
  • Backend tool: resolves the query_id via getQueryResult (current run, then persisted chat history) and returns actionable errors (missing query, unresolvable column, no plottable rows) so the model is never told a map displayed when the UI can't render one. On success it reports plotted/dropped counts and warns about unresolvable popup keys or results over the render cap, so the model can self-correct its SQL.
  • Frontend: mirrors display_chart (same source-query resolution, now factored into a shared useSourceQuery hook used by both). MapLibre loads as a lazy chunk; popups are built with textContent only so query values can't inject HTML; rendering is capped at 5,000 points with a "showing first N" note; WebGL-unavailable clients get a graceful fallback instead of a crash.
  • Non-web surfaces: Slack/Teams/Telegram can't render an interactive map, so a completed display_map degrades to a "View interactive map in nao" link card. Automations, the MCP ask_nao sub-agent and WhatsApp exclude the tool entirely (WEB_CHAT_ONLY_TOOLS + a new getTools excludeBuiltinTools option), and the system prompt only advertises the tool when the run's tool set includes it.

Tests

  • apps/shared/tests/map-points.test.ts — coordinate validation, key resolution, wrapped bounds (16 tests)
  • apps/backend/tests/display-map-tool.test.ts — tool validation paths incl. history fallback (13 tests)
  • apps/backend/tests/agent-tools.test.ts + system-prompt gating tests

Known limitations / follow-ups

  • Basemap is a third-party service (tiles.openfreemap.org) and the VITE_MAP_STYLE_URL override is build-time only — see [feature] Map visualization for latitude/longitude query results #1205 for the configurability question.
  • Map colors and basemap style are snapshotted on mount (light/dark toggle needs a remount) — TODO in code.
  • No static PNG rendering for Slack/Teams/email yet (charts have one via generate-chart); WhatsApp link card TODO.
  • Polar data beyond ±85.05° is dropped (Web Mercator); globe projection would be the real answer.

🤖 Generated with Claude Code

Review in cubic

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

URL https://pr-1206-65a79d5.preview.getnao.io
Commit 65a79d5

⚠️ No LLM API keys configured - you'll see the API key setup flow when trying to chat.


Preview will be automatically removed when this PR is closed.

@tckaros
tckaros force-pushed the feat/display-map-tool branch from 8cfda0a to 801f2e5 Compare July 16, 2026 21:21

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/backend/src/agents/tools/display-map.ts
Comment thread apps/backend/src/agents/tools/index.ts Outdated
Adds a new display_map agent tool that renders geographic point data
from execute_sql results on an interactive MapLibre GL map, using
OpenFreeMap (OSM-based vector tiles) as the default basemap,
overridable via VITE_MAP_STYLE_URL.

- Shared zod schema with query_id reference, lat/lng column keys and
  optional popup label/tooltip keys; map_type enum ready for a future
  heatmap mode
- Point building/validation lives in @nao/shared (map-points.ts):
  column keys resolve exact-first then case-insensitively
  (resolveColumnName, with resolveDataKey moved to shared on top of
  it), rows with null or blank coordinates are rejected before
  Number() coercion so no fabricated (0,0) points are plotted,
  latitudes beyond the Web Mercator limit (85.051129°) are dropped
  instead of collapsing onto the map edge, and keys resolving to the
  same column are rejected instead of silently plotting lat=lng
- The tool resolves the query_id through getQueryResult (current run,
  then persisted chat history) and fails with an actionable error when
  the query is missing, a column does not resolve, or no row carries
  plottable coordinates; on success it reports plotted and dropped row
  counts so the model can notice partial data and fix its SQL
- Frontend component mirrors display_chart: resolves rows from the
  sibling execute_sql message part; MapLibre loads in a lazy chunk so
  the main bundle is unaffected; kept out of collapsed tool groups
  like charts; rendering is capped at 5000 points with a "showing
  first N" note
- fitBounds wraps across the antimeridian (computeMapBounds), so
  points at 179°E/179°W fit a 2° span instead of a world-scale view
- Popups are built with textContent only so query values cannot
  inject HTML
- Slack, Teams and Telegram cannot render an interactive map, so a
  completed display_map degrades to a "View interactive map in nao"
  link card; automations, the MCP ask_nao sub-agent and WhatsApp
  exclude the tool (shared WEB_CHAT_ONLY_TOOLS list, getTools
  excludeBuiltinTools option) and the system prompt only advertises
  the tool when the run's tool set includes it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tckaros
tckaros force-pushed the feat/display-map-tool branch from 801f2e5 to 65a79d5 Compare July 16, 2026 21:37
@Bl3f

Bl3f commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Hello @tckaros I really really like this idea and this is something we should add in nao either with this PR or in another form so lets move it forward.

2 things to have in mind I think before the review:

  • we already have a PR opened that we can expect to merge this week to support "vibe-coded chart" ; which is a system of plugins so as admins you could define a set of viz that you want your users to be able to use (cf. feat: custom chart plugins ("vibe coded charts") #945) — so really customs maps could fall into this
  • tho, i think we should have by default a scatterplot map at least and a choropleth, for this the main question for me is: should we give the agent a display_map tool or should we consider maps like charts and include this in display_chart tool

Happy to hear your thoughts!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants