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.
Files changed (25) hide show
  1. {snowglobe-0.4.0/src/snowglobe.egg-info → snowglobe-0.4.2}/PKG-INFO +3 -10
  2. {snowglobe-0.4.0 → snowglobe-0.4.2}/README.md +2 -9
  3. {snowglobe-0.4.0 → snowglobe-0.4.2}/pyproject.toml +1 -1
  4. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/app.py +348 -152
  5. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/cli.py +114 -41
  6. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/cli_utils.py +155 -9
  7. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/config.py +36 -4
  8. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/utils.py +3 -3
  9. {snowglobe-0.4.0 → snowglobe-0.4.2/src/snowglobe.egg-info}/PKG-INFO +3 -10
  10. {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_cli.py +80 -2
  11. {snowglobe-0.4.0 → snowglobe-0.4.2}/LICENSE +0 -0
  12. {snowglobe-0.4.0 → snowglobe-0.4.2}/setup.cfg +0 -0
  13. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/__init__.py +0 -0
  14. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/models.py +0 -0
  15. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/project_manager.py +0 -0
  16. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe/client/src/stats.py +0 -0
  17. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/SOURCES.txt +0 -0
  18. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/dependency_links.txt +0 -0
  19. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/entry_points.txt +0 -0
  20. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/requires.txt +0 -0
  21. {snowglobe-0.4.0 → snowglobe-0.4.2}/src/snowglobe.egg-info/top_level.txt +0 -0
  22. {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_app.py +0 -0
  23. {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_config.py +0 -0
  24. {snowglobe-0.4.0 → snowglobe-0.4.2}/tests/test_heartbeat.py +0 -0
  25. {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.0
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 -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
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 -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
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 -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
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 -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "snowglobe"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  authors = [
5
5
  {name = "Guardrails AI", email = "contact@guardrailsai.com"}
6
6
  ]
@@ -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": config.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": config.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": config.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
- async with httpx.AsyncClient() as client:
291
- LOGGER.info(f"Polling {len(experiments)} experiments for completions...")
292
- for experiment in experiments:
293
- LOGGER.debug(
294
- f"Checking experiment: id={experiment.get('id')}, name={experiment.get('name', 'unknown')}"
295
- )
296
- for experiment in experiments:
297
- app_id = experiment.get("app_id")
298
- if app_id not in apps:
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"Skipping experiment as we do not have a completion function for app_id: {app_id}"
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
- limit = 10
316
- LOGGER.debug(f"Checking for tests for experiment {experiment_id}")
317
- tests_response = await client.get(
318
- f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment_id}/tests?appId={app_id}&include-risk-evaluations=false&limit={limit}&unprocessed-only=true",
319
- headers={"x-api-key": config.API_KEY},
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
- if not tests_response.is_success:
323
- LOGGER.error(f"Error fetching tests: {tests_response.text}")
324
- continue
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
- tests = tests_response.json()
327
- completion_count = 0
328
- for test in tests:
329
- LOGGER.debug(f"Found test {test['id']} for experiment {experiment_id}")
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
- # if 429 raise and exception and stop this batch
337
- if (
338
- not completion_request.is_success
339
- and completion_request.status_code == 429
340
- ):
341
- LOGGER.warning(
342
- f"Completion Rate limit exceeded for test {test['id']}: {completion_request.text}"
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
- completion_count += 1
349
- queued_tests[test["id"]] = True
350
- if completion_count > 0:
351
- experiment_name = experiment.get("name", "unknown")
352
- if LOGGER.level <= logging.INFO: # Verbose mode
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
- else: # Clean UI mode
357
- timestamp = datetime.datetime.now().strftime("%H:%M:%S")
358
- info(
359
- f"[{timestamp}] Batch complete: {completion_count} responses sent ({experiment_name})"
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
- # Track batch completion
363
- track_batch_completion(experiment_name, completion_count)
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
- connection_test_count = 0
368
- LOGGER.info("Processing application heartbeats...")
369
- for app_id, app_info in apps.items():
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
- LOGGER.info(f"Processed {connection_test_count} heartbeats for applications.")
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
- async def poll_for_risk_evaluations():
386
- """Poll for risk evaluations and process them."""
387
- experiments = await fetch_experiments()
388
- LOGGER.info("Checking for pending risk evaluations...")
389
- LOGGER.debug(f"Found {len(experiments)} experiments with validation in progress")
390
- # experiments = [{"id": "123"}]
391
- async with httpx.AsyncClient() as client:
392
- for experiment in experiments:
393
- experiment_request = await client.get(
394
- f"{config.CONTROL_PLANE_URL}/api/experiments/{experiment['id']}",
395
- headers={"x-api-key": config.API_KEY},
396
- )
397
- if not experiment_request.is_success:
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 fetching experiment {experiment['id']}: {experiment_request.text}"
479
+ f"Error sending heartbeat for application {app_id}: {connection_test_request.text}"
400
480
  )
401
481
  continue
402
- experiment = experiment_request.json()
403
- risk_eval_count = 0
404
- for risk_name in risks.keys():
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
- if (
407
- risk_name
408
- not in experiment.get("source_data", {})
409
- .get("evaluation_configuration", {})
410
- .keys()
411
- ):
412
- LOGGER.debug(
413
- f"Skipping experiment {experiment['id']} as it does not have risk {risk_name}"
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
- LOGGER.debug(
417
- f"checking for tests for experiment {experiment['id']}"
418
- )
419
- tests_response = await client.get(
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
- if not tests_response.is_success:
425
- message = (
426
- tests_response.json().get("message") or tests_response.text
427
- )
428
- raise ValueError(
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
- for test in tests:
435
- test_id = test["id"]
553
+ try:
436
554
  if (
437
- test_id not in queued_evaluations
438
- and test.get("response") is not None
555
+ risk_name
556
+ not in experiment.get("source_data", {})
557
+ .get("evaluation_configuration", {})
558
+ .keys()
439
559
  ):
440
- risk_eval_response = await httpx.AsyncClient().post(
441
- f"{config.SNOWGLOBE_CLIENT_URL}/risk-evaluation",
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
- # if risk evaltion response is 429 raise and exception and bail on this batch
446
- if (
447
- not risk_eval_response.is_success
448
- and risk_eval_response.status_code == 429
449
- ):
450
- LOGGER.error(
451
- f"Rate limit exceeded for risk evaluation {test['id']}: {risk_eval_response.text}"
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
- raise ValueError(
454
- status_code=429,
455
- detail=f"Rate limit exceeded for risk evaluation {test['id']}",
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
- queued_evaluations[test_id] = True
458
- risk_eval_count += 1
459
- except Exception as e:
460
- LOGGER.error(f"Error fetching tests: {e}")
461
- if risk_eval_count > 0:
462
- LOGGER.info(
463
- f"Processed {risk_eval_count} risk evaluations for experiment {experiment.get('name', 'unknown')} ({experiment['id']})"
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
- f"{config.CONTROL_PLANE_URL}/api/failed-code-connection-tests"
798
+ f"{config.CONTROL_PLANE_URL}/api/failed-code-connection-tests"
603
799
  )
604
800
  connection_test_payload = {
605
801
  "appId": app_id,