aiqa-client 0.4.3__tar.gz → 0.5.2__tar.gz
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.
- {aiqa_client-0.4.3/aiqa_client.egg-info → aiqa_client-0.5.2}/PKG-INFO +1 -1
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa/__init__.py +1 -1
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa/client.py +108 -23
- aiqa_client-0.5.2/aiqa/constants.py +8 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa/experiment_runner.py +12 -29
- aiqa_client-0.5.2/aiqa/http_utils.py +143 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa/object_serialiser.py +136 -115
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa/tracing.py +155 -267
- aiqa_client-0.5.2/aiqa/tracing_llm_utils.py +191 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2/aiqa_client.egg-info}/PKG-INFO +1 -1
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa_client.egg-info/SOURCES.txt +8 -5
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/pyproject.toml +1 -1
- aiqa_client-0.5.2/tests/test_api_key.py +96 -0
- aiqa_client-0.5.2/tests/test_integration.py +285 -0
- aiqa_client-0.5.2/tests/test_object_serialiser.py +382 -0
- aiqa_client-0.5.2/tests/test_startup_reliability.py +108 -0
- {aiqa_client-0.4.3/aiqa → aiqa_client-0.5.2/tests}/test_tracing.py +188 -5
- aiqa_client-0.4.3/aiqa/aiqa_exporter.py +0 -679
- aiqa_client-0.4.3/aiqa/constants.py +0 -6
- aiqa_client-0.4.3/aiqa/test_experiment_runner.py +0 -176
- aiqa_client-0.4.3/aiqa/test_startup_reliability.py +0 -249
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/LICENSE.txt +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/MANIFEST.in +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/README.md +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa/py.typed +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa_client.egg-info/dependency_links.txt +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa_client.egg-info/requires.txt +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/aiqa_client.egg-info/top_level.txt +0 -0
- {aiqa_client-0.4.3 → aiqa_client-0.5.2}/setup.cfg +0 -0
|
@@ -6,7 +6,7 @@ The client initializes automatically when WithTracing is first used.
|
|
|
6
6
|
|
|
7
7
|
Set environment variables:
|
|
8
8
|
AIQA_SERVER_URL: URL of the AIQA server
|
|
9
|
-
AIQA_API_KEY: API key for authentication
|
|
9
|
+
AIQA_API_KEY: API key for authentication (required)
|
|
10
10
|
AIQA_COMPONENT_TAG: Optional component identifier
|
|
11
11
|
AIQA_STARTUP_DELAY_SECONDS: Optional delay before first flush (default: 10s)
|
|
12
12
|
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import logging
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from typing import Optional, TYPE_CHECKING, Any
|
|
5
|
+
from typing import Optional, TYPE_CHECKING, Any, Dict
|
|
6
6
|
from opentelemetry import trace
|
|
7
7
|
from opentelemetry.sdk.trace import TracerProvider
|
|
8
8
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
9
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
10
|
+
import requests
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
from .constants import AIQA_TRACER_NAME, LOG_TAG
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(LOG_TAG)
|
|
11
15
|
|
|
12
16
|
# Compatibility import for TraceIdRatioBased sampler
|
|
13
17
|
# In older OpenTelemetry versions it was TraceIdRatioBasedSampler
|
|
@@ -20,7 +24,7 @@ except ImportError:
|
|
|
20
24
|
from opentelemetry.sdk.trace.sampling import TraceIdRatioBasedSampler as TraceIdRatioBased
|
|
21
25
|
except ImportError:
|
|
22
26
|
logger.warning(
|
|
23
|
-
"Could not import TraceIdRatioBased or TraceIdRatioBasedSampler from "
|
|
27
|
+
f"Could not import TraceIdRatioBased or TraceIdRatioBasedSampler from "
|
|
24
28
|
"opentelemetry.sdk.trace.sampling. AIQA tracing may not work correctly. "
|
|
25
29
|
"Please ensure opentelemetry-sdk>=1.24.0 is installed. "
|
|
26
30
|
"Try: pip install --upgrade opentelemetry-sdk"
|
|
@@ -28,7 +32,7 @@ except ImportError:
|
|
|
28
32
|
# Set to None so we can check later
|
|
29
33
|
TraceIdRatioBased = None
|
|
30
34
|
|
|
31
|
-
from .
|
|
35
|
+
from .http_utils import get_server_url, get_api_key, build_headers, format_http_error
|
|
32
36
|
|
|
33
37
|
class AIQAClient:
|
|
34
38
|
"""
|
|
@@ -93,19 +97,19 @@ class AIQAClient:
|
|
|
93
97
|
This will also set enabled=False to prevent further tracing attempts.
|
|
94
98
|
"""
|
|
95
99
|
try:
|
|
96
|
-
logger.info("AIQA tracing shutting down")
|
|
100
|
+
logger.info(f"AIQA tracing shutting down")
|
|
97
101
|
# Disable tracing to prevent attempts to use shut-down system
|
|
98
102
|
self.enabled = False
|
|
99
103
|
if self._provider:
|
|
100
104
|
self._provider.shutdown()
|
|
101
|
-
|
|
102
|
-
self._exporter.shutdown()
|
|
105
|
+
# OTLP exporter doesn't have a separate shutdown method - it's handled by the provider
|
|
103
106
|
except Exception as e:
|
|
104
107
|
logger.error(f"Error shutting down tracing: {e}")
|
|
105
108
|
# Still disable even if shutdown had errors
|
|
106
109
|
self.enabled = False
|
|
107
110
|
|
|
108
111
|
|
|
112
|
+
|
|
109
113
|
# Global singleton instance
|
|
110
114
|
client: AIQAClient = AIQAClient()
|
|
111
115
|
|
|
@@ -150,7 +154,7 @@ def get_aiqa_client() -> AIQAClient:
|
|
|
150
154
|
# Optional: Initialize explicitly (usually not needed)
|
|
151
155
|
client = get_aiqa_client()
|
|
152
156
|
if client.enabled:
|
|
153
|
-
print("Tracing is enabled")
|
|
157
|
+
print(f"Tracing is enabled")
|
|
154
158
|
|
|
155
159
|
@WithTracing
|
|
156
160
|
def my_function():
|
|
@@ -161,7 +165,7 @@ def get_aiqa_client() -> AIQAClient:
|
|
|
161
165
|
_init_tracing()
|
|
162
166
|
except Exception as e:
|
|
163
167
|
logger.error(f"Failed to initialize AIQA tracing: {e}")
|
|
164
|
-
logger.warning("AIQA tracing is disabled. Your application will continue to run without tracing.")
|
|
168
|
+
logger.warning(f"AIQA tracing is disabled. Your application will continue to run without tracing.")
|
|
165
169
|
return client
|
|
166
170
|
|
|
167
171
|
def _init_tracing() -> None:
|
|
@@ -171,14 +175,13 @@ def _init_tracing() -> None:
|
|
|
171
175
|
return
|
|
172
176
|
|
|
173
177
|
try:
|
|
174
|
-
server_url =
|
|
175
|
-
api_key =
|
|
178
|
+
server_url = get_server_url()
|
|
179
|
+
api_key = get_api_key()
|
|
176
180
|
|
|
177
|
-
if not
|
|
181
|
+
if not api_key:
|
|
178
182
|
client.enabled = False
|
|
179
|
-
missing_vars = [var for var, val in [("AIQA_SERVER_URL", server_url), ("AIQA_API_KEY", api_key)] if not val]
|
|
180
183
|
logger.warning(
|
|
181
|
-
f"AIQA tracing is disabled: missing required environment variables:
|
|
184
|
+
f"AIQA tracing is disabled: missing required environment variables: AIQA_API_KEY"
|
|
182
185
|
)
|
|
183
186
|
client._initialized = True
|
|
184
187
|
return
|
|
@@ -225,23 +228,43 @@ def _init_tracing() -> None:
|
|
|
225
228
|
|
|
226
229
|
def _attach_aiqa_processor(provider: TracerProvider) -> None:
|
|
227
230
|
"""Attach AIQA span processor to the provider. Idempotent - safe to call multiple times."""
|
|
228
|
-
from .aiqa_exporter import AIQASpanExporter
|
|
229
|
-
|
|
230
231
|
try:
|
|
231
232
|
# Check if already attached
|
|
232
233
|
for p in provider._active_span_processor._span_processors:
|
|
233
|
-
if isinstance(getattr(p, "exporter", None),
|
|
234
|
-
logger.debug("AIQA span processor already attached, skipping")
|
|
234
|
+
if isinstance(getattr(p, "exporter", None), OTLPSpanExporter):
|
|
235
|
+
logger.debug(f"AIQA span processor already attached, skipping")
|
|
235
236
|
return
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
server_url = get_server_url()
|
|
239
|
+
api_key = get_api_key()
|
|
240
|
+
|
|
241
|
+
# Build headers for authentication
|
|
242
|
+
# OTLP exporter sets its own Content-Type, so we only need Authorization
|
|
243
|
+
auth_headers = {}
|
|
244
|
+
if api_key:
|
|
245
|
+
auth_headers["Authorization"] = f"ApiKey {api_key}"
|
|
246
|
+
elif os.getenv("AIQA_API_KEY"):
|
|
247
|
+
auth_headers["Authorization"] = f"ApiKey {os.getenv('AIQA_API_KEY')}"
|
|
248
|
+
|
|
249
|
+
# OTLP HTTP exporter requires the full endpoint URL including /v1/traces
|
|
250
|
+
# Ensure server_url doesn't have trailing slash or /v1/traces, then append /v1/traces
|
|
251
|
+
base_url = server_url.rstrip('/')
|
|
252
|
+
if base_url.endswith('/v1/traces'):
|
|
253
|
+
endpoint = base_url
|
|
254
|
+
else:
|
|
255
|
+
endpoint = f"{base_url}/v1/traces"
|
|
256
|
+
|
|
257
|
+
# Create OTLP exporter with authentication headers only
|
|
258
|
+
# The exporter will set Content-Type and other headers automatically
|
|
259
|
+
exporter = OTLPSpanExporter(
|
|
260
|
+
endpoint=endpoint,
|
|
261
|
+
headers=auth_headers if auth_headers else None,
|
|
240
262
|
)
|
|
263
|
+
|
|
241
264
|
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
242
265
|
global client
|
|
243
266
|
client.exporter = exporter
|
|
244
|
-
logger.debug("AIQA span processor attached successfully")
|
|
267
|
+
logger.debug(f"AIQA span processor attached successfully")
|
|
245
268
|
except Exception as e:
|
|
246
269
|
logger.error(f"Error attaching AIQA span processor: {e}")
|
|
247
270
|
# Re-raise to let _init_tracing handle it - it will log and continue
|
|
@@ -263,4 +286,66 @@ def get_aiqa_tracer() -> trace.Tracer:
|
|
|
263
286
|
except Exception as e:
|
|
264
287
|
# Log issue but still return a tracer
|
|
265
288
|
logger.info(f"Issue getting AIQA tracer with version: {e}, using fallback")
|
|
266
|
-
return trace.get_tracer(AIQA_TRACER_NAME)
|
|
289
|
+
return trace.get_tracer(AIQA_TRACER_NAME)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_organisation(
|
|
293
|
+
organisation_id: str,
|
|
294
|
+
server_url: Optional[str] = None,
|
|
295
|
+
api_key: Optional[str] = None
|
|
296
|
+
) -> Dict[str, Any]:
|
|
297
|
+
"""
|
|
298
|
+
Get organisation information based on API key via an API call.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
organisation_id: ID of the organisation to retrieve
|
|
302
|
+
server_url: Optional server URL (defaults to AIQA_SERVER_URL env var)
|
|
303
|
+
api_key: Optional API key (defaults to AIQA_API_KEY env var)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Organisation object as a dictionary
|
|
307
|
+
"""
|
|
308
|
+
url = get_server_url(server_url)
|
|
309
|
+
key = get_api_key(api_key)
|
|
310
|
+
headers = build_headers(key)
|
|
311
|
+
|
|
312
|
+
response = requests.get(
|
|
313
|
+
f"{url}/organisation/{organisation_id}",
|
|
314
|
+
headers=headers,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if not response.ok:
|
|
318
|
+
raise Exception(format_http_error(response, "get organisation"))
|
|
319
|
+
|
|
320
|
+
return response.json()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_api_key_info(
|
|
324
|
+
api_key_id: str,
|
|
325
|
+
server_url: Optional[str] = None,
|
|
326
|
+
api_key: Optional[str] = None
|
|
327
|
+
) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Get API key information via an API call.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
api_key_id: ID of the API key to retrieve
|
|
333
|
+
server_url: Optional server URL (defaults to AIQA_SERVER_URL env var)
|
|
334
|
+
api_key: Optional API key (defaults to AIQA_API_KEY env var)
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
ApiKey object as a dictionary
|
|
338
|
+
"""
|
|
339
|
+
url = get_server_url(server_url)
|
|
340
|
+
key = get_api_key(api_key)
|
|
341
|
+
headers = build_headers(key)
|
|
342
|
+
|
|
343
|
+
response = requests.get(
|
|
344
|
+
f"{url}/api-key/{api_key_id}",
|
|
345
|
+
headers=headers,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if not response.ok:
|
|
349
|
+
raise Exception(format_http_error(response, "get api key info"))
|
|
350
|
+
|
|
351
|
+
return response.json()
|
|
@@ -4,6 +4,8 @@ ExperimentRunner - runs experiments on datasets and scores results
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
+
from .constants import LOG_TAG
|
|
8
|
+
from .http_utils import build_headers, get_server_url, get_api_key, format_http_error
|
|
7
9
|
from typing import Any, Dict, List, Optional, Callable, Awaitable, Union
|
|
8
10
|
import requests
|
|
9
11
|
|
|
@@ -35,18 +37,15 @@ class ExperimentRunner:
|
|
|
35
37
|
"""
|
|
36
38
|
self.dataset_id = dataset_id
|
|
37
39
|
self.experiment_id = experiment_id
|
|
38
|
-
self.server_url = (server_url
|
|
39
|
-
self.api_key = api_key
|
|
40
|
+
self.server_url = get_server_url(server_url)
|
|
41
|
+
self.api_key = get_api_key(api_key)
|
|
40
42
|
self.organisation = organisation_id
|
|
41
43
|
self.experiment: Optional[Dict[str, Any]] = None
|
|
42
44
|
self.scores: List[Dict[str, Any]] = []
|
|
43
45
|
|
|
44
46
|
def _get_headers(self) -> Dict[str, str]:
|
|
45
47
|
"""Build HTTP headers for API requests."""
|
|
46
|
-
|
|
47
|
-
if self.api_key:
|
|
48
|
-
headers["Authorization"] = f"ApiKey {self.api_key}"
|
|
49
|
-
return headers
|
|
48
|
+
return build_headers(self.api_key)
|
|
50
49
|
|
|
51
50
|
def get_dataset(self) -> Dict[str, Any]:
|
|
52
51
|
"""
|
|
@@ -61,10 +60,7 @@ class ExperimentRunner:
|
|
|
61
60
|
)
|
|
62
61
|
|
|
63
62
|
if not response.ok:
|
|
64
|
-
|
|
65
|
-
raise Exception(
|
|
66
|
-
f"Failed to fetch dataset: {response.status_code} {response.reason} - {error_text}"
|
|
67
|
-
)
|
|
63
|
+
raise Exception(format_http_error(response, "fetch dataset"))
|
|
68
64
|
|
|
69
65
|
return response.json()
|
|
70
66
|
|
|
@@ -92,10 +88,7 @@ class ExperimentRunner:
|
|
|
92
88
|
)
|
|
93
89
|
|
|
94
90
|
if not response.ok:
|
|
95
|
-
|
|
96
|
-
raise Exception(
|
|
97
|
-
f"Failed to fetch example inputs: {response.status_code} {response.reason} - {error_text}"
|
|
98
|
-
)
|
|
91
|
+
raise Exception(format_http_error(response, "fetch example inputs"))
|
|
99
92
|
|
|
100
93
|
data = response.json()
|
|
101
94
|
return data.get("hits", [])
|
|
@@ -130,7 +123,7 @@ class ExperimentRunner:
|
|
|
130
123
|
"summary_results": {},
|
|
131
124
|
}
|
|
132
125
|
|
|
133
|
-
print("Creating experiment")
|
|
126
|
+
print(f"Creating experiment")
|
|
134
127
|
response = requests.post(
|
|
135
128
|
f"{self.server_url}/experiment",
|
|
136
129
|
json=experiment_setup,
|
|
@@ -138,10 +131,7 @@ class ExperimentRunner:
|
|
|
138
131
|
)
|
|
139
132
|
|
|
140
133
|
if not response.ok:
|
|
141
|
-
|
|
142
|
-
raise Exception(
|
|
143
|
-
f"Failed to create experiment: {response.status_code} {response.reason} - {error_text}"
|
|
144
|
-
)
|
|
134
|
+
raise Exception(format_http_error(response, "create experiment"))
|
|
145
135
|
|
|
146
136
|
experiment = response.json()
|
|
147
137
|
self.experiment_id = experiment["id"]
|
|
@@ -186,10 +176,7 @@ class ExperimentRunner:
|
|
|
186
176
|
)
|
|
187
177
|
|
|
188
178
|
if not response.ok:
|
|
189
|
-
|
|
190
|
-
raise Exception(
|
|
191
|
-
f"Failed to score and store: {response.status_code} {response.reason} - {error_text}"
|
|
192
|
-
)
|
|
179
|
+
raise Exception(format_http_error(response, "score and store"))
|
|
193
180
|
|
|
194
181
|
json_result = response.json()
|
|
195
182
|
print(f"scoreAndStore response: {json_result}")
|
|
@@ -270,8 +257,7 @@ class ExperimentRunner:
|
|
|
270
257
|
input_data = example["spans"][0].get("attributes", {}).get("input")
|
|
271
258
|
|
|
272
259
|
if not input_data:
|
|
273
|
-
print(
|
|
274
|
-
f"Warning: Example has no input field or spans with input attribute: {example}"
|
|
260
|
+
print(f"Warning: Example has no input field or spans with input attribute: {example}"
|
|
275
261
|
)
|
|
276
262
|
# Run engine anyway -- this could make sense if it's all about the parameters
|
|
277
263
|
|
|
@@ -326,10 +312,7 @@ class ExperimentRunner:
|
|
|
326
312
|
)
|
|
327
313
|
|
|
328
314
|
if not response.ok:
|
|
329
|
-
|
|
330
|
-
raise Exception(
|
|
331
|
-
f"Failed to fetch summary results: {response.status_code} {response.reason} - {error_text}"
|
|
332
|
-
)
|
|
315
|
+
raise Exception(format_http_error(response, "fetch summary results"))
|
|
333
316
|
|
|
334
317
|
experiment2 = response.json()
|
|
335
318
|
return experiment2.get("summary_results", {})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared HTTP utilities for AIQA client.
|
|
3
|
+
Provides common functions for building headers, handling errors, and accessing environment variables.
|
|
4
|
+
Supports AIQA-specific env vars (AIQA_SERVER_URL, AIQA_API_KEY) with fallback to OTLP standard vars.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_headers(api_key: Optional[str] = None) -> Dict[str, str]:
|
|
12
|
+
"""
|
|
13
|
+
Build HTTP headers for AIQA API requests.
|
|
14
|
+
|
|
15
|
+
Checks AIQA_API_KEY first, then falls back to OTEL_EXPORTER_OTLP_HEADERS if not set.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
api_key: Optional API key. If not provided, will try to get from AIQA_API_KEY env var,
|
|
19
|
+
then from OTEL_EXPORTER_OTLP_HEADERS.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dictionary with Content-Type, Accept-Encoding, and optionally Authorization header.
|
|
23
|
+
"""
|
|
24
|
+
headers = {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
"Accept-Encoding": "gzip, deflate, br", # Request compression (aiohttp handles decompression automatically)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Check parameter first
|
|
30
|
+
if api_key:
|
|
31
|
+
headers["Authorization"] = f"ApiKey {api_key}"
|
|
32
|
+
return headers
|
|
33
|
+
|
|
34
|
+
# Check AIQA_API_KEY env var
|
|
35
|
+
aiqa_api_key = os.getenv("AIQA_API_KEY")
|
|
36
|
+
if aiqa_api_key:
|
|
37
|
+
headers["Authorization"] = f"ApiKey {aiqa_api_key}"
|
|
38
|
+
return headers
|
|
39
|
+
|
|
40
|
+
# Fallback to OTLP headers (format: "key1=value1,key2=value2")
|
|
41
|
+
otlp_headers = os.getenv("OTEL_EXPORTER_OTLP_HEADERS")
|
|
42
|
+
if otlp_headers:
|
|
43
|
+
# Parse comma-separated key=value pairs
|
|
44
|
+
for header_pair in otlp_headers.split(","):
|
|
45
|
+
header_pair = header_pair.strip()
|
|
46
|
+
if "=" in header_pair:
|
|
47
|
+
key, value = header_pair.split("=", 1)
|
|
48
|
+
key = key.strip()
|
|
49
|
+
value = value.strip()
|
|
50
|
+
if key.lower() == "authorization":
|
|
51
|
+
headers["Authorization"] = value
|
|
52
|
+
else:
|
|
53
|
+
headers[key] = value
|
|
54
|
+
|
|
55
|
+
return headers
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_server_url(server_url: Optional[str] = None) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Get server URL from parameter or environment variable, with trailing slash removed.
|
|
61
|
+
|
|
62
|
+
Checks AIQA_SERVER_URL first, then falls back to OTEL_EXPORTER_OTLP_ENDPOINT if not set.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
server_url: Optional server URL. If not provided, will get from AIQA_SERVER_URL env var,
|
|
66
|
+
then from OTEL_EXPORTER_OTLP_ENDPOINT.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Server URL with trailing slash removed. Defaults to https://server-aiqa.winterwell.com if not set.
|
|
70
|
+
"""
|
|
71
|
+
# Check parameter first
|
|
72
|
+
if server_url:
|
|
73
|
+
return server_url.rstrip("/")
|
|
74
|
+
|
|
75
|
+
# Check AIQA_SERVER_URL env var
|
|
76
|
+
url = os.getenv("AIQA_SERVER_URL")
|
|
77
|
+
if url:
|
|
78
|
+
return url.rstrip("/")
|
|
79
|
+
|
|
80
|
+
# Fallback to OTLP endpoint
|
|
81
|
+
url = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
82
|
+
if url:
|
|
83
|
+
return url.rstrip("/")
|
|
84
|
+
|
|
85
|
+
# Default fallback
|
|
86
|
+
return "https://server-aiqa.winterwell.com"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_api_key(api_key: Optional[str] = None) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Get API key from parameter or environment variable.
|
|
92
|
+
|
|
93
|
+
Checks AIQA_API_KEY first, then falls back to OTEL_EXPORTER_OTLP_HEADERS if not set.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
api_key: Optional API key. If not provided, will get from AIQA_API_KEY env var,
|
|
97
|
+
then from OTEL_EXPORTER_OTLP_HEADERS (looking for Authorization header).
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
API key or empty string if not set.
|
|
101
|
+
"""
|
|
102
|
+
# Check parameter first
|
|
103
|
+
if api_key:
|
|
104
|
+
return api_key
|
|
105
|
+
|
|
106
|
+
# Check AIQA_API_KEY env var
|
|
107
|
+
aiqa_api_key = os.getenv("AIQA_API_KEY")
|
|
108
|
+
if aiqa_api_key:
|
|
109
|
+
return aiqa_api_key
|
|
110
|
+
|
|
111
|
+
# Fallback to OTLP headers (look for Authorization header)
|
|
112
|
+
otlp_headers = os.getenv("OTEL_EXPORTER_OTLP_HEADERS")
|
|
113
|
+
if otlp_headers:
|
|
114
|
+
for header_pair in otlp_headers.split(","):
|
|
115
|
+
header_pair = header_pair.strip()
|
|
116
|
+
if "=" in header_pair:
|
|
117
|
+
key, value = header_pair.split("=", 1)
|
|
118
|
+
key = key.strip()
|
|
119
|
+
value = value.strip()
|
|
120
|
+
if key.lower() == "authorization":
|
|
121
|
+
# Extract API key from "ApiKey <key>" or just return the value
|
|
122
|
+
if value.startswith("ApiKey "):
|
|
123
|
+
return value[7:]
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def format_http_error(response, operation: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Format an HTTP error message from a response object.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
response: Response object with status_code, reason, and text attributes
|
|
135
|
+
operation: Description of the operation that failed (e.g., "fetch dataset")
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Formatted error message string.
|
|
139
|
+
"""
|
|
140
|
+
error_text = response.text if hasattr(response, "text") else "Unknown error"
|
|
141
|
+
status_code = getattr(response, "status_code", getattr(response, "status", "unknown"))
|
|
142
|
+
reason = getattr(response, "reason", "")
|
|
143
|
+
return f"Failed to {operation}: {status_code} {reason} - {error_text}"
|