1+ # Copyright The OpenTelemetry Authors
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License");
4+ # you may not use this file except in compliance with the License.
5+ # You may obtain a copy of the License at
6+ #
7+ # http://www.apache.org/licenses/LICENSE-2.0
8+ #
9+ # Unless required by applicable law or agreed to in writing, software
10+ # distributed under the License is distributed on an "AS IS" BASIS,
11+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ # See the License for the specific language governing permissions and
13+ # limitations under the License.
14+
115import json
216from functools import wraps
317import os
418from typing import Optional , TypeVar , Callable , Awaitable , Any , Union
519import inspect
620import traceback
21+ import logging
22+ from typing import Any , Dict , List
23+ from opentelemetry .util .genai .data import ToolFunction
724
8- from opentelemetry .genai .sdk . decorators . helpers import (
25+ from opentelemetry .util . genai .decorators import (
926 _is_async_method ,
1027 _get_original_function_name ,
1128 _is_async_generator ,
1229)
1330
14- from opentelemetry .genai . sdk .decorators .util import camel_to_snake
31+ from opentelemetry .util . genai .decorators .util import camel_to_snake
1532from opentelemetry import trace
1633from opentelemetry import context as context_api
1734from typing_extensions import ParamSpec
1835from ..version import __version__
1936
20- from opentelemetry .genai .sdk . utils . const import (
37+ from opentelemetry .util . genai .types import (
2138 ObserveSpanKindValues ,
2239)
2340
24- from opentelemetry .genai . sdk .data import Message , ChatGeneration
25- from opentelemetry .genai . sdk .exporters import _get_property_value
41+ from opentelemetry .util . genai .data import Message , ChatGeneration
42+ from opentelemetry .util . genai .exporters import _get_property_value
2643
27- from opentelemetry .genai . sdk .api import get_telemetry_client
44+ from opentelemetry .util . genai .api import get_telemetry_client
2845
2946P = ParamSpec ("P" )
3047
@@ -51,39 +68,41 @@ def should_emit_events() -> bool:
5168telemetry = get_telemetry_client (exporter_type_full )
5269
5370
54- def _get_parent_run_id ():
55- # Placeholder for parent run ID logic; return None if not available
56- return None
57-
5871def _should_send_prompts ():
5972 return (
6073 os .getenv ("OBSERVE_TRACE_CONTENT" ) or "true"
6174 ).lower () == "true" or context_api .get_value ("override_enable_content_tracing" )
6275
6376
6477def _handle_llm_span_attributes (tlp_span_kind , args , kwargs , res = None ):
65- """Add GenAI-specific attributes to span for LLM operations by delegating to TelemetryClient logic."""
66- if tlp_span_kind != ObserveSpanKindValues .LLM :
67- return None
78+ """
79+ Add GenAI-specific attributes to span for LLM operations by delegating to TelemetryClient logic.
6880
81+ Returns:
82+ run_id (UUID): The run_id if tlp_span_kind is ObserveSpanKindValues.LLM, otherwise None.
83+
84+ Note:
85+ If tlp_span_kind is not ObserveSpanKindValues.LLM, this function returns None.
86+ Downstream code should check for None before using run_id.
87+ """
6988 # Import here to avoid circular import issues
7089 from uuid import uuid4
7190
7291 # Extract messages and attributes as before
7392 messages = _extract_messages_from_args_kwargs (args , kwargs )
7493 tool_functions = _extract_tool_functions_from_args_kwargs (args , kwargs )
75- run_id = uuid4 ()
76-
7794 try :
95+ run_id = uuid4 ()
7896 telemetry .start_llm (prompts = messages ,
7997 tool_functions = tool_functions ,
8098 run_id = run_id ,
8199 parent_run_id = _get_parent_run_id (),
82100 ** _extract_llm_attributes_from_args_kwargs (args , kwargs , res ))
83101 return run_id # Return run_id so it can be used later
84102 except Exception as e :
85- print (f"Warning: TelemetryClient.start_llm failed: { e } " )
86- return None
103+ logging .error (f"TelemetryClient.start_llm failed: { e } " )
104+ raise
105+ return None
87106
88107
89108def _finish_llm_span (run_id , res , ** attributes ):
@@ -98,7 +117,7 @@ def _finish_llm_span(run_id, res, **attributes):
98117 with contextlib .suppress (Exception ):
99118 telemetry .stop_llm (run_id , chat_generations , ** attributes )
100119 except Exception as e :
101- print (f"Warning: TelemetryClient.stop_llm failed: { e } " )
120+ logging . warning (f"TelemetryClient.stop_llm failed: { e } " )
102121
103122
104123def _extract_messages_from_args_kwargs (args , kwargs ):
@@ -142,22 +161,23 @@ def _extract_messages_from_args_kwargs(args, kwargs):
142161
143162 return messages
144163
164+ def _extract_tool_functions_from_args_kwargs (args : Any , kwargs : Dict [str , Any ]) -> List ["ToolFunction" ]:
165+ """Collect tools from kwargs (tools/functions) or first arg attributes,
166+ normalize each object/dict/callable to a ToolFunction (name, description, parameters={}),
167+ skipping anything malformed.
168+ """
169+
170+ tool_functions : List [ToolFunction ] = []
145171
146- def _extract_tool_functions_from_args_kwargs (args , kwargs ):
147- """Extract tool functions from function arguments"""
148- from opentelemetry .genai .sdk .data import ToolFunction
149-
150- tool_functions = []
151-
152172 # Try to find tools in various places
153173 tools = None
154-
174+
155175 # Check kwargs for tools
156176 if kwargs .get ('tools' ):
157177 tools = kwargs ['tools' ]
158178 elif kwargs .get ('functions' ):
159179 tools = kwargs ['functions' ]
160-
180+
161181 # Check args for objects that might have tools
162182 if not tools and len (args ) > 0 :
163183 for arg in args :
@@ -167,7 +187,11 @@ def _extract_tool_functions_from_args_kwargs(args, kwargs):
167187 elif hasattr (arg , 'functions' ):
168188 tools = getattr (arg , 'functions' , [])
169189 break
170-
190+
191+ # Ensure tools is always a list for consistent processing
192+ if tools and not isinstance (tools , list ):
193+ tools = [tools ]
194+
171195 # Convert tools to ToolFunction objects
172196 if tools :
173197 for tool in tools :
@@ -187,16 +211,16 @@ def _extract_tool_functions_from_args_kwargs(args, kwargs):
187211 tool_description = getattr (tool , '__doc__' , '' ) or ''
188212 else :
189213 continue
190-
214+
191215 tool_functions .append (ToolFunction (
192216 name = tool_name ,
193217 description = tool_description ,
194- parameters = {}
218+ parameters = {}
195219 ))
196220 except Exception :
197221 # Skip tools that can't be processed
198222 continue
199-
223+
200224 return tool_functions
201225
202226def _extract_llm_attributes_from_args_kwargs (args , kwargs , res = None ):
@@ -290,7 +314,14 @@ def _extract_response_attributes(res, attributes):
290314
291315
292316def _extract_chat_generations_from_response (res ):
293- """Extract chat generations from response similar to exporter logic"""
317+ """
318+ Normalize various response shapes into a list of ChatGeneration objects.
319+ Supported:
320+ - OpenAI style: res.choices[*].message.content (+ role, finish_reason)
321+ - Fallback: res.content (+ optional res.type, finish_reason defaults to "stop")
322+ Returns an empty list on unrecognized structures or errors. Never raises.
323+ All content/type values are coerced to str; finish_reason may be None.
324+ """
294325 chat_generations = []
295326
296327 try :
@@ -381,7 +412,7 @@ async def async_wrap(*args, **kwargs):
381412 _finish_llm_span (run_id , res , ** attributes )
382413
383414 except Exception as e :
384- print (traceback .format_exc ())
415+ logging . error (traceback .format_exc ())
385416 raise e
386417 return res
387418
0 commit comments