Code
Reference implementation of the process page. Needs PANGRAM_API_KEY in the environment and an OpenAI key for the rewrite model.
evolve_lean.py
#!/usr/bin/env python3
"""Lean score-guided hill-climb. Usage: evolve_lean.py input.txt outdir [max_calls]
Changes vs evolve.py: rank once (re-rank only when a pass accepts nothing); delete fact-free sentences before
rewriting; 3 candidates per sentence (the shapes that won); stop at the Human label.
`calls` counts every distinct text scored in this run, including cache hits, so it equals a fresh run's cost."""
import json, os, re, sys, urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from score import score
src, outdir = sys.argv[1], sys.argv[2]; budget = int(sys.argv[3]) if len(sys.argv) > 3 else 60
TARGET = float(os.environ.get("TARGET", "0.35")); MODEL = os.environ.get("CAND_MODEL", "gpt-5.5")
os.makedirs(outdir, exist_ok=True)
text = open(src).read().strip(); paras = text.split("\n\n")
greet = paras.pop(0) if len(paras[0].split()) <= 3 else None
sents = [[pi, s] for pi, p in enumerate(paras) for s in re.split(r'(?<=[.!?])\s+', p.strip())]
seen = set()
def build(ss):
kept = {}
for pj, sj in ss:
if sj: kept.setdefault(pj, []).append(sj)
return (greet + "\n\n" if greet else "") + "\n\n".join(" ".join(kept[k]) for k in sorted(kept)) + "\n"
def sc(t):
seen.add(t); d = score(t); ws = d.get("windows", [])
return (max(w["ai_assistance_score"] for w in ws) if ws else 1.0), d.get("prediction_short")
def llm(system, user):
body = {"model": MODEL, "messages": [{"role": "system", "content": system}, {"role": "user", "content": user}]}
r = urllib.request.Request("https://api.openai.com/v1/chat/completions", data=json.dumps(body).encode(),
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", "Content-Type": "application/json"})
return json.load(urllib.request.urlopen(r, timeout=180))["choices"][0]["message"]["content"].strip()
CAND = open("CANDIDATE_PROMPT_LEAN.txt").read()
def candidates(full, sent):
out = llm(CAND, f"FULL TEXT:\n{full}\n\nSENTENCE TO REPLACE:\n{sent}")
return [l.strip() for l in out.splitlines() if l.strip()][:3]
done = lambda: len(seen) >= budget
cur, lab = sc(build(sents)); print(f"base {cur:.3f} {lab}")
# one LLM call: which sentences carry no concrete fact (safe to delete)?
listing = "\n".join(f"{i}: {s}" for i, (_, s) in enumerate(sents))
ans = llm("Reply with only the comma-separated indices of sentences that contain NO concrete fact (no number, named thing, part, timeframe or outcome): sentences that only report, attribute, generalise, evaluate or conclude. Reply 'none' if none.", listing)
deletable = {int(x) for x in re.findall(r'\d+', ans)} if ans.lower() != "none" else set()
print("fact-free (deletable):", sorted(deletable))
def rank():
imp = []
for i in range(len(sents)):
if done() or not sents[i][1]: continue
tmp = [list(x) for x in sents]; tmp[i][1] = ""
imp.append((sc(build(tmp))[0], i))
return sorted(imp)
ranking = rank()
# deletions first: cheapest possible edit, already scored by the ranking
for s, i in ranking:
if i in deletable and s < cur:
sents[i][1] = ""; cur, lab = sc(build(sents)); print(f"deleted s{i} -> {cur:.3f} {lab}")
if lab == "Human" and cur <= TARGET: break
ranking = [(s, i) for s, i in ranking if sents[i][1]]
rnd = 0; tried = {} # sentence index -> consecutive rounds with no accepted edit
while not (lab == "Human" and cur <= TARGET) and not done():
rnd += 1; start = cur
active = [(s, i) for s, i in ranking if sents[i][1] and tried.get(i, 0) < 2][:4]
if not active:
print("all ranked sentences exhausted; re-ranking"); ranking = rank(); tried = {}; continue
for s, i in active:
if done() or (lab == "Human" and cur <= TARGET): break
print(f"round {rnd} s{i} (del -> {s:.3f}): {sents[i][1][:70]}")
best = (cur, sents[i][1])
for c in candidates(build(sents), sents[i][1]):
if done(): break
tmp = [list(x) for x in sents]; tmp[i][1] = c
s2, l2 = sc(build(tmp)); print(f" {s2:.3f} {l2:<6} | {c[:90]}")
if s2 < best[0]: best = (s2, c)
if best[1] != sents[i][1]:
sents[i][1] = best[1]; cur, lab = sc(build(sents)); tried[i] = 0; print(f" -> accepted, now {cur:.3f} {lab}")
else:
tried[i] = tried.get(i, 0) + 1
if start - cur < 0.05 and not done():
# a flat round means the ranking is stale: re-rank (n calls) rather than keep paying 12 per round for nothing
print(f"round {rnd} gained only {start-cur:.3f}; re-ranking"); ranking = rank(); tried = {}
final = build(sents); open(f"{outdir}/FINAL.txt", "w").write(final)
print(f"\nFINAL {cur:.3f} {lab} distinct texts scored (= fresh-run calls): {len(seen)}\n{final}")
score.py
#!/usr/bin/env python3
"""Score text files against Pangram 4. Usage: score.py file1.txt [file2.txt ...]
Caches by sha256 of text in cache/ so re-runs are free."""
import hashlib, json, os, sys, time, urllib.request
KEY = os.environ["PANGRAM_API_KEY"]
BASE = "https://text.external-api.pangram.com"
os.makedirs("cache", exist_ok=True)
def req(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(BASE + path, data=data, method=method,
headers={"x-api-key": KEY, "Content-Type": "application/json"})
try:
with urllib.request.urlopen(r, timeout=60) as resp:
return json.load(resp)
except urllib.error.HTTPError as e:
sys.exit(f"HTTP {e.code} on {path}: {e.read().decode()[:300]}")
def score(text):
h = hashlib.sha256(text.encode()).hexdigest()[:16]
cp = f"cache/{h}.json"
if os.path.exists(cp):
return json.load(open(cp))
tid = req("POST", "/task", {"text": text, "model": "pangram-4"})["task_id"]
for _ in range(60):
time.sleep(2)
d = req("GET", f"/task/{tid}")
if d.get("stage") in ("STAGE_SUCCESS", "STAGE_FAILED"):
break
json.dump(d, open(cp, "w"), indent=1)
return d
if __name__ == "__main__":
rows = []
for f in sys.argv[1:]:
d = score(open(f).read())
rows.append((f, d.get("prediction_short"), d.get("fraction_ai"), d.get("fraction_ai_assisted"), d.get("fraction_human")))
print(f"\n=== {f}: {d.get('prediction_short')} ai={d.get('fraction_ai')} assisted={d.get('fraction_ai_assisted')} human={d.get('fraction_human')}")
for w in d.get("windows", []):
print(f" {w['ai_assistance_score']:.3f} {w['label']:<14} {w['confidence']:<6} hum={w.get('humanizer_score',0):.2f} | {w['text'][:70]!r}")
with open("results.tsv", "a") as out:
for r in rows:
out.write("\t".join(str(x) for x in r) + "\n")
CANDIDATE_PROMPT_LEAN.txt
You rewrite ONE sentence from a text. Output exactly 3 alternative versions of that sentence, one per line, no numbering, no commentary. Every version must keep the same concrete facts (numbers, named parts, timeframes, outcomes) and still fit the surrounding text:
1. a short main clause with the facts attached in parentheses, e.g. "They fixed the faults (loose ridge pointing, a leaking vent flashing)."
2. a version under eight words, dropping any opinion but keeping the facts.
3. a version with no reporting, attribution or evaluating verb (no "found", "said", "told", "saw", "suggested", "mattered", "my view is") and no word like "overall", "key", "real", "important".
Do not add casual filler, slang, typos, or extra hedging. Do not change the other sentences.