hirundo 0.3.2__tar.gz → 0.3.3__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 (47) hide show
  1. {hirundo-0.3.2 → hirundo-0.3.3}/PKG-INFO +1 -1
  2. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/__init__.py +3 -1
  3. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/dataset_qa.py +22 -5
  4. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/external_eval.py +9 -0
  5. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/llm_behavior_eval.py +29 -1
  6. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/unlearning_llm.py +18 -9
  7. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo.egg-info/PKG-INFO +1 -1
  8. {hirundo-0.3.2 → hirundo-0.3.3}/pyproject.toml +2 -2
  9. {hirundo-0.3.2 → hirundo-0.3.3}/tests/test_external_eval.py +60 -0
  10. {hirundo-0.3.2 → hirundo-0.3.3}/tests/test_llm_behavior_eval.py +18 -1
  11. {hirundo-0.3.2 → hirundo-0.3.3}/LICENSE +0 -0
  12. {hirundo-0.3.2 → hirundo-0.3.3}/README.md +0 -0
  13. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/__main__.py +0 -0
  14. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_column_options.py +0 -0
  15. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_constraints.py +0 -0
  16. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_dataframe.py +0 -0
  17. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_env.py +0 -0
  18. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_headers.py +0 -0
  19. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_hirundo_error.py +0 -0
  20. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_http.py +0 -0
  21. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_iter_sse_retrying.py +0 -0
  22. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_llm_pipeline.py +0 -0
  23. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_llm_sources.py +0 -0
  24. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_run_checking.py +0 -0
  25. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_run_status.py +0 -0
  26. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_sse_event_data.py +0 -0
  27. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_timeouts.py +0 -0
  28. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/_urls.py +0 -0
  29. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/cli.py +0 -0
  30. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/dataset_enum.py +0 -0
  31. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/dataset_qa_results.py +0 -0
  32. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/git.py +0 -0
  33. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/labeling.py +0 -0
  34. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/llm_behavior_eval_results.py +0 -0
  35. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/llm_bias_type.py +0 -0
  36. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/logger.py +0 -0
  37. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/storage.py +0 -0
  38. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo/unzip.py +0 -0
  39. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo.egg-info/SOURCES.txt +0 -0
  40. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo.egg-info/dependency_links.txt +0 -0
  41. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo.egg-info/entry_points.txt +0 -0
  42. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo.egg-info/requires.txt +0 -0
  43. {hirundo-0.3.2 → hirundo-0.3.3}/hirundo.egg-info/top_level.txt +0 -0
  44. {hirundo-0.3.2 → hirundo-0.3.3}/setup.cfg +0 -0
  45. {hirundo-0.3.2 → hirundo-0.3.3}/tests/test_llm_behavior_eval_presets.py +0 -0
  46. {hirundo-0.3.2 → hirundo-0.3.3}/tests/test_unzip.py +0 -0
  47. {hirundo-0.3.2 → hirundo-0.3.3}/tests/testing_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hirundo
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: The Hirundo platform provides APIs for: LLM behavior unlearning & dataset QA.
5
5
  Author-email: Hirundo <dev@hirundo.io>
6
6
  License: MIT License
@@ -67,6 +67,7 @@ from .storage import (
67
67
  StorageS3,
68
68
  )
69
69
  from .unlearning_llm import (
70
+ Aggressiveness,
70
71
  BiasBehavior,
71
72
  CustomBehavior,
72
73
  HallucinationBehavior,
@@ -131,6 +132,7 @@ __all__ = [
131
132
  "StorageGit",
132
133
  "StorageConfig",
133
134
  "DatasetQAResults",
135
+ "Aggressiveness",
134
136
  "BiasBehavior",
135
137
  "CustomBehavior",
136
138
  "HallucinationBehavior",
@@ -152,4 +154,4 @@ __all__ = [
152
154
  ]
153
155
 
154
156
  # Keep this literal in sync with pyproject.toml; release workflows read it.
155
- __version__ = "0.3.2"
157
+ __version__ = "0.3.3"
@@ -326,7 +326,7 @@ class QADataset(BaseModel):
326
326
  if self.storage_config == StorageTypes.LOCAL:
327
327
  return False
328
328
  if (
329
- isinstance(self.storage_config, (StorageConfig, ResponseStorageConfig))
329
+ isinstance(self.storage_config, StorageConfig | ResponseStorageConfig)
330
330
  and self.storage_config.type == StorageTypes.LOCAL
331
331
  ):
332
332
  return False
@@ -608,13 +608,26 @@ class QADataset(BaseModel):
608
608
 
609
609
  Args:
610
610
  dataset_id: The ID of the dataset to run QA on.
611
+ organization_id: The organization ID to use for this run. When set,
612
+ it overrides the default organization selection.
613
+ run_args: Optional QA run arguments. When omitted, the SDK loads
614
+ the dataset type and sends default classification arguments for
615
+ non-speech datasets, but omits `run_args` for speech-to-text
616
+ datasets. When supplied, the SDK serializes them into the
617
+ payload. Speech-to-text runs must not supply run arguments.
611
618
 
612
619
  Returns:
613
620
  ID of the run (`run_id`).
614
621
  """
615
- run_info: dict[str, typing.Any] = {
616
- "run_args": run_args.model_dump(mode="json") if run_args else {},
617
- }
622
+ dataset = QADataset.get_by_id(dataset_id)
623
+ if run_args is None:
624
+ if dataset.labeling_type != LabelingType.SPEECH_TO_TEXT:
625
+ run_args = ClassificationRunArgs()
626
+ else:
627
+ dataset._validate_run_args(run_args)
628
+ run_info: dict[str, typing.Any] = {}
629
+ if run_args is not None:
630
+ run_info["run_args"] = run_args.model_dump(mode="json")
618
631
  if organization_id is not None:
619
632
  run_info["organization_id"] = organization_id
620
633
  run_response = requests.post(
@@ -661,7 +674,9 @@ class QADataset(BaseModel):
661
674
  organization_id: The ID of the organization to run the QA for.
662
675
  replace_dataset_if_exists: If True, the dataset will be replaced if it already exists
663
676
  (this is determined by a dataset of the same name in the same organization).
664
- run_args: The run arguments to use for the QA run
677
+ run_args: The run arguments to use for the QA run. When omitted,
678
+ non-speech datasets use default classification arguments, while
679
+ speech-to-text datasets omit run arguments.
665
680
 
666
681
  Returns:
667
682
  An ID of the run (`run_id`) and stores that `run_id` on the instance
@@ -672,6 +687,8 @@ class QADataset(BaseModel):
672
687
  organization_id=organization_id,
673
688
  replace_if_exists=replace_dataset_if_exists,
674
689
  )
690
+ if run_args is None and self.labeling_type != LabelingType.SPEECH_TO_TEXT:
691
+ run_args = ClassificationRunArgs()
675
692
  if run_args is not None:
676
693
  self._validate_run_args(run_args)
677
694
  run_id = self.launch_qa_run(self.id, organization_id, run_args)
@@ -46,6 +46,15 @@ class ExternalEvalRunInfo(BaseModel):
46
46
  source_run_id: str | None = None
47
47
  task_ids: list[CanonicalTaskReference] = Field(min_length=1)
48
48
  sample_limit: int | None = Field(default=None, gt=0)
49
+ max_model_len: int | None = Field(
50
+ default=None,
51
+ gt=0,
52
+ strict=True,
53
+ description=(
54
+ "Maximum model context length for the evaluation. "
55
+ "Omit or set None to use the model's context length."
56
+ ),
57
+ )
49
58
 
50
59
  @model_validator(mode="after")
51
60
  def _validate_unique_task_ids(self) -> "ExternalEvalRunInfo":
@@ -7,7 +7,7 @@ from enum import Enum
7
7
  from typing import overload
8
8
 
9
9
  import httpx
10
- from pydantic import BaseModel, ConfigDict, model_validator
10
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
11
11
  from tqdm import tqdm
12
12
  from tqdm.contrib.logging import logging_redirect_tqdm
13
13
 
@@ -153,6 +153,31 @@ class EvalRunRecord(BaseModel):
153
153
  bias_type: BBQBiasType | UnqoverBiasType | None = None
154
154
  task_ids: list[str] | None = None
155
155
  sample_limit: int | None = None
156
+ attempt_timeout: int | None = Field(
157
+ default=None,
158
+ description=(
159
+ "Server-side Inspect timeout, in seconds, for each model generation "
160
+ "attempt. None for non-Inspect runs or runs created before this "
161
+ "setting was recorded."
162
+ ),
163
+ )
164
+ max_retries: int | None = Field(
165
+ default=None,
166
+ description=(
167
+ "Server-side Inspect retry limit for failed model generations. "
168
+ "Unrelated to the SDK's stream reconnection `max_retries` argument "
169
+ "on `check_run_by_id`. None for non-Inspect runs or runs created "
170
+ "before this setting was recorded."
171
+ ),
172
+ )
173
+ max_model_len: int | None = Field(
174
+ default=None,
175
+ description=(
176
+ "Maximum model context length used by the Inspect evaluation. "
177
+ "None when the model's context length was used, for non-Inspect "
178
+ "runs, or for runs created before this setting was recorded."
179
+ ),
180
+ )
156
181
  judge_model: JudgeModel | None = None
157
182
  run_id: str
158
183
  mlflow_run_id: str | None
@@ -218,6 +243,9 @@ class LlmBehaviorEval:
218
243
  bias_type=response_payload.get("bias_type"),
219
244
  task_ids=response_payload.get("task_ids"),
220
245
  sample_limit=response_payload.get("sample_limit"),
246
+ attempt_timeout=response_payload.get("attempt_timeout"),
247
+ max_retries=response_payload.get("max_retries"),
248
+ max_model_len=response_payload.get("max_model_len"),
221
249
  judge_model=judge_model,
222
250
  run_id=response_payload["run_id"],
223
251
  mlflow_run_id=response_payload.get("mlflow_run_id"),
@@ -319,6 +319,8 @@ OutputBehaviorOptions = (
319
319
  | CustomBehavior
320
320
  )
321
321
 
322
+ Aggressiveness = Annotated[float, Field(gt=0, le=1, multiple_of=0.001)]
323
+
322
324
 
323
325
  class LlmRunInfo(BaseModel):
324
326
  model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump"))
@@ -328,10 +330,11 @@ class LlmRunInfo(BaseModel):
328
330
  target_behaviors: list[TargetBehavior]
329
331
  target_utilities: list[CustomUtility] = Field(default_factory=list)
330
332
  advanced_options: UnlearningLlmAdvancedOptions | None = None
333
+ aggressiveness: Aggressiveness | None = None
331
334
 
332
335
  @model_validator(mode="after")
333
336
  def validate_refusal_utilities(self) -> "LlmRunInfo":
334
- """Validate utility targets are compatible with the selected behaviors.
337
+ """Validate run options are compatible with the selected behaviors.
335
338
 
336
339
  Args:
337
340
  self: The `LlmRunInfo` instance to validate.
@@ -340,8 +343,8 @@ class LlmRunInfo(BaseModel):
340
343
  The validated `LlmRunInfo` instance.
341
344
 
342
345
  Raises:
343
- ValueError: If refusal behavior is combined with non-empty target
344
- utilities.
346
+ ValueError: If refusal behavior is combined with an unsupported
347
+ option.
345
348
  """
346
349
  has_refusal = any(
347
350
  isinstance(target_behavior, RefusalBehavior)
@@ -349,6 +352,8 @@ class LlmRunInfo(BaseModel):
349
352
  )
350
353
  if has_refusal and self.target_utilities:
351
354
  raise ValueError("Refusal behavior does not support target utilities")
355
+ if has_refusal and self.aggressiveness is not None:
356
+ raise ValueError("Refusal behavior does not support aggressiveness")
352
357
  return self
353
358
 
354
359
 
@@ -381,6 +386,7 @@ class OutputUnlearningLlmRun(BaseModel):
381
386
  target_behaviors: list[OutputBehaviorOptions]
382
387
  target_utilities: list[CustomUtility]
383
388
  advanced_options: UnlearningLlmAdvancedOptions | None
389
+ aggressiveness: float | None = None
384
390
  run_id: str
385
391
  mlflow_run_id: str | None
386
392
  status: CeleryTaskState
@@ -407,13 +413,16 @@ class LlmUnlearningRun:
407
413
  run_info: The `LlmRunInfo` request model to serialize.
408
414
 
409
415
  Returns:
410
- A JSON-serializable payload derived from
411
- `run_info.model_dump(mode="json")`. Bias targets include the
412
- backend-only `bias_type` field set to `BBQBiasType.ALL.value`.
413
- Refusal targets retain their validated empty `target_utilities`
414
- field for the API request schema.
416
+ A JSON-serializable payload derived from a freshly validated copy
417
+ of `run_info`. Bias targets include the backend-only `bias_type`
418
+ field set to `BBQBiasType.ALL.value`. Refusal targets retain their
419
+ validated empty `target_utilities` field for the API request
420
+ schema.
415
421
  """
416
- payload = run_info.model_dump(mode="json")
422
+ validated_run_info = LlmRunInfo.model_validate(run_info.model_dump())
423
+ payload = validated_run_info.model_dump(mode="json")
424
+ if validated_run_info.aggressiveness is None:
425
+ payload.pop("aggressiveness")
417
426
  for target_behavior in payload["target_behaviors"]:
418
427
  if target_behavior["type"] == "BIAS":
419
428
  target_behavior["bias_type"] = BBQBiasType.ALL.value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hirundo
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: The Hirundo platform provides APIs for: LLM behavior unlearning & dataset QA.
5
5
  Author-email: Hirundo <dev@hirundo.io>
6
6
  License: MIT License
@@ -7,7 +7,7 @@ packages = ["hirundo"]
7
7
 
8
8
  [project]
9
9
  name = "hirundo"
10
- version = "0.3.2"
10
+ version = "0.3.3"
11
11
  description = "The Hirundo platform provides APIs for: LLM behavior unlearning & dataset QA."
12
12
  authors = [{ name = "Hirundo", email = "dev@hirundo.io" }]
13
13
  readme = "README.md"
@@ -121,7 +121,7 @@ docs = [
121
121
  ]
122
122
 
123
123
  [tool.bumpver]
124
- current_version = "0.3.2"
124
+ current_version = "0.3.3"
125
125
  version_pattern = "MAJOR.MINOR.PATCH[.PYTAGNUM]"
126
126
 
127
127
  [tool.bumpver.file_patterns]
@@ -30,8 +30,10 @@ class _Response:
30
30
  return None
31
31
 
32
32
 
33
+ @pytest.mark.parametrize("max_model_len", [None, 32768])
33
34
  def test_launch_external_eval_run_serializes_request(
34
35
  monkeypatch: pytest.MonkeyPatch,
36
+ max_model_len: int | None,
35
37
  ) -> None:
36
38
  captured_request: dict[str, Any] = {}
37
39
 
@@ -49,6 +51,7 @@ def test_launch_external_eval_run_serializes_request(
49
51
  name="Inspect AIME",
50
52
  source_run_id="unlearning-run-id",
51
53
  task_ids=["inspect_evals/aime25"],
54
+ max_model_len=max_model_len,
52
55
  ),
53
56
  )
54
57
 
@@ -59,9 +62,66 @@ def test_launch_external_eval_run_serializes_request(
59
62
  "name": "Inspect AIME",
60
63
  "source_run_id": "unlearning-run-id",
61
64
  "task_ids": ["inspect_evals/aime25"],
65
+ **({"max_model_len": max_model_len} if max_model_len is not None else {}),
62
66
  }
63
67
 
64
68
 
69
+ def test_launch_model_external_eval_run_serializes_context_cap(
70
+ monkeypatch: pytest.MonkeyPatch,
71
+ ) -> None:
72
+ captured_request: dict[str, Any] = {}
73
+
74
+ def fake_post(*args: Any, **kwargs: Any) -> _Response:
75
+ captured_request["url"] = args[0]
76
+ captured_request["json"] = kwargs["json"]
77
+ return _Response({"message": "Run launched", "run_id": "inspect-run-id"})
78
+
79
+ monkeypatch.setattr("hirundo.external_eval.get_headers", lambda: {})
80
+ monkeypatch.setattr("hirundo.external_eval.requests.post", fake_post)
81
+
82
+ ExternalEval.launch_eval_run(
83
+ ModelOrRun.MODEL,
84
+ ExternalEvalRunInfo(
85
+ model_id=123,
86
+ task_ids=["inspect_evals/aime25"],
87
+ max_model_len=4096,
88
+ ),
89
+ )
90
+
91
+ assert captured_request["url"].endswith("/external-evals/run/model")
92
+ assert captured_request["json"] == {
93
+ "model_id": 123,
94
+ "task_ids": ["inspect_evals/aime25"],
95
+ "max_model_len": 4096,
96
+ }
97
+
98
+
99
+ @pytest.mark.parametrize("max_model_len", [0, -1, 1.5, 1.0, True, "4096"])
100
+ def test_external_eval_run_info_rejects_invalid_context_cap(
101
+ max_model_len: int | float | bool | str,
102
+ ) -> None:
103
+ with pytest.raises(ValidationError):
104
+ ExternalEvalRunInfo.model_validate(
105
+ {
106
+ "model_id": 123,
107
+ "task_ids": ["inspect_evals/aime25"],
108
+ "max_model_len": max_model_len,
109
+ }
110
+ )
111
+
112
+
113
+ def test_external_eval_run_info_rejects_server_owned_settings() -> None:
114
+ with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
115
+ ExternalEvalRunInfo.model_validate(
116
+ {
117
+ "model_id": 123,
118
+ "task_ids": ["inspect_evals/aime25"],
119
+ "attempt_timeout": 60,
120
+ "max_retries": 0,
121
+ }
122
+ )
123
+
124
+
65
125
  def test_get_external_eval_catalog(monkeypatch: pytest.MonkeyPatch) -> None:
66
126
  def fake_get(*args: Any, **kwargs: Any) -> _Response:
67
127
  assert args[0].endswith("/external-evals/catalog")
@@ -68,7 +68,17 @@ def test_launch_eval_run_omits_removed_custom_dataset_path(
68
68
  assert "file_path" not in captured_request["json"]
69
69
 
70
70
 
71
- def test_parse_inspect_evaluation_run() -> None:
71
+ @pytest.mark.parametrize(
72
+ "generation_settings",
73
+ [
74
+ {"attempt_timeout": 3600, "max_retries": 0, "max_model_len": 32768},
75
+ {"attempt_timeout": None, "max_retries": None, "max_model_len": None},
76
+ {},
77
+ ],
78
+ )
79
+ def test_parse_inspect_evaluation_run(
80
+ generation_settings: dict[str, int | None],
81
+ ) -> None:
72
82
  run_record = LlmBehaviorEval._parse_eval_run_record(
73
83
  {
74
84
  "id": 1,
@@ -82,6 +92,7 @@ def test_parse_inspect_evaluation_run() -> None:
82
92
  "bias_type": None,
83
93
  "task_ids": ["inspect_evals/aime25"],
84
94
  "sample_limit": None,
95
+ **generation_settings,
85
96
  "judge_model": None,
86
97
  "run_id": "eval-run-id",
87
98
  "mlflow_run_id": None,
@@ -106,6 +117,9 @@ def test_parse_inspect_evaluation_run() -> None:
106
117
  assert run_record.framework is EvalFramework.INSPECT_EVALS
107
118
  assert run_record.task_ids == ["inspect_evals/aime25"]
108
119
  assert run_record.sample_limit is None
120
+ assert run_record.attempt_timeout == generation_settings.get("attempt_timeout")
121
+ assert run_record.max_retries == generation_settings.get("max_retries")
122
+ assert run_record.max_model_len == generation_settings.get("max_model_len")
109
123
  assert run_record.metrics == LlmEvalMetrics(
110
124
  rows=[
111
125
  LlmEvalMetricRow(
@@ -141,6 +155,9 @@ def test_parse_legacy_evaluation_run_defaults_framework() -> None:
141
155
  )
142
156
 
143
157
  assert run_record.framework is EvalFramework.LLM_BEHAVIOR_EVAL
158
+ assert run_record.attempt_timeout is None
159
+ assert run_record.max_retries is None
160
+ assert run_record.max_model_len is None
144
161
 
145
162
 
146
163
  def test_parse_legacy_evaluation_run_defaults_null_framework() -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes