Nirnay v3.1 is the new default: request routing 0.40 -> 0.88, SMS spam, emotion, toxicity, RAG up; previous default moved to v2/
61ffaac verified Download code/train/build_data.py from nandukmelath/nirnay: direct link, hf CLI and curl.
- Browser
- Download file 57.4 kB
-
https://huggingface.co/nandukmelath/nirnay/resolve/main/code/train/build_data.py
- Command line
-
hf download hf://nandukmelath/nirnay/code/train/build_data.py
-
curl -L -o build_data.py https://huggingface.co/nandukmelath/nirnay/resolve/main/code/train/build_data.py
57.4 kB
| """Build Nirnay's multi-task decision corpus from public datasets. | |
| Every record is one typed question about one state, in the Jev/Laya question spec: | |
| {"src", "state", "q": {"type", "instructions", "criteria"}, "y": gold option index, "split"} | |
| Held out of training entirely (they are benchmark suites, mirroring Laya's own held-out policy): | |
| dair-ai/emotion, mteb/banking77, lmsys/toxic-chat (jailbreak + toxicity), openai/gsm8k, | |
| google-research-datasets/mbpp (model routing), SetFit/sst5, ucirvine/sms_spam, XNLI, MASSIVE, | |
| and every jailbreak / prompt-injection dataset. | |
| Datasets Laya also trained on (ag_news, enron spam, phishing, support tickets, MS MARCO) contribute | |
| only rows that are not in the benchmark cases, and any state whose text appears in a benchmark | |
| suite is dropped. | |
| v3 adds request routing (code / math / SQL / writing / trivia / small-talk sources, never gsm8k or mbpp), | |
| short-message spam (any text that matches the held-out UCI SMS collection is dropped), request safety | |
| (NVIDIA Aegis 2.0, which is not a jailbreak dataset), urgency questions, and more phrasings of every | |
| shared question template. | |
| python train\\build_data.py (NIRNAY_SCALE=0.1 for a quick smoke build) | |
| """ | |
| import hashlib | |
| import json | |
| import os | |
| import random | |
| import re | |
| import sys | |
| from collections import Counter | |
| from datasets import load_dataset | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| ROOT = os.path.dirname(HERE) | |
| OUT = os.environ.get("NIRNAY_RECORDS_OUT", os.path.join(HERE, "data", "records.jsonl")) | |
| SUITES = os.path.join(ROOT, "bench", "data", "suites.jsonl") | |
| SCALE = float(os.environ.get("NIRNAY_SCALE", "1.0")) | |
| SEED = 1234 | |
| # per-source multipliers, e.g. NIRNAY_SRC_SCALE="ag_news=2.86,msmarco=2.86" (source = function name minus src_) | |
| SRC_SCALE = {k: float(v) for k, v in (p.split("=") for p in os.environ.get("NIRNAY_SRC_SCALE", "").split(",") if "=" in p)} | |
| CURRENT = {"src": ""} | |
| def n_(k): | |
| return max(50, int(k * SCALE * SRC_SCALE.get(CURRENT["src"], 1.0))) | |
| def norm(s: str) -> str: | |
| return re.sub(r"\s+", " ", str(s).lower()).strip()[:300] | |
| def leaves(x): | |
| if isinstance(x, str): | |
| yield x | |
| elif isinstance(x, dict): | |
| for v in x.values(): | |
| yield from leaves(v) | |
| elif isinstance(x, (list, tuple)): | |
| for v in x: | |
| yield from leaves(v) | |
| EVAL_KEYS = set() | |
| def load_eval_keys(): | |
| if not os.path.exists(SUITES): | |
| sys.exit("build the benchmark suites first: python bench\\suites.py") | |
| multi = os.path.join(ROOT, "bench", "data", "suites_multi.jsonl") # MASSIVE + SIB-200, when built | |
| for path in [SUITES] + ([multi] if os.path.exists(multi) else []): | |
| for line in open(path, encoding="utf-8"): | |
| r = json.loads(line) | |
| for s in leaves(r["state"]): | |
| if len(s) >= 20: | |
| EVAL_KEYS.add(norm(s)) | |
| print("eval texts held out:", len(EVAL_KEYS), flush=True) | |
| def leaks(state) -> bool: | |
| return any(len(s) >= 20 and norm(s) in EVAL_KEYS for s in leaves(state)) | |
| # ------------------------------------------------------------------ question helpers | |
| FIELDS_GENERIC = ["text", "message", "content", "input", "document"] | |
| def wrap(rng, text, fields=FIELDS_GENERIC, p_plain=0.35): | |
| """-> (state, reference used in instructions).""" | |
| if rng.random() < p_plain: | |
| return text, rng.choice(["the text", "this text", "the input", "this message"]) | |
| f = rng.choice(fields) | |
| return {f: text}, "`%s`" % f | |
| def pick(rng, xs): | |
| return xs[rng.randrange(len(xs))] | |
| def choice_q(rng, ins, labels, gold, descs=None, kmax=None, p_full=0.5, p_desc=0.5, kmin=2): | |
| n = len(labels) | |
| cap = min(n, kmax or n) | |
| if n > cap or rng.random() > p_full: | |
| k = rng.randint(min(kmin, cap), cap) | |
| others = rng.sample([i for i in range(n) if i != gold], k - 1) | |
| idx = others + [gold] | |
| else: | |
| idx = list(range(n)) | |
| rng.shuffle(idx) | |
| use_desc = descs is not None and rng.random() < p_desc | |
| crit = {labels[i]: (descs.get(labels[i]) if use_desc else None) for i in idx} | |
| return {"type": "choice", "instructions": ins, "criteria": crit}, idx.index(gold) | |
| def noul_q(ins, crit=None): | |
| q = {"type": "noul", "instructions": ins} | |
| if crit: | |
| q["criteria"] = crit | |
| return q | |
| def noul_from_class(rng, templates, ref, labels, gold, shown=None): | |
| """Binary question about one class: 50% the gold class (yes), 50% another class (no).""" | |
| if rng.random() < 0.5: | |
| lab, y = labels[gold], 1 | |
| else: | |
| lab, y = labels[rng.choice([i for i in range(len(labels)) if i != gold])], 0 | |
| lab = (shown or {}).get(lab, lab) | |
| return noul_q(pick(rng, templates).format(ref=ref, label=lab)), y | |
| # ------------------------------------------------------------------ sources | |
| TOPIC_INS = ["What is the topic of {ref}?", "Which category best describes {ref}?", "What is {ref} mainly about?", | |
| "Classify {ref} by subject.", "Pick the topic that fits {ref}.", "What subject area does {ref} cover?", | |
| "Which section would {ref} be filed under?"] | |
| TOPIC_NOUL = ["Is {ref} about {label}?", "Does {ref} belong to the {label} category?", "Is the topic of {ref} {label}?"] | |
| def src_ag_news(rng): | |
| d = load_dataset("fancyzhx/ag_news", split="train") | |
| variants = [ | |
| (["world", "sports", "business", "sci_tech"], {"world": "world news and international politics", "sports": "sports", | |
| "business": "business and economy", "sci_tech": "science and technology"}), | |
| (["World", "Sports", "Business", "Sci/Tech"], {"World": "international news", "Sports": "games, teams and athletes", | |
| "Business": "companies, markets and the economy", | |
| "Sci/Tech": "technology, science and computing"}), | |
| (["international", "sport", "economy", "technology"], None), | |
| ] | |
| rows = list(d.shuffle(seed=SEED).select(range(n_(14000)))) | |
| for r in rows: | |
| labels, descs = pick(rng, variants) | |
| st, ref = wrap(rng, r["text"], ["article", "headline", "text", "news"]) | |
| if rng.random() < 0.8: | |
| q, y = choice_q(rng, pick(rng, TOPIC_INS).format(ref=ref), labels, r["label"], descs, p_full=0.8) | |
| else: | |
| q, y = noul_from_class(rng, TOPIC_NOUL, ref, labels, r["label"], descs) | |
| yield "ag_news", st, q, y | |
| def src_dbpedia(rng): | |
| d = load_dataset("fancyzhx/dbpedia_14", split="train").shuffle(seed=SEED).select(range(n_(9000))) | |
| names = d.features["label"].names | |
| nice = {"Company": "company", "EducationalInstitution": "educational institution", "Artist": "artist", | |
| "Athlete": "athlete", "OfficeHolder": "politician or office holder", "MeanOfTransportation": "vehicle or means of transportation", | |
| "Building": "building", "NaturalPlace": "natural place", "Village": "village", "Animal": "animal", | |
| "Plant": "plant", "Album": "music album", "Film": "film", "WrittenWork": "book or written work"} | |
| labels = [nice[n] for n in names] | |
| ins = ["What kind of entity does {ref} describe?", "Which category does the subject of {ref} belong to?", | |
| "What is {ref} about?", "Classify the entity described in {ref}."] | |
| for r in d: | |
| text = (r["title"] + ". " + r["content"]).strip() | |
| st, ref = wrap(rng, text, ["description", "text", "article", "entry"]) | |
| if rng.random() < 0.8: | |
| q, y = choice_q(rng, pick(rng, ins).format(ref=ref), labels, r["label"], None, p_full=0.5) | |
| else: | |
| q, y = noul_from_class(rng, ["Does {ref} describe a {label}?", "Is the subject of {ref} a {label}?"], | |
| ref, labels, r["label"]) | |
| yield "dbpedia", st, q, y | |
| def src_yahoo(rng): | |
| d = load_dataset("community-datasets/yahoo_answers_topics", split="train").shuffle(seed=SEED).select(range(n_(9000))) | |
| labels = [n.replace("&", "and") for n in d.features["topic"].names] | |
| ins = ["Which forum category fits {ref}?", "What is the topic of {ref}?", "Which section of a Q&A site should {ref} go to?", | |
| "What subject is {ref} about?"] | |
| for r in d: | |
| text = (r["question_title"] + " " + (r["question_content"] or "")).strip() | |
| if rng.random() < 0.4 and r["best_answer"]: | |
| st = {"question": text, "answer": r["best_answer"][:1200]} | |
| ref = "this question and answer" | |
| else: | |
| st, ref = wrap(rng, text, ["question", "post", "text"]) | |
| q, y = choice_q(rng, pick(rng, ins).format(ref=ref), labels, r["topic"], None, p_full=0.6) | |
| yield "yahoo", st, q, y | |
| def src_20ng(rng): | |
| d = load_dataset("SetFit/20_newsgroups", split="train").shuffle(seed=SEED) | |
| names = sorted(set(d["label_text"])) | |
| out = 0 | |
| for r in d: | |
| if len(r["text"].strip()) < 40: | |
| continue | |
| st, ref = wrap(rng, r["text"][:2500], ["post", "message", "text"]) | |
| q, y = choice_q(rng, pick(rng, ["Which newsgroup was {ref} posted to?", "Which discussion group does {ref} belong to?", | |
| "What is the topic of {ref}?"]).format(ref=ref), | |
| names, names.index(r["label_text"]), None, kmax=12, p_full=0.2) | |
| yield "20ng", st, q, y | |
| out += 1 | |
| if out >= n_(4000): | |
| break | |
| SENT_INS = ["What is the sentiment of {ref}?", "How does the author of {ref} feel?", "Is {ref} positive or negative?", | |
| "Classify the opinion expressed in {ref}.", "What attitude does {ref} express?"] | |
| def src_sst2(rng): | |
| d = load_dataset("stanfordnlp/sst2", split="train").shuffle(seed=SEED).select(range(n_(7000))) | |
| variants = [(["negative", "positive"], None), (["bad", "good"], None), | |
| (["negative", "positive"], {"negative": "the writer dislikes it", "positive": "the writer likes it"})] | |
| for r in d: | |
| st, ref = wrap(rng, r["sentence"], ["review", "text", "comment", "sentence"]) | |
| u = rng.random() | |
| if u < 0.6: | |
| labels, descs = pick(rng, variants) | |
| q, y = choice_q(rng, pick(rng, SENT_INS).format(ref=ref), labels, r["label"], descs, p_full=1.0) | |
| elif u < 0.8: | |
| q, y = noul_q(pick(rng, ["Is {ref} positive?", "Does {ref} express a favourable opinion?", | |
| "Did the writer of {ref} like it?"]).format(ref=ref)), r["label"] | |
| else: | |
| q, y = noul_q(pick(rng, ["Is {ref} negative?", "Does {ref} express criticism?", | |
| "Is the writer of {ref} unhappy?"]).format(ref=ref)), 1 - r["label"] | |
| yield "sst2", st, q, y | |
| def src_imdb(rng): | |
| d = load_dataset("stanfordnlp/imdb", split="train").shuffle(seed=SEED).select(range(n_(4000))) | |
| for r in d: | |
| st, ref = wrap(rng, r["text"].replace("<br />", " ")[:3000], ["review", "text", "comment"]) | |
| if rng.random() < 0.6: | |
| q, y = choice_q(rng, pick(rng, SENT_INS).format(ref=ref), ["negative", "positive"], r["label"], None, p_full=1.0) | |
| else: | |
| q, y = noul_q(pick(rng, ["Did the reviewer enjoy the film in {ref}?", "Is {ref} a positive review?", | |
| "Would the author of {ref} recommend the movie?"]).format(ref=ref)), r["label"] | |
| yield "imdb", st, q, y | |
| STAR_LEVELS = [ | |
| ["very negative", "negative", "mixed or neutral", "positive", "very positive"], | |
| ["1 star: terrible", "2 stars: poor", "3 stars: average", "4 stars: good", "5 stars: excellent"], | |
| ["very dissatisfied", "dissatisfied", "neutral", "satisfied", "very satisfied"], | |
| ["awful", "bad", "okay", "good", "great"], | |
| ] | |
| SCORE_INS = ["How positive is {ref}?", "Rate the sentiment of {ref}.", "How satisfied is the writer of {ref}?", | |
| "What rating best fits {ref}?", "How favourable is {ref}?"] | |
| def _rating(rng, src, text, label, fields): | |
| st, ref = wrap(rng, text, fields) | |
| u = rng.random() | |
| if u < 0.65: | |
| levels = list(pick(rng, STAR_LEVELS)) | |
| q = {"type": "score", "instructions": pick(rng, SCORE_INS).format(ref=ref), "criteria": levels} | |
| return src, st, q, label | |
| if u < 0.85: | |
| labels = ["1 star", "2 stars", "3 stars", "4 stars", "5 stars"] | |
| q, y = choice_q(rng, pick(rng, ["How many stars did the writer of {ref} give?", "Which star rating matches {ref}?"]).format(ref=ref), | |
| labels, label, None, p_full=1.0) | |
| return src, st, q, y | |
| if label == 2: | |
| return None | |
| return src, st, noul_q(pick(rng, ["Is {ref} positive overall?", "Is the writer of {ref} happy with it?"]).format(ref=ref)), int(label > 2) | |
| def src_yelp(rng): | |
| d = load_dataset("Yelp/yelp_review_full", split="train").shuffle(seed=SEED).select(range(n_(10000))) | |
| for r in d: | |
| rec = _rating(rng, "yelp", r["text"][:2500], r["label"], ["review", "text", "feedback"]) | |
| if rec: | |
| yield rec | |
| def src_amazon_multi(rng): | |
| # the repo still has a loading script; read the Hub's auto-converted parquet instead | |
| d = load_dataset("parquet", data_files="hf://datasets/mteb/amazon_reviews_multi@refs%2Fconvert%2Fparquet/all_languages/train/*.parquet", | |
| split="train").shuffle(seed=SEED).select(range(n_(9000))) | |
| for r in d: | |
| rec = _rating(rng, "amazon_multi", r["text"][:2000], int(r["label"]), ["review", "text", "feedback"]) | |
| if rec: | |
| yield rec | |
| def src_tweet_sent_multi(rng): | |
| d = load_dataset("parquet", data_files="hf://datasets/cardiffnlp/tweet_sentiment_multilingual@refs%2Fconvert%2Fparquet/all/train/*.parquet", | |
| split="train").shuffle(seed=SEED).select(range(n_(7000))) | |
| variants = [["negative", "neutral", "positive"], ["bad", "neutral", "good"]] | |
| for r in d: | |
| st, ref = wrap(rng, r["text"], ["tweet", "post", "text"]) | |
| q, y = choice_q(rng, pick(rng, SENT_INS).format(ref=ref), pick(rng, variants), int(r["label"]), None, p_full=1.0) | |
| yield "tweet_sent_multi", st, q, y | |
| EMO_INS = ["Which emotion is expressed in {ref}?", "What emotion does the writer of {ref} feel most?", | |
| "Identify the main emotion in {ref}.", "What is the emotional tone of {ref}?", "How does the writer of {ref} feel?"] | |
| def src_go_emotions(rng): | |
| d = load_dataset("google-research-datasets/go_emotions", "simplified", split="train").shuffle(seed=SEED) | |
| names = d.features["labels"].feature.names | |
| out = 0 | |
| for r in d: | |
| if len(r["labels"]) != 1: | |
| continue | |
| g = r["labels"][0] | |
| st, ref = wrap(rng, r["text"], ["comment", "text", "message", "post"]) | |
| if rng.random() < 0.75: | |
| q, y = choice_q(rng, pick(rng, EMO_INS).format(ref=ref), names, g, None, kmax=12, p_full=0.15, kmin=3) | |
| else: | |
| q, y = noul_from_class(rng, ["Does {ref} express {label}?", "Is the writer of {ref} feeling {label}?"], | |
| ref, names, g) | |
| yield "go_emotions", st, q, y | |
| out += 1 | |
| if out >= n_(9000): | |
| break | |
| def src_tweet_emotion(rng): | |
| d = load_dataset("cardiffnlp/tweet_eval", "emotion", split="train").shuffle(seed=SEED) | |
| names = d.features["label"].names | |
| for r in d.select(range(min(len(d), n_(3200)))): | |
| st, ref = wrap(rng, r["text"], ["tweet", "text", "post"]) | |
| q, y = choice_q(rng, pick(rng, EMO_INS).format(ref=ref), names, r["label"], None, p_full=1.0) | |
| yield "tweet_emotion", st, q, y | |
| INTENT_INS = ["What does the user want in {ref}?", "Which intent does {ref} express?", "Classify the request in {ref}.", | |
| "What is the purpose of {ref}?", "Which action is being asked for in {ref}?", "What is {ref} asking for?"] | |
| def src_clinc(rng): | |
| d = load_dataset("clinc/clinc_oos", "plus", split="train").shuffle(seed=SEED) | |
| names = d.features["intent"].names | |
| shown = {n: ("out of scope: none of the listed requests" if n == "oos" else n.replace("_", " ")) for n in names} | |
| labels = [shown[n] for n in names] | |
| out = 0 | |
| for r in d: | |
| st, ref = wrap(rng, r["text"], ["utterance", "message", "query", "request"]) | |
| u = rng.random() | |
| if u < 0.75: | |
| q, y = choice_q(rng, pick(rng, INTENT_INS).format(ref=ref), labels, r["intent"], None, kmax=48, p_full=0.0, kmin=4) | |
| elif u < 0.83: | |
| q, y = choice_q(rng, pick(rng, INTENT_INS).format(ref=ref), labels, r["intent"], None, kmax=150, p_full=1.0) | |
| else: | |
| if names[r["intent"]] == "oos": | |
| continue | |
| q, y = noul_from_class(rng, ["Does {ref} ask to {label}?", "Is {ref} a request about {label}?", | |
| "Does the user in {ref} want {label}?"], ref, labels, r["intent"]) | |
| yield "clinc", st, q, y | |
| out += 1 | |
| if out >= n_(14000): | |
| break | |
| def src_bitext(rng): | |
| d = load_dataset("bitext/Bitext-customer-support-llm-chatbot-training-dataset", split="train").shuffle(seed=SEED) | |
| intents = sorted(set(d["intent"])) | |
| cats = sorted(set(d["category"])) | |
| il = [x.replace("_", " ") for x in intents] | |
| cl = [x.lower() for x in cats] | |
| for r in d.select(range(n_(7000))): | |
| st, ref = wrap(rng, r["instruction"], ["message", "ticket", "request", "customer_message"]) | |
| u = rng.random() | |
| if u < 0.55: | |
| q, y = choice_q(rng, pick(rng, INTENT_INS).format(ref=ref), il, intents.index(r["intent"]), None, kmax=27, p_full=0.4) | |
| elif u < 0.8: | |
| q, y = choice_q(rng, pick(rng, ["Which support category does {ref} fall under?", "Which department should handle {ref}?"]).format(ref=ref), | |
| cl, cats.index(r["category"]), None, p_full=0.7) | |
| else: | |
| q, y = noul_from_class(rng, ["Does the customer in {ref} want to {label}?", "Is {ref} about {label}?"], | |
| ref, il, intents.index(r["intent"])) | |
| yield "bitext", st, q, y | |
| QUEUES = {"Technical Support": "technical problems, bugs, outages, integrations", | |
| "Product Support": "help using a product or feature", | |
| "Customer Service": "general account or service questions", | |
| "IT Support": "internal IT, devices, access, networks", | |
| "Billing and Payments": "invoices, charges, refunds, payment methods", | |
| "Returns and Exchanges": "returning or exchanging an item", | |
| "Service Outages and Maintenance": "downtime, outages, scheduled maintenance", | |
| "Sales and Pre-Sales": "pricing, quotes, buying", | |
| "Human Resources": "employment, payroll, leave, hiring", | |
| "General Inquiry": "anything else"} | |
| def _support_rows(): | |
| """Ticket rows usable for training: skips the rows the benchmark uses (the first 400 English rows that | |
| pass its filter; we skip 1500).""" | |
| d = load_dataset("Tobi-Bueck/customer-support-tickets", split="train") | |
| seen_en = 0 | |
| keep = [] | |
| for r in d: | |
| if not r.get("body") or r.get("queue") not in QUEUES: | |
| continue | |
| if r.get("language") == "en": | |
| seen_en += 1 | |
| if seen_en <= 1500: | |
| continue | |
| keep.append(r) | |
| return keep | |
| def src_support_tickets(rng): | |
| keep = _support_rows() | |
| rng.shuffle(keep) | |
| keys = list(QUEUES) | |
| pri_levels = {"low": 0, "medium": 1, "high": 2} | |
| types = sorted(set(r["type"] for r in keep if r.get("type"))) | |
| for r in keep[:n_(9000)]: | |
| st = {"subject": r["subject"] or "", "body": r["body"].replace("\\n", "\n")[:3000]} | |
| u = rng.random() | |
| if u < 0.6: | |
| q, y = choice_q(rng, pick(rng, ["Which support queue should handle this ticket?", "Which team should this ticket go to?", | |
| "Route this ticket to the right queue."]), | |
| keys, keys.index(r["queue"]), QUEUES, p_full=0.8, p_desc=0.7) | |
| elif u < 0.8 and str(r.get("priority", "")).lower() in pri_levels: | |
| q = {"type": "score", "instructions": pick(rng, ["How urgent is this ticket?", "What priority should this ticket get?"]), | |
| "criteria": ["low: can wait", "medium: should be handled soon", "high: needs immediate attention"]} | |
| y = pri_levels[str(r["priority"]).lower()] | |
| elif r.get("type") in types and len(types) >= 2: | |
| q, y = choice_q(rng, "What type of ticket is this?", types, types.index(r["type"]), None, p_full=1.0) | |
| else: | |
| q, y = noul_from_class(rng, ["Should this ticket go to {label}?", "Is this ticket for the {label} queue?"], | |
| "", keys, keys.index(r["queue"])) | |
| yield "support_tickets", st, q, y | |
| def _email_state(subject, body): | |
| from laya import email_state | |
| return email_state(subject, body) | |
| SPAM_INS = ["Is this email spam?", "Is this email unsolicited bulk marketing?", "Is this message junk mail?", | |
| "Should this email go to the spam folder?"] | |
| # medium-agnostic wording, so "spam" is not learned as an email-only concept | |
| SPAM_GENERIC = ["Is {ref} spam?", "Is {ref} unsolicited advertising?", "Is {ref} a spam message?", | |
| "Is {ref} junk or a mass-marketing message?", "Would a spam filter be right to block {ref}?"] | |
| SPAM_FIELDS = ["message", "text", "content", "msg", "body", "post", "sms", "note"] | |
| def src_enron(rng): | |
| d = load_dataset("SetFit/enron_spam", split="train").shuffle(seed=SEED).select(range(n_(7000))) | |
| for r in d: | |
| if rng.random() < 0.35: # body only, under a random field name and generic wording | |
| body = (r.get("message") or r.get("subject") or "").strip()[:1500] | |
| if not body: | |
| continue | |
| st, ref = wrap(rng, body, SPAM_FIELDS, p_plain=0.3) | |
| yield "enron_spam", st, noul_q(pick(rng, SPAM_GENERIC).format(ref=ref)), int(r["label"]) | |
| continue | |
| st = _email_state(r.get("subject") or "", (r.get("message") or "")[:3000]) | |
| if rng.random() < 0.7: | |
| q, y = noul_q(pick(rng, SPAM_INS)), int(r["label"]) | |
| else: | |
| q, y = choice_q(rng, "Is this email spam or a legitimate message?", ["legitimate", "spam"], int(r["label"]), | |
| {"legitimate": "normal personal or business email", "spam": "unsolicited bulk or marketing email"}, | |
| p_full=1.0) | |
| yield "enron_spam", st, q, y | |
| def src_phishing(rng): | |
| d = load_dataset("zefang-liu/phishing-email-dataset", split="train") | |
| rows = [r for r in list(d)[6000:] if (r.get("Email Text") or "").strip() | |
| and r.get("Email Type") in ("Safe Email", "Phishing Email")] | |
| rng.shuffle(rows) | |
| ins = ["Is this email a phishing or scam attempt?", "Is `email` trying to steal money, passwords or personal data?", | |
| "Is this a fraudulent email?", "Is `email` a scam?"] | |
| generic = ["Is {ref} a scam?", "Is {ref} trying to trick the reader into giving money or passwords?", | |
| "Is {ref} fraudulent?", "Is {ref} a phishing attempt?"] | |
| for r in rows[:n_(6000)]: | |
| y = int(r["Email Type"] == "Phishing Email") | |
| if rng.random() < 0.3: | |
| st, ref = wrap(rng, r["Email Text"][:1500], SPAM_FIELDS, p_plain=0.3) | |
| yield "phishing", st, noul_q(pick(rng, generic).format(ref=ref)), y | |
| continue | |
| st = {"email": r["Email Text"][:3000]} if rng.random() < 0.7 else r["Email Text"][:3000] | |
| if rng.random() < 0.75: | |
| crit = {"true": "phishing, scam, or fraud", "false": "a legitimate email"} if rng.random() < 0.5 else None | |
| q = noul_q(pick(rng, ins), crit) | |
| else: | |
| q, y = choice_q(rng, "What kind of email is this?", ["legitimate", "phishing"], y, | |
| {"legitimate": "a normal email", "phishing": "a scam or credential-theft attempt"}, p_full=1.0) | |
| yield "phishing", st, q, y | |
| NLI_DESC = {"entailment": "the hypothesis must be true given the premise", | |
| "neutral": "the hypothesis may or may not be true", | |
| "contradiction": "the hypothesis must be false given the premise"} | |
| def _nli(rng, src, prem, hyp, label): | |
| if rng.random() < 0.7: | |
| st = {"premise": prem, "hypothesis": hyp} | |
| P, H = "`premise`", "`hypothesis`" | |
| else: | |
| st = "Premise: %s\nHypothesis: %s" % (prem, hyp) | |
| P, H = "the premise", "the hypothesis" | |
| names = ["entailment", "neutral", "contradiction"] | |
| u = rng.random() | |
| if u < 0.5: | |
| q, y = choice_q(rng, pick(rng, ["How does %s relate to %s?" % (H, P), "Given %s, is %s true, false or undetermined?" % (P, H)]), | |
| names, label, NLI_DESC, p_full=1.0, p_desc=0.7) | |
| elif u < 0.8: | |
| q, y = noul_q(pick(rng, ["Does %s imply %s?" % (P, H), "If %s is true, must %s also be true?" % (P, H)])), int(label == 0) | |
| else: | |
| q, y = noul_q(pick(rng, ["Does %s contradict %s?" % (H, P), "Are %s and %s incompatible?" % (P, H)])), int(label == 2) | |
| return src, st, q, y | |
| def src_mnli(rng): | |
| d = load_dataset("nyu-mll/glue", "mnli", split="train").shuffle(seed=SEED).select(range(n_(11000))) | |
| for r in d: | |
| yield _nli(rng, "mnli", r["premise"], r["hypothesis"], r["label"]) | |
| def src_snli(rng): | |
| d = load_dataset("stanfordnlp/snli", split="train").shuffle(seed=SEED).select(range(n_(7000))) | |
| for r in d: | |
| if r["label"] in (0, 1, 2): | |
| yield _nli(rng, "snli", r["premise"], r["hypothesis"], r["label"]) | |
| def _pair(rng, src, a, b, y, kind): | |
| if kind == "q": | |
| st = {"question1": a, "question2": b} | |
| ins = ["Are `question1` and `question2` asking the same thing?", "Is `question2` a duplicate of `question1`?"] | |
| else: | |
| st = {"sentence1": a, "sentence2": b} | |
| ins = ["Do `sentence1` and `sentence2` mean the same thing?", "Is `sentence2` a paraphrase of `sentence1`?"] | |
| return src, st, noul_q(pick(rng, ins)), int(y) | |
| def src_paws(rng): | |
| d = load_dataset("google-research-datasets/paws", "labeled_final", split="train").shuffle(seed=SEED).select(range(n_(5000))) | |
| for r in d: | |
| yield _pair(rng, "paws", r["sentence1"], r["sentence2"], r["label"], "s") | |
| def src_qqp(rng): | |
| d = load_dataset("nyu-mll/glue", "qqp", split="train").shuffle(seed=SEED).select(range(n_(5000))) | |
| for r in d: | |
| yield _pair(rng, "qqp", r["question1"], r["question2"], r["label"], "q") | |
| def src_mrpc(rng): | |
| d = load_dataset("nyu-mll/glue", "mrpc", split="train") | |
| for r in d: | |
| yield _pair(rng, "mrpc", r["sentence1"], r["sentence2"], r["label"], "s") | |
| def src_boolq(rng): | |
| d = load_dataset("google/boolq", split="train").shuffle(seed=SEED).select(range(n_(9000))) | |
| for r in d: | |
| qtext = r["question"].strip() | |
| qtext = qtext[0].upper() + qtext[1:] + ("" if qtext.endswith("?") else "?") | |
| if rng.random() < 0.6: | |
| st = {"passage": r["passage"][:2500]} | |
| ins = pick(rng, ["According to `passage`: %s" % qtext, "Based on `passage`, %s" % qtext[0].lower() + qtext[1:]]) | |
| else: | |
| st = {"passage": r["passage"][:2500], "question": qtext} | |
| ins = "According to `passage`, is the answer to `question` yes?" | |
| yield "boolq", st, noul_q(ins), int(r["answer"]) | |
| def src_race(rng): | |
| d = load_dataset("ehovy/race", "all", split="train").shuffle(seed=SEED).select(range(n_(9000))) | |
| for r in d: | |
| opts = r["options"] | |
| g = "ABCD".index(r["answer"]) | |
| st = {"article": r["article"][:3000]} | |
| qt = r["question"].strip() | |
| if "_" in qt: | |
| ins = "Fill the blank using `article`: %s" % qt | |
| else: | |
| ins = "Based on `article`: %s" % qt | |
| if rng.random() < 0.6: | |
| crit = {k: v for k, v in zip("ABCD", opts)} | |
| q = {"type": "choice", "instructions": ins, "criteria": crit} | |
| y = g | |
| else: | |
| uniq = list(dict.fromkeys(o.strip() for o in opts)) | |
| if len(uniq) != 4: | |
| continue | |
| q, y = choice_q(rng, ins, uniq, g, None, p_full=1.0) | |
| yield "race", st, q, y | |
| def src_csqa(rng): | |
| d = load_dataset("tau/commonsense_qa", split="train").shuffle(seed=SEED).select(range(n_(9000))) | |
| for r in d: | |
| texts = r["choices"]["text"] | |
| labs = r["choices"]["label"] | |
| if r["answerKey"] not in labs or len(set(texts)) != len(texts): | |
| continue | |
| g = labs.index(r["answerKey"]) | |
| if rng.random() < 0.5: | |
| st, ins = {"question": r["question"]}, "Which answer to `question` is correct?" | |
| else: | |
| st, ins = r["question"], pick(rng, ["Pick the best answer.", "Which option answers the question?"]) | |
| q, y = choice_q(rng, ins, texts, g, None, p_full=1.0) | |
| yield "commonsense_qa", st, q, y | |
| REL_INS = ["Does `passage` help answer `query`?", "Is `passage` relevant to `query`?", | |
| "Does `passage` contain the answer to `query`?", "Would `passage` be useful for answering `query`?"] | |
| def src_msmarco(rng): | |
| d = load_dataset("microsoft/ms_marco", "v1.1", split="train").shuffle(seed=SEED) | |
| out = 0 | |
| allp = [] | |
| for r in d: | |
| texts, sel = r["passages"]["passage_text"], r["passages"]["is_selected"] | |
| pos = [t for t, s in zip(texts, sel) if s == 1] | |
| neg = [t for t, s in zip(texts, sel) if s == 0] | |
| if not pos: | |
| continue | |
| allp.extend(texts[:2]) | |
| take_pos = rng.random() < 0.5 | |
| if take_pos: | |
| p, y = rng.choice(pos), 1 | |
| elif neg and rng.random() < 0.8: | |
| p, y = rng.choice(neg), 0 | |
| elif len(allp) > 100: | |
| p, y = rng.choice(allp[:-10]), 0 | |
| else: | |
| continue | |
| yield "ms_marco", {"query": r["query"], "passage": p}, noul_q(pick(rng, REL_INS)), y | |
| out += 1 | |
| if out >= n_(14000): | |
| break | |
| TOX_INS = ["Is {ref} toxic?", "Is {ref} rude or disrespectful?", "Does {ref} contain insults or hateful language?", | |
| "Would {ref} make someone leave a discussion?", "Is {ref} abusive?"] | |
| def src_civil(rng): | |
| d = load_dataset("google/civil_comments", split="train").shuffle(seed=SEED) | |
| pos, neg = [], [] | |
| target = n_(8000) | |
| for r in d: | |
| t = r["toxicity"] | |
| if t >= 0.5 and len(pos) < target // 2: | |
| pos.append(r) | |
| elif t < 0.1 and len(neg) < target // 2: | |
| neg.append(r) | |
| if len(pos) >= target // 2 and len(neg) >= target // 2: | |
| break | |
| rows = pos + neg | |
| rng.shuffle(rows) | |
| for r in rows: | |
| st, ref = wrap(rng, r["text"][:2500], ["comment", "post", "text", "message"]) | |
| y = int(r["toxicity"] >= 0.5) | |
| if rng.random() < 0.75: | |
| yield "civil_comments", st, noul_q(pick(rng, TOX_INS).format(ref=ref)), y | |
| else: | |
| q, y = choice_q(rng, "Is {ref} acceptable or toxic?".format(ref=ref), ["acceptable", "toxic"], y, | |
| {"acceptable": "civil, polite or neutral", "toxic": "rude, insulting or hateful"}, p_full=1.0) | |
| yield "civil_comments", st, q, y | |
| def src_hate_offensive(rng): | |
| d = load_dataset("tdavidson/hate_speech_offensive", split="train").shuffle(seed=SEED) | |
| names = ["hate speech", "offensive", "neither"] | |
| descs = {"hate speech": "attacks a group based on identity", "offensive": "rude or vulgar but not hate speech", | |
| "neither": "not offensive"} | |
| for r in d.select(range(min(len(d), n_(5000)))): | |
| st, ref = wrap(rng, r["tweet"], ["tweet", "post", "text"]) | |
| if rng.random() < 0.6: | |
| q, y = choice_q(rng, pick(rng, ["Is {ref} hate speech, offensive, or neither?", "How offensive is {ref}?"]).format(ref=ref), | |
| names, r["class"], descs, p_full=1.0) | |
| else: | |
| q, y = noul_q(pick(rng, ["Is {ref} offensive?", "Does {ref} contain offensive language?"]).format(ref=ref)), int(r["class"] < 2) | |
| yield "hate_offensive", st, q, y | |
| def src_tweet_off_hate(rng): | |
| for cfg, ins in (("offensive", ["Is {ref} offensive?", "Does {ref} contain offensive language?"]), | |
| ("hate", ["Does {ref} contain hate speech?", "Does {ref} attack people for who they are?"])): | |
| d = load_dataset("cardiffnlp/tweet_eval", cfg, split="train").shuffle(seed=SEED) | |
| for r in d.select(range(min(len(d), n_(4000)))): | |
| st, ref = wrap(rng, r["text"], ["tweet", "post", "text"]) | |
| yield "tweet_" + cfg, st, noul_q(pick(rng, ins).format(ref=ref)), int(r["label"]) | |
| # ------------------------------------------------------------------ v3: wording variety | |
| # More phrasings of the shared templates, so an answer depends on the concept asked, not on one exact wording. | |
| TOPIC_INS += ["What subject does {ref} deal with?", "Which topic fits {ref} best?", "What theme does {ref} cover?", | |
| "Under which heading would you file {ref}?", "Which category does {ref} fall into?", | |
| "What area is {ref} concerned with?", "Name the topic of {ref}.", "What kind of story is {ref}?"] | |
| SENT_INS += ["Is the tone of {ref} positive or negative?", "What opinion does {ref} convey?", | |
| "Does {ref} sound favourable or critical?", "How would you describe the sentiment in {ref}?", | |
| "What is the writer's attitude in {ref}?", "Is the author of {ref} pleased or displeased?", | |
| "Which sentiment best matches {ref}?", "What overall feeling does {ref} give?"] | |
| EMO_INS += ["Which feeling comes through in {ref}?", "What emotion best describes {ref}?", | |
| "Which emotion is the author of {ref} showing?", "What mood does {ref} convey?", | |
| "Name the dominant emotion in {ref}.", "How is the writer of {ref} feeling?", | |
| "Which emotional state does {ref} reflect?", "What is the strongest emotion in {ref}?"] | |
| INTENT_INS += ["What is the user trying to do in {ref}?", "What request is made in {ref}?", "Which goal does {ref} express?", | |
| "What does the sender of {ref} need?", "What task is {ref} about?", "Identify the intent behind {ref}.", | |
| "What help is requested in {ref}?", "Why did the user send {ref}?"] | |
| SCORE_INS += ["How favourable is the opinion in {ref}?", "On this scale, how happy is the writer of {ref}?", | |
| "How would the author of {ref} rate it?", "How pleased is the writer of {ref}?", "Score how positive {ref} is.", | |
| "What level of satisfaction does {ref} show?", "How much did the writer of {ref} like it?", | |
| "Where does {ref} fall on the scale?"] | |
| SPAM_GENERIC += ["Is {ref} junk?", "Does {ref} look like spam?", "Is {ref} an unwanted promotional message?", | |
| "Should {ref} be filtered out as spam?", "Is {ref} a mass-marketing or scam message?", | |
| "Would you mark {ref} as spam?", "Is {ref} unsolicited?", "Is {ref} spam rather than a genuine message?"] | |
| TOX_INS += ["Is {ref} offensive or hostile?", "Does {ref} insult or demean someone?", "Is the language in {ref} abusive?", | |
| "Would a moderator remove {ref} for toxicity?", "Is {ref} disrespectful?", "Does {ref} contain toxic language?", | |
| "Is {ref} hurtful or aggressive?", "Is {ref} likely to upset or drive away readers?"] | |
| REL_INS += ["Is `passage` useful for answering `query`?", "Does `passage` address `query`?", | |
| "Can `query` be answered from `passage`?", "Is `passage` on topic for `query`?", | |
| "Does `passage` give information that answers `query`?", "Would `passage` satisfy someone searching for `query`?"] | |
| def _alnum(s): | |
| return re.sub(r"[^a-z0-9]+", " ", str(s).lower()).strip() | |
| _SODA = [] | |
| def _soda_openers(k): | |
| """First lines of SODA dialogues: everyday casual messages (streamed; the corpus has 1.2M dialogues).""" | |
| if len(_SODA) < k: | |
| import itertools | |
| _SODA.clear() | |
| for r in itertools.islice(load_dataset("allenai/soda", split="train", streaming=True), k * 3): | |
| t = ((r.get("dialogue") or [""])[0] or "").strip() | |
| if 12 <= len(t) <= 220: | |
| _SODA.append(t) | |
| if len(_SODA) >= k: | |
| break | |
| return _SODA[:k] | |
| # ------------------------------------------------------------------ v3: request routing | |
| # Which kind of task a request is. Built from code, math, SQL, writing, trivia and small-talk sources; the routing | |
| # benchmark's own sources (gsm8k, mbpp) stay held out. | |
| ROUTE_DOMAINS = ["code", "math", "data", "writing", "facts", "chat"] | |
| ROUTE_TAX = [ | |
| {"code": ("code", "programming, debugging, software"), "math": ("math", "calculations, word problems, proofs"), | |
| "data": ("data analysis", "SQL queries, tables, statistics"), "writing": ("writing", "stories, essays, emails, creative text"), | |
| "facts": ("factual question", "facts, definitions, trivia, history"), "chat": ("chitchat", "small talk and casual conversation")}, | |
| {"code": ("programming", "writing or fixing code"), "math": ("mathematics", "solving a math or logic problem"), | |
| "data": ("data and SQL", "querying or analysing data"), "writing": ("creative writing", "producing a story, post or other text"), | |
| "facts": ("general knowledge", "looking up a fact"), "chat": ("casual chat", "chatting without a specific task")}, | |
| {"code": ("software engineering", None), "math": ("math or logic", None), "data": ("analytics", None), | |
| "writing": ("content writing", None), "facts": ("knowledge lookup", None), "chat": ("conversation", None)}, | |
| ] | |
| ROUTE_MODELS = {"code": "a coding assistant", "math": "a math solver", "data": "a data analyst", | |
| "writing": "a writing assistant", "facts": "a search or encyclopedia assistant", "chat": "a friendly chat companion"} | |
| ROUTE_INS = ["What domain does {ref} belong to?", "Which kind of request is {ref}?", "What type of task does {ref} describe?", | |
| "Which category best fits the request in {ref}?", "What is {ref} asking for help with?", | |
| "Route {ref} to the right category.", "What area does the request in {ref} fall under?", | |
| "Which skill is needed to handle {ref}?", "What sort of task is {ref}?", "Classify {ref} by task type."] | |
| ROUTE_MODEL_INS = ["Which assistant should handle {ref}?", "Which model is best suited to {ref}?", "Who should answer {ref}?", | |
| "Send {ref} to which specialist?"] | |
| ROUTE_NOUL = {"code": ["Is {ref} a programming request?", "Does {ref} ask for code?", "Would a coding assistant handle {ref}?"], | |
| "math": ["Does {ref} ask to solve a math problem?", "Is {ref} a math question?", "Does {ref} need a calculation?"], | |
| "data": ["Is {ref} about querying or analysing data?", "Does {ref} need SQL or data analysis?"], | |
| "writing": ["Does {ref} ask for creative writing?", "Is {ref} a writing task?"], | |
| "facts": ["Is {ref} a factual question?", "Does {ref} ask to look up a fact?"], | |
| "chat": ["Is {ref} small talk?", "Is {ref} casual conversation?"]} | |
| def src_route(rng): | |
| pool = [] | |
| def add(texts, dom, k): | |
| texts = [t.strip() for t in texts if t and len(t.strip()) >= 12] | |
| pool.extend((t[:1500], dom) for t in rng.sample(texts, min(k, len(texts)))) | |
| d = load_dataset("sahil2801/CodeAlpaca-20k", split="train") | |
| add([r["instruction"] + ("\n" + r["input"] if r["input"] and rng.random() < 0.5 else "") for r in d], "code", n_(1840)) | |
| add(load_dataset("openai/openai_humaneval", split="test")["prompt"], "code", n_(164)) | |
| sv = [] | |
| for sp in ("train", "test"): | |
| sv += [r["Body"].strip() + " " + r["Question"].strip() for r in load_dataset("ChilleD/SVAMP", split=sp)] | |
| add(sv, "math", n_(800)) | |
| add(load_dataset("deepmind/aqua_rat", "raw", split="train")["question"], "math", n_(600)) | |
| for cfg in ("algebra", "counting_and_probability", "geometry", "intermediate_algebra", "number_theory", "prealgebra", | |
| "precalculus"): | |
| add(load_dataset("EleutherAI/hendrycks_math", cfg, split="train")["problem"], "math", n_(86)) | |
| d = load_dataset("b-mc2/sql-create-context", split="train") | |
| add([r["question"] + ("\nTable: " + r["context"] if rng.random() < 0.3 else "") for r in d], "data", n_(1000)) | |
| d = load_dataset("gretelai/synthetic_text_to_sql", split="train") | |
| add([r["sql_prompt"] + ("\nSchema: " + r["sql_context"][:600] if rng.random() < 0.3 else "") for r in d], "data", n_(1000)) | |
| wp = [re.sub(r"^(\s*\[\s*[A-Za-z]+\s*\]\s*)+", "", p) for p in load_dataset("euclaise/writingprompts", split="validation")["prompt"]] | |
| add([("Write a story: " + p if rng.random() < 0.5 else p) for p in wp], "writing", n_(1000)) | |
| dolly = load_dataset("databricks/databricks-dolly-15k", split="train") | |
| add([r["instruction"] for r in dolly if r["category"] in ("creative_writing", "brainstorming")], "writing", n_(1000)) | |
| add([r["instruction"] for r in dolly if r["category"] in ("open_qa", "general_qa")], "facts", n_(800)) | |
| nq = load_dataset("google-research-datasets/nq_open", split="train")["question"][:40000] | |
| add([q[0].upper() + q[1:] + ("" if q.endswith("?") else "?") for q in nq], "facts", n_(900)) | |
| add(load_dataset("fancyzhx/dbpedia_14", split="train").shuffle(seed=SEED + 7).select(range(4000))["content"], "facts", n_(300)) | |
| add(_soda_openers(n_(2000)), "chat", n_(2000)) | |
| rng.shuffle(pool) | |
| for text, dom in pool: | |
| st, ref = wrap(rng, text, ["request", "query", "prompt", "message", "user_input", "question", "task"], p_plain=0.3) | |
| u = rng.random() | |
| if u < 0.55: | |
| tax = pick(rng, ROUTE_TAX) | |
| labels = [tax[k][0] for k in ROUTE_DOMAINS] | |
| descs = {tax[k][0]: tax[k][1] for k in ROUTE_DOMAINS} if tax["code"][1] else None | |
| q, y = choice_q(rng, pick(rng, ROUTE_INS).format(ref=ref), labels, ROUTE_DOMAINS.index(dom), descs, | |
| p_full=0.6, p_desc=0.6) | |
| elif u < 0.75: | |
| q, y = choice_q(rng, pick(rng, ROUTE_MODEL_INS).format(ref=ref), [ROUTE_MODELS[k] for k in ROUTE_DOMAINS], | |
| ROUTE_DOMAINS.index(dom), None, p_full=0.6) | |
| else: | |
| asked = dom if rng.random() < 0.5 else pick(rng, [k for k in ROUTE_DOMAINS if k != dom]) | |
| q, y = noul_q(pick(rng, ROUTE_NOUL[asked]).format(ref=ref)), int(asked == dom) | |
| yield "route", st, q, y | |
| # ------------------------------------------------------------------ v3: short-message spam | |
| HAM_INS = ["Is {ref} a legitimate message?", "Is {ref} a normal personal, work or service message?", | |
| "Is {ref} a genuine message rather than spam?", "Can {ref} be trusted as a real message?"] | |
| _UCI = set() | |
| def _uci_sms_keys(): | |
| """Every text of ucirvine/sms_spam (a held-out benchmark dataset): no training text may match one.""" | |
| if not _UCI: | |
| _UCI.update(_alnum(r["sms"]) for r in load_dataset("ucirvine/sms_spam", split="train")) | |
| return _UCI | |
| def src_short_msgs(rng): | |
| uci = _uci_sms_keys() | |
| d = load_dataset("CloveAI/india-spam-sms", split="train") | |
| spam = [r["text"] for r in d if str(r["label"]) == "1"] | |
| ham = [r["text"] for r in d if str(r["label"]) == "0"] | |
| t = load_dataset("alissonpadua/ham-spam-scam-toxic", split="train") | |
| templ = [r["text"] for r in t if r["label"] in ("spam", "scam")] # templated: only a small sample, for variety | |
| pos = rng.sample(spam, min(len(spam), n_(2600))) + rng.sample(templ, n_(400)) | |
| neg = rng.sample(ham, min(len(ham), n_(2400))) + _soda_openers(3000 + n_(600))[3000:] # past the openers routing uses | |
| items = [(x, 1) for x in pos] + [(x, 0) for x in neg] | |
| items = [(x.strip(), y) for x, y in items if x and x.strip() and _alnum(x) not in uci] | |
| rng.shuffle(items) | |
| for text, y in items: | |
| st, ref = wrap(rng, text[:600], ["sms", "message", "text", "msg", "chat_message", "notification", "comment"], p_plain=0.3) | |
| u = rng.random() | |
| if u < 0.6: | |
| q, yy = noul_q(pick(rng, SPAM_GENERIC).format(ref=ref)), y | |
| elif u < 0.75: | |
| q, yy = noul_q(pick(rng, HAM_INS).format(ref=ref)), 1 - y | |
| else: | |
| q, yy = choice_q(rng, pick(rng, ["Is {ref} spam or a normal message?", "Classify {ref} as spam or legitimate."]).format(ref=ref), | |
| ["legitimate", "spam"], y, {"legitimate": "a normal personal, work or service message", | |
| "spam": "unsolicited promotion, scam or junk"}, p_full=1.0) | |
| yield "short_msgs", st, q, yy | |
| # ------------------------------------------------------------------ v3: request safety (NVIDIA Aegis 2.0, CC-BY-4.0) | |
| # Harmful-request detection and harm categories. Not a jailbreak dataset: those all stay held out. | |
| AEGIS_CATS = {"Criminal Planning/Confessions": "planning or admitting a crime", "Violence": "violence", | |
| "Hate/Identity Hate": "hate against a group of people", "Harassment": "harassment or bullying", | |
| "Profanity": "profanity or obscene language", "Sexual": "sexual content", | |
| "Controlled/Regulated Substances": "drugs or other controlled substances", | |
| "PII/Privacy": "someone's private personal information", "Guns and Illegal Weapons": "guns or illegal weapons", | |
| "Suicide and Self Harm": "suicide or self-harm", "Unauthorized Advice": "risky medical, legal or financial advice", | |
| "Political/Misinformation/Conspiracy": "misinformation or conspiracy theories", | |
| "Fraud/Deception": "fraud or deception", "Sexual (minor)": "sexual content involving minors", | |
| "Illegal Activity": "illegal activity", "Threat": "threats", "Malware": "malware or hacking", | |
| "Copyright/Trademark/Plagiarism": "copyright infringement or plagiarism"} | |
| SAFE_NONE = "none: a safe, harmless request" | |
| # Content-harm wording only. Aegis labels say whether the CONTENT is harmful; phrasings about AI-assistant behaviour, | |
| # policies or filters ("should an assistant decline...") taught v3 to equate "harmful" with "tries to make an AI | |
| # ignore its rules" (jailbreak AUC 0.71 -> 0.56), a different concept these labels do not encode. | |
| SAFETY_INS = ["Is {ref} a harmful or unsafe request?", "Does {ref} ask for dangerous, illegal or abusive content?", | |
| "Could answering {ref} cause harm?", "Is {ref} requesting something harmful?", "Is {ref} a request for harmful content?", | |
| "Does {ref} seek to hurt someone or break the law?", "Is the content of {ref} dangerous?"] | |
| SAFE_INS = ["Is {ref} a harmless request?", "Is {ref} an acceptable request?", "Is {ref} harmless?", "Is {ref} a benign request?"] | |
| CAT_NOUL = ["Does {ref} involve {label}?", "Is {ref} about {label}?", "Does {ref} ask for {label}?", "Does {ref} contain {label}?"] | |
| CAT_CHOICE = ["What kind of harm does {ref} involve?", "Which risk category fits {ref}?", | |
| "What type of unsafe content, if any, is in {ref}?"] | |
| def src_aegis(rng): | |
| d = load_dataset("nvidia/Aegis-AI-Content-Safety-Dataset-2.0", split="train") | |
| rows = [r for r in d if (r["prompt"] or "").strip() not in ("", "REDACTED") and r["prompt_label"] in ("safe", "unsafe")] | |
| rng.shuffle(rows) | |
| allc = list(AEGIS_CATS) | |
| for r in rows[:n_(9000)]: | |
| y = int(r["prompt_label"] == "unsafe") | |
| cats = [c.strip() for c in (r["violated_categories"] or "").split(",") if c.strip()] | |
| known = [c for c in cats if c in AEGIS_CATS] | |
| st, ref = wrap(rng, r["prompt"].strip()[:2000], ["prompt", "request", "message", "user_input", "query"], p_plain=0.3) | |
| u = rng.random() | |
| if u < 0.5: | |
| q, yy = noul_q(pick(rng, SAFETY_INS).format(ref=ref)), y | |
| elif u < 0.65: | |
| q, yy = noul_q(pick(rng, SAFE_INS).format(ref=ref)), 1 - y | |
| elif y and not known: # unsafe without a named category: category questions would be ambiguous | |
| continue | |
| elif u < 0.85: | |
| if y and rng.random() < 0.5: | |
| c, yy = pick(rng, known), 1 | |
| else: | |
| c, yy = pick(rng, [x for x in allc if x not in cats]), 0 | |
| q = noul_q(pick(rng, CAT_NOUL).format(ref=ref, label=AEGIS_CATS[c])) | |
| else: | |
| gold = AEGIS_CATS[pick(rng, known)] if y else SAFE_NONE | |
| labels = rng.sample([AEGIS_CATS[c] for c in allc if c not in cats], rng.randint(2, 5)) + [gold] | |
| if y: | |
| labels.append(SAFE_NONE) | |
| rng.shuffle(labels) | |
| q = {"type": "choice", "instructions": pick(rng, CAT_CHOICE).format(ref=ref), "criteria": {l: None for l in labels}} | |
| yy = labels.index(gold) | |
| yield "aegis_safety", st, q, yy | |
| # ------------------------------------------------------------------ v3: urgency, asked many ways | |
| URGENCY_INS = ["Is this ticket urgent?", "Does the customer express urgency?", "Does this ticket need immediate attention?", | |
| "Is the sender in a hurry?", "Should this ticket be handled first?", "Is this a time-critical request?", | |
| "Does the writer sound urgent?", "Is this an emergency for the customer?", "Must this be resolved right away?", | |
| "Is the customer asking for a fast response?", "Is this request time-sensitive?", | |
| "Does this issue need to be fixed quickly?"] | |
| URGENCY_REF = ["Is {ref} urgent?", "Does {ref} express urgency?", "Does {ref} need immediate attention?", | |
| "Is the writer of {ref} in a hurry?", "Is {ref} time-sensitive?", "Should {ref} be dealt with right away?"] | |
| def src_ticket_urgency(rng): | |
| rows = [r for r in _support_rows() if r.get("language") == "en"] | |
| high = [r for r in rows if str(r.get("priority", "")).lower() == "high"] | |
| low = [r for r in rows if str(r.get("priority", "")).lower() == "low"] | |
| k = min(len(high), len(low), n_(3500) // 2) # balanced: the base rate must not decide the answer | |
| rows = rng.sample(high, k) + rng.sample(low, k) | |
| rng.shuffle(rows) | |
| for r in rows: | |
| y = int(str(r["priority"]).lower() == "high") | |
| body = r["body"].replace("\\n", "\n")[:3000] | |
| if rng.random() < 0.6: | |
| st, q = {"subject": r["subject"] or "", "body": body}, noul_q(pick(rng, URGENCY_INS)) | |
| else: | |
| st, ref = wrap(rng, body, ["ticket", "message", "email", "request"], p_plain=0.3) | |
| q = noul_q(pick(rng, URGENCY_REF).format(ref=ref)) | |
| yield "ticket_urgency", st, q, y | |
| # ------------------------------------------------------------------ multilingual sources (Nirnay-multilingual only) | |
| # English builds skip these and amazon_multi / tweet_sent_multi: | |
| # NIRNAY_SKIP_SOURCES=amazon_multi,tweet_sent_multi,mtop,paws_x,nli_multi,toxic_multi,lang_id | |
| # MASSIVE and SIB-200 (the multilingual benchmarks) stay held out; MTOP is a different dataset with similar | |
| # assistant intents. | |
| def _mtop_intent(x): | |
| return x.split(":", 1)[-1].lower().replace("_", " ") | |
| def src_mtop(rng): | |
| d = load_dataset("tasksource/mtop", split="train").shuffle(seed=SEED) | |
| intents = sorted({_mtop_intent(x) for x in d["intent"]}) | |
| domains = sorted(set(d["domain"])) | |
| for r in d.select(range(min(len(d), n_(10000)))): | |
| st, ref = wrap(rng, r["question"], ["utterance", "message", "query", "request", "command"]) | |
| u = rng.random() | |
| if u < 0.6: | |
| q, y = choice_q(rng, pick(rng, INTENT_INS).format(ref=ref), intents, intents.index(_mtop_intent(r["intent"])), | |
| None, kmax=48, p_full=0.1, kmin=4) | |
| elif u < 0.85: | |
| q, y = choice_q(rng, pick(rng, ["Which assistant domain does {ref} belong to?", "Which app should handle {ref}?", | |
| "What area is {ref} about?"]).format(ref=ref), domains, domains.index(r["domain"]), | |
| None, p_full=0.7) | |
| else: | |
| q, y = noul_from_class(rng, ["Does {ref} ask to {label}?", "Is {ref} a request to {label}?"], ref, intents, | |
| intents.index(_mtop_intent(r["intent"]))) | |
| yield "mtop", st, q, y | |
| def src_paws_x(rng): | |
| for lang in ("de", "es", "fr", "ja", "ko", "zh"): | |
| d = load_dataset("google-research-datasets/paws-x", lang, split="train").shuffle(seed=SEED) | |
| for r in d.select(range(min(len(d), n_(900)))): | |
| if r["sentence1"] and r["sentence2"]: | |
| yield _pair(rng, "paws_x", r["sentence1"], r["sentence2"], int(r["label"]), "s") | |
| def src_nli_multi(rng): | |
| from huggingface_hub import HfApi | |
| repo = "MoritzLaurer/multilingual-NLI-26lang-2mil7" | |
| files = sorted(f for f in HfApi().list_repo_files(repo, repo_type="dataset") if f.endswith(".parquet") and "_mnli-" in f) | |
| for f in files: # machine-translated MNLI in 26 languages (labels: 0 entailment, 1 neutral, 2 contradiction) | |
| d = load_dataset("parquet", data_files="hf://datasets/%s/%s" % (repo, f), split="train").shuffle(seed=SEED) | |
| for r in d.select(range(min(len(d), n_(380)))): | |
| if r["premise"] and r["hypothesis"] and int(r["label"]) in (0, 1, 2): | |
| yield _nli(rng, "nli_multi", r["premise"], r["hypothesis"], int(r["label"])) | |
| def src_toxic_multi(rng): | |
| for lang in ("ru", "uk", "de", "es", "am", "zh", "ar", "hi", "it", "fr", "he", "hin", "tt", "ja"): | |
| d = load_dataset("textdetox/multilingual_toxicity_dataset", split=lang).shuffle(seed=SEED) | |
| for r in d.select(range(min(len(d), n_(650)))): | |
| st, ref = wrap(rng, r["text"][:1500], ["comment", "post", "text", "message"]) | |
| y = int(r["toxic"]) | |
| if rng.random() < 0.8: | |
| yield "toxic_multi", st, noul_q(pick(rng, TOX_INS).format(ref=ref)), y | |
| else: | |
| q, y = choice_q(rng, "Is {ref} acceptable or toxic?".format(ref=ref), ["acceptable", "toxic"], y, | |
| {"acceptable": "civil, polite or neutral", "toxic": "rude, insulting or hateful"}, p_full=1.0) | |
| yield "toxic_multi", st, q, y | |
| LANG_NAMES = {"ar": "Arabic", "bg": "Bulgarian", "de": "German", "el": "Greek", "en": "English", "es": "Spanish", | |
| "fr": "French", "hi": "Hindi", "it": "Italian", "ja": "Japanese", "nl": "Dutch", "pl": "Polish", | |
| "pt": "Portuguese", "ru": "Russian", "sw": "Swahili", "th": "Thai", "tr": "Turkish", "ur": "Urdu", | |
| "vi": "Vietnamese", "zh": "Chinese"} | |
| def src_lang_id(rng): | |
| d = load_dataset("papluca/language-identification", split="train").shuffle(seed=SEED) | |
| codes = sorted(LANG_NAMES) | |
| names = [LANG_NAMES[c] for c in codes] | |
| for r in d.select(range(min(len(d), n_(6000)))): | |
| st, ref = wrap(rng, r["text"][:1000], ["text", "message", "sentence", "comment"]) | |
| g = codes.index(r["labels"]) | |
| if rng.random() < 0.75: | |
| q, y = choice_q(rng, pick(rng, ["Which language is {ref} written in?", "What language is {ref} in?", | |
| "Identify the language of {ref}."]).format(ref=ref), names, g, None, | |
| kmax=8, p_full=0.15, kmin=3) | |
| else: | |
| q, y = noul_from_class(rng, ["Is {ref} written in {label}?", "Is the language of {ref} {label}?"], ref, names, g) | |
| yield "lang_id", st, q, y | |
| SOURCES = [src_ag_news, src_dbpedia, src_yahoo, src_20ng, src_sst2, src_imdb, src_yelp, src_amazon_multi, | |
| src_tweet_sent_multi, src_go_emotions, src_tweet_emotion, src_clinc, src_bitext, src_support_tickets, | |
| src_enron, src_phishing, src_mnli, src_snli, src_paws, src_qqp, src_mrpc, src_boolq, src_race, src_csqa, | |
| src_msmarco, src_civil, src_hate_offensive, src_tweet_off_hate, | |
| src_route, src_short_msgs, src_aegis, src_ticket_urgency, | |
| src_mtop, src_paws_x, src_nli_multi, src_toxic_multi, src_lang_id] | |
| def split_of(state) -> str: | |
| h = int(hashlib.sha1(json.dumps(state, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()[:8], 16) % 100 | |
| return "dev" if h < 2 else "cal" if h < 4 else "train" | |
| def main(): | |
| os.makedirs(os.path.dirname(OUT), exist_ok=True) | |
| load_eval_keys() | |
| only = set(filter(None, os.environ.get("NIRNAY_SOURCES", "").split(","))) | |
| skip = set(filter(None, os.environ.get("NIRNAY_SKIP_SOURCES", "").split(","))) | |
| stats, dropped = Counter(), Counter() | |
| with open(OUT, "a" if os.environ.get("NIRNAY_APPEND") else "w", encoding="utf-8") as f: | |
| for fn in SOURCES: | |
| name = fn.__name__[4:] | |
| if (only and name not in only) or name in skip: | |
| continue | |
| rng = random.Random("%s-%s" % (SEED, name)) | |
| CURRENT["src"] = name | |
| try: | |
| for src, st, q, y in fn(rng): | |
| if leaks(st): | |
| dropped[src] += 1 | |
| continue | |
| f.write(json.dumps({"src": src, "state": st, "q": q, "y": int(y), "split": split_of(st)}, | |
| ensure_ascii=False) + "\n") | |
| stats[src] += 1 | |
| except Exception as e: # one broken source must not sink the build | |
| print(" FAIL %-18s %s" % (name, str(e)[:160]), flush=True) | |
| continue | |
| print(" %-18s done (%s)" % (name, ", ".join("%s=%d" % (k, v) for k, v in stats.items() if k.startswith(name[:5]) or name in k)), flush=True) | |
| print("records:", sum(stats.values()), dict(stats)) | |
| print("dropped as benchmark overlap:", dict(dropped)) | |
| if __name__ == "__main__": | |
| main() | |