mseep-agentops 0.4.18__py3-none-any.whl → 0.4.22__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 (153) hide show
  1. agentops/__init__.py +0 -0
  2. agentops/client/api/base.py +28 -30
  3. agentops/client/api/versions/v3.py +29 -25
  4. agentops/client/api/versions/v4.py +87 -46
  5. agentops/client/client.py +98 -29
  6. agentops/client/http/README.md +87 -0
  7. agentops/client/http/http_client.py +126 -172
  8. agentops/config.py +8 -2
  9. agentops/instrumentation/OpenTelemetry.md +133 -0
  10. agentops/instrumentation/README.md +167 -0
  11. agentops/instrumentation/__init__.py +13 -1
  12. agentops/instrumentation/agentic/ag2/__init__.py +18 -0
  13. agentops/instrumentation/agentic/ag2/instrumentor.py +922 -0
  14. agentops/instrumentation/agentic/agno/__init__.py +19 -0
  15. agentops/instrumentation/agentic/agno/attributes/__init__.py +20 -0
  16. agentops/instrumentation/agentic/agno/attributes/agent.py +250 -0
  17. agentops/instrumentation/agentic/agno/attributes/metrics.py +214 -0
  18. agentops/instrumentation/agentic/agno/attributes/storage.py +158 -0
  19. agentops/instrumentation/agentic/agno/attributes/team.py +195 -0
  20. agentops/instrumentation/agentic/agno/attributes/tool.py +210 -0
  21. agentops/instrumentation/agentic/agno/attributes/workflow.py +254 -0
  22. agentops/instrumentation/agentic/agno/instrumentor.py +1313 -0
  23. agentops/instrumentation/agentic/crewai/LICENSE +201 -0
  24. agentops/instrumentation/agentic/crewai/NOTICE.md +10 -0
  25. agentops/instrumentation/agentic/crewai/__init__.py +6 -0
  26. agentops/instrumentation/agentic/crewai/crewai_span_attributes.py +335 -0
  27. agentops/instrumentation/agentic/crewai/instrumentation.py +535 -0
  28. agentops/instrumentation/agentic/crewai/version.py +1 -0
  29. agentops/instrumentation/agentic/google_adk/__init__.py +19 -0
  30. agentops/instrumentation/agentic/google_adk/instrumentor.py +68 -0
  31. agentops/instrumentation/agentic/google_adk/patch.py +767 -0
  32. agentops/instrumentation/agentic/haystack/__init__.py +1 -0
  33. agentops/instrumentation/agentic/haystack/instrumentor.py +186 -0
  34. agentops/instrumentation/agentic/langgraph/__init__.py +3 -0
  35. agentops/instrumentation/agentic/langgraph/attributes.py +54 -0
  36. agentops/instrumentation/agentic/langgraph/instrumentation.py +598 -0
  37. agentops/instrumentation/agentic/langgraph/version.py +1 -0
  38. agentops/instrumentation/agentic/openai_agents/README.md +156 -0
  39. agentops/instrumentation/agentic/openai_agents/SPANS.md +145 -0
  40. agentops/instrumentation/agentic/openai_agents/TRACING_API.md +144 -0
  41. agentops/instrumentation/agentic/openai_agents/__init__.py +30 -0
  42. agentops/instrumentation/agentic/openai_agents/attributes/common.py +549 -0
  43. agentops/instrumentation/agentic/openai_agents/attributes/completion.py +172 -0
  44. agentops/instrumentation/agentic/openai_agents/attributes/model.py +58 -0
  45. agentops/instrumentation/agentic/openai_agents/attributes/tokens.py +275 -0
  46. agentops/instrumentation/agentic/openai_agents/exporter.py +469 -0
  47. agentops/instrumentation/agentic/openai_agents/instrumentor.py +107 -0
  48. agentops/instrumentation/agentic/openai_agents/processor.py +58 -0
  49. agentops/instrumentation/agentic/smolagents/README.md +88 -0
  50. agentops/instrumentation/agentic/smolagents/__init__.py +12 -0
  51. agentops/instrumentation/agentic/smolagents/attributes/agent.py +354 -0
  52. agentops/instrumentation/agentic/smolagents/attributes/model.py +205 -0
  53. agentops/instrumentation/agentic/smolagents/instrumentor.py +286 -0
  54. agentops/instrumentation/agentic/smolagents/stream_wrapper.py +258 -0
  55. agentops/instrumentation/agentic/xpander/__init__.py +15 -0
  56. agentops/instrumentation/agentic/xpander/context.py +112 -0
  57. agentops/instrumentation/agentic/xpander/instrumentor.py +877 -0
  58. agentops/instrumentation/agentic/xpander/trace_probe.py +86 -0
  59. agentops/instrumentation/agentic/xpander/version.py +3 -0
  60. agentops/instrumentation/common/README.md +65 -0
  61. agentops/instrumentation/common/attributes.py +1 -2
  62. agentops/instrumentation/providers/anthropic/__init__.py +24 -0
  63. agentops/instrumentation/providers/anthropic/attributes/__init__.py +23 -0
  64. agentops/instrumentation/providers/anthropic/attributes/common.py +64 -0
  65. agentops/instrumentation/providers/anthropic/attributes/message.py +541 -0
  66. agentops/instrumentation/providers/anthropic/attributes/tools.py +231 -0
  67. agentops/instrumentation/providers/anthropic/event_handler_wrapper.py +90 -0
  68. agentops/instrumentation/providers/anthropic/instrumentor.py +146 -0
  69. agentops/instrumentation/providers/anthropic/stream_wrapper.py +436 -0
  70. agentops/instrumentation/providers/google_genai/README.md +33 -0
  71. agentops/instrumentation/providers/google_genai/__init__.py +24 -0
  72. agentops/instrumentation/providers/google_genai/attributes/__init__.py +25 -0
  73. agentops/instrumentation/providers/google_genai/attributes/chat.py +125 -0
  74. agentops/instrumentation/providers/google_genai/attributes/common.py +88 -0
  75. agentops/instrumentation/providers/google_genai/attributes/model.py +284 -0
  76. agentops/instrumentation/providers/google_genai/instrumentor.py +170 -0
  77. agentops/instrumentation/providers/google_genai/stream_wrapper.py +238 -0
  78. agentops/instrumentation/providers/ibm_watsonx_ai/__init__.py +28 -0
  79. agentops/instrumentation/providers/ibm_watsonx_ai/attributes/__init__.py +27 -0
  80. agentops/instrumentation/providers/ibm_watsonx_ai/attributes/attributes.py +277 -0
  81. agentops/instrumentation/providers/ibm_watsonx_ai/attributes/common.py +104 -0
  82. agentops/instrumentation/providers/ibm_watsonx_ai/instrumentor.py +162 -0
  83. agentops/instrumentation/providers/ibm_watsonx_ai/stream_wrapper.py +302 -0
  84. agentops/instrumentation/providers/mem0/__init__.py +45 -0
  85. agentops/instrumentation/providers/mem0/common.py +377 -0
  86. agentops/instrumentation/providers/mem0/instrumentor.py +270 -0
  87. agentops/instrumentation/providers/mem0/memory.py +430 -0
  88. agentops/instrumentation/providers/openai/__init__.py +21 -0
  89. agentops/instrumentation/providers/openai/attributes/__init__.py +7 -0
  90. agentops/instrumentation/providers/openai/attributes/common.py +55 -0
  91. agentops/instrumentation/providers/openai/attributes/response.py +607 -0
  92. agentops/instrumentation/providers/openai/config.py +36 -0
  93. agentops/instrumentation/providers/openai/instrumentor.py +312 -0
  94. agentops/instrumentation/providers/openai/stream_wrapper.py +941 -0
  95. agentops/instrumentation/providers/openai/utils.py +44 -0
  96. agentops/instrumentation/providers/openai/v0.py +176 -0
  97. agentops/instrumentation/providers/openai/v0_wrappers.py +483 -0
  98. agentops/instrumentation/providers/openai/wrappers/__init__.py +30 -0
  99. agentops/instrumentation/providers/openai/wrappers/assistant.py +277 -0
  100. agentops/instrumentation/providers/openai/wrappers/chat.py +259 -0
  101. agentops/instrumentation/providers/openai/wrappers/completion.py +109 -0
  102. agentops/instrumentation/providers/openai/wrappers/embeddings.py +94 -0
  103. agentops/instrumentation/providers/openai/wrappers/image_gen.py +75 -0
  104. agentops/instrumentation/providers/openai/wrappers/responses.py +191 -0
  105. agentops/instrumentation/providers/openai/wrappers/shared.py +81 -0
  106. agentops/instrumentation/utilities/concurrent_futures/__init__.py +10 -0
  107. agentops/instrumentation/utilities/concurrent_futures/instrumentation.py +206 -0
  108. agentops/integration/callbacks/dspy/__init__.py +11 -0
  109. agentops/integration/callbacks/dspy/callback.py +471 -0
  110. agentops/integration/callbacks/langchain/README.md +59 -0
  111. agentops/integration/callbacks/langchain/__init__.py +15 -0
  112. agentops/integration/callbacks/langchain/callback.py +791 -0
  113. agentops/integration/callbacks/langchain/utils.py +54 -0
  114. agentops/legacy/crewai.md +121 -0
  115. agentops/logging/instrument_logging.py +4 -0
  116. agentops/sdk/README.md +220 -0
  117. agentops/sdk/core.py +75 -32
  118. agentops/sdk/descriptors/classproperty.py +28 -0
  119. agentops/sdk/exporters.py +152 -33
  120. agentops/semconv/README.md +125 -0
  121. agentops/semconv/span_kinds.py +0 -2
  122. agentops/validation.py +102 -63
  123. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/METADATA +30 -40
  124. mseep_agentops-0.4.22.dist-info/RECORD +178 -0
  125. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/WHEEL +1 -2
  126. mseep_agentops-0.4.18.dist-info/RECORD +0 -94
  127. mseep_agentops-0.4.18.dist-info/top_level.txt +0 -2
  128. tests/conftest.py +0 -10
  129. tests/unit/client/__init__.py +0 -1
  130. tests/unit/client/test_http_adapter.py +0 -221
  131. tests/unit/client/test_http_client.py +0 -206
  132. tests/unit/conftest.py +0 -54
  133. tests/unit/sdk/__init__.py +0 -1
  134. tests/unit/sdk/instrumentation_tester.py +0 -207
  135. tests/unit/sdk/test_attributes.py +0 -392
  136. tests/unit/sdk/test_concurrent_instrumentation.py +0 -468
  137. tests/unit/sdk/test_decorators.py +0 -763
  138. tests/unit/sdk/test_exporters.py +0 -241
  139. tests/unit/sdk/test_factory.py +0 -1188
  140. tests/unit/sdk/test_internal_span_processor.py +0 -397
  141. tests/unit/sdk/test_resource_attributes.py +0 -35
  142. tests/unit/test_config.py +0 -82
  143. tests/unit/test_context_manager.py +0 -777
  144. tests/unit/test_events.py +0 -27
  145. tests/unit/test_host_env.py +0 -54
  146. tests/unit/test_init_py.py +0 -501
  147. tests/unit/test_serialization.py +0 -433
  148. tests/unit/test_session.py +0 -676
  149. tests/unit/test_user_agent.py +0 -34
  150. tests/unit/test_validation.py +0 -405
  151. {tests → agentops/instrumentation/agentic/openai_agents/attributes}/__init__.py +0 -0
  152. /tests/unit/__init__.py → /agentops/instrumentation/providers/openai/attributes/tools.py +0 -0
  153. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
