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.
Files changed (94) hide show
  1. agentops/__init__.py +488 -0
  2. agentops/client/__init__.py +5 -0
  3. agentops/client/api/__init__.py +71 -0
  4. agentops/client/api/base.py +162 -0
  5. agentops/client/api/types.py +21 -0
  6. agentops/client/api/versions/__init__.py +10 -0
  7. agentops/client/api/versions/v3.py +65 -0
  8. agentops/client/api/versions/v4.py +104 -0
  9. agentops/client/client.py +211 -0
  10. agentops/client/http/__init__.py +0 -0
  11. agentops/client/http/http_adapter.py +116 -0
  12. agentops/client/http/http_client.py +215 -0
  13. agentops/config.py +268 -0
  14. agentops/enums.py +36 -0
  15. agentops/exceptions.py +38 -0
  16. agentops/helpers/__init__.py +44 -0
  17. agentops/helpers/dashboard.py +54 -0
  18. agentops/helpers/deprecation.py +50 -0
  19. agentops/helpers/env.py +52 -0
  20. agentops/helpers/serialization.py +137 -0
  21. agentops/helpers/system.py +178 -0
  22. agentops/helpers/time.py +11 -0
  23. agentops/helpers/version.py +36 -0
  24. agentops/instrumentation/__init__.py +598 -0
  25. agentops/instrumentation/common/__init__.py +82 -0
  26. agentops/instrumentation/common/attributes.py +278 -0
  27. agentops/instrumentation/common/instrumentor.py +147 -0
  28. agentops/instrumentation/common/metrics.py +100 -0
  29. agentops/instrumentation/common/objects.py +26 -0
  30. agentops/instrumentation/common/span_management.py +176 -0
  31. agentops/instrumentation/common/streaming.py +218 -0
  32. agentops/instrumentation/common/token_counting.py +177 -0
  33. agentops/instrumentation/common/version.py +71 -0
  34. agentops/instrumentation/common/wrappers.py +235 -0
  35. agentops/legacy/__init__.py +277 -0
  36. agentops/legacy/event.py +156 -0
  37. agentops/logging/__init__.py +4 -0
  38. agentops/logging/config.py +86 -0
  39. agentops/logging/formatters.py +34 -0
  40. agentops/logging/instrument_logging.py +91 -0
  41. agentops/sdk/__init__.py +27 -0
  42. agentops/sdk/attributes.py +151 -0
  43. agentops/sdk/core.py +607 -0
  44. agentops/sdk/decorators/__init__.py +51 -0
  45. agentops/sdk/decorators/factory.py +486 -0
  46. agentops/sdk/decorators/utility.py +216 -0
  47. agentops/sdk/exporters.py +87 -0
  48. agentops/sdk/processors.py +71 -0
  49. agentops/sdk/types.py +21 -0
  50. agentops/semconv/__init__.py +36 -0
  51. agentops/semconv/agent.py +29 -0
  52. agentops/semconv/core.py +19 -0
  53. agentops/semconv/enum.py +11 -0
  54. agentops/semconv/instrumentation.py +13 -0
  55. agentops/semconv/langchain.py +63 -0
  56. agentops/semconv/message.py +61 -0
  57. agentops/semconv/meters.py +24 -0
  58. agentops/semconv/resource.py +52 -0
  59. agentops/semconv/span_attributes.py +118 -0
  60. agentops/semconv/span_kinds.py +50 -0
  61. agentops/semconv/status.py +11 -0
  62. agentops/semconv/tool.py +15 -0
  63. agentops/semconv/workflow.py +69 -0
  64. agentops/validation.py +357 -0
  65. mseep_agentops-0.4.18.dist-info/METADATA +49 -0
  66. mseep_agentops-0.4.18.dist-info/RECORD +94 -0
  67. mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
  68. mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
  69. mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
  70. tests/__init__.py +0 -0
  71. tests/conftest.py +10 -0
  72. tests/unit/__init__.py +0 -0
  73. tests/unit/client/__init__.py +1 -0
  74. tests/unit/client/test_http_adapter.py +221 -0
  75. tests/unit/client/test_http_client.py +206 -0
  76. tests/unit/conftest.py +54 -0
  77. tests/unit/sdk/__init__.py +1 -0
  78. tests/unit/sdk/instrumentation_tester.py +207 -0
  79. tests/unit/sdk/test_attributes.py +392 -0
  80. tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
  81. tests/unit/sdk/test_decorators.py +763 -0
  82. tests/unit/sdk/test_exporters.py +241 -0
  83. tests/unit/sdk/test_factory.py +1188 -0
  84. tests/unit/sdk/test_internal_span_processor.py +397 -0
  85. tests/unit/sdk/test_resource_attributes.py +35 -0
  86. tests/unit/test_config.py +82 -0
  87. tests/unit/test_context_manager.py +777 -0
  88. tests/unit/test_events.py +27 -0
  89. tests/unit/test_host_env.py +54 -0
  90. tests/unit/test_init_py.py +501 -0
  91. tests/unit/test_serialization.py +433 -0
  92. tests/unit/test_session.py +676 -0
  93. tests/unit/test_user_agent.py +34 -0
  94. 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)