Skip to content

feat: backend devcontainer + experimental MCP server + chatbox-core integration - #115

Draft
romer8 wants to merge 217 commits into
mainfrom
feature/tethysdash-mcp-server
Draft

feat: backend devcontainer + experimental MCP server + chatbox-core integration#115
romer8 wants to merge 217 commits into
mainfrom
feature/tethysdash-mcp-server

Conversation

@romer8

@romer8 romer8 commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Three additions, scoped intentionally. Two production-grade (devcontainer + chatbox-core wiring) and one experimental / work-in-progress (the MCP server). The experimental piece is gated behind opt-in editor permissions and a separately-running localhost service; it does not change any default user-facing behavior.

1. Backend-only devcontainer (.devcontainer/)

A self-contained dev environment for Python / Django / Tethys work. Pip-only (no conda), SQLite (no Postgres), no Node / Webpack. The frontend is served from the bundled tethysapp/tethysdash/public/frontend/ assets committed in the repo.

Files:

  • .devcontainer/Dockerfile — Python base + system deps for Tethys.
  • .devcontainer/devcontainer.json — VS Code Dev Containers / Codex config; declares postCreateCommand + postStartCommand.
  • .devcontainer/init.sh — idempotent first-boot orchestrator. Calls the two scripts in order, gates on a marker file under ${TETHYS_PERSIST}.
  • .devcontainer/scripts/tethyscore.sh — generates portal_config.yml, runs tethys db migrate, creates the default admin/admin superuser. Mirrors the role of tethys_portal_firo/docker/tethyscore.sls.
  • .devcontainer/scripts/devcontainer-app.sh — provisions the SQLite persistent store, links it to tethysdash:ps_database:primary_db, runs tethys syncstores tethysdash. Mirrors tethys_portal_firo/docker/salt/init_apps.sls.
  • .devcontainer/run.sh — convenience wrapper for tethys manage start -p 0.0.0.0:8000.
  • .devcontainer/README.md — how to use it + the rationale for choosing bash idempotence over Salt (pip-distributed Salt has incomplete install_requires on slim Python images, and the SaltProject apt repo is not reliably reachable from all build environments).

If you need to do React work, the existing conda-based workflow in the top-level README.md is unchanged.

2. Experimental MCP server (tethysapp/tethysdash/mcp/) — WIP

Status: experimental. Off-path for default tethysdash users. The MCP server is a separately-launched Python process (default python -m tethysapp.tethysdash.mcp.tethysdash_mcp_server, port 9001, loopback-only) that the in-app ChatSidebar connects to over HTTP. Nothing in the existing Django request path changes. Editors / admins on a dashboard see an opt-in sidebar; viewers see no change at all.

A FastMCP-based Model Context Protocol server that exposes dashboard-creation, visualization-rendering, and tile-modification capabilities as 25 typed tools + 25 slash-command prompt templates. The intent is to let an LLM (configured in the chatbox sidebar) build / edit dashboards via tool calls instead of point-and-click — for users who want it.

Areas added:

  • tethysapp/tethysdash/mcp/tethysdash_mcp_server.py — the FastMCP server (25 @mcp.tool + 25 @mcp.prompt definitions). Streamable HTTP transport at /mcp is the default; legacy SSE at /sse is opt-in via MCP_TRANSPORT=sse for migrating users. Host is 127.0.0.1 by default; set MCP_HOST=0.0.0.0 only when fronting with an authenticated reverse proxy. CORS is env-driven (ALLOWED_ORIGINS, default *; ALLOW_CREDENTIALS is auto-derived to avoid the credentials:true + origin:* spec violation).
  • tethysapp/tethysdash/mcp/_input_validation_middleware.py — converts Pydantic ValidationErrors on tool input into a typed envelope with expected_kwargs and a fix_hint, so an LLM gets a single-retry recovery path instead of an opaque trace.
  • tethysapp/tethysdash/mcp/_observability_middleware.py — per-tool-call structured logs.
  • tethysapp/tethysdash/tests/mcp/ — ~140 contract tests across 14 files covering tool output shapes, prompt-to-tool routing, validation envelopes, transport / CORS, and per-source-type layer-tool inputs.
  • New runtime deps in pyproject.toml: pydantic, python-dateutil, pytz, jsonpatch==1.33. Optional test extra: fastmcp.

