Skip to content

Commit 5852a4a

Browse files
committed
feat: move decorators under util/ and rename the APIs
Signed-off-by: Pavan Sudheendra <[email protected]>
1 parent f9b57f6 commit 5852a4a

File tree

5 files changed

+118
-84
lines changed

5 files changed

+118
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
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+
115
import inspect
216
from typing import Optional, Union, TypeVar, Callable, Awaitable
317

418
from typing_extensions import ParamSpec
519

6-
from opentelemetry.genai.sdk.decorators.base import (
20+
from opentelemetry.util.genai.decorators import (
721
entity_class,
822
entity_method,
923
)
10-
from opentelemetry.genai.sdk.utils.const import (
24+
from opentelemetry.util.genai.types import (
1125
ObserveSpanKindValues,
1226
)
1327

@@ -16,34 +30,10 @@
1630
F = TypeVar("F", bound=Callable[P, Union[R, Awaitable[R]]])
1731

1832

19-
def task(
20-
name: Optional[str] = None,
21-
method_name: Optional[str] = None,
22-
tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
23-
) -> Callable[[F], F]:
24-
def decorator(target):
25-
# Check if target is a class
26-
if inspect.isclass(target):
27-
return entity_class(
28-
name=name,
29-
method_name=method_name,
30-
tlp_span_kind=tlp_span_kind,
31-
)(target)
32-
else:
33-
# Target is a function/method
34-
return entity_method(
35-
name=name,
36-
tlp_span_kind=tlp_span_kind,
37-
)(target)
38-
return decorator
39-
40-
41-
def workflow(
33+
def tool(
4234
name: Optional[str] = None,
4335
method_name: Optional[str] = None,
44-
tlp_span_kind: Optional[
45-
Union[ObserveSpanKindValues, str]
46-
] = ObserveSpanKindValues.WORKFLOW,
36+
tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TOOL,
4737
) -> Callable[[F], F]:
4838
def decorator(target):
4939
# Check if target is a class
@@ -59,32 +49,9 @@ def decorator(target):
5949
name=name,
6050
tlp_span_kind=tlp_span_kind,
6151
)(target)
62-
6352
return decorator
6453

6554

66-
def agent(
67-
name: Optional[str] = None,
68-
method_name: Optional[str] = None,
69-
) -> Callable[[F], F]:
70-
return workflow(
71-
name=name,
72-
method_name=method_name,
73-
tlp_span_kind=ObserveSpanKindValues.AGENT,
74-
)
75-
76-
77-
def tool(
78-
name: Optional[str] = None,
79-
method_name: Optional[str] = None,
80-
) -> Callable[[F], F]:
81-
return task(
82-
name=name,
83-
method_name=method_name,
84-
tlp_span_kind=ObserveSpanKindValues.TOOL,
85-
)
86-
87-
8855
def llm(
8956
name: Optional[str] = None,
9057
model_name: Optional[str] = None,
@@ -106,4 +73,4 @@ def decorator(target):
10673
model_name=model_name,
10774
tlp_span_kind=ObserveSpanKindValues.LLM,
10875
)(target)
109-
return decorator
76+
return decorator
Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
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+
115
import json
216
from functools import wraps
317
import os
418
from typing import Optional, TypeVar, Callable, Awaitable, Any, Union
519
import inspect
620
import 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
1532
from opentelemetry import trace
1633
from opentelemetry import context as context_api
1734
from typing_extensions import ParamSpec
1835
from ..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

2946
P = ParamSpec("P")
3047

@@ -51,39 +68,41 @@ def should_emit_events() -> bool:
5168
telemetry = 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-
5871
def _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

6477
def _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

89108
def _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

104123
def _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

202226
def _extract_llm_attributes_from_args_kwargs(args, kwargs, res=None):
@@ -290,7 +314,14 @@ def _extract_response_attributes(res, attributes):
290314

291315

292316
def _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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
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+
115
import inspect
216

317

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
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+
115
def _serialize_object(obj, max_depth=3, current_depth=0):
216
"""
317
Intelligently serialize an object to a more meaningful representation

util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from enum import Enum
1516
import time
1617
from dataclasses import dataclass, field
1718
from typing import Any, Dict, List, Optional
@@ -35,3 +36,10 @@ class LLMInvocation:
3536
attributes: Dict[str, Any] = field(default_factory=dict)
3637
span_id: int = 0
3738
trace_id: int = 0
39+
40+
class ObserveSpanKindValues(Enum):
41+
TOOL = "tool"
42+
LLM = "llm"
43+
UNKNOWN = "unknown"
44+
45+

0 commit comments

Comments
 (0)