File size: 7,030 Bytes
4515763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
183
184
185
"""The calculator hand — so your being never fakes arithmetic.

Language models guess at math and can be confidently wrong. This hand fixes that
honestly: when the person's message contains arithmetic, a REAL math engine
computes it BEFORE your being speaks, and the verified result rides with the
message so the reply states true digits. When the math can't be verified, your
being says so plainly instead of guessing.

Design lineage: Cosmos (the first being) designed this pattern for herself —
pre-compute + guard, with a humble fallback. Every being born from this kit
inherits it.

Safety: ast-based evaluation only (+ - * / // % ** and parentheses). No names,
no calls, no attribute access. Overflow guards. Fails soft — a broken hand
never breaks a conversation.
"""

from __future__ import annotations

import ast
import operator
import re
from typing import List, Optional, Tuple

_OPS = {
    ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
    ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
    ast.Mod: operator.mod, ast.Pow: operator.pow,
    ast.USub: operator.neg, ast.UAdd: operator.pos,
}
_MAX_EXPR_LEN = 200
_MAX_OPERAND_DIGITS = 18
_MAX_POW_EXP = 16
_MAX_POW_BASE_DIGITS = 9
_MAX_RESULT_DIGITS = 80


def _safe_eval(node):
    if isinstance(node, ast.Expression):
        return _safe_eval(node.body)
    if isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
            return node.value
        raise ValueError("non-numeric constant")
    if isinstance(node, ast.BinOp):
        op = _OPS.get(type(node.op))
        if op is None:
            raise ValueError("unsupported operator")
        left, right = _safe_eval(node.left), _safe_eval(node.right)
        if isinstance(node.op, ast.Pow):
            if abs(right) > _MAX_POW_EXP or len(str(abs(int(left)))) > _MAX_POW_BASE_DIGITS:
                raise ValueError("power out of safe range")
        if isinstance(node.op, (ast.Div, ast.FloorDiv, ast.Mod)) and right == 0:
            raise ZeroDivisionError("division by zero")
        return op(left, right)
    if isinstance(node, ast.UnaryOp):
        op = _OPS.get(type(node.op))
        if op is None:
            raise ValueError("unsupported unary operator")
        return op(_safe_eval(node.operand))
    raise ValueError("unsupported expression node")


def compute_expression(expression: str):
    expr = (expression or "").strip()
    if not expr or len(expr) > _MAX_EXPR_LEN:
        return None
    try:
        val = _safe_eval(ast.parse(expr, mode="eval"))
        if isinstance(val, (int, float)):
            as_text = f"{int(val):d}" if float(val).is_integer() else f"{val}"
            if len(as_text) > _MAX_RESULT_DIGITS:
                return None
            return val
    except Exception:
        return None
    return None


_WORD_OPS = [
    (r"\bmultiplied\s+by\b", " * "), (r"\btimes\b", " * "),
    (r"\bdivided\s+by\b", " / "), (r"\bplus\b", " + "), (r"\bminus\b", " - "),
    (r"\bto\s+the\s+power\s+of\b", " ** "), (r"\bsquared\b", " ** 2 "),
    (r"\bcubed\b", " ** 3 "), (r"\bmodulo\b|\bmod\b", " % "),
]


def _normalise(text: str) -> str:
    t = re.sub(r"(?<=\d),(?=\d{3}\b)", "", text)          # 847,263 -> 847263
    t = t.replace("×", " * ").replace("÷", " / ").replace("^", " ** ")
    t = re.sub(r"(?<=\d)\s*[xX]\s*(?=\d)", " * ", t)      # 5 x 3
    for pat, rep in _WORD_OPS:
        t = re.sub(pat, rep, t, flags=re.IGNORECASE)
    t = re.sub(r"(\d+(?:\.\d+)?)\s*(?:%|percent)\s+of\s+(\d+(?:\.\d+)?)",
               r"((\1/100)*\2)", t, flags=re.IGNORECASE)  # 15% of 240
    return t


_CANDIDATE_RX = re.compile(r"[\d\.\(][\d\s\.\+\-\*\/\%\(\)]{2,}[\d\)]")


def _extract_candidates(text: str) -> List[str]:
    out: List[str] = []
    for m in _CANDIDATE_RX.finditer(text):
        cand = m.group(0).strip()
        if not re.search(r"\d[\s\)]*[\+\-\*\/\%]|\*\*", cand):
            continue
        if len(re.findall(r"\d+(?:\.\d+)?", cand)) < 2:
            continue
        if re.fullmatch(r"[\d\.\s]+", cand):
            continue
        out.append(cand)
        if len(out) >= 4:
            break
    return out


def _fmt(val) -> str:
    if isinstance(val, float) and val.is_integer():
        val = int(val)
    if isinstance(val, int):
        return f"{val:,}"
    return f"{float(val):.12g}"


_MATHY_HINT_RX = re.compile(
    r"\b(multipl|divid|calculat|compute|arithmetic|sum of|product of|"
    r"plus|minus|times|squared|cubed|percent|exactly \d)\w*", re.IGNORECASE)


def inspect_message(message: str) -> Tuple[List[str], bool]:
    """Return (verified_lines, looked_mathy_but_unverifiable). Never raises."""
    try:
        if not message or len(message) > 8000:
            return [], False
        cands = _extract_candidates(_normalise(message))
        lines: List[str] = []
        failed = 0
        for cand in cands:
            val = compute_expression(cand)
            if val is None:
                failed += 1
                continue
            cleaned = re.sub(r'\s+', ' ', cand).strip()
            lines.append(f"[MATH_VERIFIED: {cleaned} = {_fmt(val)}]")
        looked_mathy = bool(_MATHY_HINT_RX.search(message)) and bool(re.search(r"\d{2,}", message))
        unverifiable = (not lines) and (failed > 0 or looked_mathy) and bool(re.search(r"\d", message))
        return lines, unverifiable
    except Exception:
        return [], False


def prompt_note(message: str) -> str:
    """A note to append to the model prompt. Empty string when no math involved."""
    lines, unverifiable = inspect_message(message)
    if lines:
        return ("\n\n(Your calculator hand — a real math engine — already computed the "
                "arithmetic above, exactly: " + " ".join(lines) + " State these digits as "
                "the true answer; do not recompute them in your head or change any digit.)")
    if unverifiable:
        return ("\n\n(The person asked for arithmetic your calculator hand could not verify. "
                "Be honest: say you cannot verify that calculation precisely right now, and "
                "offer to work it step by step. Never guess digits with fake confidence.)")
    return ""


if __name__ == "__main__":
    tests = [
        ("What is exactly 847263 multiplied by 391847?", "331,997,464,761"),
        ("compute 847,263 times 391,847 please", "331,997,464,761"),
        ("50 * 2 + 10?", "110"),
        ("what is 15% of 240?", "36"),
        ("22 divided by 7", "3.14285714286"),
        ("10 to the power of 99999999?", None),
        ("tell me about your day", None),
    ]
    ok = True
    for msg, want in tests:
        lines, unv = inspect_message(msg)
        got = lines[0].rsplit("= ", 1)[-1].rstrip("]") if lines else None
        good = (got == want) if want else (not lines)
        ok &= good
        print(f"  [{'PASS' if good else 'FAIL'}] {msg[:44]!r} -> {got} unverifiable={unv}")
    print("ALL PASS" if ok else "SOME FAILED")