3 browser-native chat surfaces (atlas-chat / atlas-hud / atlas-command, no build step) + 11 Python automation scripts (cluster, tray, finance, reminders, autonomy loops) + 2 patterns documented (cluster-then-polish, anti-impulse R1-R10) MIT licensed, anonymized from production stack.
214 lines
7.7 KiB
Python
214 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
cluster_then_polish.py — two-pass LLM pattern wrapper.
|
|
|
|
Wraps cluster.ask() with explicit "this is a draft, mark for human/Claude polish"
|
|
metadata. Goal: cluster does the heavy lifting (parallel, cheap, always-on),
|
|
Claude or human review handles the final polish.
|
|
|
|
Why: open-source 7B models can produce structured / classified / extracted output
|
|
reliably, but produce generic narrative text without grounding. Letting them
|
|
"finish" content sent to clients = quality loss. This wrapper bakes in the
|
|
two-pass expectation: cluster draft → human/Claude approval before any external use.
|
|
|
|
Public API:
|
|
draft(prompt, task='general', node_pref=None, **kw) -> {ok, text, model, node, polish_required: True, draft_id, hint}
|
|
save_draft(draft, path) -> writes md with frontmatter
|
|
mark_polished(draft_id) -> updates draft registry
|
|
|
|
Usage example:
|
|
from cluster_then_polish import draft, save_draft
|
|
d = draft("Schrijf cold-WhatsApp voor lunchroom in Tilburg", task='chat', node_pref='pc')
|
|
save_draft(d, '~/.config/atlas/drafts/lunchroom_outreach.md')
|
|
# User/Claude reviews + edits before sending
|
|
|
|
Registry: drafts logged to ~/.config/atlas/drafts/_registry.json with
|
|
status (draft/polished/sent), creation time, node/model used, prompt hash.
|
|
"""
|
|
from __future__ import annotations
|
|
import sys, json, hashlib, time
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
HOME = Path.home()
|
|
ATLAS = HOME / '.config' / 'atlas'
|
|
DRAFTS_DIR = ATLAS / 'drafts'
|
|
REGISTRY = DRAFTS_DIR / '_registry.json'
|
|
|
|
sys.path.insert(0, str(ATLAS))
|
|
try:
|
|
import cluster
|
|
except ImportError:
|
|
raise RuntimeError("cluster.py not importable; check ~/.config/atlas/cluster.py")
|
|
|
|
|
|
# Task types that should ALWAYS go through polish (narrative, klant-facing)
|
|
POLISH_REQUIRED_TASKS = {'chat', 'reason', 'general'}
|
|
|
|
# Task types where cluster output is directly usable (structured, classified)
|
|
POLISH_OPTIONAL_TASKS = {'fast', 'embed', 'code', 'vision'}
|
|
|
|
|
|
def _hash_prompt(prompt: str) -> str:
|
|
return hashlib.sha1(prompt.encode('utf-8')).hexdigest()[:10]
|
|
|
|
|
|
def _load_registry() -> dict:
|
|
DRAFTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
if REGISTRY.exists():
|
|
try:
|
|
return json.loads(REGISTRY.read_text(encoding='utf-8'))
|
|
except Exception:
|
|
pass
|
|
return {"drafts": {}}
|
|
|
|
|
|
def _save_registry(reg: dict) -> None:
|
|
REGISTRY.write_text(json.dumps(reg, indent=2), encoding='utf-8')
|
|
|
|
|
|
def draft(prompt: str, task: str = 'general', node_pref: str | None = None,
|
|
max_tokens: int = 800, temperature: float = 0.5, timeout: int = 180,
|
|
purpose: str = '') -> dict:
|
|
"""
|
|
Call cluster.ask() but tag the result as draft requiring polish.
|
|
|
|
:param purpose: short label (e.g. 'cold_outreach', 'morning_brief', 'mail_triage')
|
|
used for the draft filename and routing decisions
|
|
"""
|
|
t0 = time.time()
|
|
r = cluster.ask(prompt, task=task, node_pref=node_pref,
|
|
max_tokens=max_tokens, temperature=temperature, timeout=timeout)
|
|
elapsed = round(time.time() - t0, 1)
|
|
|
|
if not r.get('ok'):
|
|
return {
|
|
"ok": False,
|
|
"err": r.get('err', 'cluster error'),
|
|
"polish_required": True,
|
|
"hint": "cluster call failed; do not use any partial output"
|
|
}
|
|
|
|
draft_id = f"{int(time.time())}-{_hash_prompt(prompt)}"
|
|
polish_required = task in POLISH_REQUIRED_TASKS
|
|
|
|
result = {
|
|
"ok": True,
|
|
"draft_id": draft_id,
|
|
"text": r['text'],
|
|
"model": r.get('model', '?'),
|
|
"node": r.get('node', '?'),
|
|
"elapsed_s": elapsed,
|
|
"task": task,
|
|
"purpose": purpose,
|
|
"polish_required": polish_required,
|
|
"polish_status": "pending" if polish_required else "not_required",
|
|
"created_at": datetime.now().isoformat(timespec='seconds'),
|
|
"hint": (
|
|
"POLISH REQUIRED before external use. Cluster 7B models produce "
|
|
"generic narrative; review for: specificity, sector-fit, tone, "
|
|
"factual claims. Then call mark_polished(draft_id)."
|
|
if polish_required else
|
|
"Structured output, safe to use as-is for triage/classify. "
|
|
"Manual spot-check still recommended."
|
|
)
|
|
}
|
|
|
|
# Register
|
|
reg = _load_registry()
|
|
reg['drafts'][draft_id] = {
|
|
"purpose": purpose,
|
|
"task": task,
|
|
"node": result['node'],
|
|
"model": result['model'],
|
|
"created_at": result['created_at'],
|
|
"polish_status": result['polish_status'],
|
|
"prompt_hash": _hash_prompt(prompt),
|
|
"char_count": len(r['text']),
|
|
}
|
|
_save_registry(reg)
|
|
|
|
return result
|
|
|
|
|
|
def save_draft(draft_result: dict, path: str | Path) -> Path:
|
|
"""Save draft to .md with frontmatter so reviewer knows status."""
|
|
p = Path(path).expanduser()
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
fm = (
|
|
f"---\n"
|
|
f"draft_id: {draft_result.get('draft_id', '?')}\n"
|
|
f"polish_status: {draft_result.get('polish_status', '?')}\n"
|
|
f"node: {draft_result.get('node', '?')}\n"
|
|
f"model: {draft_result.get('model', '?')}\n"
|
|
f"task: {draft_result.get('task', '?')}\n"
|
|
f"purpose: {draft_result.get('purpose', '?')}\n"
|
|
f"created_at: {draft_result.get('created_at', '?')}\n"
|
|
f"polish_required: {draft_result.get('polish_required', False)}\n"
|
|
f"---\n\n"
|
|
f"> **HINT**: {draft_result.get('hint', '')}\n\n"
|
|
)
|
|
p.write_text(fm + draft_result.get('text', ''), encoding='utf-8')
|
|
return p
|
|
|
|
|
|
def mark_polished(draft_id: str, polished_by: str = 'human') -> bool:
|
|
"""Update registry to indicate draft has been reviewed/polished."""
|
|
reg = _load_registry()
|
|
if draft_id in reg.get('drafts', {}):
|
|
reg['drafts'][draft_id]['polish_status'] = 'polished'
|
|
reg['drafts'][draft_id]['polished_by'] = polished_by
|
|
reg['drafts'][draft_id]['polished_at'] = datetime.now().isoformat(timespec='seconds')
|
|
_save_registry(reg)
|
|
return True
|
|
return False
|
|
|
|
|
|
def list_pending(purpose_filter: str = '') -> list:
|
|
"""Return list of drafts still awaiting polish (polish_status='pending')."""
|
|
reg = _load_registry()
|
|
out = []
|
|
for did, info in reg.get('drafts', {}).items():
|
|
if info.get('polish_status') != 'pending':
|
|
continue
|
|
if purpose_filter and purpose_filter not in info.get('purpose', ''):
|
|
continue
|
|
out.append({"draft_id": did, **info})
|
|
return sorted(out, key=lambda x: x.get('created_at', ''), reverse=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import argparse
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('prompt', nargs='?', help='Prompt text')
|
|
ap.add_argument('-task', default='general')
|
|
ap.add_argument('-node', default=None)
|
|
ap.add_argument('-purpose', default='ad_hoc')
|
|
ap.add_argument('-out', default=None, help='Save draft to this path')
|
|
ap.add_argument('--list-pending', action='store_true')
|
|
args = ap.parse_args()
|
|
|
|
if args.list_pending:
|
|
for d in list_pending():
|
|
print(f"{d['draft_id']} [{d['purpose']}] {d['model']}@{d['node']} {d['char_count']}ch {d['created_at']}")
|
|
sys.exit(0)
|
|
|
|
if not args.prompt:
|
|
print("usage: cluster_then_polish.py 'prompt' [-task T] [-node N] [-purpose P] [-out FILE]")
|
|
sys.exit(1)
|
|
|
|
d = draft(args.prompt, task=args.task, node_pref=args.node, purpose=args.purpose)
|
|
if not d['ok']:
|
|
print(f"ERROR: {d.get('err')}")
|
|
sys.exit(1)
|
|
|
|
print(f"[{d['node']}/{d['model']} {d['elapsed_s']}s] draft_id={d['draft_id']}")
|
|
print(f"polish_status: {d['polish_status']}")
|
|
print(f"--- text ({len(d['text'])} chars) ---")
|
|
print(d['text'])
|
|
print()
|
|
|
|
if args.out:
|
|
p = save_draft(d, args.out)
|
|
print(f"saved -> {p}")
|