Skip to content

Commit c6dec00

Browse files
google-genai-botcopybara-github
authored andcommitted
feat(telemetry): add gen_ai.invoke_agent.{inference,tool}_calls metrics
Adds the two per-invocation call-count metrics from semconv #336 (open-telemetry/semantic-conventions-genai#336): * gen_ai.invoke_agent.inference_calls: model calls in one invoke_agent span. * gen_ai.invoke_agent.tool_calls: tool calls in one invoke_agent span. Both are counted on the per-span TelemetryContext at the call sites (record_inference_telemetry, record_tool_execution) and flushed in record_agent_invocation, keyed by gen_ai.agent.name. One point per invoke_agent span, so a multi-agent invocation does not double count. Failed calls still count; only client-side tool calls count. Always emitted (a no-op without a MeterProvider). PiperOrigin-RevId: 940760757
1 parent 196f770 commit c6dec00

7 files changed

Lines changed: 5 additions & 149 deletions

File tree

src/google/adk/agents/invocation_context.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from typing import Any
1919
from typing import cast
2020
from typing import Optional
21-
from typing import TYPE_CHECKING
2221

2322
from google.adk.platform import uuid as platform_uuid
2423
from google.genai import types
@@ -48,9 +47,6 @@
4847
from .run_config import RunConfig
4948
from .transcription_entry import TranscriptionEntry
5049

51-
if TYPE_CHECKING:
52-
from google.adk.telemetry._instrumentation import TelemetryContext
53-
5450

5551
class LlmCallsLimitExceededError(Exception):
5652
"""Error thrown when the number of LLM calls exceed the limit."""
@@ -274,12 +270,6 @@ class InvocationContext(BaseModel):
274270
of this invocation.
275271
"""
276272

277-
_invoke_agent_telemetry_context: Optional[TelemetryContext] = PrivateAttr(
278-
default=None
279-
)
280-
"""TelemetryContext of the active ``invoke_agent`` span, if any.
281-
"""
282-
283273
@property
284274
def is_resumable(self) -> bool:
285275
"""Returns whether the current invocation is resumable."""

src/google/adk/telemetry/_instrumentation.py

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727

2828
from . import _metrics
2929
from . import tracing
30+
from ..events import event as event_lib
3031

3132
if TYPE_CHECKING:
3233
from ..agents.base_agent import BaseAgent
3334
from ..agents.invocation_context import InvocationContext
34-
from ..events import event as event_lib
3535
from ..models.llm_request import LlmRequest
3636
from ..models.llm_response import LlmResponse
3737
from ..tools.base_tool import BaseTool
@@ -78,31 +78,11 @@ class TelemetryContext:
7878
error_type: str | None = None
7979
span: tracing.GenerateContentSpan | trace.Span | None = None
8080
_llm_responses: list[LlmResponse] = dataclasses.field(default_factory=list)
81-
_inference_call_count: int = 0
82-
_tool_call_count: int = 0
8381

8482
@property
8583
def llm_responses(self) -> list[LlmResponse]:
8684
return self._llm_responses
8785

88-
@property
89-
def inference_call_count(self) -> int:
90-
"""Number of model calls counted against this invoke_agent span."""
91-
return self._inference_call_count
92-
93-
def increment_inference_calls(self) -> None:
94-
"""Counts one model call against this span (including calls that raised)."""
95-
self._inference_call_count += 1
96-
97-
def increment_tool_calls(self) -> None:
98-
"""Counts one tool call against this invoke_agent span."""
99-
self._tool_call_count += 1
100-
101-
@property
102-
def tool_call_count(self) -> int:
103-
"""Number of tool calls counted against this invoke_agent span."""
104-
return self._tool_call_count
105-
10686
def record_llm_response(
10787
self, invocation_context: InvocationContext, response: LlmResponse
10888
) -> None:
@@ -130,30 +110,6 @@ def _record_agent_metrics(
130110
logger.exception("Failed to record agent metrics for agent %s", agent_name)
131111

132112

133-
def _flush_invoke_agent_metrics(
134-
tel_ctx: TelemetryContext, agent_name: str
135-
) -> None:
136-
"""Flushes this span's accumulated inference/tool-call metrics."""
137-
_metrics.record_invoke_agent_inference_calls(
138-
agent_name, tel_ctx.inference_call_count
139-
)
140-
_metrics.record_invoke_agent_tool_calls(agent_name, tel_ctx.tool_call_count)
141-
142-
143-
def _accumulate_invoke_agent_tool_call(ctx: InvocationContext) -> None:
144-
"""Counts one tool call against the active invoke_agent span."""
145-
span_tel_ctx = ctx._invoke_agent_telemetry_context # pylint: disable=protected-access
146-
if span_tel_ctx is not None:
147-
span_tel_ctx.increment_tool_calls()
148-
149-
150-
def _accumulate_invoke_agent_inference_call(ctx: InvocationContext) -> None:
151-
"""Counts one model call against the active invoke_agent span."""
152-
span_tel_ctx = ctx._invoke_agent_telemetry_context # pylint: disable=protected-access
153-
if span_tel_ctx is not None:
154-
span_tel_ctx.increment_inference_calls()
155-
156-
157113
@contextlib.asynccontextmanager
158114
async def record_agent_invocation(
159115
ctx: InvocationContext, agent: BaseAgent
@@ -163,13 +119,11 @@ async def record_agent_invocation(
163119
caught_error: Exception | None = None
164120
span: trace.Span | None = None
165121
span_name = f"invoke_agent {agent.name}"
166-
tel_ctx = TelemetryContext()
167122
try:
168123
with tracing.tracer.start_as_current_span(span_name) as s:
169124
span = s
170125
tracing.trace_agent_invocation(span, agent, ctx)
171-
tel_ctx.otel_context = context_api.get_current()
172-
ctx._invoke_agent_telemetry_context = tel_ctx # pylint: disable=protected-access
126+
tel_ctx = TelemetryContext(otel_context=context_api.get_current())
173127
yield tel_ctx
174128
except Exception as e:
175129
caught_error = e
@@ -182,15 +136,14 @@ async def record_agent_invocation(
182136
getattr(getattr(ctx, "session", None), "events", []),
183137
caught_error,
184138
)
185-
_flush_invoke_agent_metrics(tel_ctx, agent.name)
186139

187140

188141
@contextlib.asynccontextmanager
189142
async def record_tool_execution(
190143
tool: BaseTool,
191144
agent: BaseAgent,
192145
function_args: dict[str, object],
193-
invocation_context: InvocationContext,
146+
invocation_context: InvocationContext | None = None,
194147
) -> AsyncIterator[TelemetryContext]:
195148
"""Unified context manager for consolidated tool execution telemetry."""
196149
start_time = time.monotonic()
@@ -218,7 +171,6 @@ async def record_tool_execution(
218171
invocation_context=invocation_context,
219172
error_type=tel_ctx.error_type,
220173
)
221-
_accumulate_invoke_agent_tool_call(invocation_context)
222174
finally:
223175
try:
224176
_metrics.record_tool_execution_duration(
@@ -253,7 +205,6 @@ async def record_inference_telemetry(
253205
yield tel_ctx
254206
finally:
255207
inference_error = sys.exc_info()[1]
256-
_accumulate_invoke_agent_inference_call(invocation_context)
257208
agent = invocation_context.agent
258209
elapsed_s = _get_elapsed_s(tel_ctx.span, start_time)
259210
try:

src/google/adk/telemetry/_metrics.py

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -146,44 +146,6 @@
146146
gen_ai_metrics.create_gen_ai_client_operation_duration(meter)
147147
)
148148
_client_token_usage = gen_ai_metrics.create_gen_ai_client_token_usage(meter)
149-
_invoke_agent_inference_calls = meter.create_histogram(
150-
"gen_ai.invoke_agent.inference_calls",
151-
unit="1",
152-
description="Number of inference (model) calls per agent invocation.",
153-
explicit_bucket_boundaries_advisory=[
154-
1,
155-
2,
156-
3,
157-
4,
158-
5,
159-
6,
160-
8,
161-
12,
162-
16,
163-
24,
164-
32,
165-
64,
166-
],
167-
)
168-
_invoke_agent_tool_calls = meter.create_histogram(
169-
"gen_ai.invoke_agent.tool_calls",
170-
unit="1",
171-
description="Number of tool calls per agent invocation.",
172-
explicit_bucket_boundaries_advisory=[
173-
1,
174-
2,
175-
3,
176-
4,
177-
5,
178-
6,
179-
8,
180-
12,
181-
16,
182-
24,
183-
32,
184-
64,
185-
],
186-
)
187149

188150

189151
def record_agent_invocation_duration(
@@ -198,18 +160,6 @@ def record_agent_invocation_duration(
198160
_agent_invocation_duration.record(elapsed_s, attributes=attrs)
199161

200162

201-
def record_invoke_agent_inference_calls(agent_name: str, count: int):
202-
"""Records the number of inference (model) calls in an agent invocation."""
203-
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
204-
_invoke_agent_inference_calls.record(count, attributes=attrs)
205-
206-
207-
def record_invoke_agent_tool_calls(agent_name: str, count: int):
208-
"""Records the number of tool calls in an agent invocation."""
209-
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
210-
_invoke_agent_tool_calls.record(count, attributes=attrs)
211-
212-
213163
def record_agent_request_size(
214164
agent_name: str, user_content: types.Content | None
215165
):

tests/unittests/telemetry/functional_node_test_cases.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2900,12 +2900,6 @@
29002900
value=NON_DETERMINISTIC,
29012901
),
29022902
}),
2903-
"gen_ai.invoke_agent.inference_calls": frozenset({
2904-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2905-
}),
2906-
"gen_ai.invoke_agent.tool_calls": frozenset({
2907-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2908-
}),
29092903
}
29102904

29112905

@@ -2953,12 +2947,6 @@
29532947
value=NON_DETERMINISTIC,
29542948
),
29552949
}),
2956-
"gen_ai.invoke_agent.inference_calls": frozenset({
2957-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2958-
}),
2959-
"gen_ai.invoke_agent.tool_calls": frozenset({
2960-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2961-
}),
29622950
}
29632951

