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")