sofhiaazzhr Claude Opus 4.8 commited on
Commit
ef0c7b1
·
1 Parent(s): 32abc41

check: table-level column drill + drop misleading "name a table" hint

Browse files

check could only drill to the SOURCE level: "what columns are in badactor" (a
table in a multi-table DB) returned the whole-DB table list and ignored the
named table. And a >3-table DB rendered a "Name a table to see its columns"
hint that nothing honored — check is stateless, so the follow-up (e.g. bare
"pareto") is re-classified by the intent router (and "pareto" reads as a Pareto
analysis) into structured_flow, giving an analytical answer instead of columns.

- _filter_to_named_tables narrows drilled schemas to the table(s) named in the
message (whole-word match), dropping sources with none of them, so
"columns in <table>" answers with that table's columns — even for a DB that
would otherwise be summarised.
- Remove the "Name a table..." hint (db_hint) that promised a follow-up the
handler can't reliably serve.
- Factor the whole-word matcher into _name_in_message, reused for source + tables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (1) hide show
  1. src/agents/handlers/check.py +57 -8
src/agents/handlers/check.py CHANGED
@@ -278,6 +278,18 @@ async def _fetch_db_tables(
278
  }
279
 
280
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  def _matched_source_ids(message: str, inventory: ToolOutput) -> list[str]:
282
  """All source_ids whose name appears as a whole word in the message.
283
 
@@ -285,9 +297,9 @@ def _matched_source_ids(message: str, inventory: ToolOutput) -> list[str]:
285
  the tool needs exact `source_id`s. We resolve them against the inventory
286
  rows (kind="table", columns include "source_id" + "name") instead of an LLM
287
  — a cheap match against catalog metadata already in hand. Whole-word match
288
- (`\\b`) avoids nuisance hits ("orders" inside "reorders") and treats `_` as
289
- part of the word, so "sales" won't pick up "sales_archive". Multiple named
290
- sources all match, so the caller can show each schema.
291
  """
292
  if inventory.kind != "table" or not inventory.rows:
293
  return []
@@ -301,7 +313,7 @@ def _matched_source_ids(message: str, inventory: ToolOutput) -> list[str]:
301
  matched: list[str] = []
302
  for row in inventory.rows:
303
  name = str(row[name_idx])
304
- if name and re.search(rf"\b{re.escape(name)}\b", message, re.IGNORECASE):
305
  matched.append(str(row[id_idx]))
306
  return matched
307
 
@@ -370,7 +382,6 @@ _SCHEMA_STR = {
370
  "yes": "Yes", "no": "—",
371
  "db_tables": "{name} has {n} tables:",
372
  "db_item": "- {table} ({cols} columns{rows})",
373
- "db_hint": "Name a table to see its columns.",
374
  },
375
  "Indonesian": {
376
  "lead": "Berikut kolom dan tipe data di {n} sumber yang kamu punya:",
@@ -383,7 +394,6 @@ _SCHEMA_STR = {
383
  "yes": "Ya", "no": "—",
384
  "db_tables": "{name} punya {n} tabel:",
385
  "db_item": "- {table} ({cols} kolom{rows})",
386
- "db_hint": "Sebut nama tabelnya untuk lihat kolomnya.",
387
  },
388
  }
389
 
@@ -542,7 +552,6 @@ def _render_schema_source(out: ToolOutput, reply_language: str) -> str:
542
  lines.append(
543
  sc["db_item"].format(table=tname, cols=len(tcols), rows=_rows_suffix(rc))
544
  )
545
- lines.append(sc["db_hint"])
546
  return "\n".join(lines)
547
 
548
  parts: list[str] = []
@@ -580,6 +589,41 @@ def _render_schemas(schemas: list[ToolOutput], reply_language: str) -> str:
580
  return lead + "\n\n" + "\n\n".join(blocks)
581
 
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  async def run_check(
584
  message: str, invoker: ToolInvoker, reply_language: str = "English"
585
  ) -> str:
@@ -618,7 +662,12 @@ async def run_check(
618
  schemas = await asyncio.gather(
619
  *(invoker.invoke("check_data", {"source_id": sid}) for sid in ids)
620
  )
621
- return _render_schemas(schemas, reply_language) or _no_match
 
 
 
 
 
622
 
623
  if intent == "data":
624
  inventory = await invoker.invoke("check_data", {})
 
278
  }
279
 
280
 
281
+ def _name_in_message(name: str, message: str) -> bool:
282
+ """Whole-word, case-insensitive match of `name` in `message`.
283
+
284
+ `\\b` treats `_` as a word char, so "sales" won't hit "sales_archive" and the
285
+ short table name "pa" won't hit "pareto" — the same rule used for source names,
286
+ reused here for table names.
287
+ """
288
+ return bool(name) and (
289
+ re.search(rf"\b{re.escape(name)}\b", message, re.IGNORECASE) is not None
290
+ )
291
+
292
+
293
  def _matched_source_ids(message: str, inventory: ToolOutput) -> list[str]:
294
  """All source_ids whose name appears as a whole word in the message.
295
 
 
297
  the tool needs exact `source_id`s. We resolve them against the inventory
298
  rows (kind="table", columns include "source_id" + "name") instead of an LLM
299
  — a cheap match against catalog metadata already in hand. Whole-word match
300
+ avoids nuisance hits ("orders" inside "reorders") and treats `_` as part of
301
+ the word, so "sales" won't pick up "sales_archive". Multiple named sources all
302
+ match, so the caller can show each schema.
303
  """
304
  if inventory.kind != "table" or not inventory.rows:
305
  return []
 
313
  matched: list[str] = []
314
  for row in inventory.rows:
315
  name = str(row[name_idx])
316
+ if _name_in_message(name, message):
317
  matched.append(str(row[id_idx]))
318
  return matched
319
 
 
382
  "yes": "Yes", "no": "—",
383
  "db_tables": "{name} has {n} tables:",
384
  "db_item": "- {table} ({cols} columns{rows})",
 
385
  },
386
  "Indonesian": {
387
  "lead": "Berikut kolom dan tipe data di {n} sumber yang kamu punya:",
 
394
  "yes": "Ya", "no": "—",
395
  "db_tables": "{name} punya {n} tabel:",
396
  "db_item": "- {table} ({cols} kolom{rows})",
 
397
  },
398
  }
399
 
 
552
  lines.append(
553
  sc["db_item"].format(table=tname, cols=len(tcols), rows=_rows_suffix(rc))
554
  )
 
555
  return "\n".join(lines)
556
 
557
  parts: list[str] = []
 
589
  return lead + "\n\n" + "\n\n".join(blocks)
590
 
591
 
592
+ def _filter_to_named_tables(
593
+ schemas: list[ToolOutput], message: str
594
+ ) -> list[ToolOutput] | None:
595
+ """Narrow drilled schemas to the specific table(s) named in the message.
596
+
597
+ Lets "what columns are in badactor" answer with badactor's columns instead of
598
+ the whole-DB table list. When the message names table(s) present in a source,
599
+ that source is narrowed to just those tables; sources with none of the named
600
+ tables are dropped, so a table-specific question doesn't dump every other
601
+ source's schema. Returns None when the message names no table at all, so the
602
+ caller keeps the default whole-source view. Table names are matched
603
+ whole-word (same rule as source names) against each drilled schema's rows.
604
+ """
605
+ narrowed: list[ToolOutput] = []
606
+ any_named = False
607
+ for out in schemas:
608
+ if out.kind != "table":
609
+ continue
610
+ cols = out.columns or []
611
+ if "table_name" not in cols:
612
+ continue
613
+ tn_idx = cols.index("table_name")
614
+ keep = {
615
+ str(r[tn_idx])
616
+ for r in (out.rows or [])
617
+ if _name_in_message(str(r[tn_idx]), message)
618
+ }
619
+ if not keep:
620
+ continue
621
+ any_named = True
622
+ rows = [r for r in (out.rows or []) if str(r[tn_idx]) in keep]
623
+ narrowed.append(out.model_copy(update={"rows": rows}))
624
+ return narrowed if any_named else None
625
+
626
+
627
  async def run_check(
628
  message: str, invoker: ToolInvoker, reply_language: str = "English"
629
  ) -> str:
 
662
  schemas = await asyncio.gather(
663
  *(invoker.invoke("check_data", {"source_id": sid}) for sid in ids)
664
  )
665
+ # Table-level drill: if the message names specific table(s) e.g. "what
666
+ # columns are in badactor" — narrow to those so we answer with that table's
667
+ # columns instead of a whole-DB table list. Falls back to the full view when
668
+ # no table is named.
669
+ narrowed = _filter_to_named_tables(schemas, message)
670
+ return _render_schemas(narrowed or schemas, reply_language) or _no_match
671
 
672
  if intent == "data":
673
  inventory = await invoker.invoke("check_data", {})