Fix output type loss in AgentOperator's human-in-the-loop review#70132
Fix output type loss in AgentOperator's human-in-the-loop review#70132ColtenOuO wants to merge 1 commit into
Conversation
When enable_hitl_review=True and output_type is a non-str, non-BaseModel type (e.g. list[str], bool), the reviewed output was serialized with str(output), producing a Python repr rather than valid JSON. Rehydrating it back into output_type then silently failed and returned the raw repr string instead of the original value. Serialize with TypeAdapter instead, and reuse the existing rehydrate_pydantic_output helper (already used by the require_approval path) instead of a separate, broken json.loads fallback.
dec8555 to
5c61485
Compare
| return json.loads(result_str) | ||
| except (ValueError, TypeError): | ||
| return result_str | ||
| return rehydrate_pydantic_output( |
There was a problem hiding this comment.
Dropping the json.loads fallback here regresses the exact case this PR targets. rehydrate_pydantic_output returns raw unchanged when output_type isn't a BaseModel subclass (its docstring notes the caller is expected to apply its own json.loads), so with output_type=list[str] this returns the JSON string '["tag-a","tag-b"]' instead of the list. The new test_execute_with_hitl_rehydrates_non_base_model_output fails on exactly this (assert '["tag-a", "tag-b"]' == ['tag-a', 'tag-b']). Keeping the previous json.loads/except fallback after the rehydrate call fixes it while preserving the BaseModel handling.
There was a problem hiding this comment.
In PR #70075 (merged), rehydrate_pydantic_output was updated to handle any non-str output_type, not just BaseModel subclasses:
if output_type is str:
return raw
try:
rehydrated = TypeAdapter(output_type).validate_json(raw)
except (ValidationError, ValueError, TypeError):
return rawuv run --project providers/common/ai python -c "tic_output
from airflow.providers.common.ai.utils.output_type import rehydrate_pydantic_outputtput=False)
result = rehydrate_pydantic_output(list[str], '[\"tag-a\",\"tag-b\"]', serialize_output=False)
print(repr(result), type(result))
"Output:
['tag-a', 'tag-b'] <class 'list'>
So I don't think the json.loads fallback needs to come back (?
Let me know what you think, or if I might have missed anything!
Thanks for your review!
Summary
AgentOperator's human-in-the-loop review path (enable_hitl_review=True) loses the original output type for anyoutput_typethat is neitherstrnor a PydanticBaseModel(e.g.list[str],bool,dict).The output is round-tripped through a string while it's shown to a human reviewer:
HITLReviewMixin._to_stringserializes it before review, andAgentOperator.execute()deserializes it back intooutput_typeafter approval. The serialization side usedstr(output), which produces a Python repr (e.g."['tag-a', 'tag-b']"), not valid JSON.The deserialization side then tried
json.loads()on that repr, which always raised, and silently fell back to returning the raw repr string instead of the original list/bool/dict — a type change downstream tasks don't expect.regenerate_with_feedback()(used when a reviewer requests changes) had the identicalstr(output)bug.This is the same defect class already fixed for the
require_approval=Truepath in #70075, which switched toTypeAdapter(...).dump_json(...). That fix wasn't applied to the separateenable_hitl_review=Truepath, so it regressed here.Changes
HITLReviewMixin._to_stringandAgentOperator.regenerate_with_feedbacknow serialize non-str/non-
BaseModeloutput withTypeAdapter(type(output)).dump_json(output), matching the patternalready used by
LLMApprovalMixin.defer_for_approval.AgentOperator.execute()'s HITL-review branch now reuses the existingrehydrate_pydantic_outputhelper (already used by the approval path)instead of a bespoke
json.loads/exceptfallback, so both review pathsbehave consistently.
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Sonnet 5) following the guidelines