29642952

tests/unittests/telemetry/functional_test_cases.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,12 +2294,6 @@
22942294
value=NON_DETERMINISTIC,
22952295
),
22962296
}),
2297-
"gen_ai.invoke_agent.inference_calls": frozenset({
2298-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2299-
}),
2300-
"gen_ai.invoke_agent.tool_calls": frozenset({
2301-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2302-
}),
23032297
}
23042298

23052299

@@ -2347,12 +2341,6 @@
23472341
value=NON_DETERMINISTIC,
23482342
),
23492343
}),
2350-
"gen_ai.invoke_agent.inference_calls": frozenset({
2351-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=2),
2352-
}),
2353-
"gen_ai.invoke_agent.tool_calls": frozenset({
2354-
MetricPoint(attributes={"gen_ai.agent.name": AGENT_NAME}, value=1),
2355-
}),
23562344
}
23572345

23582346

tests/unittests/telemetry/functional_test_helpers.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -339,16 +339,6 @@ class HistogramSpec(NamedTuple):
339339
attr="_client_token_usage",
340340
metric_name="gen_ai.client.token.usage",
341341
),
342-
HistogramSpec(
343-
module=_metrics,
344-
attr="_invoke_agent_inference_calls",
345-
metric_name="gen_ai.invoke_agent.inference_calls",
346-
),
347-
HistogramSpec(
348-
module=_metrics,
349-
attr="_invoke_agent_tool_calls",
350-
metric_name="gen_ai.invoke_agent.tool_calls",
351-
),
352342
)
353343

354344

tests/unittests/telemetry/test_instrumentation.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
# pylint: disable=protected-access
1616

1717
import time
18-
import types
1918
from unittest import mock
2019

2120
from google.adk.telemetry import _instrumentation
@@ -95,8 +94,8 @@ async def test_record_agent_invocation_tolerates_minimal_context():
9594
"""
9695
agent = mock.MagicMock()
9796
agent.name = "test_agent"
98-
# Context-like without `user_content` and without `session`.
99-
bare_ctx = types.SimpleNamespace()
97+
# Bare object without `user_content` and without `session`.
98+
bare_ctx = object()
10099

101100
with (
102101
mock.patch.object(

0 commit comments

Comments
 (0)