Skip to content

Commit 9a629b0

Browse files
authored
[opentelemetry-instrumentation-google-genai] Add tool definitions attribute on interactions API, other minor changes (#271)
* Finnaly finish making changes * Update README * Add changelog * Fix changelog * Fix readme * Address comment * Precommit * Fix typecheck
1 parent 64a8297 commit 9a629b0

6 files changed

Lines changed: 234 additions & 70 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add tool definitions attribute to interactions API spans/events. Use invoke remote agent span instead of inference span when an agent is specified.

instrumentation/opentelemetry-instrumentation-google-genai/README.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ GenAI operations in aggregate.
1515
Experimental
1616
------------
1717

18-
This package is still experimental. The instrumentation may not be complete or correct just yet.
18+
The ``interactions`` and ``embed_content`` methods are newly instrumented and may contain issues.
19+
Please treat the telemetry produced by these methods as experimental.
1920

20-
Please see "TODOS.md" for a list of known defects/TODOs that are blockers to package stability.
21+
The ``interactions`` API currently does not support automatic function calling, so no ``execute_tool`` spans
22+
are generated.
2123

2224

2325
Installation
@@ -66,7 +68,7 @@ Make sure to configure OpenTelemetry tracing, logging, and metrics to capture al
6668
Limitations
6769
***********
6870

69-
When using the Google GenAI SDK with automatic function calling enabled,
71+
When using the Google GenAI SDK with automatic function calling enabled for ``generate_content``,
7072
the OpenTelemetry instrumentation creates an ``execute_tool`` span for each tool call the SDK executes,
7173
these spans are nested under the ``generate_content`` span.
7274

instrumentation/opentelemetry-instrumentation-google-genai/TODOS.md

Lines changed: 0 additions & 18 deletions
This file was deleted.

instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py

Lines changed: 120 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,23 @@ class Stream:
8383
)
8484
from opentelemetry.util.genai.handler import TelemetryHandler
8585
from opentelemetry.util.genai.invocation import (
86+
AgentInvocation,
8687
InferenceInvocation,
8788
)
8889
from opentelemetry.util.genai.stream import (
8990
AsyncStreamWrapper,
9091
SyncStreamWrapper,
9192
)
9293
from opentelemetry.util.genai.types import (
94+
FunctionToolDefinition,
9395
GenericPart,
96+
GenericToolDefinition,
9497
InputMessage,
9598
OutputMessage,
9699
Text,
97100
ToolCallRequest,
98101
ToolCallResponse,
102+
ToolDefinition,
99103
Uri,
100104
)
101105

@@ -127,18 +131,20 @@ def _set_co_filename(wrapped: object) -> None:
127131

128132
def _apply_interaction_response_attributes(
129133
response: Interaction,
130-
invocation: InferenceInvocation,
134+
invocation: InferenceInvocation | AgentInvocation,
131135
telemetry_handler: TelemetryHandler,
132136
) -> None:
133-
invocation.response_model_name = response.model
134-
if getattr(response, "id", None):
135-
invocation.response_id = response.id
137+
if isinstance(invocation, InferenceInvocation):
138+
invocation.response_model_name = response.model
139+
if getattr(response, "id", None):
140+
invocation.response_id = response.id
136141

137142
usage = response.usage or Usage()
138143

139144
invocation.input_tokens = usage.total_input_tokens
140145
invocation.output_tokens = usage.total_output_tokens
141-
invocation.thinking_tokens = usage.total_thought_tokens
146+
if isinstance(invocation, InferenceInvocation):
147+
invocation.thinking_tokens = usage.total_thought_tokens
142148
invocation.cache_read_input_tokens = usage.total_cached_tokens
143149

144150
if telemetry_handler.should_capture_content():
@@ -245,7 +251,7 @@ class InteractionsStreamWrapper(SyncStreamWrapper[InteractionSSEEvent]):
245251
def __init__(
246252
self,
247253
stream: Iterable[InteractionSSEEvent],
248-
invocation: InferenceInvocation,
254+
invocation: InferenceInvocation | AgentInvocation,
249255
telemetry_handler: TelemetryHandler,
250256
) -> None:
251257
super().__init__(stream)
@@ -277,7 +283,7 @@ class AsyncInteractionsStreamWrapper(AsyncStreamWrapper[InteractionSSEEvent]):
277283
def __init__(
278284
self,
279285
stream: AsyncIterable[InteractionSSEEvent],
280-
invocation: InferenceInvocation,
286+
invocation: InferenceInvocation | AgentInvocation,
281287
telemetry_handler: TelemetryHandler,
282288
) -> None:
283289
super().__init__(stream)
@@ -305,6 +311,102 @@ def _on_stream_error(self, error: Exception) -> None:
305311
self._self_invocation.fail(error)
306312

307313

