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,598 @@
|
|
1
|
+
"""
|
2
|
+
AgentOps Instrumentation Module
|
3
|
+
|
4
|
+
This module provides automatic instrumentation for various LLM providers and agentic libraries.
|
5
|
+
It works by monitoring Python imports and automatically instrumenting packages as they are imported.
|
6
|
+
|
7
|
+
Key Features:
|
8
|
+
- Automatic detection and instrumentation of LLM providers (OpenAI, Anthropic, etc.)
|
9
|
+
- Support for agentic libraries (CrewAI, AutoGen, etc.)
|
10
|
+
- Version-aware instrumentation (only activates for supported versions)
|
11
|
+
- Smart handling of provider vs agentic library conflicts
|
12
|
+
- Non-intrusive monitoring using Python's import system
|
13
|
+
"""
|
14
|
+
|
15
|
+
from typing import Optional, Set, TypedDict
|
16
|
+
|
17
|
+
try:
|
18
|
+
from typing import NotRequired
|
19
|
+
except ImportError:
|
20
|
+
from typing_extensions import NotRequired
|
21
|
+
from types import ModuleType
|
22
|
+
from dataclasses import dataclass
|
23
|
+
import importlib
|
24
|
+
import sys
|
25
|
+
from packaging.version import Version, parse
|
26
|
+
import builtins
|
27
|
+
|
28
|
+
# Add os and site for path checking
|
29
|
+
import os
|
30
|
+
import site
|
31
|
+
|
32
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore
|
33
|
+
|
34
|
+
from agentops.logging import logger
|
35
|
+
from agentops.sdk.core import tracer
|
36
|
+
from agentops.instrumentation.common import get_library_version
|
37
|
+
|
38
|
+
|
39
|
+
# Define the structure for instrumentor configurations
|
40
|
+
class InstrumentorConfig(TypedDict):
|
41
|
+
module_name: str
|
42
|
+
class_name: str
|
43
|
+
min_version: str
|
44
|
+
package_name: NotRequired[str] # Optional: actual pip package name if different from module
|
45
|
+
|
46
|
+
|
47
|
+
# Configuration for supported LLM providers
|
48
|
+
PROVIDERS: dict[str, InstrumentorConfig] = {
|
49
|
+
"openai": {
|
50
|
+
"module_name": "agentops.instrumentation.providers.openai",
|
51
|
+
"class_name": "OpenaiInstrumentor",
|
52
|
+
"min_version": "1.0.0",
|
53
|
+
},
|
54
|
+
"anthropic": {
|
55
|
+
"module_name": "agentops.instrumentation.providers.anthropic",
|
56
|
+
"class_name": "AnthropicInstrumentor",
|
57
|
+
"min_version": "0.32.0",
|
58
|
+
},
|
59
|
+
"ibm_watsonx_ai": {
|
60
|
+
"module_name": "agentops.instrumentation.providers.ibm_watsonx_ai",
|
61
|
+
"class_name": "WatsonxInstrumentor",
|
62
|
+
"min_version": "0.1.0",
|
63
|
+
},
|
64
|
+
"google.genai": {
|
65
|
+
"module_name": "agentops.instrumentation.providers.google_genai",
|
66
|
+
"class_name": "GoogleGenaiInstrumentor",
|
67
|
+
"min_version": "0.1.0",
|
68
|
+
"package_name": "google-genai", # Actual pip package name
|
69
|
+
},
|
70
|
+
"mem0": {
|
71
|
+
"module_name": "agentops.instrumentation.providers.mem0",
|
72
|
+
"class_name": "Mem0Instrumentor",
|
73
|
+
"min_version": "0.1.0",
|
74
|
+
"package_name": "mem0ai",
|
75
|
+
},
|
76
|
+
}
|
77
|
+
|
78
|
+
# Configuration for supported agentic libraries
|
79
|
+
AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = {
|
80
|
+
"crewai": {
|
81
|
+
"module_name": "agentops.instrumentation.agentic.crewai",
|
82
|
+
"class_name": "CrewaiInstrumentor",
|
83
|
+
"min_version": "0.56.0",
|
84
|
+
},
|
85
|
+
"autogen": {
|
86
|
+
"module_name": "agentops.instrumentation.agentic.ag2",
|
87
|
+
"class_name": "AG2Instrumentor",
|
88
|
+
"min_version": "0.3.2",
|
89
|
+
},
|
90
|
+
"agents": {
|
91
|
+
"module_name": "agentops.instrumentation.agentic.openai_agents",
|
92
|
+
"class_name": "OpenAIAgentsInstrumentor",
|
93
|
+
"min_version": "0.0.1",
|
94
|
+
},
|
95
|
+
"google.adk": {
|
96
|
+
"module_name": "agentops.instrumentation.agentic.google_adk",
|
97
|
+
"class_name": "GooogleAdkInstrumentor",
|
98
|
+
"min_version": "0.1.0",
|
99
|
+
},
|
100
|
+
"agno": {
|
101
|
+
"module_name": "agentops.instrumentation.agentic.agno",
|
102
|
+
"class_name": "AgnoInstrumentor",
|
103
|
+
"min_version": "1.5.8",
|
104
|
+
},
|
105
|
+
"smolagents": {
|
106
|
+
"module_name": "agentops.instrumentation.agentic.smolagents",
|
107
|
+
"class_name": "SmolagentsInstrumentor",
|
108
|
+
"min_version": "1.0.0",
|
109
|
+
},
|
110
|
+
"langgraph": {
|
111
|
+
"module_name": "agentops.instrumentation.agentic.langgraph",
|
112
|
+
"class_name": "LanggraphInstrumentor",
|
113
|
+
"min_version": "0.2.0",
|
114
|
+
},
|
115
|
+
}
|
116
|
+
|
117
|
+
# Combine all target packages for monitoring
|
118
|
+
TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys())
|
119
|
+
|
120
|
+
# Create a single instance of the manager
|
121
|
+
# _manager = InstrumentationManager() # Removed
|
122
|
+
|
123
|
+
# Module-level state variables
|
124
|
+
_active_instrumentors: list[BaseInstrumentor] = []
|
125
|
+
_original_builtins_import = builtins.__import__ # Store original import
|
126
|
+
_instrumenting_packages: Set[str] = set()
|
127
|
+
_has_agentic_library: bool = False
|
128
|
+
|
129
|
+
|
130
|
+
# New helper function to check module origin
|
131
|
+
def _is_installed_package(module_obj: ModuleType, package_name_key: str) -> bool:
|
132
|
+
"""
|
133
|
+
Determines if the given module object corresponds to an installed site-package
|
134
|
+
rather than a local module, especially when names might collide.
|
135
|
+
`package_name_key` is the key from TARGET_PACKAGES (e.g., 'agents', 'google.adk').
|
136
|
+
"""
|
137
|
+
if not hasattr(module_obj, "__file__") or not module_obj.__file__:
|
138
|
+
logger.debug(
|
139
|
+
f"_is_installed_package: Module '{package_name_key}' has no __file__, assuming it might be an SDK namespace package. Returning True."
|
140
|
+
)
|
141
|
+
return True
|
142
|
+
|
143
|
+
module_path = os.path.normcase(os.path.realpath(os.path.abspath(module_obj.__file__)))
|
144
|
+
|
145
|
+
# Priority 1: Check if it's in any site-packages directory.
|
146
|
+
site_packages_dirs = site.getsitepackages()
|
147
|
+
if isinstance(site_packages_dirs, str):
|
148
|
+
site_packages_dirs = [site_packages_dirs]
|
149
|
+
|
150
|
+
if hasattr(site, "USER_SITE") and site.USER_SITE and os.path.exists(site.USER_SITE):
|
151
|
+
site_packages_dirs.append(site.USER_SITE)
|
152
|
+
|
153
|
+
normalized_site_packages_dirs = [
|
154
|
+
os.path.normcase(os.path.realpath(p)) for p in site_packages_dirs if p and os.path.exists(p)
|
155
|
+
]
|
156
|
+
|
157
|
+
for sp_dir in normalized_site_packages_dirs:
|
158
|
+
if module_path.startswith(sp_dir):
|
159
|
+
logger.debug(
|
160
|
+
f"_is_installed_package: Module '{package_name_key}' is a library, instrumenting '{package_name_key}'."
|
161
|
+
)
|
162
|
+
return True
|
163
|
+
|
164
|
+
# Priority 2: If not in site-packages, it's highly likely a local module or not an SDK we target.
|
165
|
+
logger.debug(f"_is_installed_package: Module '{package_name_key}' is a local module, skipping instrumentation.")
|
166
|
+
return False
|
167
|
+
|
168
|
+
|
169
|
+
def _is_package_instrumented(package_name: str) -> bool:
|
170
|
+
"""Check if a package is already instrumented by looking at active instrumentors."""
|
171
|
+
# Handle package.module names by converting dots to underscores for comparison
|
172
|
+
normalized_target_name = package_name.replace(".", "_").lower()
|
173
|
+
for instrumentor in _active_instrumentors:
|
174
|
+
# Check based on the key it was registered with
|
175
|
+
if (
|
176
|
+
hasattr(instrumentor, "_agentops_instrumented_package_key")
|
177
|
+
and instrumentor._agentops_instrumented_package_key == package_name
|
178
|
+
):
|
179
|
+
return True
|
180
|
+
|
181
|
+
# Fallback to class name check (existing logic, less precise)
|
182
|
+
# We use split('.')[-1] for cases like 'google.genai' to match GenAIInstrumentor
|
183
|
+
instrumentor_class_name_prefix = instrumentor.__class__.__name__.lower().replace("instrumentor", "")
|
184
|
+
target_base_name = package_name.split(".")[-1].lower()
|
185
|
+
normalized_class_name_match = (
|
186
|
+
normalized_target_name.startswith(instrumentor_class_name_prefix)
|
187
|
+
or target_base_name == instrumentor_class_name_prefix
|
188
|
+
)
|
189
|
+
|
190
|
+
if normalized_class_name_match:
|
191
|
+
# This fallback can be noisy, let's make it more specific or rely on the key above more
|
192
|
+
# For now, if the key matches or this broad name match works, consider instrumented.
|
193
|
+
# This helps if _agentops_instrumented_package_key was somehow not set.
|
194
|
+
return True
|
195
|
+
|
196
|
+
return False
|
197
|
+
|
198
|
+
|
199
|
+
def _uninstrument_providers():
|
200
|
+
"""Uninstrument all provider instrumentors while keeping agentic libraries active."""
|
201
|
+
global _active_instrumentors
|
202
|
+
new_active_instrumentors = []
|
203
|
+
uninstrumented_any = False
|
204
|
+
for instrumentor in _active_instrumentors:
|
205
|
+
instrumented_key = getattr(instrumentor, "_agentops_instrumented_package_key", None)
|
206
|
+
if instrumented_key and instrumented_key in PROVIDERS:
|
207
|
+
try:
|
208
|
+
instrumentor.uninstrument()
|
209
|
+
logger.debug(
|
210
|
+
f"AgentOps: Uninstrumented provider: {instrumentor.__class__.__name__} (for package '{instrumented_key}') due to agentic library activation."
|
211
|
+
)
|
212
|
+
uninstrumented_any = True
|
213
|
+
except Exception as e:
|
214
|
+
logger.error(f"Error uninstrumenting provider {instrumentor.__class__.__name__}: {e}")
|
215
|
+
else:
|
216
|
+
# Keep non-provider instrumentors or those without our key (shouldn't happen for managed ones)
|
217
|
+
new_active_instrumentors.append(instrumentor)
|
218
|
+
|
219
|
+
if uninstrumented_any or not new_active_instrumentors and _active_instrumentors:
|
220
|
+
logger.debug(
|
221
|
+
f"_uninstrument_providers: Processed. Previous active: {len(_active_instrumentors)}, New active after filtering providers: {len(new_active_instrumentors)}"
|
222
|
+
)
|
223
|
+
_active_instrumentors = new_active_instrumentors
|
224
|
+
|
225
|
+
|
226
|
+
def _should_instrument_package(package_name: str) -> bool:
|
227
|
+
"""
|
228
|
+
Determine if a package should be instrumented based on current state.
|
229
|
+
Handles special cases for agentic libraries and providers.
|
230
|
+
"""
|
231
|
+
global _has_agentic_library
|
232
|
+
|
233
|
+
# If already instrumented by AgentOps (using our refined check), skip.
|
234
|
+
if _is_package_instrumented(package_name):
|
235
|
+
logger.debug(f"_should_instrument_package: '{package_name}' already instrumented by AgentOps. Skipping.")
|
236
|
+
return False
|
237
|
+
|
238
|
+
is_target_agentic = package_name in AGENTIC_LIBRARIES
|
239
|
+
is_target_provider = package_name in PROVIDERS
|
240
|
+
|
241
|
+
if not is_target_agentic and not is_target_provider:
|
242
|
+
logger.debug(
|
243
|
+
f"_should_instrument_package: '{package_name}' is not a targeted provider or agentic library. Skipping."
|
244
|
+
)
|
245
|
+
return False
|
246
|
+
|
247
|
+
if _has_agentic_library:
|
248
|
+
# An agentic library is already active.
|
249
|
+
if is_target_agentic:
|
250
|
+
logger.debug(
|
251
|
+
f"AgentOps: An agentic library is active. Skipping instrumentation for subsequent agentic library '{package_name}'."
|
252
|
+
)
|
253
|
+
return False
|
254
|
+
if is_target_provider:
|
255
|
+
logger.debug(
|
256
|
+
f"AgentOps: An agentic library is active. Skipping instrumentation for provider '{package_name}'."
|
257
|
+
)
|
258
|
+
return False
|
259
|
+
else:
|
260
|
+
# No agentic library is active yet.
|
261
|
+
if is_target_agentic:
|
262
|
+
logger.debug(
|
263
|
+
f"AgentOps: '{package_name}' is the first-targeted agentic library. Will uninstrument providers if any are/become active."
|
264
|
+
)
|
265
|
+
_uninstrument_providers()
|
266
|
+
return True
|
267
|
+
if is_target_provider:
|
268
|
+
logger.debug(
|
269
|
+
f"_should_instrument_package: '{package_name}' is a provider, no agentic library active. Allowing."
|
270
|
+
)
|
271
|
+
return True
|
272
|
+
|
273
|
+
logger.debug(
|
274
|
+
f"_should_instrument_package: Defaulting to False for '{package_name}' (state: _has_agentic_library={_has_agentic_library})"
|
275
|
+
)
|
276
|
+
return False
|
277
|
+
|
278
|
+
|
279
|
+
def _perform_instrumentation(package_name: str):
|
280
|
+
"""Helper function to perform instrumentation for a given package."""
|
281
|
+
global _instrumenting_packages, _active_instrumentors, _has_agentic_library
|
282
|
+
if not _should_instrument_package(package_name):
|
283
|
+
return
|
284
|
+
|
285
|
+
# Get the appropriate configuration for the package
|
286
|
+
# Ensure package_name is a key in either PROVIDERS or AGENTIC_LIBRARIES
|
287
|
+
if package_name not in PROVIDERS and package_name not in AGENTIC_LIBRARIES:
|
288
|
+
logger.debug(
|
289
|
+
f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS or AGENTIC_LIBRARIES. Skipping."
|
290
|
+
)
|
291
|
+
return
|
292
|
+
|
293
|
+
config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name)
|
294
|
+
loader = InstrumentorLoader(**config)
|
295
|
+
|
296
|
+
# instrument_one already checks loader.should_activate
|
297
|
+
instrumentor_instance = instrument_one(loader)
|
298
|
+
if instrumentor_instance is not None:
|
299
|
+
# Check if it was *actually* instrumented by instrument_one by seeing if the instrument method was called successfully.
|
300
|
+
# This relies on instrument_one returning None if its internal .instrument() call failed (if we revert that, this needs adjustment)
|
301
|
+
# For now, assuming instrument_one returns instance only on full success.
|
302
|
+
# User request was to return instrumentor even if .instrument() fails. So, we check if _agentops_instrumented_package_key was set by us.
|
303
|
+
|
304
|
+
# Let's assume instrument_one might return an instance whose .instrument() failed.
|
305
|
+
# The key is set before _active_instrumentors.append, so if it's already there and matches, it means it's a re-attempt on the same package.
|
306
|
+
# The _is_package_instrumented check at the start of _should_instrument_package should prevent most re-entry for the same package_name.
|
307
|
+
|
308
|
+
# Store the package key this instrumentor is for, to aid _is_package_instrumented
|
309
|
+
instrumentor_instance._agentops_instrumented_package_key = package_name
|
310
|
+
|
311
|
+
# Add to active_instrumentors only if it's not a duplicate in terms of package_key being instrumented
|
312
|
+
# This is a safeguard, _is_package_instrumented should catch this earlier.
|
313
|
+
is_newly_added = True
|
314
|
+
for existing_inst in _active_instrumentors:
|
315
|
+
if (
|
316
|
+
hasattr(existing_inst, "_agentops_instrumented_package_key")
|
317
|
+
and existing_inst._agentops_instrumented_package_key == package_name
|
318
|
+
):
|
319
|
+
is_newly_added = False
|
320
|
+
logger.debug(
|
321
|
+
f"_perform_instrumentation: Instrumentor for '{package_name}' already in _active_instrumentors. Not adding again."
|
322
|
+
)
|
323
|
+
break
|
324
|
+
if is_newly_added:
|
325
|
+
_active_instrumentors.append(instrumentor_instance)
|
326
|
+
|
327
|
+
# If this was an agentic library AND it's newly effectively instrumented.
|
328
|
+
if (
|
329
|
+
package_name in AGENTIC_LIBRARIES and not _has_agentic_library
|
330
|
+
): # Check _has_agentic_library to ensure this is the *first* one.
|
331
|
+
# _uninstrument_providers() was already called in _should_instrument_package for the first agentic library.
|
332
|
+
_has_agentic_library = True
|
333
|
+
|
334
|
+
# Special case: If mem0 is instrumented, also instrument concurrent.futures
|
335
|
+
if package_name == "mem0" and is_newly_added:
|
336
|
+
try:
|
337
|
+
# Check if concurrent.futures module is available
|
338
|
+
|
339
|
+
# Create config for concurrent.futures instrumentor
|
340
|
+
concurrent_config = InstrumentorConfig(
|
341
|
+
module_name="agentops.instrumentation.utilities.concurrent_futures",
|
342
|
+
class_name="ConcurrentFuturesInstrumentor",
|
343
|
+
min_version="3.7.0", # Python 3.7+ (concurrent.futures is stdlib)
|
344
|
+
package_name="python", # Special case for stdlib modules
|
345
|
+
)
|
346
|
+
|
347
|
+
# Create and instrument concurrent.futures
|
348
|
+
concurrent_loader = InstrumentorLoader(**concurrent_config)
|
349
|
+
concurrent_instrumentor = instrument_one(concurrent_loader)
|
350
|
+
|
351
|
+
if concurrent_instrumentor is not None:
|
352
|
+
concurrent_instrumentor._agentops_instrumented_package_key = "concurrent.futures"
|
353
|
+
_active_instrumentors.append(concurrent_instrumentor)
|
354
|
+
logger.debug("AgentOps: Instrumented concurrent.futures as a dependency of mem0.")
|
355
|
+
except Exception as e:
|
356
|
+
logger.debug(f"Could not instrument concurrent.futures for mem0: {e}")
|
357
|
+
else:
|
358
|
+
logger.debug(
|
359
|
+
f"_perform_instrumentation: instrument_one for '{package_name}' returned None. Not added to active instrumentors."
|
360
|
+
)
|
361
|
+
|
362
|
+
|
363
|
+
def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), level=0):
|
364
|
+
"""
|
365
|
+
Monitor imports and instrument packages as they are imported.
|
366
|
+
This replaces the built-in import function to intercept package imports.
|
367
|
+
"""
|
368
|
+
global _instrumenting_packages, _has_agentic_library
|
369
|
+
|
370
|
+
# If an agentic library is already instrumented, skip all further instrumentation
|
371
|
+
if _has_agentic_library:
|
372
|
+
return _original_builtins_import(name, globals_dict, locals_dict, fromlist, level)
|
373
|
+
|
374
|
+
# First, do the actual import
|
375
|
+
module = _original_builtins_import(name, globals_dict, locals_dict, fromlist, level)
|
376
|
+
|
377
|
+
# Check for exact matches first (handles package.module like google.adk)
|
378
|
+
packages_to_check = set()
|
379
|
+
|
380
|
+
# Check the imported module itself
|
381
|
+
if name in TARGET_PACKAGES:
|
382
|
+
packages_to_check.add(name)
|
383
|
+
else:
|
384
|
+
# Check if any target package is a prefix of the import name
|
385
|
+
for target in TARGET_PACKAGES:
|
386
|
+
if name.startswith(target + ".") or name == target:
|
387
|
+
packages_to_check.add(target)
|
388
|
+
|
389
|
+
# For "from X import Y" style imports, also check submodules
|
390
|
+
if fromlist:
|
391
|
+
for item in fromlist:
|
392
|
+
# Construct potential full name, e.g., "google.adk" from name="google", item="adk"
|
393
|
+
# Or if name="os", item="path", full_name="os.path"
|
394
|
+
# If the original name itself is a multi-part name like "a.b", and item is "c", then "a.b.c"
|
395
|
+
# This logic needs to correctly identify the root package if 'name' is already a sub-package.
|
396
|
+
# The existing TARGET_PACKAGES check is simpler: it checks against pre-defined full names.
|
397
|
+
|
398
|
+
# Check full name if item forms part of a target package name
|
399
|
+
full_item_name_candidate = f"{name}.{item}"
|
400
|
+
|
401
|
+
if full_item_name_candidate in TARGET_PACKAGES:
|
402
|
+
packages_to_check.add(full_item_name_candidate)
|
403
|
+
else: # Fallback to checking if 'name' itself is a target
|
404
|
+
for target in TARGET_PACKAGES:
|
405
|
+
if name == target or name.startswith(target + "."):
|
406
|
+
packages_to_check.add(target) # Check the base target if a submodule is imported from it.
|
407
|
+
|
408
|
+
# Instrument all matching packages
|
409
|
+
for package_to_check in packages_to_check:
|
410
|
+
if package_to_check not in _instrumenting_packages and not _is_package_instrumented(package_to_check):
|
411
|
+
target_module_obj = sys.modules.get(package_to_check)
|
412
|
+
|
413
|
+
if target_module_obj:
|
414
|
+
is_sdk = _is_installed_package(target_module_obj, package_to_check)
|
415
|
+
if not is_sdk:
|
416
|
+
logger.debug(
|
417
|
+
f"AgentOps: Target '{package_to_check}' appears to be a local module/directory. Skipping AgentOps SDK instrumentation for it."
|
418
|
+
)
|
419
|
+
continue
|
420
|
+
else:
|
421
|
+
logger.debug(
|
422
|
+
f"_import_monitor: No module object found in sys.modules for '{package_to_check}', proceeding with SDK instrumentation attempt."
|
423
|
+
)
|
424
|
+
|
425
|
+
_instrumenting_packages.add(package_to_check)
|
426
|
+
try:
|
427
|
+
_perform_instrumentation(package_to_check)
|
428
|
+
# If we just instrumented an agentic library, stop
|
429
|
+
if _has_agentic_library:
|
430
|
+
break
|
431
|
+
except Exception as e:
|
432
|
+
logger.error(f"Error instrumenting {package_to_check}: {str(e)}")
|
433
|
+
finally:
|
434
|
+
_instrumenting_packages.discard(package_to_check)
|
435
|
+
|
436
|
+
return module
|
437
|
+
|
438
|
+
|
439
|
+
@dataclass
|
440
|
+
class InstrumentorLoader:
|
441
|
+
"""
|
442
|
+
Represents a dynamically-loadable instrumentor.
|
443
|
+
Handles version checking and instantiation of instrumentors.
|
444
|
+
"""
|
445
|
+
|
446
|
+
module_name: str
|
447
|
+
class_name: str
|
448
|
+
min_version: str
|
449
|
+
package_name: Optional[str] = None # Optional: actual pip package name
|
450
|
+
|
451
|
+
@property
|
452
|
+
def module(self) -> ModuleType:
|
453
|
+
"""Get the instrumentor module."""
|
454
|
+
return importlib.import_module(self.module_name)
|
455
|
+
|
456
|
+
@property
|
457
|
+
def should_activate(self) -> bool:
|
458
|
+
"""Check if the package is available and meets version requirements."""
|
459
|
+
try:
|
460
|
+
# Special case for stdlib modules (like concurrent.futures)
|
461
|
+
if self.package_name == "python":
|
462
|
+
import sys
|
463
|
+
|
464
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
465
|
+
return Version(python_version) >= parse(self.min_version)
|
466
|
+
|
467
|
+
# Use explicit package_name if provided, otherwise derive from module_name
|
468
|
+
if self.package_name:
|
469
|
+
provider_name = self.package_name
|
470
|
+
else:
|
471
|
+
provider_name = self.module_name.split(".")[-1]
|
472
|
+
|
473
|
+
# Use common version utility
|
474
|
+
module_version = get_library_version(provider_name)
|
475
|
+
return module_version != "unknown" and Version(module_version) >= parse(self.min_version)
|
476
|
+
except Exception:
|
477
|
+
return False
|
478
|
+
|
479
|
+
def get_instance(self) -> BaseInstrumentor:
|
480
|
+
"""Create and return a new instance of the instrumentor."""
|
481
|
+
return getattr(self.module, self.class_name)()
|
482
|
+
|
483
|
+
|
484
|
+
def instrument_one(loader: InstrumentorLoader) -> Optional[BaseInstrumentor]:
|
485
|
+
"""
|
486
|
+
Instrument a single package using the provided loader.
|
487
|
+
Returns the instrumentor instance if successful, None otherwise.
|
488
|
+
"""
|
489
|
+
if not loader.should_activate:
|
490
|
+
# This log is important for users to know why something wasn't instrumented.
|
491
|
+
logger.debug(
|
492
|
+
f"AgentOps: Package '{loader.package_name or loader.module_name}' not found or version is less than minimum required ('{loader.min_version}'). Skipping instrumentation."
|
493
|
+
)
|
494
|
+
return None
|
495
|
+
|
496
|
+
instrumentor = loader.get_instance()
|
497
|
+
try:
|
498
|
+
# Use the provider directly from the global tracer instance
|
499
|
+
instrumentor.instrument(tracer_provider=tracer.provider)
|
500
|
+
logger.debug(
|
501
|
+
f"AgentOps: Successfully instrumented '{loader.class_name}' for package '{loader.package_name or loader.module_name}'."
|
502
|
+
)
|
503
|
+
except Exception as e:
|
504
|
+
logger.error(
|
505
|
+
f"Failed to instrument {loader.class_name} for {loader.package_name or loader.module_name}: {e}",
|
506
|
+
exc_info=True,
|
507
|
+
)
|
508
|
+
return instrumentor
|
509
|
+
|
510
|
+
|
511
|
+
def instrument_all():
|
512
|
+
"""Start monitoring and instrumenting packages if not already started."""
|
513
|
+
# Check if active_instrumentors is empty, as a proxy for not started.
|
514
|
+
if not _active_instrumentors:
|
515
|
+
builtins.__import__ = _import_monitor
|
516
|
+
global _instrumenting_packages, _has_agentic_library
|
517
|
+
|
518
|
+
# If an agentic library is already instrumented, don't instrument anything else
|
519
|
+
if _has_agentic_library:
|
520
|
+
return
|
521
|
+
|
522
|
+
for name in list(sys.modules.keys()):
|
523
|
+
# Stop if an agentic library gets instrumented during the loop
|
524
|
+
if _has_agentic_library:
|
525
|
+
break
|
526
|
+
|
527
|
+
module = sys.modules.get(name)
|
528
|
+
if not isinstance(module, ModuleType):
|
529
|
+
continue
|
530
|
+
|
531
|
+
# Check for exact matches first (handles package.module like google.adk)
|
532
|
+
package_to_check = None
|
533
|
+
if name in TARGET_PACKAGES:
|
534
|
+
package_to_check = name
|
535
|
+
else:
|
536
|
+
# Check if any target package is a prefix of the module name
|
537
|
+
for target in TARGET_PACKAGES:
|
538
|
+
if name.startswith(target + ".") or name == target:
|
539
|
+
package_to_check = target
|
540
|
+
break
|
541
|
+
|
542
|
+
if (
|
543
|
+
package_to_check
|
544
|
+
and package_to_check not in _instrumenting_packages
|
545
|
+
and not _is_package_instrumented(package_to_check)
|
546
|
+
):
|
547
|
+
target_module_obj = sys.modules.get(package_to_check)
|
548
|
+
|
549
|
+
if target_module_obj:
|
550
|
+
is_sdk = _is_installed_package(target_module_obj, package_to_check)
|
551
|
+
if not is_sdk:
|
552
|
+
continue
|
553
|
+
else:
|
554
|
+
logger.debug(
|
555
|
+
f"instrument_all: No module object found for '{package_to_check}' in sys.modules during startup scan. Proceeding cautiously."
|
556
|
+
)
|
557
|
+
|
558
|
+
_instrumenting_packages.add(package_to_check)
|
559
|
+
try:
|
560
|
+
_perform_instrumentation(package_to_check)
|
561
|
+
except Exception as e:
|
562
|
+
logger.error(f"Error instrumenting {package_to_check}: {str(e)}")
|
563
|
+
finally:
|
564
|
+
_instrumenting_packages.discard(package_to_check)
|
565
|
+
|
566
|
+
|
567
|
+
def uninstrument_all():
|
568
|
+
"""Stop monitoring and uninstrument all packages."""
|
569
|
+
global _active_instrumentors, _has_agentic_library
|
570
|
+
builtins.__import__ = _original_builtins_import
|
571
|
+
for instrumentor in _active_instrumentors:
|
572
|
+
instrumentor.uninstrument()
|
573
|
+
logger.debug(f"Uninstrumented {instrumentor.__class__.__name__}")
|
574
|
+
_active_instrumentors = []
|
575
|
+
_has_agentic_library = False
|
576
|
+
|
577
|
+
|
578
|
+
def get_active_libraries() -> set[str]:
|
579
|
+
"""
|
580
|
+
Get all actively used libraries in the current execution context.
|
581
|
+
Returns a set of package names that are currently imported and being monitored.
|
582
|
+
"""
|
583
|
+
active_libs = set()
|
584
|
+
for name, module in sys.modules.items():
|
585
|
+
if not isinstance(module, ModuleType):
|
586
|
+
continue
|
587
|
+
|
588
|
+
# Check for exact matches first
|
589
|
+
if name in TARGET_PACKAGES:
|
590
|
+
active_libs.add(name)
|
591
|
+
else:
|
592
|
+
# Check if any target package is a prefix of the module name
|
593
|
+
for target in TARGET_PACKAGES:
|
594
|
+
if name.startswith(target + ".") or name == target:
|
595
|
+
active_libs.add(target)
|
596
|
+
break
|
597
|
+
|
598
|
+
return active_libs
|
@@ -0,0 +1,82 @@
|
|
1
|
+
"""Common utilities for AgentOps instrumentation.
|
2
|
+
|
3
|
+
This module provides shared functionality for instrumenting various libraries,
|
4
|
+
including base classes, attribute management, metrics, and streaming utilities.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from agentops.instrumentation.common.attributes import AttributeMap, _extract_attributes_from_mapping
|
8
|
+
from agentops.instrumentation.common.wrappers import _with_tracer_wrapper, WrapConfig, wrap, unwrap
|
9
|
+
from agentops.instrumentation.common.instrumentor import (
|
10
|
+
InstrumentorConfig,
|
11
|
+
CommonInstrumentor,
|
12
|
+
create_wrapper_factory,
|
13
|
+
)
|
14
|
+
from agentops.instrumentation.common.metrics import StandardMetrics, MetricsRecorder
|
15
|
+
from agentops.instrumentation.common.span_management import (
|
16
|
+
SpanAttributeManager,
|
17
|
+
create_span,
|
18
|
+
timed_span,
|
19
|
+
StreamingSpanManager,
|
20
|
+
extract_parent_context,
|
21
|
+
safe_set_attribute,
|
22
|
+
get_span_context_info,
|
23
|
+
)
|
24
|
+
from agentops.instrumentation.common.token_counting import (
|
25
|
+
TokenUsage,
|
26
|
+
TokenUsageExtractor,
|
27
|
+
calculate_token_efficiency,
|
28
|
+
calculate_cache_efficiency,
|
29
|
+
set_token_usage_attributes,
|
30
|
+
)
|
31
|
+
from agentops.instrumentation.common.streaming import (
|
32
|
+
BaseStreamWrapper,
|
33
|
+
SyncStreamWrapper,
|
34
|
+
AsyncStreamWrapper,
|
35
|
+
create_stream_wrapper_factory,
|
36
|
+
StreamingResponseHandler,
|
37
|
+
)
|
38
|
+
from agentops.instrumentation.common.version import (
|
39
|
+
get_library_version,
|
40
|
+
LibraryInfo,
|
41
|
+
)
|
42
|
+
|
43
|
+
__all__ = [
|
44
|
+
# Attributes
|
45
|
+
"AttributeMap",
|
46
|
+
"_extract_attributes_from_mapping",
|
47
|
+
# Wrappers
|
48
|
+
"_with_tracer_wrapper",
|
49
|
+
"WrapConfig",
|
50
|
+
"wrap",
|
51
|
+
"unwrap",
|
52
|
+
# Instrumentor
|
53
|
+
"InstrumentorConfig",
|
54
|
+
"CommonInstrumentor",
|
55
|
+
"create_wrapper_factory",
|
56
|
+
# Metrics
|
57
|
+
"StandardMetrics",
|
58
|
+
"MetricsRecorder",
|
59
|
+
# Span Management
|
60
|
+
"SpanAttributeManager",
|
61
|
+
"create_span",
|
62
|
+
"timed_span",
|
63
|
+
"StreamingSpanManager",
|
64
|
+
"extract_parent_context",
|
65
|
+
"safe_set_attribute",
|
66
|
+
"get_span_context_info",
|
67
|
+
# Token Counting
|
68
|
+
"TokenUsage",
|
69
|
+
"TokenUsageExtractor",
|
70
|
+
"calculate_token_efficiency",
|
71
|
+
"calculate_cache_efficiency",
|
72
|
+
"set_token_usage_attributes",
|
73
|
+
# Streaming
|
74
|
+
"BaseStreamWrapper",
|
75
|
+
"SyncStreamWrapper",
|
76
|
+
"AsyncStreamWrapper",
|
77
|
+
"create_stream_wrapper_factory",
|
78
|
+
"StreamingResponseHandler",
|
79
|
+
# Version
|
80
|
+
"get_library_version",
|
81
|
+
"LibraryInfo",
|
82
|
+
]
|