kaxil commented on code in PR #73275:
URL: https://github.com/apache/airflow/pull/73275#discussion_r4036670575
##########
providers/common/ai/src/airflow/providers/common/ai/operators/agent.py:
##########
@@ -573,10 +594,29 @@ def _emit_message_history(self, context: Context, result:
Any) -> None:
transcript =
ModelMessagesTypeAdapter.dump_json(result.all_messages()).decode()
context["task_instance"].xcom_push(key="message_history",
value=transcript)
+ def _emit_run_metadata(self, context: Context, result: Any) -> None:
+ """Expose the pydantic-ai run id and token usage on XCom for
downstream tasks."""
+ usage = result.usage
+ ti = context["task_instance"]
+ ti.xcom_push(key="run_id", value=result.run_id)
Review Comment:
These two go out unconditionally, so `do_xcom_push=False` no longer stops
this operator writing XCom (`RuntimeTaskInstance.xcom_push` doesn't consult it,
and nothing in the provider does either). Every other push in the class sits
behind `message_history` or `enable_hitl_review`, so this is the first
always-on one. Worth gating on `self.do_xcom_push`?
##########
providers/common/ai/src/airflow/providers/common/ai/operators/agent.py:
##########
@@ -470,7 +481,16 @@ def execute(self, context: Context) -> Any:
agent = self._build_agent()
+ ti = context["task_instance"]
+ self._run_identity_attrs = build_run_identity_attributes(ti)
+ stamp_identity_on_agent_spans(agent, self._run_identity_attrs)
+
run_kwargs: dict[str, Any] = {"usage_limits": usage_limits}
+ if ti.id is not None:
Review Comment:
Can `ti.id` actually be None here? It's a required non-nullable `UUID` on
the Task SDK datamodel
([_generated.py#L554](https://github.com/apache/airflow/blob/11a3892daa9dfd5e84fb037d49373b174a372fb8/task-sdk/src/airflow/sdk/api/datamodels/_generated.py#L554)),
so this branch looks unreachable and
`test_run_id_omitted_when_task_instance_has_no_id` is asserting on a state the
runtime can't produce. `build_run_identity_attributes` takes the opposite view
of the same question: it calls `str(ti.id)` with no guard, so if None were
reachable every GenAI span would carry the literal
`airflow.task_instance.id="None"`.
##########
providers/common/ai/src/airflow/providers/common/ai/observability.py:
##########
@@ -106,3 +111,71 @@ def genai_instrumentation_settings() ->
InstrumentationSettings | None:
include_binary_content=False,
tracer_provider=provider,
)
+
+
+def build_run_identity_attributes(ti: Any) -> dict[str, Any]:
+ """
+ Build the Airflow identity attributes to stamp on a run's GenAI spans.
+
+ Reuses core's task-span attribute keys (see ``_make_task_span``) so agent
+ spans filter identically to the task span they nest under, plus the
+ per-attempt task-instance id as the run join key carried on every span.
+ """
+ return {
+ "airflow.dag_id": ti.dag_id,
+ "airflow.task_id": ti.task_id,
+ "airflow.dag_run.run_id": ti.run_id,
+ "airflow.task_instance.try_number": ti.try_number,
+ "airflow.task_instance.map_index": ti.map_index if ti.map_index is not
None else -1,
+ "airflow.task_instance.id": str(ti.id),
+ }
+
+
+def stamp_identity_on_agent_spans(agent: Agent, attributes: dict[str, Any]) ->
None:
+ """
+ Stamp *attributes* on every GenAI span *agent* emits during its run.
+
+ pydantic-ai opens all of a run's agent/model/tool spans from
+ ``InstrumentationSettings.tracer``, so wrapping that one tracer reaches
them
+ all without touching the shared core ``TracerProvider``. No-op when the
+ agent is not instrumented with an ``InstrumentationSettings`` (tracing off,
+ or the caller supplied its own non-settings ``instrument`` value).
+ """
+ from pydantic_ai.models.instrumented import InstrumentationSettings
+
+ instrument = agent.instrument
+ if isinstance(instrument, InstrumentationSettings):
+ # _IdentityTracer implements the Tracer surface structurally (it cannot
+ # subclass Tracer without importing opentelemetry at module load).
+ instrument.tracer = cast("Tracer", _IdentityTracer(instrument.tracer,
attributes))
Review Comment:
This writes through to an object the caller may own. `create_agent` pops
`instrument` out of `agent_params`, so a DAG that builds one module-level
`InstrumentationSettings` and hands it to several agent tasks ends up with that
object carrying whichever task stamped last. The HITL path shows the other
half: `regenerate_with_feedback` rebuilds the agent every round and stamps
again, and against a caller-supplied settings object the `_IdentityTracer`s
nest one deeper each time (depth 4 after 4 stamps when I tried it). Copying
first (`settings = copy.copy(instrument)`, set `.tracer` on the copy, then
`agent.instrument = settings`) keeps it per-run. `dataclasses.replace` won't
work, `tracer` isn't accepted by `__init__`.
##########
providers/common/ai/src/airflow/providers/common/ai/operators/agent.py:
##########
@@ -573,10 +594,29 @@ def _emit_message_history(self, context: Context, result:
Any) -> None:
transcript =
ModelMessagesTypeAdapter.dump_json(result.all_messages()).decode()
context["task_instance"].xcom_push(key="message_history",
value=transcript)
+ def _emit_run_metadata(self, context: Context, result: Any) -> None:
+ """Expose the pydantic-ai run id and token usage on XCom for
downstream tasks."""
+ usage = result.usage
+ ti = context["task_instance"]
+ ti.xcom_push(key="run_id", value=result.run_id)
+ ti.xcom_push(
+ key="usage",
+ value={
+ "requests": usage.requests,
+ "input_tokens": usage.input_tokens,
+ "output_tokens": usage.output_tokens,
+ "total_tokens": usage.total_tokens,
+ "tool_calls": usage.tool_calls,
Review Comment:
The class docstring up at line 133 says a downstream task can reference the
run "and its cost", but `cost` isn't in this dict. `RunUsage.cost` is the field
the `pydantic-ai-slim>=2.23.0` floor is pinned for, and `log_run_summary`
already prints it. It's a `Decimal | None` so it needs stringifying before it
goes to XCom, but either add it here or drop the cost half of the docstring
promise.
--
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]