Skip to content

Commit ac3dadc

Browse files
authored
Enhance async functionality and logging in AgentOps SDK (#1162)
* Enhance async functionality and logging in AgentOps SDK * Enhance LLM span validation and improve V4 API client functionality * Refactor header handling in AuthenticatedOTLPExporter to prevent critical headers from being overridden by user-supplied values. Update tests to verify protection of critical headers and ensure proper JWT token usage.
1 parent db7207f commit ac3dadc

File tree

12 files changed

+745
-469
lines changed

12 files changed

+745
-469
lines changed

agentops/client/api/base.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ def __call__(self, api_key: str) -> str:
2121

2222
class BaseApiClient:
2323
"""
24-
Base class for API communication with connection pooling.
24+
Base class for API communication with async HTTP methods.
2525
2626
This class provides the core HTTP functionality without authentication.
27-
It should be used for APIs that don't require authentication.
27+
All HTTP methods are asynchronous.
2828
"""
2929

3030
def __init__(self, endpoint: str):
@@ -72,16 +72,16 @@ def _get_full_url(self, path: str) -> str:
7272
"""
7373
return f"{self.endpoint}{path}"
7474

75-
def request(
75+
async def async_request(
7676
self,
7777
method: str,
7878
path: str,
7979
data: Optional[Dict[str, Any]] = None,
8080
headers: Optional[Dict[str, str]] = None,
8181
timeout: int = 30,
82-
) -> requests.Response:
82+
) -> Optional[Dict[str, Any]]:
8383
"""
84-
Make a generic HTTP request
84+
Make a generic async HTTP request
8585
8686
Args:
8787
method: HTTP method (e.g., 'get', 'post', 'put', 'delete')
@@ -91,72 +91,71 @@ def request(
9191
timeout: Request timeout in seconds
9292
9393
Returns:
94-
Response from the API
94+
JSON response as dictionary, or None if request failed
9595
9696
Raises:
9797
Exception: If the request fails
9898
"""
9999
url = self._get_full_url(path)
100100

101101
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
102+
response_data = await self.http_client.async_request(
103+
method=method, url=url, data=data, headers=headers, timeout=timeout
104+
)
105+
return response_data
106+
except Exception as e:
108107
raise Exception(f"{method.upper()} request failed: {str(e)}") from e
109108

110-
def post(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> requests.Response:
109+
async def post(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
111110
"""
112-
Make POST request
111+
Make async POST request
113112
114113
Args:
115114
path: API endpoint path
116115
data: Request payload
117116
headers: Request headers
118117
119118
Returns:
120-
Response from the API
119+
JSON response as dictionary, or None if request failed
121120
"""
122-
return self.request("post", path, data=data, headers=headers)
121+
return await self.async_request("post", path, data=data, headers=headers)
123122

124-
def get(self, path: str, headers: Dict[str, str]) -> requests.Response:
123+
async def get(self, path: str, headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
125124
"""
126-
Make GET request
125+
Make async GET request
127126
128127
Args:
129128
path: API endpoint path
130129
headers: Request headers
131130
132131
Returns:
133-
Response from the API
132+
JSON response as dictionary, or None if request failed
134133
"""
135-
return self.request("get", path, headers=headers)
134+
return await self.async_request("get", path, headers=headers)
136135

137-
def put(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> requests.Response:
136+
async def put(self, path: str, data: Dict[str, Any], headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
138137
"""
139-
Make PUT request
138+
Make async PUT request
140139
141140
Args:
142141
path: API endpoint path
143142
data: Request payload
144143
headers: Request headers
145144
146145
Returns:
147-
Response from the API
146+
JSON response as dictionary, or None if request failed
148147
"""
149-
return self.request("put", path, data=data, headers=headers)
148+
return await self.async_request("put", path, data=data, headers=headers)
150149

151-
def delete(self, path: str, headers: Dict[str, str]) -> requests.Response:
150+
async def delete(self, path: str, headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
152151
"""
153-
Make DELETE request
152+
Make async DELETE request
154153
155154
Args:
156155
path: API endpoint path
157156
headers: Request headers
158157
159158
Returns:
160-
Response from the API
159+
JSON response as dictionary, or None if request failed
161160
"""
162-
return self.request("delete", path, headers=headers)
161+
return await self.async_request("delete", path, headers=headers)

agentops/client/api/versions/v3.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from agentops.client.api.base import BaseApiClient
88
from agentops.client.api.types import AuthTokenResponse
9-
from agentops.exceptions import ApiServerException
9+
from agentops.client.http.http_client import HttpClient
1010
from agentops.logging import logger
1111
from termcolor import colored
1212

@@ -24,42 +24,46 @@ def __init__(self, endpoint: str):
2424
# Set up with V3-specific auth endpoint
2525
super().__init__(endpoint)
2626

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.
3330
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
4433
34+
Returns:
35+
AuthTokenResponse containing token and project information, or None if failed
36+
"""
4537
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")
4851
if not token:
49-
raise ApiServerException("No token in authentication response")
52+
logger.warning("Authentication failed: Perhaps an invalid API key?")
53+
return None
5054

5155
# Check project premium status
52-
if jr.get("project_prem_status") != "pro":
56+
if response_data.get("project_prem_status") != "pro":
5357
logger.info(
5458
colored(
5559
"\x1b[34mYou're on the agentops free plan 🤔\x1b[0m",
5660
"blue",
5761
)
5862
)
5963

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
6468

6569
# Add V3-specific API methods here

agentops/client/api/versions/v4.py

Lines changed: 87 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
This module provides the client for the V4 version of the AgentOps API.
55
"""
66

7-
from typing import Optional, Union, Dict
7+
from typing import Optional, Union, Dict, Any
88

9+
import requests
910
from agentops.client.api.base import BaseApiClient
11+
from agentops.client.http.http_client import HttpClient
1012
from agentops.exceptions import ApiServerException
11-
from agentops.client.api.types import UploadedObjectResponse
1213
from agentops.helpers.version import get_agentops_version
1314

1415

1516
class V4Client(BaseApiClient):
1617
"""Client for the AgentOps V4 API"""
1718

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
1923

2024
def set_auth_token(self, token: str):
2125
"""
@@ -36,69 +40,106 @@ def prepare_headers(self, custom_headers: Optional[Dict[str, str]] = None) -> Di
3640
Headers dictionary with standard headers and any custom headers
3741
"""
3842
headers = {
39-
"Authorization": f"Bearer {self.auth_token}",
4043
"User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
4144
}
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+
4250
if custom_headers:
4351
headers.update(custom_headers)
4452
return headers
4553

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:
4755
"""
48-
Upload an object to the API and return the response.
56+
Make a POST request to the V4 API.
4957
5058
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+
5263
Returns:
53-
UploadedObjectResponse: The response from the API after upload.
64+
The response object
5465
"""
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()
5768

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)
5970

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
6980
81+
Raises:
82+
ApiServerException: If the upload fails
83+
"""
7084
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}")
75107

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]:
77109
"""
78-
Upload an log file to the API and return the response.
110+
Upload a logfile to the V4 API.
79111
80112
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+
82116
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
87118
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)
89139

90-
if response.status_code != 200:
91-
error_msg = f"Upload failed: {response.status_code}"
92140
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}")

0 commit comments

Comments
 (0)