The MCP tool surface intentionally covers the same capabilities as the existing Edit Visualization modal — it does not unlock any privileged operations. Authorization piggybacks on the editor-only sidebar mount and the localhost-bound transport.

3. Chatbox-core library reference (@chatbox/core)

The in-app chat sidebar (reactapp/components/sidebar/ChatSidebar.js) consumes the generic @aquaveo/chatbox-core engine, which lives outside this repo. The engine handles the LLM tool-use conversation loop, MCP wire transport, model capability detection, slash-prompt popover, and per-dashboard chat persistence.

package.json:

"@chatbox/core": "file:../lib/chatbox-core"

The file: link is the workspace-aware dev path (chatbox-core sits as a sibling subrepo so tethysdash and chatbox-core can co-evolve). For stable downstream consumption an npm install of @aquaveo/chatbox-core (currently on the beta dist-tag) is the equivalent — the import paths under @chatbox/core/... move to @aquaveo/chatbox-core/....

ChatSidebar.js uses a generic system prompt and passes no engineExtensions, so the engine runs with its defaults. Variable input values published by the sidebar feed VariableInputsContext so MCP-created variable inputs integrate cleanly with the existing dashboard interactivity system.

Playwright E2E suite — archived (not in this PR)

The Playwright E2E suite (36 mocked tests + 4 skipped) was archived to the aquaveo remote at branch archive/playwright-suite-2026-05-11 and removed from the working tree before opening this PR. Rationale: the suite was paying more in UI-churn maintenance + perceived flake risk than it returned in caught bugs (0 documented Playwright-caught bugs that Jest missed). The full state of the suite — every spec, helper, the playwright.config.js, and the SQLite fixture script — is preserved at the exact state it would have been if we'd merged the suite upstream. To restore: git checkout aquaveo/archive/playwright-suite-2026-05-11 -- reactapp/__tests__/e2e/ reactapp/playwright.config.js and re-add the three devDeps (@playwright/test, better-sqlite3, pg).

The decision-making framework for whether and how to re-introduce Playwright lives at docs/brainstorms/2026-05-11-tethysdash-playwright-smoke-charter-requirements.md in the firoh workspace. Future Playwright additions should clear the charter's R2.1 unique-coverage criteria + R2.2 stability bar + R4 forward-going reviewer gate before merging. Reviewers who want a Playwright surface upstream are explicitly invited to flag that here — the trim direction is being communicated up front, not imposed.

What is intentionally NOT in this PR

  • No changes to default tethysdash behavior for non-editor users.
  • No new exposed network surface in the default dev workflow — MCP is loopback only and the sidebar gates on editor permission.
  • The MCP server is not auto-started by the Django app; it runs as a separate process the user launches.
  • No Playwright suite (see "Playwright E2E suite — archived" above).

Test plan

  • Backend: pytest tethysapp/tethysdash/tests/mcp/ — ~140 MCP contract tests pass.
  • Backend: pytest tethysapp/tethysdash/tests/unit_tests/ — existing unit tests + new test_plugin_helpers, test_cli, test_plugin_registry_loader pass.
  • Frontend: npm test — Jest component suite passes (~800 tests).
  • Devcontainer: clean build + first-launch idempotence verified (marker file gating prevents double-init).
  • Reviewer smoke (recommended): open in Dev Containers, run bash .devcontainer/run.sh, log in as admin/admin, confirm dashboard list loads at /apps/tethysdash/.

Status framing for reviewers

  • Devcontainer and chatbox-core wiring: production-ready; no flags, no opt-ins. Reviewers should treat as standard additions.
  • MCP server: experimental. Treat the public tool surface and the system prompt as subject to change until we have signal from real usage. Default tethysdash users never touch it. We are explicitly soliciting reviewer feedback on the boundary, the off-by-default posture, and whether the server-as-separate-process model fits upstream conventions.
  • Playwright removal: opt-in. If reviewers want a Playwright surface upstream, flag here and we'll bring tests back from the archive branch under the charter.

romer8 added 30 commits March 31, 2026 20:36
- Update .gitignore to include generated client plugin files
- Enhance package.json with pre-collect, pre-start, and pre-build scripts
- Implement dynamic panel creation in DashboardLayout for embedded plugins
- Introduce ClientModuleLoader for loading React components from npm-installed client plugins
- Modify AppLoader to merge npm-installed client plugins into the visualization list
- Create collectClientPlugins script to discover and register client plugins
- Update utilities for handling client custom visualizations
- Added babel configuration for JSX syntax support.
- Updated package dependencies, including @babel/preset-react to version 7.28.5.
- Integrated @chatbox/core component into ChatSidebar, replacing dynamic federated component loading.
- Modified Header component to conditionally render chatbox configuration.
- Updated Tethys app settings to use new chatbox Ollama host and key settings.
- Implemented Ollama proxy endpoints to handle requests to the chatbox API.
- Updated package.json to include @huggingface/transformers and fixed chatbox dependency path.
- Modified DashboardLayout.js to initialize panel positions correctly and added checks for grid item results.
- Updated ChatSidebar.js to clarify LLM provider configuration management.
- Changed runtimePluginRegistry.json to update plugin ID and description.
- Removed obsolete chatbox Ollama host settings from app.py and adjusted chatbox configuration in controllers.py.
- Enhanced MCP server with CORS support and added new visualization tools with detailed descriptions.
- Updated render functions to improve plugin rendering and discovery processes.
- Remove source="nwmps" example from parameter description and docstring
- Remove source="my_plugin" example from create_variable_input docstring
- LLMs copied these example values verbatim instead of calling list_intake_plugins
…atch events

Add validGridItems deduplication layer to prevent duplicate i keys from
corrupting react-grid-layout's compact() function. Coerce layout values
to proper types with minH/minW minimums. Use gridItemsUpdated ref in
updateLayout to avoid stale closures during React 18 batched state
updates. Extend batch event format to support per-panel source with
backward-compatible fallback. Bump create_variable_input defaults to
w=25, h=12 for better sizing.
MCP tools create_text, create_custom_image, and create_map_visualization
used synthetic "Inline *" source names that don't exist in the Default
visualization registry. This prevented the edit modal from resolving
the visualization type — findVisualizationBySource returned null.

Change source names to match the Default group entries:
- "Inline Text" → "Text"
- "Inline Image" → "Custom Image"
- "Inline Map" → "Map"

This follows the pattern create_variable_input already uses correctly
with source "Variable Input". Rendering is unaffected — Base.js routes
by args.inlineData/vizType, not source name.
…eoJSON

Map.js passed raw lon/lat degrees to an EPSG:3857 view, centering
maps in Africa. Now detects EPSG:4326 coordinates via range heuristic
and transforms via fromLonLat/transformExtent before setCenter/fit.

loadGeoJSON returned early for object inputs, skipping CRS assignment.
ModuleLoader then crashed accessing crs.properties.name on undefined.
Now runs checkForCRS for all inputs regardless of type.
… drawing, variables

Redesign the create_map_visualization MCP tool for better LLM ergonomics:

- Smart defaults: streets base map, zoom 12, layer control off
- High-level markers param: [{lon, lat, label}] auto-builds GeoJSON
  VectorLayer with correct CRS (EPSG:4326)
- Nominatim geocoding: optional 'center' param accepts place names
  (e.g., "Salt Lake City") and geocodes to coordinates
- Drawing tools: drawing_tools, drawing_limit, drawing_variable params
  for interactive geometry capture with dashboard variable publishing
- Variable support: extent_variable for map extent publishing,
  ${var} syntax passthrough in layer args documented
- Expanded base maps: added terrain and ocean shorthands (7 total)
- All extent coordinates include projection metadata for the frontend
- Auto-centers on first marker when no extent or center is provided
…tom Image

The frontend has dedicated rendering short-circuits for source "Map",
"Text", and "Custom Image" (utilities.js:99-123) that read args at the
top level. The inlineData wrapper bypassed these paths, making the edit
modal unable to read or modify the visualization config.

