-
Notifications
You must be signed in to change notification settings - Fork 17.6k
Skip downstream tasks on LLMBranchOperator reject #71073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
|
|
||
| from airflow.providers.common.ai.operators.llm import LLMOperator | ||
| from airflow.providers.common.ai.utils.logging import log_run_summary | ||
| from airflow.providers.standard.exceptions import HITLRejectException | ||
| from airflow.providers.standard.operators.branch import BranchMixIn | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -46,14 +47,21 @@ class LLMBranchOperator(LLMOperator, BranchMixIn): | |
| :param system_prompt: System-level instructions for the LLM agent. | ||
| :param allow_multiple_branches: When ``False`` (default) the LLM returns a | ||
| single task ID. When ``True`` the LLM may return one or more task IDs. | ||
| :param fail_on_reject: If ``True``, a rejected review fails the task | ||
| instead of skipping every downstream task. Generally discouraged, | ||
| as for :class:`~airflow.providers.standard.operators.hitl.ApprovalOperator`. | ||
| Default ``False``. | ||
| :param agent_params: Additional keyword arguments passed to the pydantic-ai | ||
| ``Agent`` constructor (e.g. ``retries``, ``model_settings``, ``tools``). | ||
|
|
||
| Human-in-the-Loop approval parameters are inherited from | ||
| :class:`~airflow.providers.common.ai.operators.llm.LLMOperator` | ||
| (``require_approval``, ``approval_timeout``, ``allow_modifications``). | ||
| The task pauses after the LLM chooses the branch(es) and only skips the | ||
| unselected downstream tasks once a reviewer approves. The review form | ||
| unselected downstream tasks once a reviewer approves. Rejecting the | ||
| review skips every downstream task, matching | ||
| :class:`~airflow.providers.standard.operators.hitl.ApprovalOperator`; | ||
| set ``fail_on_reject=True`` to fail the task instead. The review form | ||
| lists the valid downstream task IDs; with ``allow_modifications=True`` | ||
| the editable choice is rendered as a dropdown of those IDs (single-branch | ||
| mode) or a multi-select of them (``allow_multiple_branches=True``), and | ||
|
|
@@ -69,11 +77,13 @@ def __init__( | |
| self, | ||
| *, | ||
| allow_multiple_branches: bool = False, | ||
| fail_on_reject: bool = False, | ||
| **kwargs: Any, | ||
| ) -> None: | ||
| kwargs.pop("output_type", None) | ||
| super().__init__(**kwargs) | ||
| self.allow_multiple_branches = allow_multiple_branches | ||
| self.fail_on_reject = fail_on_reject | ||
|
|
||
| def execute(self, context: Context) -> str | Iterable[str] | None: | ||
| if self.require_approval: | ||
|
|
@@ -133,7 +143,13 @@ def execute(self, context: Context) -> str | Iterable[str] | None: | |
|
|
||
| def execute_complete(self, context: Context, generated_output: str, event: dict[str, Any]) -> Any: | ||
| """Resume after human review, validating the reviewed choice before branching.""" | ||
| output = super().execute_complete(context, generated_output, event) | ||
| try: | ||
| output = super().execute_complete(context, generated_output, event) | ||
| except HITLRejectException: | ||
| if self.fail_on_reject: | ||
| raise | ||
| self.log.info("Rejected. Skipping all downstream tasks...") | ||
| return self.do_branch(context, None) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Either filter teardowns here, or drop the "matching |
||
| branches = self._parse_reviewed_branches(output) | ||
| selected = {branches} if isinstance(branches, str) else set(branches) | ||
| invalid = selected - self.downstream_task_ids | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exception being swallowed here carries the reviewer name (
Output was rejected by the reviewer <user>.), and the task log was the only place that surfaced.event["responded_by_user"]is right here, soself.log.info("Rejected by %s. Skipping all downstream tasks.", event.get("responded_by_user"))would keep the attribution. Right now the log for a rejected gate no longer says who rejected it.