Skip to content

Discovery surface: report searchable columns, and make multi-database tool sets distinguishable #40

Description

@rohan-hotdata

Extends hotdata_langchain/schema.py (hotdata_describe_tables) to report which columns are searchable, and makes multi-database setups distinguishable to the model.

1. Report which columns are indexed

hotdata_describe_tables currently returns tables, columns and types from information_schema. It cannot say which columns are searchable — and that single missing fact is why hotdata_search_text has to pin its corpus at construction rather than letting the agent choose one.

Newly unblocked: this needs no engine change. Indexes are not visible in SQL —

SELECT * FROM pg_indexes                    -> table 'default.public.pg_indexes' not found
SELECT * FROM information_schema.indexes    -> table 'default.information_schema.indexes' not found

— but the control plane returns them, reachable with what the client already holds:

db  = client.resolve_managed_database(DATABASE)
idx = hotdata.IndexesApi(client.api).list_indexes(db.default_connection_id, "public", "listings")
# -> [('listings_description_bm25', 'bm25', ['description'], IndexStatus.READY)]

So the tool can annotate each column with whether it is searchable and by what kind of index, without waiting on runtimedb.

Design notes:

  • Report the capability, not the mechanism — "searchable by text relevance" rather than "has a BM25 index" — consistent with the naming rule enforced in tests/test_search.py. The raw index type can still appear as detail for humans debugging.
  • Only surface READY indexes as usable; a PENDING index will fail a search.
  • One list_indexes call per table, so the overview (no-argument) call should probably stay index-free and the per-table drill-down carry the annotation, to avoid N API calls for a wide database.

What this unlocks: once an agent can ask what is searchable, hotdata_search_text no longer needs a pinned corpus — the constrained-enum or agent-supplied-table variants become safe, because the agent picks from something it actually read rather than a guess. That is the discovery gap noted when the pinning decision was made.

2. Multi-database description clarity

One client can query many databases — execute_sql(sql, database=...) takes the scope per call, and the same client read from two different managed databases in one session. So registering several tool sets already works:

sales   = hl.make_hotdata_tools(client, database="<id>", search_tool_name="search_sales")
support = hl.make_hotdata_tools(client, database="<id>", search_tool_name="search_support")
agent   = create_agent(model=..., tools=[*sales, *support])

But the tool descriptions never mention which database each set addresses, so with two sets the model cannot tell them apart — and hotdata_execute_sql appears twice with identical text. Descriptions should name their database (and make_hotdata_tools likely needs a name suffix or prefix option so the SQL/describe tools do not collide either).

Cross-database references inside a single query do not work by default (table 'f1_db.public.drivers' not found from another database's scope). DatabasesApi.attach_database_catalog looks like the supported route — unverified, see docs/engine-contract.md.

Related

Activity

rohan-hotdata commented on Jul 27, 2026

@rohan-hotdata
ContributorAuthor

Additional item, from review on #43: the schema overview has no row cap while the column list does.

DEFAULT_MAX_COLUMNS exists so one wide table cannot flood the context, but the no-argument path — the one an agent calls first, unprompted — returns a row per table with no bound. A database with a few thousand tables floods exactly the context the other branch protects.

Fix is a LIMIT on table_overview_sql() plus a truncated_at in the payload, mirroring what the per-table branch already does. Left out of #43 to keep that PR scoped; it belongs with this issue since both are about what the agent can learn about a workspace.

rohan-hotdata commented on Aug 13, 2026

@rohan-hotdata
ContributorAuthor

A concrete case for part 1, found 2026-08-13.

sql_tool_description's bm25_search paragraph is unconditional — only the example varies on
search_table/search_column. So an agent whose database has no BM25 index anywhere is still
told to "prefer this whenever the answer aggregates over the matches", for a function that would
hard-error, BM25 having no brute-force fallback.

Measured once: an agent with SQL and schema tools only, over a span table with no index, asked
"Which span names contain 'chart-data'? Use text ranking if you can." — it wrote
name ILIKE '%chart-data%' and did not reach for the TVF. One sample, against a prompt that
never mentions BM25, so the risk is unconfirmed rather than disproved.

Deliberately not "fixed" by conditioning the paragraph on a registered corpus, because that
would remove the composable route from every caller who has an index but did not pass
search_table — trading a hypothetical error for a certain loss of capability, on one sample.

Part 1 of this issue is the actual fix: once the agent can ask what is searchable, the
description can point at discovery rather than assert availability. Noting it here so the
motivation is recorded rather than rediscovered.

rohan-hotdata commented on Aug 18, 2026

@rohan-hotdata
ContributorAuthor

Measured: a provider-backed index adds a column that describe reports as ordinary data

Found while shipping #39's semantic search. This makes part 1 of this issue worth more than
tidy-up, and the machinery for it now exists.

Building a provider-backed vector index — one created with embedding_provider_id, over a
text column — materialises a generated vector column beside the source. Indexing content
produced content_embedding (List(Float32), 1536 wide), and it appears in
information_schema like any other column, so hotdata_describe_tables reports it with a
non_null count and nothing to distinguish it from data someone loaded.

Verified in front of an agent (gpt-5.1, one-sentence system prompt, 500-row corpus). Asked what
columns the dataset has and which are worth analysing, it did not misunderstand the column
— it inferred correctly from the name and type that this was the vector representation used for
semantic search. It then recommended work that cannot be done here:

content_embedding (List(Float32), 500 non-null) — Vector representation of content
used for semantic search. […] Essential for similarity search and clustering. You'd analyze
it indirectly: clustering listings (e.g. "peaceful/green" vs "downtown/urban") or visualizing
in 2D via dimensionality reduction.

and carried it into the closing "you'll get the most value from" set. Neither clustering nor
dimensionality reduction is reachable through these tools or the engine, so this is an
unbuildable suggestion offered with the same confidence as the real columns — not a wrong
answer, but a real cost, and a user takes it at face value.

The secondary risk is untested but obvious: nothing stops a model writing
SELECT content_embedding FROM …, which returns 1536 floats per row.

What this changes about part 1

The filter needs no new API call beyond the one this issue already proposes.
hotdata_langchain/indexes.py (added in #39) already has it:

  • generated_vector_columns(indexes) yields exactly these columns, and
  • SearchIndex.vector_column carries the name for each provider-backed index

So "report which columns are searchable" and "hide the columns the index generated" are the
same list_indexes call, and part 1 should cover both. The note in this issue about keeping
the no-argument overview index-free still holds — this belongs on the per-table drill-down,
which is also where the column actually shows up.

One wrinkle worth deciding when we pick this up: whether a generated column is hidden or
labelled. Hiding it makes the table read as it was loaded, which is what a user means by
"what columns does this have". Labelling it ("generated by the index that makes content
searchable by meaning") is more honest about what is in storage. I lean hidden for the model-
facing output, since the agent has no use for a column it cannot compute over.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-native-layerPart of the AI-native query layer effort for LangChainenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions