Files
OSGKeyboard/Scripts/clipboard_semantics/generate_online_holdout.py
T
2026-08-27 18:01:46 +08:00

1292 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Build a frozen holdout from licensed, human-authored online corpora."""
from __future__ import annotations
import argparse
import csv
import hashlib
import io
import json
import random
import re
import tarfile
import time
import urllib.parse
import urllib.request
from urllib.error import HTTPError, URLError
from pathlib import Path
SEED = 20260831
ASAP_COMMIT = "975122a60065240124df62cb4d5dbfd19ed9ef2c"
OUTPUT_DIRECTORY = Path("ModelTraining/ClipboardSemantics")
TRAINING_CORPUS_PATH = OUTPUT_DIRECTORY / "clipboard_semantic_corpus.jsonl"
CORPUS_PATH = OUTPUT_DIRECTORY / "online-real-holdout-corpus.jsonl"
SOURCES_PATH = OUTPUT_DIRECTORY / "online-real-holdout-sources.json"
COMPREHENSIVE_CORPUS_PATH = (
OUTPUT_DIRECTORY / "comprehensive-online-holdout-corpus.jsonl"
)
COMPREHENSIVE_SOURCES_PATH = (
OUTPUT_DIRECTORY / "comprehensive-online-holdout-sources.json"
)
CACHE_DIRECTORY = Path("/tmp/osg-online-holdout-cache")
HUGGING_FACE_ROWS_URL = "https://datasets-server.huggingface.co/rows"
ASAP_TEST_URL = (
"https://raw.githubusercontent.com/Meituan-Dianping/asap/"
f"{ASAP_COMMIT}/data/test.csv"
)
ENRON_POSITIVE_URL = (
"https://raw.githubusercontent.com/vseledkin/"
"enron_intent_dataset_verified/master/intent_pos"
)
ENRON_NEGATIVE_URL = (
"https://raw.githubusercontent.com/vseledkin/"
"enron_intent_dataset_verified/master/intent_neg"
)
CPED_TEST_URL = (
"https://raw.githubusercontent.com/qftie/CPED/main/"
"data/CPED/test_split.csv"
)
GO_EMOTIONS_TEST_URL = (
"https://raw.githubusercontent.com/google-research/google-research/"
"master/goemotions/data/test.tsv"
)
MASSIVE_ARCHIVE_URL = (
"https://amazon-massive-nlu-dataset.s3.amazonaws.com/"
"amazon-massive-dataset-1.1.tar.gz"
)
MASSIVE_INTENT_NAMES = (
"datetime_query",
"iot_hue_lightchange",
"transport_ticket",
"takeaway_query",
"qa_stock",
"general_greet",
"recommendation_events",
"music_dislikeness",
"iot_wemo_off",
"cooking_recipe",
"qa_currency",
"transport_traffic",
"general_quirky",
"weather_query",
"audio_volume_up",
"email_addcontact",
"takeaway_order",
"email_querycontact",
"iot_hue_lightup",
"recommendation_locations",
"play_audiobook",
"lists_createoradd",
"news_query",
"alarm_query",
"iot_wemo_on",
"general_joke",
"qa_definition",
"social_query",
"music_settings",
"audio_volume_other",
"calendar_remove",
"iot_hue_lightdim",
"calendar_query",
"email_sendemail",
"iot_cleaning",
"audio_volume_down",
"play_radio",
"cooking_query",
"datetime_convert",
"qa_maths",
"iot_hue_lightoff",
"iot_hue_lighton",
"transport_query",
"music_likeness",
"email_query",
"play_music",
"audio_volume_mute",
"social_post",
"alarm_set",
"qa_factoid",
"calendar_set",
"play_game",
"alarm_remove",
"lists_remove",
"transport_taxi",
"recommendation_movies",
"iot_coffee",
"music_query",
"play_podcasts",
"lists_query",
)
TASK_INTENTS = {
15, 21, 30, 33, 47, 48, 50, 52, 53,
}
QUERY_INTENTS = {
0, 3, 4, 6, 9, 10, 11, 12, 13, 17, 19, 22, 23, 26, 27,
32, 37, 38, 39, 42, 44, 49, 55, 57, 59,
}
ENGLISH_TASK_MARKERS = (
"please ",
"can you",
"could you",
"i request",
"i am requesting",
"i would like",
"send me",
"provide me",
)
CHINESE_TASK_MARKERS = ("请", "麻烦", "要求", "希望你", "能否", "可以帮")
CHINESE_ACTION_MARKERS = (
"请",
"提醒",
"添加",
"安排",
"设置",
"删除",
"取消",
"发送",
"发到",
"邮件",
"告诉",
"记录",
"创建",
"清单",
"列表",
"发布",
"回复",
"帮我",
"联系",
"把",
)
SENSITIVE_PATTERN = re.compile(
r"(?:[\w.+-]+@[\w.-]+\.\w+)|(?:\+?\d[\d ()-]{8,}\d)",
re.IGNORECASE,
)
CHINESE_PERSONAL_DATA_PATTERN = re.compile(
r"(?:工牌|姓名|名叫|手机号|电话号码|微信号|身份证|QQ号)"
)
GITHUB_TASK_START_PATTERN = re.compile(
r"^(?:add|allow|build|change|create|enable|"
r"expose|extend|implement|improve|introduce|make|move|"
r"provide|refactor|remove|rename|replace|support|update|upgrade|use)\b",
re.IGNORECASE,
)
GITHUB_BUG_SIGNAL_PATTERN = re.compile(
r"\b(?:404|bug|crash|error|fail(?:ed|ing|s|ure)?|incorrect|leak|"
r"missing|not working|regression)\b",
re.IGNORECASE,
)
ENGLISH_QUESTION_START_PATTERN = re.compile(
r"^(?:what|when|where|which|who|why|how|can|could|would|do|does|"
r"did|has|have|is|are|will|should)\b",
re.IGNORECASE,
)
ENGLISH_INVITATION_PATTERN = re.compile(
r"\b(?:join us|would you like to|are you available|we should go|"
r"come (?:over|along|by|to)|meet us|having you join|"
r"you(?: are|'re) invited|join (?:the|our) (?:call|meeting|event)|"
r"(?:we can|could we|would you like to) meet)\b",
re.IGNORECASE,
)
ENGLISH_SCHEDULE_PATTERN = re.compile(
r"\b(?:reschedule|what time|which day|does .+ work|"
r"available (?:on|at)|move (?:the|our) meeting|"
r"(?:monday|tuesday|wednesday|thursday|friday).{0,20}\bor\b.{0,20}"
r"(?:monday|tuesday|wednesday|thursday|friday)|"
r"(?:call|meet|meeting).{0,20}\b(?:at|on)\b.{0,20}\bor\b|"
r"if it works better|delay .{0,20}\buntil\b)\b",
re.IGNORECASE,
)
ENGLISH_DECISION_PATTERN = re.compile(
r"\b(?:i approve|i choose|let[']?s go with|go ahead|"
r"proceed with|confirmed|we decided)\b",
re.IGNORECASE,
)
ENGLISH_FOLLOW_UP_PATTERN = re.compile(
r"\b(?:remind me|follow up|follow-up|check back|circle back|"
r"don[']?t forget|next step)\b",
re.IGNORECASE,
)
ENGLISH_BLESSING_PATTERN = re.compile(
r"\b(?:happy birthday|happy new year|merry christmas|happy holidays|"
r"best wishes|good luck|congratulations|congrats|wishing you|"
r"wish (?:you|him|her|them|everyone)|may you|"
r"wonderful and prosperous year)\b",
re.IGNORECASE,
)
ENGLISH_SURFACE_QUESTION_PATTERN = re.compile(
r"\?$|^(?:what|when|where|which|who|why|how|can|could|would|"
r"do|does|did|has|have|is|are|will|should)\b",
re.IGNORECASE,
)
CHINESE_INVITATION_PATTERN = re.compile(
r"(?:要不要|愿不愿意|来不来|有空吗|约一下|约你|叫你来|"
r"你一定要到|请你参加|欢迎来|邀请|"
r"一起(?:去|吃|喝|看|参加|见|聚))"
)
CHINESE_SCHEDULE_PATTERN = re.compile(
r"(?:(?:改到|改成|改期|几点|什么时候|哪天|有空|方便).{0,16}"
r"(?:见面|开会|碰面|约|吃饭|出发)|"
r"(?:周[一二三四五六日天]|星期[一二三四五六日天]|明天|后天|今晚)"
r".{0,16}(?:还是|或者|或).{0,16}|时间随你定)"
)
CHINESE_DECISION_PATTERN = re.compile(
r"(?:我决定|我们决定|决定采用|我选|就按|同意|批准|"
r"确定用|采取.{0,8}方案|可以开始|就这么定|成交)"
)
CHINESE_FOLLOW_UP_PATTERN = re.compile(
r"(?:提醒|别忘|记得|回头|跟进|下一步)"
)
CHINESE_BLESSING_PATTERN = re.compile(
r"(?:生日快乐|新年快乐|节日快乐|恭喜|"
r"预祝|(?<!庆)祝(?:你|您|大家|我们|他|她))"
)
CHINESE_SURFACE_QUESTION_PATTERN = re.compile(
r"(?:[吗呢么?]$|^(?:怎么|为什么|哪|谁|什么|是否|能否|"
r"可以|你能|有没有|是不是))"
)
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--seed", type=int, default=SEED)
parser.add_argument(
"--profile",
choices=("focused", "comprehensive"),
default="focused",
)
parser.add_argument("--output", type=Path)
parser.add_argument("--sources", type=Path)
arguments = parser.parse_args()
if arguments.output is None:
arguments.output = (
COMPREHENSIVE_CORPUS_PATH
if arguments.profile == "comprehensive"
else CORPUS_PATH
)
if arguments.sources is None:
arguments.sources = (
COMPREHENSIVE_SOURCES_PATH
if arguments.profile == "comprehensive"
else SOURCES_PATH
)
return arguments
def fetch_bytes(url: str) -> bytes:
cache_key = hashlib.sha256(url.encode()).hexdigest()
cache_path = CACHE_DIRECTORY / cache_key
if cache_path.is_file():
return cache_path.read_bytes()
request = urllib.request.Request(
url,
headers={"User-Agent": "OSGKeyboard-online-holdout/1.0"},
)
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=60) as response:
content = response.read()
CACHE_DIRECTORY.mkdir(parents=True, exist_ok=True)
cache_path.write_bytes(content)
time.sleep(0.4)
return content
except HTTPError as error:
if error.code != 429 or attempt == 4:
raise
retry_after = int(error.headers.get("Retry-After", "3"))
time.sleep(max(retry_after, 3 * (attempt + 1)))
except (URLError, TimeoutError):
if attempt == 4:
raise
time.sleep(2 * (attempt + 1))
raise RuntimeError(f"Unable to download {url}")
def fetch_hugging_face_rows(
dataset: str,
config: str,
split: str,
offset: int,
length: int = 100,
) -> list[dict]:
query = urllib.parse.urlencode(
{
"dataset": dataset,
"config": config,
"split": split,
"offset": offset,
"length": length,
}
)
payload = json.loads(fetch_bytes(f"{HUGGING_FACE_ROWS_URL}?{query}"))
return payload["rows"]
def normalized_text(value: str) -> str:
return " ".join(value.replace("\u0000", " ").split()).strip()
def is_eligible_text(text: str, minimum: int, maximum: int) -> bool:
return (
minimum <= len(text) <= maximum
and not SENSITIVE_PATTERN.search(text)
and text.count("XXXX") <= 8
)
def base_record(
*,
record_id: str,
family: str,
language: str,
text: str,
source_dataset: str,
source_url: str,
source_license: str,
task: bool = False,
question: bool = False,
invitation: bool = False,
complaint: bool = False,
schedule_negotiation: bool = False,
confirmation_decision: bool = False,
follow_up_reminder: bool = False,
blessing: bool = False,
replyable: bool = True,
sentiment: str = "neutral",
split: str = "onlineRealHoldout",
source_author: str | None = None,
source_author_url: str | None = None,
) -> dict:
record = {
"id": record_id,
"family": family,
"language": language,
"text": text,
"task": task,
"question": question,
"invitation": invitation,
"complaint": complaint,
"scheduleNegotiation": schedule_negotiation,
"confirmationDecision": confirmation_decision,
"followUpReminder": follow_up_reminder,
"blessing": blessing,
"replyable": replyable,
"sentiment": sentiment,
"split": split,
"sourceDataset": source_dataset,
"sourceURL": source_url,
"sourceLicense": source_license,
}
if source_author:
record["sourceAuthor"] = source_author
if source_author_url:
record["sourceAuthorURL"] = source_author_url
return record
def complaint_records(
rng: random.Random,
count: int = 40,
offsets: tuple[int, ...] = (1200, 8800, 17500, 29000),
) -> list[dict]:
rows: list[dict] = []
for offset in offsets:
rows.extend(
fetch_hugging_face_rows(
"hpe-ai/customer-complaints",
"default",
"train",
offset,
)
)
candidates: list[dict] = []
seen: set[str] = set()
for item in rows:
source = item["row"]
text = normalized_text(source["Consumer_complaint_narrative"] or "")
fingerprint = text.casefold()
if fingerprint in seen or not is_eligible_text(text, 40, 700):
continue
seen.add(fingerprint)
lowered = text.casefold()
task = any(marker in lowered for marker in ENGLISH_TASK_MARKERS)
complaint_id = str(source["Complaint_ID"])
candidates.append(
base_record(
record_id=f"online-cfpb-{complaint_id}",
family="online_cfpb_complaint",
language="en",
text=text,
task=task,
question="?" in text,
complaint=True,
sentiment="negative",
source_dataset="CFPB via hpe-ai/customer-complaints",
source_url=(
"https://www.consumerfinance.gov/data-research/"
f"consumer-complaints/search/detail/{complaint_id}"
),
source_license="CC0-1.0 source / Apache-2.0 mirror",
)
)
if len(candidates) < count:
raise RuntimeError(f"Only found {len(candidates)} CFPB samples")
return rng.sample(candidates, count)
def asap_records(
rng: random.Random,
count: int = 40,
) -> list[dict]:
content = fetch_bytes(ASAP_TEST_URL).decode("utf-8-sig")
candidates: list[dict] = []
seen: set[str] = set()
for row in csv.DictReader(io.StringIO(content)):
text = normalized_text(row["review"])
fingerprint = text.casefold()
if fingerprint in seen or not is_eligible_text(text, 20, 260):
continue
if float(row["star"]) > 2:
continue
if CHINESE_PERSONAL_DATA_PATTERN.search(text):
continue
if not any(value == "-1" for value in list(row.values())[3:]):
continue
seen.add(fingerprint)
task = any(marker in text for marker in CHINESE_TASK_MARKERS)
row_id = row["id"]
candidates.append(
base_record(
record_id=f"online-asap-{row_id}",
family="online_asap_complaint",
language="zh-Hans",
text=text,
task=task,
question="" in text or "?" in text,
complaint=True,
sentiment="negative",
source_dataset="Meituan-Dianping/ASAP test",
source_url=(
"https://github.com/Meituan-Dianping/asap/blob/"
f"{ASAP_COMMIT}/data/test.csv#row-{row_id}"
),
source_license="Apache-2.0",
)
)
if len(candidates) < count:
raise RuntimeError(f"Only found {len(candidates)} ASAP samples")
return rng.sample(candidates, count)
def massive_records(
rng: random.Random,
*,
config: str,
language: str,
task_count: int,
) -> list[dict]:
rows: list[dict] = []
for offset in range(0, 2900, 200):
rows.extend(
fetch_hugging_face_rows(
"AmazonScience/massive",
config,
"test",
offset,
)
)
task_candidates: list[dict] = []
query_candidates: list[dict] = []
seen: set[str] = set()
for item in rows:
source = item["row"]
text = normalized_text(source["utt"])
fingerprint = text.casefold()
if fingerprint in seen or not is_eligible_text(text, 4, 220):
continue
seen.add(fingerprint)
intent = int(source["intent"])
is_task = intent in TASK_INTENTS
is_query = intent in QUERY_INTENTS
if not is_task and not is_query:
continue
if is_task and language == "zh-Hans":
if not any(marker in text for marker in CHINESE_ACTION_MARKERS):
continue
family = "online_massive_task" if is_task else "online_massive_query_boundary"
record = base_record(
record_id=f"online-massive-{config}-{source['id']}",
family=family,
language=language,
text=text,
task=is_task,
question=is_query,
complaint=False,
sentiment="neutral",
source_dataset=f"AmazonScience/MASSIVE {config} test",
source_url=(
"https://huggingface.co/datasets/AmazonScience/massive"
f"?row={item['row_idx']}"
),
source_license="CC-BY-4.0",
)
(task_candidates if is_task else query_candidates).append(record)
if len(task_candidates) < task_count or len(query_candidates) < 20:
raise RuntimeError(
f"Insufficient MASSIVE {config} samples: "
f"task={len(task_candidates)} query={len(query_candidates)}"
)
return rng.sample(task_candidates, task_count) + rng.sample(query_candidates, 20)
def github_task_records(rng: random.Random) -> list[dict]:
rows: list[dict] = []
for offset in range(0, 4000, 100):
rows.extend(
fetch_hugging_face_rows(
"Sulak2020/github-issues-multirepo-datasets-Sulakshana",
"default",
"train",
offset,
)
)
candidates: list[dict] = []
seen: set[str] = set()
for item in rows:
source = item["row"]
if source["is_pull_request"]:
continue
text = normalized_text(source["title"])
normalized_title = re.sub(r"^(?:\[[^\]]+\]\s*)+", "", text)
fingerprint = text.casefold()
if fingerprint in seen or not is_eligible_text(text, 8, 180):
continue
if not GITHUB_TASK_START_PATTERN.search(normalized_title):
continue
if GITHUB_BUG_SIGNAL_PATTERN.search(normalized_title):
continue
seen.add(fingerprint)
author = source.get("user") or {}
candidates.append(
base_record(
record_id=f"online-github-{source['id']}",
family="online_github_task",
language="en",
text=text,
task=True,
question="?" in text,
complaint=False,
sentiment="neutral",
source_dataset="Sulak2020 GitHub issues multi-repo dataset",
source_url=source["html_url"],
source_license="CC-BY-4.0 dataset / attributed source issue",
source_author=author.get("login"),
source_author_url=author.get("html_url"),
)
)
if len(candidates) < 40:
raise RuntimeError(f"Only found {len(candidates)} GitHub task samples")
return rng.sample(candidates, 40)
def comprehensive_massive_records(
*,
config: str,
language: str,
) -> list[dict]:
intent_indexes = {
name: index for index, name in enumerate(MASSIVE_INTENT_NAMES)
}
archive_bytes = fetch_bytes(MASSIVE_ARCHIVE_URL)
archive = tarfile.open(fileobj=io.BytesIO(archive_bytes), mode="r:gz")
member = next(
(
candidate
for candidate in archive.getmembers()
if candidate.isfile()
and candidate.name.endswith(f"/{config}.jsonl")
),
None,
)
if member is None:
raise RuntimeError(f"MASSIVE archive is missing {config}.jsonl")
extracted = archive.extractfile(member)
if extracted is None:
raise RuntimeError(f"Unable to read MASSIVE {config}.jsonl")
records: list[dict] = []
seen: set[str] = set()
row_index = 0
for raw_line in extracted:
source = json.loads(raw_line)
if source["partition"] != "test":
continue
text = normalized_text(source["utt"])
fingerprint = text.casefold()
if fingerprint in seen or not is_eligible_text(text, 4, 220):
continue
seen.add(fingerprint)
intent = intent_indexes[source["intent"]]
is_reminder = (
intent == 48
or bool(ENGLISH_FOLLOW_UP_PATTERN.search(text))
if language == "en"
else intent == 48
or bool(CHINESE_FOLLOW_UP_PATTERN.search(text))
)
is_task = intent in TASK_INTENTS and not is_reminder
sentiment = (
"negative"
if intent == 7
else "positive"
if intent == 43
else "neutral"
)
records.append(
base_record(
record_id=f"comprehensive-massive-{config}-{source['id']}",
family=f"online_massive_intent_{intent}",
language=language,
text=text,
task=is_task,
question=(
intent in QUERY_INTENTS
or bool(ENGLISH_SURFACE_QUESTION_PATTERN.search(text))
if language == "en"
else intent in QUERY_INTENTS
or bool(CHINESE_SURFACE_QUESTION_PATTERN.search(text))
),
follow_up_reminder=is_reminder,
sentiment=sentiment,
source_dataset=f"AmazonScience/MASSIVE {config} test",
source_url=(
"https://huggingface.co/datasets/AmazonScience/massive"
f"?row={row_index}"
),
source_license="CC-BY-4.0",
split="comprehensiveOnlineHoldout",
)
)
row_index += 1
archive.close()
return records
def enron_intent_records() -> list[dict]:
records: list[dict] = []
for is_actionable, url, source_name in (
(True, ENRON_POSITIVE_URL, "intent_pos"),
(False, ENRON_NEGATIVE_URL, "intent_neg"),
):
lines = fetch_bytes(url).decode("utf-8").splitlines()
for index, raw_text in enumerate(lines, start=1):
text = normalized_text(raw_text)
if not is_eligible_text(text, 4, 500):
continue
lowered = text.casefold()
invitation = bool(ENGLISH_INVITATION_PATTERN.search(text))
schedule = bool(ENGLISH_SCHEDULE_PATTERN.search(text))
follow_up = bool(ENGLISH_FOLLOW_UP_PATTERN.search(text))
question = (
bool(ENGLISH_SURFACE_QUESTION_PATTERN.search(text))
)
task = is_actionable and (
any(marker in lowered for marker in ENGLISH_TASK_MARKERS)
or bool(GITHUB_TASK_START_PATTERN.search(text))
or follow_up
)
records.append(
base_record(
record_id=f"comprehensive-enron-{source_name}-{index}",
family=f"online_enron_{source_name}",
language="en",
text=text,
task=task,
question=question,
invitation=invitation,
schedule_negotiation=schedule,
confirmation_decision=bool(
ENGLISH_DECISION_PATTERN.search(text)
),
follow_up_reminder=follow_up,
blessing=bool(ENGLISH_BLESSING_PATTERN.search(text)),
replyable=is_actionable,
source_dataset="Verified Enron Intent Dataset",
source_url=(
"https://github.com/vseledkin/"
"enron_intent_dataset_verified/blob/master/"
f"{source_name}#L{index}"
),
source_license="MIT / public Enron source corpus",
split="comprehensiveOnlineHoldout",
)
)
return records
def cped_records() -> list[dict]:
content = fetch_bytes(CPED_TEST_URL).decode("utf-8-sig")
records: list[dict] = []
complaint_pattern = re.compile(
r"(?:怎么|为什么|一直|又|根本|受不了|太差|坏了|错了|"
r"没用|不行|骗人|故障|问题)"
)
replyable_dialogue_acts = {
"question",
"command",
"statement-opinion",
"statement-non-opinion",
"apology",
"agreement/acceptance",
"disagreement",
"reject",
"comfort",
}
for index, row in enumerate(csv.DictReader(io.StringIO(content)), start=2):
text = normalized_text(row.get("Utterance") or "")
if not is_eligible_text(text, 2, 220):
continue
dialogue_act = row["DA"]
sentiment = row["Sentiment"]
emotion = row["Emotion"]
invitation = bool(CHINESE_INVITATION_PATTERN.search(text))
schedule = bool(CHINESE_SCHEDULE_PATTERN.search(text))
follow_up = bool(CHINESE_FOLLOW_UP_PATTERN.search(text))
complaint = (
sentiment == "negative"
and emotion in {"angry", "disgusted", "depressed", "worried"}
and bool(complaint_pattern.search(text))
)
records.append(
base_record(
record_id=f"comprehensive-cped-{row['Utterance_ID']}",
family=f"online_cped_{dialogue_act.replace('/', '_')}",
language="zh-Hans",
text=text,
task=dialogue_act == "command",
question=(
dialogue_act == "question"
or bool(CHINESE_SURFACE_QUESTION_PATTERN.search(text))
),
invitation=invitation,
complaint=complaint,
schedule_negotiation=schedule,
confirmation_decision=bool(
CHINESE_DECISION_PATTERN.search(text)
),
follow_up_reminder=follow_up,
blessing=bool(CHINESE_BLESSING_PATTERN.search(text)),
replyable=dialogue_act in replyable_dialogue_acts,
sentiment=(
sentiment
if sentiment in {"positive", "neutral", "negative"}
else "neutral"
),
source_dataset="CPED test split",
source_url=(
"https://github.com/qftie/CPED/blob/main/"
f"data/CPED/test_split.csv#L{index}"
),
source_license="Apache-2.0",
split="comprehensiveOnlineHoldout",
)
)
return records
def go_emotions_records() -> list[dict]:
positive_labels = {0, 1, 4, 5, 13, 15, 17, 18, 20, 21, 23}
negative_labels = {2, 3, 9, 10, 11, 12, 14, 16, 19, 24, 25}
complaint_pattern = re.compile(
r"\b(?:problem|issue|fail(?:ed|ing|s|ure)?|broken|"
r"not working|doesn[']?t work|terrible service|worst service)\b",
re.IGNORECASE,
)
records: list[dict] = []
content = fetch_bytes(GO_EMOTIONS_TEST_URL).decode("utf-8")
for index, line in enumerate(content.splitlines(), start=1):
columns = line.split("\t")
if len(columns) != 3:
continue
raw_text, raw_labels, source_id = columns
text = normalized_text(raw_text)
if not is_eligible_text(text, 3, 500):
continue
labels = {int(value) for value in raw_labels.split(",")}
has_positive = bool(labels & positive_labels)
has_negative = bool(labels & negative_labels)
sentiment = (
"positive"
if has_positive and not has_negative
else "negative"
if has_negative and not has_positive
else "neutral"
)
lowered = text.casefold()
records.append(
base_record(
record_id=f"comprehensive-goemotions-{source_id}",
family="online_goemotions_reddit",
language="en",
text=text,
task=any(
marker in lowered for marker in ENGLISH_TASK_MARKERS
),
question=bool(ENGLISH_SURFACE_QUESTION_PATTERN.search(text)),
complaint=(
has_negative
and bool(complaint_pattern.search(text))
),
blessing=bool(ENGLISH_BLESSING_PATTERN.search(text)),
sentiment=sentiment,
source_dataset="Google Research GoEmotions test",
source_url=(
"https://github.com/google-research/google-research/"
f"blob/master/goemotions/data/test.tsv#L{index}"
),
source_license="Apache-2.0",
split="comprehensiveOnlineHoldout",
)
)
return records
def comprehensive_github_records() -> list[dict]:
records: list[dict] = []
for offset in range(0, 4000, 100):
for item in fetch_hugging_face_rows(
"Sulak2020/github-issues-multirepo-datasets-Sulakshana",
"default",
"train",
offset,
):
source = item["row"]
if source["is_pull_request"]:
continue
text = normalized_text(source["title"])
if not is_eligible_text(text, 5, 220):
continue
normalized_title = re.sub(r"^(?:\[[^\]]+\]\s*)+", "", text)
complaint = bool(GITHUB_BUG_SIGNAL_PATTERN.search(normalized_title))
author = source.get("user") or {}
records.append(
base_record(
record_id=f"comprehensive-github-{source['id']}",
family="online_github_issue_title",
language="en",
text=text,
task=(
bool(GITHUB_TASK_START_PATTERN.search(normalized_title))
and not complaint
),
question=bool(
ENGLISH_SURFACE_QUESTION_PATTERN.search(text)
),
complaint=complaint,
schedule_negotiation=bool(
ENGLISH_SCHEDULE_PATTERN.search(text)
),
confirmation_decision=bool(
ENGLISH_DECISION_PATTERN.search(text)
),
follow_up_reminder=bool(
ENGLISH_FOLLOW_UP_PATTERN.search(text)
),
blessing=bool(ENGLISH_BLESSING_PATTERN.search(text)),
sentiment="negative" if complaint else "neutral",
source_dataset=(
"Sulak2020 GitHub issues multi-repo dataset"
),
source_url=source["html_url"],
source_license=(
"CC-BY-4.0 dataset / attributed source issue"
),
split="comprehensiveOnlineHoldout",
source_author=author.get("login"),
source_author_url=author.get("html_url"),
)
)
for comment_index, raw_comment in enumerate(
source.get("comments") or [],
start=1,
):
comment = normalized_text(raw_comment)
if not is_eligible_text(comment, 4, 500):
continue
invitation = bool(
ENGLISH_INVITATION_PATTERN.search(comment)
)
schedule = bool(ENGLISH_SCHEDULE_PATTERN.search(comment))
decision = bool(ENGLISH_DECISION_PATTERN.search(comment))
follow_up = bool(ENGLISH_FOLLOW_UP_PATTERN.search(comment))
blessing = bool(ENGLISH_BLESSING_PATTERN.search(comment))
if not any(
(invitation, schedule, decision, follow_up, blessing)
):
continue
records.append(
base_record(
record_id=(
f"comprehensive-github-{source['id']}-"
f"comment-{comment_index}"
),
family="online_github_issue_comment_intent",
language="en",
text=comment,
task=follow_up,
question=bool(
ENGLISH_SURFACE_QUESTION_PATTERN.search(comment)
),
invitation=invitation,
schedule_negotiation=schedule,
confirmation_decision=decision,
follow_up_reminder=follow_up,
blessing=blessing,
source_dataset=(
"Sulak2020 GitHub issues multi-repo dataset"
),
source_url=source["html_url"],
source_license=(
"CC-BY-4.0 dataset / attributed source issue"
),
split="comprehensiveOnlineHoldout",
)
)
return records
def deduplicated(records: list[dict]) -> list[dict]:
result: list[dict] = []
seen: set[str] = set()
for record in records:
fingerprint = normalized_text(record["text"]).casefold()
if fingerprint in seen:
continue
seen.add(fingerprint)
result.append(record)
return result
def excluding_training_overlaps(records: list[dict]) -> list[dict]:
if not TRAINING_CORPUS_PATH.is_file():
return records
training_texts = {
normalized_text(json.loads(line)["text"]).casefold()
for line in TRAINING_CORPUS_PATH.read_text(encoding="utf-8").splitlines()
if line.strip()
}
return [
record
for record in records
if normalized_text(record["text"]).casefold() not in training_texts
]
def comprehensive_records(rng: random.Random) -> list[dict]:
records = (
complaint_records(
rng,
count=250,
offsets=tuple(range(0, 30000, 3000)),
)
+ asap_records(rng, count=120)
+ comprehensive_massive_records(config="en-US", language="en")
+ comprehensive_massive_records(config="zh-CN", language="zh-Hans")
+ enron_intent_records()
+ cped_records()
+ go_emotions_records()
+ comprehensive_github_records()
)
return excluding_training_overlaps(deduplicated(records))
def record_count(
records: list[dict],
source_prefix: str,
) -> int:
return sum(
record["sourceDataset"].startswith(source_prefix)
for record in records
)
def focused_source_summary(
records: list[dict],
serialized: str,
seed: int,
) -> dict:
return {
"containsProjectUserClipboardData": False,
"corpusSHA256": hashlib.sha256(serialized.encode()).hexdigest(),
"generatedFromOnlineSources": True,
"labelDefinition": {
"complaint": (
"A user reports a negative experience, failure, or grievance."
),
"task": (
"An actionable work request suitable for todo extraction; "
"device-control commands are excluded."
),
},
"labelCorrection": {
"reason": (
"Replaced generic MASSIVE device-control commands with "
"attributed GitHub work requests."
),
"supersededCorpusSHA256": (
"2962b6d28fe0ef551ae46f62accbde93c9da7f0fad93451c06c2e226350aad33"
),
},
"recordCount": len(records),
"seed": seed,
"sources": [
{
"dataset": "CFPB via hpe-ai/customer-complaints",
"license": "CC0-1.0 source / Apache-2.0 mirror",
"records": 40,
"url": (
"https://huggingface.co/datasets/"
"hpe-ai/customer-complaints"
),
},
{
"dataset": "Meituan-Dianping/ASAP",
"license": "Apache-2.0",
"records": 40,
"revision": ASAP_COMMIT,
"url": "https://github.com/Meituan-Dianping/asap",
},
{
"dataset": "AmazonScience/MASSIVE",
"license": "CC-BY-4.0",
"records": 80,
"url": (
"https://huggingface.co/datasets/"
"AmazonScience/massive"
),
},
{
"dataset": "Sulak2020 GitHub issues multi-repo dataset",
"license": "CC-BY-4.0 with per-record attribution",
"records": 40,
"url": (
"https://huggingface.co/datasets/"
"Sulak2020/github-issues-multirepo-datasets-Sulakshana"
),
},
],
}
def comprehensive_source_summary(
records: list[dict],
serialized: str,
seed: int,
) -> dict:
intents = (
"task",
"question",
"invitation",
"complaint",
"scheduleNegotiation",
"confirmationDecision",
"followUpReminder",
"blessing",
"replyable",
)
languages = ("en", "zh-Hans")
coverage = {
intent: {
language: {
"positive": sum(
record["language"] == language
and bool(record[intent])
for record in records
),
"negative": sum(
record["language"] == language
and not bool(record[intent])
for record in records
),
}
for language in languages
}
for intent in intents
}
sentiment_coverage = {
language: {
label: sum(
record["language"] == language
and record["sentiment"] == label
for record in records
)
for label in ("positive", "neutral", "negative")
}
for language in languages
}
return {
"containsProjectUserClipboardData": False,
"corpusSHA256": hashlib.sha256(serialized.encode()).hexdigest(),
"generatedFromOnlineSources": True,
"profile": "comprehensive",
"recordCount": len(records),
"seed": seed,
"coverage": coverage,
"sentimentCoverage": sentiment_coverage,
"labelingMethod": (
"Original dataset labels are retained where available. Product "
"intents not present in source schemas use conservative lexical "
"mapping; metrics must therefore be read with per-source results "
"and label-coverage counts."
),
"labelCorrection": {
"reason": (
"An initial prelabel audit found obvious surface questions, "
"invitations, schedule alternatives, reminders, and wishes "
"that source intent schemas did not encode. A second audit "
"removed quoted, sarcastic, and substring-only blessing "
"matches. Deterministic label rules were corrected before "
"the frozen evaluation."
),
"supersededCorpusSHA256": [
"36f736f69e0c9ac42262d2b57640f9889534ffdc6e70da67fda6f41d7211b2f9",
"baffdfff4b5436810c45d92d28c5885d8ed475b878531220852ae6f2a55011e4",
],
},
"holdoutPolicy": (
"Frozen online evaluation only. These records are excluded from "
"training, model selection, and threshold calibration."
),
"sources": [
{
"dataset": "CFPB via hpe-ai/customer-complaints",
"license": "CC0-1.0 source / Apache-2.0 mirror",
"records": record_count(records, "CFPB"),
"url": (
"https://huggingface.co/datasets/"
"hpe-ai/customer-complaints"
),
},
{
"dataset": "Meituan-Dianping/ASAP",
"license": "Apache-2.0",
"records": record_count(records, "Meituan-Dianping/ASAP"),
"revision": ASAP_COMMIT,
"url": "https://github.com/Meituan-Dianping/asap",
},
{
"dataset": "AmazonScience/MASSIVE",
"license": "CC-BY-4.0",
"records": record_count(records, "AmazonScience/MASSIVE"),
"url": (
"https://huggingface.co/datasets/"
"AmazonScience/massive"
),
},
{
"dataset": "Verified Enron Intent Dataset",
"license": "MIT / public Enron source corpus",
"records": record_count(
records,
"Verified Enron Intent Dataset",
),
"url": (
"https://github.com/vseledkin/"
"enron_intent_dataset_verified"
),
},
{
"dataset": "CPED",
"license": "Apache-2.0",
"records": record_count(records, "CPED"),
"url": "https://github.com/qftie/CPED",
},
{
"dataset": "Google Research GoEmotions",
"license": "Apache-2.0",
"records": record_count(
records,
"Google Research GoEmotions",
),
"url": (
"https://huggingface.co/datasets/"
"google-research-datasets/go_emotions"
),
},
{
"dataset": "Sulak2020 GitHub issues multi-repo dataset",
"license": "CC-BY-4.0 with per-record attribution",
"records": record_count(
records,
"Sulak2020 GitHub issues",
),
"url": (
"https://huggingface.co/datasets/"
"Sulak2020/github-issues-multirepo-datasets-Sulakshana"
),
},
],
}
def main() -> None:
arguments = parse_arguments()
rng = random.Random(arguments.seed)
if arguments.profile == "comprehensive":
records = comprehensive_records(rng)
else:
records = (
complaint_records(rng)
+ asap_records(rng)
+ github_task_records(rng)
+ massive_records(rng, config="en-US", language="en", task_count=0)
+ massive_records(
rng,
config="zh-CN",
language="zh-Hans",
task_count=40,
)
)
rng.shuffle(records)
arguments.output.parent.mkdir(parents=True, exist_ok=True)
serialized = "".join(
json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n"
for record in records
)
arguments.output.write_text(serialized, encoding="utf-8")
source_summary = (
comprehensive_source_summary(
records,
serialized,
arguments.seed,
)
if arguments.profile == "comprehensive"
else focused_source_summary(
records,
serialized,
arguments.seed,
)
)
arguments.sources.write_text(
json.dumps(source_summary, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(
"ONLINE_HOLDOUT_DONE "
f"records={len(records)} seed={arguments.seed} "
f"sha256={source_summary['corpusSHA256']}"
)
if __name__ == "__main__":
main()