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,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
+ ]