1
  from typing import Dict, Optional
2
+ import threading
2
3
 
3
4
  import requests
4
5
 
@@ -6,150 +7,118 @@ from agentops.client.http.http_adapter import BaseHTTPAdapter
6
7
  from agentops.logging import logger
7
8
  from agentops.helpers.version import get_agentops_version
8
9
 
10
+ # Import aiohttp for async requests
11
+ try:
12
+ import aiohttp
13
+
14
+ AIOHTTP_AVAILABLE = True
15
+ except ImportError:
16
+ AIOHTTP_AVAILABLE = False
17
+ # Don't log warning here, only when actually trying to use async functionality
18
+
9
19
 
10
20
  class HttpClient:
11
- """Base HTTP client with connection pooling and session management"""
21
+ """HTTP client with async-first design and optional sync fallback for log uploads"""
12
22
 
13
23
  _session: Optional[requests.Session] = None
24
+ _async_session: Optional[aiohttp.ClientSession] = None
14
25
  _project_id: Optional[str] = None
26
+ _session_lock = threading.Lock()
15
27
 
16
28
  @classmethod
17
29
  def get_project_id(cls) -> Optional[str]:
18
30
  """Get the stored project ID"""
19
31
  return cls._project_id
20
32
 
33
+ @classmethod
34
+ def set_project_id(cls, project_id: str) -> None:
35
+ """Set the project ID"""
36
+ cls._project_id = project_id
37
+
21
38
  @classmethod
