alignmenter 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alignmenter-0.3.1/src/alignmenter.egg-info → alignmenter-0.3.2}/PKG-INFO +1 -1
- {alignmenter-0.3.1 → alignmenter-0.3.2}/datasets/README.md +22 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/_version.py +1 -1
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/cli.py +3 -0
- alignmenter-0.3.2/src/alignmenter/dataset_cli.py +205 -0
- alignmenter-0.3.2/src/alignmenter/schemas/dataset.py +139 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2/src/alignmenter.egg-info}/PKG-INFO +1 -1
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter.egg-info/SOURCES.txt +3 -0
- alignmenter-0.3.2/tests/test_dataset_management.py +142 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/LICENSE +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/MANIFEST.in +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/README.md +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/configs/demo_config.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/configs/judges/safety_prompt.txt +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/configs/persona/default.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/configs/run-grounded.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/configs/run.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/configs/safety_keywords.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/datasets/demo_conversations.jsonl +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/datasets/grounded_demo.jsonl +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/datasets/wendys_twitter.jsonl +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/pyproject.toml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/setup.cfg +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/analyze.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/bounds.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/diagnose.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/generate.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/label.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/optimize.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/sampling.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/calibration/validate.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/config.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/demo_config.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/judges/safety_prompt.txt +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/persona/default.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/run-grounded.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/run.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/safety_keywords.yaml +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/datasets/demo_conversations.jsonl +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/datasets/grounded_demo.jsonl +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/evaluators/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/evaluators/custom.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/evaluators/evidence.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/evaluators/faithfulness.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/evaluators/grounding.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/evaluators/metrics.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/examples/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/examples/resource_task.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/archive.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/artifacts.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/comparison.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/evaluation.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/gates.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/leases.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/legacy.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/recovery.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/review.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/execution/suite.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/judges/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/judges/authenticity_judge.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/judges/prompts.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/anthropic.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/base.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/callable.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/classifiers.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/durable_judge.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/embeddings.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/judges.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/local.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/providers/openai.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/release_cli.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/reporting/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/reporting/durable.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/reporting/github_comment.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/reporting/html.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/reporting/json_out.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/run_config.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/runner.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/evaluation.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/execution.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/gates.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/metrics.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/review.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/scoring.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/schemas/suite.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scorers/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scorers/authenticity.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scorers/faithfulness.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scorers/grounding.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scorers/safety.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scorers/stability.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scripts/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scripts/bootstrap_dataset.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scripts/calibrate_persona.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scripts/run_openai_demo.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/scripts/sanitize_dataset.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/sdk.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/storage/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/storage/evaluations.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/storage/reviews.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/storage/runs.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/utils/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/utils/io.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/utils/optional.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/utils/tokens.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/utils/yaml.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter.egg-info/dependency_links.txt +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter.egg-info/entry_points.txt +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter.egg-info/requires.txt +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter.egg-info/top_level.txt +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/__init__.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/conftest.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/data/durable_evaluation_judge.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/data/durable_evaluation_worker.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/data/durable_recovery_target.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/data/durable_recovery_worker.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/data/durable_run_worker.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/data/mini_cli_dataset.jsonl +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_authenticity_judge.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_builtin_evaluations.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_calibrate_persona.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_capture_recovery.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_cli_errors.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_cli_grounded.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_cli_helpers.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_cli_import.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_cli_init.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_cli_run_config.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_config.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_durable_evaluations.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_durable_execution.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_faithfulness.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_github_comment.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_grounding.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_html_report.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_judge_providers.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_offline_safety.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_persona_gpt.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_provider_local.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_provider_openai.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_providers.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_release_workflow.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_review_workflow.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_run_config_grounded.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_run_config_loader.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_run_openai_demo.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_runner.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_sampling.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_scorers.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_smoke.py +0 -0
- {alignmenter-0.3.1 → alignmenter-0.3.2}/tests/test_suite_archive.py +0 -0
|
@@ -11,6 +11,28 @@ Alignmenter datasets serve two primary purposes:
|
|
|
11
11
|
|
|
12
12
|
All datasets in this repository follow strict data hygiene practices to protect user privacy and enable reproducible research.
|
|
13
13
|
|
|
14
|
+
## Dataset management commands
|
|
15
|
+
|
|
16
|
+
The `alignmenter dataset` sub-app manages datasets as content-addressed, versioned
|
|
17
|
+
artifacts:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
alignmenter dataset stats data.jsonl [--json] # coverage: counts, roles, tag/persona histograms
|
|
21
|
+
alignmenter dataset validate data.jsonl [--strict] # canonical schema check (exit 1 on errors)
|
|
22
|
+
alignmenter dataset dedupe data.jsonl --out out.jsonl # drop content-identical records
|
|
23
|
+
alignmenter dataset merge a.jsonl b.jsonl --out m.jsonl [--dedupe] [--namespace-sessions]
|
|
24
|
+
alignmenter dataset split data.jsonl --out dir/ --holdout 0.2 --by split_group # group-aware train/holdout
|
|
25
|
+
alignmenter dataset manifest data.jsonl --out manifest.json # content-addressed manifest + provenance
|
|
26
|
+
alignmenter dataset manifest data.jsonl --verify manifest.json # confirm the data still matches (exit 2 on drift)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`validate` checks the row schema below (lenient by default, `--strict` for the fuller
|
|
30
|
+
contract); `lint` additionally checks turn sequencing, scenario-tag coverage, and
|
|
31
|
+
persona files. `split` keeps a
|
|
32
|
+
case and its counterfactual variants on the same side of the boundary via the
|
|
33
|
+
grouping unit (`split_group` / `group:` tag / session / persona). `manifest`'s digest
|
|
34
|
+
is order-independent (reordering rows does not change the dataset's identity).
|
|
35
|
+
|
|
14
36
|
## Dataset Format
|
|
15
37
|
|
|
16
38
|
### Conversation JSONL Schema
|
|
@@ -21,6 +21,7 @@ import typer
|
|
|
21
21
|
import yaml
|
|
22
22
|
|
|
23
23
|
from alignmenter.config import get_settings
|
|
24
|
+
from alignmenter.dataset_cli import register_dataset_commands
|
|
24
25
|
from alignmenter.evaluators.custom import evaluator_registry, load_evaluators
|
|
25
26
|
from alignmenter.execution.evaluation import evaluate_saved, evaluation_summary
|
|
26
27
|
from alignmenter.execution.recovery import resume_capture
|
|
@@ -58,6 +59,8 @@ app.add_typer(dataset_app, name="dataset")
|
|
|
58
59
|
app.add_typer(import_app, name="import")
|
|
59
60
|
app.add_typer(calibrate_app, name="calibrate")
|
|
60
61
|
|
|
62
|
+
register_dataset_commands(dataset_app)
|
|
63
|
+
|
|
61
64
|
|
|
62
65
|
def _show_version(value):
|
|
63
66
|
if value:
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Dataset-management commands: stats, validate, dedupe, merge, split, manifest.
|
|
2
|
+
|
|
3
|
+
Registered onto the existing `dataset` sub-app (alongside lint/sanitize/bootstrap)
|
|
4
|
+
via register_dataset_commands(dataset_app). Builds on the shared primitives
|
|
5
|
+
(content_digest, read/write_jsonl) and schemas/dataset.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import random
|
|
12
|
+
from collections import Counter, defaultdict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from alignmenter.schemas.dataset import (
|
|
18
|
+
ProvenanceEntry,
|
|
19
|
+
build_manifest,
|
|
20
|
+
dataset_digest,
|
|
21
|
+
validate_records,
|
|
22
|
+
)
|
|
23
|
+
from alignmenter.schemas.execution import content_digest
|
|
24
|
+
from alignmenter.utils.io import read_jsonl, write_json, write_jsonl
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _group_key(record: dict, by: str) -> str:
|
|
28
|
+
"""Grouping key for split — keeps a case + its variants on the same side."""
|
|
29
|
+
if not isinstance(record, dict):
|
|
30
|
+
return "" # non-dict rows group together (like the other commands, don't crash)
|
|
31
|
+
metadata = record.get("metadata") or {}
|
|
32
|
+
session = record.get("session_id") or ""
|
|
33
|
+
if by == "split_group":
|
|
34
|
+
return str(metadata.get("split_group") or session)
|
|
35
|
+
if by == "group":
|
|
36
|
+
for tag in record.get("tags") or []:
|
|
37
|
+
if isinstance(tag, str) and tag.startswith("group:"):
|
|
38
|
+
return tag
|
|
39
|
+
return session
|
|
40
|
+
if by == "persona":
|
|
41
|
+
return str(record.get("persona_id") or session)
|
|
42
|
+
return session
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register_dataset_commands(dataset_app: typer.Typer) -> None:
|
|
46
|
+
@dataset_app.command("stats")
|
|
47
|
+
def stats(
|
|
48
|
+
path: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
49
|
+
as_json: bool = typer.Option(False, "--json", help="Emit the coverage report as JSON."),
|
|
50
|
+
):
|
|
51
|
+
"""Coverage report — record/session counts, role balance, tag + persona histograms."""
|
|
52
|
+
records = read_jsonl(path)
|
|
53
|
+
manifest = build_manifest(records, id=path.stem, revision="stats")
|
|
54
|
+
roles = Counter(r.get("role") for r in records if isinstance(r, dict))
|
|
55
|
+
with_context = sum(
|
|
56
|
+
1 for r in records
|
|
57
|
+
if isinstance(r, dict) and isinstance(r.get("metadata"), dict) and "context" in r["metadata"]
|
|
58
|
+
)
|
|
59
|
+
if as_json:
|
|
60
|
+
typer.echo(json.dumps({
|
|
61
|
+
"records": manifest.record_count, "sessions": manifest.session_count,
|
|
62
|
+
"roles": {k: v for k, v in roles.items()}, "with_context": with_context,
|
|
63
|
+
"tags": manifest.tags, "personas": manifest.personas,
|
|
64
|
+
"content_digest": manifest.content_digest,
|
|
65
|
+
}, indent=2))
|
|
66
|
+
return
|
|
67
|
+
typer.echo(f"records {manifest.record_count} · sessions {manifest.session_count} · turns-with-context {with_context}")
|
|
68
|
+
typer.echo("roles: " + ", ".join(f"{k}={v}" for k, v in sorted(roles.items(), key=lambda kv: str(kv[0]))))
|
|
69
|
+
if manifest.tags:
|
|
70
|
+
typer.echo("tags: " + ", ".join(f"{k}={v}" for k, v in sorted(manifest.tags.items())))
|
|
71
|
+
if manifest.personas:
|
|
72
|
+
typer.echo("personas: " + ", ".join(f"{k}={v}" for k, v in sorted(manifest.personas.items())))
|
|
73
|
+
typer.echo(f"content_digest: {manifest.content_digest[:12]}…")
|
|
74
|
+
|
|
75
|
+
@dataset_app.command("validate")
|
|
76
|
+
def validate(
|
|
77
|
+
path: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
78
|
+
strict: bool = typer.Option(False, "--strict", help="Require turn_index+tags+persona_id and an assistant turn per session."),
|
|
79
|
+
):
|
|
80
|
+
"""Schema validator — lenient by default, --strict adds turn_index+tags+persona_id + an assistant turn per session.
|
|
81
|
+
|
|
82
|
+
Complements `dataset lint`, which additionally checks turn-index contiguity,
|
|
83
|
+
scenario-tag coverage, and persona-file existence.
|
|
84
|
+
"""
|
|
85
|
+
records = read_jsonl(path)
|
|
86
|
+
errors = validate_records(records, strict=strict)
|
|
87
|
+
for error in errors:
|
|
88
|
+
typer.echo(error, err=True)
|
|
89
|
+
typer.echo(f"{len(records)} records, {len(errors)} error(s)")
|
|
90
|
+
raise typer.Exit(1 if errors else 0)
|
|
91
|
+
|
|
92
|
+
@dataset_app.command("dedupe")
|
|
93
|
+
def dedupe(
|
|
94
|
+
path: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
95
|
+
out: Path | None = typer.Option(None, "--out", help="Output path (default: <stem>.dedup.jsonl)."),
|
|
96
|
+
in_place: bool = typer.Option(False, "--in-place"),
|
|
97
|
+
):
|
|
98
|
+
"""Drop content-identical duplicate records (order preserved)."""
|
|
99
|
+
records = read_jsonl(path)
|
|
100
|
+
seen: set[str] = set()
|
|
101
|
+
kept = []
|
|
102
|
+
for record in records:
|
|
103
|
+
digest = content_digest(record)
|
|
104
|
+
if digest in seen:
|
|
105
|
+
continue
|
|
106
|
+
seen.add(digest)
|
|
107
|
+
kept.append(record)
|
|
108
|
+
destination = path if in_place else (out or path.with_name(f"{path.stem}.dedup.jsonl"))
|
|
109
|
+
write_jsonl(destination, kept)
|
|
110
|
+
typer.echo(f"removed {len(records) - len(kept)} duplicate(s); wrote {len(kept)} -> {destination}")
|
|
111
|
+
|
|
112
|
+
@dataset_app.command("merge")
|
|
113
|
+
def merge(
|
|
114
|
+
paths: list[Path] = typer.Argument(..., exists=True, dir_okay=False),
|
|
115
|
+
out: Path = typer.Option(..., "--out"),
|
|
116
|
+
dedupe: bool = typer.Option(False, "--dedupe", help="Drop content-identical records after merging."),
|
|
117
|
+
namespace_sessions: bool = typer.Option(False, "--namespace-sessions", help="Prefix session_id with the source file stem to avoid collisions."),
|
|
118
|
+
):
|
|
119
|
+
"""Concatenate datasets into one (optionally dedupe / namespace session ids)."""
|
|
120
|
+
merged: list[dict] = []
|
|
121
|
+
for source in paths:
|
|
122
|
+
records = read_jsonl(source)
|
|
123
|
+
if namespace_sessions:
|
|
124
|
+
for record in records:
|
|
125
|
+
if isinstance(record, dict) and record.get("session_id"):
|
|
126
|
+
record["session_id"] = f"{source.stem}:{record['session_id']}"
|
|
127
|
+
merged.extend(records)
|
|
128
|
+
if dedupe:
|
|
129
|
+
seen: set[str] = set()
|
|
130
|
+
deduped = []
|
|
131
|
+
for record in merged:
|
|
132
|
+
digest = content_digest(record)
|
|
133
|
+
if digest in seen:
|
|
134
|
+
continue
|
|
135
|
+
seen.add(digest)
|
|
136
|
+
deduped.append(record)
|
|
137
|
+
merged = deduped
|
|
138
|
+
write_jsonl(out, merged)
|
|
139
|
+
typer.echo(f"merged {len(paths)} file(s) -> {len(merged)} records -> {out}")
|
|
140
|
+
|
|
141
|
+
@dataset_app.command("split")
|
|
142
|
+
def split(
|
|
143
|
+
path: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
144
|
+
out: Path = typer.Option(..., "--out", help="Output directory for train.jsonl + holdout.jsonl."),
|
|
145
|
+
holdout: float = typer.Option(0.2, "--holdout", min=0.0, max=1.0, help="Target holdout fraction of records."),
|
|
146
|
+
by: str = typer.Option("split_group", "--by", help="Grouping unit kept together: split_group | group | session | persona."),
|
|
147
|
+
seed: int = typer.Option(42, "--seed"),
|
|
148
|
+
):
|
|
149
|
+
"""Group-aware train/holdout split — a case and its variants never straddle the boundary."""
|
|
150
|
+
if by not in {"split_group", "group", "session", "persona"}:
|
|
151
|
+
raise typer.BadParameter("--by must be split_group, group, session, or persona")
|
|
152
|
+
records = read_jsonl(path)
|
|
153
|
+
groups: dict[str, list[dict]] = defaultdict(list)
|
|
154
|
+
for record in records:
|
|
155
|
+
groups[_group_key(record, by)].append(record)
|
|
156
|
+
names = sorted(groups)
|
|
157
|
+
random.Random(seed).shuffle(names)
|
|
158
|
+
target = holdout * len(records)
|
|
159
|
+
held: set[str] = set()
|
|
160
|
+
accumulated = 0
|
|
161
|
+
for name in names:
|
|
162
|
+
if accumulated >= target:
|
|
163
|
+
break
|
|
164
|
+
held.add(name)
|
|
165
|
+
accumulated += len(groups[name])
|
|
166
|
+
train = [r for r in records if _group_key(r, by) not in held]
|
|
167
|
+
holdout_records = [r for r in records if _group_key(r, by) in held]
|
|
168
|
+
write_jsonl(Path(out) / "train.jsonl", train)
|
|
169
|
+
write_jsonl(Path(out) / "holdout.jsonl", holdout_records)
|
|
170
|
+
typer.echo(
|
|
171
|
+
f"train {len(train)} · holdout {len(holdout_records)} "
|
|
172
|
+
f"({len(held)}/{len(names)} '{by}' groups) -> {out}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@dataset_app.command("manifest")
|
|
176
|
+
def manifest(
|
|
177
|
+
path: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
178
|
+
out: Path | None = typer.Option(None, "--out", help="Write the manifest JSON here (default: stdout)."),
|
|
179
|
+
identifier: str | None = typer.Option(None, "--id", help="Dataset id (default: file stem)."),
|
|
180
|
+
revision: str = typer.Option("v1", "--revision"),
|
|
181
|
+
verify: Path | None = typer.Option(None, "--verify", help="Verify the data still matches this manifest's content_digest."),
|
|
182
|
+
):
|
|
183
|
+
"""Build (or --verify) a content-addressed dataset manifest with provenance."""
|
|
184
|
+
records = read_jsonl(path)
|
|
185
|
+
if verify is not None:
|
|
186
|
+
try:
|
|
187
|
+
expected = json.loads(Path(verify).read_text()).get("content_digest")
|
|
188
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
189
|
+
raise typer.BadParameter(f"Could not read manifest {verify}: {exc}") from exc
|
|
190
|
+
actual = dataset_digest(records)
|
|
191
|
+
match = expected == actual
|
|
192
|
+
typer.echo(f"{'OK' if match else 'MISMATCH'}: manifest {str(expected)[:12]} vs data {actual[:12]}")
|
|
193
|
+
raise typer.Exit(0 if match else 2)
|
|
194
|
+
built = build_manifest(
|
|
195
|
+
records,
|
|
196
|
+
id=identifier or path.stem,
|
|
197
|
+
revision=revision,
|
|
198
|
+
provenance=(ProvenanceEntry(kind="authored", ref=str(path), count=len(records), digest=dataset_digest(records)),),
|
|
199
|
+
)
|
|
200
|
+
payload = built.model_dump(mode="json")
|
|
201
|
+
if out is not None:
|
|
202
|
+
write_json(out, payload)
|
|
203
|
+
typer.echo(f"wrote manifest -> {out}")
|
|
204
|
+
else:
|
|
205
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Dataset manifest + canonical record validation for dataset management.
|
|
2
|
+
|
|
3
|
+
Datasets are flat JSONL of turn records (see datasets/README.md). `validate_records`
|
|
4
|
+
is a schema validator: lenient by default (matching what capture requires —
|
|
5
|
+
session_id + role + text, unique turn_index per session), `strict=True` adds
|
|
6
|
+
turn_index + tags + persona_id and an assistant turn per session. It complements
|
|
7
|
+
`dataset lint`, which additionally checks turn-index contiguity, scenario-tag
|
|
8
|
+
coverage, and persona-file existence. `DatasetManifest` gives a dataset a
|
|
9
|
+
content-addressed identity + provenance so a curated set can be versioned and its
|
|
10
|
+
lineage tracked.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections import Counter, defaultdict
|
|
16
|
+
|
|
17
|
+
from pydantic import AwareDatetime, Field
|
|
18
|
+
|
|
19
|
+
from alignmenter.schemas.evaluation import NonBlank
|
|
20
|
+
from alignmenter.schemas.execution import (
|
|
21
|
+
Digest,
|
|
22
|
+
NonNegativeInt,
|
|
23
|
+
VersionedRecord,
|
|
24
|
+
content_digest,
|
|
25
|
+
utc_now,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
ROLES = {"user", "assistant"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dataset_digest(records) -> str:
|
|
32
|
+
"""Order-independent content hash of a dataset (same records, any order → same digest).
|
|
33
|
+
|
|
34
|
+
content_digest sorts dict keys but preserves list order, so hashing the record
|
|
35
|
+
list directly would change when rows are merely reordered. Hashing the sorted
|
|
36
|
+
per-record digests gives a stable content identity for versioning/verification.
|
|
37
|
+
|
|
38
|
+
Note: this is distinct from RunManifest.dataset_digest (runner.py), which is the
|
|
39
|
+
order-SENSITIVE content_digest(records) used for run reproducibility — don't
|
|
40
|
+
compare the two.
|
|
41
|
+
"""
|
|
42
|
+
return content_digest(sorted(content_digest(r) for r in records))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProvenanceEntry(VersionedRecord):
|
|
46
|
+
kind: NonBlank # authored | bootstrap | import | merge | dedupe | split | promote
|
|
47
|
+
ref: str = "" # source path / url / description
|
|
48
|
+
count: NonNegativeInt = 0
|
|
49
|
+
digest: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DatasetManifest(VersionedRecord):
|
|
53
|
+
id: NonBlank
|
|
54
|
+
revision: NonBlank
|
|
55
|
+
content_digest: Digest
|
|
56
|
+
record_count: NonNegativeInt
|
|
57
|
+
session_count: NonNegativeInt
|
|
58
|
+
created_at: AwareDatetime = Field(default_factory=utc_now)
|
|
59
|
+
tags: dict[str, int] = Field(default_factory=dict)
|
|
60
|
+
personas: dict[str, int] = Field(default_factory=dict)
|
|
61
|
+
provenance: tuple[ProvenanceEntry, ...] = ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_manifest(records, *, id: str, revision: str, provenance=()) -> DatasetManifest:
|
|
65
|
+
"""Content-address a record list into a DatasetManifest (order-independent digest)."""
|
|
66
|
+
dicts = [r for r in records if isinstance(r, dict)]
|
|
67
|
+
sessions = {r.get("session_id") for r in dicts if r.get("session_id")}
|
|
68
|
+
tags = Counter(t for r in dicts for t in (r.get("tags") or []) if isinstance(t, str))
|
|
69
|
+
personas = Counter(r["persona_id"] for r in dicts if isinstance(r.get("persona_id"), str))
|
|
70
|
+
return DatasetManifest(
|
|
71
|
+
id=id,
|
|
72
|
+
revision=revision,
|
|
73
|
+
content_digest=dataset_digest(records),
|
|
74
|
+
record_count=len(records),
|
|
75
|
+
session_count=len(sessions),
|
|
76
|
+
tags=dict(tags),
|
|
77
|
+
personas=dict(personas),
|
|
78
|
+
provenance=tuple(provenance),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_records(records, *, strict: bool = False) -> list[str]:
|
|
83
|
+
"""Return a list of human-readable schema errors (empty == valid).
|
|
84
|
+
|
|
85
|
+
Default rules match ingest (session_id + role + text; turn_index optional but,
|
|
86
|
+
when present, a non-negative int unique within its session). ``strict`` adds the
|
|
87
|
+
lint contract: turn_index + tags + persona_id required, and every session must
|
|
88
|
+
have an assistant turn.
|
|
89
|
+
"""
|
|
90
|
+
errors: list[str] = []
|
|
91
|
+
seen_turns: dict[str, set[int]] = defaultdict(set)
|
|
92
|
+
session_roles: dict[str, set] = defaultdict(set)
|
|
93
|
+
|
|
94
|
+
for i, r in enumerate(records):
|
|
95
|
+
loc = f"record {i}"
|
|
96
|
+
if not isinstance(r, dict):
|
|
97
|
+
errors.append(f"{loc}: not a JSON object")
|
|
98
|
+
continue
|
|
99
|
+
sid = r.get("session_id")
|
|
100
|
+
if not isinstance(sid, str) or not sid.strip():
|
|
101
|
+
errors.append(f"{loc}: missing or blank session_id")
|
|
102
|
+
role = r.get("role")
|
|
103
|
+
if role not in ROLES:
|
|
104
|
+
errors.append(f"{loc}: role must be one of {sorted(ROLES)} (got {role!r})")
|
|
105
|
+
if not isinstance(r.get("text"), str):
|
|
106
|
+
errors.append(f"{loc}: text must be a string")
|
|
107
|
+
|
|
108
|
+
ti = r.get("turn_index")
|
|
109
|
+
if ti is not None:
|
|
110
|
+
if isinstance(ti, bool) or not isinstance(ti, int) or ti < 0:
|
|
111
|
+
errors.append(f"{loc}: turn_index must be a non-negative int")
|
|
112
|
+
elif isinstance(sid, str):
|
|
113
|
+
if ti in seen_turns[sid]:
|
|
114
|
+
errors.append(f"{loc}: duplicate turn_index {ti} in session {sid!r}")
|
|
115
|
+
seen_turns[sid].add(ti)
|
|
116
|
+
|
|
117
|
+
tags = r.get("tags")
|
|
118
|
+
if tags is not None and not (isinstance(tags, list) and all(isinstance(t, str) for t in tags)):
|
|
119
|
+
errors.append(f"{loc}: tags must be a list of strings")
|
|
120
|
+
md = r.get("metadata")
|
|
121
|
+
if md is not None and not isinstance(md, dict):
|
|
122
|
+
errors.append(f"{loc}: metadata must be an object")
|
|
123
|
+
|
|
124
|
+
if isinstance(sid, str) and role in ROLES:
|
|
125
|
+
session_roles[sid].add(role)
|
|
126
|
+
|
|
127
|
+
if strict:
|
|
128
|
+
if ti is None:
|
|
129
|
+
errors.append(f"{loc}: turn_index is required in --strict mode")
|
|
130
|
+
if not isinstance(tags, list):
|
|
131
|
+
errors.append(f"{loc}: tags is required in --strict mode")
|
|
132
|
+
if not isinstance(r.get("persona_id"), str):
|
|
133
|
+
errors.append(f"{loc}: persona_id is required in --strict mode")
|
|
134
|
+
|
|
135
|
+
if strict:
|
|
136
|
+
for sid, roles in session_roles.items():
|
|
137
|
+
if "assistant" not in roles:
|
|
138
|
+
errors.append(f"session {sid!r}: has no assistant turn (--strict)")
|
|
139
|
+
return errors
|
|
@@ -16,6 +16,7 @@ src/alignmenter/__init__.py
|
|
|
16
16
|
src/alignmenter/_version.py
|
|
17
17
|
src/alignmenter/cli.py
|
|
18
18
|
src/alignmenter/config.py
|
|
19
|
+
src/alignmenter/dataset_cli.py
|
|
19
20
|
src/alignmenter/release_cli.py
|
|
20
21
|
src/alignmenter/run_config.py
|
|
21
22
|
src/alignmenter/runner.py
|
|
@@ -81,6 +82,7 @@ src/alignmenter/reporting/github_comment.py
|
|
|
81
82
|
src/alignmenter/reporting/html.py
|
|
82
83
|
src/alignmenter/reporting/json_out.py
|
|
83
84
|
src/alignmenter/schemas/__init__.py
|
|
85
|
+
src/alignmenter/schemas/dataset.py
|
|
84
86
|
src/alignmenter/schemas/evaluation.py
|
|
85
87
|
src/alignmenter/schemas/execution.py
|
|
86
88
|
src/alignmenter/schemas/gates.py
|
|
@@ -121,6 +123,7 @@ tests/test_cli_import.py
|
|
|
121
123
|
tests/test_cli_init.py
|
|
122
124
|
tests/test_cli_run_config.py
|
|
123
125
|
tests/test_config.py
|
|
126
|
+
tests/test_dataset_management.py
|
|
124
127
|
tests/test_durable_evaluations.py
|
|
125
128
|
tests/test_durable_execution.py
|
|
126
129
|
tests/test_faithfulness.py
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Dataset-management: canonical validation, manifest, and the dataset CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from alignmenter.cli import app
|
|
11
|
+
from alignmenter.schemas.dataset import build_manifest, validate_records
|
|
12
|
+
from alignmenter.utils.io import read_jsonl, write_jsonl
|
|
13
|
+
|
|
14
|
+
runner = CliRunner()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _records():
|
|
18
|
+
return [
|
|
19
|
+
{"session_id": "a", "turn_index": 0, "role": "user", "text": "hi", "tags": ["family:g"], "persona_id": "aver"},
|
|
20
|
+
{"session_id": "a", "turn_index": 1, "role": "assistant", "text": "hello", "tags": ["family:g"], "persona_id": "aver"},
|
|
21
|
+
{"session_id": "b", "turn_index": 0, "role": "user", "text": "q", "tags": ["group:x"], "persona_id": "aver"},
|
|
22
|
+
{"session_id": "b", "turn_index": 1, "role": "assistant", "text": "a", "tags": ["group:x"], "persona_id": "aver"},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write(tmp_path, records, name="data.jsonl") -> Path:
|
|
27
|
+
p = tmp_path / name
|
|
28
|
+
write_jsonl(p, records)
|
|
29
|
+
return p
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---- schema / manifest ----
|
|
33
|
+
|
|
34
|
+
def test_validate_accepts_good_records():
|
|
35
|
+
assert validate_records(_records()) == []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_validate_flags_missing_session_bad_role_and_dup_turn():
|
|
39
|
+
bad = [
|
|
40
|
+
{"role": "user", "text": "x"}, # missing session_id
|
|
41
|
+
{"session_id": "s", "role": "bot", "text": "x"}, # bad role
|
|
42
|
+
{"session_id": "s", "turn_index": 3, "role": "user", "text": "x"},
|
|
43
|
+
{"session_id": "s", "turn_index": 3, "role": "assistant", "text": "x"}, # dup turn_index
|
|
44
|
+
]
|
|
45
|
+
errors = validate_records(bad)
|
|
46
|
+
assert any("session_id" in e for e in errors)
|
|
47
|
+
assert any("role must be one of" in e for e in errors)
|
|
48
|
+
assert any("duplicate turn_index" in e for e in errors)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_validate_strict_requires_persona_and_assistant_turn():
|
|
52
|
+
records = [{"session_id": "s", "turn_index": 0, "role": "user", "text": "x", "tags": []}] # no persona, no assistant
|
|
53
|
+
errors = validate_records(records, strict=True)
|
|
54
|
+
assert any("persona_id is required" in e for e in errors)
|
|
55
|
+
assert any("has no assistant turn" in e for e in errors)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_manifest_digest_is_order_independent_and_counts_are_right():
|
|
59
|
+
m1 = build_manifest(_records(), id="d", revision="v1")
|
|
60
|
+
m2 = build_manifest(list(reversed(_records())), id="d", revision="v1")
|
|
61
|
+
assert m1.content_digest == m2.content_digest # canonical, order-independent
|
|
62
|
+
assert m1.record_count == 4 and m1.session_count == 2
|
|
63
|
+
assert m1.personas == {"aver": 4}
|
|
64
|
+
assert m1.tags["family:g"] == 2 and m1.tags["group:x"] == 2
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---- CLI ----
|
|
68
|
+
|
|
69
|
+
def test_cli_stats_json(tmp_path):
|
|
70
|
+
p = _write(tmp_path, _records())
|
|
71
|
+
res = runner.invoke(app, ["dataset", "stats", str(p), "--json"])
|
|
72
|
+
assert res.exit_code == 0, res.output
|
|
73
|
+
report = json.loads(res.output)
|
|
74
|
+
assert report["records"] == 4 and report["sessions"] == 2 and report["roles"]["assistant"] == 2
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cli_validate_exit_codes(tmp_path):
|
|
78
|
+
good = _write(tmp_path, _records(), "good.jsonl")
|
|
79
|
+
assert runner.invoke(app, ["dataset", "validate", str(good)]).exit_code == 0
|
|
80
|
+
bad = _write(tmp_path, [{"role": "user", "text": "x"}], "bad.jsonl")
|
|
81
|
+
assert runner.invoke(app, ["dataset", "validate", str(bad)]).exit_code == 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_cli_dedupe(tmp_path):
|
|
85
|
+
p = _write(tmp_path, _records() + _records()[:1]) # one duplicate
|
|
86
|
+
out = tmp_path / "dd.jsonl"
|
|
87
|
+
res = runner.invoke(app, ["dataset", "dedupe", str(p), "--out", str(out)])
|
|
88
|
+
assert res.exit_code == 0 and "removed 1 duplicate" in res.output
|
|
89
|
+
assert len(read_jsonl(out)) == 4
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_cli_merge_namespaces_sessions(tmp_path):
|
|
93
|
+
a = _write(tmp_path, _records()[:2], "a.jsonl")
|
|
94
|
+
b = _write(tmp_path, _records()[2:], "b.jsonl")
|
|
95
|
+
out = tmp_path / "m.jsonl"
|
|
96
|
+
res = runner.invoke(app, ["dataset", "merge", str(a), str(b), "--out", str(out), "--namespace-sessions"])
|
|
97
|
+
assert res.exit_code == 0
|
|
98
|
+
sessions = {r["session_id"] for r in read_jsonl(out)}
|
|
99
|
+
assert sessions == {"a:a", "b:b"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_cli_split_keeps_groups_together(tmp_path):
|
|
103
|
+
# two group:* groups; a holdout must contain whole groups, never split one across sides
|
|
104
|
+
records = []
|
|
105
|
+
for g in ("g1", "g2"):
|
|
106
|
+
for i in range(3):
|
|
107
|
+
records.append({"session_id": f"{g}-{i}", "role": "assistant", "text": "x", "tags": [f"group:{g}"]})
|
|
108
|
+
p = _write(tmp_path, records)
|
|
109
|
+
out = tmp_path / "split"
|
|
110
|
+
res = runner.invoke(app, ["dataset", "split", str(p), "--out", str(out), "--by", "group", "--holdout", "0.5"])
|
|
111
|
+
assert res.exit_code == 0, res.output
|
|
112
|
+
train = {t for r in read_jsonl(out / "train.jsonl") for t in r["tags"]}
|
|
113
|
+
hold = {t for r in read_jsonl(out / "holdout.jsonl") for t in r["tags"]}
|
|
114
|
+
assert train and hold and train.isdisjoint(hold) # no group straddles the boundary
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_cli_split_tolerates_non_dict_rows(tmp_path):
|
|
118
|
+
p = tmp_path / "mixed.jsonl"
|
|
119
|
+
write_jsonl(p, _records() + [[1, 2, 3]]) # a non-dict row must not crash split
|
|
120
|
+
out = tmp_path / "s"
|
|
121
|
+
res = runner.invoke(app, ["dataset", "split", str(p), "--out", str(out), "--by", "session", "--holdout", "0.5"])
|
|
122
|
+
assert res.exit_code == 0, res.output
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cli_manifest_verify_bad_json_errors_friendly(tmp_path):
|
|
126
|
+
p = _write(tmp_path, _records())
|
|
127
|
+
bad = tmp_path / "bad-manifest.json"
|
|
128
|
+
bad.write_text("not json at all")
|
|
129
|
+
res = runner.invoke(app, ["dataset", "manifest", str(p), "--verify", str(bad)])
|
|
130
|
+
assert res.exit_code != 0
|
|
131
|
+
assert "Could not read manifest" in res.output
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_cli_manifest_build_and_verify(tmp_path):
|
|
135
|
+
p = _write(tmp_path, _records())
|
|
136
|
+
man = tmp_path / "manifest.json"
|
|
137
|
+
assert runner.invoke(app, ["dataset", "manifest", str(p), "--out", str(man)]).exit_code == 0
|
|
138
|
+
assert json.loads(man.read_text())["record_count"] == 4
|
|
139
|
+
assert runner.invoke(app, ["dataset", "manifest", str(p), "--verify", str(man)]).exit_code == 0
|
|
140
|
+
# mutate the data → verify must fail
|
|
141
|
+
write_jsonl(p, _records() + [{"session_id": "z", "role": "user", "text": "new"}])
|
|
142
|
+
assert runner.invoke(app, ["dataset", "manifest", str(p), "--verify", str(man)]).exit_code == 2
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/configs/judges/safety_prompt.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alignmenter-0.3.1 → alignmenter-0.3.2}/src/alignmenter/data/datasets/demo_conversations.jsonl
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|