314+
# See https://ai.google.dev/gemini-api/docs/function-calling
315+
def _maybe_get_tool_definitions(
316+
tools: Any | None,
317+
) -> list[ToolDefinition] | None:
318+
if not isinstance(tools, Sequence) or isinstance(tools, (str, bytes)):
319+
return None
320+
definitions: list[ToolDefinition] = []
321+
for tool in tools:
322+
# Tool must be a list of dictionaries
323+
if not isinstance(tool, dict):
324+
continue
325+
# This is currently a required field and the SDK will raise an error if it isn't present or
326+
# takes on a type field that isn't supported.
327+
tool_type = tool.get("type")
328+
if tool_type in (
329+
"bash",
330+
"code_execution",
331+
"filesystem",
332+
"file_search",
333+
"google_maps",
334+
"google_search",
335+
"url_context",
336+
"computer_use",
337+
):
338+
definitions.append(
339+
GenericToolDefinition(
340+
name=tool.get("name") or tool_type,
341+
type=tool_type,
342+
)
343+
)
344+
345+
elif tool_type == "mcp_server":
346+
# Name and uri are optional.
347+
name = tool.get("name") or tool.get("url") or "mcp_server"
348+
# GenericToolDefinition only has 2 fields (name and type). It'd be useful to
349+
# add support for extra fields somehow, so we can put the URI in here.
350+
definitions.append(
351+
GenericToolDefinition(
352+
name=name,
353+
type="mcp_server",
354+
)
355+
)
356+
elif tool_type == "function":
357+
definitions.append(
358+
FunctionToolDefinition(
359+
name=tool.get("name") or "function",
360+
description=tool.get("description"),
361+
parameters=tool.get("parameters"),
362+
)
363+
)
364+
return definitions if definitions else None
365+
366+
367+
def _start_interactions_invocation(
368+
telemetry_handler: TelemetryHandler,
369+
instance: InteractionsResource | AsyncInteractionsResource,
370+
kwargs: dict[str, Any],
371+
) -> InferenceInvocation | AgentInvocation:
372+
# Vertex AI does not support the interactions API yet, but eventually will.
373+
# SDK will raise an exception if model or agent is not passed or if input data is not passed.
374+
is_vertex, server_address = _get_client_info(instance)
375+
provider = (
376+
GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
377+
if is_vertex
378+
else GenAIAttributes.GenAiSystemValues.GEMINI.value
379+
)
380+
if agent := kwargs.get("agent"):
381+
invocation: InferenceInvocation | AgentInvocation = (
382+
telemetry_handler.invoke_remote_agent(
383+
provider=provider,
384+
request_model=kwargs.get("model"),
385+
server_address=server_address,
386+
agent_name=agent,
387+
)
388+
)
389+
else:
390+
invocation = telemetry_handler.inference(
391+
provider=provider,
392+
request_model=kwargs.get("model"),
393+
operation_name="interactions.create",
394+
server_address=server_address,
395+
)
396+
invocation.tool_definitions = _maybe_get_tool_definitions(
397+
kwargs.get("tools")
398+
)
399+
400+
if telemetry_handler.should_capture_content():
401+
invocation.input_messages = _interactions_input_to_messages(
402+
kwargs.get("input")
403+
)
404+
if system_instruction := kwargs.get("system_instruction"):
405+
invocation.system_instruction = [Text(content=system_instruction)]
406+
407+
return invocation
408+
409+
308410
def _create_instrumented_interactions_create(
309411
telemetry_handler: TelemetryHandler,
310412
) -> Callable[
@@ -322,32 +424,15 @@ def instrumented_interactions_create(
322424
args: tuple[Any, ...],
323425
kwargs: dict[str, Any],
324426
) -> Interaction | InteractionsStreamWrapper:
325-
# Vertex AI does not support the interactions API yet, but eventually will.
326-
# SDK will raise an exception if model or agent is not passed or if input data is not passed.
327-
is_vertex, server_address = _get_client_info(instance)
328-
invocation = telemetry_handler.inference(
329-
provider=(
330-
GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
331-
if is_vertex
332-
else GenAIAttributes.GenAiSystemValues.GEMINI.value
333-
),
334-
request_model=kwargs.get("model") or kwargs.get("agent"),
335-
operation_name="interactions.create",
336-
server_address=server_address,
427+
invocation = _start_interactions_invocation(
428+
telemetry_handler, instance, kwargs
337429
)
338430

339-
if telemetry_handler.should_capture_content():
340-
invocation.input_messages = _interactions_input_to_messages(
341-
kwargs.get("input")
342-
)
343-
if system_instruction := kwargs.get("system_instruction"):
344-
invocation.system_instruction = [
345-
Text(content=system_instruction)
346-
]
347-
348431
if kwargs.get("stream", False):
349432
return InteractionsStreamWrapper(
350-
wrapped(*args, **kwargs), invocation, telemetry_handler
433+
wrapped(*args, **kwargs),
434+
invocation,
435+
telemetry_handler,
351436
)
352437
try:
353438
response = wrapped(*args, **kwargs)
@@ -380,35 +465,21 @@ async def instrumented_interactions_create(
380465
args: tuple[Any, ...],
381466
kwargs: dict[str, Any],
382467
) -> Interaction | AsyncInteractionsStreamWrapper:
383-
is_vertex, server_address = _get_client_info(instance)
384-
invocation = telemetry_handler.inference(
385-
provider=(
386-
GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
387-
if is_vertex
388-
else GenAIAttributes.GenAiSystemValues.GEMINI.value
389-
),
390-
request_model=kwargs.get("model") or kwargs.get("agent"),
391-
operation_name="interactions.create",
392-
server_address=server_address,
468+
invocation = _start_interactions_invocation(
469+
telemetry_handler, instance, kwargs
393470
)
394471

395-
if telemetry_handler.should_capture_content():
396-
invocation.input_messages = _interactions_input_to_messages(
397-
kwargs.get("input")
398-
)
399-
if system_instruction := kwargs.get("system_instruction"):
400-
invocation.system_instruction = [
401-
Text(content=system_instruction)
402-
]
403-
404472
if kwargs.get("stream", False):
405473
return AsyncInteractionsStreamWrapper(
406474
await wrapped(*args, **kwargs),
407475
invocation,
408476
telemetry_handler,
409477
)
410478
try:
411-
response = cast(Interaction, await wrapped(*args, **kwargs))
479+
response = cast(
480+
Interaction,
481+
await wrapped(*args, **kwargs),
482+
)
412483
_apply_interaction_response_attributes(
413484
response, invocation, telemetry_handler
414485
)

0 commit comments

Comments
 (0)