Repository navigation
FEAT: tool argument matching, so ToolsCalled can check "send_email to=x" - #4150
WatchTree-19 wants to merge 1 commit into
Conversation
ToolCallRequirement gains optional arguments: each ToolArgumentRequirement names one exact top-level argument and compares its value with a TextMatcher, so conditions compose matchers as Rule 7 asks. One execution of the tool must satisfy every argument requirement. A string value is compared as it is; any other JSON value as its compact JSON text with sorted keys. MessageToolCallScorer reads the arguments from the function_call piece of each executed call, so it can now confirm an argument requirement; it still never reports false. Trace events retain no arguments, so OtelToolCallScorer cannot confirm one and leaves it undetermined, while complete evidence with no call of the tool still refutes it. A call whose arguments were not retained or do not parse as a JSON object is undetermined, never a match or a miss. The arguments field is left out of serialization while empty, so stored name-only conditions keep their form and expectation fingerprint (pinned in a test). Name-only rationales are unchanged.
There was a problem hiding this comment.
🟡 Changes recommended
The PR description incorrectly claims type distinctions that textual matching does not preserve and must acknowledge cross-type matches between strings and numbers, booleans, or null.
1 open issue
What changed in this PR
Adds optional argument constraints to PyRIT’s ToolsCalled conditions, allowing tool-execution checks to reuse existing text matchers while preserving name-only compatibility.
Changes:
- Introduces
ToolArgumentRequirementand shared matching helpers. - Updates message and trace scorers to handle argument constraints within their evidence limits.
- Adds compatibility and behavior tests, plus synchronized documentation examples.
| File | Description |
|---|---|
tests/unit/score/test_tool_argument_matching.py |
Tests argument rendering, matching, and combined verdicts. |
tests/unit/score/test_otel_tool_call_scorer.py |
Tests constrained requirements against name-only trace evidence. |
tests/unit/score/test_message_tool_call_scorer.py |
Tests arguments from executed calls in both provider formats. |
tests/unit/models/test_condition.py |
Tests validation, serialization, schema, and fingerprint compatibility. |
pyrit/score/true_false/otel_tool_call_scorer.py |
Preserves uncertainty when traces lack argument evidence. |
pyrit/score/true_false/message_tool_call_scorer.py |
Matches stored request arguments for confirmed executions. |
pyrit/score/tool_argument_matching.py |
Adds shared argument matching and rationale helpers. |
pyrit/models/score/condition.py |
Defines optional argument constraints with compatible serialization. |
pyrit/models/score/__init__.py |
Exports the new argument requirement model. |
pyrit/models/__init__.py |
Exposes the model through the public models package. |
doc/code/scoring/5_tool_call_scorer.py |
Adds explanatory Markdown and an argument-constraint example. |
doc/code/scoring/5_tool_call_scorer.ipynb |
Mirrors the documentation changes in notebook Markdown cells. |
Review effort: Balanced
| if isinstance(value, str): | ||
| return value | ||
| return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True) |
Muhammad Waqar Anwar (WaqarAnwar)
left a comment
There was a problem hiding this comment.
Thanks, the matching semantics look right (one execution must satisfy every argument constraint, and undetermined never collapses into false), and pinning the fingerprint for name-only conditions is appreciated. Three things I'd suggest fixed before merge:
-
The message scorer's rationale is wrong when the tool ran with other arguments. If
send_emailran withto=b@example.comand the requirement asks forto=a@example.com, the rationale says "Stored messages show no execution of: send_email(to)". The tool did run. The rationale should separate "not executed" from "executed, but no call matched the required arguments", or anyone debugging a result will be misled. -
OtelToolCallScorercan never confirm an argument requirement, and the result doesn't say why.ToolExecutionkeeps only names, so a requirement withargumentscan only come back false (complete absence) or undetermined. The rationale says "Trace evidence cannot establish invocation of all required tools", which doesn't tell the user that traces don't keep arguments. Please say that in the rationale, and consider warning when this scorer gets an argument-constrained condition. -
The doc intro now misdescribes the OTel scorer.
5_tool_call_scorer.py/.ipynbsaysOtelToolCallScorer"checks arguments only when a requirement constrains them". It can't confirm arguments at all. Please restore "does not check arguments" and point readers toMessageToolCallScorerfor argument matching.

Description
Phases 5 and 6 of the scorer contract design match
ToolsCalledby tool name only and defer argument matching. This adds argument matching as an opt-in constraint onToolCallRequirement, inside the evidence rules those phases settled.ToolArgumentRequirement(name=..., matcher=...)names one argument and compares its value with the existingTextMatchertypes (Equals,Contains,Regex). The condition composes matchers, as Rule 7 ("matchers are a tier below conditions") asks, so no new matching vocabulary is added.ToolCallRequirement.argumentsis a tuple of them and is empty by default.3,trueand objects each have one stable text form, and the string"3"is not the number3.MessageToolCallScorerreads the arguments from thefunction_callpiece of each call that has an output, so it can now confirm an argument requirement. It still never reports false: a call with other arguments is undetermined, because stored messages are partial evidence.OtelToolCallScorer: trace events keep no arguments, so it cannot confirm an argument requirement and leaves it undetermined. Complete evidence with no call of the tool still refutes it, because no call means no matching call, and an absent name-only tool still refutes the condition next to an undetermined argument requirement.send_email(to). Name-only rationales are unchanged.The design sketch in section 5 has
arguments: dict[str, str] | Noneas a subset match. A tuple of matcher requirements keeps the same "subset" meaning (unnamed arguments are ignored), but lets an argument be checked withContainsorRegexas well as equality, and reuses the matcher types every other condition already uses.Compatibility.
argumentsis left out of serialization while empty, so stored name-only conditions keep their form and their expectation fingerprint; a test pins the fingerprint computed on main. Name-only verdicts are unchanged, so the scorers'matching_versionis unchanged.Tests and Documentation
tests/unit/score/test_tool_argument_matching.py(new): every constraint must match one call, exact argument names, compact JSON for non-string values, a JSON string is not its quoted form, regex search, one matching call among several, undetermined calls, rationale descriptions.tests/unit/models/test_condition.py: the pinned stored form and fingerprint of name-only requirements, explicit emptyarguments, round trip through persistence, invalid input, the JSON schema.argumentsis removed from the parametrized list of rejected fields, since it is now supported;count,orderandsuccessare still rejected.tests/unit/score/test_message_tool_call_scorer.py: matching and non-matching arguments in both the Responses and Chat Completions shapes, unparsable or non-object arguments, one matching execution among several, all constraints within one execution, arguments of a requested call that never ran are not evidence.tests/unit/score/test_otel_tool_call_scorer.py: name-only trace evidence cannot confirm an argument requirement (and the raw arguments attribute is not stored), complete absence still refutes, an absent name-only tool refutes alongside an undetermined argument requirement.doc/code/scoring/5_tool_call_scorer.pyand.ipynb: a "Require arguments" section, kept in sync.How it was tested
pytest tests/unit/score tests/unit/models tests/unit/common tests/unit/_code_execution tests/unit/output tests/unit/executor/attack -n 2: 11597 passed, 65 skipped.ruff checkandruff format --checkclean.tyreports no new diagnostics on the changed files compared with main..ipynbcells were regenerated from the.pycells and checked to match them, with outputs untouched.arguments, even when emptysort_keysfrom the compact JSON form