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.
@@ -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