Repository navigation
Fail the task when a reviewed output no longer matches output_type - #74473
Conversation
942a114 to
13c9cb1
Compare
When a reviewer edits the output in the approval form, the edited text is validated back into the operator's output_type. On a validation failure rehydrate_pydantic_output silently returned the raw string, so the task succeeded and downstream tasks received a str instead of an int, a list or a model. It now raises ReviewedOutputValidationError, a ValueError subclass that names the output type, the text received and the pydantic message. output_type=str still passes any text through. Types whose value is bare text, such as a str Literal, an Enum or str | None, are still accepted as plain text. An output_type that pydantic has no schema for keeps passing the text through, since there is nothing to validate against. AgentOperator review serialized non-model outputs with str(), which gives a Python repr for a list and would now fail an unedited approval. It uses JSON instead, for the first output and for regenerated ones. Values that pydantic cannot serialize keep str(). LLMSchemaCompareOperator re-validated the string after the parent returned and raised its own ValueError. That fallback can no longer be reached, so the override is removed. Co-Authored-By: Claude <noreply@anthropic.com>
…t functions An unedited approval of a model with field aliases failed because the review string was dumped with field names while validation expects the aliases. The review serializers now dump by alias. An output function used as output_type was validated as if the text were its arguments, so an unedited approval raised and the function could be called a second time. It now returns the text unchanged until it is handled properly. The validation error now reports the plain text failure when the JSON attempt only failed because the text is not JSON, and it shortens a long received text. Tests now pin the regenerated output serialization and the enum conversion, and the agent review test is renamed to match the only way that operator can reach the error. Co-Authored-By: Claude <noreply@anthropic.com>
The review serializers dumped by alias, which breaks models whose alias is serialization-only, so an unedited approval failed validation. Both serializers now share dump_output_to_json, which dumps by field name with round_trip. Validation accepts a field name or an alias, bare text gets one retry with surrounding whitespace removed, and an output function is recognised even when pydantic wraps it in a definitions schema. An unedited approval of an output that cannot be validated back now returns the text with a warning, and only an edited output fails the task. Types pydantic cannot build a schema for keep passing the text through. Tests cover the no-schema and unresolved forward reference types, models serialized by alias, computed fields, and the unedited tolerance. Co-Authored-By: Claude <noreply@anthropic.com>
13c9cb1 to
d869d0e
Compare
AgentOperator never validates its review string back, so the round-trip dump only hid computed fields from the reviewer. The review text now comes from dump_output_for_display, and dump_output_to_json with round_trip stays for the approval gate where an edit is validated back. Both share one helper that keeps the str() fallback and its warning. A JSON edit with a typo such as a trailing comma reported a top-level type mismatch from the plain text attempt. The message now keeps the JSON error when the text error is only a container mismatch, and bare-text types such as Literal, Enum and int still report the text error. Co-Authored-By: Claude <noreply@anthropic.com>
…e-validators The Airflow HITL form parses a JSON-object value into an object before sending it back, so approving a model output in the UI handed the mixin a dict and failed the task even when nothing was edited. Dump a dict to compact JSON, as a list already is, so an untouched object compares equal to the generated output. The bare-text retry feeds a string to validators that may only expect the type's input, so a mode="before" validator calling data.get raised AttributeError past the except tuple, on an unedited approval too. Any failure on that retry now just means "not bare text", and the JSON error is reported. A JSON typo in a union, dataclass, frozenset or NamedTuple edit now reports the JSON error, pydantic-ai output markers pass the text through instead of raising AttributeError, and the docs and changelog say how a failed edit behaves under retries.
The HITL form round-trips an object output through JavaScript, which rounds integers above 2**53, writes 1.0 as 1 and may reorder keys (as may the metadata database). The re-dumped text then differed from the generated output, so an untouched approval read as an edit: a large id could change silently, and a model whose dump does not validate back failed the task. Compare values the way the browser parses them, with a bool kept distinct from a number, and keep the generated output unchanged when they match. A before-validator that raises AttributeError, KeyError or IndexError on a JSON edit that is not an object now fails with ReviewedOutputValidationError on the JSON attempt too, while any other validator error (a timeout, say) still propagates instead of reading as a failed conversion. An object or array the reviewer typed reports its JSON error even when the type also takes bare text, and the docs no longer claim a retry opens a new review on every Airflow version.
|
:( test failure: https://github.com/apache/airflow/actions/runs/38529714144/job/115743582407?pr=74473 |
… 2.14 Pydantic 2.14 raises named_tuple_type instead of arguments_type when a whole string meets a NamedTuple. The error was missing from the container mismatch set, so the review message showed the plain text error where it should show the JSON error. The set now lists both, and the test notes the split. Co-Authored-By: Claude <noreply@anthropic.com>
Yes :) Looks like the test was newly added in the recent commit a4f58a9. It seems to have failed because pydantic 2.14 renamed the NamedTuple error from |
Yeah, I was hoping to get CI to green at midnight to merge to this in 1.0.0rc1 😬 |
…ame pattern A decision-policy review body showed the output's Python repr, so the docs' advice to copy "the JSON shown in the review body" into the edit field did not hold there. It now shows the same JSON the default body and the edit field use. Telling a JSON typo from plain text relied on a list of pydantic error names, which pydantic 2.14 renamed for a NamedTuple and a deque. Match the "_type" suffix those errors share, plus "sequence_str", instead. Rehydrating no longer forces validation by alias, which overrode a model that sets validate_by_alias=False: with an alias named like another field, an untouched approval swapped the two values. Field names and, by default, aliases are still accepted. A multi-select edit is dumped without escaping non-ASCII, like the generated output, so an untouched choice no longer reads as an edit. The changelog no longer says a Literal or str Enum keeps accepting free text, and the docs say a union with a str member keeps an edit that is not the model as text.
When a reviewer edits the output of an
LLMOperatorapproval gate, the edited text is validated back into the operator'soutput_type. If validation failed,rehydrate_pydantic_outputsilently returned the raw string. The task succeeded and downstream tasks received astrinstead of anint, alistor a model. For exampleoutput_type=intwith the edit "thirty-one" produced the string 'thirty-one'.It now raises
ReviewedOutputValidationError, aValueErrorsubclass in the provider's exceptions module. The message names the output type, the text received (truncated) and the pydantic validation message, so the task fails with a readable error. Details:output_type=strstill accepts any text.strLiteral, astrEnum,str | None) still accept bare text. For these the message reports the plain text failure instead of "Invalid JSON". A JSON edit with a typo, such as a trailing comma, keeps the JSON error, because the plain text attempt would only report a top-level type mismatch.1.0as1and may reorder keys. An untouched object is compared by value the way the browser parses it, so it is never read as an edit.output_typethat pydantic has no schema for, a pydantic-ai output function or marker (such asToolOutput), and an output function whose parameters pydantic wraps in adefinitionsschema, still pass the text through. There is nothing to validate against, or validating would call the function again with the reviewed text. No warning is logged for this passthrough. A union with astrmember, such asModel | str, keeps an edit that is not the model as text.The approval gate (
LLMOperatorand its subclasses) builds the review string withdump_output_to_json. It dumps by field name withround_trip=True, and validation accepts a field name, and an alias unless the model setsvalidate_by_alias=False. So models with aliases, including serialization-only aliases, validate back, and computed fields are left out. The decision-policy review body now shows the same JSON as the edit field.AgentOperatornever validates its review text back, so it usesdump_output_for_display, a plain JSON dump that keeps computed fields visible to the reviewer. Both fall back tostr()with a warning for values pydantic cannot serialize.LLMSchemaCompareOperatorre-validated the string after the parent returned and raised its ownValueError. That fallback is no longer reachable, so the override is removed. The new exception is still aValueError.Behavior change: an invalid reviewer edit now fails the task instead of passing a string downstream.
Related PRs: #74537 (merged) made
AgentOperatorreturn the output it already holds instead of converting the approved text, so this PR no longer touches its review loop apart from regenerated output using the shared helper. #70132 (open) also editsrehydrate_pydantic_outputandmixins/hitl_review.py, and #74493 (open) overlapsoperators/llm.py. Whichever merges later will need a rebase. A note for the behavior change is added to the provider changelog.Follow-up idea, not part of this PR: validate the edit when the reviewer submits the form, so they see the error immediately instead of the task failing after approval.
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Sonnet 5.5) following the guidelines
{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.🤖 Generated with Claude Code