File size: 9,265 Bytes
4f740eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Create: rephrase good source documents into new training documents, or write new ones where sources are thin.

Rephrase styles are the five from Sutra (weights 30/25/20/15/10, after BeyondWeb), plus math and code styles.
A fidelity check flags rephrasings that introduce numbers or names the source does not contain, which matters when the
training data must respect a knowledge cutoff.
"""
from __future__ import annotations

import random
import re

from .llm import Endpoint, EndpointError

NO_PREAMBLE = ("Output ONLY the rewritten document. No preamble, no meta-commentary, no phrases like \"Here is\", "
               "\"Let me\", \"Sure\" or \"Okay\". Do not add facts, numbers, names or dates that are not in the original.")

STYLES = {
    "conversational": ("Teacher-student dialogue", 30, """Rewrite the content as a natural conversation between a teacher and a student.
The student asks curious, natural questions; the teacher explains clearly with concrete examples taken from the content.
Keep all key information and the same technical accuracy. Keep a similar length (within 20%).
Use the labels **Teacher**: and **Student**: and start directly with "**Teacher**:"."""),
    "tutorial": ("Step-by-step tutorial", 25, """Rewrite the content as a clear step-by-step tutorial.
Start with "## Tutorial: <topic>", then an overview, prerequisites, numbered steps with what to do and how to check it,
a short practice exercise and key takeaways. Keep all key information accurate. Keep a similar length (within 20%)."""),
    "qa": ("Questions and answers", 20, """Rewrite the content as 4 to 7 questions and thorough answers that together cover every key point,
moving from basic (what) to deeper (why, how) to applied. Start with "## Understanding <topic>: Questions and Answers".
Use **Q:** and **A:** labels. Keep a similar total length (within 20%)."""),
    "dense_reference": ("Dense reference notes", 15, """Rewrite the content as compact reference notes at 70 to 85 percent of the original length.
Remove filler and repetition. Use headings and bullet points: core definition, key properties, relationships,
essential examples, formulas or procedures if any, pitfalls."""),
    "pedagogical": ("Textbook section", 10, """Rewrite the content as a well-structured textbook section: learning objectives, the connection to what
the reader already knows, the core explanation, one worked example with step-by-step reasoning, a common question
with its answer, and a quick self-check question. Keep a similar length."""),
    "worked_examples": ("Worked problems (math)", 0, """Rewrite the content as 2 to 4 worked problems that use exactly the ideas in it. For each: state the problem,
solve it step by step with the reasoning for each step, and check the answer. Use LaTeX for mathematics."""),
    "code_walkthrough": ("Code walkthrough (code)", 0, """Rewrite the content as an explained code walkthrough: what the code does, then the code in a fenced block
(keep it correct and runnable; you may tidy names and add comments), then an explanation of the key lines and one
example of using it."""),
}

KIND_STYLES = {"math": ["worked_examples", "pedagogical", "qa", "tutorial"],
               "code": ["code_walkthrough", "tutorial", "qa", "dense_reference"]}


def styles_for(kind: str) -> list[str]:
    return KIND_STYLES.get(kind, ["conversational", "tutorial", "qa", "dense_reference", "pedagogical"])


def pick_style(kind: str, rng: random.Random, allowed: list[str] | None = None) -> str:
    opts = [s for s in (allowed or styles_for(kind)) if s in STYLES]
    weights = [max(STYLES[s][1], 10) for s in opts]
    return rng.choices(opts, weights=weights, k=1)[0]


def chunks(text: str, max_chars: int = 6000) -> list[str]:
    """Split a long document at paragraph breaks so each rewrite sees a piece it can keep at a similar length."""
    if len(text) <= max_chars:
        return [text]
    out, cur = [], ""
    for para in re.split(r"\n\s*\n", text):
        if cur and len(cur) + len(para) + 2 > max_chars:
            out.append(cur)
            cur = ""
        cur = (cur + "\n\n" + para) if cur else para
        while len(cur) > max_chars:  # one giant paragraph
            out.append(cur[:max_chars])
            cur = cur[max_chars:]
    if cur.strip():
        out.append(cur)
    return [c for c in out if len(c.split()) >= 80] or [text[:max_chars]]


def rephrase_prompt(text: str, style: str, language: str = "") -> str:
    lang = f"\nWrite it in {language}." if language and language.lower() != "same" else ""
    return f"""{STYLES[style][2]}{lang}
{NO_PREAMBLE}

