mseep-agentops 0.4.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentops/__init__.py +488 -0
- agentops/client/__init__.py +5 -0
- agentops/client/api/__init__.py +71 -0
- agentops/client/api/base.py +162 -0
- agentops/client/api/types.py +21 -0
- agentops/client/api/versions/__init__.py +10 -0
- agentops/client/api/versions/v3.py +65 -0
- agentops/client/api/versions/v4.py +104 -0
- agentops/client/client.py +211 -0
- agentops/client/http/__init__.py +0 -0
- agentops/client/http/http_adapter.py +116 -0
- agentops/client/http/http_client.py +215 -0
- agentops/config.py +268 -0
- agentops/enums.py +36 -0
- agentops/exceptions.py +38 -0
- agentops/helpers/__init__.py +44 -0
- agentops/helpers/dashboard.py +54 -0
- agentops/helpers/deprecation.py +50 -0
- agentops/helpers/env.py +52 -0
- agentops/helpers/serialization.py +137 -0
- agentops/helpers/system.py +178 -0
- agentops/helpers/time.py +11 -0
- agentops/helpers/version.py +36 -0
- agentops/instrumentation/__init__.py +598 -0
- agentops/instrumentation/common/__init__.py +82 -0
- agentops/instrumentation/common/attributes.py +278 -0
- agentops/instrumentation/common/instrumentor.py +147 -0
- agentops/instrumentation/common/metrics.py +100 -0
- agentops/instrumentation/common/objects.py +26 -0
- agentops/instrumentation/common/span_management.py +176 -0
- agentops/instrumentation/common/streaming.py +218 -0
- agentops/instrumentation/common/token_counting.py +177 -0
- agentops/instrumentation/common/version.py +71 -0
- agentops/instrumentation/common/wrappers.py +235 -0
- agentops/legacy/__init__.py +277 -0
- agentops/legacy/event.py +156 -0
- agentops/logging/__init__.py +4 -0
- agentops/logging/config.py +86 -0
- agentops/logging/formatters.py +34 -0
- agentops/logging/instrument_logging.py +91 -0
- agentops/sdk/__init__.py +27 -0
- agentops/sdk/attributes.py +151 -0
- agentops/sdk/core.py +607 -0
- agentops/sdk/decorators/__init__.py +51 -0
- agentops/sdk/decorators/factory.py +486 -0
- agentops/sdk/decorators/utility.py +216 -0
- agentops/sdk/exporters.py +87 -0
- agentops/sdk/processors.py +71 -0
- agentops/sdk/types.py +21 -0
- agentops/semconv/__init__.py +36 -0
- agentops/semconv/agent.py +29 -0
- agentops/semconv/core.py +19 -0
- agentops/semconv/enum.py +11 -0
- agentops/semconv/instrumentation.py +13 -0
- agentops/semconv/langchain.py +63 -0
- agentops/semconv/message.py +61 -0
- agentops/semconv/meters.py +24 -0
- agentops/semconv/resource.py +52 -0
- agentops/semconv/span_attributes.py +118 -0
- agentops/semconv/span_kinds.py +50 -0
- agentops/semconv/status.py +11 -0
- agentops/semconv/tool.py +15 -0
- agentops/semconv/workflow.py +69 -0
- agentops/validation.py +357 -0
- mseep_agentops-0.4.18.dist-info/METADATA +49 -0
- mseep_agentops-0.4.18.dist-info/RECORD +94 -0
- mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
- mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
- mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +10 -0
- tests/unit/__init__.py +0 -0
- tests/unit/client/__init__.py +1 -0
- tests/unit/client/test_http_adapter.py +221 -0
- tests/unit/client/test_http_client.py +206 -0
- tests/unit/conftest.py +54 -0
- tests/unit/sdk/__init__.py +1 -0
- tests/unit/sdk/instrumentation_tester.py +207 -0
- tests/unit/sdk/test_attributes.py +392 -0
- tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
- tests/unit/sdk/test_decorators.py +763 -0
- tests/unit/sdk/test_exporters.py +241 -0
- tests/unit/sdk/test_factory.py +1188 -0
- tests/unit/sdk/test_internal_span_processor.py +397 -0
- tests/unit/sdk/test_resource_attributes.py +35 -0
- tests/unit/test_config.py +82 -0
- tests/unit/test_context_manager.py +777 -0
- tests/unit/test_events.py +27 -0
- tests/unit/test_host_env.py +54 -0
- tests/unit/test_init_py.py +501 -0
- tests/unit/test_serialization.py +433 -0
- tests/unit/test_session.py +676 -0
- tests/unit/test_user_agent.py +34 -0
- tests/unit/test_validation.py +405 -0
@@ -0,0 +1,278 @@
|
|
1
|
+
"""Common attribute processing utilities shared across all instrumentors.
|
2
|
+
|
3
|
+
This utility ensures consistent attribute extraction and transformation across different
|
4
|
+
instrumentation use cases.
|
5
|
+
|
6
|
+
This module provides core utilities for extracting and formatting
|
7
|
+
OpenTelemetry-compatible attributes from span data. These functions
|
8
|
+
are provider-agnostic and used by all instrumentors in the AgentOps
|
9
|
+
package.
|
10
|
+
|
11
|
+
The module includes:
|
12
|
+
|
13
|
+
1. Helper functions for attribute extraction and mapping
|
14
|
+
2. Common attribute getters used across all providers
|
15
|
+
3. Base trace and span attribute functions
|
16
|
+
|
17
|
+
All functions follow a consistent pattern:
|
18
|
+
- Accept span/trace data as input
|
19
|
+
- Process according to semantic conventions
|
20
|
+
- Return a dictionary of formatted attributes
|
21
|
+
|
22
|
+
These utilities ensure consistent attribute handling across different
|
23
|
+
LLM service instrumentors while maintaining separation of concerns.
|
24
|
+
"""
|
25
|
+
|
26
|
+
from typing import runtime_checkable, Protocol, Any, Optional, Dict, TypedDict
|
27
|
+
from agentops.logging import logger
|
28
|
+
from agentops.helpers import safe_serialize, get_agentops_version
|
29
|
+
from agentops.semconv import (
|
30
|
+
CoreAttributes,
|
31
|
+
InstrumentationAttributes,
|
32
|
+
WorkflowAttributes,
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
# `AttributeMap` is a dictionary that maps target attribute keys to source attribute keys.
|
37
|
+
# It is used to extract and transform attributes from a span or trace data object
|
38
|
+
# into a standardized format following OpenTelemetry semantic conventions.
|
39
|
+
#
|
40
|
+
# Key-Value Format:
|
41
|
+
# - Key (str): The target attribute key in the standardized output format
|
42
|
+
# - Value (str): The source attribute key in the input data object
|
43
|
+
#
|
44
|
+
# Example Usage:
|
45
|
+
# --------------
|
46
|
+
#
|
47
|
+
# Create your mapping:
|
48
|
+
# attribute_mapping: AttributeMap = {
|
49
|
+
# CoreAttributes.TRACE_ID: "trace_id",
|
50
|
+
# CoreAttributes.SPAN_ID: "span_id"
|
51
|
+
# }
|
52
|
+
#
|
53
|
+
# Extract the attributes:
|
54
|
+
# span_data = {
|
55
|
+
# "trace_id": "12345",
|
56
|
+
# "span_id": "67890",
|
57
|
+
# }
|
58
|
+
#
|
59
|
+
# attributes = _extract_attributes_from_mapping(span_data, attribute_mapping)
|
60
|
+
# # >> {"trace.id": "12345", "span.id": "67890"}
|
61
|
+
AttributeMap = Dict[str, str] # target_attribute_key: source_attribute
|
62
|
+
|
63
|
+
|
64
|
+
# `IndexedAttributeMap` differs from `AttributeMap` in that it allows for dynamic formatting of
|
65
|
+
# target attribute keys using indices `i` and optionally `j`. This is particularly useful
|
66
|
+
# when dealing with collections of similar attributes that should be uniquely identified
|
67
|
+
# in the output.
|
68
|
+
#
|
69
|
+
# Key-Value Format:
|
70
|
+
# - Key (IndexedAttribute): An object implementing the IndexedAttribute protocol with a format method
|
71
|
+
# - Value (str): The source attribute key in the input data object
|
72
|
+
#
|
73
|
+
# Example Usage:
|
74
|
+
# --------------
|
75
|
+
#
|
76
|
+
# Create your mapping:
|
77
|
+
# attribute_mapping: IndexedAttributeMap = {
|
78
|
+
# MessageAttributes.TOOL_CALL_ID: "id",
|
79
|
+
# MessageAttributes.TOOL_CALL_TYPE: "type"
|
80
|
+
# }
|
81
|
+
#
|
82
|
+
# Process tool calls:
|
83
|
+
# span_data = {
|
84
|
+
# "id": "tool_1",
|
85
|
+
# "type": "search",
|
86
|
+
# }
|
87
|
+
#
|
88
|
+
# attributes = _extract_attributes_from_mapping_with_index(
|
89
|
+
# span_data, attribute_mapping, i=0)
|
90
|
+
# # >> {"gen_ai.request.tools.0.id": "tool_1", "gen_ai.request.tools.0.type": "search"}
|
91
|
+
|
92
|
+
|
93
|
+
@runtime_checkable
|
94
|
+
class IndexedAttribute(Protocol):
|
95
|
+
"""
|
96
|
+
Protocol for objects that define a method to format indexed attributes using
|
97
|
+
only the provided indices `i` and optionally `j`. This allows for dynamic
|
98
|
+
formatting of attribute keys based on the indices.
|
99
|
+
"""
|
100
|
+
|
101
|
+
def format(self, *, i: int, j: Optional[int] = None) -> str:
|
102
|
+
...
|
103
|
+
|
104
|
+
|
105
|
+
IndexedAttributeMap = Dict[IndexedAttribute, str] # target_attribute_key: source_attribute
|
106
|
+
|
107
|
+
|
108
|
+
class IndexedAttributeData(TypedDict, total=False):
|
109
|
+
"""
|
110
|
+
Represents a dictionary structure for indexed attribute data.
|
111
|
+
|
112
|
+
Attributes:
|
113
|
+
i (int): The primary index value. This field is required.
|
114
|
+
j (Optional[int]): An optional secondary index value.
|
115
|
+
"""
|
116
|
+
|
117
|
+
i: int
|
118
|
+
j: Optional[int] = None
|
119
|
+
|
120
|
+
|
121
|
+
def _extract_attributes_from_mapping(span_data: Any, attribute_mapping: AttributeMap) -> AttributeMap:
|
122
|
+
"""Helper function to extract attributes based on a mapping.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
span_data: The span data object or dict to extract attributes from
|
126
|
+
attribute_mapping: Dictionary mapping target attributes to source attributes
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
Dictionary of extracted attributes
|
130
|
+
"""
|
131
|
+
attributes = {}
|
132
|
+
for target_attr, source_attr in attribute_mapping.items():
|
133
|
+
if hasattr(span_data, source_attr):
|
134
|
+
# Use getattr to handle properties
|
135
|
+
value = getattr(span_data, source_attr)
|
136
|
+
elif isinstance(span_data, dict) and source_attr in span_data:
|
137
|
+
# Use direct key access for dicts
|
138
|
+
value = span_data[source_attr]
|
139
|
+
else:
|
140
|
+
continue
|
141
|
+
|
142
|
+
# Skip if value is None or empty
|
143
|
+
if value is None or (isinstance(value, (list, dict, str)) and not value):
|
144
|
+
continue
|
145
|
+
|
146
|
+
# Serialize complex objects
|
147
|
+
elif isinstance(value, (dict, list, object)) and not isinstance(value, (str, int, float, bool)):
|
148
|
+
value = safe_serialize(value)
|
149
|
+
|
150
|
+
attributes[target_attr] = value
|
151
|
+
|
152
|
+
return attributes
|
153
|
+
|
154
|
+
|
155
|
+
def _extract_attributes_from_mapping_with_index(
|
156
|
+
span_data: Any, attribute_mapping: IndexedAttributeMap, i: int, j: Optional[int] = None
|
157
|
+
) -> AttributeMap:
|
158
|
+
"""Helper function to extract attributes based on a mapping with index.
|
159
|
+
|
160
|
+
This function extends `_extract_attributes_from_mapping` by allowing for indexed keys in the attribute mapping.
|
161
|
+
|
162
|
+
Span data is expected to have keys which contain format strings for i/j, e.g. `my_attr_{i}` or `my_attr_{i}_{j}`.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
span_data: The span data object or dict to extract attributes from
|
166
|
+
attribute_mapping: Dictionary mapping target attributes to source attributes, with format strings for i/j
|
167
|
+
i: The primary index to use in formatting the attribute keys
|
168
|
+
j: An optional secondary index (default is None)
|
169
|
+
Returns:
|
170
|
+
Dictionary of extracted attributes with formatted indexed keys.
|
171
|
+
"""
|
172
|
+
|
173
|
+
# `i` is required for formatting the attribute keys, `j` is optional
|
174
|
+
format_kwargs: IndexedAttributeData = {"i": i}
|
175
|
+
if j is not None:
|
176
|
+
format_kwargs["j"] = j
|
177
|
+
|
178
|
+
# Update the attribute mapping to include the index for the span
|
179
|
+
attribute_mapping_with_index: AttributeMap = {}
|
180
|
+
for target_attr, source_attr in attribute_mapping.items():
|
181
|
+
attribute_mapping_with_index[target_attr.format(**format_kwargs)] = source_attr
|
182
|
+
|
183
|
+
return _extract_attributes_from_mapping(span_data, attribute_mapping_with_index)
|
184
|
+
|
185
|
+
|
186
|
+
def get_common_attributes() -> AttributeMap:
|
187
|
+
"""Get common instrumentation attributes used across traces and spans.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
Dictionary of common instrumentation attributes
|
191
|
+
"""
|
192
|
+
return {
|
193
|
+
InstrumentationAttributes.NAME: "agentops",
|
194
|
+
InstrumentationAttributes.VERSION: get_agentops_version(),
|
195
|
+
}
|
196
|
+
|
197
|
+
|
198
|
+
def get_base_trace_attributes(trace: Any) -> AttributeMap:
|
199
|
+
"""Create the base attributes dictionary for an OpenTelemetry trace.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
trace: The trace object to extract attributes from
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Dictionary containing base trace attributes
|
206
|
+
"""
|
207
|
+
if not hasattr(trace, "trace_id"):
|
208
|
+
logger.warning("Cannot create trace attributes: missing trace_id")
|
209
|
+
return {}
|
210
|
+
|
211
|
+
attributes = {
|
212
|
+
WorkflowAttributes.WORKFLOW_NAME: trace.name,
|
213
|
+
CoreAttributes.TRACE_ID: trace.trace_id,
|
214
|
+
WorkflowAttributes.WORKFLOW_STEP_TYPE: "trace",
|
215
|
+
**get_common_attributes(),
|
216
|
+
}
|
217
|
+
|
218
|
+
# Add tags from the config to the trace attributes (these should only be added to the trace)
|
219
|
+
from agentops import get_client
|
220
|
+
|
221
|
+
config = get_client().config
|
222
|
+
tags = []
|
223
|
+
if config.default_tags:
|
224
|
+
# `default_tags` can either be a `set` or a `list`
|
225
|
+
tags = list(config.default_tags)
|
226
|
+
|
227
|
+
attributes[CoreAttributes.TAGS] = tags
|
228
|
+
|
229
|
+
return attributes
|
230
|
+
|
231
|
+
|
232
|
+
def get_base_span_attributes(span: Any) -> AttributeMap:
|
233
|
+
"""Create the base attributes dictionary for an OpenTelemetry span.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
span: The span object to extract attributes from
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
Dictionary containing base span attributes
|
240
|
+
"""
|
241
|
+
span_id = getattr(span, "span_id", "unknown")
|
242
|
+
trace_id = getattr(span, "trace_id", "unknown")
|
243
|
+
parent_id = getattr(span, "parent_id", None)
|
244
|
+
|
245
|
+
attributes = {
|
246
|
+
CoreAttributes.TRACE_ID: trace_id,
|
247
|
+
CoreAttributes.SPAN_ID: span_id,
|
248
|
+
**get_common_attributes(),
|
249
|
+
}
|
250
|
+
|
251
|
+
if parent_id:
|
252
|
+
attributes[CoreAttributes.PARENT_ID] = parent_id
|
253
|
+
|
254
|
+
return attributes
|
255
|
+
|
256
|
+
|
257
|
+
def extract_token_usage(response: Any) -> Dict[str, int]:
|
258
|
+
"""Extract token usage information from a response.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
response: The response object to extract token usage from
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Dictionary containing token usage information
|
265
|
+
"""
|
266
|
+
usage = {}
|
267
|
+
|
268
|
+
# Try to extract token counts from response
|
269
|
+
if hasattr(response, "usage"):
|
270
|
+
usage_data = response.usage
|
271
|
+
if hasattr(usage_data, "prompt_tokens"):
|
272
|
+
usage["prompt_tokens"] = usage_data.prompt_tokens
|
273
|
+
if hasattr(usage_data, "completion_tokens"):
|
274
|
+
usage["completion_tokens"] = usage_data.completion_tokens
|
275
|
+
if hasattr(usage_data, "total_tokens"):
|
276
|
+
usage["total_tokens"] = usage_data.total_tokens
|
277
|
+
|
278
|
+
return usage
|
@@ -0,0 +1,147 @@
|
|
1
|
+
"""Base instrumentor utilities for AgentOps instrumentation.
|
2
|
+
|
3
|
+
This module provides base classes and utilities for creating instrumentors,
|
4
|
+
reducing boilerplate code across different provider instrumentations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import Collection, Dict, List, Optional, Any, Callable
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
|
11
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
12
|
+
from opentelemetry.trace import Tracer, get_tracer
|
13
|
+
from opentelemetry.metrics import Meter, get_meter
|
14
|
+
|
15
|
+
from agentops.instrumentation.common.wrappers import WrapConfig, wrap, unwrap
|
16
|
+
from agentops.logging import logger
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class InstrumentorConfig:
|
21
|
+
"""Configuration for an instrumentor."""
|
22
|
+
|
23
|
+
library_name: str
|
24
|
+
library_version: str
|
25
|
+
wrapped_methods: List[WrapConfig] = field(default_factory=list)
|
26
|
+
metrics_enabled: bool = True
|
27
|
+
dependencies: Collection[str] = field(default_factory=list)
|
28
|
+
|
29
|
+
|
30
|
+
class CommonInstrumentor(BaseInstrumentor, ABC):
|
31
|
+
"""Base class for AgentOps instrumentors with common functionality."""
|
32
|
+
|
33
|
+
def __init__(self, config: InstrumentorConfig):
|
34
|
+
super().__init__()
|
35
|
+
self.config = config
|
36
|
+
self._tracer: Optional[Tracer] = None
|
37
|
+
self._meter: Optional[Meter] = None
|
38
|
+
self._metrics: Dict[str, Any] = {}
|
39
|
+
|
40
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
41
|
+
"""Return required dependencies."""
|
42
|
+
return self.config.dependencies
|
43
|
+
|
44
|
+
def _instrument(self, **kwargs):
|
45
|
+
"""Instrument the target library."""
|
46
|
+
# Initialize tracer
|
47
|
+
tracer_provider = kwargs.get("tracer_provider")
|
48
|
+
self._tracer = get_tracer(self.config.library_name, self.config.library_version, tracer_provider)
|
49
|
+
|
50
|
+
# Initialize meter if metrics enabled
|
51
|
+
if self.config.metrics_enabled:
|
52
|
+
meter_provider = kwargs.get("meter_provider")
|
53
|
+
self._meter = get_meter(self.config.library_name, self.config.library_version, meter_provider)
|
54
|
+
self._metrics = self._create_metrics(self._meter)
|
55
|
+
|
56
|
+
# Perform custom initialization
|
57
|
+
self._initialize(**kwargs)
|
58
|
+
|
59
|
+
# Wrap all configured methods
|
60
|
+
self._wrap_methods()
|
61
|
+
|
62
|
+
# Perform custom wrapping
|
63
|
+
self._custom_wrap(**kwargs)
|
64
|
+
|
65
|
+
def _uninstrument(self, **kwargs):
|
66
|
+
"""Remove instrumentation."""
|
67
|
+
# Unwrap all configured methods
|
68
|
+
for wrap_config in self.config.wrapped_methods:
|
69
|
+
try:
|
70
|
+
unwrap(wrap_config)
|
71
|
+
except Exception as e:
|
72
|
+
logger.debug(
|
73
|
+
f"Failed to unwrap {wrap_config.package}.{wrap_config.class_name}.{wrap_config.method_name}: {e}"
|
74
|
+
)
|
75
|
+
|
76
|
+
# Perform custom unwrapping
|
77
|
+
self._custom_unwrap(**kwargs)
|
78
|
+
|
79
|
+
# Clear references
|
80
|
+
self._tracer = None
|
81
|
+
self._meter = None
|
82
|
+
self._metrics.clear()
|
83
|
+
|
84
|
+
def _wrap_methods(self):
|
85
|
+
"""Wrap all configured methods."""
|
86
|
+
for wrap_config in self.config.wrapped_methods:
|
87
|
+
try:
|
88
|
+
wrap(wrap_config, self._tracer)
|
89
|
+
except (AttributeError, ModuleNotFoundError) as e:
|
90
|
+
logger.debug(
|
91
|
+
f"Could not wrap {wrap_config.package}.{wrap_config.class_name}.{wrap_config.method_name}: {e}"
|
92
|
+
)
|
93
|
+
|
94
|
+
@abstractmethod
|
95
|
+
def _create_metrics(self, meter: Meter) -> Dict[str, Any]:
|
96
|
+
"""Create metrics for the instrumentor.
|
97
|
+
|
98
|
+
Returns a dictionary of metric name to metric instance.
|
99
|
+
"""
|
100
|
+
pass
|
101
|
+
|
102
|
+
def _initialize(self, **kwargs):
|
103
|
+
"""Perform custom initialization.
|
104
|
+
|
105
|
+
Override in subclasses for custom initialization logic.
|
106
|
+
"""
|
107
|
+
pass
|
108
|
+
|
109
|
+
def _custom_wrap(self, **kwargs):
|
110
|
+
"""Perform custom wrapping beyond configured methods.
|
111
|
+
|
112
|
+
Override in subclasses for special wrapping needs.
|
113
|
+
"""
|
114
|
+
pass
|
115
|
+
|
116
|
+
def _custom_unwrap(self, **kwargs):
|
117
|
+
"""Perform custom unwrapping beyond configured methods.
|
118
|
+
|
119
|
+
Override in subclasses for special unwrapping needs.
|
120
|
+
"""
|
121
|
+
pass
|
122
|
+
|
123
|
+
|
124
|
+
def create_wrapper_factory(wrapper_func: Callable, *wrapper_args, **wrapper_kwargs) -> Callable:
|
125
|
+
"""Create a factory function for wrapt-style wrappers.
|
126
|
+
|
127
|
+
This is useful for creating wrappers that need additional arguments
|
128
|
+
beyond the standard (wrapped, instance, args, kwargs).
|
129
|
+
|
130
|
+
Args:
|
131
|
+
wrapper_func: The wrapper function to call
|
132
|
+
*wrapper_args: Arguments to pass to the wrapper
|
133
|
+
**wrapper_kwargs: Keyword arguments to pass to the wrapper
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
A factory function that returns the configured wrapper
|
137
|
+
"""
|
138
|
+
|
139
|
+
def factory(tracer: Tracer):
|
140
|
+
def wrapper(wrapped, instance, args, kwargs):
|
141
|
+
return wrapper_func(
|
142
|
+
tracer, *wrapper_args, wrapped=wrapped, instance=instance, args=args, kwargs=kwargs, **wrapper_kwargs
|
143
|
+
)
|
144
|
+
|
145
|
+
return wrapper
|
146
|
+
|
147
|
+
return factory
|
@@ -0,0 +1,100 @@
|
|
1
|
+
"""Common metrics utilities for AgentOps instrumentation.
|
2
|
+
|
3
|
+
This module provides utilities for creating and managing standard metrics
|
4
|
+
across different instrumentations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Dict, Any, Optional
|
8
|
+
from opentelemetry.metrics import Meter, Histogram, Counter
|
9
|
+
from agentops.semconv import Meters
|
10
|
+
|
11
|
+
|
12
|
+
class StandardMetrics:
|
13
|
+
"""Factory for creating standard metrics used across instrumentations."""
|
14
|
+
|
15
|
+
@staticmethod
|
16
|
+
def create_token_histogram(meter: Meter) -> Histogram:
|
17
|
+
"""Create a histogram for token usage."""
|
18
|
+
return meter.create_histogram(
|
19
|
+
name=Meters.LLM_TOKEN_USAGE, unit="token", description="Measures number of input and output tokens used"
|
20
|
+
)
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def create_duration_histogram(meter: Meter) -> Histogram:
|
24
|
+
"""Create a histogram for operation duration."""
|
25
|
+
return meter.create_histogram(
|
26
|
+
name=Meters.LLM_OPERATION_DURATION, unit="s", description="GenAI operation duration"
|
27
|
+
)
|
28
|
+
|
29
|
+
@staticmethod
|
30
|
+
def create_exception_counter(meter: Meter, name: str = Meters.LLM_COMPLETIONS_EXCEPTIONS) -> Counter:
|
31
|
+
"""Create a counter for exceptions."""
|
32
|
+
return meter.create_counter(
|
33
|
+
name=name, unit="time", description="Number of exceptions occurred during operations"
|
34
|
+
)
|
35
|
+
|
36
|
+
@staticmethod
|
37
|
+
def create_choice_counter(meter: Meter) -> Counter:
|
38
|
+
"""Create a counter for generation choices."""
|
39
|
+
return meter.create_counter(
|
40
|
+
name=Meters.LLM_GENERATION_CHOICES,
|
41
|
+
unit="choice",
|
42
|
+
description="Number of choices returned by completions call",
|
43
|
+
)
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def create_standard_metrics(meter: Meter) -> Dict[str, Any]:
|
47
|
+
"""Create a standard set of metrics for LLM operations.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Dictionary with metric names as keys and metric instances as values
|
51
|
+
"""
|
52
|
+
return {
|
53
|
+
"token_histogram": StandardMetrics.create_token_histogram(meter),
|
54
|
+
"duration_histogram": StandardMetrics.create_duration_histogram(meter),
|
55
|
+
"exception_counter": StandardMetrics.create_exception_counter(meter),
|
56
|
+
}
|
57
|
+
|
58
|
+
|
59
|
+
class MetricsRecorder:
|
60
|
+
"""Utility class for recording metrics in a consistent way."""
|
61
|
+
|
62
|
+
def __init__(self, metrics: Dict[str, Any]):
|
63
|
+
self.metrics = metrics
|
64
|
+
|
65
|
+
def record_token_usage(
|
66
|
+
self,
|
67
|
+
prompt_tokens: Optional[int] = None,
|
68
|
+
completion_tokens: Optional[int] = None,
|
69
|
+
attributes: Optional[Dict[str, Any]] = None,
|
70
|
+
):
|
71
|
+
"""Record token usage metrics."""
|
72
|
+
token_histogram = self.metrics.get("token_histogram")
|
73
|
+
if not token_histogram:
|
74
|
+
return
|
75
|
+
|
76
|
+
attrs = attributes or {}
|
77
|
+
|
78
|
+
if prompt_tokens is not None:
|
79
|
+
token_histogram.record(prompt_tokens, attributes={**attrs, "token.type": "input"})
|
80
|
+
|
81
|
+
if completion_tokens is not None:
|
82
|
+
token_histogram.record(completion_tokens, attributes={**attrs, "token.type": "output"})
|
83
|
+
|
84
|
+
def record_duration(self, duration: float, attributes: Optional[Dict[str, Any]] = None):
|
85
|
+
"""Record operation duration."""
|
86
|
+
duration_histogram = self.metrics.get("duration_histogram")
|
87
|
+
if duration_histogram:
|
88
|
+
duration_histogram.record(duration, attributes=attributes or {})
|
89
|
+
|
90
|
+
def record_exception(self, attributes: Optional[Dict[str, Any]] = None):
|
91
|
+
"""Record an exception occurrence."""
|
92
|
+
exception_counter = self.metrics.get("exception_counter")
|
93
|
+
if exception_counter:
|
94
|
+
exception_counter.add(1, attributes=attributes or {})
|
95
|
+
|
96
|
+
def record_choices(self, count: int, attributes: Optional[Dict[str, Any]] = None):
|
97
|
+
"""Record number of choices returned."""
|
98
|
+
choice_counter = self.metrics.get("choice_counter")
|
99
|
+
if choice_counter:
|
100
|
+
choice_counter.add(count, attributes=attributes or {})
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from agentops.client.api.types import UploadedObjectResponse
|
2
|
+
from agentops.instrumentation.common import AttributeMap, _extract_attributes_from_mapping
|
3
|
+
|
4
|
+
|
5
|
+
UPLOADED_OBJECT_ATTRIBUTES: AttributeMap = {
|
6
|
+
"object_url": "url",
|
7
|
+
"object_size": "size",
|
8
|
+
}
|
9
|
+
|
10
|
+
|
11
|
+
def get_uploaded_object_attributes(uploaded_object: UploadedObjectResponse, prefix: str) -> AttributeMap:
|
12
|
+
"""Extract attributes from an uploaded object.
|
13
|
+
|
14
|
+
This is a common function so we can standardize the data format we serialize
|
15
|
+
stored objects to.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
uploaded_object: The uploaded object to extract attributes from.
|
19
|
+
prefix: The prefix to use for the attribute keys. Keys will be concatenated
|
20
|
+
with the prefix and a dot (.) separator.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
A dictionary of extracted attributes.
|
24
|
+
"""
|
25
|
+
attribute_map = {f"{prefix}.{key}": value for key, value in UPLOADED_OBJECT_ATTRIBUTES.items()}
|
26
|
+
return _extract_attributes_from_mapping(uploaded_object, attribute_map)
|