benchmax 0.2.3__tar.gz → 0.2.4__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.
- {benchmax-0.2.3 → benchmax-0.2.4}/PKG-INFO +8 -2
- {benchmax-0.2.3 → benchmax-0.2.4}/README.md +6 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/pyproject.toml +1 -1
- benchmax-0.2.4/src/benchmax/sft/README.md +59 -0
- benchmax-0.2.4/src/benchmax/sft/__init__.py +36 -0
- benchmax-0.2.4/src/benchmax/sft/dataset.py +187 -0
- benchmax-0.2.4/src/benchmax/sft/issues.py +73 -0
- benchmax-0.2.4/src/benchmax/sft/rows.py +419 -0
- benchmax-0.2.4/src/benchmax/sft/strict_json.py +153 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/all_masked.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/bad_arguments.input.jsonl +2 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/bad_weight.input.jsonl +3 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/deep_nesting.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/duplicate_keys.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/empty.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/expected_issues.json +170 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/legacy_prompt_completion.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/lone_surrogate.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/missing_tool_result.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/multimodal.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/nonfinite_number.input.jsonl +2 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/tool_result_out_of_order.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/tools_missing_for_calls.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/undefined_tool_function.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/invalid/unknown_tool_call_id.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/valid/masked.canonical.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/valid/masked.input.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/valid/plain.canonical.jsonl +2 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/valid/plain.input.jsonl +3 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/valid/tools.canonical.jsonl +1 -0
- benchmax-0.2.4/tests/fixtures/sft_v1/valid/tools.input.jsonl +1 -0
- benchmax-0.2.4/tests/unit/sft/test_dataset.py +577 -0
- benchmax-0.2.4/tests/unit/sft/test_fixtures.py +62 -0
- benchmax-0.2.4/tests/unit/sft/test_import_safety.py +23 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/.gitignore +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/pytest.ini +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/auth.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/bundle.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/README.md +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/__init__.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/base/README.md +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/base/__init__.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/base/dataset.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/base/env.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/base/openai_types.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/dataset.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/environment.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/README.md +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/__init__.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/bundled_agent.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/credentials.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/dataset.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/dep_check.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/env.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/harbor/types.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/identity.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/logging.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/envs/shared_types.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rag/__init__.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rag/embed.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rag/env.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rag/search.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/README.md +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/__init__.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/adaptive.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/deterministic.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/diversity.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/judge.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/prompts.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/rubric.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/src/benchmax/rewards/scoring.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/conftest.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/bundle/test_artifact.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/bundle/test_source_capture.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/envs/test_base_dataset.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/envs/test_base_env_group.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/envs/test_contract_types.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/envs/test_environment_group.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/envs/test_example_id.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/fakes/model_server.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/harbor/test_bundled_agent.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/harbor/test_harbor_dataset.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/harbor/test_harbor_env.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rag/corpus/test_embed.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rag/test_rag_env.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/conftest.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_adaptive.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_deterministic.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_diversity.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_diversity_env.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_judge.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_rubric.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/rewards/test_rubric_rewards.py +0 -0
- {benchmax-0.2.3 → benchmax-0.2.4}/tests/unit/test_auth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: benchmax
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Platform-independent runtime for grouped LLM environments
|
|
5
5
|
Author: benchmax Authors
|
|
6
6
|
Classifier: Operating System :: OS Independent
|
|
@@ -31,6 +31,12 @@ all benchmax environments implement the same dataset and rollout contracts. choo
|
|
|
31
31
|
|
|
32
32
|
most custom environments should extend `BaseEnv`. most Harbor users configure `HarborEnv` directly rather than subclassing it.
|
|
33
33
|
|
|
34
|
+
## sft datasets
|
|
35
|
+
|
|
36
|
+
supervised finetuning does not use an environment. construct a validated
|
|
37
|
+
[`benchmax.sft.SftDataset`](src/benchmax/sft/README.md) from chat-format rows and upload it
|
|
38
|
+
through the castform package; validation is strict and all-or-nothing.
|
|
39
|
+
|
|
34
40
|
## architecture
|
|
35
41
|
|
|
36
42
|
an environment defines its dataset and how a group of rollouts runs against each example.
|
|
@@ -16,6 +16,12 @@ all benchmax environments implement the same dataset and rollout contracts. choo
|
|
|
16
16
|
|
|
17
17
|
most custom environments should extend `BaseEnv`. most Harbor users configure `HarborEnv` directly rather than subclassing it.
|
|
18
18
|
|
|
19
|
+
## sft datasets
|
|
20
|
+
|
|
21
|
+
supervised finetuning does not use an environment. construct a validated
|
|
22
|
+
[`benchmax.sft.SftDataset`](src/benchmax/sft/README.md) from chat-format rows and upload it
|
|
23
|
+
through the castform package; validation is strict and all-or-nothing.
|
|
24
|
+
|
|
19
25
|
## architecture
|
|
20
26
|
|
|
21
27
|
an environment defines its dataset and how a group of rollouts runs against each example.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# benchmax.sft
|
|
2
|
+
|
|
3
|
+
the validated `benchmax-sft-v1` supervised-finetuning dataset artifact. construction is
|
|
4
|
+
all-or-nothing: a dataset either satisfies the whole contract or raises with every ordered,
|
|
5
|
+
line-aware issue.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from benchmax.sft import SftDataset, SftDatasetError
|
|
9
|
+
|
|
10
|
+
train = SftDataset.from_jsonl("train.jsonl") # or SftDataset.from_rows(rows)
|
|
11
|
+
payload = train.to_jsonl_bytes() # canonical, deterministic bytes
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## row contract
|
|
15
|
+
|
|
16
|
+
one JSONL line per row. a row is a closed object: required `messages`, optional `tools`,
|
|
17
|
+
optional `metadata`.
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"messages": [
|
|
22
|
+
{"role": "user", "content": "What is 2 + 2?"},
|
|
23
|
+
{"role": "assistant", "content": "4", "weight": 1}
|
|
24
|
+
],
|
|
25
|
+
"tools": [],
|
|
26
|
+
"metadata": {"id": "optional producer identity"}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `system`/`user`: exactly `role` + non-empty string `content`.
|
|
31
|
+
- `assistant`: optional string-or-null `content`, optional non-empty `tool_calls`,
|
|
32
|
+
optional integer `weight` `0 | 1` (omitted means `1`; `0` masks the turn from loss).
|
|
33
|
+
each assistant turn needs non-empty content or a tool call; each row needs at least one
|
|
34
|
+
assistant turn with effective weight `1`.
|
|
35
|
+
- `tool`: exactly `role` + string `content` + non-empty `tool_call_id`. every tool call
|
|
36
|
+
gets exactly one result, in declaration order, before the next non-tool message.
|
|
37
|
+
- tool definitions/calls are OpenAI-style function shapes, validated deeply; called
|
|
38
|
+
names must match a definition; `function.arguments` must decode as a JSON object.
|
|
39
|
+
- `metadata` is an open finite JSON object (canonical size ≤ 64 KiB; `_castform_*` keys
|
|
40
|
+
are reserved for the runtime). rows are ≤ 1 MiB canonical, `messages` ≤ 1024 entries,
|
|
41
|
+
`tools` ≤ 128 unique functions.
|
|
42
|
+
|
|
43
|
+
rejected outright: multimodal content parts, fractional weights, legacy
|
|
44
|
+
prompt/completion shapes, duplicate JSON keys, lone surrogates, non-finite numbers,
|
|
45
|
+
nesting deeper than 64 levels, and unknown keys anywhere outside `metadata` and
|
|
46
|
+
`parameters`.
|
|
47
|
+
|
|
48
|
+
## canonical bytes
|
|
49
|
+
|
|
50
|
+
`to_jsonl_bytes()` is deterministic for equivalent inputs: UTF-8 without a BOM,
|
|
51
|
+
`ensure_ascii=False`, sorted keys, compact separators, one trailing newline per row,
|
|
52
|
+
original row order.
|
|
53
|
+
|
|
54
|
+
## golden fixtures
|
|
55
|
+
|
|
56
|
+
`packages/benchmax/tests/fixtures/sft_v1/` is the cross-repository contract consumed by
|
|
57
|
+
the castform trainer: valid inputs with byte-exact canonical outputs, and invalid inputs
|
|
58
|
+
with exact ordered diagnostics in `expected_issues.json`. change them only with a format
|
|
59
|
+
version bump in mind.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Public `benchmax-sft-v1` supervised-finetuning dataset contract.
|
|
2
|
+
|
|
3
|
+
One row is a closed JSON object with required ``messages``, optional ``tools``,
|
|
4
|
+
and optional ``metadata``:
|
|
5
|
+
|
|
6
|
+
- ``system``/``user`` messages have exactly ``role`` and non-empty string
|
|
7
|
+
``content``.
|
|
8
|
+
- ``assistant`` messages have optional string-or-null ``content``, optional
|
|
9
|
+
non-empty ``tool_calls``, and optional integer ``weight`` of ``0`` or ``1``
|
|
10
|
+
(omitted means ``1``); each needs non-empty content or at least one tool
|
|
11
|
+
call, and every row needs at least one assistant turn with effective
|
|
12
|
+
weight ``1``.
|
|
13
|
+
- ``tool`` results have exactly ``role``, string ``content``, and non-empty
|
|
14
|
+
``tool_call_id``; every tool call receives exactly one result, in
|
|
15
|
+
declaration order, before the next non-tool message.
|
|
16
|
+
- OpenAI-style function tool definitions/calls are validated deeply; called
|
|
17
|
+
function names must match a definition.
|
|
18
|
+
- ``metadata`` is an open finite JSON object (``_castform_``-prefixed keys are
|
|
19
|
+
reserved for the runtime).
|
|
20
|
+
|
|
21
|
+
Validation is strict and all-or-nothing: images/audio/multimodal content
|
|
22
|
+
parts, fractional weights, legacy prompt/completion shapes, duplicate JSON
|
|
23
|
+
keys, lone surrogates, non-finite numbers, and nesting deeper than 64 levels
|
|
24
|
+
are rejected with ordered, line-aware diagnostics. Canonical serialization is
|
|
25
|
+
deterministic byte-for-byte for equivalent inputs.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from benchmax.sft.dataset import SFT_DATASET_FORMAT, SftDataset
|
|
29
|
+
from benchmax.sft.issues import SftDatasetError, SftIssue
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"SFT_DATASET_FORMAT",
|
|
33
|
+
"SftDataset",
|
|
34
|
+
"SftDatasetError",
|
|
35
|
+
"SftIssue",
|
|
36
|
+
]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""The validated `benchmax-sft-v1` dataset artifact.
|
|
2
|
+
|
|
3
|
+
:class:`SftDataset` is the single trust boundary for SFT training data: it is
|
|
4
|
+
either fully valid or it does not exist. Both constructors run the complete
|
|
5
|
+
strict contract — closed row schema, strict JSON rules, size limits — and raise
|
|
6
|
+
:class:`~benchmax.sft.issues.SftDatasetError` with every ordered issue on any
|
|
7
|
+
violation, including empty input.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Iterable, Iterator
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from benchmax.sft.issues import SftDatasetError, SftIssue
|
|
18
|
+
from benchmax.sft.rows import MAX_ROW_BYTES, validate_row
|
|
19
|
+
from benchmax.sft.strict_json import (
|
|
20
|
+
StrictJsonError,
|
|
21
|
+
canonical_json_bytes,
|
|
22
|
+
check_json_tree,
|
|
23
|
+
parse_strict_json,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = ["SFT_DATASET_FORMAT", "SftDataset"]
|
|
27
|
+
|
|
28
|
+
SFT_DATASET_FORMAT = "benchmax-sft-v1"
|
|
29
|
+
"""Format identifier used at the platform boundary for this row contract."""
|
|
30
|
+
|
|
31
|
+
_INTERNAL = object()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SftDataset:
|
|
35
|
+
"""Immutable, fully validated SFT training dataset.
|
|
36
|
+
|
|
37
|
+
Construct only through :meth:`from_jsonl` or :meth:`from_rows`; direct
|
|
38
|
+
instantiation is rejected so a partially valid dataset can never exist.
|
|
39
|
+
Row order is preserved from the input. :meth:`to_jsonl_bytes` returns
|
|
40
|
+
canonical bytes — UTF-8 without a BOM, ``ensure_ascii=False``, sorted keys,
|
|
41
|
+
compact separators, one trailing newline per row — deterministic for
|
|
42
|
+
equivalent input objects.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("_row_bytes", "_rows")
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
rows: tuple[dict[str, object], ...],
|
|
50
|
+
row_bytes: tuple[bytes, ...],
|
|
51
|
+
token: object = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
if token is not _INTERNAL:
|
|
54
|
+
raise TypeError("use SftDataset.from_jsonl(...) or SftDataset.from_rows(...)")
|
|
55
|
+
self._rows = rows
|
|
56
|
+
self._row_bytes = row_bytes
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_jsonl(cls, path: str | os.PathLike[str]) -> SftDataset:
|
|
60
|
+
"""Load and validate a JSONL file.
|
|
61
|
+
|
|
62
|
+
Whitespace-only physical lines are ignored while physical line numbers
|
|
63
|
+
are preserved in diagnostics. Raises
|
|
64
|
+
:class:`~benchmax.sft.issues.SftDatasetError` on any violation.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
data = Path(path).read_bytes()
|
|
68
|
+
collector = _IssueCollector()
|
|
69
|
+
rows: list[dict[str, object]] = []
|
|
70
|
+
row_bytes: list[bytes] = []
|
|
71
|
+
row_index = 0
|
|
72
|
+
for line_number, raw_line in enumerate(data.split(b"\n"), start=1):
|
|
73
|
+
if raw_line.strip(b" \t\r\f\v") == b"":
|
|
74
|
+
continue
|
|
75
|
+
row = row_index
|
|
76
|
+
row_index += 1
|
|
77
|
+
if line_number == 1 and raw_line.startswith(b"\xef\xbb\xbf"):
|
|
78
|
+
collector.add(line_number, row, "$", "a UTF-8 BOM is not allowed")
|
|
79
|
+
continue
|
|
80
|
+
try:
|
|
81
|
+
text = raw_line.decode("utf-8")
|
|
82
|
+
except UnicodeDecodeError:
|
|
83
|
+
collector.add(line_number, row, "$", "line is not valid UTF-8")
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
parsed = parse_strict_json(text)
|
|
87
|
+
except StrictJsonError as error:
|
|
88
|
+
collector.add(line_number, row, "$", str(error))
|
|
89
|
+
continue
|
|
90
|
+
encoded = _process_row(parsed, collector.for_row(line_number, row))
|
|
91
|
+
if encoded is not None:
|
|
92
|
+
rows.append(json.loads(encoded.decode("utf-8")))
|
|
93
|
+
row_bytes.append(encoded)
|
|
94
|
+
if row_index == 0:
|
|
95
|
+
collector.add(None, None, "$", "dataset contains no rows")
|
|
96
|
+
collector.raise_if_any()
|
|
97
|
+
return cls(tuple(rows), tuple(row_bytes), _INTERNAL)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_rows(cls, rows: Iterable[dict[str, object]]) -> SftDataset:
|
|
101
|
+
"""Validate in-memory row objects.
|
|
102
|
+
|
|
103
|
+
Rows must already use plain JSON-compatible Python types; anything else
|
|
104
|
+
is reported as an issue rather than coerced. Raises
|
|
105
|
+
:class:`~benchmax.sft.issues.SftDatasetError` on any violation.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
collector = _IssueCollector()
|
|
109
|
+
stored: list[dict[str, object]] = []
|
|
110
|
+
row_bytes: list[bytes] = []
|
|
111
|
+
row_index = -1
|
|
112
|
+
for row_index, row in enumerate(iter(rows)):
|
|
113
|
+
encoded = _process_row(row, collector.for_row(None, row_index))
|
|
114
|
+
if encoded is not None:
|
|
115
|
+
# Round-trip through canonical bytes to decouple stored rows
|
|
116
|
+
# from caller-owned objects.
|
|
117
|
+
stored.append(json.loads(encoded.decode("utf-8")))
|
|
118
|
+
row_bytes.append(encoded)
|
|
119
|
+
if row_index < 0:
|
|
120
|
+
collector.add(None, None, "$", "dataset contains no rows")
|
|
121
|
+
collector.raise_if_any()
|
|
122
|
+
return cls(tuple(stored), tuple(row_bytes), _INTERNAL)
|
|
123
|
+
|
|
124
|
+
def to_jsonl_bytes(self) -> bytes:
|
|
125
|
+
"""Serialize every row as canonical JSONL bytes in original order."""
|
|
126
|
+
|
|
127
|
+
return b"".join(encoded + b"\n" for encoded in self._row_bytes)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def rows(self) -> tuple[dict[str, object], ...]:
|
|
131
|
+
"""The validated rows in original order. Treat as read-only."""
|
|
132
|
+
|
|
133
|
+
return self._rows
|
|
134
|
+
|
|
135
|
+
def __len__(self) -> int:
|
|
136
|
+
return len(self._rows)
|
|
137
|
+
|
|
138
|
+
def __iter__(self) -> Iterator[dict[str, object]]:
|
|
139
|
+
return iter(self._rows)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _IssueCollector:
|
|
143
|
+
"""Ordered issue sink shared by both constructors."""
|
|
144
|
+
|
|
145
|
+
def __init__(self) -> None:
|
|
146
|
+
self._issues: list[SftIssue] = []
|
|
147
|
+
|
|
148
|
+
def add(self, line: int | None, row: int | None, location: str, message: str) -> None:
|
|
149
|
+
self._issues.append(SftIssue(message=message, location=location, line=line, row=row))
|
|
150
|
+
|
|
151
|
+
def for_row(self, line: int | None, row: int | None) -> _RowSink:
|
|
152
|
+
return _RowSink(self, line, row)
|
|
153
|
+
|
|
154
|
+
def raise_if_any(self) -> None:
|
|
155
|
+
if self._issues:
|
|
156
|
+
raise SftDatasetError(self._issues)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class _RowSink:
|
|
160
|
+
"""Emit adapter binding one row's line/row coordinates."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, collector: _IssueCollector, line: int | None, row: int | None) -> None:
|
|
163
|
+
self._collector = collector
|
|
164
|
+
self.line = line
|
|
165
|
+
self.row = row
|
|
166
|
+
self.count = 0
|
|
167
|
+
|
|
168
|
+
def __call__(self, location: str, message: str) -> None:
|
|
169
|
+
self.count += 1
|
|
170
|
+
self._collector.add(self.line, self.row, location, message)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _process_row(row: object, sink: _RowSink) -> bytes | None:
|
|
174
|
+
"""Fully validate one row; return its canonical bytes only when clean."""
|
|
175
|
+
|
|
176
|
+
check_json_tree(row, "$", sink)
|
|
177
|
+
if type(row) is not dict:
|
|
178
|
+
sink("$", "row must be a JSON object")
|
|
179
|
+
return None
|
|
180
|
+
validate_row(row, sink)
|
|
181
|
+
if sink.count:
|
|
182
|
+
return None
|
|
183
|
+
encoded = canonical_json_bytes(row)
|
|
184
|
+
if len(encoded) > MAX_ROW_BYTES:
|
|
185
|
+
sink("$", f"row canonical size is {len(encoded)} bytes; the maximum is {MAX_ROW_BYTES}")
|
|
186
|
+
return None
|
|
187
|
+
return encoded
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Diagnostics for `benchmax-sft-v1` dataset validation.
|
|
2
|
+
|
|
3
|
+
Validation is all-or-nothing: constructing an :class:`~benchmax.sft.SftDataset`
|
|
4
|
+
either succeeds completely or raises :class:`SftDatasetError` carrying every
|
|
5
|
+
:class:`SftIssue` found, in deterministic document order.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
__all__ = ["SftDatasetError", "SftIssue"]
|
|
14
|
+
|
|
15
|
+
_PREVIEW_ISSUES = 20
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class SftIssue:
|
|
20
|
+
"""One validation problem at one location in the input.
|
|
21
|
+
|
|
22
|
+
``location`` addresses the offending value inside the row using ``$`` for
|
|
23
|
+
the row object itself, e.g. ``$.messages[2].weight``. ``line`` is the
|
|
24
|
+
1-based physical JSONL line for :meth:`SftDataset.from_jsonl` input and
|
|
25
|
+
``None`` for :meth:`SftDataset.from_rows` input. ``row`` is the 0-based
|
|
26
|
+
dataset row index, or ``None`` for dataset-level problems such as an empty
|
|
27
|
+
input.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
message: str
|
|
31
|
+
location: str
|
|
32
|
+
line: int | None = None
|
|
33
|
+
row: int | None = None
|
|
34
|
+
|
|
35
|
+
def as_dict(self) -> dict[str, object]:
|
|
36
|
+
"""Return the stable JSON-friendly shape used by golden fixtures."""
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"line": self.line,
|
|
40
|
+
"location": self.location,
|
|
41
|
+
"message": self.message,
|
|
42
|
+
"row": self.row,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def describe(self) -> str:
|
|
46
|
+
"""Render a one-line human-readable description."""
|
|
47
|
+
|
|
48
|
+
origin: list[str] = []
|
|
49
|
+
if self.line is not None:
|
|
50
|
+
origin.append(f"line {self.line}")
|
|
51
|
+
if self.row is not None:
|
|
52
|
+
origin.append(f"row {self.row}")
|
|
53
|
+
prefix = f"{' '.join(origin)} " if origin else ""
|
|
54
|
+
return f"{prefix}{self.location}: {self.message}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SftDatasetError(ValueError):
|
|
58
|
+
"""Raised when input cannot form a valid `benchmax-sft-v1` dataset.
|
|
59
|
+
|
|
60
|
+
``issues`` holds every problem found, ordered by input position and then by
|
|
61
|
+
a fixed within-row validation order, so equivalent inputs always produce an
|
|
62
|
+
identical issue sequence.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, issues: Sequence[SftIssue]) -> None:
|
|
66
|
+
self.issues: tuple[SftIssue, ...] = tuple(issues)
|
|
67
|
+
if not self.issues:
|
|
68
|
+
raise ValueError("SftDatasetError requires at least one issue")
|
|
69
|
+
lines = [f"invalid benchmax-sft-v1 dataset: {len(self.issues)} issue(s)"]
|
|
70
|
+
lines += [f" {issue.describe()}" for issue in self.issues[:_PREVIEW_ISSUES]]
|
|
71
|
+
if len(self.issues) > _PREVIEW_ISSUES:
|
|
72
|
+
lines.append(f" ... and {len(self.issues) - _PREVIEW_ISSUES} more")
|
|
73
|
+
super().__init__("\n".join(lines))
|