- create_text: inlineData → args: {text}
- create_custom_image: inlineData → args: {image_source} (matches
  frontend's itemData.args.image_source at utilities.js:119)
- create_map_visualization: inlineData → args: {baseMap, layers, ...}
- Added create_map_visualization to BM25SearchTransform always_visible
  so the LLM can discover it without calling search_tools first

Plotly, Table, and Card keep inlineData — they have no frontend
short-circuit and require the inline rendering path.
The edit modal's checkAllInputs treats null values as "unfilled" and
blocks save with "All arguments must be filled out". mapDrawing=null
and map_extent=null triggered this when editing chatbox-created maps.

Build the args dict conditionally — only include map_extent and
mapDrawing when they have actual values. Remove mapConfig entirely
(not a registry arg — frontend generates it automatically).

Matches the format the manual UI stores when creating maps.
Split create_map_visualization into two focused tools:
- create_map_visualization: simplified (center, markers, drawing, base map).
  Generates UUID for cross-tool referencing. No raw layers param.
- add_map_service_layer: constructs layer configs from flat params with
  required field validation per source type (WMS, ESRI, GeoJSON, KML, etc.).
  Supports queryable + attributeVariables for click-to-update workflows.

Add tethysdash:update-visualization DOM event in DashboardLayout so layers
can be appended to any existing map grid item by UUID — works for maps
created in the same conversation or from previous sessions.

UUID flows through the dispatch chain: MCP generates it, Chatbox passes
it through panels, DashboardLayout uses panel.uuid || uuidv4().

Both tools added to BM25SearchTransform always_visible list.
render_plugin description emphasizes ${variable_name} linkage syntax.
handleUpdateVisualization now accepts layers (array) via operation
"append_layers" instead of a single layer. All layers for the same
map are pushed in one updateTab call, avoiding the stale-ref problem
where multiple sequential events read outdated gridItemsUpdated.current.

Same class of fix as the batch dispatch solution for add-visualization.
…rcion

Three fixes:
- Resolve ESRI service layer name for attributeVariables key so the
  click-time lookup matches the /identify response (matches manual UI)
- Update gridItemsUpdated ref immediately in handleAddVisualization and
  handleUpdateVisualization as defense-in-depth against rAF timing races
- Coerce JSON string arguments to dicts for params, geojson, and
  attribute_variables (some LLM providers serialize objects as strings)
…p-server

# Conflicts:
#	package-lock.json
#	package.json
#	reactapp/components/dashboard/DashboardLayout.js
#	reactapp/components/visualizations/Base.js
#	reactapp/components/visualizations/utilities.js
…rcion

Three changes to add_map_service_layer:
- Place GeoJSON data at source.geojson (top-level) instead of
  source.props.geojson, matching the manual UI format that
  loadLayerJSONs and ModuleLoader.loadGeoJSON both expect
- Add geojson_url parameter for externally-hosted GeoJSON files —
  the frontend fetches the URL at render time via the existing
  loadGeoJSON URL branch in utilities.js
- Coerce JSON string arguments to dicts for params, geojson, and
  attribute_variables (some LLM providers serialize objects as strings)
Extends create_variable_input to support all frontend variable input
types: dropdown (with options array), slider (with min/max/step/
outputFormat metadata), date-range, and csv-uploader. Includes LLM
string coercion for the options parameter and safe float parsing for
slider initial values.
…tions

Adds documentation for:
- MCP server architecture, tools table, and visualization data flow
- All 7 MCP data contract rules (inlineData vs args, source names, tool
  split, per-type validation, ESRI attr key, GeoJSON placement, dict coercion)
- ChatSidebar integration with @chatbox/core engine
- docs/solutions/ knowledge store reference
- docs/plans/ and docs/brainstorms/ workflow artifacts
- Key conventions: no early returns, batch dispatch, no example values
167 tests covering:
- 8 viz-creating tools (rendering path, source name, args shape)
- 9 map layer source types (config structure, GeoJSON placement, ESRI key)
- 8 variable input subtypes (dotted key metadata, slider outputFormat)
- Dict parameter coercion (string and dict inputs)
- Error paths (missing required fields, invalid types)

Validates the 7 data contract rules documented in docs/solutions/.
Runs in <1s with no DB or server dependency.
- playwright.config.js: webServer starts Django, workers:1, mocked/integration projects
- helpers/db.js: better-sqlite3 wrapper for SQLite fixtures (createDashboard, truncateAll)
- helpers/dashboards.js: composable fixture builders for all viz types and map layers
- helpers/mocks.js: route interception for tiles, API responses, auth endpoints
- setup-test-db.py: one-time setup for Tethys SQLite persistent store
- visualizations.spec.js: first E2E test (Text panel rendering)
- package.json: added @playwright/test, better-sqlite3, Jest testPathIgnorePatterns for e2e/

Requires: npx playwright install-deps chromium (system libraries)
romer8 and others added 29 commits May 21, 2026 16:15
…lause

Debug session 2026-05-21 turn 2 with gemini-flash: asked to 'Enable the
Custom Popup Modal on the China Flowlines layer...' (a FIRST-TIME
popup-modal setup), the LLM routed to patch_visualization instead of
configure_popup_modal_layer, then fabricated:

- wrong path: /args/layers/0/configuration/props/popup
  (canonical is /args/layers/0/popupConfig)
- wrong value shape: {content, title, type}
  (canonical is {mode, position, titleTemplate, gridItems})

Root cause: the PRIORITY clause in beforeFirstMessage's system content
sends 'modify / add to / update an existing tile' → patch_visualization.
'Enable popup on an existing layer' reads as modify-existing. The new
configure_popup_modal_layer tool had no carve-out, so it lost to
patch_visualization in the LLM's routing.

Fix: extend the existing 'add_*_layer for new layers on existing maps'
exception into a list of TWO exceptions:

  (a) adding a new layer to an existing map → add_*_layer (unchanged)
  (b) configuring a custom popup modal on an existing map layer for the
      FIRST TIME (the layer exists, but the layer has no popupConfig
      yet) → configure_popup_modal_layer, never patch_visualization

Plus an explicit boundary: patch_visualization is correct ONLY for
PARTIAL EDITS to an already-existing popupConfig (e.g., changing just
the titleTemplate via /args/layers/N/popupConfig/titleTemplate).

The tool-side fix is in tethysdash_mcps PR #10 (rewrites the
configure_popup_modal_layer description to lead with positive
first-time-setup framing). Both sides must land for the routing to
work end-to-end.

New test: PRIORITY rule preserves the configure_popup_modal_layer
exception for first-time popup-modal setup. Pins the wording so future
edits can't silently drop the carve-out.

Jest suite: 19/19 ChatSidebar tests pass.
…when args contain ${feature.*} tokens

Closes the deferred Unit 5 UX bug from the 2026-05-21 popup-modal debug
arc. When the Edit Visualization modal is opened on a popup gridItem
whose args carry unresolved `${feature.<key>}` tokens (no feature is
selected in the preview context), the preview pane was firing
getVisualization with the raw literal token. The plugin rejected the
unresolvable arg and the preview rendered "Failed to retrieve data" —
a confusing UX that looked like a configuration error when actually
the preview is waiting for runtime feature selection.

Base.js already had this short-circuit (Base.js:441-460, shipped with
the popup-modal feature): findUnresolvedFeatureTokens detects literal
${feature.*} tokens in resolved args and sets vizType="featurePending",
which Base.js's Visualization component renders as a friendly
"Awaiting feature selection" placeholder. VisualizationPane.js — the
parallel Edit Visualization preview path — was missing the same gate.

This commit ports the identical short-circuit into VisualizationPane.js
just before the getVisualization call:

- Imports findUnresolvedFeatureTokens from components/visualizations/utilities
- After resolving variable-input substitutions, scans the resolved
  args for unresolved ${feature.*} tokens
- If any are present: setVizType("featurePending") + setVizData with
  source (display label) + pendingTokens, then return without firing
  getVisualization
- DataViewer's preview pane reuses the same <Visualization> component
  from Base.js (DataViewer.js:22, 400), so the existing featurePending
  shell renders automatically — no UI work needed in DataViewer.

source field in setVizData uses selectedVizTypeOption["source"] (the
display label) to match Base.js's gridItemSource pattern, so the
pending hint renders "Custom Image renders when a feature is clicked
on the map" or similar.

Test: VisualizationPane Pane short-circuits to featurePending when args
contain ${feature.*} tokens. Uses Custom Image source with
image_source containing a literal "${feature.comid}" token. Asserts:
(1) setVizType called with "featurePending" before getVisualization
would have fired, (2) setVizData includes pendingTokens listing
"feature.comid", (3) negative: setVizType never called with "image"
(the success path that would have fired if getVisualization had run).

Jest suite: 20/20 VisualizationPane tests pass (19 existing + 1 new).
No regression to other DataViewer paths.
The CIROH DevCon 2026 workshop image
(ghcr.io/aquaveo/ciroh-devcon-2026:...) ships the tethysdash app,
and the workshop demos depend on four FIRO-Tethys plugins being
available out-of-the-box so participants don't have to pip install
anything during the 2-hour session.

Added one RUN layer after the main tethysdash install that pip-installs
all four plugins from git, branch-pinned to @main per the workshop's
branch-track convention (rebuilding the image close to event day picks
up plugin fixes without re-pinning):

  - FIRO-Tethys/ciroh_plugins
  - FIRO-Tethys/tethysdash_plugin_cnrfc
  - FIRO-Tethys/tethysdash_examples
  - FIRO-Tethys/RFC-Plugins

Each is a tethysdash plugin that brings its own intake driver via
setup.py entry-points; no separate intake / intake-* install needed.

Next step (manual, outside this commit): rebuild + push
ghcr.io/aquaveo/ciroh-devcon-2026:<next-tag> so the workshop compose
service picks up the new plugin catalog.
Cascades the chatbox-core denylist-lift release (Aquaveo/chatbox-core@0.15.0,
commits 59e0fc0 + 568bf68, tagged v0.15.0, CI-published 2026-05-21) into
the tethysdash workshop image build.

Two changes:

1. .devcontainer/Dockerfile sed-swap pin bumped from
   @aquaveo/chatbox-core@0.6.4 → @aquaveo/chatbox-core@0.15.0.
   This is a forward upgrade through the stable line (0.6.4 → 0.7.0 →
   0.8.0 → ... → 0.14.0 → 0.15.0). Workshop image rebuild will pull the
   new version from npm; per project_tethysdash_devcontainer_multistage_
   shipped.md the sed-swap fires before npm install in the multi-stage
   build, so the build resolves @aquaveo/chatbox-core@0.15.0 from npm
   instead of the local file: link.

2. tethysapp/tethysdash/public/frontend/ rebuilt by webpack against the
   local file: link (which symlinks to ../lib/chatbox-core, currently at
   v0.15.0). Old hashed bundle main.0f9d130a8a4f0652bd62.js replaced by
   main.c60861e5afe3620bdd0d.js; manifest.json updated to match. The
   committed bundle is the dev artifact — when participants build the
   workshop image, the Dockerfile rebuilds the bundle inside the image
   from sed-swapped npm-installed source (different hash, equivalent
   behavior).

Net behavior change in the workshop image after this commit + a workshop
image rebuild: Ollama Cloud catalog filter no longer hides qwen /
deepseek / glm / chatglm / yi / baichuan / ernie / hunyuan / minicpm /
xverse / internlm / skywork / kimi / minimax families. Operator-policy
carve-out shape preserved (BLOCKED_MODEL_PREFIXES is frozen-empty in
chatbox-core 0.15.0).
…rature drop)

Cascades the chatbox-core fix (Aquaveo/chatbox-core@0.15.1, commit
e13ef0c + bump 4735b5f, tagged v0.15.1, CI-published 2026-05-22) into
the tethysdash workshop image build.

Two changes:

1. .devcontainer/Dockerfile sed-swap pin bumped from
   @aquaveo/chatbox-core@0.15.0 → @aquaveo/chatbox-core@0.15.1.
   Workshop image rebuild will pull the new version from npm; the sed-
   swap fires before npm install in the multi-stage build, so the
   build resolves @aquaveo/chatbox-core@0.15.1 from npm instead of
   the local file: link.

2. tethysapp/tethysdash/public/frontend/ rebuilt by webpack against
   the local file: link (which symlinks to ../lib/chatbox-core,
   currently at v0.15.1 source with the anthropic temperature drop).
   Old hashed bundle main.c60861e5afe3620bdd0d.js replaced by
   main.fed9c822e1af905e559b.js; manifest.json updated to match.

Net behavior change after this commit + a workshop image rebuild:
Anthropic Claude 4.x models (claude-opus-4-7 and siblings) stop
returning HTTP 400 `temperature is deprecated for this model` on
every request. The chatbox-core Anthropic adapter no longer emits a
temperature key in streamParams; older Claude 3.x models that still
accept temperature ignore it on reasoning-style calls anyway.

Next: tag a new workshop image version (e.g., v2026.05.2) on
Aquaveo/ciroh_devcon_2026_workshop so the workshop image rebuilds
against the new chatbox-core. The two-step CI build will pick up
0.15.1 via the sed-swap automatically.
…sponses-API graceful-block)

Cascades the chatbox-core fix (Aquaveo/chatbox-core@0.15.2, commit
8cdfdcc + bump c6ddc7b, tagged v0.15.2, CI-published 2026-05-22) into
the tethysdash workshop image build.