22
39
  def get_session(cls) -> requests.Session:
23
- """Get or create the global session with optimized connection pooling"""
40
+ """
41
+ Get or create the global session with optimized connection pooling.
42
+
43
+ Note: This method is deprecated. Use async_request() instead.
44
+ Only kept for log upload module compatibility.
45
+ """
24
46
  if cls._session is None:
25
- cls._session = requests.Session()
26
-
27
- # Configure connection pooling
28
- adapter = BaseHTTPAdapter()
29
-
30
- # Mount adapter for both HTTP and HTTPS
31
- cls._session.mount("http://", adapter)
32
- cls._session.mount("https://", adapter)
33
-
34
- # Set default headers
35
- cls._session.headers.update(
36
- {
37
- "Connection": "keep-alive",
38
- "Keep-Alive": "timeout=10, max=1000",
39
- "Content-Type": "application/json",
40
- "User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
41
- }
42
- )
43
- logger.debug(f"Agentops version: agentops-python/{get_agentops_version() or 'unknown'}")
47
+ with cls._session_lock:
48
+ if cls._session is None: # Double-check locking
49
+ cls._session = requests.Session()
50
+
51
+ # Configure connection pooling
52
+ adapter = BaseHTTPAdapter()
53
+
54
+ # Mount adapter for both HTTP and HTTPS
55
+ cls._session.mount("http://", adapter)
56
+ cls._session.mount("https://", adapter)
57
+
58
+ # Set default headers
59
+ cls._session.headers.update(
60
+ {
61
+ "Connection": "keep-alive",
62
+ "Keep-Alive": "timeout=10, max=1000",
63
+ "Content-Type": "application/json",
64
+ "User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
65
+ }
66
+ )
67
+ logger.debug(f"Agentops version: agentops-python/{get_agentops_version() or 'unknown'}")
44
68
  return cls._session
