QEV1.1-9B / code /qev_multitask_prepare.py
querypieai's picture
Release self-contained QEV multitask model
0127113 verified
Raw History Blame Contribute Delete
12.2 kB
#!/usr/bin/env python3
"""Build an isolated QEV2 DLP multitask dataset.
The source DLP corpus has document-level findings. This converter keeps that
ground truth and adds a small, deterministic bundle of type questions to every
record: is_dlp plus up to three type decisions. Japanese vulnerability rows
are converted from their existing explicit annotations and are never used to
overwrite the official corpus.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import random
from collections import Counter
from pathlib import Path
TYPE_MAP = {
"PERSON_NAME": "is_person_name",
"GOVERNMENT_ID": "is_government_id",
"CREDIT_CARD_NUMBER": "is_credit_card",
"FINANCIAL_ACCOUNT_NUMBER": "is_financial_account",
"PASSWORD": "is_password",
"MEDICAL_RECORD_NUMBER": "is_medical_record",
"EMAIL_ADDRESS": "is_email",
"PHONE_NUMBER": "is_phone",
"API_KEY": "is_api_key",
"AUTH_TOKEN": "is_auth_token",
"DEVICE_ID": "is_device_id",
"MAC_ADDRESS": "is_mac_address",
"DATE_OF_BIRTH": "is_birth_date",
"IP_ADDRESS": "is_ip_address",
"DRIVERS_LICENSE_NUMBER": "is_drivers_license",
"STREET_ADDRESS": "is_address",
"BLOOD_TYPE": "is_blood_type",
"PASSPORT": "is_passport",
"AGE": "is_age",
"VEHICLE_IDENTIFICATION_NUMBER": "is_vehicle_id",
"IBAN_CODE": "is_iban",
}
TYPE_ORDER = list(dict.fromkeys(TYPE_MAP.values()))
JP_KEY_MAP = {
"phone": "is_phone",
"date_of_birth": "is_birth_date",
"drivers_license": "is_drivers_license",
"passport": "is_passport",
"government_id": "is_government_id",
"medical": "is_medical_record",
"biometric": "is_biometric",
"email": "is_email",
"address": "is_address",
"credit_card": "is_credit_card",
"password": "is_password",
"api_key": "is_api_key",
"auth_token": "is_auth_token",
}
LABEL_TEXT = {
"en": {
"is_dlp": ("Does this source text contain one or more actual privacy-sensitive values?", "No actual privacy-sensitive value is present.", "At least one actual privacy-sensitive value is present."),
"is_email": "an actual email address",
"is_phone": "an actual telephone number",
"is_birth_date": "an actual date of birth",
"is_drivers_license": "an actual driver's license number",
"is_passport": "an actual passport or travel document number",
"is_government_id": "an actual government-issued identifier",
"is_medical_record": "an actual medical record or patient identifier",
"is_person_name": "an actual person's name",
"is_address": "an actual street address",
},
"jp": {
"is_dlp": ("ใ“ใฎใƒ†ใ‚ญใ‚นใƒˆใซๅฎŸ้š›ใฎใƒ—ใƒฉใ‚คใƒใ‚ทใƒผใƒปๆฉŸๅฏ†ๆƒ…ๅ ฑใฎๅ€คใŒ1ใคไปฅไธŠๅซใพใ‚Œใฆใ„ใพใ™ใ‹๏ผŸ", "ๅฎŸ้š›ใฎๅ€คใฏๅซใพใ‚Œใฆใ„ใชใ„ใ€‚", "ๅฎŸ้š›ใฎๅ€คใŒๅซใพใ‚Œใฆใ„ใ‚‹ใ€‚"),
"is_email": "ๅฎŸ้š›ใฎใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚น",
"is_phone": "ๅฎŸ้š›ใฎ้›ป่ฉฑ็•ชๅท",
"is_birth_date": "ๅฎŸ้š›ใฎ็”Ÿๅนดๆœˆๆ—ฅ๏ผˆ่ฅฟๆšฆใƒปๅ’Œๆšฆใƒปๅ…จ่ง’ๆ•ฐๅญ—ใ‚’ๅซใ‚€๏ผ‰",
"is_drivers_license": "ๅฎŸ้š›ใฎ้‹่ปขๅ…่จฑ่จผ็•ชๅทใƒปๅ…่จฑ็•ชๅท",
"is_passport": "ๅฎŸ้š›ใฎใƒ‘ใ‚นใƒใƒผใƒˆ็•ชๅทใƒปๆ—…ๅˆธ็•ชๅท",
"is_government_id": "ๅฎŸ้š›ใฎๅ›ฝๆฐ‘IDใƒปๅ€‹ไบบ็•ชๅทใชใฉๆ”ฟๅบœ็™บ่กŒID",
"is_medical_record": "ๅฎŸ้š›ใฎๆ‚ฃ่€…IDใƒป่จบ็™‚้Œฒ็•ชๅทใชใฉๅŒป็™‚่ญ˜ๅˆฅๅญ",
"is_person_name": "ๅฎŸ้š›ใฎๅ€‹ไบบๅ",
"is_address": "ๅฎŸ้š›ใฎไฝๆ‰€",
},
"ko": {
"is_dlp": ("์ด ํ…์ŠคํŠธ์— ์‹ค์ œ ๊ฐœ์ธ์ •๋ณดยท๋ฏผ๊ฐ์ •๋ณด ๊ฐ’์ด ํ•˜๋‚˜ ์ด์ƒ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๊นŒ?", "์‹ค์ œ ๊ฐ’์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", "์‹ค์ œ ๊ฐ’์ด ํ•˜๋‚˜ ์ด์ƒ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."),
"is_email": "์‹ค์ œ ์ด๋ฉ”์ผ ์ฃผ์†Œ",
"is_phone": "์‹ค์ œ ์ „ํ™”๋ฒˆํ˜ธ",
"is_birth_date": "์‹ค์ œ ์ƒ๋…„์›”์ผ",
"is_drivers_license": "์‹ค์ œ ์šด์ „๋ฉดํ—ˆ๋ฒˆํ˜ธ",
"is_passport": "์‹ค์ œ ์—ฌ๊ถŒ๋ฒˆํ˜ธ",
"is_government_id": "์‹ค์ œ ์ •๋ถ€ ๋ฐœํ–‰ ์‹๋ณ„๋ฒˆํ˜ธ",
"is_medical_record": "์‹ค์ œ ํ™˜์žยท์ง„๋ฃŒ ๊ธฐ๋ก ์‹๋ณ„์ž",
"is_person_name": "์‹ค์ œ ์‚ฌ๋žŒ ์ด๋ฆ„",
"is_address": "์‹ค์ œ ์ฃผ์†Œ",
},
}
def lang_of(row: dict, default="en") -> str:
value = row.get("language") or row.get("_meta", {}).get("language") or default
value = {"ja": "jp", "japanese": "jp", "en-us": "en", "ko-kr": "ko"}.get(str(value).lower(), str(value).lower())
return value if value in {"en", "jp", "ko"} else default
def question(qid: str, lang: str, value: bool, *, is_dlp=False) -> dict:
if is_dlp:
instruction, no_text, yes_text = LABEL_TEXT[lang]["is_dlp"]
else:
phrase = LABEL_TEXT[lang].get(qid, qid)
if lang == "jp":
instruction = f"ใ“ใฎใƒ†ใ‚ญใ‚นใƒˆใซ{phrase}ใŒๅซใพใ‚Œใฆใ„ใพใ™ใ‹๏ผŸๅ…จ่ง’ใƒปๅŠ่ง’ใ€ๅŒบๅˆ‡ใ‚Š่จ˜ๅทใ€่กจๅฝขๅผใ€ๆ”น่กŒใ‚’ๅซใ‚€่กจ่จ˜ใ‚’ๅˆคๅฎšใ—ใพใ™ใ€‚"
elif lang == "ko":
instruction = f"์ด ํ…์ŠคํŠธ์— {phrase}๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๊นŒ? ์ „๊ฐยท๋ฐ˜๊ฐ, ๊ตฌ๋ถ„๊ธฐํ˜ธ, ํ‘œ ํ˜•์‹, ์ค„๋ฐ”๊ฟˆ ํ‘œ๊ธฐ๋ฅผ ํฌํ•จํ•ด ํŒ์ •ํ•ฉ๋‹ˆ๋‹ค."
else:
instruction = f"Does this source text contain {phrase}? Include full-width/half-width, separators, tables, and line-wrapped forms."
no_text = "ๅฎŸ้š›ใฎๅ€คใฏๅซใพใ‚Œใฆใ„ใชใ„ใ€‚" if lang == "jp" else ("์‹ค์ œ ๊ฐ’์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค." if lang == "ko" else "No actual value is present.")
yes_text = "ๅฎŸ้š›ใฎๅ€คใŒๅซใพใ‚Œใฆใ„ใ‚‹ใ€‚" if lang == "jp" else ("์‹ค์ œ ๊ฐ’์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค." if lang == "ko" else "An actual value is present.")
return {
"type": "noul",
"instructions": instruction,
"criteria": {"false": no_text, "true": yes_text},
"label": "true" if value else "false",
}
def stable_choice(seed: str, candidates: list[str], n: int) -> list[str]:
rng = random.Random(int(hashlib.sha256(seed.encode()).hexdigest()[:16], 16))
shuffled = list(candidates)
rng.shuffle(shuffled)
return shuffled[:n]
def official_record(row: dict, split: str, ordinal: int) -> dict:
lang = lang_of(row)
positive = {TYPE_MAP[x.get("info_type")] for x in row.get("findings", []) if x.get("info_type") in TYPE_MAP}
# Three type slots per record keeps the demo-compatible four-question limit.
chosen = list(sorted(positive))[:3]
if len(chosen) < 3:
pool = [x for x in TYPE_ORDER if x not in positive]
chosen += stable_choice(row.get("id", str(ordinal)), pool, 3 - len(chosen))
qs = {"is_dlp": question("is_dlp", lang, bool(row.get("findings")), is_dlp=True)}
for qid in chosen:
qs[qid] = question(qid, lang, qid in positive)
return {
"id": f"mt-{split}-{row.get('id', ordinal)}",
"state": row.get("text", ""),
"questions": qs,
"_meta": {"origin": "official-dlp-findings", "source_id": row.get("id"), "source_split": split,
"language": lang, "info_types": sorted(x for x in positive), "binary_label": int(bool(row.get("findings")))},
}
def ja_record(row: dict, split: str, ordinal: int) -> list[dict]:
lang = "jp"
state = row.get("state", "")
src_q = row.get("questions", {})
labels = {JP_KEY_MAP[k]: (q.get("label") == "true") for k, q in src_q.items() if k in JP_KEY_MAP}
pos = {k for k, v in labels.items() if v}
# retain explicitly annotated types; fill with negatives to create bundles.
keys = list(labels)
bundles = [keys[i:i + 3] for i in range(0, len(keys), 3)] or [[]]
out = []
for bi, bundle in enumerate(bundles):
qs = {"is_dlp": question("is_dlp", lang, bool(row.get("_meta", {}).get("info_types") or row.get("questions", {}).get("is_dlp", {}).get("label") == "true"), is_dlp=True)}
for qid in bundle:
qs[qid] = question(qid, lang, labels[qid])
out.append({"id": f"mt-ja-{split}-{row.get('id', ordinal)}-b{bi}", "state": state, "questions": qs,
"_meta": {"origin": "ja-vulnerability-annotations", "source_id": row.get("id"), "source_split": split,
"language": "jp", "info_types": sorted(pos), "binary_label": int(bool(pos))}})
return out
def general_record(row: dict) -> dict:
# Keep replay records unchanged except for a provenance marker.
row = dict(row)
meta = dict(row.get("_meta", {})); meta["origin"] = "general-replay"; row["_meta"] = meta
# Make the multitask contract explicit for replay too. The replay corpus
# is the non-DLP general set, so this additional target is always false.
if isinstance(row.get("questions"), dict) and "is_dlp" not in row["questions"]:
lang = lang_of(row, "en")
row["questions"] = {"is_dlp": question("is_dlp", lang, False, is_dlp=True), **row["questions"]}
return row
def read_jsonl(path: Path):
with path.open(encoding="utf-8") as f:
for line in f:
if line.strip():
yield json.loads(line)
def write_jsonl(path: Path, rows):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
for row in rows:
f.write(json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n")
def build(args):
out = Path(args.out); out.mkdir(parents=True, exist_ok=True)
source_root, dlp_root, ja_root = map(Path, (args.source_root, args.dlp_root, args.ja_root))
split_map = {"train": "train", "validation": "development", "test": "historical"}
stats = Counter(); generated = {}
for source_split, target_split in split_map.items():
rows = []
for i, row in enumerate(read_jsonl(source_root / f"{source_split}.jsonl")):
rows.append(official_record(row, source_split, i)); stats[f"official_{target_split}"] += 1
if target_split == "train":
# Existing official QEV2 replay is already a carefully selected 30% general mix.
for row in read_jsonl(dlp_root / "train-r0-replay30.jsonl"):
if row.get("_meta", {}).get("origin") == "general-replay" or row.get("_meta", {}).get("group_id"):
rows.append(general_record(row)); stats["replay"] += 1
ja_path = ja_root / "train-r4-focused.jsonl"
elif target_split == "development":
ja_path = ja_root / "development-r3.jsonl"
else:
ja_path = ja_root / "historical-test-multitask.jsonl"
# JA source is explicitly annotated; include only Japanese records to avoid duplicating EN/KO.
if ja_path.exists():
for i, row in enumerate(read_jsonl(ja_path)):
raw_lang = row.get("language") or row.get("_meta", {}).get("language")
if str(raw_lang).lower() in {"jp", "ja", "japanese"} and row.get("_meta", {}).get("origin") != "official-dlp-findings":
converted = ja_record(row, target_split, i); rows.extend(converted); stats[f"ja_{target_split}"] += len(converted)
write_jsonl(out / f"{target_split}-multitask.jsonl", rows)
generated[target_split] = len(rows)
manifest = {"schema": "qev2-train-multitask-v1", "question_limit": 4, "type_map": TYPE_MAP,
"logical_split": {"train": "train-multitask.jsonl", "development": "development-multitask.jsonl", "historical": "historical-multitask.jsonl"},
"official_source": str(source_root), "replay_source": str(dlp_root / "train-r0-replay30.jsonl"),
"ja_source": str(ja_root), "counts": generated, "stats": dict(stats),
"label_rule": "is_dlp=true iff source findings/info_types is non-empty; type labels come only from findings or explicit JA annotations"}
(out / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(manifest, ensure_ascii=False, indent=2))
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--out", required=True)
ap.add_argument("--source-root", required=True)
ap.add_argument("--dlp-root", required=True)
ap.add_argument("--ja-root", required=True)
build(ap.parse_args())