Two changes:

1. .devcontainer/Dockerfile sed-swap pin bumped from
   @aquaveo/chatbox-core@0.15.1 → @aquaveo/chatbox-core@0.15.2.

2. tethysapp/tethysdash/public/frontend/ rebuilt by webpack against
   the local file: link (which symlinks to ../lib/chatbox-core at
   v0.15.2 source). Old hashed bundle main.fed9c822e1af905e559b.js
   replaced by main.27f5be4ec7eaf653a657.js; manifest.json updated
   to match.

Net behavior change after workshop image rebuild:

  - OpenAI gpt-5.4-mini (and other GPT-5.x): no longer 400s with
    `max_tokens is deprecated` - request uses `max_completion_tokens`
    now and temperature is no longer sent.
  - OpenAI gpt-5.1-codex-mini (and codex/o1-pro/o3-pro variants): the
    model picker dropdown now disables these with ⚠️ prefix and
    "(Responses API only)" suffix; tooltip explains the limitation.
    If a participant has one persisted from before, the adapter
    short-circuits with a structured "responses_api_required" error
    instead of letting the SDK 404 with the raw API message.

Next: tag v2026.05.3 on Aquaveo/ciroh_devcon_2026_workshop to rebuild
the workshop image against the new chatbox-core. The two-step CI
build will pick up 0.15.2 via the sed-swap automatically.
… rebuild bundle

- New llm_proxy_google endpoint: forwards arbitrary paths under
  /apps/tethysdash/llm-proxy/google/<rest>/ to
  https://generativelanguage.googleapis.com/<rest>. Reads X-Goog-API-Key
  from request headers. Uses regex='.+' so colon and slash in paths
  (e.g., 'v1beta/models/gemini-2.5-flash:streamGenerateContent') match.
