gamedevelop's picture
Update chatcode/data.py
c410600 verified
Raw History Blame Contribute Delete
7.85 kB
"""Prepare a bounded, reproducible text-only dataset on the cloud machine."""
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import random
import re
from .runtime import CONFIG
from .articles import behavior_examples
def normalize_messages(row):
messages = row.get("messages")
if not isinstance(messages, list) or not messages:
raise ValueError("Expected a nonempty messages list.")
clean = []
for index, item in enumerate(messages):
role, content = item.get("role"), item.get("content")
if role not in {"system", "user", "assistant"} or not isinstance(content, str) or not content.strip():
raise ValueError("Only nonempty text messages are supported.")
if role == "system" and index != 0:
raise ValueError("System messages must come first.")
clean.append({"role": role, "content": content.strip()})
turns = clean[1:] if clean[0]["role"] == "system" else clean
if len(turns) < 2 or len(turns) % 2:
raise ValueError("Expected complete conversation turns.")
if any(m["role"] != ("user" if i % 2 == 0 else "assistant") for i, m in enumerate(turns)):
raise ValueError("Expected alternating user and assistant turns.")
if clean[0]["role"] != "system":
clean.insert(0, {"role": "system", "content": CONFIG["system_prompt"]})
return clean
def prompt_key(messages):
# Group identical opening questions, even when answers differ.
first = next(m["content"] for m in messages if m["role"] == "user")
normalized = re.sub(r"\s+", " ", first).strip().casefold()
return hashlib.sha256(normalized.encode()).hexdigest()
def encode_example(tokenizer, messages, max_length):
"""Supervise assistant content and end-of-turn tokens; mask every prompt."""
ids = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=False)
if len(ids) > max_length:
raise ValueError("Overlength example; skip rather than truncate code or its answer.")
labels = [-100] * len(ids)
for index, message in enumerate(messages):
if message["role"] != "assistant":
continue
prefix = tokenizer.apply_chat_template(messages[:index], tokenize=True, add_generation_prompt=True)
end = tokenizer.apply_chat_template(messages[:index + 1], tokenize=True, add_generation_prompt=False)
if ids[:len(prefix)] != prefix or ids[:len(end)] != end:
raise ValueError("Chat template does not preserve assistant boundaries.")
labels[len(prefix):len(end)] = ids[len(prefix):len(end)]
if not any(x != -100 for x in labels[1:]):
raise ValueError("No assistant target tokens.")
return {"input_ids": ids, "attention_mask": [1] * len(ids), "labels": labels}
def split_for(key):
bucket = int(key[:8], 16) % 100
return "validation" if bucket < 2 else "test" if bucket < 4 else "train"
def prepare(args):
from datasets import load_dataset
from transformers import AutoTokenizer
output = Path(args.output)
if output.exists() and any(output.iterdir()):
raise FileExistsError("Use an empty data output directory.")
tokenizer = AutoTokenizer.from_pretrained(
CONFIG["base_model"], revision=CONFIG["base_revision"], trust_remote_code=False)
result = {name: [] for name in ["train", "validation", "test"]}
seen = set()
stats = {}
for source in CONFIG["sources"]:
targets = {
"train": round(args.train * source["weight"]),
"validation": args.holdout_per_source,
"test": args.holdout_per_source,
}
counts, skipped = Counter(), Counter()
stream = load_dataset(
CONFIG["dataset"], source["config"], split="train",
revision=CONFIG["dataset_revision"], streaming=True)
stream = stream.shuffle(seed=CONFIG["seed"], buffer_size=4096)
for row in stream:
try:
messages = normalize_messages(row)
key = prompt_key(messages)
if key in seen:
skipped["duplicate_prompt"] += 1
continue
encode_example(tokenizer, messages, CONFIG["max_sequence_length"])
except (ValueError, TypeError, KeyError):
skipped["invalid_or_overlength"] += 1
continue
seen.add(key)
split = split_for(key)
if counts[split] >= targets[split]:
continue
content_hash = hashlib.sha256(json.dumps(messages, ensure_ascii=False, sort_keys=True).encode()).hexdigest()
result[split].append({"id": content_hash, "prompt_hash": key,
"source": source["config"], "messages": messages})
counts[split] += 1
if all(counts[s] >= targets[s] for s in targets):
break
if any(counts[s] < targets[s] for s in targets):
raise RuntimeError(f"Not enough usable data for {source['config']}: {dict(counts)}")
stats[source["config"]] = {"selected": dict(counts), "skipped": dict(skipped)}
print(source["config"], dict(counts), flush=True)
behavior_path = Path(__file__).with_name("article_examples.json")
for split, rows in behavior_examples().items():
for row in rows:
encode_example(tokenizer, row["messages"], CONFIG["max_sequence_length"])
key = prompt_key(row["messages"])
if key in seen:
raise ValueError("Article behavior prompt duplicates a selected Hub prompt.")
seen.add(key)
row.update({"id": hashlib.sha256(json.dumps(row["messages"], sort_keys=True).encode()).hexdigest(),
"prompt_hash": key})
result[split].append(row)
stats["authored-article-behavior"] = {"selected": {k: len(v) for k, v in behavior_examples().items()},
"license": "GPL-3.0-only", "fictional": True}
output.mkdir(parents=True, exist_ok=True)
hashes = {}
for split, rows in result.items():
random.Random(CONFIG["seed"]).shuffle(rows)
content = "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows)
(output / f"{split}.jsonl").write_bytes(content.encode("utf-8"))
hashes[split] = hashlib.sha256(content.encode()).hexdigest()
manifest = {"config": CONFIG, "counts": {k: len(v) for k, v in result.items()},
"article_examples_sha256": hashlib.sha256(behavior_path.read_bytes()).hexdigest(),
"sources": stats, "sha256": hashes,
"split_policy": "Hub: hash of normalized first user question. Article behavior: source-packet groups. Official source test splits unused.",
"limitations": ["Synthetic data may contain mistakes.",
"Exact prompt deduplication is not semantic or repository-level decontamination.",
"The pretrained base may already have seen these data.",
"This pilot's code source focuses on Python; other languages need additional data and evaluation."]}
(output / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Prepare Chat & Code data on the cloud machine")
parser.add_argument("--output", default="chatcode-data")
parser.add_argument("--train", type=int, default=CONFIG["train_examples"])
parser.add_argument("--holdout-per-source", type=int, default=128)
args = parser.parse_args()
if args.train < 10 or args.train % 10 or args.holdout_per_source < 1:
parser.error("--train must be a positive multiple of 10; holdouts must be positive.")
prepare(args)