lexsi-ds-agent / tests /test_schema_linking.py
bp-lexsi's picture
Add table finding tools
f619a51
Raw
History Blame Contribute Delete
3.71 kB
"""Tests for schema linking — TableIndex, find_relevant_tables, and the
text_to_sql safety cap. Provider-agnostic: assertions hold under both the
sentence-transformer and lexical-fallback embedding providers.
"""
from __future__ import annotations
from pathlib import Path
from lexsi_ds.agent.context import AgentContext, ColumnInfo, DatasetHandle, TableInfo
from lexsi_ds.agent.table_index import TableIndex
from lexsi_ds.agent.tools.find_relevant_tables import FindRelevantTablesArgs, _run
from lexsi_ds.agent.tools.text_to_sql import _render_schema
def _tbl(name, cols, comment=""):
return TableInfo(name=name, columns=[ColumnInfo(c, "VARCHAR") for c in cols],
n_rows=10, comment=comment)
def _handle(tables, ds_id="syn"):
return DatasetHandle(id=ds_id, kind="attached", duckdb_path=Path("/tmp/none.duckdb"),
tables=tables)
def _ctx(tables, ds_id="syn"):
return AgentContext(dataset=_handle(tables, ds_id), run_id="t")
_THREE = [
_tbl("loan_book", ["loan_id", "principal", "interest", "default_flag"], "loans issued to customers"),
_tbl("weather_daily", ["date", "temperature", "rainfall"], "daily weather readings"),
_tbl("customer_profile", ["customer_id", "age", "city"], "customer demographics"),
]
def test_table_index_ranks_relevant_first():
idx = TableIndex.build(_THREE)
hits = idx.search("average principal of loans that defaulted", top_k=3)
assert hits, "expected at least one hit"
assert hits[0][0].name == "loan_book"
def test_table_index_empty_query():
assert TableIndex.build(_THREE).search(" ") == []
def test_find_relevant_tables_tool():
ctx = _ctx(_THREE)
res = _run(FindRelevantTablesArgs(question="loan principal default", top_k=2), ctx)
assert res.ok
assert res.payload["total_tables"] == 3
assert len(res.payload["tables"]) <= 2
assert "loan_book" in res.payload["tables"]
def test_find_relevant_tables_no_tables():
res = _run(FindRelevantTablesArgs(question="x"), _ctx([]))
assert not res.ok and res.error == "no_tables"
def test_index_cached_on_ctx():
ctx = _ctx(_THREE)
_run(FindRelevantTablesArgs(question="loans"), ctx)
assert "table_index:syn" in ctx.cache
# ---- text_to_sql safety cap ----
def _many(n):
tables = [_tbl(f"t{i:02d}", ["col_a", "col_b"]) for i in range(n)]
tables.append(_tbl("sales_revenue", ["revenue", "region", "quarter"], "sales revenue by region"))
return tables
def _count_rendered(schema: str) -> int:
return schema.count("### TABLE")
def test_small_schema_renders_all_no_cap():
schema = _render_schema(_ctx(_THREE), question="loans")
assert _count_rendered(schema) == 3
assert "auto-selected" not in schema
def test_large_schema_auto_capped():
ctx = _ctx(_many(50), ds_id="big")
schema = _render_schema(ctx, question="total revenue by region")
n = _count_rendered(schema)
assert n <= 21 and n < 51 # capped well below the 51 total
assert "sales_revenue" in schema # the relevant table survived the cap
assert "auto-selected" in schema # cap note present
def test_explicit_tables_subset_scopes_exactly():
ctx = _ctx(_many(50), ds_id="big2")
schema = _render_schema(ctx, question="anything", tables=["t00", "sales_revenue"])
assert _count_rendered(schema) == 2
assert "sales_revenue" in schema and "### TABLE `t00`" in schema
def test_bad_explicit_subset_falls_back():
# nonexistent names → don't render an empty schema; fall back gracefully
ctx = _ctx(_THREE, ds_id="fb")
schema = _render_schema(ctx, question="loans", tables=["does_not_exist"])
assert _count_rendered(schema) == 3