Skip to content

Python semantic layer: Injected marker and the @measure decorator - #247

Open
jat255 wants to merge 6 commits into
mainfrom
jat255/wwmt-measure-basics
Open

Python semantic layer: Injected marker and the @measure decorator#247
jat255 wants to merge 6 commits into
mainfrom
jat255/wwmt-measure-basics

Conversation

@jat255

@jat255 jat255 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

First of four stacked PRs building the Python semantic layer (M3). This one adds the measure record and the rule that tells the two kinds of measure argument apart.

These are implemented in pkg-py/src/commons/_measures.py and corresponding tests.

Two types of measures:

  • The model supplies parameters annotated as Annotated[T, Field(description=...)]
  • commons supplies parameters annotated Injected[T] and they never reach the model.

A parameter that is neither raises TypeError at decoration time. This came from decision D9, and it is stricter than the R package, where a forgotten @param silently hides an argument from the model.

Some API notes

@measure returns the function unchanged and attaches the record as an attribute, so measures and the helpers they call stay ordinary callables.

The schema is not built through chatlas, because Tool.from_func() rejects any model whose fields do not cover every function parameter, which won't work on injected parameters. Measures are never registered with the provider in any case, since the provider sees only call_measure.

Argument validation uses pydantic's model_validate on an extra="forbid" model. That covers unknown arguments, required arguments, enum vocabularies, and coercion in one call, so there is no need for a hand-written validator. Its error text differs from the R package's, which is accepted because the text goes back to the model as a tool error rather than to a user.

One caveat worth noting: An Annotated Field carrying default= or default_factory= is a decoration error, and the default belongs in the signature instead.

For example, the following will not work:

@measure
def discount_rate(
    order_id: Annotated[str, Field(description="The order to price.")],
    rate: Annotated[float, Field(description="Discount fraction.", default=0.1)],
) -> float:
    ...

validate_args omits unset arguments so that Python applies the default at call time. Accepting a Field default would make the schema optional while the signature still required the argument, and the call would then fail mid-conversation.

Not in this PR: semantic_layer(), injection resolution, and the public exports. Those are the PRs above it in the stack.

…otated_attribute

Replaced deprecated pydantic API with the supported path for building FieldInfo
from annotations with defaults. This also merges all field constraints (e.g.
Field(gt=0)) rather than silently dropping them. Added test to verify constraint
merging.
FieldInfo.from_annotated_attribute overwrites a Field's default= with
PydanticUndefined whenever the signature omits its own default, silently
making the parameter required in the schema while Python's call convention
still requires it. Raise TypeError at decoration time instead, naming the
parameter and telling the author to move the default into the signature.
Route direct as_measure() dereferences through a shared _as_measure()
helper so pyrefly sees Measure instead of Measure | None.
The fail-closed check in 75914f5 only inspected the FieldInfo that
_described_field() returns, the one carrying the description. An
annotation can carry more than one Field(...), and a default declared
in a separate one slipped through, producing a required schema field
whose default Python never applies. Scan all FieldInfo metadata on the
annotation instead of just the described one.
@jat255 jat255 changed the title jat255/wwmt measure basics Python semantic layer: Injected marker and the @measure decorator Sep 2, 2026
@jat255 jat255 added the py Affects the Python implementation label Sep 2, 2026
@jat255
jat255 force-pushed the jat255/wwmt-measure-basics branch from 064a319 to 52af05b Compare September 2, 2026 03:14
@jat255
jat255 marked this pull request as ready for review September 2, 2026 04:00
@jat255
jat255 force-pushed the jat255/wwmt-measure-basics branch from 52af05b to 064a319 Compare September 2, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant