Skip to content

feat(py): build_commons_tools() and the tool bodies - #310

Merged
jat255 merged 13 commits into
jat255/6fv1-prompt-datafrom
jat255/hsjq-tools
Sep 8, 2026
Merged

feat(py): build_commons_tools() and the tool bodies#310
jat255 merged 13 commits into
jat255/6fv1-prompt-datafrom
jat255/hsjq-tools

Conversation

@jat255

@jat255 jat255 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Registers the tools an agent can reach and implements their bodies, which return plain text. pvrd (#311) builds a ToolContext, calls build_commons_tools(), and registers each tool it returns on its chat client.

Implementation

  • Registration. Seven tools, each behind a predicate: search_pool, call_measure, call_metrics, search_catalog, search_context, describe_table, run_sql. R's eighth, call_calculation, and the semantic-model half of call_metrics both read a warehouse's own semantic objects, which is deferred post-conf. run_python belongs to M6 and will be added later.
  • First-touch tracker. Shared by describe_table and run_sql: a table's dictionary entry goes in front of the model the first time either tool reaches it, keyed on the resolved source label so a table reachable under two names is not delivered twice.
  • Sample rows are summarized column by column (_sample_summary.py), as R does, rather than printed. Ranges render to four significant digits the way R's format() does, because the wording around a range is shared and the numbers inside it have to agree.
  • Result rendering speaks the shared vocabulary (_rows.py): a null reads NA, a boolean TRUE/FALSE, a float seven significant digits. Three knitr::kable behaviors are deliberately not matched — No rows. instead of a header-only empty table, line breaks folded to spaces, \| rather than | — and the module docstring records why. A measure returning a ContentToolResult is returned untouched; a list past twenty items is capped and reported.
  • call_measure loads a board source's pins before invoking a measure, as R does: the measure holds a connection, not the DataSource, so nothing else would trigger the read.

Tests

  • tool-registration.json pins which tools each composition registers and what each tells the model. Arguments are not pinned: a JSON Schema property and an ellmer type are different objects, and normalizing one into the other would pin plumbing. Two asymmetries are documented in the fixture — no case declares warehouse semantic objects, so those tools register in neither language, and each runner drops the per-language execution-tool names before comparing.
  • sample-summary.json pins the summary's wording but not the type token (R names R's types, Python names Python's), via a {type} placeholder per column. One shape is deliberately left out — an all-null string/boolean column — and the fixture says so.
  • r-format-signif.json holds 330 R-generated values, since R is not available to the Python suite.

Verified: ruff check, pyrefly check src tests, pytest (1235 passing), and both R runners (39 and 16 passing).

R changes

@simonpcouch

No R source or behavior changed. R gains two fixture runners — test-tool-registration.R and test-sample-summary.R — and the synced fixture copies. Both runners read R's existing behavior and compare it against the fixtures; no hand-written R tests were deleted.

Evidence: both R test files pass under the framework R with pkgload::load_all(). The full R suite was not run.

@jat255 jat255 added this to the py-M5: agent, tools, and prompt milestone Sep 7, 2026
@jat255 jat255 added r Affects the R implementation py Affects the Python implementation needs-manual-review Agent-created work that needs a human review labels Sep 7, 2026
@jat255
jat255 changed the base branch from jat255/g9yd-agent-helpers to jat255/6fv1-prompt-data September 7, 2026 20:57
The tools that return data all need the same three things, so they land
before the tools do: rows rendered as a capped pipe table, a backend that
can report a relation's columns when no warehouse listing already carries
them, and a dictionary that merges those live columns with what it
documents about them.

`sqlglot_dialect()` comes out of the SQL guard's privates, because
compiling a governed query needs the same backend-to-sqlglot mapping to
quote an identifier or a literal for the right dialect.
One discovery surface over measures and governed definitions, ranked
together so the model does not have to guess which kind holds its answer,
plus the metrics-by-dimensions-with-filters query the pool's metrics are
run through.

Identifiers and literals are written by sqlglot for the source's own
dialect rather than by hand, so an apostrophe in a predicate value cannot
end the literal early, and a backend that spells identifiers with
backticks gets those.
Each tool is registered only when the agent's composition earns it:
search_pool once the pool holds something the prompt does not already
show, call_measure with measures, call_metrics with a governed metric,
search_catalog with a catalog too broad to list. search_context,
describe_table, and run_sql come with any source.

describe_table and run_sql share the first-touch tracker, so a table's
dictionary entry is put in front of the model once however the model
first reached it.

The result is a list of chatlas tools rather than a mutated client, so the
surface can be built and read before there is a conversation to attach it
to.

call_calculation is left out and call_metrics covers governed definitions
alone: both of the missing halves read a warehouse's own semantic objects,
which this package does not have yet.
tool-registration.json declares a composition and the tools it earns, plus
the description each registered tool gives the model. Both are prompt
surface: registration is what the model can reach, and a description is
what it reads before choosing.

Two things the file deliberately leaves out. Tool arguments, because a
JSON Schema property and an ellmer type are different objects and
normalizing one into the other would pin the plumbing rather than what the
model reads. And the execution tool, because it is run_r in one package
and run_python in the other and each is built by its own owner, so a
runner drops both names before comparing.

No case declares a warehouse's own semantic objects or exact trusted
queries, so call_calculation and the semantic half of call_metrics never
register in either package. That is the fixture staying inside what both
implement while gcgj is deferred, and the file says so.
A refusal is an answer, so a relation the principal may not query is left
out of a catalog search. A timeout or an unreadable failure is not an
answer, and treating it as one reports a relation the agent does have as
one it does not. pkg-r/R/catalog.R draws the line in the same place.
search_pool and call_metrics were registered but never run, and
describe_table over a pins board leans on the sample query having loaded
the pin before the column read that follows it.
A board source reads a pin when a query names it, and that recovery lives
on DataSource.query(). A measure is handed the connection itself, so
nothing there fires it: a board-backed measure failed on its first call
with a catalog error for a relation that had not been written yet.

call_measure now ensures each injected source has read its pins before
invoking the measure, which is the step pkg-r/R/tools.R takes through
source_ensure_all() for the same reason. DataSource.ensure_loaded() is the
eager entry point, driving the same _load_pins() the query path uses
rather than a second copy of it.
Five rows of values tell a model less about a column than its range, its
missing count, and the values it actually takes, so describe_table now
shows the same column-by-column summary the R agent gets from ellmer's
df_schema(): the same header line, the same facts per kind of column, in
the same order, with the same punctuation. A table whose sample came back
empty is still described column by column, because the schema fixes the
columns rather than the rows.

The type token in front of those facts is the one word the two packages do
not share. R names R's types because it holds an R vector; this names
Python's, because a row here is a mapping of Python values and calling an
int an integer would describe a vector nobody has. The mapping is written
out at the top of _sample_summary.py.

sample-summary.json pins the wording and leaves the token to each package,
so a case carries a {type} placeholder and the word each side puts there.
The one shape it cannot pin is an all-null column whose R type still
carries facts of its own, and it says so.

Ranges are rendered to four significant digits the way R's format() does,
fixed unless scientific is shorter, since the wording around a range is
shared and the numbers inside it have to agree. Checked against 330 values
read off R.
Two defects in the numeric half of the sample summary.

Every value went through float before it was rendered, so a 2147483647 id
came back as 2.147e+09 where R writes it out, and a DECIMAL column was
rounded to a double's precision before anything asked it to be. A whole
number is now written in full however wide it is, and a Decimal is
formatted as itself, with its exponent padded so the same magnitude reads
alike from a DECIMAL and a DOUBLE column.

A NaN was an ordinary value, so min() returned it or not depending on
which row the query happened to return first, and the same table could
summarize two ways. R counts a NaN as NA and leaves it out of the range;
so does this now.

Both are shared behaviour, so both earn a case in sample-summary.json,
which needed a way to spell a not-a-number that JSON has no literal for.
math.isnan() refuses to convert a signalling NaN to a float and raises, so
a DECIMAL column holding one would have thrown out of describe_table
instead of counting it as missing. Decimal answers for itself.

Also guards a column mixing an aware timestamp with a naive one, which
Python will not order and which would otherwise raise from the range: no
driver returns both from one column, so it is reported as mixed.

The fixture's note on how it spells a not-a-number now stands on its own
sentence rather than pointing back at the paragraph before it.
Python calls a timestamp naive when utcoffset() answers None, which a
tzinfo can do while still being attached and still naming a zone. Deciding
on tzinfo alone let such a value count as aware, so it could be compared
against a genuinely aware one and raise from the range it was meant to
guard, and it could put a zone in front of facts about a naive column.

One offset answer for the whole column now decides both.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Preview deployed to Connect (dogfood.team.pct.posit.it): https://dogfood.team.pct.posit.it/connect/#/apps/d7a36cae-8f27-448b-a478-61b81fbe3942/draft/368844

Deployed from commit acba79f.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Preview deployed to Connect (connect.staging.pct.posit.it): https://connect.staging.pct.posit.it/connect/#/apps/ad662e1b-5048-4acc-9ad7-f9478c92274e/draft/2688

Deployed from commit acba79f.

- A null in a result table reads NA, a boolean TRUE or FALSE, and a
  float keeps seven significant digits, so the model meets one spelling
  per idea across the tools and both packages. The three knitr::kable
  behaviors deliberately not matched are documented in _rows.py.
- A measure that returns a ContentToolResult is returned untouched, and
  a list result past twenty items is capped and reported rather than
  printed in full.
- Cover the warehouse describe dispatch and its column caching, and the
  measure-argument parsing branches (a mapping passed through, a
  non-object rejected).
- Stop claiming the pool's sibling scoping matches R; it is deliberate.
@jat255 jat255 removed the needs-manual-review Agent-created work that needs a human review label Sep 8, 2026
@jat255
jat255 requested a review from simonpcouch September 8, 2026 05:16
@jat255
jat255 marked this pull request as ready for review September 8, 2026 05:16
@jat255
jat255 merged commit 12740bd into main Sep 8, 2026
13 checks passed
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Cleaned up 2 preview bundle(s) on https://dogfood.team.pct.posit.it: 368842, 368844

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Cleaned up 2 preview bundle(s) on https://connect.staging.pct.posit.it: 2686, 2688

@jat255
jat255 deleted the jat255/hsjq-tools branch September 8, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py Affects the Python implementation r Affects the R implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants