← Files Auto Preference LearnerARCHIVED FILE
skills/auto-preference-learner/scripts/prepare_review_chunks.py
4.43 KB · Oct 4, 2026 · 12:29 UTC
from __future__ import annotations
import argparse
import json
from collections import defaultdict
from pathlib import Path
from typing import Any
from _common import atomic_write_json, load_json, write_json_output
DEFAULT_MAX_RECORDS = 100
DEFAULT_MAX_CHARS = 200_000
def review_chunks(batch: dict[str, Any], max_records: int, max_chars: int) -> list[dict[str, Any]]:
if max_records < 1 or max_chars < 1:
raise ValueError("review chunk limits must be positive")
episodes_by_current: dict[str, list[dict[str, Any]]] = defaultdict(list)
for episode in batch.get("feedback_episode_candidates", []):
record_ids = episode.get("record_ids", [])
if record_ids:
episodes_by_current[str(record_ids[-1])].append(episode)
projects: dict[str, list[dict[str, Any]]] = {}
for record in batch.get("records", []):
projects.setdefault(str(record.get("project_root") or ""), []).append(record)
chunks: list[dict[str, Any]] = []
for project_root, records in projects.items():
current: list[dict[str, Any]] = []
for record in records:
candidate = [*current, record]
payload = _payload(project_root, candidate, episodes_by_current)
if current and (len(candidate) > max_records or _size(payload) > max_chars):
chunks.append(_payload(project_root, current, episodes_by_current))
candidate = [record]
payload = _payload(project_root, candidate, episodes_by_current)
if _size(payload) > max_chars:
raise ValueError(f"record {record.get('record_id')} exceeds the review chunk character limit")
current = candidate
if current:
chunks.append(_payload(project_root, current, episodes_by_current))
return chunks
def _payload(
project_root: str,
records: list[dict[str, Any]],
episodes_by_current: dict[str, list[dict[str, Any]]],
) -> dict[str, Any]:
record_ids = [str(record["record_id"]) for record in records]
record_id_set = set(record_ids)
return {
"project_root": project_root or None,
"record_ids": record_ids,
"records": records,
"feedback_episode_candidates": [
{
**episode,
"context_records": [
context
for context in episode.get("context_records", [])
if str(context.get("record_id")) not in record_id_set
],
}
for record_id in record_ids
for episode in episodes_by_current.get(record_id, [])
],
}
def _size(value: dict[str, Any]) -> int:
return len(json.dumps(value, ensure_ascii=False, indent=2)) + 1
def write_chunks(batch_path: Path, output_dir: Path, max_records: int, max_chars: int) -> dict[str, Any]:
chunks = review_chunks(load_json(batch_path, {}), max_records, max_chars)
output_dir.mkdir(parents=True, exist_ok=True)
entries = []
for number, chunk in enumerate(chunks, 1):
path = output_dir / f"chunk-{number:04d}.json"
atomic_write_json(path, chunk)
entries.append(
{
"number": number,
"path": str(path),
"project_root": chunk["project_root"],
"record_ids": chunk["record_ids"],
}
)
manifest = {
"batch": str(batch_path),
"record_ids": [record_id for entry in entries for record_id in entry["record_ids"]],
"chunks": entries,
}
atomic_write_json(output_dir / "manifest.json", manifest)
return manifest
def main() -> int:
parser = argparse.ArgumentParser(description="Split a conversation batch into bounded project review chunks.")
parser.add_argument("--batch", required=True)
parser.add_argument("--output-dir", required=True)
parser.add_argument("--max-records", type=int, default=DEFAULT_MAX_RECORDS)
parser.add_argument("--max-chars", type=int, default=DEFAULT_MAX_CHARS)
parser.add_argument("--output")
args = parser.parse_args()
manifest = write_chunks(
Path(args.batch).expanduser().resolve(),
Path(args.output_dir).expanduser().resolve(),
args.max_records,
args.max_chars,
)
write_json_output(manifest, args.output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: a6e18c31913b27392b2443889935f441eefbbe0ad8c219baca502134aa85ca36