Spaces:
Running
Running
0.2.2: route through the authenticated optillm proxy (fast tier via DI_MIN_MAX_TOKENS), prompt fixes (plain prose, no invented specifics, no callbacks/placeholders), two-order accuracy-first preference judge, accurate dataset-card provenance
Browse files- .gitattributes +1 -0
- README.md +1 -1
- app/curate_api.py +1 -1
- app/engine.py +26 -9
- app/main.py +2 -1
- app/publish.py +2 -1
- datainventor/llm.py +9 -3
- dist/datainventor-0.2.2-py3-none-any.whl +3 -0
- pyproject.toml +1 -1
.gitattributes
CHANGED
|
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
dist/datainventor-0.2.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
dist/datainventor-0.2.1-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
dist/datainventor-0.2.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
dist/datainventor-0.2.1-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
dist/datainventor-0.2.2-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -41,7 +41,7 @@ You register Hugging Face datasets as **sources** to curate, and optionally desc
|
|
| 41 |
The hosted run works on about 200 documents per source and returns the kept and created documents with a report. The runner does the same on every document:
|
| 42 |
|
| 43 |
```bash
|
| 44 |
-
pip install 'datainventor[full] @ https://huggingface.co/spaces/codelion/data-inventor/resolve/main/dist/datainventor-0.2.
|
| 45 |
export DI_API_KEY=... # key for the endpoint named in the recipe
|
| 46 |
datainventor init recipe.yaml # or download one from the app
|
| 47 |
datainventor preview recipe.yaml # every step on a sample
|
|
|
|
| 41 |
The hosted run works on about 200 documents per source and returns the kept and created documents with a report. The runner does the same on every document:
|
| 42 |
|
| 43 |
```bash
|
| 44 |
+
pip install 'datainventor[full] @ https://huggingface.co/spaces/codelion/data-inventor/resolve/main/dist/datainventor-0.2.2-py3-none-any.whl'
|
| 45 |
export DI_API_KEY=... # key for the endpoint named in the recipe
|
| 46 |
datainventor init recipe.yaml # or download one from the app
|
| 47 |
datainventor preview recipe.yaml # every step on a sample
|
app/curate_api.py
CHANGED
|
@@ -23,7 +23,7 @@ PER_SOURCE = int(os.environ.get("CURATE_PER_SOURCE", "200"))
|
|
| 23 |
GRADE_N = int(os.environ.get("CURATE_GRADE", "48"))
|
| 24 |
REPHRASE_N = int(os.environ.get("CURATE_REPHRASE", "8"))
|
| 25 |
MAX_SOURCES, MAX_TARGETS = 4, 2
|
| 26 |
-
INSTALL = "pip install 'datainventor[full] @ https://huggingface.co/spaces/codelion/data-inventor/resolve/main/dist/datainventor-0.2.
|
| 27 |
PREVIEWS: dict[str, Preview] = {}
|
| 28 |
TRIES: dict[str, list] = {}
|
| 29 |
GUARD = threading.Lock()
|
|
|
|
| 23 |
GRADE_N = int(os.environ.get("CURATE_GRADE", "48"))
|
| 24 |
REPHRASE_N = int(os.environ.get("CURATE_REPHRASE", "8"))
|
| 25 |
MAX_SOURCES, MAX_TARGETS = 4, 2
|
| 26 |
+
INSTALL = "pip install 'datainventor[full] @ https://huggingface.co/spaces/codelion/data-inventor/resolve/main/dist/datainventor-0.2.2-py3-none-any.whl'"
|
| 27 |
PREVIEWS: dict[str, Preview] = {}
|
| 28 |
TRIES: dict[str, list] = {}
|
| 29 |
GUARD = threading.Lock()
|
app/engine.py
CHANGED
|
@@ -89,6 +89,10 @@ No commentary, no code fences."""
|
|
| 89 |
LEAK = re.compile(r"the user (asks|wants)|write one|return only|^okay|^sure,|^here is|^here's|^as an ai", re.I)
|
| 90 |
END = re.compile(r"[.?!)\"”’'।:。!?]$")
|
| 91 |
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
LENGTHS = ["one blunt sentence", "two sentences with one specific detail",
|
| 93 |
"three or four sentences with concrete specifics", "a short paragraph that sounds a little rushed"]
|
| 94 |
|
|
@@ -156,10 +160,12 @@ def run_job(job: Job, ep: Endpoint):
|
|
| 156 |
|
| 157 |
def ok_q(t):
|
| 158 |
t = t.strip().strip('"')
|
| 159 |
-
return 15 <= len(t) <= 1500 and END.search(t) and not degenerate(t) and not LEAK.search(t[:150])
|
|
|
|
| 160 |
|
| 161 |
def ok_a(t):
|
| 162 |
-
return len(t.split()) >= 12 and not degenerate(t[:600]) and not LEAK.search(t[:100])
|
|
|
|
| 163 |
|
| 164 |
kept_grams: list[set] = []
|
| 165 |
job.state, job.message = "generating", "Writing examples"
|
|
@@ -178,6 +184,7 @@ def run_job(job: Job, ep: Endpoint):
|
|
| 178 |
Write ONE realistic message that {persona} would send to {role}, for this dataset, about: {leaf} ({area}, {sub}).
|
| 179 |
Shape: {fmt}. Difficulty: {diff}. Length: {length}. Language: {lang}.
|
| 180 |
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.
|
|
|
|
| 181 |
Return only the message.""", max_tokens=700, temperature=1.0, validate=ok_q).strip().strip('"')
|
| 182 |
g = grams(q)
|
| 183 |
with job.lock:
|
|
@@ -186,6 +193,9 @@ Return only the message.""", max_tokens=700, temperature=1.0, validate=ok_q).str
|
|
| 186 |
return
|
| 187 |
answer_prompt = f"""You are {role}. Reply to the message below. The reply is a training example for this dataset: "{request}".
|
| 188 |
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.
|
|
|
|
|
|
|
|
|
|
| 189 |
{('Requirements: ' + constraints.strip()) if constraints.strip() else ''}
|
| 190 |
|
| 191 |
Message: {q}"""
|
|
@@ -195,18 +205,25 @@ Message: {q}"""
|
|
| 195 |
if kind == "preference":
|
| 196 |
a2 = ep.chat(answer_prompt + "\n\nWrite a different answer from the obvious one, in a different structure and with different emphasis.",
|
| 197 |
max_tokens=2500, temperature=1.0, validate=ok_a)
|
| 198 |
-
|
|
|
|
| 199 |
Message: {q}
|
| 200 |
|
| 201 |
Answer A:
|
| 202 |
-
{
|
| 203 |
|
| 204 |
Answer B:
|
| 205 |
-
{
|
| 206 |
-
|
| 207 |
-
Reply with only the single letter A or B.""", max_tokens=
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
else:
|
| 211 |
row["completion"] = a1
|
| 212 |
with job.lock:
|
|
|
|
| 89 |
LEAK = re.compile(r"the user (asks|wants)|write one|return only|^okay|^sure,|^here is|^here's|^as an ai", re.I)
|
| 90 |
END = re.compile(r"[.?!)\"”’'।:。!?]$")
|
| 91 |
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)
|
| 92 |
+
PLACEHOLDER = re.compile(r"\[[A-Za-z'’ ]{0,30}\b(name|number|email|address|phone|date|id)\]", re.I)
|
| 93 |
+
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)
|
| 94 |
+
TABLE = re.compile(r"^\s*\|.*\|\s*$\n^\s*\|?\s*:?-{3,}", re.M)
|
| 95 |
+
VERDICT = re.compile(r"\W*\(?([AB])\b", re.I)
|
| 96 |
LENGTHS = ["one blunt sentence", "two sentences with one specific detail",
|
| 97 |
"three or four sentences with concrete specifics", "a short paragraph that sounds a little rushed"]
|
| 98 |
|
|
|
|
| 160 |
|
| 161 |
def ok_q(t):
|
| 162 |
t = t.strip().strip('"')
|
| 163 |
+
return (15 <= len(t) <= 1500 and END.search(t) and not degenerate(t) and not LEAK.search(t[:150])
|
| 164 |
+
and not MULTI.search(t[:120]) and not PLACEHOLDER.search(t) and not CALLBACK.search(t))
|
| 165 |
|
| 166 |
def ok_a(t):
|
| 167 |
+
return (len(t.split()) >= 12 and not degenerate(t[:600]) and not LEAK.search(t[:100])
|
| 168 |
+
and not TABLE.search(t) and not PLACEHOLDER.search(t))
|
| 169 |
|
| 170 |
kept_grams: list[set] = []
|
| 171 |
job.state, job.message = "generating", "Writing examples"
|
|
|
|
| 184 |
Write ONE realistic message that {persona} would send to {role}, for this dataset, about: {leaf} ({area}, {sub}).
|
| 185 |
Shape: {fmt}. Difficulty: {diff}. Length: {length}. Language: {lang}.
|
| 186 |
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.
|
| 187 |
+
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].
|
| 188 |
Return only the message.""", max_tokens=700, temperature=1.0, validate=ok_q).strip().strip('"')
|
| 189 |
g = grams(q)
|
| 190 |
with job.lock:
|
|
|
|
| 193 |
return
|
| 194 |
answer_prompt = f"""You are {role}. Reply to the message below. The reply is a training example for this dataset: "{request}".
|
| 195 |
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.
|
| 196 |
+
Write plain prose: short paragraphs, or a simple numbered list for steps. No tables, headings or emoji.
|
| 197 |
+
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].
|
| 198 |
+
For physical techniques and safety, give only the standard, widely taught practice, and make sure every step agrees with the others.
|
| 199 |
{('Requirements: ' + constraints.strip()) if constraints.strip() else ''}
|
| 200 |
|
| 201 |
Message: {q}"""
|
|
|
|
| 205 |
if kind == "preference":
|
| 206 |
a2 = ep.chat(answer_prompt + "\n\nWrite a different answer from the obvious one, in a different structure and with different emphasis.",
|
| 207 |
max_tokens=2500, temperature=1.0, validate=ok_a)
|
| 208 |
+
def judge(first: str, second: str) -> str:
|
| 209 |
+
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.
|
| 210 |
Message: {q}
|
| 211 |
|
| 212 |
Answer A:
|
| 213 |
+
{first[:3000]}
|
| 214 |
|
| 215 |
Answer B:
|
| 216 |
+
{second[:3000]}
|
| 217 |
+
|
| 218 |
+
Reply with only the single letter A or B.""", max_tokens=16, temperature=0, allow_cut=True,
|
| 219 |
+
validate=lambda s: bool(VERDICT.match(s)))
|
| 220 |
+
return VERDICT.match(v).group(1).upper()
|
| 221 |
+
# judge both orders so the first-shown answer gets no edge; keep the row only if they agree
|
| 222 |
+
v1, v2 = judge(a1, a2), judge(a2, a1)
|
| 223 |
+
if (v1 == "A") != (v2 == "B"):
|
| 224 |
+
job.reject("judge disagreed")
|
| 225 |
+
return
|
| 226 |
+
row["chosen"], row["rejected"] = (a1, a2) if v1 == "A" else (a2, a1)
|
| 227 |
else:
|
| 228 |
row["completion"] = a1
|
| 229 |
with job.lock:
|
app/main.py
CHANGED
|
@@ -54,7 +54,8 @@ def make_endpoint() -> Endpoint:
|
|
| 54 |
if not key:
|
| 55 |
raise HTTPException(503, "This host has no model configured. Set DI_API_KEY (or HF_TOKEN) and restart.")
|
| 56 |
return Endpoint(base_url=os.environ.get("DI_BASE_URL", HF_BASE).rstrip("/"), api_key=key, model=os.environ.get("DI_MODEL", HF_MODEL),
|
| 57 |
-
disable_thinking=os.environ.get("DI_DISABLE_THINKING", "0") != "0", workers=max(1, min(int(os.environ.get("DI_WORKERS", "4")), 8))
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
@app.get("/api/defaults")
|
|
|
|
| 54 |
if not key:
|
| 55 |
raise HTTPException(503, "This host has no model configured. Set DI_API_KEY (or HF_TOKEN) and restart.")
|
| 56 |
return Endpoint(base_url=os.environ.get("DI_BASE_URL", HF_BASE).rstrip("/"), api_key=key, model=os.environ.get("DI_MODEL", HF_MODEL),
|
| 57 |
+
disable_thinking=os.environ.get("DI_DISABLE_THINKING", "0") != "0", workers=max(1, min(int(os.environ.get("DI_WORKERS", "4")), 8)),
|
| 58 |
+
min_max_tokens=int(os.environ.get("DI_MIN_MAX_TOKENS", "0")))
|
| 59 |
|
| 60 |
|
| 61 |
@app.get("/api/defaults")
|
app/publish.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
| 3 |
|
| 4 |
import io
|
| 5 |
import json
|
|
|
|
| 6 |
import re
|
| 7 |
|
| 8 |
from huggingface_hub import HfApi
|
|
@@ -72,7 +73,7 @@ This dataset was generated with [DataInventor]({SPACE_URL}), an open-source tool
|
|
| 72 |
3. For each combination the model wrote one message, then the reply{" (two replies, then a ranking)" if pref else ""}.
|
| 73 |
4. Rows were rejected for cut-off or degenerate text, for asking for many items at once, and for near-duplicate questions.
|
| 74 |
|
| 75 |
-
Model: `{model}`, served through Hugging Face Inference Providers.
|
| 76 |
|
| 77 |
## Quick numbers
|
| 78 |
|
|
|
|
| 3 |
|
| 4 |
import io
|
| 5 |
import json
|
| 6 |
+
import os
|
| 7 |
import re
|
| 8 |
|
| 9 |
from huggingface_hub import HfApi
|
|
|
|
| 73 |
3. For each combination the model wrote one message, then the reply{" (two replies, then a ranking)" if pref else ""}.
|
| 74 |
4. Rows were rejected for cut-off or degenerate text, for asking for many items at once, and for near-duplicate questions.
|
| 75 |
|
| 76 |
+
Model: `{model}`{", served through Hugging Face Inference Providers" if "router.huggingface.co" in os.environ.get("DI_BASE_URL", "router.huggingface.co") else ", served through an OpenAI-compatible router that routes each request to a hosted model (mostly gpt-oss-120b and Gemini Flash)"}.
|
| 77 |
|
| 78 |
## Quick numbers
|
| 79 |
|
datainventor/llm.py
CHANGED
|
@@ -25,11 +25,16 @@ class Endpoint:
|
|
| 25 |
model: str = HF_MODEL
|
| 26 |
disable_thinking: bool = False
|
| 27 |
workers: int = 4
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def chat(self, prompt: str, max_tokens: int = 1500, temperature: float = 0.8, retries: int = 5,
|
| 30 |
-
validate: Optional[Callable[[str], bool]] = None) -> str:
|
| 31 |
reasoning = bool(_REASONING.search(self.model)) and not self.disable_thinking
|
| 32 |
-
body = {"model": self.model,
|
|
|
|
| 33 |
"temperature": temperature, "messages": [{"role": "user", "content": prompt}]}
|
| 34 |
if reasoning and "gpt-oss" in self.model.lower():
|
| 35 |
body["reasoning_effort"] = "low"
|
|
@@ -44,7 +49,8 @@ class Endpoint:
|
|
| 44 |
"Authorization": "Bearer " + self.api_key})
|
| 45 |
resp = json.load(urllib.request.urlopen(req, timeout=180))
|
| 46 |
choice = (resp.get("choices") or [{}])[0] or {}
|
| 47 |
-
|
|
|
|
| 48 |
last = "reply cut off at max_tokens"
|
| 49 |
time.sleep(1.5)
|
| 50 |
continue
|
|
|
|
| 25 |
model: str = HF_MODEL
|
| 26 |
disable_thinking: bool = False
|
| 27 |
workers: int = 4
|
| 28 |
+
# Floor on max_tokens for every call. Routers that send small requests to slow
|
| 29 |
+
# local boxes (the optillm proxy keeps <=2000-token calls local) can be steered
|
| 30 |
+
# to their fast hosted tiers by raising the floor, e.g. 2048.
|
| 31 |
+
min_max_tokens: int = 0
|
| 32 |
|
| 33 |
def chat(self, prompt: str, max_tokens: int = 1500, temperature: float = 0.8, retries: int = 5,
|
| 34 |
+
validate: Optional[Callable[[str], bool]] = None, allow_cut: bool = False) -> str:
|
| 35 |
reasoning = bool(_REASONING.search(self.model)) and not self.disable_thinking
|
| 36 |
+
body = {"model": self.model,
|
| 37 |
+
"max_tokens": max(self.min_max_tokens, max_tokens + (REASONING_HEADROOM if reasoning else 0)),
|
| 38 |
"temperature": temperature, "messages": [{"role": "user", "content": prompt}]}
|
| 39 |
if reasoning and "gpt-oss" in self.model.lower():
|
| 40 |
body["reasoning_effort"] = "low"
|
|
|
|
| 49 |
"Authorization": "Bearer " + self.api_key})
|
| 50 |
resp = json.load(urllib.request.urlopen(req, timeout=180))
|
| 51 |
choice = (resp.get("choices") or [{}])[0] or {}
|
| 52 |
+
# allow_cut: short answers (a verdict letter) can be read even if the model kept talking
|
| 53 |
+
if choice.get("finish_reason") == "length" and not (allow_cut and ((choice.get("message") or {}).get("content") or "").strip()):
|
| 54 |
last = "reply cut off at max_tokens"
|
| 55 |
time.sleep(1.5)
|
| 56 |
continue
|
dist/datainventor-0.2.2-py3-none-any.whl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1263ef18bb7c8099a1264f0103213dd981d1e3c5075edae9736b4c611f7e5b14
|
| 3 |
+
size 2414181
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "datainventor"
|
| 7 |
-
version = "0.2.
|
| 8 |
description = "Curate a pretraining corpus on your own machines: clean, calibrate, create, compose."
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "datainventor"
|
| 7 |
+
version = "0.2.2"
|
| 8 |
description = "Curate a pretraining corpus on your own machines: clean, calibrate, create, compose."
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|