Spaces:
Running
Running
File size: 14,422 Bytes
09e451a 4f740eb 09e451a 4f740eb 09e451a bf2993f 09e451a bf2993f 09e451a bf2993f 09e451a bf2993f 09e451a bf2993f 09e451a bf2993f 09e451a bf2993f 09e451a bf2993f 09e451a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | """DataInventor engine: one sentence in, a training dataset out, on any OpenAI-compatible endpoint.
Pipeline: blueprint (taxonomy + personas + question shapes) -> sampled combinations ->
question -> answer (or two answers + a ranking for preference pairs) -> filters -> near-duplicate rejection.
"""
from __future__ import annotations
import json
import random
import re
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from typing import Optional
from datainventor.llm import HF_BASE, HF_MODEL, Endpoint, EndpointError # noqa: F401 (main imports them from here)
# ---------------------------------------------------------------- blueprint
def _s(x) -> str:
"""Models sometimes return {"persona": "..."} instead of a bare string."""
if isinstance(x, dict):
for k in ("name", "persona", "role", "format", "shape", "topic", "description", "title"):
if isinstance(x.get(k), str):
return x[k].strip()
return " ".join(str(v) for v in x.values() if isinstance(v, str)).strip()
return str(x).strip()
def normalize_blueprint(bp: dict) -> dict:
out = {"topics": [], "personas": [_s(p) for p in bp.get("personas", []) if _s(p)],
"formats": [_s(f) for f in bp.get("formats", []) if _s(f)],
"difficulty": [_s(d) for d in bp.get("difficulty", []) if _s(d)] or ["basic", "intermediate", "advanced"],
"assistant_role": _s(bp.get("assistant_role") or "an excellent, accurate AI assistant")[:300]}
for a in bp.get("topics", []):
subs = []
for sub in a.get("subtopics", []):
leaves = [_s(l) for l in sub.get("leaf_topics", []) if _s(l)]
if leaves:
subs.append({"subtopic": _s(sub.get("subtopic", "")), "leaf_topics": leaves})
if subs:
out["topics"].append({"area": _s(a.get("area", "")), "subtopics": subs})
if len(out["topics"]) < 1 or len(out["personas"]) < 2 or len(out["formats"]) < 2:
raise ValueError("the blueprint is missing topics, personas or formats")
return out
def _json_from(text: str):
m = re.search(r"\{.*\}", text, re.S)
if not m:
raise ValueError("no JSON object in the reply")
return json.loads(m.group(0))
def make_blueprint(ep: Endpoint, request: str, domains: list[str], subdomains: list[str], languages: list[str],
constraints: str, rows: int) -> dict:
scope = ""
if domains:
scope += f"Domains: {', '.join(domains)}.\n"
if subdomains:
scope += f"Subdomains: {', '.join(subdomains)}.\n"
if constraints.strip():
scope += f"Output requirements: {constraints.strip()}\n"
areas = 6 if rows <= 500 else 8
prompt = f"""You are planning a training dataset. Dataset request: "{request}"
{scope}
Return ONLY JSON with these keys:
"topics": a list of {areas} objects {{"area": str, "subtopics": [4 objects {{"subtopic": str, "leaf_topics": [4 specific, concrete strings]}}]}}. Cover the whole request evenly.
"personas": 10 different kinds of people who would write the user side of this dataset (for a customer-support dataset, 10 different kinds of customer; for a coding dataset, 10 kinds of developer).
"assistant_role": one sentence describing who answers in this dataset and in what voice (for example "a friendly support agent at a small online bookshop who gives concrete next steps").
"formats": 6 different shapes the instruction could take (for example a short direct ask, a detailed brief, a request with a concrete example, a request with constraints, a follow-up, a comparison).
"difficulty": ["basic","intermediate","advanced"].
No commentary, no code fences."""
last = None
for _ in range(4):
try:
return normalize_blueprint(_json_from(ep.chat(prompt, max_tokens=5000, temperature=0.7)))
except EndpointError:
raise
except Exception as e:
last = e
raise EndpointError(f"The model did not return a usable blueprint: {last}")
# ---------------------------------------------------------------- generation
LEAK = re.compile(r"the user (asks|wants)|write one|return only|^okay|^sure,|^here is|^here's|^as an ai", re.I)
END = re.compile(r"[.?!)\"”’'।:。!?]$")
MULTI = re.compile(r"\b(generate|draft|write|compile|create|give me|produce)\s+(\d+|several|multiple|a set of|a list of)\b", re.I)
PLACEHOLDER = re.compile(r"\[[A-Za-z'’ ]{0,30}\b(name|number|email|address|phone|date|id)\]", re.I)
CALLBACK = re.compile(r"\b(like|as) you (said|mentioned|suggested|told me)\b|\b(last time|earlier) (you|we)\b|\bthanks again\b", re.I)
TABLE = re.compile(r"^\s*\|.*\|\s*$\n^\s*\|?\s*:?-{3,}", re.M)
VERDICT = re.compile(r"\W*\(?([AB])\b", re.I)
LENGTHS = ["one blunt sentence", "two sentences with one specific detail",
"three or four sentences with concrete specifics", "a short paragraph that sounds a little rushed"]
def degenerate(t: str) -> bool:
w = t.lower().split()
return bool(re.search(r"(.)\1{7,}", t)) or (len(w) >= 8 and len(set(w)) / len(w) < 0.5)
def grams(t: str, n: int = 3) -> set:
w = t.lower().split()
return set(zip(*[w[i:] for i in range(n)])) if len(w) >= n else {tuple(w)}
@dataclass
class Job:
id: str
config: dict
state: str = "queued" # queued | planning | generating | done | failed | stopped
message: str = ""
blueprint: Optional[dict] = None
rows: list = field(default_factory=list)
target: int = 0
attempts: int = 0
rejected: dict = field(default_factory=dict)
started: float = field(default_factory=time.time)
finished: float = 0.0
stop: threading.Event = field(default_factory=threading.Event)
lock: threading.Lock = field(default_factory=threading.Lock)
def reject(self, why: str):
with self.lock:
self.rejected[why] = self.rejected.get(why, 0) + 1
def run_job(job: Job, ep: Endpoint):
cfg = job.config
rows_n = int(cfg["rows"])
job.target = rows_n
kind = cfg.get("format", "instruction")
request = cfg["request"]
languages = cfg.get("languages") or ["English"]
constraints = cfg.get("constraints", "")
try:
job.state, job.message = "planning", "Drafting the blueprint"
bp = normalize_blueprint(cfg["blueprint"]) if cfg.get("blueprint") else make_blueprint(
ep, request, cfg.get("domains", []), cfg.get("subdomains", []), languages, constraints, rows_n)
job.blueprint = bp
leaves = [(a["area"], s["subtopic"], l) for a in bp["topics"] for s in a["subtopics"] for l in s["leaf_topics"]]
rng = random.Random(cfg.get("seed", 7))
combos, seen = [], set()
want = int(rows_n * 1.8) + 20
guard = 0
while len(combos) < want and guard < want * 30:
guard += 1
c = (rng.choice(leaves), rng.choice(bp["personas"]), rng.choice(bp["formats"]), rng.choice(bp["difficulty"]),
rng.choice(LENGTHS), rng.choice(languages))
if c not in seen:
seen.add(c)
combos.append(c)
scope = f'The dataset being built: "{request}"'
if constraints.strip():
scope += f"\nEvery example must satisfy: {constraints.strip()}"
def ok_q(t):
t = t.strip().strip('"')
return (15 <= len(t) <= 1500 and END.search(t) and not degenerate(t) and not LEAK.search(t[:150])
and not MULTI.search(t[:120]) and not PLACEHOLDER.search(t) and not CALLBACK.search(t))
def ok_a(t):
return (len(t.split()) >= 12 and not degenerate(t[:600]) and not LEAK.search(t[:100])
and not TABLE.search(t) and not PLACEHOLDER.search(t))
kept_grams: list[set] = []
job.state, job.message = "generating", "Writing examples"
def work(idx_combo):
idx, ((area, sub, leaf), persona, fmt, diff, length, lang) = idx_combo
if job.stop.is_set():
return
with job.lock:
if len(job.rows) >= rows_n:
return
job.attempts += 1
try:
role = bp.get("assistant_role", "an excellent AI assistant")
q = ep.chat(f"""{scope}
Write ONE realistic message that {persona} would send to {role}, for this dataset, about: {leaf} ({area}, {sub}).
Shape: {fmt}. Difficulty: {diff}. Length: {length}. Language: {lang}.
It should sound like a real person typing, specific and a little imperfect, not a textbook prompt. It must be ONE question or ONE task whose answer fits in a single reply: never ask for a list of many items, a set of Q&A pairs, or a dataset. Do not name the shape or the persona.
It is the first message in the conversation: do not refer to earlier chats or to anything said before. Do not use placeholders such as [Name] or [order number].
Return only the message.""", max_tokens=700, temperature=1.0, validate=ok_q).strip().strip('"')
g = grams(q)
with job.lock:
if any(len(g & k) / max(1, len(g | k)) > 0.5 for k in kept_grams):
job.reject("near-duplicate question")
return
answer_prompt = f"""You are {role}. Reply to the message below. The reply is a training example for this dataset: "{request}".
Language: {lang}. Be accurate, specific and complete. Start directly with the content, with no preamble about who you are. Do not invent statistics or facts you cannot know.
Write plain prose: short paragraphs, or a simple numbered list for steps. No tables, headings or emoji.
Never invent order numbers, dates, prices, phone numbers, emails, URLs, product or brand names, menu paths or policies. If a detail like that is needed, ask for it or speak generally. Do not use placeholders such as [Your Name].
For physical techniques and safety, give only the standard, widely taught practice, and make sure every step agrees with the others.
{('Requirements: ' + constraints.strip()) if constraints.strip() else ''}
Message: {q}"""
a1 = ep.chat(answer_prompt, max_tokens=2500, temperature=0.7, validate=ok_a)
row = {"area": area, "subtopic": sub, "topic": leaf, "persona": persona, "shape": fmt,
"difficulty": diff, "language": lang, "prompt": q}
if kind == "preference":
a2 = ep.chat(answer_prompt + "\n\nWrite a different answer from the obvious one, in a different structure and with different emphasis.",
max_tokens=2500, temperature=1.0, validate=ok_a)
def judge(first: str, second: str) -> str:
v = ep.chat(f"""Which answer is better for the message? Judge accuracy first: an answer that invents details (order numbers, dates, contact details, policies) or gives unsafe advice is worse. Then completeness and clarity.
Message: {q}
Answer A:
{first[:3000]}
Answer B:
{second[:3000]}
Reply with only the single letter A or B.""", max_tokens=16, temperature=0, allow_cut=True,
validate=lambda s: bool(VERDICT.match(s)))
return VERDICT.match(v).group(1).upper()
# judge both orders so the first-shown answer gets no edge; keep the row only if they agree
v1, v2 = judge(a1, a2), judge(a2, a1)
if (v1 == "A") != (v2 == "B"):
job.reject("judge disagreed")
return
row["chosen"], row["rejected"] = (a1, a2) if v1 == "A" else (a2, a1)
else:
row["completion"] = a1
with job.lock:
if len(job.rows) >= rows_n:
return
if any(len(g & k) / max(1, len(g | k)) > 0.5 for k in kept_grams):
job.reject("near-duplicate question")
return
row["id"] = len(job.rows) + 1
job.rows.append(row)
kept_grams.append(g)
job.message = f"{len(job.rows)} of {rows_n} written"
if len(job.rows) >= rows_n:
job.state, job.message, job.finished = "done", f"{len(job.rows)} rows ready", time.time()
job.stop.set()
except EndpointError as e:
if "refused the key" in str(e) or "not found" in str(e):
job.state, job.message = "failed", str(e)
job.stop.set()
else:
job.reject("model call failed")
except Exception:
job.reject("model call failed")
with ThreadPoolExecutor(max_workers=max(1, min(ep.workers, 16))) as pool:
list(pool.map(work, enumerate(combos)))
if job.state in ("failed", "done"):
return
if job.stop.is_set():
job.state, job.message = "stopped", f"Stopped with {len(job.rows)} rows"
elif len(job.rows) < rows_n:
job.state = "done"
job.message = f"Finished with {len(job.rows)} of {rows_n} rows. The model rejected too many attempts to fill the rest."
else:
job.state, job.message = "done", f"{len(job.rows)} rows ready"
except EndpointError as e:
job.state, job.message = "failed", str(e)
except Exception as e: # never leave a job hanging
job.state, job.message = "failed", f"Unexpected error: {e}"
finally:
job.finished = time.time()
# ---------------------------------------------------------------- stats
def stats(rows: list) -> dict:
if not rows:
return {}
text = "\n".join(r["prompt"] for r in rows).lower().split()
tri = list(zip(text, text[1:], text[2:]))
area_counts: dict = {}
for r in rows:
area_counts[r["area"]] = area_counts.get(r["area"], 0) + 1
comp = [len((r.get("completion") or r.get("chosen") or "").split()) for r in rows]
return {"rows": len(rows), "distinct3_prompts": round(len(set(tri)) / max(1, len(tri)), 3),
"avg_prompt_words": round(sum(len(r["prompt"].split()) for r in rows) / len(rows), 1),
"avg_answer_words": round(sum(comp) / len(rows), 1), "areas": area_counts}
|