- Rebuilt React bundle with @google/genai SDK-based Gemini adapter.
…-compat (#133)

* feat(controllers): proxy route for Ollama /v1/chat/completions OpenAI-compat

Companion to chatbox-core feat/ollama-local-openai-compat-preset. Adds
the Django proxy route the new "Ollama (local)" preset depends on:

    /apps/tethysdash/ollama-proxy/v1/chat/completions/
      → ${X-Ollama-Host}/v1/chat/completions

The new route mirrors the existing /ollama-proxy/api/{tags,show,chat}/
routes (same _proxy_to_ollama helper, same X-Ollama-Host/X-Ollama-Key
header contract), but forwards to Ollama's OpenAI-compatibility
endpoint instead of /api/chat.

Why a new endpoint instead of swapping /api/chat: newer Ollama versions
rejected the existing chat path's tool_call argument format mid-turn
("cannot unmarshal object into Go struct field ... of type string"),
while older versions reject the OpenAI-spec string format. The two
versions can't be satisfied by a single static wire format on the
native endpoint. The OpenAI-compat endpoint follows OpenAI spec stably
across versions. See chatbox-core PR #23 follow-up note for the
architectural rationale.

The Ollama Cloud preset (existing /ollama-proxy/api/chat/ route) is
unchanged — Cloud's wire format is stable.

Includes test_ollama_v1_chat_completions_proxy_forwards_to_openai_compat_path
asserting the upstream URL ends with /v1/chat/completions and respects
the X-Ollama-Host header.

* chore(devcontainer): bump @aquaveo/chatbox-core 0.15.2 → 0.15.3

0.15.3 ships the new `ollama_local` provider preset
(Aquaveo/chatbox-core#44 merged 2026-05-28). The companion proxy
controller for /ollama-proxy/v1/chat/completions/ is in the previous
commit in this PR.

Without bumping the npm pin, the devcontainer base image (which the
workshop image layers on top of) ships with chatbox-core 0.15.2 and
the new "Ollama (local)" preset is missing from the chatbox provider
dropdown.
…er gates (#134)

* feat(loader): StreamingContext bridges chatbox-core turn events

Add a dedicated StreamingContext (Contexts.js) for the chatbox-driven
isStreaming flag. DashboardLoader.js owns the state, listens for the new
tethysdash:turn-start / tethysdash:turn-end window events from
@aquaveo/chatbox-core@>=0.16.0-beta.0, and provides via the new context.

isStreaming lives in its own useState — separate from
disabledEditingMovement — so the existing reset effect at lines 75-79
(which zeroes disabledEditingMovement when isEditing flips false) does
NOT zero isStreaming spuriously.

Dedicated context (rather than extending DisabledEditingMovementContext)
keeps the re-render footprint to DashboardItem consumers only. The
existing 4+ consumers of DisabledEditingMovementContext (Header,
DashboardLayout, PopupLayoutEditor, DashboardItem) do NOT re-render on
every turn boundary — addresses the fan-out concern flagged in the
brainstorm/plan reviews.

Test additions:
- DashboardLoader.streaming.test.js (6 scenarios): initial-mount default
  false, turn-start flips true, turn-end flips back, idempotent duplicate
  starts, independence from DisabledEditingMovementContext, listener
  cleanup on unmount.
- customRender.js adds StreamingPComponent helper for asserting context
  value in tests.

Phase 2 of docs/plans/2026-05-28-002-feat-chatbox-core-incremental-tile-streaming-plan.md
Unit 6.

* feat(dashboard): gate per-tile edit/delete/reorder during chatbox streaming

DashboardItem reads isStreaming from the new StreamingContext (Unit 6) and
short-circuits the three config-mutating handlers when true:

- editGridItem: guard fires before setShowDataViewerModal — the edit modal
  does not open at all while the chatbox is mid-turn.
- deleteGridItem: guard fires BEFORE confirm() — the delete confirmation
  modal does not appear (rather than appearing and then no-op'ing on
  acceptance, which would be confusing UX).
- updateGridItemOrder: guard fires before mutating gridItems. Covers the
  per-tile reorder affordances (Bring to Front / Forward / Send to Back /
  Backward in the dropdown) which all delegate here. The react-grid-layout
  drag-to-reorder gesture is NOT routed through this function, so move /
  resize / drag remain enabled per R6.

DashboardItemDropdown propagates isStreaming to its menu items:
- Edit / Delete / Order entries: render with the Bootstrap `disabled` prop
  (rendered-but-disabled, NOT hidden — keeps affordance location stable)
  plus a `title` tooltip ("Editing disabled while dashboard is updating").
- Copy / Export remain enabled — they don't mutate tile config and don't
  conflict with chatbox patch_visualization.

Open edit modals at stream start are NOT auto-closed (accepted v1 UX cost
per Scope Boundaries).

Tests: DashboardItem.streaming.test.js (5 scenarios) covers the disabled-
class + tooltip + handler no-op contract for Edit and Delete, the enabled
state of Copy/Export under streaming, the false→true→false transition, and
normal (non-streaming) baseline behavior.

Note on a11y: react-bootstrap may or may not apply `aria-disabled='true'`
depending on version; the `disabled` class is the jsdom-stable assertion.
Production a11y verification (screen reader announces "Edit, disabled"
with the documented reason) is worth checking after PR lands.

Phase 2 of docs/plans/2026-05-28-002-feat-chatbox-core-incremental-tile-streaming-plan.md
Unit 7.

* chore: bump @aquaveo/chatbox-core 0.15.3 → 0.16.0-beta.0 + rebuild bundle

Devcontainer Dockerfile sed-swap updated so the multi-stage build pulls
the new beta from npm. The workshop image (built from this Dockerfile)
will pick up the new onToolEnvelope engine option and the two new
tethysdash:turn-start / tethysdash:turn-end window events that Phase 2
listens for.

React bundle rebuilt against the current chatbox-core file: link (which
itself is at 0.16.0-beta.0). The committed bundle keeps the
non-Docker / dev-server / Jest paths in sync with the source. The
Docker multi-stage build overwrites this bundle at image-build time
anyway — both paths produce the same code now.
…uild bundle (#135)

Picks up chatbox-core PR #46 (available_cache_uris hint on error envelopes).
When an MCP tool returns {error: ...}, the engine now appends
available_cache_uris: [{tool_name, cache_uri}, ...] so small models don't
re-query NRDS after a chart-tool failure — the URI is in immediate context.

Files:
- .devcontainer/Dockerfile sed-swap pin: 0.16.0-beta.0 → 0.16.1-beta.0.
- React bundle rebuilt: main.215c... → main.2314ae...
- manifest.json updated.
@romer8
romer8 marked this pull request as draft July 10, 2026 16:56
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.

1 participant