@@ -83,19 +83,23 @@ class Stream:
8383)
8484from opentelemetry .util .genai .handler import TelemetryHandler
8585from opentelemetry .util .genai .invocation import (
86+ AgentInvocation ,
8687 InferenceInvocation ,
8788)
8889from opentelemetry .util .genai .stream import (
8990 AsyncStreamWrapper ,
9091 SyncStreamWrapper ,
9192)
9293from 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
128132def _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+
308410def _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