YAshhh29 opened a new pull request, #69812:
URL: https://github.com/apache/airflow/pull/69812

   The `OpenAIResponseOperator` today only returns the aggregated `output_text`
   of a Responses API call. Users who want a structured JSON output — the 
pattern
   every AI-eng team uses for reliable extraction, tool selection, and 
downstream
   DAG chaining — have to bypass the operator entirely. The operator's own
   docstring instructs this:
   
   > The operator is synchronous and returns the response's aggregated output 
text. For
   > `previous_response_id` chaining, `background=True` responses, or **access 
to the full
   > structured response, use `OpenAIHook` directly**.
   
   This PR closes that gap. Pass a Pydantic `BaseModel` subclass as 
`text_format` and
   the operator uses the SDK's structured-output path (`responses.parse`) and 
returns
   `output_parsed.model_dump()` — a plain `dict`, XCom-safe, no manual 
serialization
   dance. When `text_format` is `None` the existing plain-text behavior is 
preserved
   bit-for-bit.
   
   ### How I found and verified this gap
   
   Same audit approach as #69408, #69534, and #69673: read every operator/hook 
in the
   AI/ML providers and compare surfaced API to underlying SDK. The Responses 
API is
   OpenAI's recommended interface going forward (Chat Completions is on the 
deprecation
   path), so gaps here are user-facing today, not legacy cleanup.
   
   Before writing a line of code I confirmed:
   
   1. **The API exists and is stable.** 
`openai.resources.responses.Responses.parse(input, model, text_format, ...)` is 
present in the pinned SDK (`openai>=2.37.0`) and returns 
`ParsedResponse[TextFormatT]` with `.output_parsed` as an instance of the 
passed model. Verified via `inspect.signature(Responses.parse)`.
   2. **`output_parsed` is None on refusal / incomplete.** The SDK docs and 
source confirm the model returning a refusal or the response being truncated 
yields `output_parsed=None`. The operator raises `ValueError` in that case so 
downstream tasks don't silently get `None`.
   3. **`model_dump()` produces XCom-safe dicts.** Every pinned Pydantic 
version supports it.
   4. **`pydantic` is already a core Airflow dependency.** No new dep.
   
   ### Design decisions
   
   - **Pydantic-only, not raw JSON schema.** The SDK's 
`responses.parse(text_format=Model)` path handles schema conversion, 
strict-mode, and response parsing in one call. Raw JSON schema via 
`responses.create(text={"format": {...}})` requires the caller to define a 
schema name, parse `output_text` manually, and handle refusals themselves. 
Pydantic is the pattern every AI-eng library (Instructor, LangChain, 
Pydantic-AI) wraps — starting there covers the primary use case cleanly. 
Raw-schema support can be added in a follow-up without breaking this API.
   - **One operator, opt-in param — not a new class.** `text_format=None` 
(default) keeps the existing `str`-returning behavior. Set 
`text_format=SomeModel` and you get a `dict`. Return-type annotation is `str | 
dict[str, Any]`. Users who don't touch the new param see zero change.
   - **`ValueError` on refusal, not silent None.** If `output_parsed is None`, 
the operator raises with the response ID and status. Silently returning `None` 
would break downstream tasks that assume a specific shape.
   - **No new exception class.** Per `AGENTS.md` guidance ("prefer a Python 
built-in"), `ValueError` is semantically correct — the model returned something 
that can't be converted to the requested type.
   - **`create_response` untouched.** All existing tests, DAGs, and behavior 
are preserved. The plain-text path is not touched.
   
   ### What changes
   
   - `providers/openai/src/airflow/providers/openai/hooks/openai.py`
     - New `OpenAIHook.parse_response(input, text_format, model, **kwargs) -> 
ParsedResponse[Any]`, a thin wrapper over `client.responses.parse`.
   - `providers/openai/src/airflow/providers/openai/operators/openai.py`
     - `OpenAIResponseOperator` gains a `text_format: type[BaseModel] | None = 
None` parameter. Return type widened to `str | dict[str, Any]`. Docstring 
updated to explain the two paths.
   - `providers/openai/tests/unit/openai/hooks/test_openai.py`
     - `test_parse_response`: hook forwards `text_format`, `model`, and extra 
kwargs to `conn.responses.parse` and returns the SDK's return value.
   - `providers/openai/tests/unit/openai/operators/test_openai.py`
     - `test_openai_response_operator_structured_output_returns_dict`: happy 
path — parsed model → dict via `model_dump()`, `create_response` not called.
     - `test_openai_response_operator_structured_output_refusal_raises`: 
`output_parsed=None` → `ValueError` with the response ID.
     - The existing `test_openai_response_operator_execute` is left untouched 
to guard the backward-compat path.
   - `providers/openai/tests/system/openai/example_openai.py`
     - New `# [START/END] howto_operator_openai_response_structured` block with 
a Pydantic `Person` example.
   - `providers/openai/docs/operators/openai.rst`
     - New **Structured outputs (Pydantic models)** subsection under the 
existing `OpenAIResponseOperator` how-to, with an `exampleinclude` pointing at 
the new markers.
   
   No `provider.yaml` / `get_provider_info.py` changes needed: the registry 
lists python-modules, not classes, and no new module was added. No changelog 
edit either — per `AGENTS.md`, provider changelogs are regenerated from `git 
log` by the release manager.
   
   ### Testing
   
   - **Full-provider `ruff check` + `ruff format --check`**: 29 files clean.
   - **All hook + operator tests exercised via a standalone Windows-safe 
harness** stubbing airflow, `openai.auth`, and 
`openai.types.responses.ParsedResponse` (full airflow install cannot run on 
Windows). Four assertions pass: hook forwards args, operator structured path 
returns dict + skips `create_response`, operator refusal path raises 
`ValueError`, operator backward-compat plain-text path is unchanged.
   - **Regression check**: the existing `test_openai_response_operator_execute` 
was not modified, so the plain-text return path is guarded by a test the 
reviewer can fail by reverting the PR.
   - **Line-by-line reviewed by me** against every rule in 
`.github/instructions/code-review.instructions.md` — no red flags (no 
`time.time`, no `assert` in prod, no new `AirflowException`, no `mock.Mock()` 
without spec, imports at top of file, session-parameter and DB-query rules N/A 
here).
   
   ### Author review
   
   I read every line of this PR — hook, operator, tests, example DAG, docs — end
   to end before pushing, and validated the SDK contract claims myself against
   the installed `openai` package (`inspect.signature(Responses.parse)`, the
   `ParsedResponse.output_parsed` attribute check, and the `openai>=2.37.0`
   availability of both). The design calls above — Pydantic-only for this first
   cut, `ValueError` on refusal instead of silent `None`, opt-in `text_format`
   param instead of a new class — are mine, not the agent's default choices.
   The generated code was iterated on until it matched what I wanted to ship.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes — GitHub Copilot (Claude Opus 4.7)
   
   Generated-by: GitHub Copilot (Claude Opus 4.7) following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to