snowglobe 0.4.0__tar.gz → 0.4.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.
- {snowglobe-0.4.0/src/snowglobe.egg-info → snowglobe-0.4.2}/PKG-INFO +3 -10
- {snowglobe-0.4.0 → snowglobe-0.4.2}/README.md +2 -9
- {snowglobe-0.4.0 → snowglobe-0.4.2}/pyproject.toml +1 -1
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/app.py +348 -152
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/cli.py +114 -41
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/cli_utils.py +155 -9
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/config.py +36 -4
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/utils.py +3 -3
- {snowglobe-0.4.0 → snowglobe-0.4.2/src/snowglobe.egg-info}/PKG-INFO +3 -10
- {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_cli.py +80 -2
- {snowglobe-0.4.0 → snowglobe-0.4.2}/LICENSE +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/setup.cfg +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/__init__.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/models.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/project_manager.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/stats.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/SOURCES.txt +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/dependency_links.txt +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/entry_points.txt +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/requires.txt +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/top_level.txt +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_app.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_config.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_heartbeat.py +0 -0
- {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: snowglobe
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.2
|
4
4
|
Summary: client server for usage with snowglobe experiments
|
5
5
|
Author-email: Guardrails AI <contact@guardrailsai.com>
|
6
6
|
License: MIT License
|
@@ -49,21 +49,14 @@ The Snowglobe Connect SDK helps you connect your AI agents to Snowglobe. It send
|
|
49
49
|
|
50
50
|
## Installation
|
51
51
|
|
52
|
-
```bash
|
53
|
-
# Paste Guardrails Client or extract from guardrailsrc
|
54
|
-
export GUARDRAILS_TOKEN=$(cat ~/.guardrailsrc| awk -F 'token=' '{print $2}' | awk '{print $1}' | tr -d '\n')
|
55
|
-
```
|
56
|
-
|
57
52
|
```
|
58
53
|
# Install client
|
59
|
-
pip install -
|
60
|
-
--extra-index-url="https://pypi.org/simple" snowglobe-connect
|
54
|
+
pip install snowglobe-connect
|
61
55
|
```
|
62
56
|
|
63
57
|
If using uv, set the `--prerelease=allow` flag
|
64
58
|
```
|
65
|
-
pip install
|
66
|
-
--extra-index-url="https://pypi.org/simple" --prerelease=allow snowglobe-connect
|
59
|
+
uv pip install --prerelease=allow snowglobe-connect
|
67
60
|
```
|
68
61
|
|
69
62
|
|
@@ -4,21 +4,14 @@ The Snowglobe Connect SDK helps you connect your AI agents to Snowglobe. It send
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
```bash
|
8
|
-
# Paste Guardrails Client or extract from guardrailsrc
|
9
|
-
export GUARDRAILS_TOKEN=$(cat ~/.guardrailsrc| awk -F 'token=' '{print $2}' | awk '{print $1}' | tr -d '\n')
|
10
|
-
```
|
11
|
-
|
12
7
|
```
|
13
8
|
# Install client
|
14
|
-
pip install -
|
15
|
-
--extra-index-url="https://pypi.org/simple" snowglobe-connect
|
9
|
+
pip install snowglobe-connect
|
16
10
|
```
|
17
11
|
|
18
12
|
If using uv, set the `--prerelease=allow` flag
|
19
13
|
```
|
20
|
-
pip install
|
21
|
-
--extra-index-url="https://pypi.org/simple" --prerelease=allow snowglobe-connect
|
14
|
+
uv pip install --prerelease=allow snowglobe-connect
|
22
15
|
```
|
23
16
|
|
24
17
|
|
@@ -20,8 +20,8 @@ from apscheduler import AsyncScheduler
|
|
20
20
|
from apscheduler.triggers.interval import IntervalTrigger
|
21
21
|
from fastapi import FastAPI, HTTPException, Request
|
22
22
|
|
23
|
-
from .cli_utils import info
|
24
|
-
from .config import config
|
23
|
+
from .cli_utils import info, shutdown_manager
|
24
|
+
from .config import config, get_api_key_or_raise
|
25
25
|
from .models import CompletionFunctionOutputs, CompletionRequest, RiskEvaluationRequest
|
26
26
|
from .stats import initialize_stats, track_batch_completion
|
27
27
|
from .utils import fetch_experiments, fetch_messages
|
@@ -190,7 +190,7 @@ async def process_application_heartbeat(app_id):
|
|
190
190
|
connection_test_response = await client.post(
|
191
191
|
connection_test_url,
|
192
192
|
json=connection_test_payload,
|
193
|
-
headers={"x-api-key":
|
193
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
194
194
|
)
|
195
195
|
|
196
196
|
if not connection_test_response.is_success:
|
@@ -240,7 +240,7 @@ async def process_risk_evaluation(test, risk_name):
|
|
240
240
|
"risk_type": risk_name,
|
241
241
|
"risk_triggered": risk_evaluation.triggered,
|
242
242
|
},
|
243
|
-
headers={"x-api-key":
|
243
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
244
244
|
)
|
245
245
|
LOGGER.debug(f"Time taken: {time.time() - start} seconds")
|
246
246
|
if not risk_evaluation_response.is_success:
|
@@ -271,7 +271,7 @@ async def process_test(test, completion_fn, app_id):
|
|
271
271
|
"response": completionOutput.response,
|
272
272
|
"persona": test["persona"],
|
273
273
|
},
|
274
|
-
headers={"x-api-key":
|
274
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
275
275
|
)
|
276
276
|
LOGGER.debug(f"Time taken: {time.time() - start} seconds")
|
277
277
|
# remove test['id'] from queued_tests
|
@@ -282,186 +282,382 @@ async def process_test(test, completion_fn, app_id):
|
|
282
282
|
|
283
283
|
# The task to run
|
284
284
|
async def poll_for_completions():
|
285
|
+
# Exit early if shutdown requested
|
286
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
287
|
+
return
|
288
|
+
|
289
|
+
job_id = "poll_for_completions"
|
290
|
+
|
285
291
|
if len(apps) == 0:
|
286
292
|
LOGGER.warning("No applications found. Skipping completions check.")
|
287
293
|
return
|
288
|
-
experiments = await fetch_experiments()
|
289
294
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
295
|
+
# Register this job as active
|
296
|
+
shutdown_manager.register_active_job(job_id)
|
297
|
+
|
298
|
+
try:
|
299
|
+
experiments = await fetch_experiments()
|
300
|
+
except Exception as e:
|
301
|
+
# Handle shutdown-related connection errors gracefully
|
302
|
+
if shutdown_manager.is_shutdown_requested():
|
303
|
+
LOGGER.debug(f"Connection error during shutdown (expected): {e}")
|
304
|
+
return
|
305
|
+
else:
|
306
|
+
LOGGER.error(f"Error fetching experiments: {e}")
|
307
|
+
raise
|
308
|
+
finally:
|
309
|
+
shutdown_manager.unregister_active_job(job_id)
|
310
|
+
|
311
|
+
try:
|
312
|
+
async with httpx.AsyncClient() as client:
|
313
|
+
LOGGER.info(f"Polling {len(experiments)} experiments for completions...")
|
314
|
+
for experiment in experiments:
|
299
315
|
LOGGER.debug(
|
300
|
-
f"
|
301
|
-
)
|
302
|
-
continue
|
303
|
-
experiment_id = experiment["id"]
|
304
|
-
experiment_request = await client.get(
|
305
|
-
f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment_id}?appId={app_id}",
|
306
|
-
headers={"x-api-key": config.API_KEY},
|
307
|
-
)
|
308
|
-
if not experiment_request.is_success:
|
309
|
-
LOGGER.error(
|
310
|
-
f"Error fetching experiment {experiment_id}: {experiment_request.text}"
|
316
|
+
f"Checking experiment: id={experiment.get('id')}, name={experiment.get('name', 'unknown')}"
|
311
317
|
)
|
312
|
-
continue
|
313
|
-
experiment = experiment_request.json()
|
314
318
|
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
)
|
319
|
+
for experiment in experiments:
|
320
|
+
# Check for shutdown between each experiment
|
321
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
322
|
+
LOGGER.debug("Shutdown requested, stopping completions polling")
|
323
|
+
return
|
321
324
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
+
app_id = experiment.get("app_id")
|
326
|
+
if app_id not in apps:
|
327
|
+
LOGGER.debug(
|
328
|
+
f"Skipping experiment as we do not have a completion function for app_id: {app_id}"
|
329
|
+
)
|
330
|
+
continue
|
331
|
+
experiment_id = experiment["id"]
|
325
332
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
if not test["response"] and test["id"] not in queued_tests:
|
331
|
-
completion_request = await httpx.AsyncClient().post(
|
332
|
-
f"{config.SNOWGLOBE_CLIENT_URL}/completion",
|
333
|
-
json={"test": test, "app_id": app_id},
|
334
|
-
timeout=30,
|
333
|
+
try:
|
334
|
+
experiment_request = await client.get(
|
335
|
+
f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment_id}?appId={app_id}",
|
336
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
335
337
|
)
|
336
|
-
|
337
|
-
if (
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
LOGGER.
|
342
|
-
f"
|
343
|
-
)
|
344
|
-
raise ValueError(
|
345
|
-
status_code=429,
|
346
|
-
detail=f"Rate limit exceeded for test {test['id']}",
|
338
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
339
|
+
if shutdown_manager.is_shutdown_requested():
|
340
|
+
LOGGER.debug(f"HTTP error during shutdown (expected): {e}")
|
341
|
+
return
|
342
|
+
else:
|
343
|
+
LOGGER.error(
|
344
|
+
f"Connection error fetching experiment {experiment_id}: {e}"
|
347
345
|
)
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
LOGGER.info(
|
354
|
-
f"Processed {completion_count} completions for experiment {experiment_name} ({experiment_id})"
|
346
|
+
continue
|
347
|
+
|
348
|
+
if not experiment_request.is_success:
|
349
|
+
LOGGER.error(
|
350
|
+
f"Error fetching experiment {experiment_id}: {experiment_request.text}"
|
355
351
|
)
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
352
|
+
continue
|
353
|
+
experiment = experiment_request.json()
|
354
|
+
|
355
|
+
limit = 10
|
356
|
+
LOGGER.debug(f"Checking for tests for experiment {experiment_id}")
|
357
|
+
|
358
|
+
try:
|
359
|
+
tests_response = await client.get(
|
360
|
+
f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment_id}/tests?appId={app_id}&include-risk-evaluations=false&limit={limit}&unprocessed-only=true",
|
361
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
360
362
|
)
|
363
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
364
|
+
if shutdown_manager.is_shutdown_requested():
|
365
|
+
LOGGER.debug(f"HTTP error during shutdown (expected): {e}")
|
366
|
+
return
|
367
|
+
else:
|
368
|
+
LOGGER.error(f"Connection error fetching tests: {e}")
|
369
|
+
continue
|
370
|
+
|
371
|
+
if not tests_response.is_success:
|
372
|
+
LOGGER.error(f"Error fetching tests: {tests_response.text}")
|
373
|
+
continue
|
361
374
|
|
362
|
-
|
363
|
-
|
375
|
+
tests = tests_response.json()
|
376
|
+
completion_count = 0
|
377
|
+
for test in tests:
|
378
|
+
# Check for shutdown before processing each test
|
379
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
380
|
+
LOGGER.debug("Shutdown requested, stopping test processing")
|
381
|
+
return
|
382
|
+
|
383
|
+
LOGGER.debug(
|
384
|
+
f"Found test {test['id']} for experiment {experiment_id}"
|
385
|
+
)
|
386
|
+
if not test["response"] and test["id"] not in queued_tests:
|
387
|
+
try:
|
388
|
+
completion_request = await httpx.AsyncClient().post(
|
389
|
+
f"{config.SNOWGLOBE_CLIENT_URL}/completion",
|
390
|
+
json={"test": test, "app_id": app_id},
|
391
|
+
timeout=30,
|
392
|
+
)
|
393
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
394
|
+
if shutdown_manager.is_shutdown_requested():
|
395
|
+
LOGGER.debug(
|
396
|
+
f"HTTP error during shutdown (expected): {e}"
|
397
|
+
)
|
398
|
+
return
|
399
|
+
else:
|
400
|
+
LOGGER.error(
|
401
|
+
f"Connection error posting completion: {e}"
|
402
|
+
)
|
403
|
+
continue
|
404
|
+
|
405
|
+
# if 429 raise and exception and stop this batch
|
406
|
+
if (
|
407
|
+
not completion_request.is_success
|
408
|
+
and completion_request.status_code == 429
|
409
|
+
):
|
410
|
+
LOGGER.warning(
|
411
|
+
f"Completion Rate limit exceeded for test {test['id']}: {completion_request.text}"
|
412
|
+
)
|
413
|
+
raise ValueError(
|
414
|
+
status_code=429,
|
415
|
+
detail=f"Rate limit exceeded for test {test['id']}",
|
416
|
+
)
|
417
|
+
completion_count += 1
|
418
|
+
queued_tests[test["id"]] = True
|
419
|
+
|
420
|
+
if completion_count > 0:
|
421
|
+
experiment_name = experiment.get("name", "unknown")
|
422
|
+
if LOGGER.level <= logging.INFO: # Verbose mode
|
423
|
+
LOGGER.info(
|
424
|
+
f"Processed {completion_count} completions for experiment {experiment_name} ({experiment_id})"
|
425
|
+
)
|
426
|
+
else: # Clean UI mode
|
427
|
+
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
428
|
+
info(
|
429
|
+
f"[{timestamp}] ✓ Batch complete: {completion_count} responses sent ({experiment_name})"
|
430
|
+
)
|
431
|
+
|
432
|
+
# Track batch completion
|
433
|
+
track_batch_completion(experiment_name, completion_count)
|
434
|
+
|
435
|
+
except Exception as e:
|
436
|
+
# Handle any other errors
|
437
|
+
if shutdown_manager.is_shutdown_requested():
|
438
|
+
LOGGER.debug(f"Error during shutdown (expected): {e}")
|
439
|
+
else:
|
440
|
+
LOGGER.error(f"Unexpected error in poll_for_completions: {e}")
|
441
|
+
raise
|
364
442
|
|
365
443
|
|
366
444
|
async def process_application_heartbeats():
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
connection_test_request = await httpx.AsyncClient().post(
|
371
|
-
f"{config.SNOWGLOBE_CLIENT_URL}/heartbeat",
|
372
|
-
json={"app_id": app_id},
|
373
|
-
timeout=30,
|
374
|
-
)
|
375
|
-
if not connection_test_request.is_success:
|
376
|
-
LOGGER.error(
|
377
|
-
f"Error sending heartbeat for application {app_id}: {connection_test_request.text}"
|
378
|
-
)
|
379
|
-
continue
|
380
|
-
connection_test_count += 1
|
445
|
+
# Exit early if shutdown requested
|
446
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
447
|
+
return
|
381
448
|
|
382
|
-
|
449
|
+
job_id = "process_application_heartbeats"
|
450
|
+
shutdown_manager.register_active_job(job_id)
|
383
451
|
|
452
|
+
try:
|
453
|
+
connection_test_count = 0
|
454
|
+
LOGGER.info("Processing application heartbeats...")
|
455
|
+
for app_id, app_info in apps.items():
|
456
|
+
# Check for shutdown between each heartbeat
|
457
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
458
|
+
LOGGER.debug("Shutdown requested, stopping heartbeats")
|
459
|
+
return
|
384
460
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
461
|
+
try:
|
462
|
+
connection_test_request = await httpx.AsyncClient().post(
|
463
|
+
f"{config.SNOWGLOBE_CLIENT_URL}/heartbeat",
|
464
|
+
json={"app_id": app_id},
|
465
|
+
timeout=30,
|
466
|
+
)
|
467
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
468
|
+
if shutdown_manager.is_shutdown_requested():
|
469
|
+
LOGGER.debug(f"HTTP error during shutdown (expected): {e}")
|
470
|
+
return
|
471
|
+
else:
|
472
|
+
LOGGER.error(
|
473
|
+
f"Connection error sending heartbeat for {app_id}: {e}"
|
474
|
+
)
|
475
|
+
continue
|
476
|
+
|
477
|
+
if not connection_test_request.is_success:
|
398
478
|
LOGGER.error(
|
399
|
-
f"Error
|
479
|
+
f"Error sending heartbeat for application {app_id}: {connection_test_request.text}"
|
400
480
|
)
|
401
481
|
continue
|
402
|
-
|
403
|
-
|
404
|
-
|
482
|
+
connection_test_count += 1
|
483
|
+
|
484
|
+
LOGGER.info(f"Processed {connection_test_count} heartbeats for applications.")
|
485
|
+
finally:
|
486
|
+
shutdown_manager.unregister_active_job(job_id)
|
487
|
+
|
488
|
+
|
489
|
+
async def poll_for_risk_evaluations():
|
490
|
+
"""Poll for risk evaluations and process them."""
|
491
|
+
# Exit early if shutdown requested
|
492
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
493
|
+
return
|
494
|
+
|
495
|
+
job_id = "poll_for_risk_evaluations"
|
496
|
+
shutdown_manager.register_active_job(job_id)
|
497
|
+
|
498
|
+
try:
|
499
|
+
experiments = await fetch_experiments()
|
500
|
+
except Exception as e:
|
501
|
+
# Handle shutdown-related connection errors gracefully
|
502
|
+
if shutdown_manager.is_shutdown_requested():
|
503
|
+
LOGGER.debug(f"Connection error during shutdown (expected): {e}")
|
504
|
+
return
|
505
|
+
else:
|
506
|
+
LOGGER.error(f"Error fetching experiments: {e}")
|
507
|
+
raise
|
508
|
+
finally:
|
509
|
+
shutdown_manager.unregister_active_job(job_id)
|
510
|
+
|
511
|
+
try:
|
512
|
+
LOGGER.info("Checking for pending risk evaluations...")
|
513
|
+
LOGGER.debug(
|
514
|
+
f"Found {len(experiments)} experiments with validation in progress"
|
515
|
+
)
|
516
|
+
|
517
|
+
async with httpx.AsyncClient() as client:
|
518
|
+
for experiment in experiments:
|
519
|
+
# Check for shutdown between experiments
|
520
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
521
|
+
LOGGER.debug("Shutdown requested, stopping risk evaluations")
|
522
|
+
return
|
523
|
+
|
405
524
|
try:
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
):
|
412
|
-
LOGGER.debug(
|
413
|
-
|
525
|
+
experiment_request = await client.get(
|
526
|
+
f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment['id']}",
|
527
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
528
|
+
)
|
529
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
530
|
+
if shutdown_manager.is_shutdown_requested():
|
531
|
+
LOGGER.debug(f"HTTP error during shutdown (expected): {e}")
|
532
|
+
return
|
533
|
+
else:
|
534
|
+
LOGGER.error(
|
535
|
+
f"Connection error fetching experiment {experiment['id']}: {e}"
|
414
536
|
)
|
415
537
|
continue
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment['id']}/tests?unevaluated-risk={quote_plus(risk_name)}&include-risk-evaluations=true",
|
421
|
-
headers={"x-api-key": config.API_KEY},
|
538
|
+
|
539
|
+
if not experiment_request.is_success:
|
540
|
+
LOGGER.error(
|
541
|
+
f"Error fetching experiment {experiment['id']}: {experiment_request.text}"
|
422
542
|
)
|
543
|
+
continue
|
544
|
+
experiment = experiment_request.json()
|
545
|
+
risk_eval_count = 0
|
423
546
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
)
|
428
|
-
|
429
|
-
status_code=tests_response.status_code,
|
430
|
-
message=message,
|
431
|
-
)
|
432
|
-
tests = tests_response.json()
|
547
|
+
for risk_name in risks.keys():
|
548
|
+
# Check for shutdown before processing each risk
|
549
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
550
|
+
LOGGER.debug("Shutdown requested, stopping risk processing")
|
551
|
+
return
|
433
552
|
|
434
|
-
|
435
|
-
test_id = test["id"]
|
553
|
+
try:
|
436
554
|
if (
|
437
|
-
|
438
|
-
|
555
|
+
risk_name
|
556
|
+
not in experiment.get("source_data", {})
|
557
|
+
.get("evaluation_configuration", {})
|
558
|
+
.keys()
|
439
559
|
):
|
440
|
-
|
441
|
-
f"{
|
442
|
-
json={"test": test, "risk_name": risk_name},
|
443
|
-
timeout=30,
|
560
|
+
LOGGER.debug(
|
561
|
+
f"Skipping experiment {experiment['id']} as it does not have risk {risk_name}"
|
444
562
|
)
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
563
|
+
continue
|
564
|
+
LOGGER.debug(
|
565
|
+
f"checking for tests for experiment {experiment['id']}"
|
566
|
+
)
|
567
|
+
|
568
|
+
try:
|
569
|
+
tests_response = await client.get(
|
570
|
+
f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment['id']}/tests?unevaluated-risk={quote_plus(risk_name)}&include-risk-evaluations=true",
|
571
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
572
|
+
)
|
573
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
574
|
+
if shutdown_manager.is_shutdown_requested():
|
575
|
+
LOGGER.debug(
|
576
|
+
f"HTTP error during shutdown (expected): {e}"
|
452
577
|
)
|
453
|
-
|
454
|
-
|
455
|
-
|
578
|
+
return
|
579
|
+
else:
|
580
|
+
LOGGER.error(f"Connection error fetching tests: {e}")
|
581
|
+
continue
|
582
|
+
|
583
|
+
if not tests_response.is_success:
|
584
|
+
message = (
|
585
|
+
tests_response.json().get("message")
|
586
|
+
or tests_response.text
|
587
|
+
)
|
588
|
+
raise ValueError(
|
589
|
+
status_code=tests_response.status_code,
|
590
|
+
message=message,
|
591
|
+
)
|
592
|
+
tests = tests_response.json()
|
593
|
+
|
594
|
+
for test in tests:
|
595
|
+
# Check for shutdown before processing each test
|
596
|
+
if await shutdown_manager.is_async_shutdown_requested():
|
597
|
+
LOGGER.debug(
|
598
|
+
"Shutdown requested, stopping test evaluation"
|
456
599
|
)
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
600
|
+
return
|
601
|
+
|
602
|
+
test_id = test["id"]
|
603
|
+
if (
|
604
|
+
test_id not in queued_evaluations
|
605
|
+
and test.get("response") is not None
|
606
|
+
):
|
607
|
+
try:
|
608
|
+
risk_eval_response = await httpx.AsyncClient().post(
|
609
|
+
f"{config.SNOWGLOBE_CLIENT_URL}/risk-evaluation",
|
610
|
+
json={"test": test, "risk_name": risk_name},
|
611
|
+
timeout=30,
|
612
|
+
)
|
613
|
+
except (
|
614
|
+
httpx.ConnectError,
|
615
|
+
httpx.TimeoutException,
|
616
|
+
) as e:
|
617
|
+
if shutdown_manager.is_shutdown_requested():
|
618
|
+
LOGGER.debug(
|
619
|
+
f"HTTP error during shutdown (expected): {e}"
|
620
|
+
)
|
621
|
+
return
|
622
|
+
else:
|
623
|
+
LOGGER.error(
|
624
|
+
f"Connection error posting risk evaluation: {e}"
|
625
|
+
)
|
626
|
+
continue
|
627
|
+
|
628
|
+
# if risk evaltion response is 429 raise and exception and bail on this batch
|
629
|
+
if (
|
630
|
+
not risk_eval_response.is_success
|
631
|
+
and risk_eval_response.status_code == 429
|
632
|
+
):
|
633
|
+
LOGGER.error(
|
634
|
+
f"Rate limit exceeded for risk evaluation {test['id']}: {risk_eval_response.text}"
|
635
|
+
)
|
636
|
+
raise ValueError(
|
637
|
+
status_code=429,
|
638
|
+
detail=f"Rate limit exceeded for risk evaluation {test['id']}",
|
639
|
+
)
|
640
|
+
queued_evaluations[test_id] = True
|
641
|
+
risk_eval_count += 1
|
642
|
+
except Exception as e:
|
643
|
+
if shutdown_manager.is_shutdown_requested():
|
644
|
+
LOGGER.debug(f"Error during shutdown (expected): {e}")
|
645
|
+
return
|
646
|
+
else:
|
647
|
+
LOGGER.error(f"Error fetching tests: {e}")
|
648
|
+
|
649
|
+
if risk_eval_count > 0:
|
650
|
+
LOGGER.info(
|
651
|
+
f"Processed {risk_eval_count} risk evaluations for experiment {experiment.get('name', 'unknown')} ({experiment['id']})"
|
652
|
+
)
|
653
|
+
|
654
|
+
except Exception as e:
|
655
|
+
# Handle any other errors
|
656
|
+
if shutdown_manager.is_shutdown_requested():
|
657
|
+
LOGGER.debug(f"Error during shutdown (expected): {e}")
|
658
|
+
else:
|
659
|
+
LOGGER.error(f"Unexpected error in poll_for_risk_evaluations: {e}")
|
660
|
+
raise
|
465
661
|
|
466
662
|
|
467
663
|
# Ensure the scheduler shuts down properly on application exit.
|
@@ -599,7 +795,7 @@ async def lifespan(app: FastAPI):
|
|
599
795
|
for app_id in apps.keys():
|
600
796
|
try:
|
601
797
|
connection_test_url = (
|
602
|
-
|
798
|
+
f"{config.CONTROL_PLANE_URL}/api/failed-code-connection-tests"
|
603
799
|
)
|
604
800
|
connection_test_payload = {
|
605
801
|
"appId": app_id,
|