sediment-api 0.1.0__py3-none-any.whl

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,311 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """DPO/SFT dataset diagnostics.
3
+
4
+ Prints ``sediment_export.generate_dataset_diagnostics`` for one org: compact
5
+ tables on stdout by default, or ``--json`` for machines.
6
+
7
+ sediment report dataset-diagnostics --org acme-corp
8
+ sediment report dataset-diagnostics --org acme-corp --json
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import sys
17
+ from dataclasses import asdict
18
+
19
+ from sediment_core import normalize_org_id
20
+ from sediment_derive import MirrorManager
21
+ from sediment_export import (
22
+ DEFAULT_DPO_NEAR_DUPLICATE_THRESHOLD,
23
+ DPOPolicy,
24
+ DatasetDiagnostics,
25
+ SFTPolicy,
26
+ generate_dataset_diagnostics,
27
+ )
28
+
29
+ from ..database import add_database_url_argument, one_shot_fact_store
30
+
31
+ _DEFAULT_MIRROR_PATH = "./mirrors"
32
+
33
+
34
+ def _recipe_label(recipe_id: str, recipe_version: int) -> str:
35
+ return f"{recipe_id} v{recipe_version}"
36
+
37
+
38
+ def _source_label(
39
+ eligibility_source: str | None,
40
+ chosen_label_source: str | None,
41
+ rejected_label_source: str | None,
42
+ ) -> str:
43
+ if eligibility_source is not None:
44
+ return eligibility_source
45
+ return f"{chosen_label_source}/{rejected_label_source}"
46
+
47
+
48
+ def _print_balance(report: DatasetDiagnostics) -> None:
49
+ print("class/model balance")
50
+ if not report.model_balance:
51
+ print(" no data")
52
+ return
53
+ print(f"{'dataset':<8} {'recipe':<16} {'source':<32} {'model':<28} {'rows':>8}")
54
+ for row in report.model_balance:
55
+ source = _source_label(
56
+ row.eligibility_source,
57
+ row.chosen_label_source,
58
+ row.rejected_label_source,
59
+ )
60
+ print(
61
+ f"{row.dataset:<8} {_recipe_label(row.recipe_id, row.recipe_version):<16} "
62
+ f"{source:<32} {row.model:<28} {row.rows:>8}"
63
+ )
64
+
65
+
66
+ def _print_distributions(report: DatasetDiagnostics) -> None:
67
+ print("confidence distribution")
68
+ if not report.confidence_distributions:
69
+ print(" no data")
70
+ return
71
+ print(
72
+ f"{'dataset':<8} {'recipe':<16} {'source':<32} {'metric':<12} "
73
+ f"{'model':<28} {'count':>7} {'mean':>8} {'median':>8} {'min':>8} "
74
+ f"{'max':>8} {'stdev':>8}"
75
+ )
76
+ for row in report.confidence_distributions:
77
+ stats = row.stats
78
+ source = _source_label(
79
+ row.eligibility_source,
80
+ row.chosen_label_source,
81
+ row.rejected_label_source,
82
+ )
83
+ print(
84
+ f"{row.dataset:<8} {_recipe_label(row.recipe_id, row.recipe_version):<16} "
85
+ f"{source:<32} {row.metric:<12} {row.model:<28} "
86
+ f"{stats.count:>7} {stats.mean:>8.3f} {stats.median:>8.3f} "
87
+ f"{stats.min:>8.3f} {stats.max:>8.3f} {stats.stdev:>8.3f}"
88
+ )
89
+
90
+
91
+ def _print_duplicates(report: DatasetDiagnostics) -> None:
92
+ print("cross-split prompt duplicates")
93
+ for row in report.cross_split_duplicates:
94
+ source = _source_label(
95
+ row.eligibility_source,
96
+ row.chosen_label_source,
97
+ row.rejected_label_source,
98
+ )
99
+ print(
100
+ f"{row.dataset} {_recipe_label(row.recipe_id, row.recipe_version)} "
101
+ f"{source}: {row.duplicate_prompt_count}"
102
+ )
103
+ for example in row.examples:
104
+ print(
105
+ f" train={example.train_count} eval={example.eval_count} "
106
+ f"prompt={example.prompt}"
107
+ )
108
+
109
+
110
+ def _print_floor_exclusions(report: DatasetDiagnostics) -> None:
111
+ print("confidence-floor exclusions")
112
+ if not report.confidence_floor_exclusions:
113
+ print(" no data")
114
+ return
115
+ print(
116
+ f"{'dataset':<8} {'recipe':<16} {'source':<32} {'model':<28} "
117
+ f"{'eligible':>10} {'excluded':>10} {'rate':>8}"
118
+ )
119
+ for row in report.confidence_floor_exclusions:
120
+ print(
121
+ f"{row.dataset:<8} {_recipe_label(row.recipe_id, row.recipe_version):<16} "
122
+ f"{row.eligibility_source:<32} {row.model:<28} "
123
+ f"{row.otherwise_eligible_completions:>10} "
124
+ f"{row.excluded_by_floor:>10} {row.exclusion_rate:>8.3f}"
125
+ )
126
+
127
+
128
+ def _print_dpo_near_duplicates(report: DatasetDiagnostics) -> None:
129
+ print("dpo near-duplicate prompt buckets")
130
+ if not report.dpo_near_duplicate_buckets:
131
+ print(" no data")
132
+ return
133
+ for row in report.dpo_near_duplicate_buckets:
134
+ print(
135
+ f" recipe={row.recipe_id} v{row.recipe_version} "
136
+ f"sources={row.chosen_label_source}/{row.rejected_label_source} "
137
+ f"buckets={row.bucket_count} compared={row.comparable_pair_count} "
138
+ f"threshold={row.threshold:.3f} near_misses={row.near_miss_pair_count}"
139
+ )
140
+ for example in row.examples:
141
+ print(
142
+ f" org={example.org_id} model={example.model} "
143
+ f"similarity={example.similarity:.3f}"
144
+ )
145
+
146
+
147
+ def _print_dpo_bucket_sparsity(report: DatasetDiagnostics) -> None:
148
+ print("dpo prompt-bucket sparsity")
149
+ if not report.dpo_bucket_sparsity:
150
+ print(" no data")
151
+ return
152
+ for sparsity in report.dpo_bucket_sparsity:
153
+ print(
154
+ f" recipe={sparsity.recipe_id} v{sparsity.recipe_version} "
155
+ f"sources={sparsity.chosen_label_source}/{sparsity.rejected_label_source}"
156
+ )
157
+ print(f" buckets: {sparsity.total_buckets}")
158
+ print(f" candidates: {sparsity.total_candidates}")
159
+ print(
160
+ " singleton candidates: "
161
+ f"{sparsity.singleton_candidates} "
162
+ f"({sparsity.singleton_candidate_fraction:.1%})"
163
+ )
164
+ print(f" cap-hit buckets: {sparsity.cap_hit_buckets}")
165
+
166
+
167
+ def _format_counts(counts: dict[str, int]) -> str:
168
+ return ", ".join(f"{key}={value}" for key, value in counts.items()) or "-"
169
+
170
+
171
+ def _print_fate_diagnostic(report: DatasetDiagnostics) -> None:
172
+ print("Fate diagnostic")
173
+ print(f" fates: {_format_counts(report.fate.fates)}")
174
+ print(
175
+ " human-explicit accept fates: "
176
+ f"{_format_counts(report.fate.explicit_accept_fates)}"
177
+ )
178
+ print(
179
+ " fates with external changes: "
180
+ f"{_format_counts(report.fate.fates_with_external_changes)}"
181
+ )
182
+ print(f" derivation skipped: {_format_counts(report.fate.skipped)}")
183
+ print(f" provenance: {report.fate.provenance or '-'}")
184
+
185
+
186
+ def _print_table(report: DatasetDiagnostics) -> None:
187
+ _print_balance(report)
188
+ print()
189
+ _print_distributions(report)
190
+ print()
191
+ _print_dpo_bucket_sparsity(report)
192
+ print()
193
+ _print_duplicates(report)
194
+ print()
195
+ _print_floor_exclusions(report)
196
+ print()
197
+ _print_dpo_near_duplicates(report)
198
+ print()
199
+ _print_fate_diagnostic(report)
200
+ print()
201
+ print("abandonment")
202
+ print(f" abandoned sessions: {report.abandonment.abandoned_sessions}")
203
+ print(f" grade-eligible sessions: {report.abandonment.grade_eligible_sessions}")
204
+ print(f" implicit-only sessions: {report.abandonment.implicit_only_sessions}")
205
+ print(f" negative completions: {report.abandonment.negative_completions}")
206
+ print(
207
+ f" explicit accepts unjoined: {report.abandonment.explicit_accepts_unjoined}"
208
+ )
209
+ skipped = ", ".join(
210
+ f"{reason}={count}"
211
+ for reason, count in report.abandonment.derivation_skipped.items()
212
+ )
213
+ print(f" derivation skipped: {skipped or '-'}")
214
+ print(f" provenance: {report.abandonment.provenance or '-'}")
215
+
216
+
217
+ def build_parser() -> argparse.ArgumentParser:
218
+ """The argv contract for this report, extracted so the generated
219
+ CLI reference can walk it."""
220
+ parser = argparse.ArgumentParser(
221
+ prog="dataset_diagnostics", description=__doc__.splitlines()[0]
222
+ )
223
+ parser.add_argument(
224
+ "--org",
225
+ default=os.environ.get("SEDIMENT_ORG_ID"),
226
+ required=not os.environ.get("SEDIMENT_ORG_ID"),
227
+ help="org id (default: $SEDIMENT_ORG_ID)",
228
+ )
229
+ parser.add_argument(
230
+ "--json", action="store_true", help="emit JSON report instead of tables"
231
+ )
232
+ parser.add_argument(
233
+ "--dpo-recipe",
234
+ choices=("dpo_human", "dpo_outcome"),
235
+ default="dpo_human",
236
+ help="DPO evidence recipe (default: dpo_human)",
237
+ )
238
+ parser.add_argument(
239
+ "--sft-recipe",
240
+ choices=("sft_curated", "sft_verified"),
241
+ default="sft_curated",
242
+ help="SFT evidence recipe (default: sft_curated)",
243
+ )
244
+ parser.add_argument(
245
+ "--max-examples",
246
+ type=int,
247
+ default=5,
248
+ help="maximum examples per diagnostics section (default: 5)",
249
+ )
250
+ parser.add_argument(
251
+ "--dpo-near-duplicate-threshold",
252
+ type=float,
253
+ default=DEFAULT_DPO_NEAR_DUPLICATE_THRESHOLD,
254
+ help=(
255
+ "minimum token Jaccard similarity for DPO near-duplicate prompt "
256
+ f"bucket flags (default: {DEFAULT_DPO_NEAR_DUPLICATE_THRESHOLD})"
257
+ ),
258
+ )
259
+ add_database_url_argument(parser)
260
+ parser.add_argument(
261
+ "--mirror-path",
262
+ default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
263
+ help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
264
+ f"{_DEFAULT_MIRROR_PATH!r})",
265
+ )
266
+ return parser
267
+
268
+
269
+ def main(argv: list[str] | None = None) -> int:
270
+ args = build_parser().parse_args(argv)
271
+
272
+ if args.max_examples < 0:
273
+ print("error: --max-examples must be >= 0", file=sys.stderr)
274
+ return 2
275
+ if not 0.0 <= args.dpo_near_duplicate_threshold <= 1.0:
276
+ print(
277
+ "error: --dpo-near-duplicate-threshold must be between 0.0 and 1.0",
278
+ file=sys.stderr,
279
+ )
280
+ return 2
281
+
282
+ try:
283
+ org_id = normalize_org_id(args.org)
284
+ except ValueError as exc:
285
+ print(f"error: {exc}", file=sys.stderr)
286
+ return 2
287
+
288
+ with one_shot_fact_store(
289
+ args.database_url, operation="generate dataset diagnostics"
290
+ ) as store:
291
+ report = generate_dataset_diagnostics(
292
+ store,
293
+ MirrorManager(args.mirror_path),
294
+ org_id,
295
+ dpo_policy=DPOPolicy(recipe_id=args.dpo_recipe),
296
+ sft_policy=SFTPolicy(recipe_id=args.sft_recipe),
297
+ max_duplicate_examples=args.max_examples,
298
+ dpo_near_duplicate_threshold=args.dpo_near_duplicate_threshold,
299
+ max_near_duplicate_examples=args.max_examples,
300
+ )
301
+
302
+ if args.json:
303
+ print(json.dumps(asdict(report), indent=2))
304
+ else:
305
+ print(f"org: {org_id}")
306
+ _print_table(report)
307
+ return 0
308
+
309
+
310
+ if __name__ == "__main__":
311
+ raise SystemExit(main())