Raiff1982's picture
Upgrade to REAL orchestrated Codette on ZeroGPU (transformers backend for the llama.cpp pipeline)
c8fbdf1 verified
Raw
History Blame Contribute Delete
10.5 kB
#!/usr/bin/env python3
"""Dream Cycle — nightly cocoon review (Phase 4: self-maintenance).
Reviews the day's reasoning cocoons, flags candidate contradictions,
distills lessons, and writes a dream report. DreamReweaver made
operational: instead of manual archaeology through cocoons/, Codette
reviews her own day on a schedule.
REPORT-ONLY BY DESIGN (standing boundary): this job READS cocoons and
WRITES reports to data/dream_reports/. It never mutates memory stores,
never writes cocoons, never injects anything into recall. Any write-back
of distilled lessons is a separate, human-reviewed step.
Contradiction detection is honest about its limits: it flags CANDIDATE
contradictions (same-topic pairs whose answers diverge by letter, number,
or negation) for review — it does not claim semantic certainty.
Usage:
python reasoning_forge/dream_cycle.py # review last 24h
python reasoning_forge/dream_cycle.py --days 7 # review the week
python reasoning_forge/dream_cycle.py --days 1 --verbose
Nightly schedule (Windows Task Scheduler):
schtasks /Create /SC DAILY /ST 03:30 /TN CodetteDreamCycle ^
/TR "<python> J:\\codette-clean\\reasoning_forge\\dream_cycle.py"
"""
from __future__ import annotations
import argparse
import json
import os
import re
import time
from collections import Counter
from datetime import datetime
from pathlib import Path
_REPO = Path(__file__).resolve().parent.parent
COCOON_DIR = _REPO / "cocoons"
REPORT_DIR = _REPO / "data" / "dream_reports"
_MCQ_RE = re.compile(r"correct answer is \(([ABCD])\)", re.IGNORECASE)
_NUM_RE = re.compile(r"-?\d+\.?\d*")
_NEG_RE = re.compile(r"\b(no|not|never|isn't|doesn't|cannot|can't|won't|false)\b",
re.IGNORECASE)
_STOP = set("the a an is are was were be been being to of in on at for with and or "
"but if then than so as by from this that these those it its i you he "
"she we they what which who how why when where do does did can could "
"would should will your my our their his her them us me".split())
def _content_words(text: str) -> set:
return {w for w in re.findall(r"[a-z']+", (text or "").lower())
if len(w) > 2 and w not in _STOP}
def _jaccard(a: set, b: set) -> float:
if not a or not b:
return 0.0
return len(a & b) / len(a | b)
def load_recent_cocoons(days: float) -> list[dict]:
"""Load cocoons written in the window that carry a query/response pair."""
cutoff = time.time() - days * 86400
out = []
for f in COCOON_DIR.glob("*.json"):
try:
if f.stat().st_mtime < cutoff:
continue
d = json.loads(f.read_text(encoding="utf-8"))
except Exception:
continue
w = d.get("wrapped") or {}
q, r = w.get("query"), w.get("response")
if not (isinstance(q, str) and isinstance(r, str) and q.strip() and r.strip()):
continue
v3 = d.get("v3") or {}
out.append({
"file": f.name,
"query": q.strip(),
"response": r.strip(),
"adapter": w.get("adapter") or v3.get("dominant_perspective") or "?",
"ts": w.get("timestamp") or d.get("timestamp"),
"coherence": v3.get("gamma_coherence"),
"confidence": v3.get("confidence"),
"hallucination_flag": bool(v3.get("is_hallucination_flagged")),
"sycophancy_flag": bool(v3.get("is_sycophancy_flagged")),
"known_contradictions": v3.get("contradicts_cocoon_ids") or [],
})
return out
def group_similar(cocoons: list[dict], threshold: float = 0.45) -> list[list[int]]:
"""Group cocoon indices by query similarity (greedy, lexical Jaccard)."""
words = [_content_words(c["query"]) for c in cocoons]
groups: list[list[int]] = []
assigned = [False] * len(cocoons)
for i in range(len(cocoons)):
if assigned[i]:
continue
group = [i]
assigned[i] = True
for j in range(i + 1, len(cocoons)):
if not assigned[j] and _jaccard(words[i], words[j]) >= threshold:
group.append(j)
assigned[j] = True
groups.append(group)
return groups
def find_contradiction_candidates(cocoons: list[dict],
groups: list[list[int]]) -> list[dict]:
"""Same-topic pairs whose answers diverge. CANDIDATES, not verdicts."""
candidates = []
for group in groups:
if len(group) < 2:
continue
for ai in range(len(group)):
for bi in range(ai + 1, len(group)):
a, b = cocoons[group[ai]], cocoons[group[bi]]
reasons = []
# MCQ letter divergence
ma, mb = _MCQ_RE.search(a["response"]), _MCQ_RE.search(b["response"])
if ma and mb and ma.group(1).upper() != mb.group(1).upper():
reasons.append(f"MCQ letters differ: {ma.group(1)} vs {mb.group(1)}")
# negation asymmetry on similar-length short answers
na = bool(_NEG_RE.search(a["response"][:200]))
nb = bool(_NEG_RE.search(b["response"][:200]))
if na != nb and _jaccard(_content_words(a["response"]),
_content_words(b["response"])) > 0.3:
reasons.append("negation asymmetry in similar responses")
# leading numeric answer divergence
fa, fb = _NUM_RE.findall(a["response"][:120]), _NUM_RE.findall(b["response"][:120])
if fa and fb and fa[0] != fb[0] and _jaccard(
_content_words(a["query"]), _content_words(b["query"])) > 0.6:
reasons.append(f"leading numbers differ: {fa[0]} vs {fb[0]}")
if reasons:
candidates.append({
"a": a["file"], "b": b["file"],
"query_a": a["query"][:140], "query_b": b["query"][:140],
"reasons": reasons,
})
return candidates
def distill(cocoons: list[dict], groups: list[list[int]],
contradictions: list[dict]) -> dict:
themes = Counter()
for c in cocoons:
themes.update(_content_words(c["query"]))
adapters = Counter(c["adapter"] for c in cocoons)
low_coherence = [c for c in cocoons
if isinstance(c["coherence"], (int, float)) and c["coherence"] < 0.5]
flagged = [c for c in cocoons if c["hallucination_flag"] or c["sycophancy_flag"]]
known = [c for c in cocoons if c["known_contradictions"]]
lessons = []
if contradictions:
lessons.append(f"{len(contradictions)} candidate contradiction(s) need review "
"— same-topic answers diverged.")
if low_coherence:
lessons.append(f"{len(low_coherence)} turn(s) ran at coherence < 0.5 — "
"inspect what those queries had in common.")
if flagged:
lessons.append(f"{len(flagged)} turn(s) carried hallucination/sycophancy flags.")
big = [g for g in groups if len(g) >= 3]
if big:
top = max(big, key=len)
rep = cocoons[top[0]]["query"][:100]
lessons.append(f"Most-revisited topic ({len(top)} turns): \"{rep}\" — "
"recurring themes are candidates for memory distillation.")
if not lessons:
lessons.append("Quiet day: no contradictions, flags, or low-coherence turns.")
return {
"top_themes": themes.most_common(12),
"adapter_usage": adapters.most_common(),
"low_coherence_count": len(low_coherence),
"flagged_count": len(flagged),
"already_marked_contradictions": len(known),
"lessons": lessons,
}
def write_report(cocoons, groups, contradictions, summary) -> Path:
REPORT_DIR.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M")
md = REPORT_DIR / f"dream_{stamp}.md"
js = REPORT_DIR / f"dream_{stamp}.json"
lines = [f"# Dream Report — {datetime.now().strftime('%Y-%m-%d %H:%M')}",
"",
f"Reviewed **{len(cocoons)}** cocoons in **{len(groups)}** topic groups.",
"",
"## Lessons"]
lines += [f"- {l}" for l in summary["lessons"]]
lines += ["", "## Candidate contradictions (need human review)"]
if contradictions:
for c in contradictions[:20]:
lines += [f"- `{c['a']}` vs `{c['b']}` — {'; '.join(c['reasons'])}",
f" - A: {c['query_a']}", f" - B: {c['query_b']}"]
else:
lines.append("- none found")
lines += ["", "## Day shape",
f"- Adapter usage: {dict(summary['adapter_usage'])}",
f"- Top themes: {', '.join(w for w, _ in summary['top_themes'])}",
f"- Low-coherence turns: {summary['low_coherence_count']}",
f"- Flagged turns: {summary['flagged_count']}",
"",
"_Report-only job: no memory was modified. Write-back of lessons is a "
"separate human-reviewed step._"]
md.write_text("\n".join(lines), encoding="utf-8")
js.write_text(json.dumps({
"generated": datetime.now().isoformat(),
"cocoons_reviewed": len(cocoons), "groups": len(groups),
"contradiction_candidates": contradictions, "summary": summary,
}, indent=2, ensure_ascii=False), encoding="utf-8")
return md
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--days", type=float, default=1.0)
ap.add_argument("--verbose", action="store_true")
args = ap.parse_args()
cocoons = load_recent_cocoons(args.days)
print(f"Dream cycle: {len(cocoons)} reasoning cocoons from the last {args.days} day(s)")
if not cocoons:
print("Nothing to review — no report written.")
return
groups = group_similar(cocoons)
contradictions = find_contradiction_candidates(cocoons, groups)
summary = distill(cocoons, groups, contradictions)
report = write_report(cocoons, groups, contradictions, summary)
for lesson in summary["lessons"]:
print(f" - {lesson}")
print(f"\nReport: {report}")
if __name__ == "__main__":
main()