45
69
 
46
- # @classmethod
47
- # def get_authenticated_session(
48
- # cls,
49
- # endpoint: str,
50
- # api_key: str,
51
- # token_fetcher: Optional[Callable[[str], str]] = None,
52
- # ) -> requests.Session:
53
- # """
54
- # Create a new session with authentication handling.
55
- #
56
- # Args:
57
- # endpoint: Base API endpoint (used to derive auth endpoint if needed)
58
- # api_key: The API key to use for authentication
59
- # token_fetcher: Optional custom token fetcher function
60
- #
61
- # Returns:
62
- # A requests.Session with authentication handling
63
- # """
64
- # # Create auth manager with default token endpoint
65
- # auth_endpoint = f"{endpoint}/auth/token"
66
- # auth_manager = AuthManager(auth_endpoint)
67
- #
68
- # # Use provided token fetcher or create a default one
69
- # if token_fetcher is None:
70
- # def default_token_fetcher(key: str) -> str:
71
- # # Simple token fetching implementation
72
- # try:
73
- # response = requests.post(
74
- # auth_manager.token_endpoint,
75
- # json={"api_key": key},
76
- # headers={"Content-Type": "application/json"},
77
- # timeout=30
78
- # )
79
- #
80
- # if response.status_code == 401 or response.status_code == 403:
81
- # error_msg = "Invalid API key or unauthorized access"
82
- # try:
83
- # error_data = response.json()
84
- # if "error" in error_data:
85
- # error_msg = error_data["error"]
86
- # except Exception:
87
- # if response.text:
88
- # error_msg = response.text
89
- #
90
- # logger.error(f"Authentication failed: {error_msg}")
91
- # raise AgentOpsApiJwtExpiredException(f"Authentication failed: {error_msg}")
92
- #
93
- # if response.status_code >= 500:
94
- # logger.error(f"Server error during authentication: {response.status_code}")
95
- # raise ApiServerException(f"Server error during authentication: {response.status_code}")
96
- #
97
- # if response.status_code != 200:
98
- # logger.error(f"Unexpected status code during authentication: {response.status_code}")
99
- # raise AgentOpsApiJwtExpiredException(f"Failed to fetch token: {response.status_code}")
100
- #
101
- # token_data = response.json()
102
- # if "token" not in token_data:
103
- # logger.error("Token not found in response")
104
- # raise AgentOpsApiJwtExpiredException("Token not found in response")
105
- #
106
- # # Store project_id if present in the response
107
- # if "project_id" in token_data:
108
- # HttpClient._project_id = token_data["project_id"]
109
- # logger.debug(f"Project ID stored: {HttpClient._project_id} (will be set as {ResourceAttributes.PROJECT_ID})")
110
- #
111
- # return token_data["token"]
112
- # except requests.RequestException as e:
113
- # logger.error(f"Network error during authentication: {e}")
114
- # raise AgentOpsApiJwtExpiredException(f"Network error during authentication: {e}")
115
- #
116
- # token_fetcher = default_token_fetcher
117
- #
118
- # # Create a new session
119
- # session = requests.Session()
120
- #
121
- # # Create an authenticated adapter
122
- # adapter = AuthenticatedHttpAdapter(
123
- # auth_manager=auth_manager,
124
- # api_key=api_key,
125
- # token_fetcher=token_fetcher
126
- # )
127
- #
128
- # # Mount the adapter for both HTTP and HTTPS
129
- # session.mount("http://", adapter)
130
- # session.mount("https://", adapter)
131
- #
132
- # # Set default headers
133
- # session.headers.update({
134
- # "Connection": "keep-alive",
135
- # "Keep-Alive": "timeout=10, max=1000",
136
- # "Content-Type": "application/json",
137
- # })
138
- #
139
- # return session
70
+ @classmethod
71
+ async def get_async_session(cls) -> Optional[aiohttp.ClientSession]:
72
+ """Get or create the global async session with optimized connection pooling"""
73
+ if not AIOHTTP_AVAILABLE:
74
+ logger.warning("aiohttp not available, cannot create async session")
75
+ return None
76
+
77
+ # Always create a new session if the current one is None or closed
78
+ if cls._async_session is None or cls._async_session.closed:
79
+ # Close the old session if it exists but is closed
80
+ if cls._async_session is not None and cls._async_session.closed:
81
+ cls._async_session = None
82
+
83
+ # Create connector with connection pooling
84
+ connector = aiohttp.TCPConnector(
85
+ limit=100, # Total connection pool size
86
+ limit_per_host=30, # Per-host connection limit
87
+ ttl_dns_cache=300, # DNS cache TTL
88
+ use_dns_cache=True,
89
+ enable_cleanup_closed=True,
90
+ )
91
+
92
+ # Create session with default headers
93
+ headers = {
94
+ "Content-Type": "application/json",
95
+ "User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
96
+ }
97
+
98
+ cls._async_session = aiohttp.ClientSession(
99
+ connector=connector, headers=headers, timeout=aiohttp.ClientTimeout(total=30)
100
+ )
101
+
102
+ return cls._async_session
103
+
104
+ @classmethod
105
+ async def close_async_session(cls):
106
+ """Close the async session"""
107
+ if cls._async_session and not cls._async_session.closed:
108
+ await cls._async_session.close()
109
+ cls._async_session = None
140
110
 
141
111
  @classmethod
142
- def request(
112
+ async def async_request(
143
113
  cls,
144
114
  method: str,
145
115
  url: str,
146
116
  data: Optional[Dict] = None,
147
117
  headers: Optional[Dict] = None,
148
118
  timeout: int = 30,
149
- max_redirects: int = 5,
150
- ) -> requests.Response:
119
+ ) -> Optional[Dict]:
151
120
  """
152
- Make a generic HTTP request
121
+ Make an async HTTP request and return JSON response
153
122
 
154
123
  Args:
155
124
  method: HTTP method (e.g., 'get', 'post', 'put', 'delete')
@@ -157,59 +126,44 @@ class HttpClient:
157
126
  data: Request payload (for POST, PUT methods)
158
127
  headers: Request headers
159
128
  timeout: Request timeout in seconds
160
- max_redirects: Maximum number of redirects to follow (default: 5)
161
129
 
162
130
  Returns:
163
- Response from the API
164
-
165
- Raises:
166
- requests.RequestException: If the request fails
167
- ValueError: If the redirect limit is exceeded or an unsupported HTTP method is used
131
+ JSON response as dictionary, or None if request failed
168
132
  """
169
- session = cls.get_session()
170
- method = method.lower()
171
- redirect_count = 0
172
-
173
- while redirect_count <= max_redirects:
174
- # Make the request with allow_redirects=False
175
- if method == "get":
176
- response = session.get(url, headers=headers, timeout=timeout, allow_redirects=False)
177
- elif method == "post":
178
- response = session.post(url, json=data, headers=headers, timeout=timeout, allow_redirects=False)
179
- elif method == "put":
180
- response = session.put(url, json=data, headers=headers, timeout=timeout, allow_redirects=False)
181
- elif method == "delete":
182
- response = session.delete(url, headers=headers, timeout=timeout, allow_redirects=False)
183
- else:
184
- raise ValueError(f"Unsupported HTTP method: {method}")
185
-
186
- # Check if we got a redirect response
187
- if response.status_code in (301, 302, 303, 307, 308):
188
- redirect_count += 1
189
-
190
- if redirect_count > max_redirects:
191
- raise ValueError(f"Exceeded maximum number of redirects ({max_redirects})")
192
-
193
- # Get the new location
194
- if "location" not in response.headers:
195
- # No location header, can't redirect
196
- return response
197
-
198
- # Update URL to the redirect location
199
- url = response.headers["location"]
200
-
201
- # For 303 redirects, always use GET for the next request
202
- if response.status_code == 303:
203
- method = "get"
204
- data = None
205
-
206
- logger.debug(f"Following redirect ({redirect_count}/{max_redirects}) to: {url}")
207
-
208
- # Continue the loop to make the next request
209
- continue
210
-
211
- # Not a redirect, return the response
212
- return response
213
-
214
- # This should never be reached due to the max_redirects check above
215
- return response
133
+ if not AIOHTTP_AVAILABLE:
134
+ logger.warning("aiohttp not available, cannot make async request")
135
+ return None
136
+
137
+ try:
138
+ session = await cls.get_async_session()
139
+ if not session:
140
+ return None
141
+
142
+ logger.debug(f"Making async {method} request to {url}")
143
+
144
+ # Prepare request parameters
145
+ kwargs = {"timeout": aiohttp.ClientTimeout(total=timeout), "headers": headers or {}}
146
+
147
+ if data and method.lower() in ["post", "put", "patch"]:
148
+ kwargs["json"] = data
149
+
150
+ # Make the request
151
+ async with session.request(method.upper(), url, **kwargs) as response:
152
+ logger.debug(f"Async request response status: {response.status}")
153
+
154
+ # Check if response is successful
155
+ if response.status >= 400:
156
+ return None
157
+
158
+ # Parse JSON response
159
+ try:
160
+ response_data = await response.json()
161
+ logger.debug(
162
+ f"Async request successful, response keys: {list(response_data.keys()) if response_data else 'None'}"
163
+ )
164
+ return response_data
165
+ except Exception:
166
+ return None
167
+
168
+ except Exception:
169
+ return None
agentops/config.py CHANGED
@@ -9,7 +9,6 @@ from uuid import UUID
9
9
  from opentelemetry.sdk.trace import SpanProcessor
