File size: 7,951 Bytes
c27193b | 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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | """The reasoning pass β logic and Socratic questioning before the model answers.
This is the oldest idea in the house and the one that keeps being right: **aGLM is not a model.** It is
a layer that augments whatever model you have β Socratic questioning, truth tables, autoepistemic and
nonmonotonic revision, BDI. Public documentation: <https://rage.pythai.net>. The lineage:
`GATERAGE/aglm` (`logic.py`, `socratic.py`, `reasoning.py`, `epistemic.py`, `nonmonotonic.py`,
`bdi.py`) and its eighteen UIUX iterations, whose real contribution was the *shape*: the UI calls a
reasoning layer, never a model directly.
Two ways this module works, and it says which one ran:
1. **the real aGLM** β when `aglm` is importable (`pip install -e ~/aglm`, or `MINDXTRAIN_AGLM=/path`),
its `LogicTables` and `SocraticReasoning` do the work.
2. **the in-house fallback** β a small, zero-dependency version of the same two moves, so the pass is
never simply absent: extract the claims, ask the questions a sceptic would ask, and check the
claims for contradiction.
The pass never answers for the model. It writes scaffolding into the prompt β the questions the answer
should survive β and the model still has to answer. That distinction is why it improves a small model
instead of flattering it.
"""
from __future__ import annotations
import os
import re
import sys
from dataclasses import dataclass, field
from typing import Any
DOCS = "https://rage.pythai.net"
_SENT = re.compile(r"(?<=[.!?])\s+")
_HEDGE = re.compile(r"\b(always|never|every|all|none|must|cannot|impossible|certainly|proves?)\b", re.I)
_CAUSAL = re.compile(r"\b(because|therefore|so|thus|hence|since|implies|means that)\b", re.I)
_QUANT = re.compile(r"\b(\d+(?:\.\d+)?%?|\d+x|[a-z]+ per [a-z]+)\b", re.I)
# ββ the real layer, if it is installed ββββββββββββββββββββββββββββββββββββββββ
def _aglm():
"""Import aGLM if it is reachable. Never raises; returns None when it is not."""
extra = os.environ.get("MINDXTRAIN_AGLM")
if extra and extra not in sys.path:
sys.path.insert(0, extra)
try:
from logic import LogicTables # type: ignore
from socratic import SocraticReasoning # type: ignore
return {"LogicTables": LogicTables, "SocraticReasoning": SocraticReasoning}
except Exception:
return None
def available() -> dict[str, Any]:
a = _aglm()
return {"aglm": bool(a), "docs": DOCS,
"note": ("the real aGLM layer is in use" if a else
"aGLM is not importable here β the in-house fallback runs instead "
"(set MINDXTRAIN_AGLM=/path/to/aglm to use the original)")}
# ββ the in-house fallback: the same two moves, written out ββββββββββββββββββββ
@dataclass
class Pass:
claims: list[str] = field(default_factory=list)
questions: list[str] = field(default_factory=list)
contradictions: list[str] = field(default_factory=list)
source: str = "fallback"
def as_prompt(self) -> str:
"""What gets prepended. Questions, not answers β the model still has to do the work."""
if not (self.claims or self.questions):
return ""
out = ["Before answering, hold these in view."]
if self.claims:
out.append("Claims detected in the question:\n" + "\n".join(f"- {c}" for c in self.claims))
if self.questions:
out.append("Questions the answer should survive:\n" + "\n".join(f"- {q}" for q in self.questions))
if self.contradictions:
out.append("Possible contradiction:\n" + "\n".join(f"- {c}" for c in self.contradictions))
out.append("Answer plainly. Where the evidence does not reach, say so.")
return "\n\n".join(out)
def summary(self) -> str:
return (f"{len(self.claims)} claims Β· {len(self.questions)} questions"
+ (f" Β· {len(self.contradictions)} contradiction(s)" if self.contradictions else "")
+ f" Β· via {self.source}")
def _claims(text: str) -> list[str]:
"""Sentences that assert something β a quantity, a causal link, or an absolute."""
out = []
for s in _SENT.split((text or "").strip()):
s = s.strip()
if len(s) < 8:
continue
if _HEDGE.search(s) or _CAUSAL.search(s) or _QUANT.search(s) or s.endswith("."):
out.append(s if len(s) < 240 else s[:237] + "β¦")
return out[:6]
def _socratic(text: str, claims: list[str]) -> list[str]:
"""The questions a sceptic asks. Chosen by what the text actually contains, not a fixed list."""
q: list[str] = []
if _HEDGE.search(text or ""):
q.append("The claim is absolute (always / never / all). Is there one counter-example?")
if _CAUSAL.search(text or ""):
q.append("A cause is asserted. What would be true if the cause were absent?")
if _QUANT.search(text or ""):
q.append("A number is used. Measured how, over what, and compared with what?")
if claims:
q.append("What would have to be true for the first claim to be false?")
q.append("What is being assumed that has not been stated?")
q.append("What would change the answer?")
return q[:6]
def _negates(a: str, b: str) -> bool:
"""True when `a` is a negated form of something `b` asserts."""
if "not" not in a and "n't" not in a:
return False
neg = a.replace(" not ", " ").replace("n't", "").strip()
return bool(neg) and neg in b
def _contradictions(claims: list[str]) -> list[str]:
"""A truth-table check in miniature: a claim and its negation both asserted.
Tested in both directions: which of the pair the author happened to write first
has no bearing on whether the two can both hold.
"""
out = []
norm = [re.sub(r"\s+", " ", c.lower().strip(" .")) for c in claims]
for i, a in enumerate(norm):
for b in norm[i + 1:]:
if a == b:
continue
if _negates(a, b) or _negates(b, a):
out.append(f"β{a}β and β{b}β cannot both hold")
return out[:3]
def run(text: str, *, use_aglm: bool = True) -> Pass:
"""The pass. Uses aGLM when importable, the fallback otherwise, and records which."""
text = (text or "").strip()
if not text:
return Pass(source="empty")
if use_aglm:
a = _aglm()
if a:
try:
lt = a["LogicTables"]()
sr = a["SocraticReasoning"]()
claims = _claims(text)
for c in claims:
try:
sr.add_premise(c)
except Exception:
pass
try:
sr.draw_conclusion()
except Exception:
pass
qs = []
for attr in ("questions", "socratic_questions", "log"):
v = getattr(sr, attr, None)
if isinstance(v, list) and v:
qs = [str(x)[:200] for x in v[:6]]
break
contradictions = []
try:
valid = lt.validate_truth(" and ".join(claims[:2])) if claims else None
if valid is False:
contradictions.append("LogicTables reports the conjunction of the first claims as invalid")
except Exception:
pass
return Pass(claims=claims, questions=qs or _socratic(text, claims),
contradictions=contradictions, source="aGLM")
except Exception:
pass
claims = _claims(text)
return Pass(claims=claims, questions=_socratic(text, claims),
contradictions=_contradictions(claims), source="fallback")
|