DataMate — Database Schema Selection Feature · Implementation Plan
Add a sidebar dropdown to pick a source-DB schema; DataMate then answers questions grounded in that schema's structure (writes correct SQL, explains tables/columns/relationships). Schema-only reasoning — no live row execution.
Date: 2026-06-10 · Scope: asktge (UI + backend) + datamate/agent + new S3 store
1. Locked Decisions
| Decision | Choice |
|---|---|
| Data access model | Schema-only reasoning — agent reads DDL, answers structure / generates SQL. No live query execution against these DBs. |
| Selection | Single select — one schema per conversation. |
| Schema delivery | S3 canonical store + agent-side cached loader + context injection (recommended below). Your S3 instinct was right; refined to fix the token-cost problem. |
The 6 files in asktge/DB_Schema_Files/ are MySQL/MariaDB DDL dumps (CREATE TABLE only, no rows): Goldhopper_DEV2, MYTEAMGE, Rockhopper_NSW, TAPS, TED-EDS, TED-RDS. They are source-system schemas, not the Redshift the agent's MCP tools already reach.
2. What Already Exists (reuse, don't rebuild)
Investigation found partial plumbing already in place:
asktge/routes/chat_routes.pyalready carries aselected_schemathrough the session and into the invoke flow. The SQLMatic branch already doessession["selected_schema"] = session.get("selectedSchema", "Goldhopper")— i.e. a schema-selection slot exists and already defaults toGoldhopper.asktge/utils/session_utils.py::get_agent_id_for_schema()andasktge/config/knowledge_bases.py::get_kb_id_for_schema()map a schema name → agent/KB.asktge/invoke_client.py::invoke_datamate_agent()posts to the DataMate AgentCore runtime (bedrock-agentcore…/runtimes/…datamate_agent-VHS3PoFgPB/invocations) with a Bearer (Azure AD JWT) and SSE handling.- The datamate agent already uses S3 (
tools/kb_search.py::_generate_presigned_url, boto3 S3) — so S3 read IAM + client are present. No new infra category. datamate/agent/api/models.py::InvokeRequestis the payload contract;ai/graphs.pybuilds the LangGraph and binds tools;ai/prompts.py::seed_system_prompt_withToolsis DataMate's system prompt.
Implication: we extend an existing selected_schema channel and add one payload field (databaseId) + one loader + a prompt block. We are not inventing the pathway.
3. Chosen Delivery Approach & Why
S3 canonical DDL + build-time condensation + agent-side cached loader + system-prompt injection, with an on-demand drill-down tool.
Why not the plain "S3 + read-tool" as first described: feeding a raw 9,000-line MySQL dump (MYTEAMGE) into context every turn is heavy and noisy (SET statements, charset clauses, ENGINE/AUTO_INCREMENT boilerplate). Two refinements fix it:
- Condense at upload time. A preprocessing step strips dump noise and emits a compact schema view per DB: tables → columns (name, type, nullable, PK/FK), plus foreign-key edges. ~9k DDL lines → roughly 1.5–3k tokens. Store both condensed and raw on S3.
- Inject condensed schema into the system prompt for the selected DB (deterministic — the agent always has structure, so "ask anything" reliably works), and expose a
get_table_ddl(databaseId, table)tool for the exact raw DDL of a specific table when precise constraints/types are needed. Use Bedrock prompt caching on the schema block to amortize tokens across turns.
This keeps requests tiny (only databaseId travels), lets schemas update without an agent redeploy (re-upload to S3), and bounds token cost.
S3: s3://<bucket>/datamate/schemas/<databaseId>/
raw.sql (original dump, for drill-down)
condensed.json (tables/columns/keys — injected)
manifest.json (display name, table count, updated_at)
4. Data Flow (target)
User opens DataMate ──► sidebar dropdown lists schemas (from /api/datamate/schemas)
│ picks "Goldhopper"
▼
chat.js stores selectedSchema ──► sends selectedSchema in /send_message body
▼
chat_routes.py: session["selected_schema"]=...; resolve databaseId
▼
invoke_client.invoke_datamate_agent(..., databaseId="goldhopper")
▼ payload += {"databaseId": "..."}
datamate InvokeRequest.databaseId ──► graph state
▼
schema_loader.load_condensed(databaseId) [S3 GET + TTL cache]
▼
system prompt = base + "<DB SCHEMA: Goldhopper>…condensed…</>" (prompt-cached)
▼
LLM answers / writes SQL. Needs exact DDL? → tool get_table_ddl(databaseId, table)
5. Implementation Phases
Phase 0 — Schema prep & S3 (foundation)
- Create S3 prefix
datamate/schemas/in the existing datamate bucket (same bucket as KB docs; reuse IAM). - Write
datamate/agent/scripts/build_schema_store.py: parse each.sqldump →condensed.json(tables, columns w/ type+nullable, PK, FK edges) +manifest.json; upload raw + condensed + manifest. Parser can usesqlparseor regex overCREATE TABLEblocks (dumps are uniformmysqldumpoutput). - Define a schema registry
datamate/agent/config/schemas.py:databaseId → {display_name, s3_prefix, enabled}. Seed the 6 DBs. This is the single source of truth the UI list is derived from. - One-time run: upload the 6 current files. (Move ownership of
DB_Schema_Files/here, or keep asktge copy as the editing source and add a CI step that re-runs the builder.)
Phase 1 — asktge UI (sidebar dropdown)
-
asktge/templates/chat.html: add a<select id="datamate-schema-select">block inside#chat-sidebar, rendered only whencurrentAgent === "DataMate". -
asktge/static/js/chat.js: on DataMate load,GET /api/datamate/schemas→ populate dropdown; persist choice toChatState.selectedSchema+localStorage; includeselectedSchemain the/send_messagerequest body (chat.js already trackscurrentAgent). -
asktge/static/css/agent_select.css(or chat.css): style the selector to match the sidebar. - UX: default to last-used (localStorage) else first enabled schema; show the active schema as a chip above the input.
Phase 2 — asktge backend (list endpoint + payload)
- New route (e.g.
utility_routes.pyor a smalldatamate_routes.py):GET /api/datamate/schemas→ returns[{databaseId, display_name}]from a config that mirrorsdatamate/agent/config/schemas.py(keep one canonical list; asktge can fetch it from the agent at startup or hold a static copy). -
chat_routes.py: in the DataMate branch, readselectedSchemafrom request → setsession["selected_schema"]→ resolve todatabaseId(slugify / map). Validate against the allowed list; reject unknown. -
invoke_client.py::invoke_datamate_agent(): adddatabase_idparam;payload["databaseId"] = database_id. - Pass
databaseIdthroughchat_service/ whereverinvoke_datamate_agentis called.
Phase 3 — datamate agent (ingest + load)
-
api/models.py::InvokeRequest: adddatabaseId: Optional[str] = None(+ optional validator againstconfig/schemas.py). -
app.py: readrequest.databaseId, put it in the LangGraph initial state (e.g.state["database_id"]). - New
datamate/agent/tools/schema_loader.py:load_condensed(database_id) -> dict— S3 GETcondensed.json,cachetools.TTLCache(e.g. 30 min) keyed by databaseId; reuse the existing boto3 S3 client fromkb_search.load_table_ddl(database_id, table) -> str— slice the table's block fromraw.sql(cache raw per DB).list_schemas() -> list— fromconfig/schemas.py(used by the asktge endpoint too, or exposed via a tiny agent route).
Phase 4 — prompt + tool wiring
-
ai/prompts.py: whenstate.get("database_id"), append a## Selected Database Schemablock built fromload_condensed(...), plus rules: "Answer using ONLY this schema; if a question needs another DB, say it isn't selected; for exact column constraints callget_table_ddl." Mark the block for prompt caching. -
ai/graphs.py: addget_table_ddl(databaseId, table)to the bound tool list (alongside the existingredshift_*/semantic_schema_*tools). Keep the liveredshift_*tools available but instruct the prompt that for the selected source-schema mode they are for reference only (schema-only reasoning — no execution expected). - Ensure
database_idsurvives across turns: persist it in session metadata (storage/dynamodb.py::_store_session_metadata) so restored conversations keep their schema.
6. Config / Registry (single source of truth)
datamate/agent/config/schemas.py:
SCHEMAS = {
"goldhopper": {"display_name": "Goldhopper (DEV2)", "s3_prefix": "datamate/schemas/goldhopper/", "enabled": True},
"myteamge": {"display_name": "MyTeamGE", "s3_prefix": "datamate/schemas/myteamge/", "enabled": True},
"rockhopper_nsw":{"display_name": "Rockhopper (NSW)", "s3_prefix": "datamate/schemas/rockhopper_nsw/","enabled": True},
"taps": {"display_name": "TAPS", "s3_prefix": "datamate/schemas/taps/", "enabled": True},
"ted_eds": {"display_name": "TED-EDS", "s3_prefix": "datamate/schemas/ted_eds/", "enabled": True},
"ted_rds": {"display_name": "TED-RDS", "s3_prefix": "datamate/schemas/ted_rds/", "enabled": True},
}
UI dropdown, asktge validation, and agent loader all derive from this. Adding a DB = add a row + run the builder upload.
7. Token / Performance
- Condensed schema ~1.5–3k tokens; inject once per turn, prompt-cache the block (Sonnet 4 on Bedrock supports caching) → near-zero marginal cost after first turn.
schema_loaderTTLCache avoids repeat S3 GETs within a session.- Drill-down
get_table_ddlkeeps the big raw DDL out of context unless a specific table is needed.
8. Security / Compliance
- Schemas may expose table/column names that are themselves sensitive (PII-revealing). Treat the S3 prefix as internal; agent role gets read-only
s3:GetObjectondatamate/schemas/*(extend the existing policy). - No credentials to the source DBs are involved (schema-only). If this later becomes live-query, that's a new auth+secrets workstream (the earlier "new DB connections" option).
- Keep schema selection server-validated against the registry — never let an arbitrary
databaseIdreach S3 path construction (prevent path traversal: map via registry, don't interpolate raw input).
9. Testing
- Unit:
build_schema_storeparser on each of the 6 dumps (table/column/FK counts vs. eyeball). - Unit:
schema_loadercache hit/miss, unknown databaseId → safe error. - Integration: invoke datamate with
databaseId="goldhopper"+ "list the tables and the key columns of X" → answer grounded in condensed schema;get_table_ddlreturns exact block. - E2E: UI dropdown → ask a structural question → correct, schema-scoped answer; switch schema mid-session → context updates.
- Negative: ask about a table from a non-selected DB → agent declines / notes it isn't selected.
10. Rollout
- Phase 0 offline (S3 + builder) — no user impact.
- Ship agent changes behind tolerance for missing
databaseId(absent → current generic behavior). - Ship asktge backend (sends
databaseId). - Ship UI dropdown last. Optionally feature-flag via the agent registry
enabledflags + aDATAMATE_SCHEMA_SELECTIONenv toggle.
11. Effort (rough)
| Phase | Effort |
|---|---|
| 0 S3 + builder + registry | 1–1.5 d |
| 1 UI dropdown | 0.5–1 d |
| 2 asktge backend | 0.5 d |
| 3 agent ingest + loader | 1 d |
| 4 prompt + tool | 0.5–1 d |
| Test + rollout | 1 d |
| Total | ~4.5–6 dev-days |
12. Open Items to Confirm
- Bucket: reuse the existing datamate KB bucket, or a dedicated one? (Plan assumes reuse.)
- Editing source of truth: keep
asktge/DB_Schema_Files/as the human-edited copy with a CI step that condenses+uploads, or move ownership into the datamate repo? Recommend: keep asktge copy as source, add a GitLab CI job runningbuild_schema_store.pyon change. - Redshift relationship: confirm these 6 source schemas are intentionally separate from the agent's live Redshift, so "schema-only" is the right long-term model and we won't surprise users who expect real numbers back.
- Naming: surface as "Database" or "Schema" in the UI? (Affects label only.)