From e98633aecce2511ddb3998eeaae8f837200c0ba7 Mon Sep 17 00:00:00 2001 From: Max Ind Date: Wed, 1 Jul 2026 16:42:14 -0700 Subject: [PATCH] feat(telemetry): emit entrypoint invoke_workflow span under schema v2 When ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN resolves to 2, replace the legacy top-level 'invocation' span with an entrypoint 'invoke_workflow {entrypoint}' span (entrypoint = root agent or root node name), set is_entrypoint, and emit the gen_ai.invoke_workflow.duration metric alongside it. Entrypoint status is tracked via a contextvar so nested/agent-as-tool workflows report is_entrypoint=false and the span is not double-emitted when the ADK entrypoint is itself a workflow. Schema v1 is unchanged. Co-authored-by: Max Ind PiperOrigin-RevId: 941359381 --- src/google/adk/telemetry/_instrumentation.py | 68 +- src/google/adk/telemetry/_metrics.py | 61 + src/google/adk/telemetry/node_tracing.py | 177 +- .../telemetry/functional_node_test_cases.py | 1756 +++++++---------- .../telemetry/functional_test_cases.py | 57 +- .../telemetry/functional_test_helpers.py | 5 + .../telemetry/test_instrumentation.py | 13 +- tests/unittests/telemetry/test_metrics.py | 42 + 8 files changed, 1065 insertions(+), 1114 deletions(-) diff --git a/src/google/adk/telemetry/_instrumentation.py b/src/google/adk/telemetry/_instrumentation.py index 1f8cc31feba..8bda08edd82 100644 --- a/src/google/adk/telemetry/_instrumentation.py +++ b/src/google/adk/telemetry/_instrumentation.py @@ -29,6 +29,8 @@ from . import _metrics from . import tracing from ..events import event as event_lib +from ._schema_version import resolve_schema_version +from ._schema_version import SCHEMA_VERSION_SEMCONV_ALIGNED if TYPE_CHECKING: from ..agents.base_agent import BaseAgent @@ -41,52 +43,42 @@ logger = logging.getLogger("google_adk." + __name__) -def _get_elapsed_s( - span: trace.Span | tracing.GenerateContentSpan | None, - fallback_start: float, -) -> float: - """Guarantees consistent time source for duration calculation. - - Note: This must be called with an ended span. - - Args: - span (trace.Span | tracing.GenerateContentSpan | None): The ended span to - extract duration from. - fallback_start (float): Fallback start time in seconds (monotonic). - - Returns: - float: Elapsed duration in seconds. - """ - if span is None: - return time.monotonic() - fallback_start - - span = span.span if hasattr(span, "span") else span - start_ns = getattr(span, "start_time", None) - end_ns = getattr(span, "end_time", None) - - if isinstance(start_ns, int) and isinstance(end_ns, int): - return (end_ns - start_ns) / 1e9 # Convert ns to s - - # Fallback if span times are missing - return time.monotonic() - fallback_start - - @contextlib.contextmanager def record_invocation( entrypoint_node: BaseNode | None, conversation_id: str, ) -> Iterator[None]: - """Top-level ``invocation`` span for a runner invocation. + """Top-level invocation span for a runner invocation. + + Schema v1 emits the legacy ``invocation`` span. Schema v2 replaces it with an + entrypoint ``invoke_workflow {entrypoint}`` span (entrypoint = root agent or + root node name), which omits the ``gen_ai.workflow.nested`` attribute, and a + ``gen_ai.invoke_workflow.duration`` metric -- unless the entrypoint is itself + a workflow, in which case its own node span is the entrypoint + ``invoke_workflow`` span and we avoid double-emitting it here. Args: entrypoint_node: The runner's root agent/node. - conversation_id: Session/conversation id. + conversation_id: Session/conversation id (stamped on the v2 span). Yields: - Nothing; the span is active for the duration of the block. + Nothing; the span (if any) is active for the duration of the block. """ - del entrypoint_node, conversation_id # Unused until schema v2 lands. - with tracing.tracer.start_as_current_span("invocation"): + if resolve_schema_version() < SCHEMA_VERSION_SEMCONV_ALIGNED: + with tracing.tracer.start_as_current_span("invocation"): + yield + return + + from . import node_tracing + from ..workflow._workflow import Workflow + + if isinstance(entrypoint_node, Workflow): + # The workflow's own node span is the entrypoint `invoke_workflow` span. + yield + return + + entrypoint_name = entrypoint_node.name if entrypoint_node else "" + with node_tracing._use_invoke_workflow_span(entrypoint_name, conversation_id): yield @@ -152,7 +144,7 @@ async def record_agent_invocation( finally: _record_agent_metrics( agent.name, - _get_elapsed_s(span, start_time), + _metrics.get_elapsed_s(span, start_time), getattr(ctx, "user_content", None), getattr(getattr(ctx, "session", None), "events", []), caught_error, @@ -198,7 +190,7 @@ async def record_tool_execution( tool_name=tool.name, tool_type=tool.__class__.__name__, agent_name=agent.name, - elapsed_s=_get_elapsed_s(span, start_time), + elapsed_s=_metrics.get_elapsed_s(span, start_time), error=caught_error, ) except Exception: # pylint: disable=broad-exception-caught @@ -227,7 +219,7 @@ async def record_inference_telemetry( finally: inference_error = sys.exc_info()[1] agent = invocation_context.agent - elapsed_s = _get_elapsed_s(tel_ctx.span, start_time) + elapsed_s = _metrics.get_elapsed_s(tel_ctx.span, start_time) try: if agent is not None and tracing._should_emit_native_telemetry(agent): _metrics.record_client_operation_duration( diff --git a/src/google/adk/telemetry/_metrics.py b/src/google/adk/telemetry/_metrics.py index eac65b48ca0..4435462e5c3 100644 --- a/src/google/adk/telemetry/_metrics.py +++ b/src/google/adk/telemetry/_metrics.py @@ -15,6 +15,7 @@ from __future__ import annotations import logging +import time from typing import TYPE_CHECKING from google.adk import version @@ -30,6 +31,10 @@ from google.adk.events.event import Event from google.adk.models.llm_request import LlmRequest from google.adk.models.llm_response import LlmResponse + from opentelemetry.trace import Span + from opentelemetry.util.types import AttributeValue + + from .tracing import GenerateContentSpan logger = logging.getLogger("google_adk." + __name__) @@ -61,6 +66,11 @@ 409.6, ], ) +_workflow_invocation_duration = meter.create_histogram( + "gen_ai.invoke_workflow.duration", + unit="s", + description="Duration of workflow invocations.", +) _tool_execution_duration = meter.create_histogram( "gen_ai.tool.execution.duration", unit="s", @@ -160,6 +170,27 @@ def record_agent_invocation_duration( _agent_invocation_duration.record(elapsed_s, attributes=attrs) +def record_workflow_invocation_duration( + *, + workflow_name: str, + elapsed_s: float, + nested: bool, + error: BaseException | None = None, +) -> None: + """Records the duration of a workflow invocation.""" + attrs: dict[str, AttributeValue] = { + gen_ai_attributes.GEN_AI_OPERATION_NAME: "invoke_workflow", + } + # Root workflow omits the attribute entirely; only nested ones emit it. + if nested: + attrs["gen_ai.workflow.nested"] = True + if error is not None: + attrs[error_attributes.ERROR_TYPE] = type(error).__name__ + if workflow_name: + attrs["gen_ai.workflow.name"] = workflow_name + _workflow_invocation_duration.record(elapsed_s, attributes=attrs) + + def record_agent_request_size( agent_name: str, user_content: types.Content | None ): @@ -303,3 +334,33 @@ def _get_content_size( def _get_provider_name() -> str: return tracing._guess_gemini_system_name() + + +def get_elapsed_s( + span: Span | GenerateContentSpan | None, + fallback_start: float, +) -> float: + """Guarantees consistent time source for duration calculation. + + Note: This must be called with an ended span. + + Args: + span (trace.Span | tracing.GenerateContentSpan | None): The ended span to + extract duration from. + fallback_start (float): Fallback start time in seconds (monotonic). + + Returns: + float: Elapsed duration in seconds. + """ + if span is None: + return time.monotonic() - fallback_start + + span = span.span if hasattr(span, "span") else span + start_ns = getattr(span, "start_time", None) + end_ns = getattr(span, "end_time", None) + + if isinstance(start_ns, int) and isinstance(end_ns, int): + return (end_ns - start_ns) / 1e9 # Convert ns to s + + # Fallback if span times are missing + return time.monotonic() - fallback_start diff --git a/src/google/adk/telemetry/node_tracing.py b/src/google/adk/telemetry/node_tracing.py index 500cb894594..9fc4a75f411 100644 --- a/src/google/adk/telemetry/node_tracing.py +++ b/src/google/adk/telemetry/node_tracing.py @@ -15,24 +15,46 @@ from __future__ import annotations from collections.abc import AsyncIterator +from collections.abc import Iterator from contextlib import asynccontextmanager +from contextlib import contextmanager from dataclasses import dataclass from dataclasses import field +import sys +import time from typing import TYPE_CHECKING from opentelemetry import context as context_api from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_CONVERSATION_ID from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_OPERATION_NAME -from opentelemetry.util.types import Attributes +from opentelemetry.trace import Span +from . import _metrics from ..agents.context import Context from ..workflow._base_node import BaseNode from .tracing import tracer if TYPE_CHECKING: + from opentelemetry.util.types import AttributeValue + + from ..agents.base_agent import BaseAgent from ..events.event import Event from ..workflow._workflow import Workflow +# Span/metric attribute flagging that an `invoke_workflow` span is nested +# within another workflow. Only emitted for nested workflows; the root +# (entrypoint) workflow omits it entirely. +GEN_AI_WORKFLOW_NESTED = "gen_ai.workflow.nested" + +# OTel-context key recording that an entrypoint workflow is already active. It +# rides along the otel_context propagated to child nodes, so only the first +# workflow invoked within an invocation is treated as the root -- nested +# workflows (incl. agents-as-tool that spin up their own runner) see the key +# already set and report nested=true. +_ENTRYPOINT_WORKFLOW_KEY = context_api.create_key( + "adk-entrypoint-workflow-active" +) + @dataclass(frozen=True) class TelemetryContext: @@ -49,12 +71,6 @@ def add_event(self, event: Event) -> None: self._associated_event_ids.append(event.id) -@dataclass -class _SpanMetadata: - name: str - attributes: Attributes - - @asynccontextmanager async def start_as_current_node_span( context: Context, node: BaseNode @@ -83,64 +99,123 @@ async def start_as_current_node_span( Context with the started span. """ - span_metadata = _span_metadata(context, node) - if span_metadata is None: - token = context_api.attach(context.telemetry_context.otel_context) - try: - yield TelemetryContext( - otel_context=context.telemetry_context.otel_context - ) - finally: - context_api.detach(token) - return - - with tracer.start_as_current_span( - span_metadata.name, - attributes=span_metadata.attributes, - context=context.telemetry_context.otel_context, - ) as span: - telemetry_context = TelemetryContext(otel_context=context_api.get_current()) - yield telemetry_context - - if span.is_recording() and len(telemetry_context._associated_event_ids) > 0: - span.set_attribute( - "gcp.vertex.agent.associated_event_ids", - telemetry_context._associated_event_ids, - ) - - -def _span_metadata(context: Context, node: BaseNode) -> _SpanMetadata | None: from ..agents.base_agent import BaseAgent from ..workflow._workflow import Workflow if isinstance(node, BaseAgent): - return None + with _invoke_agent_span(context, node) as tel_ctx: + yield tel_ctx elif isinstance(node, Workflow): - return _workflow_span_metadata(context, node) + with _invoke_workflow_span(context, node) as tel_ctx: + yield tel_ctx else: - return _default_node_span_metadata(context, node) + with _invoke_node_span(context, node) as tel_ctx: + yield tel_ctx + + +@contextmanager +def _invoke_agent_span( + context: Context, agent: BaseAgent +) -> Iterator[TelemetryContext]: + """Passes through an agent node; agents emit their own `invoke_agent` span.""" + del agent + token = context_api.attach(context.telemetry_context.otel_context) + try: + yield TelemetryContext(otel_context=context.telemetry_context.otel_context) + finally: + context_api.detach(token) -def _workflow_span_metadata( +@contextmanager +def _invoke_workflow_span( context: Context, workflow: Workflow -) -> _SpanMetadata: - return _SpanMetadata( - name=f"invoke_workflow {workflow.name}", - attributes={ - GEN_AI_OPERATION_NAME: "invoke_workflow", - "gen_ai.workflow.name": workflow.name, - GEN_AI_CONVERSATION_ID: context.session.id, - }, - ) +) -> Iterator[TelemetryContext]: + """Opens an `invoke_workflow` span plus its duration metric for ``node``.""" + with _use_invoke_workflow_span( + workflow.name, + context.session.id, + otel_context=context.telemetry_context.otel_context, + ) as span: + tel_ctx = TelemetryContext(otel_context=context_api.get_current()) + yield tel_ctx + _maybe_set_associated_events(span, tel_ctx) -def _default_node_span_metadata( +@contextmanager +def _invoke_node_span( context: Context, node: BaseNode -) -> _SpanMetadata: - return _SpanMetadata( - name=f"invoke_node {node.name}", +) -> Iterator[TelemetryContext]: + """Opens an `invoke_node` span for a plain node.""" + with tracer.start_as_current_span( + f"invoke_node {node.name}", attributes={ GEN_AI_OPERATION_NAME: "invoke_node", GEN_AI_CONVERSATION_ID: context.session.id, }, + context=context.telemetry_context.otel_context, + ) as span: + tel_ctx = TelemetryContext(otel_context=context_api.get_current()) + yield tel_ctx + _maybe_set_associated_events(span, tel_ctx) + + +def _maybe_set_associated_events( + span: Span, telemetry_context: TelemetryContext +) -> None: + """Stamps the node's associated event IDs onto its span, if any.""" + if span.is_recording() and len(telemetry_context._associated_event_ids) > 0: + span.set_attribute( + "gcp.vertex.agent.associated_event_ids", + telemetry_context._associated_event_ids, + ) + + +@contextmanager +def _use_invoke_workflow_span( + workflow_name: str, + conversation_id: str, + *, + otel_context: context_api.Context | None = None, +) -> Iterator[Span]: + """Opens an `invoke_workflow {workflow_name}` span.""" + if otel_context is None: + otel_context = context_api.get_current() + # First workflow in the invocation is the root; subsequent ones are nested. + # The flag rides along the otel_context propagated to child nodes, so nested + # workflows see it set. + nested = bool(context_api.get_value(_ENTRYPOINT_WORKFLOW_KEY, otel_context)) + if not nested: + otel_context = context_api.set_value( + _ENTRYPOINT_WORKFLOW_KEY, True, otel_context + ) + attributes: dict[str, AttributeValue] = { + GEN_AI_OPERATION_NAME: "invoke_workflow", + GEN_AI_CONVERSATION_ID: conversation_id, + } + # Root workflow omits the attribute entirely; only nested ones emit it. + if nested: + attributes[GEN_AI_WORKFLOW_NESTED] = True + if workflow_name: + attributes["gen_ai.workflow.name"] = workflow_name + + span_name = ( + f"invoke_workflow {workflow_name}" if workflow_name else "invoke_workflow" ) + + start_s = time.monotonic() + workflow_span: Span | None = None + try: + with tracer.start_as_current_span( + name=span_name, + attributes=attributes, + context=otel_context, + ) as span: + workflow_span = span + yield span + finally: + _metrics.record_workflow_invocation_duration( + workflow_name=workflow_name, + elapsed_s=_metrics.get_elapsed_s(workflow_span, start_s), + nested=nested, + error=sys.exc_info()[1], + ) diff --git a/tests/unittests/telemetry/functional_node_test_cases.py b/tests/unittests/telemetry/functional_node_test_cases.py index 56e58a7fa7a..43063a7e47e 100644 --- a/tests/unittests/telemetry/functional_node_test_cases.py +++ b/tests/unittests/telemetry/functional_node_test_cases.py @@ -1513,525 +1513,433 @@ EXPECTED_STABLE_NO_CAPTURE_V2 = SpanDigest( - name="invocation", - attributes={}, + name=f"invoke_workflow {WORKFLOW_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( - name=f"invoke_workflow {WORKFLOW_NAME}", + name=f"invoke_agent {AGENT_NAME}", attributes={ - "gen_ai.operation.name": "invoke_workflow", - "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.description": AGENT_DESCRIPTION, + "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, }, children=[ SpanDigest( - name=f"invoke_agent {AGENT_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.description": AGENT_DESCRIPTION, - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.system": "gemini", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], }, + logs=[ + LogDigest( + event_name=GEN_AI_CHOICE_EVENT, + body={ + "content": "", + "index": 0, + "finish_reason": "STOP", + }, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, + body={"content": ""}, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={"content": ""}, + attributes={"gen_ai.system": "gemini"}, + ), + ], children=[ SpanDigest( - name="generate_content mock", + name=f"execute_tool {TOOL_NAME}", attributes={ - "gen_ai.system": "gemini", - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.description": ( + TOOL_DESCRIPTION + ), + "gen_ai.tool.name": TOOL_NAME, + "gen_ai.tool.type": "FunctionTool", + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.tool_call_args": "{}", + "gen_ai.tool.call.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], + "gcp.vertex.agent.tool_response": "{}", }, - logs=[ - LogDigest( - event_name=GEN_AI_CHOICE_EVENT, - body={ - "content": "", - "index": 0, - "finish_reason": "STOP", - }, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, - body={"content": ""}, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={"content": ""}, - attributes={ - "gen_ai.system": "gemini" - }, - ), - ], - children=[ - SpanDigest( - name=f"execute_tool {TOOL_NAME}", - attributes={ - "gen_ai.operation.name": ( - "execute_tool" - ), - "gen_ai.tool.description": ( - TOOL_DESCRIPTION - ), - "gen_ai.tool.name": TOOL_NAME, - "gen_ai.tool.type": ( - "FunctionTool" - ), - "gcp.vertex.agent.llm_request": ( - "{}" - ), - "gcp.vertex.agent.llm_response": ( - "{}" - ), - "gcp.vertex.agent.tool_call_args": ( - "{}" - ), - "gen_ai.tool.call.id": PRESENT, - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.tool_response": ( - "{}" - ), - }, - ), - ], ), ], ), + ], + ), + SpanDigest( + name="call_llm", + attributes={ + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], + }, + children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.system": "gemini", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], }, - children=[ - SpanDigest( - name="generate_content mock", - attributes={ - "gen_ai.system": "gemini", - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], + logs=[ + LogDigest( + event_name=GEN_AI_CHOICE_EVENT, + body={ + "content": "", + "index": 0, + "finish_reason": "STOP", }, - logs=[ - LogDigest( - event_name=GEN_AI_CHOICE_EVENT, - body={ - "content": "", - "index": 0, - "finish_reason": "STOP", - }, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, - body={"content": ""}, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={"content": ""}, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={"content": ""}, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={"content": ""}, - attributes={ - "gen_ai.system": "gemini" - }, - ), - ], + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, + body={"content": ""}, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={"content": ""}, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={"content": ""}, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={"content": ""}, + attributes={"gen_ai.system": "gemini"}, ), ], ), ], ), - SpanDigest( - name=f"invoke_node {NODE_NAME}", - attributes={ - "gen_ai.operation.name": "invoke_node", - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.associated_event_ids": PRESENT, - }, - ), ], ), + SpanDigest( + name=f"invoke_node {NODE_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_node", + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.associated_event_ids": PRESENT, + }, + ), ], ) EXPECTED_STABLE_CAPTURE_V2 = SpanDigest( - name="invocation", - attributes={}, + name=f"invoke_workflow {WORKFLOW_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( - name=f"invoke_workflow {WORKFLOW_NAME}", + name=f"invoke_agent {AGENT_NAME}", attributes={ - "gen_ai.operation.name": "invoke_workflow", - "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.description": AGENT_DESCRIPTION, + "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, }, children=[ SpanDigest( - name=f"invoke_agent {AGENT_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.description": AGENT_DESCRIPTION, - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.system": "gemini", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], }, + logs=[ + LogDigest( + event_name=GEN_AI_CHOICE_EVENT, + body={ + "content": { + "parts": [{ + "function_call": { + "args": TOOL_ARGS, + "name": TOOL_NAME, + } + }], + "role": "model", + }, + "index": 0, + "finish_reason": "STOP", + }, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, + body={"content": _NODE_SYSTEM_INSTRUCTION}, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={ + "content": { + "parts": [ + {"text": _AGENT_USER_INPUT} + ], + "role": "user", + } + }, + attributes={ + "gen_ai.system": "gemini", + "user.id": "some_user", + }, + ), + ], children=[ SpanDigest( - name="generate_content mock", + name=f"execute_tool {TOOL_NAME}", attributes={ - "gen_ai.system": "gemini", - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.description": ( + TOOL_DESCRIPTION + ), + "gen_ai.tool.name": TOOL_NAME, + "gen_ai.tool.type": "FunctionTool", + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.tool_call_args": "{}", + "gen_ai.tool.call.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], + "gcp.vertex.agent.tool_response": "{}", }, - logs=[ - LogDigest( - event_name=GEN_AI_CHOICE_EVENT, - body={ - "content": { - "parts": [{ - "function_call": { - "args": TOOL_ARGS, - "name": TOOL_NAME, - } - }], - "role": "model", - }, - "index": 0, - "finish_reason": "STOP", - }, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, - body={ - "content": ( - _NODE_SYSTEM_INSTRUCTION - ) - }, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={ - "content": { - "parts": [{ - "text": ( - _AGENT_USER_INPUT - ) - }], - "role": "user", - } - }, - attributes={ - "gen_ai.system": "gemini", - "user.id": "some_user", - }, - ), - ], - children=[ - SpanDigest( - name=f"execute_tool {TOOL_NAME}", - attributes={ - "gen_ai.operation.name": ( - "execute_tool" - ), - "gen_ai.tool.description": ( - TOOL_DESCRIPTION - ), - "gen_ai.tool.name": TOOL_NAME, - "gen_ai.tool.type": ( - "FunctionTool" - ), - "gcp.vertex.agent.llm_request": ( - "{}" - ), - "gcp.vertex.agent.llm_response": ( - "{}" - ), - "gcp.vertex.agent.tool_call_args": ( - "{}" - ), - "gen_ai.tool.call.id": PRESENT, - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.tool_response": ( - "{}" - ), - }, - ), - ], ), ], ), + ], + ), + SpanDigest( + name="call_llm", + attributes={ + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], + }, + children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.system": "gemini", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=GEN_AI_CHOICE_EVENT, + body={ + "content": { + "parts": [{"text": FINAL_TEXT}], + "role": "model", + }, + "index": 0, + "finish_reason": "STOP", + }, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, + body={"content": _NODE_SYSTEM_INSTRUCTION}, + attributes={"gen_ai.system": "gemini"}, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={ + "content": { + "parts": [{ + "function_call": { + "args": TOOL_ARGS, + "name": TOOL_NAME, + } + }], + "role": "model", + } + }, attributes={ "gen_ai.system": "gemini", - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], + "user.id": "some_user", }, - logs=[ - LogDigest( - event_name=GEN_AI_CHOICE_EVENT, - body={ - "content": { - "parts": [ - {"text": FINAL_TEXT} - ], - "role": "model", - }, - "index": 0, - "finish_reason": "STOP", - }, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_SYSTEM_MESSAGE_EVENT, - body={ - "content": ( - _NODE_SYSTEM_INSTRUCTION - ) - }, - attributes={ - "gen_ai.system": "gemini" - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={ - "content": { - "parts": [{ - "function_call": { - "args": TOOL_ARGS, - "name": TOOL_NAME, - } - }], - "role": "model", - } - }, - attributes={ - "gen_ai.system": "gemini", - "user.id": "some_user", - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={ - "content": { - "parts": [{ - "function_response": { - "name": TOOL_NAME, - "response": { - "result": ( - TOOL_RESULT - ) - }, - } - }], - "role": "user", - } - }, - attributes={ - "gen_ai.system": "gemini", - "user.id": "some_user", - }, - ), - LogDigest( - event_name=GEN_AI_USER_MESSAGE_EVENT, - body={ - "content": { - "parts": [{ - "text": ( - _AGENT_USER_INPUT - ) - }], - "role": "user", + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={ + "content": { + "parts": [{ + "function_response": { + "name": TOOL_NAME, + "response": { + "result": TOOL_RESULT + }, } - }, - attributes={ - "gen_ai.system": "gemini", - "user.id": "some_user", - }, - ), - ], + }], + "role": "user", + } + }, + attributes={ + "gen_ai.system": "gemini", + "user.id": "some_user", + }, + ), + LogDigest( + event_name=GEN_AI_USER_MESSAGE_EVENT, + body={ + "content": { + "parts": [ + {"text": _AGENT_USER_INPUT} + ], + "role": "user", + } + }, + attributes={ + "gen_ai.system": "gemini", + "user.id": "some_user", + }, ), ], ), ], ), - SpanDigest( - name=f"invoke_node {NODE_NAME}", - attributes={ - "gen_ai.operation.name": "invoke_node", - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.associated_event_ids": PRESENT, - }, - ), ], ), + SpanDigest( + name=f"invoke_node {NODE_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_node", + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.associated_event_ids": PRESENT, + }, + ), ], ) EXPECTED_EXPERIMENTAL_NO_CONTENT_V2 = SpanDigest( - name="invocation", - attributes={}, + name=f"invoke_workflow {WORKFLOW_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( - name=f"invoke_workflow {WORKFLOW_NAME}", + name=f"invoke_agent {AGENT_NAME}", attributes={ - "gen_ai.operation.name": "invoke_workflow", - "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.description": AGENT_DESCRIPTION, + "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, }, children=[ SpanDigest( - name=f"invoke_agent {AGENT_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.description": AGENT_DESCRIPTION, - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.tool.definitions": [{ + "name": TOOL_NAME, + "description": TOOL_DESCRIPTION, + "type": "function", + }], }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, @@ -2047,197 +1955,66 @@ "type": "function", }], }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.tool.definitions": [{ - "name": TOOL_NAME, - "description": ( - TOOL_DESCRIPTION - ), - "type": "function", - }], - }, - ), - ], - children=[ - SpanDigest( - name=f"execute_tool {TOOL_NAME}", - attributes={ - "gen_ai.operation.name": ( - "execute_tool" - ), - "gen_ai.tool.description": ( - TOOL_DESCRIPTION - ), - "gen_ai.tool.name": TOOL_NAME, - "gen_ai.tool.type": ( - "FunctionTool" - ), - "gcp.vertex.agent.llm_request": ( - "{}" - ), - "gcp.vertex.agent.llm_response": ( - "{}" - ), - "gcp.vertex.agent.tool_call_args": ( - "{}" - ), - "gen_ai.tool.call.id": PRESENT, - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.tool_response": ( - "{}" - ), - }, - ), - ], ), ], - ), - SpanDigest( - name="call_llm", - attributes={ - "gen_ai.system": "gcp.vertex.agent", - "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, - "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", - "gen_ai.response.finish_reasons": ["stop"], - }, children=[ SpanDigest( - name="generate_content mock", + name=f"execute_tool {TOOL_NAME}", attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.description": ( + TOOL_DESCRIPTION + ), + "gen_ai.tool.name": TOOL_NAME, + "gen_ai.tool.type": "FunctionTool", + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.tool_call_args": "{}", + "gen_ai.tool.call.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.tool.definitions": [{ - "name": TOOL_NAME, - "description": TOOL_DESCRIPTION, - "type": "function", - }], + "gcp.vertex.agent.tool_response": "{}", }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.tool.definitions": [{ - "name": TOOL_NAME, - "description": ( - TOOL_DESCRIPTION - ), - "type": "function", - }], - }, - ), - ], ), ], ), ], ), SpanDigest( - name=f"invoke_node {NODE_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_node", - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.associated_event_ids": PRESENT, - }, - ), - ], - ), - ], -) - - -EXPECTED_EXPERIMENTAL_SPAN_ONLY_V2 = SpanDigest( - name="invocation", - attributes={}, - children=[ - SpanDigest( - name=f"invoke_workflow {WORKFLOW_NAME}", - attributes={ - "gen_ai.operation.name": "invoke_workflow", - "gen_ai.workflow.name": WORKFLOW_NAME, - "gen_ai.conversation.id": PRESENT, - }, - children=[ - SpanDigest( - name=f"invoke_agent {AGENT_NAME}", - attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.description": AGENT_DESCRIPTION, - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.tool.definitions": [{ + "name": TOOL_NAME, + "description": TOOL_DESCRIPTION, + "type": "function", + }], }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, @@ -2247,101 +2024,89 @@ "gen_ai.response.finish_reasons": [ "stop" ], - "gen_ai.input.messages": ( - _TURN_1_INPUT_MESSAGES - ), - "gen_ai.system_instructions": ( - _SYSTEM_INSTRUCTIONS - ), - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_FULL - ], - "gen_ai.output.messages": ( - _TURN_1_OUTPUT_MESSAGES - ), - }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_NO_CONTENT - ], - }, - ), - ], - children=[ - SpanDigest( - name=f"execute_tool {TOOL_NAME}", - attributes={ - "gen_ai.operation.name": ( - "execute_tool" - ), - "gen_ai.tool.description": ( - TOOL_DESCRIPTION - ), - "gen_ai.tool.name": TOOL_NAME, - "gen_ai.tool.type": ( - "FunctionTool" - ), - "gcp.vertex.agent.llm_request": ( - "{}" - ), - "gcp.vertex.agent.llm_response": ( - "{}" - ), - "gcp.vertex.agent.tool_call_args": ( - "{}" - ), - "gen_ai.tool.call.id": PRESENT, - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.tool_response": ( - "{}" - ), - }, - ), - ], + "gen_ai.tool.definitions": [{ + "name": TOOL_NAME, + "description": TOOL_DESCRIPTION, + "type": "function", + }], + }, ), ], ), + ], + ), + ], + ), + SpanDigest( + name=f"invoke_node {NODE_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_node", + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.associated_event_ids": PRESENT, + }, + ), + ], +) + + +EXPECTED_EXPERIMENTAL_SPAN_ONLY_V2 = SpanDigest( + name=f"invoke_workflow {WORKFLOW_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.conversation.id": PRESENT, + }, + children=[ + SpanDigest( + name=f"invoke_agent {AGENT_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.description": AGENT_DESCRIPTION, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, + children=[ + SpanDigest( + name="call_llm", + attributes={ + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], + }, + children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.input.messages": _TURN_1_INPUT_MESSAGES, + "gen_ai.system_instructions": ( + _SYSTEM_INSTRUCTIONS + ), + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_FULL + ], + "gen_ai.output.messages": ( + _TURN_1_OUTPUT_MESSAGES + ), }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, @@ -2351,107 +2116,161 @@ "gen_ai.response.finish_reasons": [ "stop" ], - "gen_ai.input.messages": ( - _TURN_2_INPUT_MESSAGES - ), - "gen_ai.system_instructions": ( - _SYSTEM_INSTRUCTIONS - ), "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_FULL + _TOOL_DEFINITION_NO_CONTENT ], - "gen_ai.output.messages": ( - _TURN_2_OUTPUT_MESSAGES - ), }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_NO_CONTENT - ], - }, - ), - ], + ), + ], + children=[ + SpanDigest( + name=f"execute_tool {TOOL_NAME}", + attributes={ + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.description": ( + TOOL_DESCRIPTION + ), + "gen_ai.tool.name": TOOL_NAME, + "gen_ai.tool.type": "FunctionTool", + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.tool_call_args": "{}", + "gen_ai.tool.call.id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.tool_response": "{}", + }, ), ], ), ], ), SpanDigest( - name=f"invoke_node {NODE_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_node", - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.associated_event_ids": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, + children=[ + SpanDigest( + name="generate_content mock", + attributes={ + "gen_ai.operation.name": "generate_content", + "gen_ai.request.model": "mock", + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.invocation_id": PRESENT, + "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.input.messages": _TURN_2_INPUT_MESSAGES, + "gen_ai.system_instructions": ( + _SYSTEM_INSTRUCTIONS + ), + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_FULL + ], + "gen_ai.output.messages": ( + _TURN_2_OUTPUT_MESSAGES + ), + }, + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, + attributes={ + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.invocation_id": ( + PRESENT + ), + "gen_ai.response.finish_reasons": [ + "stop" + ], + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_NO_CONTENT + ], + }, + ), + ], + ), + ], ), ], ), + SpanDigest( + name=f"invoke_node {NODE_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_node", + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.associated_event_ids": PRESENT, + }, + ), ], ) EXPECTED_EXPERIMENTAL_EVENT_ONLY_V2 = SpanDigest( - name="invocation", - attributes={}, + name=f"invoke_workflow {WORKFLOW_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( - name=f"invoke_workflow {WORKFLOW_NAME}", + name=f"invoke_agent {AGENT_NAME}", attributes={ - "gen_ai.operation.name": "invoke_workflow", - "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.description": AGENT_DESCRIPTION, + "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, }, children=[ SpanDigest( - name=f"invoke_agent {AGENT_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.description": AGENT_DESCRIPTION, - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_NO_CONTENT + ], }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, + "user.id": "some_user", "gcp.vertex.agent.event_id": PRESENT, "gcp.vertex.agent.invocation_id": ( PRESENT @@ -2459,104 +2278,80 @@ "gen_ai.response.finish_reasons": [ "stop" ], + "gen_ai.input.messages": ( + _TURN_1_INPUT_MESSAGES + ), + "gen_ai.system_instructions": ( + _SYSTEM_INSTRUCTIONS + ), "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_NO_CONTENT + _TOOL_DEFINITION_FULL ], - }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "user.id": "some_user", - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.input.messages": ( - _TURN_1_INPUT_MESSAGES - ), - "gen_ai.system_instructions": ( - _SYSTEM_INSTRUCTIONS - ), - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_FULL - ], - "gen_ai.output.messages": ( - _TURN_1_OUTPUT_MESSAGES - ), - }, - ), - ], - children=[ - SpanDigest( - name=f"execute_tool {TOOL_NAME}", - attributes={ - "gen_ai.operation.name": ( - "execute_tool" - ), - "gen_ai.tool.description": ( - TOOL_DESCRIPTION - ), - "gen_ai.tool.name": TOOL_NAME, - "gen_ai.tool.type": ( - "FunctionTool" - ), - "gcp.vertex.agent.llm_request": ( - "{}" - ), - "gcp.vertex.agent.llm_response": ( - "{}" - ), - "gcp.vertex.agent.tool_call_args": ( - "{}" - ), - "gen_ai.tool.call.id": PRESENT, - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.tool_response": ( - "{}" - ), - }, + "gen_ai.output.messages": ( + _TURN_1_OUTPUT_MESSAGES ), - ], + }, + ), + ], + children=[ + SpanDigest( + name=f"execute_tool {TOOL_NAME}", + attributes={ + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.description": ( + TOOL_DESCRIPTION + ), + "gen_ai.tool.name": TOOL_NAME, + "gen_ai.tool.type": "FunctionTool", + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.tool_call_args": "{}", + "gen_ai.tool.call.id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.tool_response": "{}", + }, ), ], ), + ], + ), + SpanDigest( + name="call_llm", + attributes={ + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], + }, + children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], - }, - children=[ - SpanDigest( - name="generate_content mock", + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_NO_CONTENT + ], + }, + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, + "user.id": "some_user", "gcp.vertex.agent.event_id": PRESENT, "gcp.vertex.agent.invocation_id": ( PRESENT @@ -2564,108 +2359,99 @@ "gen_ai.response.finish_reasons": [ "stop" ], + "gen_ai.input.messages": ( + _TURN_2_INPUT_MESSAGES + ), + "gen_ai.system_instructions": ( + _SYSTEM_INSTRUCTIONS + ), "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_NO_CONTENT + _TOOL_DEFINITION_FULL ], - }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "user.id": "some_user", - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.input.messages": ( - _TURN_2_INPUT_MESSAGES - ), - "gen_ai.system_instructions": ( - _SYSTEM_INSTRUCTIONS - ), - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_FULL - ], - "gen_ai.output.messages": ( - _TURN_2_OUTPUT_MESSAGES - ), - }, + "gen_ai.output.messages": ( + _TURN_2_OUTPUT_MESSAGES ), - ], + }, ), ], ), ], ), - SpanDigest( - name=f"invoke_node {NODE_NAME}", - attributes={ - "gen_ai.operation.name": "invoke_node", - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.associated_event_ids": PRESENT, - }, - ), ], ), + SpanDigest( + name=f"invoke_node {NODE_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_node", + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.associated_event_ids": PRESENT, + }, + ), ], ) EXPECTED_EXPERIMENTAL_SPAN_AND_EVENT_V2 = SpanDigest( - name="invocation", - attributes={}, + name=f"invoke_workflow {WORKFLOW_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( - name=f"invoke_workflow {WORKFLOW_NAME}", + name=f"invoke_agent {AGENT_NAME}", attributes={ - "gen_ai.operation.name": "invoke_workflow", - "gen_ai.workflow.name": WORKFLOW_NAME, + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.description": AGENT_DESCRIPTION, + "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, }, children=[ SpanDigest( - name=f"invoke_agent {AGENT_NAME}", + name="call_llm", attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.description": AGENT_DESCRIPTION, - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": PRESENT, + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], }, children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.input.messages": _TURN_1_INPUT_MESSAGES, + "gen_ai.system_instructions": ( + _SYSTEM_INSTRUCTIONS + ), + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_FULL + ], + "gen_ai.output.messages": ( + _TURN_1_OUTPUT_MESSAGES + ), }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, + "user.id": "some_user", "gcp.vertex.agent.event_id": PRESENT, "gcp.vertex.agent.invocation_id": ( PRESENT @@ -2686,100 +2472,74 @@ _TURN_1_OUTPUT_MESSAGES ), }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "user.id": "some_user", - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.input.messages": ( - _TURN_1_INPUT_MESSAGES - ), - "gen_ai.system_instructions": ( - _SYSTEM_INSTRUCTIONS - ), - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_FULL - ], - "gen_ai.output.messages": ( - _TURN_1_OUTPUT_MESSAGES - ), - }, - ), - ], - children=[ - SpanDigest( - name=f"execute_tool {TOOL_NAME}", - attributes={ - "gen_ai.operation.name": ( - "execute_tool" - ), - "gen_ai.tool.description": ( - TOOL_DESCRIPTION - ), - "gen_ai.tool.name": TOOL_NAME, - "gen_ai.tool.type": ( - "FunctionTool" - ), - "gcp.vertex.agent.llm_request": ( - "{}" - ), - "gcp.vertex.agent.llm_response": ( - "{}" - ), - "gcp.vertex.agent.tool_call_args": ( - "{}" - ), - "gen_ai.tool.call.id": PRESENT, - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.tool_response": ( - "{}" - ), - }, - ), - ], + ), + ], + children=[ + SpanDigest( + name=f"execute_tool {TOOL_NAME}", + attributes={ + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.description": ( + TOOL_DESCRIPTION + ), + "gen_ai.tool.name": TOOL_NAME, + "gen_ai.tool.type": "FunctionTool", + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.tool_call_args": "{}", + "gen_ai.tool.call.id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.tool_response": "{}", + }, ), ], ), + ], + ), + SpanDigest( + name="call_llm", + attributes={ + "gen_ai.system": "gcp.vertex.agent", + "gen_ai.request.model": "mock", + "gcp.vertex.agent.invocation_id": PRESENT, + "gcp.vertex.agent.session_id": PRESENT, + "gcp.vertex.agent.event_id": PRESENT, + "gcp.vertex.agent.llm_request": "{}", + "gcp.vertex.agent.llm_response": "{}", + "gen_ai.response.finish_reasons": ["stop"], + }, + children=[ SpanDigest( - name="call_llm", + name="generate_content mock", attributes={ - "gen_ai.system": "gcp.vertex.agent", + "gen_ai.operation.name": "generate_content", "gen_ai.request.model": "mock", - "gcp.vertex.agent.invocation_id": PRESENT, - "gcp.vertex.agent.session_id": PRESENT, + "gen_ai.agent.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, "gcp.vertex.agent.event_id": PRESENT, - "gcp.vertex.agent.llm_request": "{}", - "gcp.vertex.agent.llm_response": "{}", + "gcp.vertex.agent.invocation_id": PRESENT, "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.input.messages": _TURN_2_INPUT_MESSAGES, + "gen_ai.system_instructions": ( + _SYSTEM_INSTRUCTIONS + ), + "gen_ai.tool.definitions": [ + _TOOL_DEFINITION_FULL + ], + "gen_ai.output.messages": ( + _TURN_2_OUTPUT_MESSAGES + ), }, - children=[ - SpanDigest( - name="generate_content mock", + logs=[ + LogDigest( + event_name=( + GEN_AI_COMPLETION_DETAILS_EVENT + ), + body=None, attributes={ - "gen_ai.operation.name": ( - "generate_content" - ), - "gen_ai.request.model": "mock", "gen_ai.agent.name": AGENT_NAME, "gen_ai.conversation.id": PRESENT, + "user.id": "some_user", "gcp.vertex.agent.event_id": PRESENT, "gcp.vertex.agent.invocation_id": ( PRESENT @@ -2800,57 +2560,21 @@ _TURN_2_OUTPUT_MESSAGES ), }, - logs=[ - LogDigest( - event_name=( - GEN_AI_COMPLETION_DETAILS_EVENT - ), - body=None, - attributes={ - "gen_ai.agent.name": AGENT_NAME, - "gen_ai.conversation.id": ( - PRESENT - ), - "user.id": "some_user", - "gcp.vertex.agent.event_id": ( - PRESENT - ), - "gcp.vertex.agent.invocation_id": ( - PRESENT - ), - "gen_ai.response.finish_reasons": [ - "stop" - ], - "gen_ai.input.messages": ( - _TURN_2_INPUT_MESSAGES - ), - "gen_ai.system_instructions": ( - _SYSTEM_INSTRUCTIONS - ), - "gen_ai.tool.definitions": [ - _TOOL_DEFINITION_FULL - ], - "gen_ai.output.messages": ( - _TURN_2_OUTPUT_MESSAGES - ), - }, - ), - ], ), ], ), ], ), - SpanDigest( - name=f"invoke_node {NODE_NAME}", - attributes={ - "gen_ai.operation.name": "invoke_node", - "gen_ai.conversation.id": PRESENT, - "gcp.vertex.agent.associated_event_ids": PRESENT, - }, - ), ], ), + SpanDigest( + name=f"invoke_node {NODE_NAME}", + attributes={ + "gen_ai.operation.name": "invoke_node", + "gen_ai.conversation.id": PRESENT, + "gcp.vertex.agent.associated_event_ids": PRESENT, + }, + ), ], ) @@ -2900,6 +2624,15 @@ value=NON_DETERMINISTIC, ), }), + "gen_ai.invoke_workflow.duration": frozenset({ + MetricPoint( + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + }, + value=NON_DETERMINISTIC, + ), + }), } @@ -2947,6 +2680,15 @@ value=NON_DETERMINISTIC, ), }), + "gen_ai.invoke_workflow.duration": frozenset({ + MetricPoint( + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": WORKFLOW_NAME, + }, + value=NON_DETERMINISTIC, + ), + }), } diff --git a/tests/unittests/telemetry/functional_test_cases.py b/tests/unittests/telemetry/functional_test_cases.py index 44c352a4c88..66cab944903 100644 --- a/tests/unittests/telemetry/functional_test_cases.py +++ b/tests/unittests/telemetry/functional_test_cases.py @@ -1275,8 +1275,12 @@ EXPECTED_STABLE_NO_CAPTURE_V2 = SpanDigest( - name="invocation", - attributes={}, + name="invoke_workflow some_root_agent", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( name="invoke_agent some_root_agent", @@ -1421,8 +1425,12 @@ EXPECTED_STABLE_CAPTURE_V2 = SpanDigest( - name="invocation", - attributes={}, + name="invoke_workflow some_root_agent", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( name="invoke_agent some_root_agent", @@ -1622,8 +1630,12 @@ EXPECTED_EXPERIMENTAL_NO_CONTENT_V2 = SpanDigest( - name="invocation", - attributes={}, + name="invoke_workflow some_root_agent", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( name="invoke_agent some_root_agent", @@ -1768,8 +1780,12 @@ EXPECTED_EXPERIMENTAL_SPAN_ONLY_V2 = SpanDigest( - name="invocation", - attributes={}, + name="invoke_workflow some_root_agent", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( name="invoke_agent some_root_agent", @@ -1920,8 +1936,12 @@ EXPECTED_EXPERIMENTAL_EVENT_ONLY_V2 = SpanDigest( - name="invocation", - attributes={}, + name="invoke_workflow some_root_agent", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( name="invoke_agent some_root_agent", @@ -2078,8 +2098,12 @@ EXPECTED_EXPERIMENTAL_SPAN_AND_EVENT_V2 = SpanDigest( - name="invocation", - attributes={}, + name="invoke_workflow some_root_agent", + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + "gen_ai.conversation.id": PRESENT, + }, children=[ SpanDigest( name="invoke_agent some_root_agent", @@ -2341,6 +2365,15 @@ value=NON_DETERMINISTIC, ), }), + "gen_ai.invoke_workflow.duration": frozenset({ + MetricPoint( + attributes={ + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": AGENT_NAME, + }, + value=NON_DETERMINISTIC, + ), + }), } diff --git a/tests/unittests/telemetry/functional_test_helpers.py b/tests/unittests/telemetry/functional_test_helpers.py index e4ae30a3607..96a29ab5375 100644 --- a/tests/unittests/telemetry/functional_test_helpers.py +++ b/tests/unittests/telemetry/functional_test_helpers.py @@ -339,6 +339,11 @@ class HistogramSpec(NamedTuple): attr="_client_token_usage", metric_name="gen_ai.client.token.usage", ), + HistogramSpec( + module=_metrics, + attr="_workflow_invocation_duration", + metric_name="gen_ai.invoke_workflow.duration", + ), ) diff --git a/tests/unittests/telemetry/test_instrumentation.py b/tests/unittests/telemetry/test_instrumentation.py index 8711aa979ef..dc41adb51fd 100644 --- a/tests/unittests/telemetry/test_instrumentation.py +++ b/tests/unittests/telemetry/test_instrumentation.py @@ -18,6 +18,7 @@ from unittest import mock from google.adk.telemetry import _instrumentation +from google.adk.telemetry import _metrics from opentelemetry import trace import pytest @@ -26,7 +27,7 @@ def test_get_elapsed_s_span_none(): """Tests fallback when span is None.""" start_time = 10.0 with mock.patch("time.monotonic", return_value=12.0): - elapsed = _instrumentation._get_elapsed_s(None, start_time) + elapsed = _metrics.get_elapsed_s(None, start_time) assert elapsed == 2.0 # 12 - 10 @@ -35,7 +36,7 @@ def test_get_elapsed_s_span_valid(): mock_span = mock.MagicMock(spec=trace.Span) mock_span.start_time = 1000000000 # 1s in ns mock_span.end_time = 2000000000 # 2s in ns - elapsed = _instrumentation._get_elapsed_s(mock_span, time.monotonic()) + elapsed = _metrics.get_elapsed_s(mock_span, time.monotonic()) assert elapsed == 1.0 # (2 - 1) s @@ -46,7 +47,7 @@ def test_get_elapsed_s_span_missing_start(): mock_span.end_time = 2000000000 start_time = 10.0 with mock.patch("time.monotonic", return_value=12.0): - elapsed = _instrumentation._get_elapsed_s(mock_span, start_time) + elapsed = _metrics.get_elapsed_s(mock_span, start_time) assert elapsed == 2.0 @@ -57,7 +58,7 @@ def test_get_elapsed_s_span_missing_end(): del mock_span.end_time start_time = 10.0 with mock.patch("time.monotonic", return_value=12.0): - elapsed = _instrumentation._get_elapsed_s(mock_span, start_time) + elapsed = _metrics.get_elapsed_s(mock_span, start_time) assert elapsed == 2.0 @@ -68,7 +69,7 @@ def test_get_elapsed_s_span_non_int_start(): mock_span.end_time = 2000000000 start_time = 10.0 with mock.patch("time.monotonic", return_value=12.0): - elapsed = _instrumentation._get_elapsed_s(mock_span, start_time) + elapsed = _metrics.get_elapsed_s(mock_span, start_time) assert elapsed == 2.0 @@ -79,7 +80,7 @@ def test_get_elapsed_s_span_non_int_end(): mock_span.end_time = 2000000000.0 start_time = 10.0 with mock.patch("time.monotonic", return_value=12.0): - elapsed = _instrumentation._get_elapsed_s(mock_span, start_time) + elapsed = _metrics.get_elapsed_s(mock_span, start_time) assert elapsed == 2.0 diff --git a/tests/unittests/telemetry/test_metrics.py b/tests/unittests/telemetry/test_metrics.py index 30778d06a5a..8d1adb0a8d0 100644 --- a/tests/unittests/telemetry/test_metrics.py +++ b/tests/unittests/telemetry/test_metrics.py @@ -27,6 +27,7 @@ def _mock_meter_setup(monkeypatch): """Sets up mock meter and histograms for testing.""" mock_meter = mock.MagicMock() agent_duration_hist = mock.MagicMock(spec=metrics.Histogram) + workflow_duration_hist = mock.MagicMock(spec=metrics.Histogram) tool_duration_hist = mock.MagicMock(spec=metrics.Histogram) request_size_hist = mock.MagicMock(spec=metrics.Histogram) response_size_hist = mock.MagicMock(spec=metrics.Histogram) @@ -35,6 +36,7 @@ def _mock_meter_setup(monkeypatch): client_token_usage_hist = mock.MagicMock(spec=metrics.Histogram) agent_duration_hist.name = "agent_invocation_duration" + workflow_duration_hist.name = "workflow_invocation_duration" tool_duration_hist.name = "tool_execution_duration" request_size_hist.name = "agent_request_size" response_size_hist.name = "agent_response_size" @@ -45,6 +47,8 @@ def _mock_meter_setup(monkeypatch): def create_histogram_side_effect(name, **_kwargs): if name == "gen_ai.agent.invocation.duration": return agent_duration_hist + elif name == "gen_ai.invoke_workflow.duration": + return workflow_duration_hist elif name == "gen_ai.tool.execution.duration": return tool_duration_hist elif name == "gen_ai.agent.request.size": @@ -66,6 +70,9 @@ def create_histogram_side_effect(name, **_kwargs): monkeypatch.setattr( _metrics, "_agent_invocation_duration", agent_duration_hist ) + monkeypatch.setattr( + _metrics, "_workflow_invocation_duration", workflow_duration_hist + ) monkeypatch.setattr(_metrics, "_tool_execution_duration", tool_duration_hist) monkeypatch.setattr(_metrics, "_agent_request_size", request_size_hist) monkeypatch.setattr(_metrics, "_agent_response_size", response_size_hist) @@ -78,6 +85,7 @@ def create_histogram_side_effect(name, **_kwargs): return { "meter": mock_meter, "agent_duration": agent_duration_hist, + "workflow_duration": workflow_duration_hist, "tool_duration": tool_duration_hist, "request_size": request_size_hist, "response_size": response_size_hist, @@ -131,6 +139,40 @@ def test_record_agent_invocation_duration_with_error(mock_meter_setup): assert kwargs["attributes"]["error.type"] == "ValueError" +def test_record_workflow_invocation_duration_root(mock_meter_setup): + """Tests record_workflow_invocation_duration omits nested for the root.""" + _metrics.record_workflow_invocation_duration( + workflow_name="my_workflow", + elapsed_s=1.0, + nested=False, + ) + hist = mock_meter_setup["workflow_duration"] + hist.record.assert_called_once() + args, kwargs = hist.record.call_args + assert args[0] == 1.0 + assert kwargs["attributes"] == { + "gen_ai.operation.name": "invoke_workflow", + "gen_ai.workflow.name": "my_workflow", + } + + +def test_record_workflow_invocation_duration_nested_with_error( + mock_meter_setup, +): + """Tests record_workflow_invocation_duration records nested + error.""" + _metrics.record_workflow_invocation_duration( + workflow_name="nested_workflow", + elapsed_s=2.0, + nested=True, + error=ValueError("boom"), + ) + hist = mock_meter_setup["workflow_duration"] + hist.record.assert_called_once() + _, kwargs = hist.record.call_args + assert kwargs["attributes"]["gen_ai.workflow.nested"] is True + assert kwargs["attributes"]["error.type"] == "ValueError" + + def test_record_agent_response_size(mock_meter_setup): """Tests record_agent_response_size records correctly.""" response_text = "response"