kaxil commented on code in PR #75358:
URL: https://github.com/apache/airflow/pull/75358#discussion_r4352909076


##########
providers/standard/src/airflow/providers/standard/operators/hitl.py:
##########
@@ -25,19 +25,35 @@
     AIRFLOW_V_3_1_3_PLUS,
     AIRFLOW_V_3_1_PLUS,
     AIRFLOW_V_3_3_PLUS,
+    AIRFLOW_V_3_4_PLUS,
 )
 
 if not AIRFLOW_V_3_1_PLUS:
     raise AirflowOptionalProviderFeatureException("Human in the loop 
functionality needs Airflow 3.1+.")
 
 from collections.abc import Collection, Mapping, Sequence
 from datetime import timedelta
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NoReturn
 from urllib.parse import ParseResult, urlencode, urlparse, urlunparse
 
-from airflow.providers.common.compat.sdk import BranchMixIn, SkipMixin, conf
-from airflow.providers.standard.exceptions import HITLRejectException, 
HITLTimeoutError, HITLTriggerEventError
+from airflow.providers.common.compat.sdk import BranchMixIn, SkipMixin, conf, 
get_current_context
+from airflow.providers.standard.exceptions import (  # noqa: F401 - kept 
importable from this module

Review Comment:
   I'd keep them: they were importable from this module before, so third-party 
code may rely on that. The noqa comment says so.



##########
providers/standard/docs/operators/hitl.rst:
##########
@@ -42,3 +42,69 @@ For an end-to-end guide on using HITL operators, including 
examples and
 recommended patterns, see the HITL tutorial:
 
 * `HITL tutorial 
<https://airflow.apache.org/docs/apache-airflow/stable/tutorial/hitl.html>`_
+
+.. _howto/operator:request_input:
+
+Asking for input from inside a Python task
+------------------------------------------
+
+Sometimes the question only comes up halfway through a task. An agent built 
with the OpenAI
+Agents SDK, Strands or Google ADK, for example, stops in front of a tool call 
and needs a person
+to approve it. 
:func:`~airflow.providers.standard.operators.hitl.request_input` asks the 
question
+and returns the answer:
+
+.. exampleinclude:: 
/../../standard/src/airflow/providers/standard/example_dags/example_hitl_request_input.py
+    :language: python
+    :start-after: [START howto_hitl_request_input]
+    :end-before: [END howto_hitl_request_input]
+
+The first time the task reaches ``request_input()``, the question appears on 
the Required Actions
+page and the task stops there. It waits in the ``awaiting_input`` state 
without holding a worker
+slot. When the reviewer answers, the task runs again from the start of its 
callable, and this
+time the same call returns the answer: a dict with ``chosen_options``, 
``params_input``,
+``responded_at`` (a ``datetime``), ``responded_by_user`` and ``timedout``.
+
+``request_input()`` takes the same request fields as
+:class:`~airflow.providers.standard.operators.hitl.HITLOperator` (``subject``, 
``options``, ``body``,
+``defaults``, ``multiple``, ``params``, ``notifiers``, ``assigned_users``), a 
``timeout``, and a
+``key``. A task can ask several questions; each run returns every answer it 
already has and stops
+at the first question that has none.
+
+Things to know:
+
+* Code before ``request_input()`` runs again on every run. Keep anything 
expensive, such as an
+  agent framework's saved run state, in ``task_state_store`` so the next run 
restores it instead of
+  redoing it.
+* Each answer goes back to the call with the same ``key``, which defaults to 
``subject``, and each
+  key is asked at most once per try. Pass ``key`` when the subject can change 
between runs (for
+  example when it includes a value computed fresh each run), or when the task 
can ask the same
+  subject more than once, such as one approval per agent tool call: key on the 
call's id.
+* The task fails instead of guessing when the keys don't line up: when it asks 
a key whose answer
+  it has already read, and when a run that resumed with an answer asks a new 
question, or finishes,
+  before reading that answer.
+* Every pause carries the answers the task has received so far in this try, 
including their
+  ``params_input``, so keep the number of questions per try small (a handful, 
not hundreds) and the
+  form fields short.
+* A side effect between two questions, such as issuing a refund before asking 
whether to email the
+  customer, runs again when the task resumes after the second answer. Record 
it in
+  ``task_state_store`` and skip it when the record is there:
+
+  .. code-block:: python
+
+      if task_state_store.get("refund_id") is None:
+          task_state_store.set("refund_id", issue_refund(order_id))
+
+* A retry asks every question again, but ``task_state_store`` keeps its values 
across retries, so
+  the retry still sees what the failed try saved. Delete those values when you 
no longer need them.
+* Without ``defaults``, the task fails with
+  :class:`~airflow.providers.standard.exceptions.HITLTimeoutError` when 
``timeout`` passes. With
+  ``defaults``, it resumes with ``timedout`` set to ``True`` and 
``responded_by_user`` set to
+  ``None``.
+* It works in ``@task``, ``@task.branch``, ``@task.short_circuit`` and 
``PythonOperator`` callables,
+  sync or async. It can't pause tasks that run their callable in another 
process
+  (``@task.virtualenv``, ``@task.external_python``, ``@task.docker``, 
``@task.kubernetes``).
+* It needs Airflow 3.4+.
+* The Required Actions page refuses an answer to a request that the task has 
since replaced. A
+  client answering through the REST API gets the same protection only if it 
sends the

Review Comment:
   Done, thanks.



-- 
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