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.
- agentops/__init__.py +0 -0
- agentops/client/api/base.py +28 -30
- agentops/client/api/versions/v3.py +29 -25
- agentops/client/api/versions/v4.py +87 -46
- agentops/client/client.py +98 -29
- agentops/client/http/README.md +87 -0
- agentops/client/http/http_client.py +126 -172
- agentops/config.py +8 -2
- agentops/instrumentation/OpenTelemetry.md +133 -0
- agentops/instrumentation/README.md +167 -0
- agentops/instrumentation/__init__.py +13 -1
- agentops/instrumentation/agentic/ag2/__init__.py +18 -0
- agentops/instrumentation/agentic/ag2/instrumentor.py +922 -0
- agentops/instrumentation/agentic/agno/__init__.py +19 -0
- agentops/instrumentation/agentic/agno/attributes/__init__.py +20 -0
- agentops/instrumentation/agentic/agno/attributes/agent.py +250 -0
- agentops/instrumentation/agentic/agno/attributes/metrics.py +214 -0
- agentops/instrumentation/agentic/agno/attributes/storage.py +158 -0
- agentops/instrumentation/agentic/agno/attributes/team.py +195 -0
- agentops/instrumentation/agentic/agno/attributes/tool.py +210 -0
- agentops/instrumentation/agentic/agno/attributes/workflow.py +254 -0
- agentops/instrumentation/agentic/agno/instrumentor.py +1313 -0
- agentops/instrumentation/agentic/crewai/LICENSE +201 -0
- agentops/instrumentation/agentic/crewai/NOTICE.md +10 -0
- agentops/instrumentation/agentic/crewai/__init__.py +6 -0
- agentops/instrumentation/agentic/crewai/crewai_span_attributes.py +335 -0
- agentops/instrumentation/agentic/crewai/instrumentation.py +535 -0
- agentops/instrumentation/agentic/crewai/version.py +1 -0
- agentops/instrumentation/agentic/google_adk/__init__.py +19 -0
- agentops/instrumentation/agentic/google_adk/instrumentor.py +68 -0
- agentops/instrumentation/agentic/google_adk/patch.py +767 -0
- agentops/instrumentation/agentic/haystack/__init__.py +1 -0
- agentops/instrumentation/agentic/haystack/instrumentor.py +186 -0
- agentops/instrumentation/agentic/langgraph/__init__.py +3 -0
- agentops/instrumentation/agentic/langgraph/attributes.py +54 -0
- agentops/instrumentation/agentic/langgraph/instrumentation.py +598 -0
- agentops/instrumentation/agentic/langgraph/version.py +1 -0
- agentops/instrumentation/agentic/openai_agents/README.md +156 -0
- agentops/instrumentation/agentic/openai_agents/SPANS.md +145 -0
- agentops/instrumentation/agentic/openai_agents/TRACING_API.md +144 -0
- agentops/instrumentation/agentic/openai_agents/__init__.py +30 -0
- agentops/instrumentation/agentic/openai_agents/attributes/common.py +549 -0
- agentops/instrumentation/agentic/openai_agents/attributes/completion.py +172 -0
- agentops/instrumentation/agentic/openai_agents/attributes/model.py +58 -0
- agentops/instrumentation/agentic/openai_agents/attributes/tokens.py +275 -0
- agentops/instrumentation/agentic/openai_agents/exporter.py +469 -0
- agentops/instrumentation/agentic/openai_agents/instrumentor.py +107 -0
- agentops/instrumentation/agentic/openai_agents/processor.py +58 -0
- agentops/instrumentation/agentic/smolagents/README.md +88 -0
- agentops/instrumentation/agentic/smolagents/__init__.py +12 -0
- agentops/instrumentation/agentic/smolagents/attributes/agent.py +354 -0
- agentops/instrumentation/agentic/smolagents/attributes/model.py +205 -0
- agentops/instrumentation/agentic/smolagents/instrumentor.py +286 -0
- agentops/instrumentation/agentic/smolagents/stream_wrapper.py +258 -0
- agentops/instrumentation/agentic/xpander/__init__.py +15 -0
- agentops/instrumentation/agentic/xpander/context.py +112 -0
- agentops/instrumentation/agentic/xpander/instrumentor.py +877 -0
- agentops/instrumentation/agentic/xpander/trace_probe.py +86 -0
- agentops/instrumentation/agentic/xpander/version.py +3 -0
- agentops/instrumentation/common/README.md +65 -0
- agentops/instrumentation/common/attributes.py +1 -2
- agentops/instrumentation/providers/anthropic/__init__.py +24 -0
- agentops/instrumentation/providers/anthropic/attributes/__init__.py +23 -0
- agentops/instrumentation/providers/anthropic/attributes/common.py +64 -0
- agentops/instrumentation/providers/anthropic/attributes/message.py +541 -0
- agentops/instrumentation/providers/anthropic/attributes/tools.py +231 -0
- agentops/instrumentation/providers/anthropic/event_handler_wrapper.py +90 -0
- agentops/instrumentation/providers/anthropic/instrumentor.py +146 -0
- agentops/instrumentation/providers/anthropic/stream_wrapper.py +436 -0
- agentops/instrumentation/providers/google_genai/README.md +33 -0
- agentops/instrumentation/providers/google_genai/__init__.py +24 -0
- agentops/instrumentation/providers/google_genai/attributes/__init__.py +25 -0
- agentops/instrumentation/providers/google_genai/attributes/chat.py +125 -0
- agentops/instrumentation/providers/google_genai/attributes/common.py +88 -0
- agentops/instrumentation/providers/google_genai/attributes/model.py +284 -0
- agentops/instrumentation/providers/google_genai/instrumentor.py +170 -0
- agentops/instrumentation/providers/google_genai/stream_wrapper.py +238 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/__init__.py +28 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/__init__.py +27 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/attributes.py +277 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/common.py +104 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/instrumentor.py +162 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/stream_wrapper.py +302 -0
- agentops/instrumentation/providers/mem0/__init__.py +45 -0
- agentops/instrumentation/providers/mem0/common.py +377 -0
- agentops/instrumentation/providers/mem0/instrumentor.py +270 -0
- agentops/instrumentation/providers/mem0/memory.py +430 -0
- agentops/instrumentation/providers/openai/__init__.py +21 -0
- agentops/instrumentation/providers/openai/attributes/__init__.py +7 -0
- agentops/instrumentation/providers/openai/attributes/common.py +55 -0
- agentops/instrumentation/providers/openai/attributes/response.py +607 -0
- agentops/instrumentation/providers/openai/config.py +36 -0
- agentops/instrumentation/providers/openai/instrumentor.py +312 -0
- agentops/instrumentation/providers/openai/stream_wrapper.py +941 -0
- agentops/instrumentation/providers/openai/utils.py +44 -0
- agentops/instrumentation/providers/openai/v0.py +176 -0
- agentops/instrumentation/providers/openai/v0_wrappers.py +483 -0
- agentops/instrumentation/providers/openai/wrappers/__init__.py +30 -0
- agentops/instrumentation/providers/openai/wrappers/assistant.py +277 -0
- agentops/instrumentation/providers/openai/wrappers/chat.py +259 -0
- agentops/instrumentation/providers/openai/wrappers/completion.py +109 -0
- agentops/instrumentation/providers/openai/wrappers/embeddings.py +94 -0
- agentops/instrumentation/providers/openai/wrappers/image_gen.py +75 -0
- agentops/instrumentation/providers/openai/wrappers/responses.py +191 -0
- agentops/instrumentation/providers/openai/wrappers/shared.py +81 -0
- agentops/instrumentation/utilities/concurrent_futures/__init__.py +10 -0
- agentops/instrumentation/utilities/concurrent_futures/instrumentation.py +206 -0
- agentops/integration/callbacks/dspy/__init__.py +11 -0
- agentops/integration/callbacks/dspy/callback.py +471 -0
- agentops/integration/callbacks/langchain/README.md +59 -0
- agentops/integration/callbacks/langchain/__init__.py +15 -0
- agentops/integration/callbacks/langchain/callback.py +791 -0
- agentops/integration/callbacks/langchain/utils.py +54 -0
- agentops/legacy/crewai.md +121 -0
- agentops/logging/instrument_logging.py +4 -0
- agentops/sdk/README.md +220 -0
- agentops/sdk/core.py +75 -32
- agentops/sdk/descriptors/classproperty.py +28 -0
- agentops/sdk/exporters.py +152 -33
- agentops/semconv/README.md +125 -0
- agentops/semconv/span_kinds.py +0 -2
- agentops/validation.py +102 -63
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/METADATA +30 -40
- mseep_agentops-0.4.22.dist-info/RECORD +178 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/WHEEL +1 -2
- mseep_agentops-0.4.18.dist-info/RECORD +0 -94
- mseep_agentops-0.4.18.dist-info/top_level.txt +0 -2
- tests/conftest.py +0 -10
- tests/unit/client/__init__.py +0 -1
- tests/unit/client/test_http_adapter.py +0 -221
- tests/unit/client/test_http_client.py +0 -206
- tests/unit/conftest.py +0 -54
- tests/unit/sdk/__init__.py +0 -1
- tests/unit/sdk/instrumentation_tester.py +0 -207
- tests/unit/sdk/test_attributes.py +0 -392
- tests/unit/sdk/test_concurrent_instrumentation.py +0 -468
- tests/unit/sdk/test_decorators.py +0 -763
- tests/unit/sdk/test_exporters.py +0 -241
- tests/unit/sdk/test_factory.py +0 -1188
- tests/unit/sdk/test_internal_span_processor.py +0 -397
- tests/unit/sdk/test_resource_attributes.py +0 -35
- tests/unit/test_config.py +0 -82
- tests/unit/test_context_manager.py +0 -777
- tests/unit/test_events.py +0 -27
- tests/unit/test_host_env.py +0 -54
- tests/unit/test_init_py.py +0 -501
- tests/unit/test_serialization.py +0 -433
- tests/unit/test_session.py +0 -676
- tests/unit/test_user_agent.py +0 -34
- tests/unit/test_validation.py +0 -405
- {tests → agentops/instrumentation/agentic/openai_agents/attributes}/__init__.py +0 -0
- /tests/unit/__init__.py → /agentops/instrumentation/providers/openai/attributes/tools.py +0 -0
- {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
|
agentops/client/api/base.py
CHANGED
@@ -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
|
23
|
+
Base class for API communication with async HTTP methods.
|
25
24
|
|
26
25
|
This class provides the core HTTP functionality without authentication.
|
27
|
-
|
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
|
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
|
-
) ->
|
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
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
return
|
106
|
-
except
|
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]) ->
|
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
|
-
|
118
|
+
JSON response as dictionary, or None if request failed
|
121
119
|
"""
|
122
|
-
return self.
|
120
|
+
return await self.async_request("post", path, data=data, headers=headers)
|
123
121
|
|
124
|
-
def get(self, path: str, headers: Dict[str, str]) ->
|
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
|
-
|
131
|
+
JSON response as dictionary, or None if request failed
|
134
132
|
"""
|
135
|
-
return self.
|
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]) ->
|
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
|
-
|
145
|
+
JSON response as dictionary, or None if request failed
|
148
146
|
"""
|
149
|
-
return self.
|
147
|
+
return await self.async_request("put", path, data=data, headers=headers)
|
150
148
|
|
151
|
-
def delete(self, path: str, headers: Dict[str, str]) ->
|
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
|
-
|
158
|
+
JSON response as dictionary, or None if request failed
|
161
159
|
"""
|
162
|
-
return self.
|
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.
|
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
|
-
|
29
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
52
|
+
logger.warning("Authentication failed: Perhaps an invalid API key?")
|
53
|
+
return None
|
50
54
|
|
51
55
|
# Check project premium status
|
52
|
-
if
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
54
|
+
def post(self, path: str, body: Union[str, bytes], headers: Optional[Dict[str, str]] = None) -> requests.Response:
|
47
55
|
"""
|
48
|
-
|
56
|
+
Make a POST request to the V4 API.
|
49
57
|
|
50
58
|
Args:
|
51
|
-
|
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
|
-
|
64
|
+
The response object
|
54
65
|
"""
|
55
|
-
|
56
|
-
|
66
|
+
url = self._get_full_url(path)
|
67
|
+
request_headers = headers or self.prepare_headers()
|
57
68
|
|
58
|
-
|
69
|
+
return HttpClient.get_session().post(url, json={"body": body}, headers=request_headers, timeout=30)
|
59
70
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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:
|
108
|
+
def upload_logfile(self, body: Union[str, bytes], trace_id: str) -> Dict[str, Any]:
|
77
109
|
"""
|
78
|
-
Upload
|
110
|
+
Upload a logfile to the V4 API.
|
79
111
|
|
80
112
|
Args:
|
81
|
-
body: The
|
113
|
+
body: The logfile content to upload
|
114
|
+
trace_id: The trace ID associated with the logfile
|
115
|
+
|
82
116
|
Returns:
|
83
|
-
|
84
|
-
"""
|
85
|
-
if isinstance(body, bytes):
|
86
|
-
body = body.decode("utf-8")
|
117
|
+
Dictionary containing upload response data
|
87
118
|
|
88
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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"] =
|
186
|
+
tracing_config["project_id"] = "temporary" # Will be updated when auth completes
|
119
187
|
|
120
|
-
|
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
|
-
#
|
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
|
+
```
|