mseep-agentops 0.4.18__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 +488 -0
- agentops/client/__init__.py +5 -0
- agentops/client/api/__init__.py +71 -0
- agentops/client/api/base.py +162 -0
- agentops/client/api/types.py +21 -0
- agentops/client/api/versions/__init__.py +10 -0
- agentops/client/api/versions/v3.py +65 -0
- agentops/client/api/versions/v4.py +104 -0
- agentops/client/client.py +211 -0
- agentops/client/http/__init__.py +0 -0
- agentops/client/http/http_adapter.py +116 -0
- agentops/client/http/http_client.py +215 -0
- agentops/config.py +268 -0
- agentops/enums.py +36 -0
- agentops/exceptions.py +38 -0
- agentops/helpers/__init__.py +44 -0
- agentops/helpers/dashboard.py +54 -0
- agentops/helpers/deprecation.py +50 -0
- agentops/helpers/env.py +52 -0
- agentops/helpers/serialization.py +137 -0
- agentops/helpers/system.py +178 -0
- agentops/helpers/time.py +11 -0
- agentops/helpers/version.py +36 -0
- agentops/instrumentation/__init__.py +598 -0
- agentops/instrumentation/common/__init__.py +82 -0
- agentops/instrumentation/common/attributes.py +278 -0
- agentops/instrumentation/common/instrumentor.py +147 -0
- agentops/instrumentation/common/metrics.py +100 -0
- agentops/instrumentation/common/objects.py +26 -0
- agentops/instrumentation/common/span_management.py +176 -0
- agentops/instrumentation/common/streaming.py +218 -0
- agentops/instrumentation/common/token_counting.py +177 -0
- agentops/instrumentation/common/version.py +71 -0
- agentops/instrumentation/common/wrappers.py +235 -0
- agentops/legacy/__init__.py +277 -0
- agentops/legacy/event.py +156 -0
- agentops/logging/__init__.py +4 -0
- agentops/logging/config.py +86 -0
- agentops/logging/formatters.py +34 -0
- agentops/logging/instrument_logging.py +91 -0
- agentops/sdk/__init__.py +27 -0
- agentops/sdk/attributes.py +151 -0
- agentops/sdk/core.py +607 -0
- agentops/sdk/decorators/__init__.py +51 -0
- agentops/sdk/decorators/factory.py +486 -0
- agentops/sdk/decorators/utility.py +216 -0
- agentops/sdk/exporters.py +87 -0
- agentops/sdk/processors.py +71 -0
- agentops/sdk/types.py +21 -0
- agentops/semconv/__init__.py +36 -0
- agentops/semconv/agent.py +29 -0
- agentops/semconv/core.py +19 -0
- agentops/semconv/enum.py +11 -0
- agentops/semconv/instrumentation.py +13 -0
- agentops/semconv/langchain.py +63 -0
- agentops/semconv/message.py +61 -0
- agentops/semconv/meters.py +24 -0
- agentops/semconv/resource.py +52 -0
- agentops/semconv/span_attributes.py +118 -0
- agentops/semconv/span_kinds.py +50 -0
- agentops/semconv/status.py +11 -0
- agentops/semconv/tool.py +15 -0
- agentops/semconv/workflow.py +69 -0
- agentops/validation.py +357 -0
- mseep_agentops-0.4.18.dist-info/METADATA +49 -0
- mseep_agentops-0.4.18.dist-info/RECORD +94 -0
- mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
- mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
- mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +10 -0
- tests/unit/__init__.py +0 -0
- tests/unit/client/__init__.py +1 -0
- tests/unit/client/test_http_adapter.py +221 -0
- tests/unit/client/test_http_client.py +206 -0
- tests/unit/conftest.py +54 -0
- tests/unit/sdk/__init__.py +1 -0
- tests/unit/sdk/instrumentation_tester.py +207 -0
- tests/unit/sdk/test_attributes.py +392 -0
- tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
- tests/unit/sdk/test_decorators.py +763 -0
- tests/unit/sdk/test_exporters.py +241 -0
- tests/unit/sdk/test_factory.py +1188 -0
- tests/unit/sdk/test_internal_span_processor.py +397 -0
- tests/unit/sdk/test_resource_attributes.py +35 -0
- tests/unit/test_config.py +82 -0
- tests/unit/test_context_manager.py +777 -0
- tests/unit/test_events.py +27 -0
- tests/unit/test_host_env.py +54 -0
- tests/unit/test_init_py.py +501 -0
- tests/unit/test_serialization.py +433 -0
- tests/unit/test_session.py +676 -0
- tests/unit/test_user_agent.py +34 -0
- tests/unit/test_validation.py +405 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from requests.adapters import HTTPAdapter
|
4
|
+
from urllib3.util import Retry
|
5
|
+
|
6
|
+
# from agentops.client.auth_manager import AuthManager
|
7
|
+
|
8
|
+
|
9
|
+
class BaseHTTPAdapter(HTTPAdapter):
|
10
|
+
"""Base HTTP adapter with enhanced connection pooling and retry logic"""
|
11
|
+
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
pool_connections: int = 15,
|
15
|
+
pool_maxsize: int = 256,
|
16
|
+
max_retries: Optional[Retry] = None,
|
17
|
+
):
|
18
|
+
"""
|
19
|
+
Initialize the base HTTP adapter.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
pool_connections: Number of connection pools to cache
|
23
|
+
pool_maxsize: Maximum number of connections to save in the pool
|
24
|
+
max_retries: Retry configuration for failed requests
|
25
|
+
"""
|
26
|
+
if max_retries is None:
|
27
|
+
max_retries = Retry(
|
28
|
+
total=3,
|
29
|
+
backoff_factor=0.1,
|
30
|
+
status_forcelist=[500, 502, 503, 504],
|
31
|
+
)
|
32
|
+
|
33
|
+
super().__init__(pool_connections=pool_connections, pool_maxsize=pool_maxsize, max_retries=max_retries)
|
34
|
+
|
35
|
+
|
36
|
+
# class AuthenticatedHttpAdapter(BaseHTTPAdapter):
|
37
|
+
# """HTTP adapter with automatic JWT authentication and refresh"""
|
38
|
+
#
|
39
|
+
# def __init__(
|
40
|
+
# self,
|
41
|
+
# auth_manager: AuthManager,
|
42
|
+
# api_key: str,
|
43
|
+
# token_fetcher: Callable[[str], Union[str, AuthTokenResponse]],
|
44
|
+
# pool_connections: int = 15,
|
45
|
+
# pool_maxsize: int = 256,
|
46
|
+
# max_retries: Optional[Retry] = None,
|
47
|
+
# ):
|
48
|
+
# """
|
49
|
+
# Initialize the authenticated HTTP adapter.
|
50
|
+
#
|
51
|
+
# Args:
|
52
|
+
# auth_manager: The authentication manager to use
|
53
|
+
# api_key: The API key to authenticate with
|
54
|
+
# token_fetcher: Function to fetch a new token if needed
|
55
|
+
# pool_connections: Number of connection pools to cache
|
56
|
+
# pool_maxsize: Maximum number of connections to save in the pool
|
57
|
+
# max_retries: Retry configuration for failed requests
|
58
|
+
# """
|
59
|
+
# self.auth_manager = auth_manager
|
60
|
+
# self.api_key = api_key
|
61
|
+
# self.token_fetcher = token_fetcher
|
62
|
+
#
|
63
|
+
# super().__init__(
|
64
|
+
# pool_connections=pool_connections,
|
65
|
+
# pool_maxsize=pool_maxsize,
|
66
|
+
# max_retries=max_retries
|
67
|
+
# )
|
68
|
+
#
|
69
|
+
# def add_headers(self, request, **kwargs):
|
70
|
+
# """Add authentication headers to the request"""
|
71
|
+
# # Get fresh auth headers from the auth manager
|
72
|
+
# self.auth_manager.maybe_fetch(self.api_key, self.token_fetcher)
|
73
|
+
# auth_headers = self.auth_manager.prepare_auth_headers(self.api_key)
|
74
|
+
#
|
75
|
+
# # Update request headers
|
76
|
+
# for key, value in auth_headers.items():
|
77
|
+
# request.headers[key] = value
|
78
|
+
#
|
79
|
+
# return request
|
80
|
+
#
|
81
|
+
# def send(self, request, **kwargs):
|
82
|
+
# """Send the request with authentication retry logic"""
|
83
|
+
# # Ensure allow_redirects is set to False
|
84
|
+
# kwargs["allow_redirects"] = False
|
85
|
+
#
|
86
|
+
# # Add auth headers to initial request
|
87
|
+
# request = self.add_headers(request, **kwargs)
|
88
|
+
#
|
89
|
+
# # Make the initial request
|
90
|
+
# response = super().send(request, **kwargs)
|
91
|
+
#
|
92
|
+
# # If we get a 401/403, check if it's due to token expiration
|
93
|
+
# if self.auth_manager.is_token_expired_response(response):
|
94
|
+
# logger.debug("Token expired, attempting to refresh")
|
95
|
+
# try:
|
96
|
+
# # Force token refresh
|
97
|
+
# self.auth_manager.clear_token()
|
98
|
+
# self.auth_manager.maybe_fetch(self.api_key, self.token_fetcher)
|
99
|
+
#
|
100
|
+
# # Update request with new token
|
101
|
+
# request = self.add_headers(request, **kwargs)
|
102
|
+
#
|
103
|
+
# # Retry the request
|
104
|
+
# logger.debug("Retrying request with new token")
|
105
|
+
# response = super().send(request, **kwargs)
|
106
|
+
# except AgentOpsApiJwtExpiredException as e:
|
107
|
+
# # Authentication failed
|
108
|
+
# logger.warning(f"Failed to refresh authentication token: {e}")
|
109
|
+
# except ApiServerException as e:
|
110
|
+
# # Server error during token refresh
|
111
|
+
# logger.error(f"Server error during token refresh: {e}")
|
112
|
+
# except Exception as e:
|
113
|
+
# # Unexpected error during token refresh
|
114
|
+
# logger.error(f"Unexpected error during token refresh: {e}")
|
115
|
+
#
|
116
|
+
# return response
|
@@ -0,0 +1,215 @@
|
|
1
|
+
from typing import Dict, Optional
|
2
|
+
|
3
|
+
import requests
|
4
|
+
|
5
|
+
from agentops.client.http.http_adapter import BaseHTTPAdapter
|
6
|
+
from agentops.logging import logger
|
7
|
+
from agentops.helpers.version import get_agentops_version
|
8
|
+
|
9
|
+
|
10
|
+
class HttpClient:
|
11
|
+
"""Base HTTP client with connection pooling and session management"""
|
12
|
+
|
13
|
+
_session: Optional[requests.Session] = None
|
14
|
+
_project_id: Optional[str] = None
|
15
|
+
|
16
|
+
@classmethod
|
17
|
+
def get_project_id(cls) -> Optional[str]:
|
18
|
+
"""Get the stored project ID"""
|
19
|
+
return cls._project_id
|
20
|
+
|
21
|
+
@classmethod
|
22
|
+
def get_session(cls) -> requests.Session:
|
23
|
+
"""Get or create the global session with optimized connection pooling"""
|
24
|
+
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'}")
|
44
|
+
return cls._session
|
45
|
+
|
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
|
140
|
+
|
141
|
+
@classmethod
|
142
|
+
def request(
|
143
|
+
cls,
|
144
|
+
method: str,
|
145
|
+
url: str,
|
146
|
+
data: Optional[Dict] = None,
|
147
|
+
headers: Optional[Dict] = None,
|
148
|
+
timeout: int = 30,
|
149
|
+
max_redirects: int = 5,
|
150
|
+
) -> requests.Response:
|
151
|
+
"""
|
152
|
+
Make a generic HTTP request
|
153
|
+
|
154
|
+
Args:
|
155
|
+
method: HTTP method (e.g., 'get', 'post', 'put', 'delete')
|
156
|
+
url: Full URL for the request
|
157
|
+
data: Request payload (for POST, PUT methods)
|
158
|
+
headers: Request headers
|
159
|
+
timeout: Request timeout in seconds
|
160
|
+
max_redirects: Maximum number of redirects to follow (default: 5)
|
161
|
+
|
162
|
+
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
|
168
|
+
"""
|
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
|
agentops/config.py
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import List, Optional, Set, TypedDict, Union
|
7
|
+
from uuid import UUID
|
8
|
+
|
9
|
+
from opentelemetry.sdk.trace import SpanProcessor
|
10
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
11
|
+
|
12
|
+
from agentops.exceptions import InvalidApiKeyException
|
13
|
+
from agentops.helpers.env import get_env_bool, get_env_int, get_env_list
|
14
|
+
from agentops.helpers.serialization import AgentOpsJSONEncoder
|
15
|
+
|
16
|
+
|
17
|
+
class ConfigDict(TypedDict):
|
18
|
+
api_key: Optional[str]
|
19
|
+
endpoint: Optional[str]
|
20
|
+
app_url: Optional[str]
|
21
|
+
max_wait_time: Optional[int]
|
22
|
+
export_flush_interval: Optional[int]
|
23
|
+
max_queue_size: Optional[int]
|
24
|
+
default_tags: Optional[List[str]]
|
25
|
+
trace_name: Optional[str]
|
26
|
+
instrument_llm_calls: Optional[bool]
|
27
|
+
auto_start_session: Optional[bool]
|
28
|
+
auto_init: Optional[bool]
|
29
|
+
skip_auto_end_session: Optional[bool]
|
30
|
+
env_data_opt_out: Optional[bool]
|
31
|
+
log_level: Optional[Union[str, int]]
|
32
|
+
fail_safe: Optional[bool]
|
33
|
+
prefetch_jwt_token: Optional[bool]
|
34
|
+
log_session_replay_url: Optional[bool]
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class Config:
|
39
|
+
api_key: Optional[str] = field(
|
40
|
+
default_factory=lambda: os.getenv("AGENTOPS_API_KEY"),
|
41
|
+
metadata={"description": "API key for authentication with AgentOps services"},
|
42
|
+
)
|
43
|
+
|
44
|
+
endpoint: str = field(
|
45
|
+
default_factory=lambda: os.getenv("AGENTOPS_API_ENDPOINT", "https://api.agentops.ai"),
|
46
|
+
metadata={"description": "Base URL for the AgentOps API"},
|
47
|
+
)
|
48
|
+
|
49
|
+
app_url: str = field(
|
50
|
+
default_factory=lambda: os.getenv("AGENTOPS_APP_URL", "https://app.agentops.ai"),
|
51
|
+
metadata={"description": "Dashboard URL for the AgentOps application"},
|
52
|
+
)
|
53
|
+
|
54
|
+
max_wait_time: int = field(
|
55
|
+
default_factory=lambda: get_env_int("AGENTOPS_MAX_WAIT_TIME", 5000),
|
56
|
+
metadata={"description": "Maximum time in milliseconds to wait for API responses"},
|
57
|
+
)
|
58
|
+
|
59
|
+
export_flush_interval: int = field(
|
60
|
+
default_factory=lambda: get_env_int("AGENTOPS_EXPORT_FLUSH_INTERVAL", 1000),
|
61
|
+
metadata={"description": "Time interval in milliseconds between automatic exports of telemetry data"},
|
62
|
+
)
|
63
|
+
|
64
|
+
max_queue_size: int = field(
|
65
|
+
default_factory=lambda: get_env_int("AGENTOPS_MAX_QUEUE_SIZE", 512),
|
66
|
+
metadata={"description": "Maximum number of events to queue before forcing a flush"},
|
67
|
+
)
|
68
|
+
|
69
|
+
default_tags: Set[str] = field(
|
70
|
+
default_factory=lambda: get_env_list("AGENTOPS_DEFAULT_TAGS"),
|
71
|
+
metadata={"description": "Default tags to apply to all sessions"},
|
72
|
+
)
|
73
|
+
|
74
|
+
trace_name: Optional[str] = field(
|
75
|
+
default_factory=lambda: os.getenv("AGENTOPS_TRACE_NAME"),
|
76
|
+
metadata={"description": "Default name for the trace/session"},
|
77
|
+
)
|
78
|
+
|
79
|
+
instrument_llm_calls: bool = field(
|
80
|
+
default_factory=lambda: get_env_bool("AGENTOPS_INSTRUMENT_LLM_CALLS", True),
|
81
|
+
metadata={"description": "Whether to automatically instrument and track LLM API calls"},
|
82
|
+
)
|
83
|
+
|
84
|
+
auto_start_session: bool = field(
|
85
|
+
default_factory=lambda: get_env_bool("AGENTOPS_AUTO_START_SESSION", True),
|
86
|
+
metadata={"description": "Whether to automatically start a session when initializing"},
|
87
|
+
)
|
88
|
+
|
89
|
+
auto_init: bool = field(
|
90
|
+
default_factory=lambda: get_env_bool("AGENTOPS_AUTO_INIT", True),
|
91
|
+
metadata={"description": "Whether to automatically initialize the client on import"},
|
92
|
+
)
|
93
|
+
|
94
|
+
skip_auto_end_session: bool = field(
|
95
|
+
default_factory=lambda: get_env_bool("AGENTOPS_SKIP_AUTO_END_SESSION", False),
|
96
|
+
metadata={"description": "Whether to skip automatically ending sessions on program exit"},
|
97
|
+
)
|
98
|
+
|
99
|
+
env_data_opt_out: bool = field(
|
100
|
+
default_factory=lambda: get_env_bool("AGENTOPS_ENV_DATA_OPT_OUT", False),
|
101
|
+
metadata={"description": "Whether to opt out of collecting environment data"},
|
102
|
+
)
|
103
|
+
|
104
|
+
log_level: Union[str, int] = field(
|
105
|
+
default_factory=lambda: os.getenv("AGENTOPS_LOG_LEVEL", "INFO"),
|
106
|
+
metadata={"description": "Logging level for AgentOps logs"},
|
107
|
+
)
|
108
|
+
|
109
|
+
fail_safe: bool = field(
|
110
|
+
default_factory=lambda: get_env_bool("AGENTOPS_FAIL_SAFE", False),
|
111
|
+
metadata={"description": "Whether to suppress errors and continue execution when possible"},
|
112
|
+
)
|
113
|
+
|
114
|
+
prefetch_jwt_token: bool = field(
|
115
|
+
default_factory=lambda: get_env_bool("AGENTOPS_PREFETCH_JWT_TOKEN", True),
|
116
|
+
metadata={"description": "Whether to prefetch JWT token during initialization"},
|
117
|
+
)
|
118
|
+
|
119
|
+
log_session_replay_url: bool = field(
|
120
|
+
default_factory=lambda: get_env_bool("AGENTOPS_LOG_SESSION_REPLAY_URL", True),
|
121
|
+
metadata={"description": "Whether to log session replay URLs to the console"},
|
122
|
+
)
|
123
|
+
|
124
|
+
exporter_endpoint: Optional[str] = field(
|
125
|
+
default_factory=lambda: os.getenv("AGENTOPS_EXPORTER_ENDPOINT", "https://otlp.agentops.ai/v1/traces"),
|
126
|
+
metadata={
|
127
|
+
"description": "Endpoint for the span exporter. When not provided, the default AgentOps endpoint will be used."
|
128
|
+
},
|
129
|
+
)
|
130
|
+
|
131
|
+
exporter: Optional[SpanExporter] = field(
|
132
|
+
default_factory=lambda: None, metadata={"description": "Custom span exporter for OpenTelemetry trace data"}
|
133
|
+
)
|
134
|
+
|
135
|
+
processor: Optional[SpanProcessor] = field(
|
136
|
+
default_factory=lambda: None, metadata={"description": "Custom span processor for OpenTelemetry trace data"}
|
137
|
+
)
|
138
|
+
|
139
|
+
def configure(
|
140
|
+
self,
|
141
|
+
api_key: Optional[str] = None,
|
142
|
+
endpoint: Optional[str] = None,
|
143
|
+
app_url: Optional[str] = None,
|
144
|
+
max_wait_time: Optional[int] = None,
|
145
|
+
export_flush_interval: Optional[int] = None,
|
146
|
+
max_queue_size: Optional[int] = None,
|
147
|
+
default_tags: Optional[List[str]] = None,
|
148
|
+
trace_name: Optional[str] = None,
|
149
|
+
instrument_llm_calls: Optional[bool] = None,
|
150
|
+
auto_start_session: Optional[bool] = None,
|
151
|
+
auto_init: Optional[bool] = None,
|
152
|
+
skip_auto_end_session: Optional[bool] = None,
|
153
|
+
env_data_opt_out: Optional[bool] = None,
|
154
|
+
log_level: Optional[Union[str, int]] = None,
|
155
|
+
fail_safe: Optional[bool] = None,
|
156
|
+
prefetch_jwt_token: Optional[bool] = None,
|
157
|
+
log_session_replay_url: Optional[bool] = None,
|
158
|
+
exporter: Optional[SpanExporter] = None,
|
159
|
+
processor: Optional[SpanProcessor] = None,
|
160
|
+
exporter_endpoint: Optional[str] = None,
|
161
|
+
):
|
162
|
+
"""Configure settings from kwargs, validating where necessary"""
|
163
|
+
if api_key is not None:
|
164
|
+
self.api_key = api_key
|
165
|
+
if not TESTING: # Allow setting dummy keys in tests
|
166
|
+
try:
|
167
|
+
UUID(api_key)
|
168
|
+
except ValueError:
|
169
|
+
raise InvalidApiKeyException(api_key, self.endpoint)
|
170
|
+
|
171
|
+
if endpoint is not None:
|
172
|
+
self.endpoint = endpoint
|
173
|
+
|
174
|
+
if app_url is not None:
|
175
|
+
self.app_url = app_url
|
176
|
+
|
177
|
+
if max_wait_time is not None:
|
178
|
+
self.max_wait_time = max_wait_time
|
179
|
+
|
180
|
+
if export_flush_interval is not None:
|
181
|
+
self.export_flush_interval = export_flush_interval
|
182
|
+
|
183
|
+
if max_queue_size is not None:
|
184
|
+
self.max_queue_size = max_queue_size
|
185
|
+
|
186
|
+
if default_tags is not None:
|
187
|
+
self.default_tags = set(default_tags)
|
188
|
+
|
189
|
+
if trace_name is not None:
|
190
|
+
self.trace_name = trace_name
|
191
|
+
|
192
|
+
if instrument_llm_calls is not None:
|
193
|
+
self.instrument_llm_calls = instrument_llm_calls
|
194
|
+
|
195
|
+
if auto_start_session is not None:
|
196
|
+
self.auto_start_session = auto_start_session
|
197
|
+
|
198
|
+
if auto_init is not None:
|
199
|
+
self.auto_init = auto_init
|
200
|
+
|
201
|
+
if skip_auto_end_session is not None:
|
202
|
+
self.skip_auto_end_session = skip_auto_end_session
|
203
|
+
|
204
|
+
if env_data_opt_out is not None:
|
205
|
+
self.env_data_opt_out = env_data_opt_out
|
206
|
+
|
207
|
+
if log_level is not None:
|
208
|
+
if isinstance(log_level, str):
|
209
|
+
log_level_str = log_level.upper()
|
210
|
+
if hasattr(logging, log_level_str):
|
211
|
+
self.log_level = getattr(logging, log_level_str)
|
212
|
+
else:
|
213
|
+
self.log_level = logging.INFO
|
214
|
+
else:
|
215
|
+
self.log_level = log_level
|
216
|
+
|
217
|
+
if fail_safe is not None:
|
218
|
+
self.fail_safe = fail_safe
|
219
|
+
|
220
|
+
if prefetch_jwt_token is not None:
|
221
|
+
self.prefetch_jwt_token = prefetch_jwt_token
|
222
|
+
|
223
|
+
if log_session_replay_url is not None:
|
224
|
+
self.log_session_replay_url = log_session_replay_url
|
225
|
+
|
226
|
+
if exporter is not None:
|
227
|
+
self.exporter = exporter
|
228
|
+
|
229
|
+
if processor is not None:
|
230
|
+
self.processor = processor
|
231
|
+
|
232
|
+
if exporter_endpoint is not None:
|
233
|
+
self.exporter_endpoint = exporter_endpoint
|
234
|
+
# else:
|
235
|
+
# self.exporter_endpoint = self.endpoint
|
236
|
+
|
237
|
+
def dict(self):
|
238
|
+
"""Return a dictionary representation of the config"""
|
239
|
+
return {
|
240
|
+
"api_key": self.api_key,
|
241
|
+
"endpoint": self.endpoint,
|
242
|
+
"app_url": self.app_url,
|
243
|
+
"max_wait_time": self.max_wait_time,
|
244
|
+
"export_flush_interval": self.export_flush_interval,
|
245
|
+
"max_queue_size": self.max_queue_size,
|
246
|
+
"default_tags": self.default_tags,
|
247
|
+
"trace_name": self.trace_name,
|
248
|
+
"instrument_llm_calls": self.instrument_llm_calls,
|
249
|
+
"auto_start_session": self.auto_start_session,
|
250
|
+
"auto_init": self.auto_init,
|
251
|
+
"skip_auto_end_session": self.skip_auto_end_session,
|
252
|
+
"env_data_opt_out": self.env_data_opt_out,
|
253
|
+
"log_level": self.log_level,
|
254
|
+
"fail_safe": self.fail_safe,
|
255
|
+
"prefetch_jwt_token": self.prefetch_jwt_token,
|
256
|
+
"log_session_replay_url": self.log_session_replay_url,
|
257
|
+
"exporter": self.exporter,
|
258
|
+
"processor": self.processor,
|
259
|
+
"exporter_endpoint": self.exporter_endpoint,
|
260
|
+
}
|
261
|
+
|
262
|
+
def json(self):
|
263
|
+
"""Return a JSON representation of the config"""
|
264
|
+
return json.dumps(self.dict(), cls=AgentOpsJSONEncoder)
|
265
|
+
|
266
|
+
|
267
|
+
# checks if pytest is imported
|
268
|
+
TESTING = "pytest" in sys.modules
|
agentops/enums.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
"""
|
2
|
+
AgentOps enums for user-friendly API.
|
3
|
+
|
4
|
+
This module provides simple enums that users can import from agentops
|
5
|
+
without needing to know about OpenTelemetry internals.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from enum import Enum
|
9
|
+
from opentelemetry.trace.status import StatusCode
|
10
|
+
|
11
|
+
|
12
|
+
class TraceState(Enum):
|
13
|
+
"""
|
14
|
+
Enum for trace end states.
|
15
|
+
|
16
|
+
This provides a user-friendly interface that maps to OpenTelemetry StatusCode internally.
|
17
|
+
Users can simply use agentops.TraceState.SUCCESS instead of importing OpenTelemetry.
|
18
|
+
"""
|
19
|
+
|
20
|
+
SUCCESS = StatusCode.OK
|
21
|
+
ERROR = StatusCode.ERROR
|
22
|
+
UNSET = StatusCode.UNSET
|
23
|
+
|
24
|
+
def __str__(self) -> str:
|
25
|
+
"""Return the name for string representation."""
|
26
|
+
return self.name
|
27
|
+
|
28
|
+
def to_status_code(self) -> StatusCode:
|
29
|
+
"""Convert to OpenTelemetry StatusCode."""
|
30
|
+
return self.value
|
31
|
+
|
32
|
+
|
33
|
+
# For backward compatibility, also provide these as module-level constants
|
34
|
+
SUCCESS = TraceState.SUCCESS
|
35
|
+
ERROR = TraceState.ERROR
|
36
|
+
UNSET = TraceState.UNSET
|
agentops/exceptions.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
class MultiSessionException(Exception):
|
2
|
+
def __init__(self, message):
|
3
|
+
super().__init__(message)
|
4
|
+
|
5
|
+
|
6
|
+
class NoSessionException(Exception):
|
7
|
+
def __init__(self, message="No session found"):
|
8
|
+
super().__init__(message)
|
9
|
+
|
10
|
+
|
11
|
+
class NoApiKeyException(Exception):
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
message="Could not initialize AgentOps client - API Key is missing."
|
15
|
+
+ "\n\t Find your API key at https://app.agentops.ai/settings/projects",
|
16
|
+
):
|
17
|
+
super().__init__(message)
|
18
|
+
|
19
|
+
|
20
|
+
class InvalidApiKeyException(Exception):
|
21
|
+
def __init__(self, api_key, endpoint):
|
22
|
+
message = f"API Key is invalid: {{{api_key}}}.\n\t Find your API key at {endpoint}/settings/projects"
|
23
|
+
super().__init__(message)
|
24
|
+
|
25
|
+
|
26
|
+
class ApiServerException(Exception):
|
27
|
+
def __init__(self, message):
|
28
|
+
super().__init__(message)
|
29
|
+
|
30
|
+
|
31
|
+
class AgentOpsClientNotInitializedException(RuntimeError):
|
32
|
+
def __init__(self, message="AgentOps client must be initialized before using this feature"):
|
33
|
+
super().__init__(message)
|
34
|
+
|
35
|
+
|
36
|
+
class AgentOpsApiJwtExpiredException(Exception):
|
37
|
+
def __init__(self, message="JWT token has expired"):
|
38
|
+
super().__init__(message)
|