Original content:
<<<
{text[:8000]}
>>>"""


LEAD = re.compile(r"^(here('s| is)|sure|okay|certainly|let me|i('ll| will))\b", re.I)
NUM = re.compile(r"\b\d[\d,.]*\b")
# a capitalized word that follows a lowercase word mid-sentence: names, places, products. Words at the start of a
# sentence, a line or a markdown label are skipped, so "Teacher:" or "Great question" never count.
HEADINGS = re.compile(r"^#+ .*$|\*\*[^*\n]{1,80}\*\*", re.M)
NAME = re.compile(r"\b[a-z][a-z'-]*[,;]? ([A-Z][a-zA-Z]{2,}(?: [A-Z][a-zA-Z]{2,})*)")


def fidelity(src: str, out: str, template: str = "") -> dict:
    """Share of numbers and mid-sentence names in the rewrite that never appear in the source. A rough proxy for
    invented facts; the threshold is a choice, not a law."""
    s_low = (src + " " + template).lower()
    body = HEADINGS.sub(" ", out)  # headings and bold labels the style asks for are not new facts
    nums = {n.rstrip(".,") for n in NUM.findall(out)}
    names = set(NAME.findall(body))
    new_nums = [n for n in nums if n and n not in src]
    new_names = [n for n in names if n.lower() not in s_low]
    total = len(nums) + len(names)
    share = (len(new_nums) + len(new_names)) / total if total else 0.0
    return {"new_numbers": sorted(new_nums)[:8], "new_names": sorted(new_names)[:8], "new_share": round(share, 3),
            "checked": total}


def rephrase(ep: Endpoint, text: str, style: str, language: str = "", max_new_share: float = 0.35) -> dict:
    """One rewrite. Returns {"text", "style", "fidelity", "ok", "why"}."""
    def ok(s):
        return len(s.split()) >= 60 and not LEAD.search(s.strip()[:40])
    try:
        out = ep.chat(rephrase_prompt(text, style, language), max_tokens=3000, temperature=0.7, retries=3, validate=ok)
    except EndpointError as e:
        return {"text": "", "style": style, "ok": False, "why": f"model call failed: {e}"[:200]}
    fid = fidelity(text, out, STYLES[style][2])
    new = len(fid["new_numbers"]) + len(fid["new_names"])
    # invented numbers are the clearest sign of made-up content; one stray number or name is not a pattern
    good = len(fid["new_numbers"]) < 3 and (new < 2 or fid["new_share"] <= max_new_share)
    return {"text": out, "style": style, "fidelity": fid, "ok": good, "why": "" if good else "adds facts the source does not contain"}


# ---------------------------------------------------------------- new documents where sources are thin

DOC_TYPES = {  # a subset of Sutra's content types that reads well as pretraining text
    "concept_introduction": "an introduction to the concept: definition, intuition, why it matters, how it works, two examples, a common misconception, and how it connects to related ideas",
    "reasoning_demonstration": "a worked demonstration of reasoning about a concrete problem in this area, thinking step by step and checking the result",
    "worked_examples": "two or three worked examples of increasing difficulty, each solved step by step",
    "common_misconceptions": "an explanation of the most common misconceptions about the concept, why people hold them, and the correct understanding",
    "problem_set": "a short problem set of four problems with full solutions",
    "case_study": "a realistic case study that applies the concept, with the decisions and their reasons",
    "technical_documentation": "clear technical documentation of the concept or tool, with a usage example",
    "historical_context": "the history of how the idea developed, who contributed what, and why it mattered",
}


def invent_prompt(topic: str, doc_type: str, level: int, request: str, language: str = "English") -> str:
    return f"""You are writing high-quality pretraining text for a language model. Collection: "{request}".
Write {DOC_TYPES[doc_type]}.
Concept: {topic}
Difficulty: {level} on a 1 (introductory) to 10 (expert) scale. Language: {language}.
Write 350 to 700 words of dense, accurate prose with markdown headings where useful. Every sentence should teach
something. Do not invent statistics, quotes or citations. {NO_PREAMBLE.split('. Do not add')[0]}."""


def invent(ep: Endpoint, topic: str, doc_type: str, level: int, request: str, language: str = "English") -> dict:
    def ok(s):
        return len(s.split()) >= 150 and not LEAD.search(s.strip()[:40])
    try:
        out = ep.chat(invent_prompt(topic, doc_type, level, request, language), max_tokens=2500, temperature=0.8, retries=3, validate=ok)
    except EndpointError as e:
        return {"text": "", "ok": False, "why": f"model call failed: {e}"[:200]}
    return {"text": out, "ok": True, "doc_type": doc_type, "topic": topic, "level": level}