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
agentops/__init__.py CHANGED
File without changes
@@ -15,16 +15,15 @@ from agentops.helpers.version import get_agentops_version
15
15
  class TokenFetcher(Protocol):
16
16
  """Protocol for token fetching functions"""
17
17
 
18
- def __call__(self, api_key: str) -> str:
19
- ...
18
+ def __call__(self, api_key: str) -> str: ...
20
19
 
21
20
 
22
21
  class BaseApiClient:
23
22
  """
24
- Base class for API communication with connection pooling.
23
+ Base class for API communication with async HTTP methods.
25
24
 
26
25
  This class provides the core HTTP functionality without authentication.
27
- It should be used for APIs that don't require authentication.
26
+ All HTTP methods are asynchronous.
28
27
  """
29
28
 
30
29
  def __init__(self, endpoint: str):
@@ -72,16 +71,16 @@ class BaseApiClient:
72
71
  """
73
72
  return f"{self.endpoint}{path}"
74
73
 
75
- def request(
74
+ async def async_request(
76
75
  self,
77
76
  method: str,
78
77
  path: str,
79
78
  data: Optional[Dict[str, Any]] = None,
80
79
  headers: Optional[Dict[str, str]] = None,
81
80
  timeout: int = 30,
82
- ) -> requests.Response:
81
+ ) -> Optional[Dict[str, Any]]:
83
82
  """
84
- Make a generic HTTP request
83
+ Make a generic async HTTP request
85
84
 
86
85
  Args:
87
86
  method: HTTP method (e.g., 'get', 'post', 'put', 'delete')
@@ -91,7 +90,7 @@ class BaseApiClient:
91
90
  timeout: Request timeout in seconds
92
91
 
93
92
  Returns:
94
- Response from the API
93
+ JSON response as dictionary, or None if request failed
95
94
 
96
95
  Raises:
97
96
  Exception: If the request fails
@@ -99,17 +98,16 @@ class BaseApiClient:
99
98
  url = self._get_full_url(path)
100
99
 
101
100
  try:
102
- response = self.http_client.request(method=method, url=url, data=data, headers=headers, timeout=timeout)
103
-
104
- self.last_response = response
105
- return response
106
- except requests.RequestException as e:
107
- self.last_response = None
101
+ response_data = await self.http_client.async_request(
102
+ method=method, url=url, data=data, headers=headers, timeout=timeout
103
+ )
104
+ return response_data
105
+ except Exception as e:
108
106
  raise Exception(f"{method.upper()} request failed: {str(e)}") from e
109
107
 
110
- def post(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> requests.Response:
108
+ async def post(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
111
109
  """
112
- Make POST request
110
+ Make async POST request
113
111
 
114
112
  Args:
115
113
  path: API endpoint path
@@ -117,26 +115,26 @@ class BaseApiClient:
117
115
  headers: Request headers
118
116
 
119
117
  Returns:
120
- Response from the API
118
+ JSON response as dictionary, or None if request failed
121
119
  """
122
- return self.request("post", path, data=data, headers=headers)
120
+ return await self.async_request("post", path, data=data, headers=headers)
123
121
 
124
- def get(self, path: str, headers: Dict[str, str]) -> requests.Response:
122
+ async def get(self, path: str, headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
125
123
  """
126
- Make GET request
124
+ Make async GET request
127
125
 
128
126
  Args:
129
127
  path: API endpoint path
130
128
  headers: Request headers
131
129
 
132
130
  Returns:
133
- Response from the API
131
+ JSON response as dictionary, or None if request failed
134
132
  """
135
- return self.request("get", path, headers=headers)
133
+ return await self.async_request("get", path, headers=headers)
136
134
 
137
- def put(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> requests.Response:
135
+ async def put(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
138
136
  """
139
- Make PUT request
137
+ Make async PUT request
140
138
 
141
139
  Args:
142
140
  path: API endpoint path
@@ -144,19 +142,19 @@ class BaseApiClient:
144
142
  headers: Request headers
145
143
 
146
144
  Returns:
147
- Response from the API
145
+ JSON response as dictionary, or None if request failed
148
146
  """
149
- return self.request("put", path, data=data, headers=headers)
147
+ return await self.async_request("put", path, data=data, headers=headers)
150
148
 
151
- def delete(self, path: str, headers: Dict[str, str]) -> requests.Response:
149
+ async def delete(self, path: str, headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
152
150
  """
153
- Make DELETE request
151
+ Make async DELETE request
154
152
 
155
153
  Args:
156
154
  path: API endpoint path
157
155
  headers: Request headers
158
156
 
159
157
  Returns:
160
- Response from the API
158
+ JSON response as dictionary, or None if request failed
161
159
  """
162
- return self.request("delete", path, headers=headers)
160
+ return await self.async_request("delete", path, headers=headers)
@@ -6,7 +6,7 @@ This module provides the client for the V3 version of the AgentOps API.
6
6
 
7
7
  from agentops.client.api.base import BaseApiClient
8
8
  from agentops.client.api.types import AuthTokenResponse
9
- from agentops.exceptions import ApiServerException
9
+ from agentops.client.http.http_client import HttpClient
10
10
  from agentops.logging import logger
11
11
  from termcolor import colored
12
12
 
@@ -24,32 +24,36 @@ class V3Client(BaseApiClient):
24
24
  # Set up with V3-specific auth endpoint
25
25
  super().__init__(endpoint)
26
26
 
27
- def fetch_auth_token(self, api_key: str) -> AuthTokenResponse:
28
- path = "/v3/auth/token"
29
- data = {"api_key": api_key}
30
- headers = self.prepare_headers()
31
-
32
- r = self.post(path, data, headers)
27
+ async def fetch_auth_token(self, api_key: str) -> AuthTokenResponse:
28
+ """
29
+ Asynchronously fetch authentication token.
33
30
 
34
- if r.status_code != 200:
35
- error_msg = f"Authentication failed: {r.status_code}"
36
- try:
37
- error_data = r.json()
38
- if "error" in error_data:
39
- error_msg = f"{error_data['error']}"
40
- except Exception:
41
- pass
42
- logger.error(f"{error_msg} - Perhaps an invalid API key?")
43
- raise ApiServerException(error_msg)
31
+ Args:
32
+ api_key: The API key to authenticate with
44
33
 
34
+ Returns:
35
+ AuthTokenResponse containing token and project information, or None if failed
36
+ """
45
37
  try:
46
- jr = r.json()
47
- token = jr.get("token")
38
+ path = "/v3/auth/token"
39
+ data = {"api_key": api_key}
40
+ headers = self.prepare_headers()
41
+
42
+ # Build full URL
43
+ url = self._get_full_url(path)
44
+
45
+ # Make async request
46
+ response_data = await HttpClient.async_request(
47
+ method="POST", url=url, data=data, headers=headers, timeout=30
48
+ )
49
+
50
+ token = response_data.get("token")
48
51
  if not token:
49
- raise ApiServerException("No token in authentication response")
52
+ logger.warning("Authentication failed: Perhaps an invalid API key?")
53
+ return None
50
54
 
51
55
  # Check project premium status
52
- if jr.get("project_prem_status") != "pro":
56
+ if response_data.get("project_prem_status") != "pro":
53
57
  logger.info(
54
58
  colored(
55
59
  "\x1b[34mYou're on the agentops free plan 🤔\x1b[0m",
@@ -57,9 +61,9 @@ class V3Client(BaseApiClient):
57
61
  )
58
62
  )
59
63
 
60
- return jr
61
- except Exception as e:
62
- logger.error(f"Failed to process authentication response: {str(e)}")
63
- raise ApiServerException(f"Failed to process authentication response: {str(e)}")
64
+ return response_data
65
+
66
+ except Exception:
67
+ return None
64
68
 
65
69
  # Add V3-specific API methods here
@@ -4,18 +4,22 @@ V4 API client for the AgentOps API.
4
4
  This module provides the client for the V4 version of the AgentOps API.
5
5
  """
6
6
 
7
- from typing import Optional, Union, Dict
7
+ from typing import Optional, Union, Dict, Any
8
8
 
9
+ import requests
9
10
  from agentops.client.api.base import BaseApiClient
11
+ from agentops.client.http.http_client import HttpClient
10
12
  from agentops.exceptions import ApiServerException
11
- from agentops.client.api.types import UploadedObjectResponse
12
13
  from agentops.helpers.version import get_agentops_version
13
14
 
14
15
 
15
16
  class V4Client(BaseApiClient):
16
17
  """Client for the AgentOps V4 API"""
17
18
 
18
- auth_token: str
19
+ def __init__(self, endpoint: str):
20
+ """Initialize the V4 API client."""
21
+ super().__init__(endpoint)
22
+ self.auth_token: Optional[str] = None
19
23
 
20
24
  def set_auth_token(self, token: str):
21
25
  """
@@ -36,69 +40,106 @@ class V4Client(BaseApiClient):
36
40
  Headers dictionary with standard headers and any custom headers
37
41
  """
38
42
  headers = {
39
- "Authorization": f"Bearer {self.auth_token}",
40
43
  "User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
41
44
  }
45
+
46
+ # Only add Authorization header if we have a token
47
+ if self.auth_token:
48
+ headers["Authorization"] = f"Bearer {self.auth_token}"
49
+
42
50
  if custom_headers:
43
51
  headers.update(custom_headers)
44
52
  return headers
45
53
 
46
- def upload_object(self, body: Union[str, bytes]) -> UploadedObjectResponse:
54
+ def post(self, path: str, body: Union[str, bytes], headers: Optional[Dict[str, str]] = None) -> requests.Response:
47
55
  """
48
- Upload an object to the API and return the response.
56
+ Make a POST request to the V4 API.
49
57
 
50
58
  Args:
51
- body: The object to upload, either as a string or bytes.
59
+ path: The API path to POST to
60
+ body: The request body (string or bytes)
61
+ headers: Optional headers to include
62
+
52
63
  Returns:
53
- UploadedObjectResponse: The response from the API after upload.
64
+ The response object
54
65
  """
55
- if isinstance(body, bytes):
56
- body = body.decode("utf-8")
66
+ url = self._get_full_url(path)
67
+ request_headers = headers or self.prepare_headers()
57
68
 
58
- response = self.post("/v4/objects/upload/", body, self.prepare_headers())
69
+ return HttpClient.get_session().post(url, json={"body": body}, headers=request_headers, timeout=30)
59
70
 
60
- if response.status_code != 200:
61
- error_msg = f"Upload failed: {response.status_code}"
62
- try:
63
- error_data = response.json()
64
- if "error" in error_data:
65
- error_msg = error_data["error"]
66
- except Exception:
67
- pass
68
- raise ApiServerException(error_msg)
71
+ def upload_object(self, body: Union[str, bytes]) -> Dict[str, Any]:
72
+ """
73
+ Upload an object to the V4 API.
74
+
75
+ Args:
76
+ body: The object body to upload
77
+
78
+ Returns:
79
+ Dictionary containing upload response data
69
80
 
81
+ Raises:
82
+ ApiServerException: If the upload fails
83
+ """
70
84
  try:
71
- response_data = response.json()
72
- return UploadedObjectResponse(**response_data)
73
- except Exception as e:
74
- raise ApiServerException(f"Failed to process upload response: {str(e)}")
85
+ # Convert bytes to string for consistency with test expectations
86
+ if isinstance(body, bytes):
87
+ body = body.decode("utf-8")
88
+
89
+ response = self.post("/v4/objects/upload/", body, self.prepare_headers())
90
+
91
+ if response.status_code != 200:
92
+ error_msg = f"Upload failed: {response.status_code}"
93
+ try:
94
+ error_data = response.json()
95
+ if "error" in error_data:
96
+ error_msg = error_data["error"]
97
+ except:
98
+ pass
99
+ raise ApiServerException(error_msg)
100
+
101
+ try:
102
+ return response.json()
103
+ except Exception as e:
104
+ raise ApiServerException(f"Failed to process upload response: {str(e)}")
105
+ except requests.exceptions.RequestException as e:
106
+ raise ApiServerException(f"Failed to upload object: {e}")
75
107
 
76
- def upload_logfile(self, body: Union[str, bytes], trace_id: int) -> UploadedObjectResponse:
108
+ def upload_logfile(self, body: Union[str, bytes], trace_id: str) -> Dict[str, Any]:
77
109
  """
78
- Upload an log file to the API and return the response.
110
+ Upload a logfile to the V4 API.
79
111
 
80
112
  Args:
81
- body: The log file to upload, either as a string or bytes.
113
+ body: The logfile content to upload
114
+ trace_id: The trace ID associated with the logfile
115
+
82
116
  Returns:
83
- UploadedObjectResponse: The response from the API after upload.
84
- """
85
- if isinstance(body, bytes):
86
- body = body.decode("utf-8")
117
+ Dictionary containing upload response data
87
118
 
88
- response = self.post("/v4/logs/upload/", body, {**self.prepare_headers(), "Trace-Id": str(trace_id)})
119
+ Raises:
120
+ ApiServerException: If the upload fails
121
+ """
122
+ try:
123
+ # Convert bytes to string for consistency with test expectations
124
+ if isinstance(body, bytes):
125
+ body = body.decode("utf-8")
126
+
127
+ headers = {**self.prepare_headers(), "Trace-Id": str(trace_id)}
128
+ response = self.post("/v4/logs/upload/", body, headers)
129
+
130
+ if response.status_code != 200:
131
+ error_msg = f"Upload failed: {response.status_code}"
132
+ try:
133
+ error_data = response.json()
134
+ if "error" in error_data:
135
+ error_msg = error_data["error"]
136
+ except:
137
+ pass
138
+ raise ApiServerException(error_msg)
89
139
 
90
- if response.status_code != 200:
91
- error_msg = f"Upload failed: {response.status_code}"
92
140
  try:
93
- error_data = response.json()
94
- if "error" in error_data:
95
- error_msg = error_data["error"]
96
- except Exception:
97
- pass
98
- raise ApiServerException(error_msg)
99
-
100
- try:
101
- response_data = response.json()
102
- return UploadedObjectResponse(**response_data)
103
- except Exception as e:
104
- raise ApiServerException(f"Failed to process upload response: {str(e)}")
141
+ return response.json()
142
+ except Exception as e:
143
+ raise ApiServerException(f"Failed to process upload response: {str(e)}")
144
+ except requests.exceptions.RequestException as e:
145
+ raise ApiServerException(f"Failed to upload logfile: {e}")
agentops/client/client.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import atexit
2
+ import asyncio
3
+ import threading
2
4
  from typing import Optional, Any
3
5
 
4
6
  from agentops.client.api import ApiClient
5
7
  from agentops.config import Config
6
- from agentops.exceptions import NoApiKeyException
7
8
  from agentops.instrumentation import instrument_all
8
9
  from agentops.logging import logger
9
10
  from agentops.logging.config import configure_logging, intercept_opentelemetry_logging
@@ -40,13 +41,17 @@ class Client:
40
41
  config: Config
41
42
  _initialized: bool
42
43
  _init_trace_context: Optional[TraceContext] = None # Stores the context of the auto-started trace
43
- _legacy_session_for_init_trace: Optional[
44
- Session
45
- ] = None # Stores the legacy Session wrapper for the auto-started trace
44
+ _legacy_session_for_init_trace: Optional[Session] = (
45
+ None # Stores the legacy Session wrapper for the auto-started trace
46
+ )
46
47
 
47
48
  __instance = None # Class variable for singleton pattern
48
49
 
49
50
  api: ApiClient
51
+ _auth_token: Optional[str] = None
52
+ _project_id: Optional[str] = None
53
+ _auth_lock = threading.Lock()
54
+ _auth_task: Optional[asyncio.Task] = None
50
55
 
51
56
  def __new__(cls, *args: Any, **kwargs: Any) -> "Client":
52
57
  if cls.__instance is None:
@@ -54,6 +59,10 @@ class Client:
54
59
  # Initialize instance variables that should only be set once per instance
55
60
  cls.__instance._init_trace_context = None
56
61
  cls.__instance._legacy_session_for_init_trace = None
62
+ cls.__instance._auth_token = None
63
+ cls.__instance._project_id = None
64
+ cls.__instance._auth_lock = threading.Lock()
65
+ cls.__instance._auth_task = None
57
66
  return cls.__instance
58
67
 
59
68
  def __init__(self):
@@ -68,6 +77,73 @@ class Client:
68
77
  # self._init_trace_context = None # Already done in __new__
69
78
  # self._legacy_session_for_init_trace = None # Already done in __new__
70
79
 
80
+ def get_current_jwt(self) -> Optional[str]:
81
+ """Get the current JWT token."""
82
+ with self._auth_lock:
83
+ return self._auth_token
84
+
85
+ def _set_auth_data(self, token: str, project_id: str):
86
+ """Set authentication data thread-safely."""
87
+ with self._auth_lock:
88
+ self._auth_token = token
89
+ self._project_id = project_id
90
+
91
+ # Update the HTTP client's project ID
92
+ from agentops.client.http.http_client import HttpClient
93
+
94
+ HttpClient.set_project_id(project_id)
95
+
96
+ async def _fetch_auth_async(self, api_key: str) -> Optional[dict]:
97
+ """Asynchronously fetch authentication token."""
98
+ try:
99
+ response = await self.api.v3.fetch_auth_token(api_key)
100
+ if response:
101
+ self._set_auth_data(response["token"], response["project_id"])
102
+
103
+ # Update V4 client with token
104
+ self.api.v4.set_auth_token(response["token"])
105
+
106
+ # Update tracer config with real project ID
107
+ tracing_config = {"project_id": response["project_id"]}
108
+ tracer.update_config(tracing_config)
109
+
110
+ logger.debug("Successfully fetched authentication token asynchronously")
111
+ return response
112
+ else:
113
+ logger.debug("Authentication failed - will continue without authentication")
114
+ return None
115
+ except Exception:
116
+ return None
117
+
118
+ def _start_auth_task(self, api_key: str):
119
+ """Start the async authentication task."""
120
+ if self._auth_task and not self._auth_task.done():
121
+ return # Task already running
122
+
123
+ try:
124
+ loop = asyncio.get_event_loop()
125
+ if loop.is_running():
126
+ # Use existing event loop
127
+ self._auth_task = loop.create_task(self._fetch_auth_async(api_key))
128
+ else:
129
+ # Create new event loop in background thread
130
+ def run_async_auth():
131
+ asyncio.run(self._fetch_auth_async(api_key))
132
+
133
+ import threading
134
+
135
+ auth_thread = threading.Thread(target=run_async_auth, daemon=True)
136
+ auth_thread.start()
137
+ except RuntimeError:
138
+ # Create new event loop in background thread
139
+ def run_async_auth():
140
+ asyncio.run(self._fetch_auth_async(api_key))
141
+
142
+ import threading
143
+
144
+ auth_thread = threading.Thread(target=run_async_auth, daemon=True)
145
+ auth_thread.start()
146
+
71
147
  def init(self, **kwargs: Any) -> None: # Return type updated to None
72
148
  # Recreate the Config object to parse environment variables at the time of initialization
73
149
  # This allows re-init with new env vars if needed, though true singletons usually init once.
@@ -94,35 +170,36 @@ class Client:
94
170
  return None # If not auto-starting, and already initialized, return None
95
171
 
96
172
  if not self.config.api_key:
97
- raise NoApiKeyException
173
+ logger.warning(
174
+ "No API key provided. AgentOps will initialize but authentication will fail. "
175
+ "Set AGENTOPS_API_KEY environment variable or pass api_key parameter."
176
+ )
177
+ # Continue without API key - spans will be created but exports will fail gracefully
98
178
 
99
179
  configure_logging(self.config)
100
180
  intercept_opentelemetry_logging()
101
181
 
102
182
  self.api = ApiClient(self.config.endpoint)
103
183
 
104
- try:
105
- response = self.api.v3.fetch_auth_token(self.config.api_key)
106
- if response is None:
107
- # If auth fails, we cannot proceed with tracer initialization that depends on project_id
108
- logger.error("Failed to fetch auth token. AgentOps SDK will not be initialized.")
109
- return None # Explicitly return None if auth fails
110
- except Exception as e:
111
- # Re-raise authentication exceptions so they can be caught by tests and calling code
112
- logger.error(f"Authentication failed: {e}")
113
- raise
114
-
115
- self.api.v4.set_auth_token(response["token"])
116
-
184
+ # Initialize tracer with JWT provider for dynamic updates
117
185
  tracing_config = self.config.dict()
118
- tracing_config["project_id"] = response["project_id"]
186
+ tracing_config["project_id"] = "temporary" # Will be updated when auth completes
119
187
 
120
- tracer.initialize_from_config(tracing_config, jwt=response["token"])
188
+ # Create JWT provider function for dynamic updates
189
+ def jwt_provider():
190
+ return self.get_current_jwt()
191
+
192
+ # Initialize tracer with JWT provider
193
+ tracer.initialize_from_config(tracing_config, jwt_provider=jwt_provider)
121
194
 
122
195
  if self.config.instrument_llm_calls:
123
196
  instrument_all()
124
197
 
125
- # self._initialized = True # Set initialized to True here - MOVED to after trace start attempt
198
+ # Start authentication task only if we have an API key
199
+ if self.config.api_key:
200
+ self._start_auth_task(self.config.api_key)
201
+ else:
202
+ logger.debug("No API key available - skipping authentication task")
126
203
 
127
204
  global _atexit_registered
128
205
  if not _atexit_registered:
@@ -201,11 +278,3 @@ class Client:
201
278
  # Deprecate and remove the old global _active_session from this module.
202
279
  # Consumers should use agentops.start_trace() or rely on the auto-init trace.
203
280
  # For a transition, the auto-init trace's legacy wrapper is set to legacy module's globals.
204
-
205
-
206
- # Ensure the global _active_session (if needed for some very old compatibility) points to the client's legacy session for init trace.
207
- # This specific global _active_session in client.py is problematic and should be phased out.
208
- # For now, _client_legacy_session_for_init_trace is the primary global for the auto-init trace's legacy Session.
209
-
210
- # Remove the old global _active_session defined at the top of this file if it's no longer the primary mechanism.
211
- # The new globals _client_init_trace_context and _client_legacy_session_for_init_trace handle the auto-init trace.
@@ -0,0 +1,87 @@
1
+ # AgentOps HTTP Client Architecture
2
+
3
+ This directory contains the HTTP client architecture for the AgentOps SDK. The architecture follows a clean separation of concerns design principle.
4
+
5
+ ## Components
6
+
7
+ ### HttpClient
8
+
9
+ The `HttpClient` class provides low-level HTTP functionality:
10
+ - Connection pooling
11
+ - Retry logic
12
+ - Basic HTTP methods (GET, POST, PUT, DELETE)
13
+
14
+ ### AuthManager
15
+
16
+ The `AuthManager` class handles authentication concerns:
17
+ - Token acquisition and storage
18
+ - Token refresh logic
19
+ - Authentication header preparation
20
+ - Thread-safe token operations
21
+
22
+ ### HTTP Adapters
23
+
24
+ #### BaseHTTPAdapter
25
+ - Enhanced connection pooling and retry logic
26
+ - Used by the `HttpClient` for basic HTTP operations
27
+
28
+ #### AuthenticatedHttpAdapter
29
+ - Extends `BaseHTTPAdapter` with authentication capabilities
30
+ - Automatically adds authentication headers to requests
31
+ - Handles token refresh when authentication fails
32
+ - Can be mounted to any requests.Session
33
+
34
+ ## Design Principles
35
+
36
+ 1. **Separation of Concerns**
37
+ - HTTP concerns are isolated from authentication concerns
38
+ - Each component has a single responsibility
39
+
40
+ 2. **Composition over Inheritance**
41
+ - Components use composition rather than inheritance
42
+ - `ApiClient` composes `HttpClient` and `AuthManager`
43
+
44
+ 3. **Clear Interfaces**
45
+ - Each component has a well-defined interface
46
+ - Implementation details are hidden
47
+
48
+ 4. **Dependency Flow**
49
+ - Dependencies flow in one direction
50
+ - Lower-level components (HTTP, Auth) don't depend on higher-level components
51
+
52
+ ## Usage
53
+
54
+ ### Basic API Client Usage
55
+
56
+ The HTTP client architecture is used by the `ApiClient` class, which provides a high-level interface for making API calls. Specific API versions (like `V3Client`) extend the `ApiClient` to provide version-specific functionality.
57
+
58
+ ```python
59
+ # Example usage
60
+ from agentops.client.v3_client import V3Client
61
+
62
+ client = V3Client(endpoint="https://api.agentops.ai")
63
+ response = client.authenticated_request(
64
+ method="get",
65
+ path="/v3/some/endpoint",
66
+ api_key="your-api-key"
67
+ )
68
+ ```
69
+
70
+ ### Using with External Libraries
71
+
72
+ The architecture also supports integration with external libraries that need authenticated HTTP sessions:
73
+
74
+ ```python
75
+ # Example with OpenTelemetry exporter
76
+ from agentops.client.v3_client import V3Client
77
+ from agentops.client.exporters import AuthenticatedOTLPExporter
78
+
79
+ client = V3Client(endpoint="https://api.agentops.ai")
80
+ session = client.create_authenticated_session(api_key="your-api-key")
81
+
82
+ exporter = AuthenticatedOTLPExporter(
83
+ endpoint="https://api.agentops.ai/v3/traces",
84
+ api_client=client,
85
+ api_key="your-api-key"
86
+ )
87
+ ```