inspect-evals-utils 0.1.0__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.
- inspect_evals_utils-0.1.0/LICENSE +21 -0
- inspect_evals_utils-0.1.0/PKG-INFO +86 -0
- inspect_evals_utils-0.1.0/README.md +62 -0
- inspect_evals_utils-0.1.0/pyproject.toml +128 -0
- inspect_evals_utils-0.1.0/pyproject.toml.orig +134 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/__init__.py +43 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/cache.py +33 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/code.py +123 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/datasets.py +311 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/huggingface.py +340 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/ids.py +232 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/messages.py +69 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/prompts.py +39 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/py.typed +0 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/readmes/__init__.py +1 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/readmes/cli.py +66 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/readmes/generate.py +670 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/readmes/sections.py +197 -0
- inspect_evals_utils-0.1.0/src/inspect_evals_utils/shuffle.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Fisher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inspect-evals-utils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Runtime helpers for Inspect AI evaluations: dataset loading with retries, sample ids, prompts and README generation
|
|
5
|
+
Author: Matt Fisher
|
|
6
|
+
Author-email: Matt Fisher <m@ttfisher.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: backoff>=2.2.0
|
|
10
|
+
Requires-Dist: inspect-ai>=0.3.262
|
|
11
|
+
Requires-Dist: platformdirs
|
|
12
|
+
Requires-Dist: pyyaml>=5.1.0
|
|
13
|
+
Requires-Dist: datasets>=4.8.5 ; extra == 'huggingface'
|
|
14
|
+
Requires-Dist: hf-xet ; extra == 'huggingface'
|
|
15
|
+
Requires-Dist: httpx>=0.27.0 ; extra == 'huggingface'
|
|
16
|
+
Requires-Dist: huggingface-hub>=1.2.0 ; extra == 'huggingface'
|
|
17
|
+
Requires-Dist: requests>=2.32.0 ; extra == 'huggingface'
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Project-URL: Homepage, https://github.com/Generality-Labs/inspect-evals-utils
|
|
20
|
+
Project-URL: Repository, https://github.com/Generality-Labs/inspect-evals-utils
|
|
21
|
+
Project-URL: Issues, https://github.com/Generality-Labs/inspect-evals-utils/issues
|
|
22
|
+
Provides-Extra: huggingface
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# inspect-evals-utils
|
|
26
|
+
|
|
27
|
+
Runtime helpers for [Inspect AI](https://inspect.aisi.org.uk/) evaluations, moved out of [inspect_evals](https://github.com/UKGovernmentBEIS/inspect_evals) so that a standalone eval repo can use them without depending on the whole registry. inspect_evals will depend on this package and re-export everything under its old `inspect_evals.utils` paths, so existing imports keep working.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add inspect-evals-utils
|
|
33
|
+
# or, for the Hugging Face wrappers:
|
|
34
|
+
uv add "inspect-evals-utils[huggingface]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Modules
|
|
38
|
+
|
|
39
|
+
- `inspect_evals_utils.ids`: `create_stable_id`, `filter_duplicate_ids`, `drop_known_broken`, `require_optional_dependency`, `DuplicateIdError`, `KnownBrokenSampleError`.
|
|
40
|
+
- `inspect_evals_utils.datasets`: `load_json_dataset` and `load_csv_dataset`, which cache remote files and retry transient errors, plus `cache_dataset_loader`, `is_remote_url`, `get_cached_fname`, `is_transient_error`.
|
|
41
|
+
- `inspect_evals_utils.huggingface` (needs the `huggingface` extra): `hf_dataset`, `load_dataset`, `get_dataset_split_names`, `snapshot_download`, `hf_hub_download`, `sentence_transformer`, `transformers_pipeline`, `auto_model_for_sequence_classification`, `auto_tokenizer`. Each retries on rate limits and transient errors and requires a `revision=` keyword, so datasets are pinned. `set_backoff_config` changes the retry policy for later calls (the default is `CI_BACKOFF_CONFIG` when the `CI` environment variable is set, otherwise `STANDARD_BACKOFF_CONFIG`). `set_hf_call_recorder` and `set_backoff_recorder` register callbacks that test sessions use to count calls and retries; both default to doing nothing.
|
|
42
|
+
- `inspect_evals_utils.code`: `extract_code_blocks`, `extract_code_block`, `strip_json_fence`, `defines`.
|
|
43
|
+
- `inspect_evals_utils.messages`: `message_content_text`, `reasoning_to_text`.
|
|
44
|
+
- `inspect_evals_utils.prompts`: `Prompt`, a dataclass recording a prompt with its provenance.
|
|
45
|
+
- `inspect_evals_utils.shuffle`: `shuffle_and_seed`.
|
|
46
|
+
- `inspect_evals_utils.cache`: `cache_path()` and `INSPECT_EVALS_CACHE_PATH`.
|
|
47
|
+
|
|
48
|
+
The most-used names are also importable from the top level, for example `from inspect_evals_utils import create_stable_id`. The Hugging Face module is not, so the base package imports without the extra.
|
|
49
|
+
|
|
50
|
+
## Cache directory
|
|
51
|
+
|
|
52
|
+
Downloads land in `INSPECT_EVALS_CACHE_PATH`, which defaults to the platform user cache directory for `inspect_evals`. Set `INSPECT_EVALS_CACHE_DIR` to move it or point it at a pre-populated directory. It must be absolute or start with `~`, and it is read once when the package is imported. inspect_evals reads the same variable and default, so one machine has one cache.
|
|
53
|
+
|
|
54
|
+
## README generation
|
|
55
|
+
|
|
56
|
+
The `inspect-evals-readmes` command rewrites the generated sections of each eval's README (Contributors, Usage, Options and Parameters) from its `eval.yaml` and `@task` signatures. It finds evals as `<src>/<eval>/eval.yaml` (default `src`), imports each package to read the task parameters, and writes between the `<!-- Usage: Automatically Generated -->` style tag pairs.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv run inspect-evals-readmes generate # rewrite the sections
|
|
60
|
+
uv run inspect-evals-readmes generate --check # exit 1 if any README is out of date; writes nothing
|
|
61
|
+
uv run inspect-evals-readmes generate --eval my_eval --create-missing
|
|
62
|
+
uv run inspect-evals-readmes add-section --key Parameters --insert-after Options
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`add-section` inserts a missing tag pair after the first listed section it finds, or at the end of the file.
|
|
66
|
+
|
|
67
|
+
## Private inspect_ai APIs
|
|
68
|
+
|
|
69
|
+
`inspect_evals_utils.datasets` uses `inspect_ai._util.file.file` to open local and remote files through one interface. There is no public equivalent yet, so a future inspect_ai release could break it.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync
|
|
75
|
+
uv run pre-commit install # optional: run the lint stack on every commit
|
|
76
|
+
uv run pytest
|
|
77
|
+
uv run basedpyright src
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each pull request adds a changelog fragment rather than editing `CHANGELOG.md`, so concurrent PRs don't conflict. Run `uv run scriv create`, uncomment the sections that apply in the new file under `changelog.d/`, and commit it with the change, or delete it if the change needs no entry. To release, run the **Prepare release** workflow from the Actions tab. It bumps `version` in `pyproject.toml`, collects the fragments into `CHANGELOG.md`, and opens a release pull request, whose CI starts once you click **Approve workflows to run** on it; merging it tags the release. It needs _Settings → Actions → General_ → **Allow GitHub Actions to create and approve pull requests**. By hand, the same is `uv version --bump minor` and `uv run scriv collect`.
|
|
81
|
+
|
|
82
|
+
Linting (ruff, [zizmor](https://docs.zizmor.sh/), mdformat) runs via [pre-commit](https://pre-commit.com); CI runs the same stack plus basedpyright and pytest via the shared [`python-ci`](https://github.com/Generality-Labs/python-project-template) reusable workflow.
|
|
83
|
+
|
|
84
|
+
## Releasing
|
|
85
|
+
|
|
86
|
+
See [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# inspect-evals-utils
|
|
2
|
+
|
|
3
|
+
Runtime helpers for [Inspect AI](https://inspect.aisi.org.uk/) evaluations, moved out of [inspect_evals](https://github.com/UKGovernmentBEIS/inspect_evals) so that a standalone eval repo can use them without depending on the whole registry. inspect_evals will depend on this package and re-export everything under its old `inspect_evals.utils` paths, so existing imports keep working.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add inspect-evals-utils
|
|
9
|
+
# or, for the Hugging Face wrappers:
|
|
10
|
+
uv add "inspect-evals-utils[huggingface]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Modules
|
|
14
|
+
|
|
15
|
+
- `inspect_evals_utils.ids`: `create_stable_id`, `filter_duplicate_ids`, `drop_known_broken`, `require_optional_dependency`, `DuplicateIdError`, `KnownBrokenSampleError`.
|
|
16
|
+
- `inspect_evals_utils.datasets`: `load_json_dataset` and `load_csv_dataset`, which cache remote files and retry transient errors, plus `cache_dataset_loader`, `is_remote_url`, `get_cached_fname`, `is_transient_error`.
|
|
17
|
+
- `inspect_evals_utils.huggingface` (needs the `huggingface` extra): `hf_dataset`, `load_dataset`, `get_dataset_split_names`, `snapshot_download`, `hf_hub_download`, `sentence_transformer`, `transformers_pipeline`, `auto_model_for_sequence_classification`, `auto_tokenizer`. Each retries on rate limits and transient errors and requires a `revision=` keyword, so datasets are pinned. `set_backoff_config` changes the retry policy for later calls (the default is `CI_BACKOFF_CONFIG` when the `CI` environment variable is set, otherwise `STANDARD_BACKOFF_CONFIG`). `set_hf_call_recorder` and `set_backoff_recorder` register callbacks that test sessions use to count calls and retries; both default to doing nothing.
|
|
18
|
+
- `inspect_evals_utils.code`: `extract_code_blocks`, `extract_code_block`, `strip_json_fence`, `defines`.
|
|
19
|
+
- `inspect_evals_utils.messages`: `message_content_text`, `reasoning_to_text`.
|
|
20
|
+
- `inspect_evals_utils.prompts`: `Prompt`, a dataclass recording a prompt with its provenance.
|
|
21
|
+
- `inspect_evals_utils.shuffle`: `shuffle_and_seed`.
|
|
22
|
+
- `inspect_evals_utils.cache`: `cache_path()` and `INSPECT_EVALS_CACHE_PATH`.
|
|
23
|
+
|
|
24
|
+
The most-used names are also importable from the top level, for example `from inspect_evals_utils import create_stable_id`. The Hugging Face module is not, so the base package imports without the extra.
|
|
25
|
+
|
|
26
|
+
## Cache directory
|
|
27
|
+
|
|
28
|
+
Downloads land in `INSPECT_EVALS_CACHE_PATH`, which defaults to the platform user cache directory for `inspect_evals`. Set `INSPECT_EVALS_CACHE_DIR` to move it or point it at a pre-populated directory. It must be absolute or start with `~`, and it is read once when the package is imported. inspect_evals reads the same variable and default, so one machine has one cache.
|
|
29
|
+
|
|
30
|
+
## README generation
|
|
31
|
+
|
|
32
|
+
The `inspect-evals-readmes` command rewrites the generated sections of each eval's README (Contributors, Usage, Options and Parameters) from its `eval.yaml` and `@task` signatures. It finds evals as `<src>/<eval>/eval.yaml` (default `src`), imports each package to read the task parameters, and writes between the `<!-- Usage: Automatically Generated -->` style tag pairs.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv run inspect-evals-readmes generate # rewrite the sections
|
|
36
|
+
uv run inspect-evals-readmes generate --check # exit 1 if any README is out of date; writes nothing
|
|
37
|
+
uv run inspect-evals-readmes generate --eval my_eval --create-missing
|
|
38
|
+
uv run inspect-evals-readmes add-section --key Parameters --insert-after Options
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`add-section` inserts a missing tag pair after the first listed section it finds, or at the end of the file.
|
|
42
|
+
|
|
43
|
+
## Private inspect_ai APIs
|
|
44
|
+
|
|
45
|
+
`inspect_evals_utils.datasets` uses `inspect_ai._util.file.file` to open local and remote files through one interface. There is no public equivalent yet, so a future inspect_ai release could break it.
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv sync
|
|
51
|
+
uv run pre-commit install # optional: run the lint stack on every commit
|
|
52
|
+
uv run pytest
|
|
53
|
+
uv run basedpyright src
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each pull request adds a changelog fragment rather than editing `CHANGELOG.md`, so concurrent PRs don't conflict. Run `uv run scriv create`, uncomment the sections that apply in the new file under `changelog.d/`, and commit it with the change, or delete it if the change needs no entry. To release, run the **Prepare release** workflow from the Actions tab. It bumps `version` in `pyproject.toml`, collects the fragments into `CHANGELOG.md`, and opens a release pull request, whose CI starts once you click **Approve workflows to run** on it; merging it tags the release. It needs _Settings → Actions → General_ → **Allow GitHub Actions to create and approve pull requests**. By hand, the same is `uv version --bump minor` and `uv run scriv collect`.
|
|
57
|
+
|
|
58
|
+
Linting (ruff, [zizmor](https://docs.zizmor.sh/), mdformat) runs via [pre-commit](https://pre-commit.com); CI runs the same stack plus basedpyright and pytest via the shared [`python-ci`](https://github.com/Generality-Labs/python-project-template) reusable workflow.
|
|
59
|
+
|
|
60
|
+
## Releasing
|
|
61
|
+
|
|
62
|
+
See [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.12.22,<0.14.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inspect-evals-utils"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Runtime helpers for Inspect AI evaluations: dataset loading with retries, sample ids, prompts and README generation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"backoff>=2.2.0",
|
|
15
|
+
"inspect_ai>=0.3.262",
|
|
16
|
+
"platformdirs",
|
|
17
|
+
"pyyaml>=5.1.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[[project.authors]]
|
|
21
|
+
name = "Matt Fisher"
|
|
22
|
+
email = "m@ttfisher.com"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
huggingface = [
|
|
26
|
+
"datasets>=4.8.5",
|
|
27
|
+
"hf_xet",
|
|
28
|
+
"httpx>=0.27.0",
|
|
29
|
+
"huggingface_hub>=1.2.0",
|
|
30
|
+
"requests>=2.32.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
inspect-evals-readmes = "inspect_evals_utils.readmes.cli:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Generality-Labs/inspect-evals-utils"
|
|
38
|
+
Repository = "https://github.com/Generality-Labs/inspect-evals-utils"
|
|
39
|
+
Issues = "https://github.com/Generality-Labs/inspect-evals-utils/issues"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.4.0",
|
|
44
|
+
"pytest-cov>=7.0.0",
|
|
45
|
+
"basedpyright>=1.39",
|
|
46
|
+
"pre-commit>=4.0.0",
|
|
47
|
+
"ruff==0.17.3",
|
|
48
|
+
"scriv>=1.8",
|
|
49
|
+
"datasets>=4.8.5",
|
|
50
|
+
"httpx>=0.27.0",
|
|
51
|
+
"huggingface_hub>=1.2.0",
|
|
52
|
+
"pytest-asyncio>=1.0",
|
|
53
|
+
"pytest-inspect-evals[huggingface]>=0.3.0",
|
|
54
|
+
"requests>=2.32.0",
|
|
55
|
+
"types-PyYAML",
|
|
56
|
+
"types-requests",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.scriv]
|
|
60
|
+
format = "md"
|
|
61
|
+
fragment_directory = "changelog.d"
|
|
62
|
+
categories = [
|
|
63
|
+
"Added",
|
|
64
|
+
"Changed",
|
|
65
|
+
"Deprecated",
|
|
66
|
+
"Removed",
|
|
67
|
+
"Fixed",
|
|
68
|
+
"Security",
|
|
69
|
+
]
|
|
70
|
+
skip_fragments = "[A-Z]*"
|
|
71
|
+
new_fragment_template = "file: TEMPLATE.md"
|
|
72
|
+
entry_title_template = "[{{ version }}] - {{ date.strftime('%Y-%m-%d') }}"
|
|
73
|
+
md_header_level = "2"
|
|
74
|
+
md_html_anchors = false
|
|
75
|
+
compact_fragments = true
|
|
76
|
+
version = "literal: pyproject.toml: project.version"
|
|
77
|
+
|
|
78
|
+
[tool.ruff]
|
|
79
|
+
line-length = 100
|
|
80
|
+
src = ["src"]
|
|
81
|
+
target-version = "py311"
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = [
|
|
85
|
+
"E",
|
|
86
|
+
"W",
|
|
87
|
+
"F",
|
|
88
|
+
"I",
|
|
89
|
+
"UP",
|
|
90
|
+
"B",
|
|
91
|
+
"SIM",
|
|
92
|
+
"D",
|
|
93
|
+
"C4",
|
|
94
|
+
"PT",
|
|
95
|
+
"PIE",
|
|
96
|
+
"DTZ",
|
|
97
|
+
"ISC",
|
|
98
|
+
"ASYNC",
|
|
99
|
+
"N",
|
|
100
|
+
"FURB",
|
|
101
|
+
"RUF",
|
|
102
|
+
"PLE",
|
|
103
|
+
"PLW",
|
|
104
|
+
]
|
|
105
|
+
ignore = [
|
|
106
|
+
"E501",
|
|
107
|
+
"D10",
|
|
108
|
+
"D415",
|
|
109
|
+
"ISC001",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[tool.ruff.lint.pydocstyle]
|
|
113
|
+
convention = "google"
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.per-file-ignores]
|
|
116
|
+
"tests/**" = ["D"]
|
|
117
|
+
|
|
118
|
+
[tool.basedpyright]
|
|
119
|
+
pythonVersion = "3.11"
|
|
120
|
+
typeCheckingMode = "standard"
|
|
121
|
+
reportMissingTypeStubs = false
|
|
122
|
+
|
|
123
|
+
[tool.pytest.ini_options]
|
|
124
|
+
testpaths = ["tests"]
|
|
125
|
+
addopts = "-rA --durations=10 --color=yes --cov=src --cov-report=term-missing"
|
|
126
|
+
asyncio_mode = "auto"
|
|
127
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
128
|
+
filterwarnings = ['ignore:The configuration option "asyncio_default_fixture_loop_scope" is unset:pytest.PytestDeprecationWarning']
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# uv's own backend. It builds src/inspect_evals_utils/ with no further config.
|
|
3
|
+
# Keep the upper bound at the next uv minor, as `uv init` writes it, and raise
|
|
4
|
+
# both bounds when moving to a new uv minor.
|
|
5
|
+
requires = ["uv_build>=0.12.22,<0.14.0"]
|
|
6
|
+
build-backend = "uv_build"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "inspect-evals-utils"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "Runtime helpers for Inspect AI evaluations: dataset loading with retries, sample ids, prompts and README generation"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"] # uv_build ships only the license files named here
|
|
15
|
+
authors = [{ name = "Matt Fisher", email = "m@ttfisher.com" }]
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"backoff>=2.2.0",
|
|
19
|
+
"inspect_ai>=0.3.262",
|
|
20
|
+
"platformdirs",
|
|
21
|
+
"pyyaml>=5.1.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
# Only inspect_evals_utils.huggingface needs these.
|
|
26
|
+
huggingface = [
|
|
27
|
+
"datasets>=4.8.5",
|
|
28
|
+
"hf_xet",
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"huggingface_hub>=1.2.0",
|
|
31
|
+
"requests>=2.32.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
inspect-evals-readmes = "inspect_evals_utils.readmes.cli:main"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/Generality-Labs/inspect-evals-utils"
|
|
39
|
+
Repository = "https://github.com/Generality-Labs/inspect-evals-utils"
|
|
40
|
+
Issues = "https://github.com/Generality-Labs/inspect-evals-utils/issues"
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.4.0",
|
|
44
|
+
"pytest-cov>=7.0.0",
|
|
45
|
+
"basedpyright>=1.39",
|
|
46
|
+
"pre-commit>=4.0.0",
|
|
47
|
+
"ruff==0.17.3",
|
|
48
|
+
"scriv>=1.8",
|
|
49
|
+
"datasets>=4.8.5",
|
|
50
|
+
"httpx>=0.27.0",
|
|
51
|
+
"huggingface_hub>=1.2.0",
|
|
52
|
+
"pytest-asyncio>=1.0",
|
|
53
|
+
"pytest-inspect-evals[huggingface]>=0.3.0",
|
|
54
|
+
"requests>=2.32.0",
|
|
55
|
+
"types-PyYAML",
|
|
56
|
+
"types-requests",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
[tool.scriv]
|
|
61
|
+
# One fragment per pull request in changelog.d/, collected into CHANGELOG.md
|
|
62
|
+
# at release time, so concurrent PRs never edit the same lines of the changelog.
|
|
63
|
+
format = "md"
|
|
64
|
+
fragment_directory = "changelog.d"
|
|
65
|
+
categories = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
|
|
66
|
+
skip_fragments = "[A-Z]*" # TEMPLATE.md
|
|
67
|
+
new_fragment_template = "file: TEMPLATE.md"
|
|
68
|
+
entry_title_template = "[{{ version }}] - {{ date.strftime('%Y-%m-%d') }}"
|
|
69
|
+
md_header_level = "2"
|
|
70
|
+
md_html_anchors = false
|
|
71
|
+
# Without this, two fragments in one category leave a blank line between them,
|
|
72
|
+
# which makes the list loose and fails mdformat on the release commit.
|
|
73
|
+
compact_fragments = true
|
|
74
|
+
# The version's single source is [project] version above, which
|
|
75
|
+
# `uv version --bump` (and the Prepare release workflow) rewrites.
|
|
76
|
+
version = "literal: pyproject.toml: project.version"
|
|
77
|
+
|
|
78
|
+
[tool.ruff]
|
|
79
|
+
line-length = 100
|
|
80
|
+
src = ["src"]
|
|
81
|
+
target-version = "py311"
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = [
|
|
85
|
+
"E", # pycodestyle errors
|
|
86
|
+
"W", # pycodestyle warnings
|
|
87
|
+
"F", # pyflakes
|
|
88
|
+
"I", # isort
|
|
89
|
+
"UP", # pyupgrade
|
|
90
|
+
"B", # flake8-bugbear
|
|
91
|
+
"SIM", # flake8-simplify
|
|
92
|
+
"D", # pydocstyle
|
|
93
|
+
"C4", # flake8-comprehensions
|
|
94
|
+
"PT", # flake8-pytest-style
|
|
95
|
+
"PIE", # flake8-pie
|
|
96
|
+
"DTZ", # flake8-datetimez (timezone-aware datetimes)
|
|
97
|
+
"ISC", # implicit-str-concat (catches missing commas)
|
|
98
|
+
"ASYNC", # flake8-async
|
|
99
|
+
"N", # pep8-naming
|
|
100
|
+
"FURB", # refurb (modernization)
|
|
101
|
+
"RUF", # ruff-specific rules
|
|
102
|
+
"PLE", # pylint errors
|
|
103
|
+
"PLW", # pylint warnings
|
|
104
|
+
]
|
|
105
|
+
ignore = [
|
|
106
|
+
"E501", # line-too-long — the formatter owns line length
|
|
107
|
+
"D10", # missing docstrings — don't force them
|
|
108
|
+
"D415", # first-line punctuation — the module docstring is the project
|
|
109
|
+
# description, whose punctuation varies
|
|
110
|
+
"ISC001", # single-line implicit concat — conflicts with the formatter
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[tool.ruff.lint.pydocstyle]
|
|
114
|
+
convention = "google"
|
|
115
|
+
|
|
116
|
+
[tool.ruff.lint.per-file-ignores]
|
|
117
|
+
"tests/**" = ["D"] # tests don't need docstring linting
|
|
118
|
+
|
|
119
|
+
[tool.basedpyright]
|
|
120
|
+
pythonVersion = "3.11"
|
|
121
|
+
# "standard", not "strict": v0.1 moves code verbatim from inspect_evals, which
|
|
122
|
+
# was checked with mypy. Tighten once the code is ours.
|
|
123
|
+
typeCheckingMode = "standard"
|
|
124
|
+
reportMissingTypeStubs = false # third-party libs are often untyped
|
|
125
|
+
|
|
126
|
+
[tool.pytest.ini_options]
|
|
127
|
+
testpaths = ["tests"]
|
|
128
|
+
addopts = "-rA --durations=10 --color=yes --cov=src --cov-report=term-missing"
|
|
129
|
+
asyncio_mode = "auto"
|
|
130
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
131
|
+
filterwarnings = [
|
|
132
|
+
# pytester-style inner runs and moved tests have no pytest-asyncio config of their own.
|
|
133
|
+
'ignore:The configuration option "asyncio_default_fixture_loop_scope" is unset:pytest.PytestDeprecationWarning',
|
|
134
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Runtime helpers for Inspect AI evaluations: dataset loading with retries, sample ids, prompts and README generation."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from inspect_evals_utils.cache import INSPECT_EVALS_CACHE_PATH
|
|
6
|
+
from inspect_evals_utils.datasets import load_csv_dataset, load_json_dataset
|
|
7
|
+
from inspect_evals_utils.ids import (
|
|
8
|
+
DuplicateIdError,
|
|
9
|
+
KnownBrokenSampleError,
|
|
10
|
+
create_stable_id,
|
|
11
|
+
drop_known_broken,
|
|
12
|
+
filter_duplicate_ids,
|
|
13
|
+
require_optional_dependency,
|
|
14
|
+
)
|
|
15
|
+
from inspect_evals_utils.messages import message_content_text, reasoning_to_text
|
|
16
|
+
from inspect_evals_utils.prompts import Prompt
|
|
17
|
+
from inspect_evals_utils.shuffle import shuffle_and_seed
|
|
18
|
+
|
|
19
|
+
# The version is set once, in pyproject.toml; this reads it back from the
|
|
20
|
+
# installed package metadata.
|
|
21
|
+
try:
|
|
22
|
+
__version__ = version("inspect-evals-utils")
|
|
23
|
+
except PackageNotFoundError: # a source tree that was never installed
|
|
24
|
+
__version__ = "unknown"
|
|
25
|
+
|
|
26
|
+
# inspect_evals_utils.huggingface is not imported here: it needs the
|
|
27
|
+
# huggingface extra, and the base package must import without it.
|
|
28
|
+
__all__ = [
|
|
29
|
+
"INSPECT_EVALS_CACHE_PATH",
|
|
30
|
+
"DuplicateIdError",
|
|
31
|
+
"KnownBrokenSampleError",
|
|
32
|
+
"Prompt",
|
|
33
|
+
"__version__",
|
|
34
|
+
"create_stable_id",
|
|
35
|
+
"drop_known_broken",
|
|
36
|
+
"filter_duplicate_ids",
|
|
37
|
+
"load_csv_dataset",
|
|
38
|
+
"load_json_dataset",
|
|
39
|
+
"message_content_text",
|
|
40
|
+
"reasoning_to_text",
|
|
41
|
+
"require_optional_dependency",
|
|
42
|
+
"shuffle_and_seed",
|
|
43
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""The cache directory shared by inspect_evals and standalone evals.
|
|
2
|
+
|
|
3
|
+
Set INSPECT_EVALS_CACHE_DIR to move it or point it at a pre-populated directory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from platformdirs import user_cache_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cache_path() -> Path:
|
|
13
|
+
"""Resolve the cache directory, rejecting a value that depends on the cwd."""
|
|
14
|
+
configured = os.environ.get("INSPECT_EVALS_CACHE_DIR")
|
|
15
|
+
if not configured:
|
|
16
|
+
return Path(user_cache_dir("inspect_evals"))
|
|
17
|
+
|
|
18
|
+
# A leading ~ survives a .env file and a quoted shell assignment, which is
|
|
19
|
+
# how this variable is usually set, so expand it rather than treating it as
|
|
20
|
+
# a directory name.
|
|
21
|
+
path = Path(configured).expanduser()
|
|
22
|
+
if not path.is_absolute():
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"INSPECT_EVALS_CACHE_DIR must be absolute or start with ~, but is "
|
|
25
|
+
f"{configured!r}. It is read once when inspect_evals_utils is imported, so "
|
|
26
|
+
f"a relative path would point somewhere different depending on where "
|
|
27
|
+
f"the eval was started from."
|
|
28
|
+
)
|
|
29
|
+
return path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Read once at import, because evals derive their own module-level paths from it.
|
|
33
|
+
INSPECT_EVALS_CACHE_PATH = cache_path()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Helpers for extracting generated code and JSON from Markdown responses."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
_LANGUAGE_TAGS = {
|
|
8
|
+
"python": ("python", "python3", "py"),
|
|
9
|
+
"cpp": ("cpp", "c++", "cc", "cxx", "c"),
|
|
10
|
+
"java": ("java",),
|
|
11
|
+
"javascript": ("javascript", "js"),
|
|
12
|
+
"typescript": ("typescript", "ts"),
|
|
13
|
+
"csharp": ("csharp", "cs", "c#"),
|
|
14
|
+
"php": ("php",),
|
|
15
|
+
"shell": ("bash", "sh", "shell"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# A block ends at a bare fence line, just before any other fence line (which a model
|
|
19
|
+
# may write without closing the block first), at a fence glued to the end of the last
|
|
20
|
+
# code line, or at the end of the text when the fence is never closed. The last case is
|
|
21
|
+
# its own group so that an unclosed unlabelled fence, usually a stray closing fence
|
|
22
|
+
# after unfenced code, can be ignored.
|
|
23
|
+
_CLOSE = r"(?:^```[ \t\r]*$|(?=^```)|(?<=\S)```[ \t\r]*$|(\Z))"
|
|
24
|
+
|
|
25
|
+
_FENCE = re.compile(
|
|
26
|
+
r"^```[ \t]*([^\s`]*)[^\n]*\r?\n(.*?)" + _CLOSE,
|
|
27
|
+
re.DOTALL | re.MULTILINE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_JSON_FENCE = re.compile(
|
|
32
|
+
r"\A\s*```[ \t]*(?:json)?[ \t]*\r?\n(.*?)\s*```\s*\Z", re.DOTALL | re.IGNORECASE
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _labelled_fence(tags: tuple[str, ...]) -> re.Pattern[str]:
|
|
37
|
+
# A labelled fence may follow prose on the same line, but only when nothing
|
|
38
|
+
# follows the label, so that a fence mentioned in a sentence is not an opener.
|
|
39
|
+
tag = "|".join(re.escape(t) for t in sorted(tags, key=len, reverse=True))
|
|
40
|
+
opener = rf"(?:^```[ \t]*(?:{tag})(?=[\s`])[^\n]*|```[ \t]*(?:{tag})[ \t]*)\r?\n"
|
|
41
|
+
return re.compile(opener + "(.*?)" + _CLOSE, re.DOTALL | re.MULTILINE | re.IGNORECASE)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_code_blocks(completion: str, language: str = "python") -> list[str]:
|
|
45
|
+
"""Return the language-labelled code blocks, or else the unlabelled ones.
|
|
46
|
+
|
|
47
|
+
Blocks are in the order they appear. Unlabelled blocks are returned only
|
|
48
|
+
when no block carries one of the language's labels. Labels are matched
|
|
49
|
+
case-insensitively and both LF and CRLF line endings are accepted. Fences
|
|
50
|
+
labelled for another language are not treated as unlabelled code, and empty
|
|
51
|
+
blocks are skipped. A labelled fence that is never closed runs to the end of
|
|
52
|
+
the text.
|
|
53
|
+
"""
|
|
54
|
+
tags = _LANGUAGE_TAGS.get(language.lower(), (language.lower(),))
|
|
55
|
+
labelled = [
|
|
56
|
+
match.group(1)
|
|
57
|
+
for match in _labelled_fence(tags).finditer(completion)
|
|
58
|
+
if match.group(1).strip()
|
|
59
|
+
]
|
|
60
|
+
if labelled:
|
|
61
|
+
return labelled
|
|
62
|
+
return [
|
|
63
|
+
match.group(2)
|
|
64
|
+
for match in _FENCE.finditer(completion)
|
|
65
|
+
if match.group(1) == "" and match.group(2).strip() and match.group(3) is None
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def extract_code_block(
|
|
70
|
+
completion: str,
|
|
71
|
+
language: str = "python",
|
|
72
|
+
accept: Callable[[str], bool] | None = None,
|
|
73
|
+
) -> str | None:
|
|
74
|
+
"""Return the first block from `extract_code_blocks`, or None if there is none.
|
|
75
|
+
|
|
76
|
+
Language-labelled blocks are preferred over unlabelled blocks even when an
|
|
77
|
+
unlabelled block appears earlier in the response.
|
|
78
|
+
|
|
79
|
+
When `accept` is given, the first block it accepts is returned instead. If
|
|
80
|
+
it accepts none, the first block is returned as without it. Pass
|
|
81
|
+
`defines(name)` to take the block that implements the function the task
|
|
82
|
+
asks for, rather than a usage example or a draft that does not parse.
|
|
83
|
+
"""
|
|
84
|
+
blocks = extract_code_blocks(completion, language)
|
|
85
|
+
|
|
86
|
+
if accept is not None:
|
|
87
|
+
for block in blocks:
|
|
88
|
+
if accept(block):
|
|
89
|
+
return block
|
|
90
|
+
|
|
91
|
+
return blocks[0] if blocks else None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def strip_json_fence(text: str) -> str:
|
|
95
|
+
"""Return the JSON inside one code fence wrapping the whole text, else the text unchanged.
|
|
96
|
+
|
|
97
|
+
The fence may be labelled `json` or unlabelled. Use it before parsing a
|
|
98
|
+
reply that should be bare JSON, such as one requested with a
|
|
99
|
+
`response_schema`: some providers accept the schema without enforcing it and
|
|
100
|
+
return the JSON in a fence. Text with prose around the fence, or more than
|
|
101
|
+
one fence, is returned unchanged, so it still fails validation.
|
|
102
|
+
"""
|
|
103
|
+
match = _JSON_FENCE.match(text)
|
|
104
|
+
if match is None or re.search(r"^[ \t]*```", match.group(1), re.MULTILINE):
|
|
105
|
+
return text
|
|
106
|
+
return match.group(1)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def defines(name: str) -> Callable[[str], bool]:
|
|
110
|
+
"""Accept Python code that parses and defines `name` at the top level."""
|
|
111
|
+
|
|
112
|
+
def check(code: str) -> bool:
|
|
113
|
+
try:
|
|
114
|
+
tree = ast.parse(code)
|
|
115
|
+
except (SyntaxError, ValueError):
|
|
116
|
+
return False
|
|
117
|
+
return any(
|
|
118
|
+
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
|
|
119
|
+
and node.name == name
|
|
120
|
+
for node in tree.body
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return check
|