codelion commited on
Commit
bf2993f
·
verified ·
1 Parent(s): 7e4e57b

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 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.1-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
 
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.1-py3-none-any.whl'"
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]) and not MULTI.search(t[:120])
 
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
- verdict = ep.chat(f"""Which answer is better for the message? Judge accuracy, completeness and clarity.
 
199
  Message: {q}
200
 
201
  Answer A:
202
- {a1[:3000]}
203
 
204
  Answer B:
205
- {a2[:3000]}
206
-
207
- Reply with only the single letter A or B.""", max_tokens=8, temperature=0, validate=lambda s: s.strip()[:1].upper() in ("A", "B"))
208
- a_wins = verdict.strip()[:1].upper() == "A"
209
- row["chosen"], row["rejected"] = (a1, a2) if a_wins else (a2, a1)
 
 
 
 
 
 
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, "max_tokens": max_tokens + (REASONING_HEADROOM if reasoning else 0),
 
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
- if choice.get("finish_reason") == "length":
 
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.1"
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"