1313"""
1414
1515from typing import Optional , Set , TypedDict
16+
17+ try :
18+ from typing import NotRequired
19+ except ImportError :
20+ from typing_extensions import NotRequired
1621from types import ModuleType
1722from dataclasses import dataclass
1823import importlib
3641
3742def _is_package_instrumented (package_name : str ) -> bool :
3843 """Check if a package is already instrumented by looking at active instrumentors."""
44+ # Handle package.module names by converting dots to underscores for comparison
45+ normalized_name = package_name .replace ("." , "_" ).lower ()
3946 return any (
40- instrumentor .__class__ .__name__ .lower ().startswith (package_name .lower ())
47+ instrumentor .__class__ .__name__ .lower ().startswith (normalized_name )
48+ or instrumentor .__class__ .__name__ .lower ().startswith (package_name .split ("." )[- 1 ].lower ())
4149 for instrumentor in _active_instrumentors
4250 )
4351
@@ -65,17 +73,14 @@ def _should_instrument_package(package_name: str) -> bool:
6573 if package_name in AGENTIC_LIBRARIES :
6674 _uninstrument_providers ()
6775 _has_agentic_library = True
68- logger .debug (f"Uninstrumented all providers due to agentic library { package_name } detection" )
6976 return True
7077
7178 # Skip providers if an agentic library is already instrumented
7279 if package_name in PROVIDERS and _has_agentic_library :
73- logger .debug (f"Skipping provider { package_name } instrumentation as an agentic library is already instrumented" )
7480 return False
7581
7682 # Skip if already instrumented
7783 if _is_package_instrumented (package_name ):
78- logger .debug (f"Package { package_name } is already instrumented" )
7984 return False
8085
8186 return True
@@ -102,36 +107,62 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(),
102107 Monitor imports and instrument packages as they are imported.
103108 This replaces the built-in import function to intercept package imports.
104109 """
105- global _instrumenting_packages
106- root = name .split ("." , 1 )[0 ]
110+ global _instrumenting_packages , _has_agentic_library
107111
108- # Skip providers if an agentic library is already instrumented
109- if _has_agentic_library and root in PROVIDERS :
112+ # If an agentic library is already instrumented, skip all further instrumentation
113+ if _has_agentic_library :
110114 return _original_builtins_import (name , globals_dict , locals_dict , fromlist , level )
111115
112- # Check if this is a package we should instrument
113- if (
114- root in TARGET_PACKAGES
115- and root not in _instrumenting_packages
116- and not _is_package_instrumented (root ) # Check if already instrumented before adding
117- ):
118- logger .debug (f"Detected import of { root } " )
119- _instrumenting_packages .add (root )
120- try :
121- _perform_instrumentation (root )
122- except Exception as e :
123- logger .error (f"Error instrumenting { root } : { str (e )} " )
124- finally :
125- _instrumenting_packages .discard (root )
126-
127- return _original_builtins_import (name , globals_dict , locals_dict , fromlist , level )
116+ # First, do the actual import
117+ module = _original_builtins_import (name , globals_dict , locals_dict , fromlist , level )
118+
119+ # Check for exact matches first (handles package.module like google.adk)
120+ packages_to_check = set ()
121+
122+ # Check the imported module itself
123+ if name in TARGET_PACKAGES :
124+ packages_to_check .add (name )
125+ else :
126+ # Check if any target package is a prefix of the import name
127+ for target in TARGET_PACKAGES :
128+ if name .startswith (target + "." ) or name == target :
129+ packages_to_check .add (target )
130+
131+ # For "from X import Y" style imports, also check submodules
132+ if fromlist :
133+ for item in fromlist :
134+ full_name = f"{ name } .{ item } "
135+ if full_name in TARGET_PACKAGES :
136+ packages_to_check .add (full_name )
137+ else :
138+ # Check if any target package matches this submodule
139+ for target in TARGET_PACKAGES :
140+ if full_name == target or full_name .startswith (target + "." ):
141+ packages_to_check .add (target )
142+
143+ # Instrument all matching packages
144+ for package_to_check in packages_to_check :
145+ if package_to_check not in _instrumenting_packages and not _is_package_instrumented (package_to_check ):
146+ _instrumenting_packages .add (package_to_check )
147+ try :
148+ _perform_instrumentation (package_to_check )
149+ # If we just instrumented an agentic library, stop
150+ if _has_agentic_library :
151+ break
152+ except Exception as e :
153+ logger .error (f"Error instrumenting { package_to_check } : { str (e )} " )
154+ finally :
155+ _instrumenting_packages .discard (package_to_check )
156+
157+ return module
128158
129159
130160# Define the structure for instrumentor configurations
131161class InstrumentorConfig (TypedDict ):
132162 module_name : str
133163 class_name : str
134164 min_version : str
165+ package_name : NotRequired [str ] # Optional: actual pip package name if different from module
135166
136167
137168# Configuration for supported LLM providers
@@ -146,16 +177,17 @@ class InstrumentorConfig(TypedDict):
146177 "class_name" : "AnthropicInstrumentor" ,
147178 "min_version" : "0.32.0" ,
148179 },
149- "google.genai" : {
150- "module_name" : "agentops.instrumentation.google_generativeai" ,
151- "class_name" : "GoogleGenerativeAIInstrumentor" ,
152- "min_version" : "0.1.0" ,
153- },
154180 "ibm_watsonx_ai" : {
155181 "module_name" : "agentops.instrumentation.ibm_watsonx_ai" ,
156182 "class_name" : "IBMWatsonXInstrumentor" ,
157183 "min_version" : "0.1.0" ,
158184 },
185+ "google.genai" : {
186+ "module_name" : "agentops.instrumentation.google_generativeai" ,
187+ "class_name" : "GoogleGenerativeAIInstrumentor" ,
188+ "min_version" : "0.1.0" ,
189+ "package_name" : "google-genai" , # Actual pip package name
190+ },
159191}
160192
161193# Configuration for supported agentic libraries
@@ -171,6 +203,11 @@ class InstrumentorConfig(TypedDict):
171203 "class_name" : "OpenAIAgentsInstrumentor" ,
172204 "min_version" : "0.0.1" ,
173205 },
206+ "google.adk" : {
207+ "module_name" : "agentops.instrumentation.google_adk" ,
208+ "class_name" : "GoogleADKInstrumentor" ,
209+ "min_version" : "0.1.0" ,
210+ },
174211}
175212
176213# Combine all target packages for monitoring
@@ -190,6 +227,7 @@ class InstrumentorLoader:
190227 module_name : str
191228 class_name : str
192229 min_version : str
230+ package_name : Optional [str ] = None # Optional: actual pip package name
193231
194232 @property
195233 def module (self ) -> ModuleType :
@@ -200,7 +238,11 @@ def module(self) -> ModuleType:
200238 def should_activate (self ) -> bool :
201239 """Check if the package is available and meets version requirements."""
202240 try :
203- provider_name = self .module_name .split ("." )[- 1 ]
241+ # Use explicit package_name if provided, otherwise derive from module_name
242+ if self .package_name :
243+ provider_name = self .package_name
244+ else :
245+ provider_name = self .module_name .split ("." )[- 1 ]
204246 module_version = version (provider_name )
205247 return module_version is not None and Version (module_version ) >= parse (self .min_version )
206248 except ImportError :
@@ -233,24 +275,44 @@ def instrument_all():
233275 # Check if active_instrumentors is empty, as a proxy for not started.
234276 if not _active_instrumentors :
235277 builtins .__import__ = _import_monitor
236- global _instrumenting_packages
278+ global _instrumenting_packages , _has_agentic_library
279+
280+ # If an agentic library is already instrumented, don't instrument anything else
281+ if _has_agentic_library :
282+ return
283+
237284 for name in list (sys .modules .keys ()):
285+ # Stop if an agentic library gets instrumented during the loop
286+ if _has_agentic_library :
287+ break
288+
238289 module = sys .modules .get (name )
239290 if not isinstance (module , ModuleType ):
240291 continue
241292
242- root = name .split ("." , 1 )[0 ]
243- if _has_agentic_library and root in PROVIDERS :
244- continue
245-
246- if root in TARGET_PACKAGES and root not in _instrumenting_packages and not _is_package_instrumented (root ):
247- _instrumenting_packages .add (root )
293+ # Check for exact matches first (handles package.module like google.adk)
294+ package_to_check = None
295+ if name in TARGET_PACKAGES :
296+ package_to_check = name
297+ else :
298+ # Check if any target package is a prefix of the module name
299+ for target in TARGET_PACKAGES :
300+ if name .startswith (target + "." ) or name == target :
301+ package_to_check = target
302+ break
303+
304+ if (
305+ package_to_check
306+ and package_to_check not in _instrumenting_packages
307+ and not _is_package_instrumented (package_to_check )
308+ ):
309+ _instrumenting_packages .add (package_to_check )
248310 try :
249- _perform_instrumentation (root )
311+ _perform_instrumentation (package_to_check )
250312 except Exception as e :
251- logger .error (f"Error instrumenting { root } : { str (e )} " )
313+ logger .error (f"Error instrumenting { package_to_check } : { str (e )} " )
252314 finally :
253- _instrumenting_packages .discard (root )
315+ _instrumenting_packages .discard (package_to_check )
254316
255317
256318def uninstrument_all ():
@@ -269,8 +331,19 @@ def get_active_libraries() -> set[str]:
269331 Get all actively used libraries in the current execution context.
270332 Returns a set of package names that are currently imported and being monitored.
271333 """
272- return {
273- name .split ("." )[0 ]
274- for name , module in sys .modules .items ()
275- if isinstance (module , ModuleType ) and name .split ("." )[0 ] in TARGET_PACKAGES
276- }
334+ active_libs = set ()
335+ for name , module in sys .modules .items ():
336+ if not isinstance (module , ModuleType ):
337+ continue
338+
339+ # Check for exact matches first
340+ if name in TARGET_PACKAGES :
341+ active_libs .add (name )
342+ else :
343+ # Check if any target package is a prefix of the module name
344+ for target in TARGET_PACKAGES :
345+ if name .startswith (target + "." ) or name == target :
346+ active_libs .add (target )
347+ break
348+
349+ return active_libs
0 commit comments