10
10
  from opentelemetry.sdk.trace.export import SpanExporter
11
11
 
12
- from agentops.exceptions import InvalidApiKeyException
13
12
  from agentops.helpers.env import get_env_bool, get_env_int, get_env_list
14
13
  from agentops.helpers.serialization import AgentOpsJSONEncoder
15
14
 
@@ -166,7 +165,14 @@ class Config:
166
165
  try:
167
166
  UUID(api_key)
168
167
  except ValueError:
169
- raise InvalidApiKeyException(api_key, self.endpoint)
168
+ # Log warning but don't throw exception - let async auth handle it
169
+ from agentops.logging import logger
170
+
171
+ logger.warning(
172
+ f"API key format appears invalid: {api_key[:8]}... "
173
+ f"Authentication may fail. Find your API key at {self.endpoint}/settings/projects"
174
+ )
175
+ # Continue with the invalid key - async auth will handle the failure gracefully
170
176
 
171
177
  if endpoint is not None:
172
178
  self.endpoint = endpoint
@@ -0,0 +1,133 @@
1
+ # OpenTelemetry Implementation Notes
2
+
3
+ This document outlines best practices and implementation details for OpenTelemetry in AgentOps instrumentations.
4
+
5
+ ## Key Concepts
6
+
7
+ ### Context Propagation
8
+
9
+ OpenTelemetry relies on proper context propagation to maintain parent-child relationships between spans. This is essential for:
10
+
11
+ - Creating accurate trace waterfalls in visualizations
12
+ - Ensuring all spans from the same logical operation share a trace ID
13
+ - Allowing proper querying and filtering of related operations
14
+
15
+ ### Core Patterns
16
+
17
+ When implementing instrumentations that need to maintain context across different execution contexts:
18
+
19
+ 1. **Store span contexts in dictionaries:**
20
+ ```python
21
+ # Use weakref dictionaries to avoid memory leaks
22
+ self._span_contexts = weakref.WeakKeyDictionary()
23
+ self._trace_root_contexts = weakref.WeakKeyDictionary()
24
+ ```
25
+
26
+ 2. **Create spans with explicit parent contexts:**
27
+ ```python
28
+ parent_context = self._get_parent_context(trace_obj)
29
+ with trace.start_as_current_span(
30
+ name=span_name,
31
+ context=parent_context,
32
+ kind=trace.SpanKind.CLIENT,
33
+ attributes=attributes,
34
+ ) as span:
35
+ # Span operations here
36
+ # Store the span's context for future reference
37
+ context = trace.set_span_in_context(span)
38
+ self._span_contexts[span_obj] = context
39
+ ```
40
+
41
+ 3. **Implement helper methods to retrieve appropriate parent contexts:**
42
+ ```python
43
+ def _get_parent_context(self, trace_obj):
44
+ # Try to get the trace's root context if it exists
45
+ if trace_obj in self._trace_root_contexts:
46
+ return self._trace_root_contexts[trace_obj]
47
+
48
+ # Otherwise, use the current context
49
+ return context_api.context.get_current()
50
+ ```
51
+
52
+ 4. **Debug trace continuity:**
53
+ ```python
54
+ current_span = trace.get_current_span()
55
+ span_context = current_span.get_span_context()
56
+ trace_id = format_trace_id(span_context.trace_id)
57
+ logging.debug(f"Current span trace ID: {trace_id}")
58
+ ```
59
+
60
+ ## Common Pitfalls
61
+
62
+ 1. **Naming conflicts:** Avoid using `trace` as a parameter name when you're also importing the OpenTelemetry `trace` module
63
+ ```python
64
+ # Bad
65
+ def on_trace_start(self, trace):
66
+ # This will cause conflicts with the imported trace module
67
+
68
+ # Good
69
+ def on_trace_start(self, trace_obj):
70
+ # No conflicts with OpenTelemetry's trace module
71
+ ```
72
+
73
+ 2. **Missing parent contexts:** Always explicitly provide parent contexts when available, don't rely on current context alone
74
+
75
+ 3. **Memory leaks:** Use `weakref.WeakKeyDictionary()` for storing spans to allow garbage collection
76
+
77
+ 4. **Lost context:** When calling async or callback functions, be sure to preserve and pass the context
78
+
79
+ ## Testing Context Propagation
80
+
81
+ To verify proper context propagation:
82
+
83
+ 1. Enable debug logging for trace IDs
84
+ 2. Run a simple end-to-end test that generates multiple spans
85
+ 3. Verify all spans share the same trace ID
86
+ 4. Check that parent-child relationships are correctly established
87
+
88
+ ```python
89
+ # Example debug logging
90
+ logging.debug(f"Span {span.name} has trace ID: {format_trace_id(span.get_span_context().trace_id)}")
91
+ ```
92
+
93
+ ## Timestamp Handling in OpenTelemetry
94
+
95
+ When working with OpenTelemetry spans and timestamps:
96
+
97
+ 1. **Automatic Timestamp Tracking:** OpenTelemetry automatically tracks timestamps for spans. When a span is created with `tracer.start_span()` or `tracer.start_as_current_span()`, the start time is captured automatically. When `span.end()` is called, the end time is recorded.
98
+
99
+ 2. **No Manual Timestamp Setting Required:** The standard instrumentation pattern does not require manually setting timestamp attributes on spans. Instead, OpenTelemetry handles this internally through the SpanProcessor and Exporter classes.
100
+
101
+ 3. **Timestamp Representation:** In the OpenTelemetry data model, timestamps are stored as nanoseconds since the Unix epoch (January 1, 1970).
102
+
103
+ 4. **Serialization Responsibility:** The serialization of timestamps from OTel spans to output formats like JSON is handled by the Exporter components. If timestamps aren't appearing correctly in output APIs, the issue is likely in the API exporter, not in the span creation code.
104
+
105
+ 5. **Debugging Timestamps:** To debug timestamp issues, verify that spans are properly starting and ending, rather than manually setting timestamp attributes:
106
+
107
+ ```python
108
+ # Good pattern - timestamps handled by OpenTelemetry automatically
109
+ with tracer.start_as_current_span("my_operation") as span:
110
+ # Do work
111
+ pass # span.end() is called automatically
112
+ ```
113
+
114
+ Note: If timestamps are missing in API output (e.g., empty "start_time" fields), focus on fixes in the exporter and serialization layer, not by manually tracking timestamps in instrumentation code.
115
+
116
+ ## Attributes in OpenTelemetry
117
+
118
+ When working with span attributes in OpenTelemetry:
119
+
120
+ 1. **Root Attributes Node:** The root `attributes` object in the API output JSON should always be empty. This is by design. All attribute data should be stored in the `span_attributes` object.
121
+
122
+ 2. **Span Attributes:** The `span_attributes` object is where all user-defined and semantic attribute data should be stored. This allows for a structured, hierarchical representation of attributes.
123
+
124
+ 3. **Structure Difference:** While the root `attributes` appears as an empty object in the API output, this is normal and expected. Do not attempt to populate this object directly or duplicate data from `span_attributes` into it.
125
+
126
+ 4. **Setting Attributes:** Always set span attributes using the semantic conventions defined in the `agentops/semconv` module:
127
+
128
+ ```python
129
+ from agentops.semconv import agent
130
+
131
+ # Good pattern - using semantic conventions
132
+ span.set_attribute(agent.AGENT_NAME, "My Agent")
133
+ ```
@@ -0,0 +1,167 @@
1
+ # AgentOps Instrumentation
2
+
3
+ This package provides OpenTelemetry instrumentation for various LLM providers and related services.
4
+
5
+ ## Available Instrumentors
6
+
7
+ - **OpenAI** (`v0.27.0+` and `v1.0.0+`)
8
+ - **Anthropic** (`v0.7.0+`)
9
+ - **Google GenAI** (`v0.1.0+`)
10
+ - **IBM WatsonX AI** (`v0.1.0+`)
11
+ - **CrewAI** (`v0.56.0+`)
12
+ - **AG2/AutoGen** (`v0.3.2+`)
13
+ - **Google ADK** (`v0.1.0+`)
14
+ - **Agno** (`v0.0.1+`)
15
+ - **Mem0** (`v0.1.0+`)
16
+ - **smolagents** (`v0.1.0+`)
17
+
18
+ ## Common Module Usage
19
+
20
+ The `agentops.instrumentation.common` module provides shared utilities for creating instrumentations:
21
+
22
+ ### Base Instrumentor
23
+
24
+ Use `CommonInstrumentor` for creating new instrumentations:
25
+
26
+ ```python
27
+ from agentops.instrumentation.common import CommonInstrumentor, InstrumentorConfig, WrapConfig
28
+
29
+ class MyInstrumentor(CommonInstrumentor):
30
+ def __init__(self):
31
+ config = InstrumentorConfig(
32
+ library_name="my-library",
33
+ library_version="1.0.0",
34
+ wrapped_methods=[
35
+ WrapConfig(
36
+ trace_name="my.method",
37
+ package="my_library.module",
38
+ class_name="MyClass",
39
+ method_name="my_method",
40
+ handler=my_attribute_handler
41
+ )
42
+ ],
43
+ dependencies=["my-library >= 1.0.0"]
44
+ )
45
+ super().__init__(config)
46
+ ```
47
+
48
+ ### Attribute Handlers
49
+
50
+ Create attribute handlers to extract data from method calls:
51
+
52
+ ```python
53
+ from agentops.instrumentation.common import AttributeMap
54
+
55
+ def my_attribute_handler(args=None, kwargs=None, return_value=None) -> AttributeMap:
56
+ attributes = {}
57
+
58
+ if kwargs and "model" in kwargs:
59
+ attributes["llm.request.model"] = kwargs["model"]
60
+
61
+ if return_value and hasattr(return_value, "usage"):
62
+ attributes["llm.usage.total_tokens"] = return_value.usage.total_tokens
63
+
64
+ return attributes
65
+ ```
66
+
67
+ ### Span Management
68
+
69
+ Use the span management utilities for consistent span creation:
70
+
71
+ ```python
72
+ from agentops.instrumentation.common import create_span, SpanAttributeManager
73
+
74
+ # Create an attribute manager
75
+ attr_manager = SpanAttributeManager(service_name="my-service")
76
+
77
+ # Use the create_span context manager
78
+ with create_span(
79
+ tracer,
80
+ "my.operation",
81
+ attributes={"my.attribute": "value"},
82
+ attribute_manager=attr_manager
83
+ ) as span:
84
+ # Your operation code here
85
+ pass
86
+ ```
87
+
88
+ ### Token Counting
89
+
90
+ Use the token counting utilities for consistent token usage extraction:
91
+
92
+ ```python
93
+ from agentops.instrumentation.common import TokenUsageExtractor, set_token_usage_attributes
94
+
95
+ # Extract token usage from a response
96
+ usage = TokenUsageExtractor.extract_from_response(response)
97
+
98
+ # Set token usage attributes on a span
99
+ set_token_usage_attributes(span, response)
100
+ ```
101
+
102
+ ### Streaming Support
103
+
104
+ Use streaming utilities for handling streaming responses:
105
+
106
+ ```python
107
+ from agentops.instrumentation.common import create_stream_wrapper_factory, StreamingResponseHandler
108
+
109
+ # Create a stream wrapper factory
110
+ wrapper = create_stream_wrapper_factory(
111
+ tracer,
112
+ "my.stream",
113
+ extract_chunk_content=StreamingResponseHandler.extract_generic_chunk_content,
114
+ initial_attributes={"stream.type": "text"}
115
+ )
116
+
117
+ # Apply to streaming methods
118
+ wrap_function_wrapper("my_module", "stream_method", wrapper)
119
+ ```
120
+
121
+ ### Metrics
122
+
123
+ Use standard metrics for consistency across instrumentations:
124
+
125
+ ```python
126
+ from agentops.instrumentation.common import StandardMetrics, MetricsRecorder
127
+
128
+ # Create standard metrics
129
+ metrics = StandardMetrics.create_standard_metrics(meter)
130
+
131
+ # Use the metrics recorder
132
+ recorder = MetricsRecorder(metrics)
133
+ recorder.record_token_usage(prompt_tokens=100, completion_tokens=50)
134
+ recorder.record_duration(1.5)
135
+ ```
136
+
137
+ ## Creating a New Instrumentor
138
+
139
+ 1. Create a new directory under `agentops/instrumentation/` for your provider
140
+ 2. Create an `__init__.py` file with version information
141
+ 3. Create an `instrumentor.py` file extending `CommonInstrumentor`
142
+ 4. Create attribute handlers in an `attributes/` subdirectory
143
+ 5. Add your instrumentor to the main `__init__.py` configuration
144
+
145
+ Example structure:
146
+ ```
147
+ agentops/instrumentation/
148
+ ├── my_provider/
149
+ │ ├── __init__.py
150
+ │ ├── instrumentor.py
151
+ │ └── attributes/
152
+ │ ├── __init__.py
153
+ │ └── handlers.py
154
+ ```
155
+
156
+ ## Best Practices
157
+
158
+ 1. **Use Common Utilities**: Leverage the common module for consistency
159
+ 2. **Follow Semantic Conventions**: Use attributes from `agentops.semconv`
160
+ 3. **Handle Errors Gracefully**: Wrap operations in try-except blocks
161
+ 4. **Support Async**: Provide both sync and async method wrapping
162
+ 5. **Document Attributes**: Comment on what attributes are captured
163
+ 6. **Test Thoroughly**: Write unit tests for your instrumentor
164
+
165
+ ## Examples
166
+
167
+ See the `examples/` directory for usage examples of each instrumentor.