Preserve quantity, relation and conversation meaning in legacy runtime paths
#2
by emberian - opened
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_operator_specs_v1.py +57 -21
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_ao.py +24 -2
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w442.py +20 -0
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w468.py +28 -8
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w488.py +15 -2
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w534.py +46 -3
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py +1 -1
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_conversation_state.py +98 -0
- root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_final_outcome.py +4 -0
- root/specialists/_gate/combo_runtime_v1.py +67 -6
- root/specialists/energy-v3/organ_energy_v1.py +215 -21
- root/specialists/geometry-v4/manifest.json +2 -2
- root/specialists/geometry-v4/organ_geometry_v4.py +175 -2
- root/wander-train/math_organ_v1.py +25 -6
- root/wander-train/math_organ_v2.py +7 -6
- root/wander-train/math_organ_v3.py +7 -6
- root/wander-train/oracle_operator_specs_v1.py +57 -21
- root/wander-train/oracle_release_runtime_ao.py +24 -2
- root/wander-train/reach_router_v1.py +5 -3
- root/wander-train/reach_router_v2.py +5 -3
- root/wander-train/reach_router_v3.py +60 -11
- root/wander-train/relation_spec_v1.py +62 -0
- root/wander-train/word_problems_v1.py +132 -10
- tests/conversation_grammar/test_m53_dummy_it.py +126 -0
- tests/conversation_grammar_r468/test_door_dummy_it.py +142 -0
- tests/conversation_guard/test_last_question.py +152 -0
- tests/usage/README.md +26 -0
- tests/usage/composition/test_energy_repair.py +131 -0
- tests/usage/computing/test_base_conversion.py +88 -0
- tests/usage/conversation/birth-replies.json +69 -0
- tests/usage/conversation/test_state.py +126 -0
- tests/usage/documents/test_conflict_repair.py +82 -0
- tests/usage/engineering/test_condition_attachment.py +142 -0
- tests/usage/evidence/test_math_finite.py +71 -0
- tests/usage/fixtures/composition/cases.json +62 -0
- tests/usage/fixtures/engineering/cases.json +16 -0
- tests/usage/fixtures/engineering/followup_cases.json +22 -0
- tests/usage/fixtures/reliability/cases.json +77 -0
- tests/usage/fixtures/reliability/preserved_answers.json +202 -0
- tests/usage/reliability/test_geometry_consumer.py +189 -0
- tests/usage/reliability/test_geometry_units.py +113 -0
- tests/usage/reliability_route/test_geometry_route.py +188 -0
- tests/usage/research/test_relation_semantics.py +83 -0
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_operator_specs_v1.py
CHANGED
|
@@ -198,33 +198,63 @@ def _gcd_ref(a):
|
|
| 198 |
|
| 199 |
|
| 200 |
# ── 3. base conversion ────────────────────────────────────────────────────────
|
| 201 |
-
#
|
| 202 |
-
|
| 203 |
-
r'\b(?:convert|write|express)?\s*(-?\d+)\s*(?:from\s*)?(?:base\s*(\d+))?\s*'
|
| 204 |
-
r'(?:in|to|into)\s*(?:base\s*)?(\d+|binary|hex|hexadecimal|octal|decimal)\b', re.I)
|
| 205 |
_BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
|
| 207 |
|
| 208 |
|
| 209 |
def _base_parse(s):
|
| 210 |
-
m = _BASE_RX.
|
| 211 |
if not m:
|
| 212 |
-
raise NotOurs('no
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
raise OutOfFragment('I convert non-negative whole numbers only')
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
|
| 226 |
def _base_fn(a):
|
| 227 |
-
n, steps, out = a['
|
| 228 |
if n == 0:
|
| 229 |
return '0', ['0 in any base is 0']
|
| 230 |
while n:
|
|
@@ -235,8 +265,13 @@ def _base_fn(a):
|
|
| 235 |
|
| 236 |
|
| 237 |
def _base_ref(a):
|
| 238 |
-
"""
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
if n == 0:
|
| 241 |
return '0'
|
| 242 |
digits, powers = [], []
|
|
@@ -418,8 +453,9 @@ SPECS = [
|
|
| 418 |
'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
|
| 419 |
'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
|
| 420 |
'warrant': 'computation',
|
| 421 |
-
'independence': 'moderate:
|
| 422 |
-
'
|
|
|
|
| 423 |
'fragment': 'non-negative whole numbers, bases 2 to 36'},
|
| 424 |
|
| 425 |
{'name': 'statistics', 'family': 'measurement',
|
|
|
|
| 198 |
|
| 199 |
|
| 200 |
# ── 3. base conversion ────────────────────────────────────────────────────────
|
| 201 |
+
# The parser binds the original digits and source base. The function decodes
|
| 202 |
+
# with int(); the reference independently expands source digits positionally.
|
|
|
|
|
|
|
| 203 |
_BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
|
| 204 |
+
_BASE_ATOM = r'(?:base\s*\d+|binary|hexadecimal|hex|octal|decimal)'
|
| 205 |
+
_BASE_RX = re.compile(
|
| 206 |
+
r'^\s*(?:(?:convert|write|express|what\s+is)\s+)?'
|
| 207 |
+
r'(?:(?P<prefix>' + _BASE_ATOM + r')\s+)?'
|
| 208 |
+
r'(?P<digits>[+-]?[0-9a-z]+)'
|
| 209 |
+
r'(?:\s+(?:(?:from|in)\s+)?(?P<source>' + _BASE_ATOM + r'))?'
|
| 210 |
+
r'\s+(?:in|to|into|as)\s+(?P<target>' + _BASE_ATOM + r'|\d+)'
|
| 211 |
+
r'\s*[.?!]?\s*$', re.I | re.ASCII)
|
| 212 |
_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
|
| 213 |
|
| 214 |
|
| 215 |
def _base_parse(s):
|
| 216 |
+
m = _BASE_RX.fullmatch(s)
|
| 217 |
if not m:
|
| 218 |
+
raise NotOurs('no complete integer base conversion here')
|
| 219 |
+
|
| 220 |
+
def base(token):
|
| 221 |
+
token = token.lower()
|
| 222 |
+
value = _BASE_NAME.get(token)
|
| 223 |
+
if value is None:
|
| 224 |
+
numeral = re.sub(r'^base\s*', '', token)
|
| 225 |
+
if len(numeral) > 3:
|
| 226 |
+
raise OutOfFragment('I convert between base 2 and base 36 only')
|
| 227 |
+
value = int(numeral)
|
| 228 |
+
if not 2 <= value <= 36:
|
| 229 |
+
raise OutOfFragment('I convert between base 2 and base 36 only')
|
| 230 |
+
return value
|
| 231 |
+
|
| 232 |
+
sources = [base(token) for token in (m['prefix'], m['source']) if token]
|
| 233 |
+
raw = m['digits'].lower()
|
| 234 |
+
if raw.startswith('-'):
|
| 235 |
raise OutOfFragment('I convert non-negative whole numbers only')
|
| 236 |
+
raw = raw.lstrip('+')
|
| 237 |
+
# A conventional literal prefix is source evidence, not a digit to discard.
|
| 238 |
+
# Explicit bases above 16 may use x as a digit; only infer prefixes when no
|
| 239 |
+
# explicit source is supplied, or that source matches the prefix convention.
|
| 240 |
+
literal = {'0b': 2, '0o': 8, '0x': 16}.get(raw[:2])
|
| 241 |
+
if literal is not None and (not sources or all(b == literal for b in sources)):
|
| 242 |
+
sources.append(literal)
|
| 243 |
+
raw = raw[2:]
|
| 244 |
+
elif literal is not None and sources and sources[0] in (2, 8, 10, 16):
|
| 245 |
+
raise OutOfFragment('the literal prefix conflicts with the stated source base')
|
| 246 |
+
if len(set(sources)) > 1:
|
| 247 |
+
raise OutOfFragment('the stated source bases disagree')
|
| 248 |
+
frm = sources[0] if sources else 10
|
| 249 |
+
to = base(m['target'])
|
| 250 |
+
if not raw or len(raw) > 128 or any(_DIGITS.index(c) >= frm for c in raw):
|
| 251 |
+
raise OutOfFragment('I need 1 to 128 valid digits in the stated source base')
|
| 252 |
+
n = int(raw, frm)
|
| 253 |
+
return {'n': n, 'digits': raw, 'frm': frm, 'to': to}
|
| 254 |
|
| 255 |
|
| 256 |
def _base_fn(a):
|
| 257 |
+
n, steps, out = int(a['digits'], a['frm']), [], ''
|
| 258 |
if n == 0:
|
| 259 |
return '0', ['0 in any base is 0']
|
| 260 |
while n:
|
|
|
|
| 265 |
|
| 266 |
|
| 267 |
def _base_ref(a):
|
| 268 |
+
"""Expand original source digits, then encode using a positional table.
|
| 269 |
+
|
| 270 |
+
This does not share int() source decoding or repeated output division with
|
| 271 |
+
the primary function. Both still depend on the same untrusted parse.
|
| 272 |
+
"""
|
| 273 |
+
n = sum(_DIGITS.index(digit) * a['frm'] ** power
|
| 274 |
+
for power, digit in enumerate(reversed(a['digits'])))
|
| 275 |
if n == 0:
|
| 276 |
return '0'
|
| 277 |
digits, powers = [], []
|
|
|
|
| 453 |
'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
|
| 454 |
'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
|
| 455 |
'warrant': 'computation',
|
| 456 |
+
'independence': 'moderate: int source decoding and repeated output division '
|
| 457 |
+
'against positional source expansion and output encoding; '
|
| 458 |
+
'both still depend on the same untrusted parse',
|
| 459 |
'fragment': 'non-negative whole numbers, bases 2 to 36'},
|
| 460 |
|
| 461 |
{'name': 'statistics', 'family': 'measurement',
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_ao.py
CHANGED
|
@@ -22,6 +22,16 @@ import re
|
|
| 22 |
|
| 23 |
from oracle_release_runtime_an import OracleReleaseRuntimeAN
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
|
| 26 |
ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
|
| 27 |
BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
|
|
@@ -41,6 +51,17 @@ PREDICATE_OF = {
|
|
| 41 |
}
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
|
| 45 |
"""Conversions are applied ONLY when the unchanged chain would not answer."""
|
| 46 |
|
|
@@ -103,10 +124,11 @@ class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
|
|
| 103 |
if not CONTINUATION.match(s):
|
| 104 |
return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
|
| 105 |
topic = self._live_topic(session_id)
|
| 106 |
-
|
|
|
|
| 107 |
if low in PREDICATE_OF:
|
| 108 |
return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
|
| 109 |
-
return
|
| 110 |
return None, None
|
| 111 |
|
| 112 |
def chat(self, text: str, session_id: str = "default") -> dict:
|
|
|
|
| 22 |
|
| 23 |
from oracle_release_runtime_an import OracleReleaseRuntimeAN
|
| 24 |
|
| 25 |
+
# Untrusted grammar guard: in "does it take/cost ... to <action>", it
|
| 26 |
+
# may be a grammatical subject rather than an entity from the conversation.
|
| 27 |
+
# Decline substitution for that token; other references remain eligible. The
|
| 28 |
+
# match stays within a clause and accepts decimal amounts before the infinitive.
|
| 29 |
+
DUMMY_IT = re.compile(
|
| 30 |
+
r"\b(?:does|did|will|would|can|could|should|has|had)\s+(?P<it>it)\s+"
|
| 31 |
+
r"(?:take|taken|cost)\s+"
|
| 32 |
+
r"(?:(?:\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|"
|
| 33 |
+
r"months?|years?|dollars?|euros?|pounds?)|[$€£]\d+(?:\.\d+)?)\s+)?"
|
| 34 |
+
r"to\s+[a-z]", re.I)
|
| 35 |
DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
|
| 36 |
ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
|
| 37 |
BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
|
| 54 |
+
def referential_deictic(text):
|
| 55 |
+
"""First eligible reference, excluding only take/cost-infinitive dummy it.
|
| 56 |
+
|
| 57 |
+
This does not claim to parse all English expletives. Skipping an ambiguous
|
| 58 |
+
token leaves the original question available to the unchanged handler.
|
| 59 |
+
"""
|
| 60 |
+
dummy_spans = {match.span("it") for match in DUMMY_IT.finditer(text)}
|
| 61 |
+
return next((match for match in DEICTIC.finditer(text)
|
| 62 |
+
if match.span() not in dummy_spans), None)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
|
| 66 |
"""Conversions are applied ONLY when the unchanged chain would not answer."""
|
| 67 |
|
|
|
|
| 124 |
if not CONTINUATION.match(s):
|
| 125 |
return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
|
| 126 |
topic = self._live_topic(session_id)
|
| 127 |
+
reference = referential_deictic(s)
|
| 128 |
+
if topic and reference:
|
| 129 |
if low in PREDICATE_OF:
|
| 130 |
return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
|
| 131 |
+
return s[:reference.start()] + topic + s[reference.end():], "REFERENT_SUBSTITUTION"
|
| 132 |
return None, None
|
| 133 |
|
| 134 |
def chat(self, text: str, session_id: str = "default") -> dict:
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w442.py
CHANGED
|
@@ -130,14 +130,34 @@ class OracleReleaseRuntimeW442(_Base):
|
|
| 130 |
numbers = {t for t in terms if t.isdigit()}
|
| 131 |
terms_stem = [_stem(t) for t in terms]
|
| 132 |
best = None
|
|
|
|
| 133 |
for sent in _sentences(passage):
|
| 134 |
pool = set(_stem(t) for t in _toks(sent))
|
| 135 |
ok, cov, via = self._r436_covered(terms, terms_stem, numbers, pool)
|
| 136 |
if ok:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
hits = sum(1 for t in terms_stem if t in pool)
|
| 138 |
if best is None or hits > best[0]:
|
| 139 |
best = (hits, sent, cov, via)
|
| 140 |
out = dict(res)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
if best is None:
|
| 142 |
return self._r434_receipt(out, res, 'I read the passage and the question but found no sentence that covers the question in full, so I will not guess.', 'no_covering_sentence')
|
| 143 |
_, sent, cov, via = best
|
|
|
|
| 130 |
numbers = {t for t in terms if t.isdigit()}
|
| 131 |
terms_stem = [_stem(t) for t in terms]
|
| 132 |
best = None
|
| 133 |
+
covered_verdicts = []
|
| 134 |
for sent in _sentences(passage):
|
| 135 |
pool = set(_stem(t) for t in _toks(sent))
|
| 136 |
ok, cov, via = self._r436_covered(terms, terms_stem, numbers, pool)
|
| 137 |
if ok:
|
| 138 |
+
# Every covering sentence matters: selecting the first tied
|
| 139 |
+
# sentence can silently choose one of conflicting versions.
|
| 140 |
+
vt = set(terms) | set(terms_stem) | {_stem(a) for _, a in via} | {a for _, a in via}
|
| 141 |
+
candidate_verdict, _ = self._r434_verdict(sent, vt, terms_stem, q)
|
| 142 |
+
if candidate_verdict is not None:
|
| 143 |
+
covered_verdicts.append((cov, candidate_verdict, sent))
|
| 144 |
hits = sum(1 for t in terms_stem if t in pool)
|
| 145 |
if best is None or hits > best[0]:
|
| 146 |
best = (hits, sent, cov, via)
|
| 147 |
out = dict(res)
|
| 148 |
+
# A partial lexical match may concern another subject (e.g. Cedar
|
| 149 |
+
# Archive rather than Lumen Archive). It must not veto a fully
|
| 150 |
+
# covered answer. Aliases count as covered, as in the existing reader.
|
| 151 |
+
best_coverage = best[2] if best is not None else 0.0
|
| 152 |
+
conflicting_candidates = [(v, sentence) for coverage, v, sentence in covered_verdicts if coverage >= best_coverage]
|
| 153 |
+
settled_verdicts = {v for v, _ in conflicting_candidates}
|
| 154 |
+
if len(settled_verdicts) > 1:
|
| 155 |
+
conflict = self._r434_receipt(out, res,
|
| 156 |
+
'The passage contains covering sentences with conflicting answers, so I will not choose a version.',
|
| 157 |
+
'conflicting_covering_sentences')
|
| 158 |
+
conflict['references'] = [{'id': 'passage', 'text': sentence}
|
| 159 |
+
for _, sentence in conflicting_candidates]
|
| 160 |
+
return conflict
|
| 161 |
if best is None:
|
| 162 |
return self._r434_receipt(out, res, 'I read the passage and the question but found no sentence that covers the question in full, so I will not guess.', 'no_covering_sentence')
|
| 163 |
_, sent, cov, via = best
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w468.py
CHANGED
|
@@ -33,6 +33,14 @@ try:
|
|
| 33 |
except Exception: # noqa: BLE001
|
| 34 |
_ir = None
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
POINTER = re.compile(r"\b(it|its|that|this|they|them|those|these)\b", re.I)
|
| 37 |
DEICTIC = re.compile(r"\b(?:the|that|this)\s+(?:thing|one|place|stuff|topic|subject|idea|person|guy|animal|city|country)"
|
| 38 |
r"(?:\s+(?:with|from|about|of|in|on)\s+(?:the\s+)?[a-z][\w\-]*(?:\s+[a-z][\w\-]*)?)?\b", re.I)
|
|
@@ -96,6 +104,20 @@ def from_thread(question, subject, replies, year_now):
|
|
| 96 |
return None
|
| 97 |
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
class OracleReleaseRuntimeW468(_Base):
|
| 100 |
|
| 101 |
def _r468_thread(self, sid):
|
|
@@ -148,19 +170,17 @@ class OracleReleaseRuntimeW468(_Base):
|
|
| 148 |
del th[:-MAX_THREAD]
|
| 149 |
|
| 150 |
def _r468_bind(self, text, subject):
|
| 151 |
-
m =
|
| 152 |
-
if m:
|
| 153 |
-
return text[:m.start()] + subject + text[m.end():]
|
| 154 |
-
m = POINTER.search(text)
|
| 155 |
if m:
|
| 156 |
-
word = m.group(
|
| 157 |
rep = subject + "'s" if word == "its" else subject
|
| 158 |
return text[:m.start()] + rep + text[m.end():]
|
| 159 |
return None
|
| 160 |
|
| 161 |
def _r468_door(self, text, sid, res):
|
| 162 |
thread = self._r468_thread(sid)
|
| 163 |
-
|
|
|
|
| 164 |
named = self._r468_subjects(text)
|
| 165 |
if points and thread and not named:
|
| 166 |
subject = thread[-1]
|
|
@@ -197,13 +217,13 @@ class OracleReleaseRuntimeW468(_Base):
|
|
| 197 |
out = dict(res)
|
| 198 |
out.update({"status": "withheld", "path": "door:bound|vessel:receipt",
|
| 199 |
"response": "I took \"%s\" to mean %s, but I have no attested answer to that about %s."
|
| 200 |
-
% (
|
| 201 |
"provenance": {"kind": "dialogue_state",
|
| 202 |
"r468_door": {"act": "bound_no_answer", "subject": subject,
|
| 203 |
"rewritten": rewritten}}})
|
| 204 |
return out
|
| 205 |
# ASK: a specific question, naming what could not be resolved and what this conversation has raised
|
| 206 |
-
unresolved =
|
| 207 |
phrase = unresolved.group(0) if unresolved else None
|
| 208 |
offers = list(reversed(thread[-3:]))
|
| 209 |
if phrase and offers:
|
|
|
|
| 33 |
except Exception: # noqa: BLE001
|
| 34 |
_ir = None
|
| 35 |
|
| 36 |
+
# Untrusted bounded grammar guard, kept equal to AO without importing its
|
| 37 |
+
# runtime chain. Both binding and clarification exclude the same dummy token.
|
| 38 |
+
DUMMY_IT = re.compile(
|
| 39 |
+
r"\b(?:does|did|will|would|can|could|should|has|had)\s+(?P<it>it)\s+"
|
| 40 |
+
r"(?:take|taken|cost)\s+"
|
| 41 |
+
r"(?:(?:\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|"
|
| 42 |
+
r"months?|years?|dollars?|euros?|pounds?)|[$€£]\d+(?:\.\d+)?)\s+)?"
|
| 43 |
+
r"to\s+[a-z]", re.I)
|
| 44 |
POINTER = re.compile(r"\b(it|its|that|this|they|them|those|these)\b", re.I)
|
| 45 |
DEICTIC = re.compile(r"\b(?:the|that|this)\s+(?:thing|one|place|stuff|topic|subject|idea|person|guy|animal|city|country)"
|
| 46 |
r"(?:\s+(?:with|from|about|of|in|on)\s+(?:the\s+)?[a-z][\w\-]*(?:\s+[a-z][\w\-]*)?)?\b", re.I)
|
|
|
|
| 104 |
return None
|
| 105 |
|
| 106 |
|
| 107 |
+
def _referential_pointer(text):
|
| 108 |
+
"""One genuine pointing phrase/token, preserving later refs after dummy it.
|
| 109 |
+
|
| 110 |
+
Take/cost-infinitive grammar may use it without any prior entity. Leave that
|
| 111 |
+
token to the original reader; this is not a general English pronoun parser.
|
| 112 |
+
"""
|
| 113 |
+
phrase = DEICTIC.search(text)
|
| 114 |
+
if phrase:
|
| 115 |
+
return phrase
|
| 116 |
+
dummy_spans = {match.span("it") for match in DUMMY_IT.finditer(text)}
|
| 117 |
+
return next((match for match in POINTER.finditer(text)
|
| 118 |
+
if match.span() not in dummy_spans), None)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
class OracleReleaseRuntimeW468(_Base):
|
| 122 |
|
| 123 |
def _r468_thread(self, sid):
|
|
|
|
| 170 |
del th[:-MAX_THREAD]
|
| 171 |
|
| 172 |
def _r468_bind(self, text, subject):
|
| 173 |
+
m = _referential_pointer(text)
|
|
|
|
|
|
|
|
|
|
| 174 |
if m:
|
| 175 |
+
word = m.group(0).lower()
|
| 176 |
rep = subject + "'s" if word == "its" else subject
|
| 177 |
return text[:m.start()] + rep + text[m.end():]
|
| 178 |
return None
|
| 179 |
|
| 180 |
def _r468_door(self, text, sid, res):
|
| 181 |
thread = self._r468_thread(sid)
|
| 182 |
+
reference = _referential_pointer(text)
|
| 183 |
+
points = reference is not None
|
| 184 |
named = self._r468_subjects(text)
|
| 185 |
if points and thread and not named:
|
| 186 |
subject = thread[-1]
|
|
|
|
| 217 |
out = dict(res)
|
| 218 |
out.update({"status": "withheld", "path": "door:bound|vessel:receipt",
|
| 219 |
"response": "I took \"%s\" to mean %s, but I have no attested answer to that about %s."
|
| 220 |
+
% (reference.group(0), subject, subject),
|
| 221 |
"provenance": {"kind": "dialogue_state",
|
| 222 |
"r468_door": {"act": "bound_no_answer", "subject": subject,
|
| 223 |
"rewritten": rewritten}}})
|
| 224 |
return out
|
| 225 |
# ASK: a specific question, naming what could not be resolved and what this conversation has raised
|
| 226 |
+
unresolved = reference
|
| 227 |
phrase = unresolved.group(0) if unresolved else None
|
| 228 |
offers = list(reversed(thread[-3:]))
|
| 229 |
if phrase and offers:
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w488.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""oracle_release_runtime_w488 - WORD PROBLEMS: single-step prose read into an equation by a stored schema.
|
| 3 |
|
|
|
|
|
|
|
| 4 |
The math organ (v1/v2) declines a word problem ("the quantities must be translated into equations first"). This fold
|
| 5 |
connects word_problems_v1 (sha 4c0c60b1): each SCHEMA is a stored form - trigger words, the TYPED quantities it needs
|
| 6 |
(money, percent, time, length, mass, volume; read with their units), and a formula - and it fires only when every
|
|
@@ -34,6 +36,7 @@ def _fmt(v, unit):
|
|
| 34 |
return "$%s" % ("{:,.2f}".format(v) if v % 1 else "{:,.0f}".format(v))
|
| 35 |
if unit == "%":
|
| 36 |
return "%s%%" % ("%g" % round(v, 4))
|
|
|
|
| 37 |
return ("%g" % round(v, 6)) + ((" " + unit) if unit and unit not in ("sq", "cu") else
|
| 38 |
(" square units" if unit == "sq" else (" cubic units" if unit == "cu" else "")))
|
| 39 |
|
|
@@ -46,13 +49,23 @@ class OracleReleaseRuntimeW488(_Base):
|
|
| 46 |
return out
|
| 47 |
a = _run(_wp.answer, text)
|
| 48 |
if not a:
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
body = ("[ev:derived] %s. Read as %s: %s. Checked: every quantity the formula needs was found with its unit, "
|
| 51 |
"and the question asks for what the formula gives." % (_fmt(a["value"], a["unit"]),
|
| 52 |
a["schema"].replace("_", " "),
|
| 53 |
"; ".join(a["steps"])))
|
| 54 |
new = dict(out)
|
| 55 |
new.update({'status': 'answered', 'path': 'organ:math>word:%s' % a['schema'], 'response': body,
|
| 56 |
-
'provenance': {'kind': 'organ_word_problem', 'schema': a['schema'], 'value': a['value'],
|
| 57 |
'parent_path': str(out.get('path', '')), 'parent_status': out.get('status')}})
|
| 58 |
return new
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""oracle_release_runtime_w488 - WORD PROBLEMS: single-step prose read into an equation by a stored schema.
|
| 3 |
|
| 4 |
+
This Python routing shim is untrusted; its checks are not proof or admission authority.
|
| 5 |
+
|
| 6 |
The math organ (v1/v2) declines a word problem ("the quantities must be translated into equations first"). This fold
|
| 7 |
connects word_problems_v1 (sha 4c0c60b1): each SCHEMA is a stored form - trigger words, the TYPED quantities it needs
|
| 8 |
(money, percent, time, length, mass, volume; read with their units), and a formula - and it fires only when every
|
|
|
|
| 36 |
return "$%s" % ("{:,.2f}".format(v) if v % 1 else "{:,.0f}".format(v))
|
| 37 |
if unit == "%":
|
| 38 |
return "%s%%" % ("%g" % round(v, 4))
|
| 39 |
+
unit = unit.replace("^2", "²").replace("^3", "³")
|
| 40 |
return ("%g" % round(v, 6)) + ((" " + unit) if unit and unit not in ("sq", "cu") else
|
| 41 |
(" square units" if unit == "sq" else (" cubic units" if unit == "cu" else "")))
|
| 42 |
|
|
|
|
| 49 |
return out
|
| 50 |
a = _run(_wp.answer, text)
|
| 51 |
if not a:
|
| 52 |
+
explain = getattr(_wp, 'explain_refusal', None)
|
| 53 |
+
reason = _run(explain, text) if callable(explain) else None
|
| 54 |
+
if not reason:
|
| 55 |
+
return out
|
| 56 |
+
new = dict(out)
|
| 57 |
+
new.update({'status': 'clarification', 'path': 'organ:math>word:%s' % reason['reason'],
|
| 58 |
+
'response': reason['response'],
|
| 59 |
+
'provenance': {'kind': 'word_problem_refusal', 'schema': reason['schema'],
|
| 60 |
+
'reason': reason['reason'], 'conflict': reason['conflict'],
|
| 61 |
+
'parent_path': str(out.get('path', '')), 'parent_status': out.get('status')}})
|
| 62 |
+
return new
|
| 63 |
body = ("[ev:derived] %s. Read as %s: %s. Checked: every quantity the formula needs was found with its unit, "
|
| 64 |
"and the question asks for what the formula gives." % (_fmt(a["value"], a["unit"]),
|
| 65 |
a["schema"].replace("_", " "),
|
| 66 |
"; ".join(a["steps"])))
|
| 67 |
new = dict(out)
|
| 68 |
new.update({'status': 'answered', 'path': 'organ:math>word:%s' % a['schema'], 'response': body,
|
| 69 |
+
'provenance': {'kind': 'organ_word_problem', 'schema': a['schema'], 'value': a['value'], 'unit': a['unit'],
|
| 70 |
'parent_path': str(out.get('path', '')), 'parent_status': out.get('status')}})
|
| 71 |
return new
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w534.py
CHANGED
|
@@ -16,6 +16,9 @@ in both variants - bar <= 2) and CONVERSATION TEST 2 (run once, head w533). Two
|
|
| 16 |
to. Here the turn is resolved FIRST (the same engine, chat_context_v2.rewrite), the whole stack is asked the
|
| 17 |
standalone question, and the conversation state is updated from the FINAL answer (its receipt names the entity).
|
| 18 |
A turn the engine splits into parts, or cannot resolve, goes down exactly as typed.
|
|
|
|
|
|
|
|
|
|
| 19 |
Every guard WITHHOLDS with its reason; nothing here invents an answer. The fold imports no runtime other than its parent;
|
| 20 |
the conversation engine is called through its own public functions (rewrite, update, split_parts)."""
|
| 21 |
from __future__ import annotations
|
|
@@ -64,6 +67,37 @@ def private_state(q):
|
|
| 64 |
return bool(PRIVATE.search(q)) and not QUANTITY.search(q)
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def premise_years(q):
|
| 68 |
m = PREMISE.match(q)
|
| 69 |
if not m or _EL is None:
|
|
@@ -98,7 +132,7 @@ def _withhold(out, rule, reason):
|
|
| 98 |
return new
|
| 99 |
|
| 100 |
|
| 101 |
-
def final_guards(q, out):
|
| 102 |
"""the w532 guards, on the FINAL answer of any route"""
|
| 103 |
if not isinstance(out, dict) or str(out.get("status", "")) != "answered":
|
| 104 |
return out
|
|
@@ -107,7 +141,7 @@ def final_guards(q, out):
|
|
| 107 |
return out
|
| 108 |
if "specialist" in path and sign_dropped(q, reply):
|
| 109 |
return _withhold(out, "G1_sign_kept", REASON["G1_sign_kept"])
|
| 110 |
-
if private_state(q):
|
| 111 |
return _withhold(out, "G2_private_state", REASON["G2_private_state"])
|
| 112 |
try:
|
| 113 |
pm = premise_years(q)
|
|
@@ -131,10 +165,19 @@ class OracleReleaseRuntimeW534(_Base):
|
|
| 131 |
standalone = s
|
| 132 |
except Exception: # noqa: BLE001 - an engine failure leaves the turn exactly as typed
|
| 133 |
standalone = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
out = super().chat(standalone or q, session_id=session_id)
|
| 135 |
if not isinstance(out, dict):
|
| 136 |
return out
|
| 137 |
-
out = final_guards(standalone or q, out)
|
| 138 |
if standalone:
|
| 139 |
out = dict(out)
|
| 140 |
if not str(out.get("path", "")).startswith("chat:"):
|
|
|
|
| 16 |
to. Here the turn is resolved FIRST (the same engine, chat_context_v2.rewrite), the whole stack is asked the
|
| 17 |
standalone question, and the conversation state is updated from the FINAL answer (its receipt names the entity).
|
| 18 |
A turn the engine splits into parts, or cannot resolve, goes down exactly as typed.
|
| 19 |
+
A last-question recall may pass G2 only when its complete reply and turn references match
|
| 20 |
+
the same session's dialogue record captured before this turn. This does not authorize
|
| 21 |
+
guessing personal facts or answering an additional private question.
|
| 22 |
Every guard WITHHOLDS with its reason; nothing here invents an answer. The fold imports no runtime other than its parent;
|
| 23 |
the conversation engine is called through its own public functions (rewrite, update, split_parts)."""
|
| 24 |
from __future__ import annotations
|
|
|
|
| 67 |
return bool(PRIVATE.search(q)) and not QUANTITY.search(q)
|
| 68 |
|
| 69 |
|
| 70 |
+
# A whole request for the last recorded question/utterance, not a substring in
|
| 71 |
+
# a compound private-state request. First-question/history-list semantics are
|
| 72 |
+
# deliberately outside this exception: the dialogue organ selects the last turn.
|
| 73 |
+
LAST_TURN = re.compile(r"\s*(?:what\s+did\s+i\s+(?:(?:just|last)\s+)?(?:ask|say)(?:\s+you)?|"
|
| 74 |
+
r"what\s+was\s+my\s+(?:last|previous)\s+question)\s*[?.!]*\s*", re.I)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def recorded_history_reply(q, out, expected):
|
| 78 |
+
"""An exact match to a same-session reply captured before the parent ran.
|
| 79 |
+
|
| 80 |
+
Path/citation strings alone are not authority. The caller obtains expected
|
| 81 |
+
from the dialogue organ's pure read of its current, bounded session history.
|
| 82 |
+
This Python check does not prove the authenticity of that in-memory history.
|
| 83 |
+
"""
|
| 84 |
+
if not LAST_TURN.fullmatch(q) or not isinstance(expected, dict):
|
| 85 |
+
return False
|
| 86 |
+
if expected.get("kind") != "discourse:asked_last":
|
| 87 |
+
return False
|
| 88 |
+
turns = expected.get("turns") or []
|
| 89 |
+
provenance = out.get("provenance")
|
| 90 |
+
references = [{"id": "session:turn %d" % turn, "text": "this conversation, turn %d" % turn,
|
| 91 |
+
"role": "discourse"} for turn in turns]
|
| 92 |
+
return (out.get("path") == "organ:dialogue>discourse:asked_last"
|
| 93 |
+
and out.get("response") == expected.get("response")
|
| 94 |
+
and out.get("references") == references
|
| 95 |
+
and isinstance(provenance, dict)
|
| 96 |
+
and provenance.get("kind") == "organ_dialogue"
|
| 97 |
+
and provenance.get("move") == expected["kind"]
|
| 98 |
+
and provenance.get("turns") == turns)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
def premise_years(q):
|
| 102 |
m = PREMISE.match(q)
|
| 103 |
if not m or _EL is None:
|
|
|
|
| 132 |
return new
|
| 133 |
|
| 134 |
|
| 135 |
+
def final_guards(q, out, history_reply=None):
|
| 136 |
"""the w532 guards, on the FINAL answer of any route"""
|
| 137 |
if not isinstance(out, dict) or str(out.get("status", "")) != "answered":
|
| 138 |
return out
|
|
|
|
| 141 |
return out
|
| 142 |
if "specialist" in path and sign_dropped(q, reply):
|
| 143 |
return _withhold(out, "G1_sign_kept", REASON["G1_sign_kept"])
|
| 144 |
+
if private_state(q) and not recorded_history_reply(q, out, history_reply):
|
| 145 |
return _withhold(out, "G2_private_state", REASON["G2_private_state"])
|
| 146 |
try:
|
| 147 |
pm = premise_years(q)
|
|
|
|
| 165 |
standalone = s
|
| 166 |
except Exception: # noqa: BLE001 - an engine failure leaves the turn exactly as typed
|
| 167 |
standalone = None
|
| 168 |
+
history_reply = None
|
| 169 |
+
if LAST_TURN.fullmatch(standalone or q):
|
| 170 |
+
try:
|
| 171 |
+
# DialogueOrgan.answer only reads its bounded session log. Read
|
| 172 |
+
# before the parent appends this turn, including on an empty log.
|
| 173 |
+
dialogue = self._c480_dialogue()
|
| 174 |
+
history_reply = dialogue.answer(standalone or q, session_id) if dialogue else None
|
| 175 |
+
except Exception: # noqa: BLE001 - missing history keeps the existing private-state guard
|
| 176 |
+
pass
|
| 177 |
out = super().chat(standalone or q, session_id=session_id)
|
| 178 |
if not isinstance(out, dict):
|
| 179 |
return out
|
| 180 |
+
out = final_guards(standalone or q, out, history_reply)
|
| 181 |
if standalone:
|
| 182 |
out = dict(out)
|
| 183 |
if not str(out.get("path", "")).startswith("chat:"):
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py
CHANGED
|
@@ -73,7 +73,7 @@ ROUTE_SPELLING = [
|
|
| 73 |
# what an answer may stand on and still become state: a citation or source tag, a proof, or a checked route
|
| 74 |
CITED = re.compile(r"\[[^\]\s]{2,}\]")
|
| 75 |
CHECKED = ("specialist_call", "specialist_after_guard", "owner_first", "family_precedence", "network", "compute:", "organ:math",
|
| 76 |
-
"op:", "verified:", "algebra:", "deduce:")
|
| 77 |
SESSION_STORES = ("dialogue", "transitions", "_pending_conversion", "_paths")
|
| 78 |
_ADJ = {s: [] for s in SEPH}
|
| 79 |
for _i, (_a, _b) in enumerate(EDGES):
|
|
|
|
| 73 |
# what an answer may stand on and still become state: a citation or source tag, a proof, or a checked route
|
| 74 |
CITED = re.compile(r"\[[^\]\s]{2,}\]")
|
| 75 |
CHECKED = ("specialist_call", "specialist_after_guard", "owner_first", "family_precedence", "network", "compute:", "organ:math",
|
| 76 |
+
"op:", "verified:", "algebra:", "deduce:", "organ:relation>")
|
| 77 |
SESSION_STORES = ("dialogue", "transitions", "_pending_conversion", "_paths")
|
| 78 |
_ADJ = {s: [] for s in SEPH}
|
| 79 |
for _i, (_a, _b) in enumerate(EDGES):
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_conversation_state.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Untrusted reconciliation of legacy conversation caches with delivered replies.
|
| 2 |
+
|
| 3 |
+
This is Python bookkeeping, not a proof or an answer authority. The runtime
|
| 4 |
+
serializes chat calls. Module-global v2 context is borrowed only for one turn;
|
| 5 |
+
committed contexts belong to a runtime instance and a conversation ID.
|
| 6 |
+
"""
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
import math
|
| 9 |
+
import re
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_TOPIC = re.compile(r"^\s*(?:new topic|switching topics|change of topic)\s*:", re.I)
|
| 13 |
+
_SCALAR = re.compile(r"^\s*\[ev:derived\]\s+([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\. In steps:")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def begin(runtime, text, sid):
|
| 17 |
+
cc = sys.modules.get("chat_context_v2")
|
| 18 |
+
stores = getattr(runtime, "_conversation_contexts", None)
|
| 19 |
+
if stores is None:
|
| 20 |
+
stores = runtime._conversation_contexts = {}
|
| 21 |
+
prior = deepcopy(stores.get(sid))
|
| 22 |
+
frame = {"cc": cc, "prior": prior, "sid": sid}
|
| 23 |
+
if cc is not None:
|
| 24 |
+
frame["ambient"] = (sid in cc._STATES, cc._STATES.get(sid))
|
| 25 |
+
cc._STATES[sid] = deepcopy(prior) if prior is not None else cc.Turn()
|
| 26 |
+
values = getattr(runtime, "_last_value", {})
|
| 27 |
+
frame["value"] = (sid in values, values.get(sid))
|
| 28 |
+
if _TOPIC.match(str(text)):
|
| 29 |
+
if cc is not None:
|
| 30 |
+
cc._STATES[sid] = cc.Turn()
|
| 31 |
+
frame["prior"] = None
|
| 32 |
+
values.pop(sid, None)
|
| 33 |
+
getattr(runtime, "_v4_disc", {}).pop(sid, None)
|
| 34 |
+
return frame
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def scalar(reply, speculative, previous):
|
| 38 |
+
"""Only the two known scalar arithmetic routes can populate w29's cache."""
|
| 39 |
+
prov = reply.get("provenance")
|
| 40 |
+
prov = prov if isinstance(prov, dict) else {}
|
| 41 |
+
path = str(reply.get("path", ""))
|
| 42 |
+
if prov.get("kind") == "computation" and prov.get("verified") is True and path in ("compute:verified", "chat:resolved>compute:verified"):
|
| 43 |
+
# The final closed arithmetic route wrote this value itself.
|
| 44 |
+
return speculative
|
| 45 |
+
if path == "organ:math>evaluate" and prov.get("kind") == "organ_math" and prov.get("directive") == "evaluate":
|
| 46 |
+
match = _SCALAR.match(str(reply.get("response", "")))
|
| 47 |
+
if match:
|
| 48 |
+
value = float(match.group(1))
|
| 49 |
+
return value if math.isfinite(value) else None
|
| 50 |
+
if path.startswith("act:"):
|
| 51 |
+
return previous
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def finish(runtime, text, reply, frame, supported):
|
| 56 |
+
sid, cc = frame["sid"], frame["cc"]
|
| 57 |
+
values = getattr(runtime, "_last_value", None)
|
| 58 |
+
if values is None:
|
| 59 |
+
values = runtime._last_value = {}
|
| 60 |
+
previous = frame["value"][1] if frame["value"][0] else None
|
| 61 |
+
value = scalar(reply, values.get(sid), previous) if supported else None
|
| 62 |
+
values.pop(sid, None)
|
| 63 |
+
if value is not None:
|
| 64 |
+
values[sid] = value
|
| 65 |
+
if cc is None:
|
| 66 |
+
return
|
| 67 |
+
prior = frame["prior"]
|
| 68 |
+
cc._STATES[sid] = deepcopy(prior) if prior is not None else cc.Turn()
|
| 69 |
+
if not supported:
|
| 70 |
+
cc._STATES[sid] = cc.Turn()
|
| 71 |
+
elif not str(reply.get("path", "")).startswith("act:"):
|
| 72 |
+
# Direct questions legitimately carry chat_resolved: null. Only a
|
| 73 |
+
# typed rewrite record with a string question overrides the input.
|
| 74 |
+
prov = reply.get("provenance")
|
| 75 |
+
resolved = prov.get("chat_resolved") if isinstance(prov, dict) else None
|
| 76 |
+
question = resolved.get("read_as") if isinstance(resolved, dict) else None
|
| 77 |
+
question = question if isinstance(question, str) and question.strip() else text
|
| 78 |
+
# Clear numerical slots before recognition so an unrecognized final
|
| 79 |
+
# correction cannot silently retain an older number or solved variable.
|
| 80 |
+
cc._STATES[sid].result = None
|
| 81 |
+
cc._STATES[sid].solved = {}
|
| 82 |
+
cc.update(sid, question, reply)
|
| 83 |
+
if value is not None:
|
| 84 |
+
cc._STATES[sid].result = (value, "")
|
| 85 |
+
stores = runtime._conversation_contexts
|
| 86 |
+
if sid not in stores and len(stores) >= cc.STATE_CAP:
|
| 87 |
+
stores.pop(next(iter(stores)))
|
| 88 |
+
stores[sid] = deepcopy(cc._STATES[sid])
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def release(frame):
|
| 92 |
+
cc = frame["cc"]
|
| 93 |
+
if cc is not None:
|
| 94 |
+
had, value = frame["ambient"]
|
| 95 |
+
if had:
|
| 96 |
+
cc._STATES[frame["sid"]] = value
|
| 97 |
+
else:
|
| 98 |
+
cc._STATES.pop(frame["sid"], None)
|
root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_final_outcome.py
CHANGED
|
@@ -10,6 +10,7 @@ from functools import wraps
|
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
import time
|
|
|
|
| 13 |
|
| 14 |
_FRAMES = ContextVar("oracle_final_turns", default=())
|
| 15 |
_CITE = re.compile(r"\[[a-z0-9_]+:?[a-z0-9_.\-]*\]")
|
|
@@ -45,6 +46,7 @@ def final_turn(method):
|
|
| 45 |
"snapshot": self._v4_snapshot(session_id),
|
| 46 |
"started": time.perf_counter(), "first": None,
|
| 47 |
"hits": sum(rc.hits for rc in self._v4_recall())}
|
|
|
|
| 48 |
token = _FRAMES.set(frames + (frame,))
|
| 49 |
try:
|
| 50 |
result = method(self, text, session_id)
|
|
@@ -65,6 +67,7 @@ def final_turn(method):
|
|
| 65 |
raise
|
| 66 |
finally:
|
| 67 |
if outer:
|
|
|
|
| 68 |
_FRAMES.reset(token)
|
| 69 |
|
| 70 |
wrapped._oracle_final_turn = True
|
|
@@ -101,6 +104,7 @@ def finish_turn(runtime, text, session_id, result, frame, stands, append, entry,
|
|
| 101 |
# AN's recorder derives all terms and metrics again from the final reply.
|
| 102 |
runtime._record_turn(text, session_id, reply, frame["started"], prior)
|
| 103 |
getattr(runtime, "_pending_conversion", {}).pop(session_id, None)
|
|
|
|
| 104 |
commit_disc = getattr(runtime, "_v4_commit_disc", None)
|
| 105 |
if commit_disc:
|
| 106 |
commit_disc(text, session_id, reply)
|
|
|
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
import time
|
| 13 |
+
import runtime_conversation_state as conversation
|
| 14 |
|
| 15 |
_FRAMES = ContextVar("oracle_final_turns", default=())
|
| 16 |
_CITE = re.compile(r"\[[a-z0-9_]+:?[a-z0-9_.\-]*\]")
|
|
|
|
| 46 |
"snapshot": self._v4_snapshot(session_id),
|
| 47 |
"started": time.perf_counter(), "first": None,
|
| 48 |
"hits": sum(rc.hits for rc in self._v4_recall())}
|
| 49 |
+
frame["conversation"] = conversation.begin(self, text, session_id)
|
| 50 |
token = _FRAMES.set(frames + (frame,))
|
| 51 |
try:
|
| 52 |
result = method(self, text, session_id)
|
|
|
|
| 67 |
raise
|
| 68 |
finally:
|
| 69 |
if outer:
|
| 70 |
+
conversation.release(frame["conversation"])
|
| 71 |
_FRAMES.reset(token)
|
| 72 |
|
| 73 |
wrapped._oracle_final_turn = True
|
|
|
|
| 104 |
# AN's recorder derives all terms and metrics again from the final reply.
|
| 105 |
runtime._record_turn(text, session_id, reply, frame["started"], prior)
|
| 106 |
getattr(runtime, "_pending_conversion", {}).pop(session_id, None)
|
| 107 |
+
conversation.finish(runtime, text, reply, frame["conversation"], answered and not unsupported)
|
| 108 |
commit_disc = getattr(runtime, "_v4_commit_disc", None)
|
| 109 |
if commit_disc:
|
| 110 |
commit_disc(text, session_id, reply)
|
root/specialists/_gate/combo_runtime_v1.py
CHANGED
|
@@ -238,6 +238,58 @@ QWORDS = set("what what's how who whom whose which where when why is are was wer
|
|
| 238 |
"please tell give name".split())
|
| 239 |
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
def _split_parts(text: str) -> list[str]:
|
| 242 |
"""split a turn at its question marks; never inside `code` or quoted spans (SQL, JSON, strings).
|
| 243 |
Semicolons never split: "10 of 40 deer were tagged; 50 had been tagged ..." is one question."""
|
|
@@ -251,7 +303,16 @@ def _split_parts(text: str) -> list[str]:
|
|
| 251 |
out.append(t[prev:c].strip())
|
| 252 |
prev = c
|
| 253 |
out.append(t[prev:].strip())
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
|
| 257 |
class Combo:
|
|
@@ -340,9 +401,9 @@ class Combo:
|
|
| 340 |
return self._arbitrate(text, self._ask_all(text, session_id), session_id)
|
| 341 |
|
| 342 |
def _segment(self, parts, session_id):
|
| 343 |
-
"""
|
| 344 |
-
|
| 345 |
-
A full question nobody covers is never glued on
|
| 346 |
segs = []
|
| 347 |
ok = lambda x: x[1] is not None or bool(x[2].get("disagree")) # noqa: E731
|
| 348 |
for p in parts:
|
|
@@ -353,13 +414,13 @@ class Combo:
|
|
| 353 |
segs[-1] = (joined,) + self._one(joined, session_id)
|
| 354 |
continue
|
| 355 |
cur = (p,) + self._one(p, session_id)
|
| 356 |
-
if segs and not ok(cur) and
|
| 357 |
joined = segs[-1][0] + " " + p
|
| 358 |
j = (joined,) + self._one(joined, session_id)
|
| 359 |
if ok(j):
|
| 360 |
segs[-1] = j
|
| 361 |
continue
|
| 362 |
-
if segs and not ok(segs[-1]) and
|
| 363 |
joined = segs[-1][0] + " " + p
|
| 364 |
j = (joined,) + self._one(joined, session_id)
|
| 365 |
if ok(j):
|
|
|
|
| 238 |
"please tell give name".split())
|
| 239 |
|
| 240 |
|
| 241 |
+
CONDITION_START = re.compile(
|
| 242 |
+
r"^(?:please\s+)?(?:use|using|assume|assuming|take|given|with|without|ignore|ignoring|neglect|neglecting)\b", re.I)
|
| 243 |
+
CONDITION_NEW_TASK = re.compile(
|
| 244 |
+
r"\b(?:calculate|compute|determine|find|tell|give|show|convert|list|summarize|compare|explain|"
|
| 245 |
+
r"evaluate|solve|plot|draw|write|read|search|estimate|measure|prove|derive|differentiate|integrate|"
|
| 246 |
+
r"derivatives?|integrals?|square root|report|what|who|whom|whose|which|where|when|why|how)\b", re.I)
|
| 247 |
+
DECLARED_CONSTANT = re.compile(
|
| 248 |
+
r"^(?:the\s+)?(?:load|gravity|density|specific heat(?: capacity)?|pressure|volume|mass|"
|
| 249 |
+
r"flow(?: rate)?|temperature|stiffness|efficiency)\s+(?:is|remains|stays)\s+"
|
| 250 |
+
r"(?:constant|steady|fixed)(?:\s+throughout)?\s*[.!]?$", re.I)
|
| 251 |
+
CONDITION_PARAMETER = re.compile(
|
| 252 |
+
r"\b(?:specific heat|heat capacity|gravity|density|efficiency|temperature|pressure|mass|volume|"
|
| 253 |
+
r"flow|stiffness|load|capacity|loss(?:es)?|friction|phase change|constant)\b|\b[a-z]\s*=", re.I)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _condition_prefix(part: str) -> tuple[str, str]:
|
| 257 |
+
"""Separate explicit declarative conditions from a later independent task.
|
| 258 |
+
|
| 259 |
+
Only condition-led sentences without an unquoted question or request verb
|
| 260 |
+
attach backwards. A new question stays separate even when its preceding
|
| 261 |
+
sentence starts with 'Use'. Quoted/code text is never a split boundary.
|
| 262 |
+
This is deliberately a small untrusted English recognizer, not a proof that
|
| 263 |
+
arbitrary prose expresses only conditions.
|
| 264 |
+
"""
|
| 265 |
+
masked = CODE_SPAN.sub(lambda m: "\x00" * len(m.group(0)), part)
|
| 266 |
+
# A 'Given ...' premise before another question normally belongs to that
|
| 267 |
+
# new question; it is only a backwards continuation when no question follows.
|
| 268 |
+
if re.match(r"^(?:please\s+)?given\b", masked, re.I) and "?" in masked:
|
| 269 |
+
return "", part
|
| 270 |
+
ends = [m.end() for m in re.finditer(r"[.!]\s+", masked)] + [len(part)]
|
| 271 |
+
start, consumed = 0, 0
|
| 272 |
+
for end in ends:
|
| 273 |
+
sentence = masked[start:end].strip()
|
| 274 |
+
if not sentence:
|
| 275 |
+
start = end
|
| 276 |
+
continue
|
| 277 |
+
if not (CONDITION_START.match(sentence) or DECLARED_CONSTANT.fullmatch(sentence)) or "?" in sentence or CONDITION_NEW_TASK.search(sentence):
|
| 278 |
+
break
|
| 279 |
+
if re.match(r"^(?:please\s+)?(?:use|using|take)\b", sentence, re.I) and not CONDITION_PARAMETER.search(sentence):
|
| 280 |
+
break # "Take the square root ..." / "Use a calculator ..." is a task, not a model input.
|
| 281 |
+
consumed = end
|
| 282 |
+
start = end
|
| 283 |
+
return part[:consumed].strip(), part[consumed:].strip()
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _short_followup_fragment(text: str) -> bool:
|
| 287 |
+
"""A small explicit elliptical vocabulary; brevity alone never implies dependence."""
|
| 288 |
+
return bool(re.fullmatch(
|
| 289 |
+
r"\s*(?:and\s+)?(?:how\s+(?:many|much|long|far)|what(?:\s+else)?|who|when|where|why|which(?:\s+one)?)\s*[?!.]*\s*",
|
| 290 |
+
text, re.I))
|
| 291 |
+
|
| 292 |
+
|
| 293 |
def _split_parts(text: str) -> list[str]:
|
| 294 |
"""split a turn at its question marks; never inside `code` or quoted spans (SQL, JSON, strings).
|
| 295 |
Semicolons never split: "10 of 40 deer were tagged; 50 had been tagged ..." is one question."""
|
|
|
|
| 303 |
out.append(t[prev:c].strip())
|
| 304 |
prev = c
|
| 305 |
out.append(t[prev:].strip())
|
| 306 |
+
grouped = []
|
| 307 |
+
for part in out:
|
| 308 |
+
if not part:
|
| 309 |
+
continue
|
| 310 |
+
condition, remainder = _condition_prefix(part) if grouped else ("", part)
|
| 311 |
+
if condition:
|
| 312 |
+
grouped[-1] += " " + condition
|
| 313 |
+
if remainder:
|
| 314 |
+
grouped.append(remainder)
|
| 315 |
+
return grouped
|
| 316 |
|
| 317 |
|
| 318 |
class Combo:
|
|
|
|
| 401 |
return self._arbitrate(text, self._ask_all(text, session_id), session_id)
|
| 402 |
|
| 403 |
def _segment(self, parts, session_id):
|
| 404 |
+
"""An explicit elliptical fragment that no organ takes alone may rejoin its neighbour
|
| 405 |
+
("... in 5-card poker? How many?"). Pronoun backreferences are handled separately.
|
| 406 |
+
A full question nobody covers is never glued on just because it is short."""
|
| 407 |
segs = []
|
| 408 |
ok = lambda x: x[1] is not None or bool(x[2].get("disagree")) # noqa: E731
|
| 409 |
for p in parts:
|
|
|
|
| 414 |
segs[-1] = (joined,) + self._one(joined, session_id)
|
| 415 |
continue
|
| 416 |
cur = (p,) + self._one(p, session_id)
|
| 417 |
+
if segs and not ok(cur) and _short_followup_fragment(p):
|
| 418 |
joined = segs[-1][0] + " " + p
|
| 419 |
j = (joined,) + self._one(joined, session_id)
|
| 420 |
if ok(j):
|
| 421 |
segs[-1] = j
|
| 422 |
continue
|
| 423 |
+
if segs and not ok(segs[-1]) and _short_followup_fragment(segs[-1][0]):
|
| 424 |
joined = segs[-1][0] + " " + p
|
| 425 |
j = (joined,) + self._one(joined, session_id)
|
| 426 |
if ok(j):
|
root/specialists/energy-v3/organ_energy_v1.py
CHANGED
|
@@ -2,9 +2,11 @@
|
|
| 2 |
"""organ_energy_v1 - household energy arithmetic, every step shown: energy used by a device (W x h -> kWh), what it costs
|
| 3 |
at a stated tariff, how long a battery runs a load (Wh, or V x Ah), a solar array's daily yield (kW x peak-sun-hours,
|
| 4 |
x a stated derate, otherwise said to be before losses), a payback period, and the yearly saving from swapping one
|
| 5 |
-
device for another. It never assumes a tariff, a price or sun-hours it was not given. Returns {kind, status, response, value}.
|
|
|
|
| 6 |
from __future__ import annotations
|
| 7 |
|
|
|
|
| 8 |
import re
|
| 9 |
|
| 10 |
N = r"(\d+(?:,\d{3})*(?:\.\d+)?)"
|
|
@@ -60,6 +62,150 @@ def money(x):
|
|
| 60 |
return "$%.2f" % x if x < 1000 else "$%s" % format(round(x), ",")
|
| 61 |
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
class EnergyOrgan:
|
| 64 |
def _say(self, kind, text, value=None, status="answered"):
|
| 65 |
return {"kind": kind, "status": status, "response": text, "value": value}
|
|
@@ -70,6 +216,63 @@ class EnergyOrgan:
|
|
| 70 |
def answer(self, text, session_id="default"):
|
| 71 |
t = " ".join(str(text or "").split())
|
| 72 |
low = t.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
# payback
|
| 74 |
if re.search(r"\bpayback|\bpay (?:for )?itself|\bbreak even\b|\bpay back\b", low):
|
| 75 |
cost = re.search(r"\$\s*" + N + r"(?:\s*(k))?", low)
|
|
@@ -83,26 +286,17 @@ class EnergyOrgan:
|
|
| 83 |
y = c / s
|
| 84 |
return self._say("payback", "The payback period is %s years (%s ÷ %s saved per year), ignoring price changes and financing." % (g(y), money(c), money(s)), y)
|
| 85 |
# battery runtime
|
| 86 |
-
if re.search(r"\
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
else:
|
| 98 |
-
return self._need("the battery's capacity (Wh, or volts and amp-hours)")
|
| 99 |
-
if not load:
|
| 100 |
-
return self._need("the load in watts")
|
| 101 |
-
w = num(load.group(1))
|
| 102 |
-
if w <= 0:
|
| 103 |
-
return self._say("impossible", "A load must draw more than 0 W.", status="withheld")
|
| 104 |
-
hrs = cap / w
|
| 105 |
-
return self._say("runtime", "The runtime is about %s hours (%s ÷ %s W), before inverter and depth-of-discharge losses." % (g(hrs), capnote, g(w)), hrs)
|
| 106 |
# solar daily yield
|
| 107 |
if re.search(r"\bsolar\b|\bpv\b|\bpanels?\b", low) and re.search(r"\bsun ?hours?\b|\bpeak sun\b|\bsunlight hours?\b", low):
|
| 108 |
k = re.search(N + r"\s*(kw|kilowatts?|w|watts?)\b(?!\s*h\b)", low)
|
|
|
|
| 2 |
"""organ_energy_v1 - household energy arithmetic, every step shown: energy used by a device (W x h -> kWh), what it costs
|
| 3 |
at a stated tariff, how long a battery runs a load (Wh, or V x Ah), a solar array's daily yield (kW x peak-sun-hours,
|
| 4 |
x a stated derate, otherwise said to be before losses), a payback period, and the yearly saving from swapping one
|
| 5 |
+
device for another. It never assumes a tariff, a price or sun-hours it was not given. Returns {kind, status, response, value}.
|
| 6 |
+
The English recognizer and Python calculations are untrusted; these are explicit constant-energy models, not proofs."""
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import math
|
| 10 |
import re
|
| 11 |
|
| 12 |
N = r"(\d+(?:,\d{3})*(?:\.\d+)?)"
|
|
|
|
| 62 |
return "$%.2f" % x if x < 1000 else "$%s" % format(round(x), ",")
|
| 63 |
|
| 64 |
|
| 65 |
+
QN = r"(?<![\w.,])([+-]?(?:\d+(?:,\d{3})*(?:\.\d+)?|\.\d+))"
|
| 66 |
+
POWER = re.compile(QN + r"\s*(kw|kilowatts?|w|watts?)\b")
|
| 67 |
+
DEVICE = r"(?:load|device|appliance|fridge|light|laptop|lamp|fan|tv|router|pump|heater|bulb|motor)\b"
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def check_energy_conditions(low):
|
| 71 |
+
for m in re.finditer(r"(?<![\w.,])[+-]?\d[\d,.]*", low):
|
| 72 |
+
token = m[0].rstrip(".")
|
| 73 |
+
if "," in token and not re.fullmatch(r"[+-]?\d{1,3}(?:,\d{3})+(?:\.\d+)?", token):
|
| 74 |
+
raise ValueError("numbers with unambiguous decimal points and three-digit comma groups")
|
| 75 |
+
if re.search(r"\bbatteries\b|\b(?:loads|devices|appliances|fridges|lights|laptops|lamps|fans|routers|pumps|heaters|bulbs|motors)\b", low):
|
| 76 |
+
raise ValueError("one battery source and one total consumed load; multiple sources or loads need an explicit combined model")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def one_value(values, description):
|
| 80 |
+
if not values:
|
| 81 |
+
raise ValueError(description)
|
| 82 |
+
if len(set(values)) != 1:
|
| 83 |
+
raise ValueError("one unambiguous " + description)
|
| 84 |
+
value = values[0]
|
| 85 |
+
if not math.isfinite(value) or value < 0:
|
| 86 |
+
raise ValueError("a finite nonnegative " + description)
|
| 87 |
+
return value
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def capacity_wh(low):
|
| 91 |
+
check_energy_conditions(low)
|
| 92 |
+
values = [num(m[1]) * (1000 if m[2] == "kwh" else 1)
|
| 93 |
+
for m in re.finditer(QN + r"\s*(kwh|wh)\b", low)]
|
| 94 |
+
if values:
|
| 95 |
+
energy = one_value(values, "battery energy in Wh or kWh")
|
| 96 |
+
if re.search(QN + r"\s*ah\b", low):
|
| 97 |
+
volts = [num(m[1]) for m in re.finditer(QN + r"\s*v(?:olts?)?\b", low)]
|
| 98 |
+
amps = [num(m[1]) for m in re.finditer(QN + r"\s*ah\b", low)]
|
| 99 |
+
equivalent = one_value(volts, "battery voltage") * one_value(amps, "battery capacity in Ah")
|
| 100 |
+
if not math.isclose(energy, equivalent, rel_tol=1e-9, abs_tol=1e-9):
|
| 101 |
+
raise ValueError("consistent Wh and voltage/amp-hour capacity values")
|
| 102 |
+
return energy
|
| 103 |
+
volts = [num(m[1]) for m in re.finditer(QN + r"\s*v(?:olts?)?\b", low)]
|
| 104 |
+
amps = [num(m[1]) for m in re.finditer(QN + r"\s*ah\b", low)]
|
| 105 |
+
return one_value([one_value(volts, "battery voltage") * one_value(amps, "battery capacity in Ah")], "battery energy")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def load_and_rating(low):
|
| 109 |
+
"""Untrusted grammar: inverter rating is a constraint, never consumed power."""
|
| 110 |
+
loads, ratings, unbound = [], [], []
|
| 111 |
+
for m in POWER.finditer(low):
|
| 112 |
+
value = num(m[1]) * (1000 if m[2].startswith("k") else 1)
|
| 113 |
+
before, after = low[max(0, m.start()-55):m.start()], low[m.end():m.end()+55]
|
| 114 |
+
is_rating = (re.match(r"\s*(?:rated\s+)?inverter\b", after)
|
| 115 |
+
or re.search(r"\binverter(?:\s+(?:rating|rated|is|of|at|for|has|a))*(?:\s*[:=])?\s*$", before)
|
| 116 |
+
or re.match(r"\s*(?:inverter\s+)?(?:rating|rated)\b", after))
|
| 117 |
+
is_load = (re.match(r"\s*(?:(?:ac|dc|constant)\s+)?" + DEVICE, after)
|
| 118 |
+
or re.search(r"\b(?:load|device|appliance)\s*(?:(?:draws|uses|is|of|at)\s*)?[:=]?\s*$", before))
|
| 119 |
+
if is_rating and not is_load:
|
| 120 |
+
ratings.append(value)
|
| 121 |
+
elif is_load and not is_rating:
|
| 122 |
+
loads.append(value)
|
| 123 |
+
else:
|
| 124 |
+
unbound.append(value)
|
| 125 |
+
if not loads and len(unbound) == 1 and not ratings:
|
| 126 |
+
loads, unbound = unbound, [] # the legacy single-load form
|
| 127 |
+
if unbound:
|
| 128 |
+
raise ValueError("the consumed load and inverter rating labelled separately")
|
| 129 |
+
if len(loads) != 1:
|
| 130 |
+
raise ValueError("one total consumed load in watts")
|
| 131 |
+
load = one_value(loads, "consumed load in watts")
|
| 132 |
+
rating = one_value(ratings, "inverter rating in watts") if ratings else None
|
| 133 |
+
if load <= 0:
|
| 134 |
+
raise ValueError("a consumed load greater than zero watts")
|
| 135 |
+
if rating is not None and load > rating:
|
| 136 |
+
raise ValueError("a load within the stated inverter rating")
|
| 137 |
+
return load, rating
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def energy_factors(low, storage=False):
|
| 141 |
+
"""Bind each stated fraction to its role, or reject an unbound condition.
|
| 142 |
+
|
| 143 |
+
These are simple constant-energy models, not battery chemistry or a proof.
|
| 144 |
+
Efficiency and loss are alternative descriptions of the same conversion;
|
| 145 |
+
depth of discharge is a separate capacity fraction.
|
| 146 |
+
"""
|
| 147 |
+
check_energy_conditions(low)
|
| 148 |
+
if (not storage and re.search(r"\b(?:charging|storage)\s+efficiency\b", low)) or (storage and re.search(r"\binverter\s+efficiency\b", low)):
|
| 149 |
+
raise ValueError("an efficiency bound to the requested conversion (charging and load supply are different stages)")
|
| 150 |
+
groups = {
|
| 151 |
+
"efficiency": r"(?:(?:inverter|overall|conversion|charging|storage|battery)\s+)?efficiency",
|
| 152 |
+
"depth": r"(?:depth of discharge|usable (?:depth|capacity|fraction)|discharge depth)",
|
| 153 |
+
"loss": r"(?:(?:inverter|conversion|charging|storage|system)\s+)?loss(?:es)?",
|
| 154 |
+
}
|
| 155 |
+
if storage:
|
| 156 |
+
groups["stored"] = r"(?:stored fraction|storage fraction)"
|
| 157 |
+
found, spans = {}, []
|
| 158 |
+
numeric = r"([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*(%?)"
|
| 159 |
+
for role, label in groups.items():
|
| 160 |
+
patterns = [r"(?<![\w.])" + numeric + r"\s+" + label + r"\b",
|
| 161 |
+
r"\b" + label + r"\s*(?:(?:of|is|=|:)\s*)?" + numeric + r"(?![\w.])"]
|
| 162 |
+
if role == "efficiency":
|
| 163 |
+
patterns.append(r"(?<![\w.])" + numeric + r"\s+efficient(?:\s+inverter)?\b")
|
| 164 |
+
if role == "stored":
|
| 165 |
+
patterns.append(r"\b(?:stores|store|retains|retain)\s+" + numeric + r"\s+of\b")
|
| 166 |
+
for pattern in patterns:
|
| 167 |
+
for m in re.finditer(pattern, low):
|
| 168 |
+
f = float(m[1]) / (100 if m[2] else 1)
|
| 169 |
+
if not math.isfinite(f) or not 0 <= f <= 1:
|
| 170 |
+
raise ValueError("efficiency, loss and usable fractions between 0 and 1 (or 0% and 100%)")
|
| 171 |
+
found.setdefault(role, []).append(f)
|
| 172 |
+
spans.append(m.span())
|
| 173 |
+
# Every percentage and every adjustment term must be accounted for. Ignoring
|
| 174 |
+
# losses is an explicit ideal-model assumption, not an unknown loss factor.
|
| 175 |
+
masked = list(low)
|
| 176 |
+
for start, end in spans:
|
| 177 |
+
masked[start:end] = " " * (end-start)
|
| 178 |
+
rest = "".join(masked)
|
| 179 |
+
rest = re.sub(r"\b(?:ignoring|ignore|without|no|neglecting)\s+(?:(?:inverter|conversion)\s+)?loss(?:es)?\b", "", rest)
|
| 180 |
+
if re.search(r"%|\befficien\w*|\bloss(?:es)?\b|\bdepth of discharge\b|\busable (?:depth|capacity|fraction)\b|\b(?:idle|standby|variable|surge|peukert|temperature|degradation|derate|reserve)\b|\bperformance ratio\b|\b(?:half full|partly charged|partially charged|state of charge)\b", rest):
|
| 181 |
+
raise ValueError("each loss, efficiency, usable-capacity or variable-load condition explicitly bound to this model")
|
| 182 |
+
factors = {role: one_value(values, role + " fraction") for role, values in found.items()}
|
| 183 |
+
efficiency = factors.get("efficiency", 1)
|
| 184 |
+
if "loss" in factors:
|
| 185 |
+
retained = 1 - factors["loss"]
|
| 186 |
+
if "efficiency" in factors and not math.isclose(efficiency, retained, abs_tol=1e-12):
|
| 187 |
+
raise ValueError("consistent efficiency and loss fractions")
|
| 188 |
+
efficiency = retained
|
| 189 |
+
if "stored" in factors:
|
| 190 |
+
if "efficiency" in factors or "loss" in factors:
|
| 191 |
+
if not math.isclose(efficiency, factors["stored"], abs_tol=1e-12):
|
| 192 |
+
raise ValueError("one unambiguous stored fraction (not two unexplained conversion stages)")
|
| 193 |
+
efficiency = factors["stored"]
|
| 194 |
+
return efficiency, factors.get("depth", 1), bool(found)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def duration_hours(low):
|
| 198 |
+
durations = [num(m[1]) for m in re.finditer(QN + r"\s*(?:hours?|hrs?|h|minutes?)\b", low)]
|
| 199 |
+
if any(not math.isfinite(x) or x < 0 for x in durations):
|
| 200 |
+
raise ValueError("a finite nonnegative load duration")
|
| 201 |
+
if len(durations) > 1:
|
| 202 |
+
raise ValueError("one unambiguous load duration")
|
| 203 |
+
duration = hours(low)
|
| 204 |
+
if duration is not None and not math.isfinite(duration[0]):
|
| 205 |
+
raise ValueError("a finite load duration")
|
| 206 |
+
return duration
|
| 207 |
+
|
| 208 |
+
|
| 209 |
class EnergyOrgan:
|
| 210 |
def _say(self, kind, text, value=None, status="answered"):
|
| 211 |
return {"kind": kind, "status": status, "response": text, "value": value}
|
|
|
|
| 216 |
def answer(self, text, session_id="default"):
|
| 217 |
t = " ".join(str(text or "").split())
|
| 218 |
low = t.lower()
|
| 219 |
+
# Explicit continuations. The goal is read from the question, not from
|
| 220 |
+
# a noun in the initial conditions ("initially stores" is not a goal).
|
| 221 |
+
question = re.search(r"\b(?:what|how much)\b[^?]*\??$", low)
|
| 222 |
+
goal = question.group(0) if question else ""
|
| 223 |
+
remaining = re.search(r"\bremain\w*\b|\bleft\b", goal)
|
| 224 |
+
stored = re.search(r"\bstored\b|\bstore\b", goal)
|
| 225 |
+
if remaining and re.search(r"\bbatter(?:y|ies)\b", low):
|
| 226 |
+
try:
|
| 227 |
+
if not re.search(r"\binitial\w*\b|\bstarts? with\b|\bfully charged\b", low):
|
| 228 |
+
raise ValueError("the initially stored energy or an explicit fully charged battery")
|
| 229 |
+
initial = capacity_wh(low)
|
| 230 |
+
load, rating = load_and_rating(low)
|
| 231 |
+
efficiency, depth, adjusted = energy_factors(low)
|
| 232 |
+
duration = duration_hours(low)
|
| 233 |
+
if duration is None:
|
| 234 |
+
raise ValueError("the duration of the constant load")
|
| 235 |
+
if efficiency == 0 and duration[0] > 0:
|
| 236 |
+
raise ValueError("positive conversion efficiency to supply this load")
|
| 237 |
+
spent = load * duration[0] / efficiency if efficiency else 0
|
| 238 |
+
if spent > initial * depth + 1e-10:
|
| 239 |
+
raise ValueError("a duration within the battery's available energy at the stated load and factors")
|
| 240 |
+
base = initial * depth if re.search(r"\busable\b", goal) else initial
|
| 241 |
+
value = (base - spent) / 1000
|
| 242 |
+
return self._say("energy", "The %sremaining energy is %s kWh (%s Wh initially%s − %s Wh withdrawn; load %s W × %s, conversion efficiency %s)." % (
|
| 243 |
+
"usable " if base != initial else "", g(value), g(initial), " × %s usable fraction" % g(depth) if base != initial else "", g(spent), g(load), duration[1], g(efficiency)), value)
|
| 244 |
+
except ValueError as error:
|
| 245 |
+
return self._need(str(error))
|
| 246 |
+
if stored and re.search(r"\b(?:solar|panel|batter(?:y|ies)|charging)\b", low):
|
| 247 |
+
try:
|
| 248 |
+
efficiency, depth, adjusted = energy_factors(low, storage=True)
|
| 249 |
+
if depth != 1:
|
| 250 |
+
raise ValueError("charging/storage efficiency; discharge depth does not specify stored energy")
|
| 251 |
+
if not adjusted and not re.search(r"\b(?:ignoring|without|no|neglecting)\s+loss", low):
|
| 252 |
+
raise ValueError("the fraction of generated/input energy stored, or an explicit no-loss assumption")
|
| 253 |
+
powers = list(POWER.finditer(low))
|
| 254 |
+
duration = duration_hours(low)
|
| 255 |
+
sun = re.search(N + r"\s*(?:peak\s+)?sun[ -]?hours?\b", low)
|
| 256 |
+
if sun:
|
| 257 |
+
duration = (num(sun[1]), "%s peak sun hours" % sun[1])
|
| 258 |
+
if len(powers) == 1 and duration:
|
| 259 |
+
power = one_value([num(powers[0][1]) * (1000 if powers[0][2].startswith("k") else 1)], "input/generating power")
|
| 260 |
+
panel_count = re.search(QN + r"\s+panels?\b", low)
|
| 261 |
+
if panel_count:
|
| 262 |
+
if not re.search(r"\beach\b|\bper panel\b", low):
|
| 263 |
+
raise ValueError("whether panel power is per panel or for the whole array")
|
| 264 |
+
power *= one_value([num(panel_count[1])], "panel count")
|
| 265 |
+
generated = power * duration[0] / 1000
|
| 266 |
+
elif not powers:
|
| 267 |
+
generated = capacity_wh(low) / 1000
|
| 268 |
+
else:
|
| 269 |
+
raise ValueError("one input/generating power and duration, or one stated input energy")
|
| 270 |
+
if re.search(r"\binitial\w*|\bcapacity|\bfull\b", low):
|
| 271 |
+
raise ValueError("a charging plan with initial charge and capacity bounds; this calculation gives newly stored energy only")
|
| 272 |
+
value = one_value([generated * efficiency], "newly stored energy")
|
| 273 |
+
return self._say("energy", "The newly stored energy is %s kWh (%s kWh generated/input energy × %s storage fraction)." % (g(value), g(generated), g(efficiency)), value)
|
| 274 |
+
except ValueError as error:
|
| 275 |
+
return self._need(str(error))
|
| 276 |
# payback
|
| 277 |
if re.search(r"\bpayback|\bpay (?:for )?itself|\bbreak even\b|\bpay back\b", low):
|
| 278 |
cost = re.search(r"\$\s*" + N + r"(?:\s*(k))?", low)
|
|
|
|
| 286 |
y = c / s
|
| 287 |
return self._say("payback", "The payback period is %s years (%s ÷ %s saved per year), ignoring price changes and financing." % (g(y), money(c), money(s)), y)
|
| 288 |
# battery runtime
|
| 289 |
+
if re.search(r"\bbatter(?:y|ies)|\bpower bank\b|\bups\b", low) and re.search(r"\bhow long\b|\bruntime\b|\brun time\b|\blast\b", low):
|
| 290 |
+
try:
|
| 291 |
+
cap = capacity_wh(low)
|
| 292 |
+
w, rating = load_and_rating(low)
|
| 293 |
+
efficiency, depth, adjusted = energy_factors(low)
|
| 294 |
+
hrs = one_value([cap * efficiency * depth / w], "runtime")
|
| 295 |
+
note = "at the stated conversion and usable-capacity factors" if adjusted else "before inverter and depth-of-discharge losses"
|
| 296 |
+
return self._say("runtime", "The runtime is about %s hours (%s Wh × %s conversion efficiency × %s usable fraction ÷ %s W load), %s%s." % (
|
| 297 |
+
g(hrs), g(cap), g(efficiency), g(depth), g(w), note, "; load is within the %s W inverter rating" % g(rating) if rating is not None else ""), hrs)
|
| 298 |
+
except ValueError as error:
|
| 299 |
+
return self._need(str(error))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
# solar daily yield
|
| 301 |
if re.search(r"\bsolar\b|\bpv\b|\bpanels?\b", low) and re.search(r"\bsun ?hours?\b|\bpeak sun\b|\bsunlight hours?\b", low):
|
| 302 |
k = re.search(N + r"\s*(kw|kilowatts?|w|watts?)\b(?!\s*h\b)", low)
|
root/specialists/geometry-v4/manifest.json
CHANGED
|
@@ -45,7 +45,7 @@
|
|
| 45 |
"underspecified",
|
| 46 |
"impossible"
|
| 47 |
],
|
| 48 |
-
"context_regex": "\\b(?:
|
| 49 |
},
|
| 50 |
"panel": {
|
| 51 |
"path": "__ORACLE7_ROOT__/specialists/geometry-v4/geometry_panel_v3.json",
|
|
@@ -73,4 +73,4 @@
|
|
| 73 |
},
|
| 74 |
"notes": "v4 = v3 + imperative verbs (compute / calculate / determine / give / work out) read as 'find' for the hypotenuse. Judged under spec_gate_v2 on a FRESH panel (new seeds, new phrasings both splits including the class). Same author - disclosed. Earlier notes: v3 = v1 + narrowing (v2: word problems to the head; no false 'I need X' when numbers are present - the full-core sweep found v1 taking 17 core word problems incl. a confident wrong 10.95 for 5) + two reading classes v1 missed (sides stated with a verb, 'sides are a, b and c' - all 8 v1 held-out misses; a hypotenuse value stated after the verb). Judged under spec_gate_v2 on a FRESH panel v2 (new seeds, new phrasings both splits, new declines). Same author - disclosed. Deliberately does NOT read '10 feet across' as a diameter: those core textbook problems use pi = 3.14.",
|
| 75 |
"supersedes": "geometry-v3 (ADMIT gate v2, 93/100; 7 misses = imperative 'Compute the hypotenuse')"
|
| 76 |
-
}
|
|
|
|
| 45 |
"underspecified",
|
| 46 |
"impossible"
|
| 47 |
],
|
| 48 |
+
"context_regex": "\\b(?:circles?|circular|spheres?|cylinders?|cones?|cubes?|rectangles?|rectangular|squares?|triangles?|box|boxes|cuboids?|hypotenuse|polygons?|pentagons?|hexagons?|heptagons?|octagons?|nonagons?|decagons?|quadrilaterals?)\\b"
|
| 49 |
},
|
| 50 |
"panel": {
|
| 51 |
"path": "__ORACLE7_ROOT__/specialists/geometry-v4/geometry_panel_v3.json",
|
|
|
|
| 73 |
},
|
| 74 |
"notes": "v4 = v3 + imperative verbs (compute / calculate / determine / give / work out) read as 'find' for the hypotenuse. Judged under spec_gate_v2 on a FRESH panel (new seeds, new phrasings both splits including the class). Same author - disclosed. Earlier notes: v3 = v1 + narrowing (v2: word problems to the head; no false 'I need X' when numbers are present - the full-core sweep found v1 taking 17 core word problems incl. a confident wrong 10.95 for 5) + two reading classes v1 missed (sides stated with a verb, 'sides are a, b and c' - all 8 v1 held-out misses; a hypotenuse value stated after the verb). Judged under spec_gate_v2 on a FRESH panel v2 (new seeds, new phrasings both splits, new declines). Same author - disclosed. Deliberately does NOT read '10 feet across' as a diameter: those core textbook problems use pi = 3.14.",
|
| 75 |
"supersedes": "geometry-v3 (ADMIT gate v2, 93/100; 7 misses = imperative 'Compute the hypotenuse')"
|
| 76 |
+
}
|
root/specialists/geometry-v4/organ_geometry_v4.py
CHANGED
|
@@ -1,16 +1,189 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""organ_geometry_v4 - organ_geometry_v3 (admitted 2026-09-29, held-out 93/100) plus imperative verbs: all 7 v3 held-out
|
| 3 |
misses were "Compute the hypotenuse of a right triangle whose legs are a and b." (v1's reader knew only find / what is /
|
| 4 |
-
how long is). Compute / calculate / determine / give / work out now read as "find". Everything else v3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from __future__ import annotations
|
| 6 |
|
| 7 |
import re
|
|
|
|
| 8 |
|
| 9 |
from organ_geometry_v3 import GeometryOrganV3
|
|
|
|
| 10 |
|
| 11 |
IMPERATIVE = re.compile(r"\b(?:compute|calculate|determine|give|work out|get)\s+(?=the (?:length of the )?hypotenuse\b)", re.I)
|
| 12 |
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
class GeometryOrganV4(GeometryOrganV3):
|
| 15 |
def answer(self, text, session_id="default"):
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""organ_geometry_v4 - organ_geometry_v3 (admitted 2026-09-29, held-out 93/100) plus imperative verbs: all 7 v3 held-out
|
| 3 |
misses were "Compute the hypotenuse of a right triangle whose legs are a and b." (v1's reader knew only find / what is /
|
| 4 |
+
how long is). Compute / calculate / determine / give / work out now read as "find". Everything else v3.
|
| 5 |
+
|
| 6 |
+
This Python dimensional reader is untrusted; it grants no proof or admission authority.
|
| 7 |
+
Explicit same-unit dimensions retain their units, bound rectangles may normalize mixed
|
| 8 |
+
length units, and inherited formulas decline unsupported or inconsistent dimensions.
|
| 9 |
+
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
import re
|
| 13 |
+
import math
|
| 14 |
|
| 15 |
from organ_geometry_v3 import GeometryOrganV3
|
| 16 |
+
from organ_geometry_v1 import g
|
| 17 |
|
| 18 |
IMPERATIVE = re.compile(r"\b(?:compute|calculate|determine|give|work out|get)\s+(?=the (?:length of the )?hypotenuse\b)", re.I)
|
| 19 |
|
| 20 |
|
| 21 |
+
# This organ remains standalone: its dimensional reader must not depend on the
|
| 22 |
+
# head's import path or turn an unsupported binding into a legacy raw product.
|
| 23 |
+
_LENGTHS = {}
|
| 24 |
+
for _unit, _scale, _names in (
|
| 25 |
+
("mm", .001, "mm millimeter millimeters"), ("cm", .01, "cm centimeter centimeters"),
|
| 26 |
+
("m", 1., "m meter meters"), ("km", 1000., "km kilometer kilometers"),
|
| 27 |
+
("in", .0254, "in inch inches"), ("ft", .3048, "ft foot feet"),
|
| 28 |
+
("yd", .9144, "yd yard yards"), ("mi", 1609.344, "mi mile miles"),
|
| 29 |
+
):
|
| 30 |
+
for _name in _names.split():
|
| 31 |
+
_LENGTHS[_name] = (_unit, _scale)
|
| 32 |
+
_NUMBER = r"(\d+(?:\.\d+)?)"
|
| 33 |
+
_FIELDS = re.compile(r"\b(length|width|radius|diameter|height|side|edge|base|leg|hypotenuse)\s*"
|
| 34 |
+
r"(?:(?:of|is|equals|exactly|measures|=|:)\s*)*" + _NUMBER, re.I)
|
| 35 |
+
_NUMBERS = re.compile(r"(?<![\w.+-])" + _NUMBER + r"(?!\d|\.\d)")
|
| 36 |
+
_POWERS = {"rect_area": 2, "rect_perimeter": 1, "circle_area": 2, "circumference": 1,
|
| 37 |
+
"sphere_volume": 3, "sphere_surface": 2, "cylinder_volume": 3, "cylinder_surface": 2,
|
| 38 |
+
"cone_volume": 3, "cube_volume": 3, "cube_surface": 2, "box_volume": 3,
|
| 39 |
+
"square_area": 2, "square_perimeter": 1, "tri_area": 2, "heron": 2,
|
| 40 |
+
"hypotenuse": 1, "leg": 1}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _unit_at(tail, strict=False):
|
| 44 |
+
tail = tail.lstrip()
|
| 45 |
+
if re.match(r"in\s+(?:the|this|a)\s+(?:diagram|drawing|figure)\b", tail):
|
| 46 |
+
return None, True, None
|
| 47 |
+
word = re.match(r"([^\W\d_]+)", tail)
|
| 48 |
+
if not word:
|
| 49 |
+
return None, False, None
|
| 50 |
+
token = word.group(1)
|
| 51 |
+
if token in _LENGTHS:
|
| 52 |
+
rest = tail[word.end():].lstrip()
|
| 53 |
+
if re.match(r"(?:\^|\d|[²³]|/|per\b)", rest):
|
| 54 |
+
return None, False, "These dimensions require length units, not powered or compound units."
|
| 55 |
+
if token == "in" and rest and not re.match(r"[.,;:?!)]|(?:and|long|wide|tall|high)\b", rest):
|
| 56 |
+
return None, False, "I cannot tell whether 'in' denotes inches or a preposition here. Please spell out the length unit."
|
| 57 |
+
return _LENGTHS[token], False, None
|
| 58 |
+
if strict and token not in ("and", "or", "by", "x", "units", "unit", "long", "wide", "high", "tall"):
|
| 59 |
+
return None, False, "I cannot bind the stated length unit '%s'; please use an explicit supported length unit." % token
|
| 60 |
+
return None, False, None
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _dimensions(t):
|
| 64 |
+
fields, positions, units, diagram = {}, set(), [], False
|
| 65 |
+
for match in _FIELDS.finditer(t):
|
| 66 |
+
role, raw = match.group(1), match.group(2)
|
| 67 |
+
unit, drawn, error = _unit_at(t[match.end():], strict=True)
|
| 68 |
+
if error:
|
| 69 |
+
return None, None, False, error
|
| 70 |
+
record = (float(raw), unit)
|
| 71 |
+
if role in fields:
|
| 72 |
+
previous = fields[role]
|
| 73 |
+
if (previous[1] is None) != (unit is None) or previous[0] * (previous[1][1] if previous[1] else 1) != record[0] * (unit[1] if unit else 1):
|
| 74 |
+
return None, None, False, "The supplied %s values contradict each other; please supply one consistent measurement." % role
|
| 75 |
+
fields[role] = record
|
| 76 |
+
positions.add(match.start(2))
|
| 77 |
+
units.append(unit)
|
| 78 |
+
diagram = diagram or drawn
|
| 79 |
+
# A unit or diagram dimension not bound to an explicit role cannot be
|
| 80 |
+
# silently fed to the old first-number reader.
|
| 81 |
+
for number in _NUMBERS.finditer(t):
|
| 82 |
+
unit, drawn, error = _unit_at(t[number.end():])
|
| 83 |
+
if error:
|
| 84 |
+
return None, None, False, error
|
| 85 |
+
if (unit or drawn) and number.start(1) not in positions:
|
| 86 |
+
return None, None, False, "I cannot bind every supplied length to a geometry role; please name each dimension explicitly."
|
| 87 |
+
if (any(units) or diagram) and number.start(1) not in positions:
|
| 88 |
+
# Explicit apartment identifiers are not shape dimensions. Other
|
| 89 |
+
# unused numbers (including a second '5 by 6' tuple) remain ambiguous.
|
| 90 |
+
if not re.search(r"\bapartment\s*$", t[:number.start(1)]):
|
| 91 |
+
return None, None, False, "I cannot account for every number in this dimensional request; please clarify the extra dimensions or constraints."
|
| 92 |
+
return fields, units, diagram, None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _request_issue(t):
|
| 96 |
+
asking = re.split(r"(?<=[.!?])\s+", t)[-1]
|
| 97 |
+
if len(set(re.findall(r"\b(?:area|perimeter|volume|circumference)\b", asking))) > 1:
|
| 98 |
+
return "I can calculate one geometry measure at a time; please ask for the measures separately."
|
| 99 |
+
if re.search(r"\b(?:rectangles|rooms|circles|spheres|cylinders|cones|cubes|boxes|triangles|squares|cuboids)\b", t) or re.search(r"\b(?:their|both)\b", asking):
|
| 100 |
+
return "I can calculate one shape at a time; this request refers to multiple shapes."
|
| 101 |
+
for requested in re.finditer(r"\b(?:in|into|to)\s+(?:(square|cubic|sq\.?|cu\.?)\s+|(units?\s+of)\s+)?([a-z]+)(?:\^?[23]|[²³])?\b", asking):
|
| 102 |
+
if requested.group(1) or requested.group(2) or requested.group(3) in _LENGTHS:
|
| 103 |
+
return "I cannot yet bind an explicitly requested output unit for this geometry calculation."
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
class GeometryOrganV4(GeometryOrganV3):
|
| 108 |
def answer(self, text, session_id="default"):
|
| 109 |
+
t = IMPERATIVE.sub("find ", str(text or ""))
|
| 110 |
+
asking = re.split(r"(?<=[.!?])\s+", t.strip())[-1]
|
| 111 |
+
if (re.match(r"^(?:who|when|where|why|explain|describe|tell me)\b", asking, re.I)
|
| 112 |
+
or re.search(r"\b(?:colou?r|name|history|origin)\b", asking, re.I)):
|
| 113 |
+
return None
|
| 114 |
+
if not re.search(r"\b(?:area|perimeter|volume|circumference|hypotenuse|leg|angles?)\b", asking, re.I):
|
| 115 |
+
return None # shape mentions alone are not requests for a calculation
|
| 116 |
+
low = " ".join(t.lower().replace("−", "-").split())
|
| 117 |
+
issue = _request_issue(low)
|
| 118 |
+
if issue and re.search(r"\b(?:rectangles?|rectangular|circles?|spheres?|cylinders?|cones?|cubes?|squares?|triangles?|box|boxes|cuboids?)\b", low):
|
| 119 |
+
return self._say("underspecified", issue, status="withheld")
|
| 120 |
+
reply = super().answer(t, session_id)
|
| 121 |
+
if not reply or reply.get("status") != "answered" or reply.get("kind") not in _POWERS:
|
| 122 |
+
return reply
|
| 123 |
+
def refuse(message):
|
| 124 |
+
return self._say("underspecified", message, status="withheld")
|
| 125 |
+
if re.search(r"(?<!\w)-\d", low):
|
| 126 |
+
return refuse("A negative length is not a valid dimension for this geometry calculation.")
|
| 127 |
+
fields, units, diagram, issue = _dimensions(low)
|
| 128 |
+
if issue:
|
| 129 |
+
return refuse(issue)
|
| 130 |
+
physical = [u for u in units if u]
|
| 131 |
+
if physical and len(physical) != len(units):
|
| 132 |
+
return refuse("Some dimensions have length units and others do not; please specify the missing units.")
|
| 133 |
+
if "radius" in fields and "diameter" in fields:
|
| 134 |
+
r, ru = fields["radius"]; d, du = fields["diameter"]
|
| 135 |
+
if 2 * r * (ru[1] if ru else 1) != d * (du[1] if du else 1):
|
| 136 |
+
return refuse("The radius and diameter contradict each other: the diameter must be twice the radius.")
|
| 137 |
+
kind = reply["kind"]
|
| 138 |
+
if physical or diagram:
|
| 139 |
+
allowed_roles = {
|
| 140 |
+
"rect_area": {"length", "width"}, "rect_perimeter": {"length", "width"},
|
| 141 |
+
"circle_area": {"radius", "diameter"}, "circumference": {"radius", "diameter"},
|
| 142 |
+
"sphere_volume": {"radius", "diameter"}, "sphere_surface": {"radius", "diameter"},
|
| 143 |
+
"cylinder_volume": {"radius", "diameter", "height"}, "cylinder_surface": {"radius", "diameter", "height"},
|
| 144 |
+
"cone_volume": {"radius", "diameter", "height"},
|
| 145 |
+
"cube_volume": {"side", "edge"}, "cube_surface": {"side", "edge"},
|
| 146 |
+
"box_volume": {"length", "width", "height"},
|
| 147 |
+
"square_area": {"side"}, "square_perimeter": {"side"},
|
| 148 |
+
"tri_area": {"base", "height"}, "hypotenuse": {"leg"}, "leg": {"hypotenuse", "leg"},
|
| 149 |
+
}.get(kind, set())
|
| 150 |
+
if set(fields) - allowed_roles:
|
| 151 |
+
return refuse("This formula does not account for all supplied geometry roles; please clarify the extra dimensions or constraints.")
|
| 152 |
+
if kind.startswith("cube_") and "side" in fields and "edge" in fields:
|
| 153 |
+
side, su = fields["side"]; edge, eu = fields["edge"]
|
| 154 |
+
if side * (su[1] if su else 1) != edge * (eu[1] if eu else 1):
|
| 155 |
+
return refuse("The cube's side and edge values contradict each other; they describe the same length.")
|
| 156 |
+
scales = {u[1] for u in physical}
|
| 157 |
+
power = _POWERS[kind]
|
| 158 |
+
unit = physical[0][0] if physical else None
|
| 159 |
+
value = reply.get("value")
|
| 160 |
+
steps = None
|
| 161 |
+
if kind in ("rect_area", "rect_perimeter") and (physical or diagram):
|
| 162 |
+
if set(fields) != {"length", "width"}:
|
| 163 |
+
return refuse("I need exactly a bound length and width for this rectangle calculation.")
|
| 164 |
+
length, lu = fields["length"]; width, wu = fields["width"]
|
| 165 |
+
if len(scales) > 1:
|
| 166 |
+
length, width = length * lu[1], width * wu[1]
|
| 167 |
+
unit = "m"
|
| 168 |
+
value = length * width if power == 2 else 2 * (length + width)
|
| 169 |
+
steps = ("A = l × w = %g × %g" if power == 2 else "P = 2(l + w) = 2(%g + %g)") % (length, width)
|
| 170 |
+
elif len(scales) > 1:
|
| 171 |
+
return refuse("I cannot combine mixed length units for this geometry formula; please express its dimensions in one unit.")
|
| 172 |
+
# Unitless published formulas retain their original behavior. Explicit
|
| 173 |
+
# diagram dimensions remain useful but must not be presented as inches.
|
| 174 |
+
if not physical and not diagram:
|
| 175 |
+
return reply
|
| 176 |
+
if not math.isfinite(value):
|
| 177 |
+
return refuse("The geometry result is outside the finite numeric range I can return.")
|
| 178 |
+
rounding = re.search(r"round(?:ed)?\s+to\s+(?:the\s+nearest\s+)?(tenth|hundredth|thousandth|whole)\b", low)
|
| 179 |
+
if re.search(r"\bround(?:ed)?\b", low) and not rounding:
|
| 180 |
+
return refuse("I cannot bind that rounding request; please name the nearest tenth, hundredth, thousandth or whole unit.")
|
| 181 |
+
if rounding:
|
| 182 |
+
value = round(value, {"tenth": 1, "hundredth": 2, "thousandth": 3, "whole": 0}[rounding.group(1)])
|
| 183 |
+
label = (unit + ("²" if power == 2 else "³" if power == 3 else "")) if unit else {1: "units", 2: "square units", 3: "cubic units"}[power]
|
| 184 |
+
response = reply["response"]
|
| 185 |
+
if steps is not None:
|
| 186 |
+
response = "The %s is %s %s (%s)." % ("area" if power == 2 else "perimeter", g(value), label, steps)
|
| 187 |
+
else:
|
| 188 |
+
response = re.sub(r"\bis\s+-?\d+(?:\.\d+)?(?:e[+-]?\d+)?", "is %s %s" % (g(value), label), response, count=1)
|
| 189 |
+
return {**reply, "value": value, "unit": unit if power == 1 else ("%s^%d" % (unit, power) if unit else label), "response": response}
|
root/wander-train/math_organ_v1.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
-
"""math_organ_v1
|
|
|
|
|
|
|
| 2 |
|
| 3 |
REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
|
| 4 |
+ the expression(s), read from the question or from its exercise set's instruction
|
| 5 |
MOVES SymPy operations legal for that directive and that expression's type
|
| 6 |
-
CHECK
|
| 7 |
solve every solution substituted back satisfies every equation / inequality
|
| 8 |
factor the product expands back to the input, and is a product
|
| 9 |
simplify equivalent to the input at random points, and no more complex than the input
|
|
@@ -15,6 +17,7 @@ STORED FORM the textbook's own method for the directive (a howto or a worked exa
|
|
| 15 |
Nothing is asserted unless the check passes; a failed check is an abstention with the reason."""
|
| 16 |
from __future__ import annotations
|
| 17 |
|
|
|
|
| 18 |
import random
|
| 19 |
import re
|
| 20 |
import sqlite3
|
|
@@ -189,6 +192,23 @@ def rand_pts(syms, k=5):
|
|
| 189 |
return [{s: sp.Rational(rng.randint(2, 29), rng.randint(3, 11)) for s in syms} for _ in range(k)]
|
| 190 |
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
def equiv(a, b):
|
| 193 |
try:
|
| 194 |
d = sp.simplify(a - b)
|
|
@@ -196,8 +216,7 @@ def equiv(a, b):
|
|
| 196 |
return True
|
| 197 |
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 198 |
for pt in rand_pts(syms):
|
| 199 |
-
|
| 200 |
-
if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
|
| 201 |
return False
|
| 202 |
return True
|
| 203 |
except Exception: # noqa: BLE001
|
|
@@ -405,9 +424,9 @@ class MathOrgan:
|
|
| 405 |
if e.free_symbols:
|
| 406 |
return self._simplify(spans, q)
|
| 407 |
v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
|
| 408 |
-
if
|
| 409 |
return None
|
| 410 |
-
return {"answer": sp.sstr(v), "value": v, "check": "
|
| 411 |
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 412 |
|
| 413 |
# --- calculus -------------------------------------------------------------------------------------------------
|
|
|
|
| 1 |
+
"""Untrusted math_organ_v1: SymPy operations with heuristic numerical checks.
|
| 2 |
+
|
| 3 |
+
These checks share the evaluator and are not independent proofs. The method is cited when available.
|
| 4 |
|
| 5 |
REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
|
| 6 |
+ the expression(s), read from the question or from its exercise set's instruction
|
| 7 |
MOVES SymPy operations legal for that directive and that expression's type
|
| 8 |
+
CHECK numerical or symbolic checks, not a theorem about the user question:
|
| 9 |
solve every solution substituted back satisfies every equation / inequality
|
| 10 |
factor the product expands back to the input, and is a product
|
| 11 |
simplify equivalent to the input at random points, and no more complex than the input
|
|
|
|
| 17 |
Nothing is asserted unless the check passes; a failed check is an abstention with the reason."""
|
| 18 |
from __future__ import annotations
|
| 19 |
|
| 20 |
+
import math
|
| 21 |
import random
|
| 22 |
import re
|
| 23 |
import sqlite3
|
|
|
|
| 192 |
return [{s: sp.Rational(rng.randint(2, 29), rng.randint(3, 11)) for s in syms} for _ in range(k)]
|
| 193 |
|
| 194 |
|
| 195 |
+
def finite_numeric_agreement(a, b, tolerance):
|
| 196 |
+
"""Reject NaN/infinity before comparison; finite complex values are allowed.
|
| 197 |
+
|
| 198 |
+
Both evaluations use SymPy. Agreement is a heuristic check, not independent
|
| 199 |
+
verification, and values outside machine complex range are declined.
|
| 200 |
+
"""
|
| 201 |
+
try:
|
| 202 |
+
va, vb = complex(a.evalf()), complex(b.evalf())
|
| 203 |
+
if not all(math.isfinite(x) for x in (va.real, va.imag, vb.real, vb.imag)):
|
| 204 |
+
return False
|
| 205 |
+
difference = abs(va - vb)
|
| 206 |
+
bound = tolerance * max(1.0, abs(va), abs(vb))
|
| 207 |
+
return math.isfinite(difference) and math.isfinite(bound) and difference <= bound
|
| 208 |
+
except (TypeError, ValueError, OverflowError):
|
| 209 |
+
return False
|
| 210 |
+
|
| 211 |
+
|
| 212 |
def equiv(a, b):
|
| 213 |
try:
|
| 214 |
d = sp.simplify(a - b)
|
|
|
|
| 216 |
return True
|
| 217 |
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 218 |
for pt in rand_pts(syms):
|
| 219 |
+
if not finite_numeric_agreement(a.subs(pt), b.subs(pt), 1e-7):
|
|
|
|
| 220 |
return False
|
| 221 |
return True
|
| 222 |
except Exception: # noqa: BLE001
|
|
|
|
| 424 |
if e.free_symbols:
|
| 425 |
return self._simplify(spans, q)
|
| 426 |
v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
|
| 427 |
+
if not finite_numeric_agreement(v, e, 1e-9):
|
| 428 |
return None
|
| 429 |
+
return {"answer": sp.sstr(v), "value": v, "check": "finite numerical comparison using SymPy",
|
| 430 |
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 431 |
|
| 432 |
# --- calculus -------------------------------------------------------------------------------------------------
|
root/wander-train/math_organ_v2.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
"""math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
|
| 2 |
request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
|
| 3 |
|
| 4 |
-
|
|
|
|
| 5 |
|
| 6 |
REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
|
| 7 |
+ the expression(s), read from the question or from its exercise set's instruction
|
| 8 |
MOVES SymPy operations legal for that directive and that expression's type
|
| 9 |
-
CHECK
|
| 10 |
solve every solution substituted back satisfies every equation / inequality
|
| 11 |
factor the product expands back to the input, and is a product
|
| 12 |
simplify equivalent to the input at random points, and no more complex than the input
|
|
@@ -23,6 +24,7 @@ import re
|
|
| 23 |
import sqlite3
|
| 24 |
|
| 25 |
import sympy as sp
|
|
|
|
| 26 |
from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
|
| 27 |
split_symbols_custom, standard_transformations)
|
| 28 |
|
|
@@ -199,8 +201,7 @@ def equiv(a, b):
|
|
| 199 |
return True
|
| 200 |
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 201 |
for pt in rand_pts(syms):
|
| 202 |
-
|
| 203 |
-
if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
|
| 204 |
return False
|
| 205 |
return True
|
| 206 |
except Exception: # noqa: BLE001
|
|
@@ -412,9 +413,9 @@ class MathOrgan:
|
|
| 412 |
if e.free_symbols:
|
| 413 |
return self._simplify(spans, q)
|
| 414 |
v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
|
| 415 |
-
if
|
| 416 |
return None
|
| 417 |
-
return {"answer": sp.sstr(v), "value": v, "check": "
|
| 418 |
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 419 |
|
| 420 |
# --- calculus -------------------------------------------------------------------------------------------------
|
|
|
|
| 1 |
"""math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
|
| 2 |
request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
|
| 3 |
|
| 4 |
+
Untrusted SymPy operations with heuristic numerical checks; the method is cited when available.
|
| 5 |
+
These checks share the evaluator and are not independent proofs.
|
| 6 |
|
| 7 |
REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
|
| 8 |
+ the expression(s), read from the question or from its exercise set's instruction
|
| 9 |
MOVES SymPy operations legal for that directive and that expression's type
|
| 10 |
+
CHECK numerical or symbolic checks, not a theorem about the user question:
|
| 11 |
solve every solution substituted back satisfies every equation / inequality
|
| 12 |
factor the product expands back to the input, and is a product
|
| 13 |
simplify equivalent to the input at random points, and no more complex than the input
|
|
|
|
| 24 |
import sqlite3
|
| 25 |
|
| 26 |
import sympy as sp
|
| 27 |
+
from math_organ_v1 import finite_numeric_agreement
|
| 28 |
from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
|
| 29 |
split_symbols_custom, standard_transformations)
|
| 30 |
|
|
|
|
| 201 |
return True
|
| 202 |
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 203 |
for pt in rand_pts(syms):
|
| 204 |
+
if not finite_numeric_agreement(a.subs(pt), b.subs(pt), 1e-7):
|
|
|
|
| 205 |
return False
|
| 206 |
return True
|
| 207 |
except Exception: # noqa: BLE001
|
|
|
|
| 413 |
if e.free_symbols:
|
| 414 |
return self._simplify(spans, q)
|
| 415 |
v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
|
| 416 |
+
if not finite_numeric_agreement(v, e, 1e-9):
|
| 417 |
return None
|
| 418 |
+
return {"answer": sp.sstr(v), "value": v, "check": "finite numerical comparison using SymPy",
|
| 419 |
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 420 |
|
| 421 |
# --- calculus -------------------------------------------------------------------------------------------------
|
root/wander-train/math_organ_v3.py
CHANGED
|
@@ -3,12 +3,13 @@ variables (found by the proposer brief: "On what date in 1969 did ..." -> 1969*d
|
|
| 3 |
math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
|
| 4 |
request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
|
| 5 |
|
| 6 |
-
|
|
|
|
| 7 |
|
| 8 |
REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
|
| 9 |
+ the expression(s), read from the question or from its exercise set's instruction
|
| 10 |
MOVES SymPy operations legal for that directive and that expression's type
|
| 11 |
-
CHECK
|
| 12 |
solve every solution substituted back satisfies every equation / inequality
|
| 13 |
factor the product expands back to the input, and is a product
|
| 14 |
simplify equivalent to the input at random points, and no more complex than the input
|
|
@@ -25,6 +26,7 @@ import re
|
|
| 25 |
import sqlite3
|
| 26 |
|
| 27 |
import sympy as sp
|
|
|
|
| 28 |
from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
|
| 29 |
split_symbols_custom, standard_transformations)
|
| 30 |
|
|
@@ -207,8 +209,7 @@ def equiv(a, b):
|
|
| 207 |
return True
|
| 208 |
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 209 |
for pt in rand_pts(syms):
|
| 210 |
-
|
| 211 |
-
if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
|
| 212 |
return False
|
| 213 |
return True
|
| 214 |
except Exception: # noqa: BLE001
|
|
@@ -420,9 +421,9 @@ class MathOrgan:
|
|
| 420 |
if e.free_symbols:
|
| 421 |
return self._simplify(spans, q)
|
| 422 |
v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
|
| 423 |
-
if
|
| 424 |
return None
|
| 425 |
-
return {"answer": sp.sstr(v), "value": v, "check": "
|
| 426 |
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 427 |
|
| 428 |
# --- calculus -------------------------------------------------------------------------------------------------
|
|
|
|
| 3 |
math_organ_v2 - v1 (admitted in w480, now immutable) + two fixes found on the gate panel: "factorial" is not a
|
| 4 |
request to factor, and a request for a PROGRAM is the code organ's, not a mathematical request.
|
| 5 |
|
| 6 |
+
Untrusted SymPy operations with heuristic numerical checks; the method is cited when available.
|
| 7 |
+
These checks share the evaluator and are not independent proofs.
|
| 8 |
|
| 9 |
REQUEST directive (solve / simplify / factor / expand / evaluate / differentiate / integrate / limit / decompose)
|
| 10 |
+ the expression(s), read from the question or from its exercise set's instruction
|
| 11 |
MOVES SymPy operations legal for that directive and that expression's type
|
| 12 |
+
CHECK numerical or symbolic checks, not a theorem about the user question:
|
| 13 |
solve every solution substituted back satisfies every equation / inequality
|
| 14 |
factor the product expands back to the input, and is a product
|
| 15 |
simplify equivalent to the input at random points, and no more complex than the input
|
|
|
|
| 26 |
import sqlite3
|
| 27 |
|
| 28 |
import sympy as sp
|
| 29 |
+
from math_organ_v1 import finite_numeric_agreement
|
| 30 |
from sympy.parsing.sympy_parser import (convert_xor, implicit_multiplication_application, parse_expr,
|
| 31 |
split_symbols_custom, standard_transformations)
|
| 32 |
|
|
|
|
| 209 |
return True
|
| 210 |
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 211 |
for pt in rand_pts(syms):
|
| 212 |
+
if not finite_numeric_agreement(a.subs(pt), b.subs(pt), 1e-7):
|
|
|
|
| 213 |
return False
|
| 214 |
return True
|
| 215 |
except Exception: # noqa: BLE001
|
|
|
|
| 421 |
if e.free_symbols:
|
| 422 |
return self._simplify(spans, q)
|
| 423 |
v = sp.nsimplify(sp.simplify(e)) if not e.is_Float else e
|
| 424 |
+
if not finite_numeric_agreement(v, e, 1e-9):
|
| 425 |
return None
|
| 426 |
+
return {"answer": sp.sstr(v), "value": v, "check": "finite numerical comparison using SymPy",
|
| 427 |
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 428 |
|
| 429 |
# --- calculus -------------------------------------------------------------------------------------------------
|
root/wander-train/oracle_operator_specs_v1.py
CHANGED
|
@@ -198,33 +198,63 @@ def _gcd_ref(a):
|
|
| 198 |
|
| 199 |
|
| 200 |
# ── 3. base conversion ────────────────────────────────────────────────────────
|
| 201 |
-
#
|
| 202 |
-
|
| 203 |
-
r'\b(?:convert|write|express)?\s*(-?\d+)\s*(?:from\s*)?(?:base\s*(\d+))?\s*'
|
| 204 |
-
r'(?:in|to|into)\s*(?:base\s*)?(\d+|binary|hex|hexadecimal|octal|decimal)\b', re.I)
|
| 205 |
_BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
|
| 207 |
|
| 208 |
|
| 209 |
def _base_parse(s):
|
| 210 |
-
m = _BASE_RX.
|
| 211 |
if not m:
|
| 212 |
-
raise NotOurs('no
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
raise OutOfFragment('I convert non-negative whole numbers only')
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
|
| 226 |
def _base_fn(a):
|
| 227 |
-
n, steps, out = a['
|
| 228 |
if n == 0:
|
| 229 |
return '0', ['0 in any base is 0']
|
| 230 |
while n:
|
|
@@ -235,8 +265,13 @@ def _base_fn(a):
|
|
| 235 |
|
| 236 |
|
| 237 |
def _base_ref(a):
|
| 238 |
-
"""
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
if n == 0:
|
| 241 |
return '0'
|
| 242 |
digits, powers = [], []
|
|
@@ -418,8 +453,9 @@ SPECS = [
|
|
| 418 |
'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
|
| 419 |
'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
|
| 420 |
'warrant': 'computation',
|
| 421 |
-
'independence': 'moderate:
|
| 422 |
-
'
|
|
|
|
| 423 |
'fragment': 'non-negative whole numbers, bases 2 to 36'},
|
| 424 |
|
| 425 |
{'name': 'statistics', 'family': 'measurement',
|
|
|
|
| 198 |
|
| 199 |
|
| 200 |
# ── 3. base conversion ────────────────────────────────────────────────────────
|
| 201 |
+
# The parser binds the original digits and source base. The function decodes
|
| 202 |
+
# with int(); the reference independently expands source digits positionally.
|
|
|
|
|
|
|
| 203 |
_BASE_NAME = {'binary': 2, 'hex': 16, 'hexadecimal': 16, 'octal': 8, 'decimal': 10}
|
| 204 |
+
_BASE_ATOM = r'(?:base\s*\d+|binary|hexadecimal|hex|octal|decimal)'
|
| 205 |
+
_BASE_RX = re.compile(
|
| 206 |
+
r'^\s*(?:(?:convert|write|express|what\s+is)\s+)?'
|
| 207 |
+
r'(?:(?P<prefix>' + _BASE_ATOM + r')\s+)?'
|
| 208 |
+
r'(?P<digits>[+-]?[0-9a-z]+)'
|
| 209 |
+
r'(?:\s+(?:(?:from|in)\s+)?(?P<source>' + _BASE_ATOM + r'))?'
|
| 210 |
+
r'\s+(?:in|to|into|as)\s+(?P<target>' + _BASE_ATOM + r'|\d+)'
|
| 211 |
+
r'\s*[.?!]?\s*$', re.I | re.ASCII)
|
| 212 |
_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
|
| 213 |
|
| 214 |
|
| 215 |
def _base_parse(s):
|
| 216 |
+
m = _BASE_RX.fullmatch(s)
|
| 217 |
if not m:
|
| 218 |
+
raise NotOurs('no complete integer base conversion here')
|
| 219 |
+
|
| 220 |
+
def base(token):
|
| 221 |
+
token = token.lower()
|
| 222 |
+
value = _BASE_NAME.get(token)
|
| 223 |
+
if value is None:
|
| 224 |
+
numeral = re.sub(r'^base\s*', '', token)
|
| 225 |
+
if len(numeral) > 3:
|
| 226 |
+
raise OutOfFragment('I convert between base 2 and base 36 only')
|
| 227 |
+
value = int(numeral)
|
| 228 |
+
if not 2 <= value <= 36:
|
| 229 |
+
raise OutOfFragment('I convert between base 2 and base 36 only')
|
| 230 |
+
return value
|
| 231 |
+
|
| 232 |
+
sources = [base(token) for token in (m['prefix'], m['source']) if token]
|
| 233 |
+
raw = m['digits'].lower()
|
| 234 |
+
if raw.startswith('-'):
|
| 235 |
raise OutOfFragment('I convert non-negative whole numbers only')
|
| 236 |
+
raw = raw.lstrip('+')
|
| 237 |
+
# A conventional literal prefix is source evidence, not a digit to discard.
|
| 238 |
+
# Explicit bases above 16 may use x as a digit; only infer prefixes when no
|
| 239 |
+
# explicit source is supplied, or that source matches the prefix convention.
|
| 240 |
+
literal = {'0b': 2, '0o': 8, '0x': 16}.get(raw[:2])
|
| 241 |
+
if literal is not None and (not sources or all(b == literal for b in sources)):
|
| 242 |
+
sources.append(literal)
|
| 243 |
+
raw = raw[2:]
|
| 244 |
+
elif literal is not None and sources and sources[0] in (2, 8, 10, 16):
|
| 245 |
+
raise OutOfFragment('the literal prefix conflicts with the stated source base')
|
| 246 |
+
if len(set(sources)) > 1:
|
| 247 |
+
raise OutOfFragment('the stated source bases disagree')
|
| 248 |
+
frm = sources[0] if sources else 10
|
| 249 |
+
to = base(m['target'])
|
| 250 |
+
if not raw or len(raw) > 128 or any(_DIGITS.index(c) >= frm for c in raw):
|
| 251 |
+
raise OutOfFragment('I need 1 to 128 valid digits in the stated source base')
|
| 252 |
+
n = int(raw, frm)
|
| 253 |
+
return {'n': n, 'digits': raw, 'frm': frm, 'to': to}
|
| 254 |
|
| 255 |
|
| 256 |
def _base_fn(a):
|
| 257 |
+
n, steps, out = int(a['digits'], a['frm']), [], ''
|
| 258 |
if n == 0:
|
| 259 |
return '0', ['0 in any base is 0']
|
| 260 |
while n:
|
|
|
|
| 265 |
|
| 266 |
|
| 267 |
def _base_ref(a):
|
| 268 |
+
"""Expand original source digits, then encode using a positional table.
|
| 269 |
+
|
| 270 |
+
This does not share int() source decoding or repeated output division with
|
| 271 |
+
the primary function. Both still depend on the same untrusted parse.
|
| 272 |
+
"""
|
| 273 |
+
n = sum(_DIGITS.index(digit) * a['frm'] ** power
|
| 274 |
+
for power, digit in enumerate(reversed(a['digits'])))
|
| 275 |
if n == 0:
|
| 276 |
return '0'
|
| 277 |
digits, powers = [], []
|
|
|
|
| 453 |
'parse': _base_parse, 'fn': _base_fn, 'ref': _base_ref,
|
| 454 |
'render': lambda a, v, s: '%d in base %d is %s.' % (a['n'], a['to'], v),
|
| 455 |
'warrant': 'computation',
|
| 456 |
+
'independence': 'moderate: int source decoding and repeated output division '
|
| 457 |
+
'against positional source expansion and output encoding; '
|
| 458 |
+
'both still depend on the same untrusted parse',
|
| 459 |
'fragment': 'non-negative whole numbers, bases 2 to 36'},
|
| 460 |
|
| 461 |
{'name': 'statistics', 'family': 'measurement',
|
root/wander-train/oracle_release_runtime_ao.py
CHANGED
|
@@ -22,6 +22,16 @@ import re
|
|
| 22 |
|
| 23 |
from oracle_release_runtime_an import OracleReleaseRuntimeAN
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
|
| 26 |
ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
|
| 27 |
BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
|
|
@@ -41,6 +51,17 @@ PREDICATE_OF = {
|
|
| 41 |
}
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
|
| 45 |
"""Conversions are applied ONLY when the unchanged chain would not answer."""
|
| 46 |
|
|
@@ -103,10 +124,11 @@ class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
|
|
| 103 |
if not CONTINUATION.match(s):
|
| 104 |
return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
|
| 105 |
topic = self._live_topic(session_id)
|
| 106 |
-
|
|
|
|
| 107 |
if low in PREDICATE_OF:
|
| 108 |
return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
|
| 109 |
-
return
|
| 110 |
return None, None
|
| 111 |
|
| 112 |
def chat(self, text: str, session_id: str = "default") -> dict:
|
|
|
|
| 22 |
|
| 23 |
from oracle_release_runtime_an import OracleReleaseRuntimeAN
|
| 24 |
|
| 25 |
+
# Untrusted grammar guard: in "does it take/cost ... to <action>", it
|
| 26 |
+
# may be a grammatical subject rather than an entity from the conversation.
|
| 27 |
+
# Decline substitution for that token; other references remain eligible. The
|
| 28 |
+
# match stays within a clause and accepts decimal amounts before the infinitive.
|
| 29 |
+
DUMMY_IT = re.compile(
|
| 30 |
+
r"\b(?:does|did|will|would|can|could|should|has|had)\s+(?P<it>it)\s+"
|
| 31 |
+
r"(?:take|taken|cost)\s+"
|
| 32 |
+
r"(?:(?:\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|"
|
| 33 |
+
r"months?|years?|dollars?|euros?|pounds?)|[$€£]\d+(?:\.\d+)?)\s+)?"
|
| 34 |
+
r"to\s+[a-z]", re.I)
|
| 35 |
DEICTIC = re.compile(r"\b(it|there|that|this|its|they|them|those|these|the same)\b", re.I)
|
| 36 |
ELLIPSIS = re.compile(r"^(?:and|what about|the same for)\s+(?P<t>.+?)\??$", re.I)
|
| 37 |
BARE = re.compile(r"^(?:the\s+)?[a-z][a-z \-']{2,40}$", re.I)
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
|
| 54 |
+
def referential_deictic(text):
|
| 55 |
+
"""First eligible reference, excluding only take/cost-infinitive dummy it.
|
| 56 |
+
|
| 57 |
+
This does not claim to parse all English expletives. Skipping an ambiguous
|
| 58 |
+
token leaves the original question available to the unchanged handler.
|
| 59 |
+
"""
|
| 60 |
+
dummy_spans = {match.span("it") for match in DUMMY_IT.finditer(text)}
|
| 61 |
+
return next((match for match in DEICTIC.finditer(text)
|
| 62 |
+
if match.span() not in dummy_spans), None)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
class OracleReleaseRuntimeAO(OracleReleaseRuntimeAN):
|
| 66 |
"""Conversions are applied ONLY when the unchanged chain would not answer."""
|
| 67 |
|
|
|
|
| 124 |
if not CONTINUATION.match(s):
|
| 125 |
return f"Summarize {s.strip()}.", "ELLIPSIS_COMPLETION"
|
| 126 |
topic = self._live_topic(session_id)
|
| 127 |
+
reference = referential_deictic(s)
|
| 128 |
+
if topic and reference:
|
| 129 |
if low in PREDICATE_OF:
|
| 130 |
return PREDICATE_OF[low].format(t=topic), "REFERENT_SUBSTITUTION"
|
| 131 |
+
return s[:reference.start()] + topic + s[reference.end():], "REFERENT_SUBSTITUTION"
|
| 132 |
return None, None
|
| 133 |
|
| 134 |
def chat(self, text: str, session_id: str = "default") -> dict:
|
root/wander-train/reach_router_v1.py
CHANGED
|
@@ -17,6 +17,7 @@ identifiers, links and media are never given unless asked for; quantities only w
|
|
| 17 |
quantities without their units). At most 5 stored values, listed; more is declined.
|
| 18 |
Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
|
| 19 |
a relation that changes over time, blank nodes, an answer that restates the question.
|
|
|
|
| 20 |
use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
|
| 21 |
import json
|
| 22 |
import os
|
|
@@ -138,7 +139,7 @@ def fragment(q, span, p):
|
|
| 138 |
j += step
|
| 139 |
if 0 <= j < len(toks):
|
| 140 |
w = toks[j]
|
| 141 |
-
if w not in L.STOP and w not in cues and not w.isdigit()
|
| 142 |
return True
|
| 143 |
return False
|
| 144 |
return False
|
|
@@ -368,8 +369,9 @@ def answer(q, pinned=None):
|
|
| 368 |
return None # a value with no English name is not shown
|
| 369 |
shown = sorted(shown)
|
| 370 |
aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
|
| 371 |
-
if
|
| 372 |
-
|
|
|
|
| 373 |
text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
|
| 374 |
return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
|
| 375 |
"route": route, "pattern": pat}
|
|
|
|
| 17 |
quantities without their units). At most 5 stored values, listed; more is declined.
|
| 18 |
Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
|
| 19 |
a relation that changes over time, blank nodes, an answer that restates the question.
|
| 20 |
+
Untrusted routing policy: stored rows and type ancestry are evidence, not proof of source truth or completeness.
|
| 21 |
use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
|
| 22 |
import json
|
| 23 |
import os
|
|
|
|
| 139 |
j += step
|
| 140 |
if 0 <= j < len(toks):
|
| 141 |
w = toks[j]
|
| 142 |
+
if w not in L.STOP and w not in cues and not w.isdigit():
|
| 143 |
return True
|
| 144 |
return False
|
| 145 |
return False
|
|
|
|
| 369 |
return None # a value with no English name is not shown
|
| 370 |
shown = sorted(shown)
|
| 371 |
aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
|
| 372 |
+
if subj in v or (not all(isinstance(x, int) for x in v)
|
| 373 |
+
and aw and all(w in set(L.norm(q).split()) for w in aw)):
|
| 374 |
+
return None # entity identity, not overlapping labels, is restatement
|
| 375 |
text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
|
| 376 |
return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
|
| 377 |
"route": route, "pattern": pat}
|
root/wander-train/reach_router_v2.py
CHANGED
|
@@ -23,6 +23,7 @@ identifiers, links and media are never given unless asked for; quantities only w
|
|
| 23 |
quantities without their units). At most 5 stored values, listed; more is declined.
|
| 24 |
Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
|
| 25 |
a relation that changes over time, blank nodes, an answer that restates the question.
|
|
|
|
| 26 |
use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
|
| 27 |
import json
|
| 28 |
import os
|
|
@@ -150,7 +151,7 @@ def fragment(q, span, p):
|
|
| 150 |
j += step
|
| 151 |
if 0 <= j < len(toks):
|
| 152 |
w = toks[j]
|
| 153 |
-
if w not in L.STOP and w not in cues and not w.isdigit()
|
| 154 |
return True
|
| 155 |
return False
|
| 156 |
return False
|
|
@@ -409,8 +410,9 @@ def answer(q, pinned=None):
|
|
| 409 |
return None # a value with no English name is not shown
|
| 410 |
shown = sorted(shown)
|
| 411 |
aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
|
| 412 |
-
if
|
| 413 |
-
|
|
|
|
| 414 |
text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
|
| 415 |
return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
|
| 416 |
"route": route, "pattern": pat}
|
|
|
|
| 23 |
quantities without their units). At most 5 stored values, listed; more is declined.
|
| 24 |
Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
|
| 25 |
a relation that changes over time, blank nodes, an answer that restates the question.
|
| 26 |
+
Untrusted routing policy: stored rows and type ancestry are evidence, not proof of source truth or completeness.
|
| 27 |
use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
|
| 28 |
import json
|
| 29 |
import os
|
|
|
|
| 151 |
j += step
|
| 152 |
if 0 <= j < len(toks):
|
| 153 |
w = toks[j]
|
| 154 |
+
if w not in L.STOP and w not in cues and not w.isdigit():
|
| 155 |
return True
|
| 156 |
return False
|
| 157 |
return False
|
|
|
|
| 410 |
return None # a value with no English name is not shown
|
| 411 |
shown = sorted(shown)
|
| 412 |
aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
|
| 413 |
+
if subj in v or (not all(isinstance(x, int) for x in v)
|
| 414 |
+
and aw and all(w in set(L.norm(q).split()) for w in aw)):
|
| 415 |
+
return None # entity identity, not overlapping labels, is restatement
|
| 416 |
text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
|
| 417 |
return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
|
| 418 |
"route": route, "pattern": pat}
|
root/wander-train/reach_router_v3.py
CHANGED
|
@@ -27,6 +27,7 @@ identifiers, links and media are never given unless asked for; quantities only w
|
|
| 27 |
quantities without their units). At most 5 stored values, listed; more is declined.
|
| 28 |
Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
|
| 29 |
a relation that changes over time, blank nodes, an answer that restates the question.
|
|
|
|
| 30 |
use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
|
| 31 |
import json
|
| 32 |
import os
|
|
@@ -144,9 +145,9 @@ def fits(q, p, d, vals):
|
|
| 144 |
return True
|
| 145 |
|
| 146 |
|
| 147 |
-
def fragment(q, span, p):
|
| 148 |
"""the span is part of a longer name: a word next to it (past name particles) is a content word nothing explains"""
|
| 149 |
-
cues = _load()["cues"].get(p, set())
|
| 150 |
toks, sp = L.norm(q).split(), L.norm(span).split()
|
| 151 |
for i in range(len(toks) - len(sp) + 1):
|
| 152 |
if toks[i:i + len(sp)] == sp:
|
|
@@ -155,7 +156,7 @@ def fragment(q, span, p):
|
|
| 155 |
j += step
|
| 156 |
if 0 <= j < len(toks):
|
| 157 |
w = toks[j]
|
| 158 |
-
if w not in L.STOP and w not in cues and not w.isdigit()
|
| 159 |
return True
|
| 160 |
return False
|
| 161 |
return False
|
|
@@ -167,17 +168,17 @@ GENERIC = set(("name names called known located famous kind type sort also exact
|
|
| 167 |
MAX_UNEXPLAINED = int(os.environ.get("REACH_MAX_UNEXPLAINED", "2")) # development: 2 stopped a Mintaka error, lost nothing
|
| 168 |
|
| 169 |
|
| 170 |
-
def unexplained(q, span, p):
|
| 171 |
"""content words of the question that neither the subject, the relation's own words nor ordinary question words explain"""
|
| 172 |
-
cues = _load()["cues"].get(p, set())
|
| 173 |
sp = set(L.norm(span).split())
|
| 174 |
return [w for w in L.norm(q).split() if w not in sp and w not in cues and w not in GENERIC and w not in L.STOP
|
| 175 |
and not w.isdigit() and len(w) >= 3]
|
| 176 |
|
| 177 |
|
| 178 |
-
def _agree(q, span, cands, p, d):
|
| 179 |
"""the readings of the name that hold the relation must agree; a one-word name answers through its top reading only"""
|
| 180 |
-
if fragment(q, span, p) or len(unexplained(q, span, p)) > MAX_UNEXPLAINED:
|
| 181 |
return None, None
|
| 182 |
one = len(L.norm(span).split()) == 1
|
| 183 |
use = cands[:1] if one else cands[:3]
|
|
@@ -310,6 +311,10 @@ def _agent_routes(q):
|
|
| 310 |
return []
|
| 311 |
al = _load()["alias"]
|
| 312 |
v = m.group("v").lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
passive, agents = (IRREG[v][0], {IRREG[v][1]}) if v in IRREG else (v, _agent_nouns(v))
|
| 314 |
props = set(al.get(passive + " by", set()))
|
| 315 |
for a in agents:
|
|
@@ -317,6 +322,43 @@ def _agent_routes(q):
|
|
| 317 |
return [(props, m.group("y"), "agent:" + v)] if props else []
|
| 318 |
|
| 319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
def _label_routes(q):
|
| 321 |
"""the question names the relation: (relation, subject text, the asked phrase)"""
|
| 322 |
al = _load()["alias"]
|
|
@@ -408,14 +450,18 @@ def answer(q, pinned=None):
|
|
| 408 |
sp = L.link(q, 3)[:3]
|
| 409 |
if pinned:
|
| 410 |
sp = [(pinned[0], [(pinned[1], 10 ** 6, label(pinned[1]))])] + [x for x in sp if L.norm(x[0]) != L.norm(pinned[0])]
|
|
|
|
|
|
|
| 411 |
found = []
|
| 412 |
for span, cands in sp: # PATTERN route: the first span whose reading holds the relation
|
| 413 |
if XR.hop(q, span) or not XR.subject_ok(q, span):
|
| 414 |
continue
|
| 415 |
for (p, d), route, pat in _routes(q, span, cands):
|
|
|
|
|
|
|
| 416 |
if p in XR.TIME_VARIANT and XR.PAST.search(q):
|
| 417 |
continue
|
| 418 |
-
v, subj = _agree(q, span, cands, p, d)
|
| 419 |
if v is not None and fits(q, p, d, v):
|
| 420 |
found.append((p, d, v, subj, route, pat))
|
| 421 |
break
|
|
@@ -429,9 +475,11 @@ def answer(q, pinned=None):
|
|
| 429 |
span, cands = ysp[0]
|
| 430 |
hits = []
|
| 431 |
for p in sorted(props):
|
|
|
|
|
|
|
| 432 |
if p in XR.TIME_VARIANT and XR.PAST.search(q):
|
| 433 |
continue
|
| 434 |
-
v, subj = _agree(q, span, cands, p, "fwd")
|
| 435 |
if v is not None and fits(q, p, "fwd", v):
|
| 436 |
hits.append((p, "fwd", v, subj, "when" if asked.startswith("when:") else "count" if asked.startswith("count:") else "agent" if asked.startswith("agent:") else "label", asked))
|
| 437 |
if len({h[2] for h in hits}) == 1:
|
|
@@ -445,8 +493,9 @@ def answer(q, pinned=None):
|
|
| 445 |
return None # a value with no English name is not shown
|
| 446 |
shown = sorted(shown)
|
| 447 |
aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
|
| 448 |
-
if
|
| 449 |
-
|
|
|
|
| 450 |
text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
|
| 451 |
return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
|
| 452 |
"route": route, "pattern": pat}
|
|
|
|
| 27 |
quantities without their units). At most 5 stored values, listed; more is declined.
|
| 28 |
Refused as in exact_router_v1: yes/no questions, a hidden hop, a quoted title that is not the span, a past-time qualifier on
|
| 29 |
a relation that changes over time, blank nodes, an answer that restates the question.
|
| 30 |
+
Untrusted routing policy: stored rows and type ancestry are evidence, not proof of source truth or completeness.
|
| 31 |
use: answer(question) -> {"answer", "values", "property", "direction", "subject_qid", "route", "pattern"} or None"""
|
| 32 |
import json
|
| 33 |
import os
|
|
|
|
| 145 |
return True
|
| 146 |
|
| 147 |
|
| 148 |
+
def fragment(q, span, p, extra_cues=()):
|
| 149 |
"""the span is part of a longer name: a word next to it (past name particles) is a content word nothing explains"""
|
| 150 |
+
cues = _load()["cues"].get(p, set()) | set(extra_cues)
|
| 151 |
toks, sp = L.norm(q).split(), L.norm(span).split()
|
| 152 |
for i in range(len(toks) - len(sp) + 1):
|
| 153 |
if toks[i:i + len(sp)] == sp:
|
|
|
|
| 156 |
j += step
|
| 157 |
if 0 <= j < len(toks):
|
| 158 |
w = toks[j]
|
| 159 |
+
if w not in L.STOP and w not in cues and not w.isdigit():
|
| 160 |
return True
|
| 161 |
return False
|
| 162 |
return False
|
|
|
|
| 168 |
MAX_UNEXPLAINED = int(os.environ.get("REACH_MAX_UNEXPLAINED", "2")) # development: 2 stopped a Mintaka error, lost nothing
|
| 169 |
|
| 170 |
|
| 171 |
+
def unexplained(q, span, p, extra_cues=()):
|
| 172 |
"""content words of the question that neither the subject, the relation's own words nor ordinary question words explain"""
|
| 173 |
+
cues = _load()["cues"].get(p, set()) | set(extra_cues)
|
| 174 |
sp = set(L.norm(span).split())
|
| 175 |
return [w for w in L.norm(q).split() if w not in sp and w not in cues and w not in GENERIC and w not in L.STOP
|
| 176 |
and not w.isdigit() and len(w) >= 3]
|
| 177 |
|
| 178 |
|
| 179 |
+
def _agree(q, span, cands, p, d, extra_cues=()):
|
| 180 |
"""the readings of the name that hold the relation must agree; a one-word name answers through its top reading only"""
|
| 181 |
+
if fragment(q, span, p, extra_cues) or len(unexplained(q, span, p, extra_cues)) > MAX_UNEXPLAINED:
|
| 182 |
return None, None
|
| 183 |
one = len(L.norm(span).split()) == 1
|
| 184 |
use = cands[:1] if one else cands[:3]
|
|
|
|
| 311 |
return []
|
| 312 |
al = _load()["alias"]
|
| 313 |
v = m.group("v").lower()
|
| 314 |
+
if v == "created":
|
| 315 |
+
# A developer can be a later maintainer, not the original creator.
|
| 316 |
+
# Designer is admitted only with programming-language type evidence below.
|
| 317 |
+
return [({170, 287}, m.group("y"), "agent:created")]
|
| 318 |
passive, agents = (IRREG[v][0], {IRREG[v][1]}) if v in IRREG else (v, _agent_nouns(v))
|
| 319 |
props = set(al.get(passive + " by", set()))
|
| 320 |
for a in agents:
|
|
|
|
| 322 |
return [(props, m.group("y"), "agent:" + v)] if props else []
|
| 323 |
|
| 324 |
|
| 325 |
+
def _programming_language(qid):
|
| 326 |
+
"""Positive bounded P31/P279 evidence only; missing ancestry is not a negative fact."""
|
| 327 |
+
db = _load()["db"]
|
| 328 |
+
frontier, seen = {qid}, set()
|
| 329 |
+
for depth in range(5):
|
| 330 |
+
if 9143 in frontier and depth > 0:
|
| 331 |
+
return True
|
| 332 |
+
next_frontier = set()
|
| 333 |
+
for item in frontier - seen:
|
| 334 |
+
rows = db.execute("SELECT o FROM fact INDEXED BY fact_sp WHERE s=? AND p=? LIMIT 33",
|
| 335 |
+
(item, 31 if depth == 0 else 279)).fetchall()
|
| 336 |
+
if len(rows) > 32:
|
| 337 |
+
return False
|
| 338 |
+
next_frontier.update(o for (o,) in rows if o is not None)
|
| 339 |
+
seen.update(frontier)
|
| 340 |
+
if len(seen | next_frontier) > 64:
|
| 341 |
+
return False
|
| 342 |
+
frontier = next_frontier - seen
|
| 343 |
+
if not frontier:
|
| 344 |
+
return False
|
| 345 |
+
return False
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def _creation_property_allowed(p, cands):
|
| 349 |
+
"""Creator means P170; a language's original designer may also answer creation.
|
| 350 |
+
|
| 351 |
+
Every candidate with a designer row must have positive language ancestry.
|
| 352 |
+
Never select a language merely because another reading lacks that type.
|
| 353 |
+
"""
|
| 354 |
+
if p == 170:
|
| 355 |
+
return True
|
| 356 |
+
if p != 287:
|
| 357 |
+
return False
|
| 358 |
+
holders = [qid for qid, _, _ in cands[:3] if has(qid, p)]
|
| 359 |
+
return bool(holders) and all(_programming_language(qid) for qid in holders)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
def _label_routes(q):
|
| 363 |
"""the question names the relation: (relation, subject text, the asked phrase)"""
|
| 364 |
al = _load()["alias"]
|
|
|
|
| 450 |
sp = L.link(q, 3)[:3]
|
| 451 |
if pinned:
|
| 452 |
sp = [(pinned[0], [(pinned[1], 10 ** 6, label(pinned[1]))])] + [x for x in sp if L.norm(x[0]) != L.norm(pinned[0])]
|
| 453 |
+
agent = AGENT.match(q)
|
| 454 |
+
asks_creation = bool(agent and agent.group("v").lower() == "created")
|
| 455 |
found = []
|
| 456 |
for span, cands in sp: # PATTERN route: the first span whose reading holds the relation
|
| 457 |
if XR.hop(q, span) or not XR.subject_ok(q, span):
|
| 458 |
continue
|
| 459 |
for (p, d), route, pat in _routes(q, span, cands):
|
| 460 |
+
if asks_creation and (d != "fwd" or not _creation_property_allowed(p, cands)):
|
| 461 |
+
continue
|
| 462 |
if p in XR.TIME_VARIANT and XR.PAST.search(q):
|
| 463 |
continue
|
| 464 |
+
v, subj = _agree(q, span, cands, p, d, {"created"} if asks_creation and p == 287 else ())
|
| 465 |
if v is not None and fits(q, p, d, v):
|
| 466 |
found.append((p, d, v, subj, route, pat))
|
| 467 |
break
|
|
|
|
| 475 |
span, cands = ysp[0]
|
| 476 |
hits = []
|
| 477 |
for p in sorted(props):
|
| 478 |
+
if asks_creation and not _creation_property_allowed(p, cands):
|
| 479 |
+
continue
|
| 480 |
if p in XR.TIME_VARIANT and XR.PAST.search(q):
|
| 481 |
continue
|
| 482 |
+
v, subj = _agree(q, span, cands, p, "fwd", {"created"} if asks_creation and p == 287 else ())
|
| 483 |
if v is not None and fits(q, p, "fwd", v):
|
| 484 |
hits.append((p, "fwd", v, subj, "when" if asked.startswith("when:") else "count" if asked.startswith("count:") else "agent" if asked.startswith("agent:") else "label", asked))
|
| 485 |
if len({h[2] for h in hits}) == 1:
|
|
|
|
| 493 |
return None # a value with no English name is not shown
|
| 494 |
shown = sorted(shown)
|
| 495 |
aw = [w for x in shown for w in L.norm(x).split() if w not in L.STOP]
|
| 496 |
+
if subj in v or (not all(isinstance(x, int) for x in v)
|
| 497 |
+
and aw and all(w in set(L.norm(q).split()) for w in aw)):
|
| 498 |
+
return None # entity identity, not overlapping labels, is restatement
|
| 499 |
text = shown[0] if len(shown) == 1 else ", ".join(shown[:-1]) + " and " + shown[-1]
|
| 500 |
return {"answer": text, "values": shown, "raw": sorted(v, key=str), "property": p, "direction": d, "subject_qid": subj,
|
| 501 |
"route": route, "pattern": pat}
|
root/wander-train/relation_spec_v1.py
CHANGED
|
@@ -11,6 +11,7 @@ A question is linked ONCE per turn however many relation specialists look at it
|
|
| 11 |
import collections
|
| 12 |
import functools
|
| 13 |
import json
|
|
|
|
| 14 |
|
| 15 |
import exact_router_v1 as X
|
| 16 |
|
|
@@ -21,6 +22,11 @@ _anc, _cls, _props = {}, {}, {}
|
|
| 21 |
|
| 22 |
|
| 23 |
def prop_label(p):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
if not _PL:
|
| 25 |
try:
|
| 26 |
with open(PROP_LABELS, encoding="utf-8") as f:
|
|
@@ -128,8 +134,64 @@ def kind_conflict(q, did):
|
|
| 128 |
return bool(named) and did in KIND_NOUNS and did not in named
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def answer(q, did, patterns):
|
| 132 |
"""patterns: {"fwd|key|word": [p, ...], "inv|key|word": [p, ...]} -> {answer, property, subject_qid, direction, pattern} or None"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
a = analyse(str(q))
|
| 134 |
if a is None or kind_conflict(str(q), did):
|
| 135 |
return None
|
|
|
|
| 11 |
import collections
|
| 12 |
import functools
|
| 13 |
import json
|
| 14 |
+
import re
|
| 15 |
|
| 16 |
import exact_router_v1 as X
|
| 17 |
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
def prop_label(p):
|
| 25 |
+
# The shipped label dump includes aliases ("wives", "written by", "wrote")
|
| 26 |
+
# for these properties. Those aliases can misstate the role in a receipt.
|
| 27 |
+
canonical = {26: "spouse", 86: "composer", 800: "notable work"}
|
| 28 |
+
if p in canonical:
|
| 29 |
+
return canonical[p]
|
| 30 |
if not _PL:
|
| 31 |
try:
|
| 32 |
with open(PROP_LABELS, encoding="utf-8") as f:
|
|
|
|
| 134 |
return bool(named) and did in KIND_NOUNS and did not in named
|
| 135 |
|
| 136 |
|
| 137 |
+
_COMPOSER_REQUEST = re.compile(
|
| 138 |
+
r"^(?:who composed(?: the (?:music|score|soundtrack) (?:for|of|to))?"
|
| 139 |
+
r"|who (?:is|was) the composer of"
|
| 140 |
+
r"|who wrote the (?:music|score|soundtrack) (?:for|of|to)"
|
| 141 |
+
r"|which composer wrote)\s+(?P<title>.+?)$", re.I)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _composer_request(q):
|
| 145 |
+
"""Untrusted, whole-question recognizer: musical composition means P86.
|
| 146 |
+
|
| 147 |
+
A mined correlation with P800 (notable work) cannot establish the role.
|
| 148 |
+
Keep the entire title; do not silently fall back to a linked title fragment.
|
| 149 |
+
"""
|
| 150 |
+
text = " ".join(str(q).split()).rstrip("?.! ")
|
| 151 |
+
text = re.sub(r"^(?:please tell me|(?:can|could) you (?:please )?tell me)\s+", "", text, flags=re.I)
|
| 152 |
+
match = _COMPOSER_REQUEST.fullmatch(text)
|
| 153 |
+
if not match:
|
| 154 |
+
return None
|
| 155 |
+
title = match.group("title").strip()
|
| 156 |
+
title = re.sub(r"^the (?:film|movie)\s+", "", title, flags=re.I)
|
| 157 |
+
dated = re.fullmatch(r"(.+?)\s*\((\d{4})\)", title)
|
| 158 |
+
year = dated.group(2) if dated else None
|
| 159 |
+
title = (dated.group(1) if dated else title).strip(" \"'“”‘’")
|
| 160 |
+
return title, year
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _composer_answer(q, did, request):
|
| 164 |
+
title, year = request
|
| 165 |
+
# Fetch one extra candidate so a capped name lookup is not mistaken for full
|
| 166 |
+
# coverage. Domain and optional publication year must select agreeing rows.
|
| 167 |
+
candidates = list(X.L.candidates(X.norm(title), 65)) if title else []
|
| 168 |
+
if not candidates and title.lower().startswith("the "):
|
| 169 |
+
candidates = list(X.L.candidates(X.norm(title[4:]), 65))
|
| 170 |
+
if not candidates or len(candidates) > 64:
|
| 171 |
+
return None
|
| 172 |
+
mine = [c[0] for c in candidates if in_domain(c[0], did)]
|
| 173 |
+
if year:
|
| 174 |
+
mine = [qid for qid in mine if any(
|
| 175 |
+
re.fullmatch(r"\+?" + year + r"(?:-\d{2}-\d{2}(?:T[^\s]+)?)?", str(value))
|
| 176 |
+
for value in values(qid, 577))]
|
| 177 |
+
sets = [values(qid, 86) for qid in mine]
|
| 178 |
+
if not mine or any(not found for found in sets) or len(set(sets)) != 1 or len(sets[0]) != 1:
|
| 179 |
+
return None
|
| 180 |
+
val = next(iter(sets[0]))
|
| 181 |
+
if _restates(q, val):
|
| 182 |
+
return None
|
| 183 |
+
return {"answer": X._show(val, q), "property": 86, "subject_qid": mine[0],
|
| 184 |
+
"direction": "fwd", "pattern": "explicit|composer|P86",
|
| 185 |
+
"linked_qid": mine[0]}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
def answer(q, did, patterns):
|
| 189 |
"""patterns: {"fwd|key|word": [p, ...], "inv|key|word": [p, ...]} -> {answer, property, subject_qid, direction, pattern} or None"""
|
| 190 |
+
request = _composer_request(q)
|
| 191 |
+
if request is not None:
|
| 192 |
+
if kind_conflict(str(q), did):
|
| 193 |
+
return None
|
| 194 |
+
return _composer_answer(q, did, request)
|
| 195 |
a = analyse(str(q))
|
| 196 |
if a is None or kind_conflict(str(q), did):
|
| 197 |
return None
|
root/wander-train/word_problems_v1.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
"""word_problems_v1 - prose to equations for the recurring shapes of textbook word problems.
|
| 2 |
|
|
|
|
|
|
|
| 3 |
Each SCHEMA is a stored form: trigger words, the TYPED quantities it needs (money, percent, time, length ...), and a
|
| 4 |
formula. A schema fires only when every quantity it needs is found with the right type AND the question asks for the
|
| 5 |
kind of quantity the formula produces (the CHECK: units must compose - money x rate x years -> money). Quantities are
|
|
@@ -178,38 +180,158 @@ def s_convert(t, q, qs):
|
|
| 178 |
tab[src[0][2]], tab[to], to)]}
|
| 179 |
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
def s_geometry(t, q, qs):
|
| 182 |
-
|
|
|
|
| 183 |
tl = t.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
def near(word):
|
| 186 |
-
|
| 187 |
-
return num(m.group(1)) if m else None
|
| 188 |
if "rectangle" in tl or "rectangular" in tl:
|
| 189 |
ln, wd = near("length"), near("width")
|
| 190 |
if ln is not None and wd is not None:
|
| 191 |
if re.search(r"\barea\b", q):
|
| 192 |
-
return
|
| 193 |
if re.search(r"perimeter", q):
|
| 194 |
-
return
|
| 195 |
if "circle" in tl or "circular" in tl:
|
| 196 |
r, d = near("radius"), near("diameter")
|
| 197 |
r = r if r is not None else (d / 2 if d is not None else None)
|
| 198 |
if r is not None:
|
| 199 |
pi = 3.14 if "3.14" in tl else math.pi
|
| 200 |
if re.search(r"\barea\b", q):
|
| 201 |
-
return
|
| 202 |
if re.search(r"circumference", q):
|
| 203 |
-
return
|
| 204 |
if re.search(r"\bvolume\b", q):
|
| 205 |
r, h = near("radius"), near("height")
|
| 206 |
pi = 3.14 if "3.14" in tl else math.pi
|
| 207 |
if "cone" in tl and r is not None and h is not None:
|
| 208 |
-
return
|
| 209 |
if "cylinder" in tl and r is not None and h is not None:
|
| 210 |
-
return
|
| 211 |
if "sphere" in tl and r is not None:
|
| 212 |
-
return
|
| 213 |
return None
|
| 214 |
|
| 215 |
|
|
|
|
| 1 |
"""word_problems_v1 - prose to equations for the recurring shapes of textbook word problems.
|
| 2 |
|
| 3 |
+
This Python recognizer is untrusted; its checks are not proof or admission authority.
|
| 4 |
+
|
| 5 |
Each SCHEMA is a stored form: trigger words, the TYPED quantities it needs (money, percent, time, length ...), and a
|
| 6 |
formula. A schema fires only when every quantity it needs is found with the right type AND the question asks for the
|
| 7 |
kind of quantity the formula produces (the CHECK: units must compose - money x rate x years -> money). Quantities are
|
|
|
|
| 180 |
tab[src[0][2]], tab[to], to)]}
|
| 181 |
|
| 182 |
|
| 183 |
+
def _geometry_dimensions(t, qs):
|
| 184 |
+
"""Untrusted complete binding for explicit geometry roles; no proof authority.
|
| 185 |
+
|
| 186 |
+
Return (bindings, conflict). Unknown syntax/units return (None, None).
|
| 187 |
+
Prefix roles and postfix long/wide/tall share coverage and uniqueness checks.
|
| 188 |
+
"""
|
| 189 |
+
tl = t.lower().replace("−", "-")
|
| 190 |
+
lengths = [x for x in qs if x[0] == "length"]
|
| 191 |
+
mentions = {}
|
| 192 |
+
for mention in re.finditer(r"\b(length|width|radius|diameter|height)\b", tl):
|
| 193 |
+
match = re.match(r"\s*(?:(?:is|of|exactly|equals|measures|=|:)\s*)*"
|
| 194 |
+
+ NUM + r"\s*([a-z]+)\b", tl[mention.end():])
|
| 195 |
+
if not match:
|
| 196 |
+
return None, None
|
| 197 |
+
# "in" is also a preposition. Accept the inch abbreviation only at
|
| 198 |
+
# a measurement boundary; "length 3 in the diagram" supplies no unit.
|
| 199 |
+
tail = tl[mention.end() + match.end():].lstrip()
|
| 200 |
+
if match.group(2) == "in" and tail and not re.match(r"[.,;:?!)]|(?:and|long|wide|tall|high)\b", tail):
|
| 201 |
+
return None, None
|
| 202 |
+
mentions[mention.end() + match.start(1)] = (mention.group(1), match.group(1), match.group(2))
|
| 203 |
+
for match in re.finditer(r"(?<![\w.+-])" + NUM + r"\s*([a-z]+)\s+(long|wide|tall|high)\b", tl):
|
| 204 |
+
role = {"long": "length", "wide": "width", "tall": "height", "high": "height"}[match.group(3)]
|
| 205 |
+
binding = (role, match.group(1), match.group(2))
|
| 206 |
+
if match.start(1) in mentions and mentions[match.start(1)] != binding:
|
| 207 |
+
return None, None
|
| 208 |
+
mentions[match.start(1)] = binding
|
| 209 |
+
# Every typed length must be bound, including repeats. Apartment IDs are
|
| 210 |
+
# unitless, so they do not masquerade as dimensions or block a valid room.
|
| 211 |
+
if len(mentions) != len(lengths):
|
| 212 |
+
return None, None
|
| 213 |
+
bindings, unit_scales, records = {}, set(), {}
|
| 214 |
+
conflict = None
|
| 215 |
+
for _, (role, raw, unit_word) in sorted(mentions.items()):
|
| 216 |
+
unit = unit_of(unit_word)
|
| 217 |
+
if unit is None or unit[0] != "length":
|
| 218 |
+
return None, None
|
| 219 |
+
value, scale = num(raw), LENGTH[unit[1]]
|
| 220 |
+
current = {"value": value, "unit": unit_word}
|
| 221 |
+
if role in bindings and bindings[role] != (value, scale):
|
| 222 |
+
if bindings[role][0] * bindings[role][1] != value * scale:
|
| 223 |
+
conflict = conflict or {"role": role, "values": [records[role], current]}
|
| 224 |
+
bindings[role] = (value, scale)
|
| 225 |
+
records[role] = current
|
| 226 |
+
unit_scales.add(scale)
|
| 227 |
+
if conflict:
|
| 228 |
+
return None, conflict
|
| 229 |
+
# Conversion between mixed units needs an output-unit contract first.
|
| 230 |
+
if len(unit_scales) > 1:
|
| 231 |
+
return None, None
|
| 232 |
+
if "radius" in bindings and "diameter" in bindings:
|
| 233 |
+
if bindings["radius"][0] * 2 != bindings["diameter"][0]:
|
| 234 |
+
return None, {"role": "radius/diameter", "values": [records["radius"], records["diameter"]]}
|
| 235 |
+
return bindings, None
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _geometry_request_issue(t, q):
|
| 239 |
+
"""Bound the subject, requested measure and output unit before calculating."""
|
| 240 |
+
shapes = re.findall(r"\b(?:rectangles?|rectangular|circles?|circular|cones?|cylinders?|spheres?)\b", t, re.I)
|
| 241 |
+
if not shapes:
|
| 242 |
+
return None
|
| 243 |
+
measures = set(re.findall(r"\b(?:area|perimeter|circumference|volume)\b", q))
|
| 244 |
+
if not measures:
|
| 245 |
+
return None
|
| 246 |
+
if len(measures) > 1:
|
| 247 |
+
return "I can calculate one geometry measure per question. Please ask for each requested measure separately."
|
| 248 |
+
# These are subject multiplicity markers, not arbitrary numbers: an
|
| 249 |
+
# apartment identifier is not a count of rectangles or a dimension.
|
| 250 |
+
plural = re.search(r"\b(?:rectangles|circles|cones|cylinders|spheres|rooms|boxes)\b", t, re.I)
|
| 251 |
+
counted = re.search(r"\b(?:" + "|".join(w for w, v in WORDNUM.items() if v != 1)
|
| 252 |
+
+ r"|[2-9]\d*|1\d+)\s+(?:(?:identical|rectangular|circular)\s+)*"
|
| 253 |
+
r"(?:room|box|rectangle|circle|cone|cylinder|sphere)\b", t, re.I)
|
| 254 |
+
if len(shapes) > 1 or plural or counted or re.search(r"\b(?:their|both)\b", q):
|
| 255 |
+
return "I can calculate one shape at a time; this question refers to multiple shapes."
|
| 256 |
+
for requested in re.finditer(r"\b(?:in|into|to)\s+(?:(square|cubic|sq\.?|cu\.?)\s+|(units?\s+of)\s+)?"
|
| 257 |
+
r"([a-z]+)(?:\^?[23]|[²³])?\b", q):
|
| 258 |
+
unit = unit_of(requested.group(3))
|
| 259 |
+
if requested.group(1) or requested.group(2) or (unit and unit[0] == "length"):
|
| 260 |
+
return "I cannot yet bind the requested output unit for this geometry question."
|
| 261 |
+
return None
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def explain_refusal(text):
|
| 265 |
+
"""Explain a bounded geometry conflict, without certifying an English parse."""
|
| 266 |
+
t = " ".join(str(text).split())
|
| 267 |
+
q = question_part(t)
|
| 268 |
+
issue = _geometry_request_issue(t, q)
|
| 269 |
+
if issue:
|
| 270 |
+
return {"reason": "unsupported_geometry_request", "schema": "geometry", "conflict": None, "response": issue}
|
| 271 |
+
# Do not conflate dimensions belonging to two explicitly named shapes.
|
| 272 |
+
shapes = re.findall(r"\b(?:rectangle|rectangular|circle|circular|cone|cylinder|sphere)\b", t, re.I)
|
| 273 |
+
if len(shapes) != 1 or MULTISTEP.search(t) or not re.search(r"\b(?:area|perimeter|circumference|volume)\b", q):
|
| 274 |
+
return None
|
| 275 |
+
_, conflict = _geometry_dimensions(t, quantities(t))
|
| 276 |
+
if not conflict:
|
| 277 |
+
return None
|
| 278 |
+
values = conflict["values"]
|
| 279 |
+
if conflict["role"] == "radius/diameter":
|
| 280 |
+
message = ("The supplied radius (%g %s) and diameter (%g %s) disagree: "
|
| 281 |
+
"the diameter must be twice the radius. Which measurement should I use?" %
|
| 282 |
+
(values[0]["value"], values[0]["unit"], values[1]["value"], values[1]["unit"]))
|
| 283 |
+
else:
|
| 284 |
+
message = ("The supplied %s values disagree (%g %s and %g %s). Which %s should I use?" %
|
| 285 |
+
(conflict["role"], values[0]["value"], values[0]["unit"],
|
| 286 |
+
values[1]["value"], values[1]["unit"], conflict["role"]))
|
| 287 |
+
return {"reason": "conflicting_dimension", "schema": "geometry", "conflict": conflict, "response": message}
|
| 288 |
+
|
| 289 |
+
|
| 290 |
def s_geometry(t, q, qs):
|
| 291 |
+
if _geometry_request_issue(t, q):
|
| 292 |
+
return None
|
| 293 |
tl = t.lower()
|
| 294 |
+
bindings, conflict = _geometry_dimensions(t, qs)
|
| 295 |
+
if bindings is None:
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
# Every binding has the same checked length scale. Keep that unit and
|
| 299 |
+
# raise its dimension with the formula; never relabel raw cm as meters.
|
| 300 |
+
units_by_scale = {LENGTH[u]: u for u in ("mm", "cm", "m", "km", "in", "ft", "yd", "mi")}
|
| 301 |
+
base_unit = units_by_scale.get(next(iter(bindings.values()))[1]) if bindings else None
|
| 302 |
+
if base_unit is None:
|
| 303 |
+
return None
|
| 304 |
+
|
| 305 |
+
def result(value, power, steps):
|
| 306 |
+
return {"value": value, "unit": base_unit if power == 1 else "%s^%d" % (base_unit, power), "steps": steps}
|
| 307 |
|
| 308 |
def near(word):
|
| 309 |
+
return bindings[word][0] if word in bindings else None
|
|
|
|
| 310 |
if "rectangle" in tl or "rectangular" in tl:
|
| 311 |
ln, wd = near("length"), near("width")
|
| 312 |
if ln is not None and wd is not None:
|
| 313 |
if re.search(r"\barea\b", q):
|
| 314 |
+
return result(ln * wd, 2, ["A = L x W = %g x %g" % (ln, wd)])
|
| 315 |
if re.search(r"perimeter", q):
|
| 316 |
+
return result(2 * (ln + wd), 1, ["P = 2(L + W) = 2(%g + %g)" % (ln, wd)])
|
| 317 |
if "circle" in tl or "circular" in tl:
|
| 318 |
r, d = near("radius"), near("diameter")
|
| 319 |
r = r if r is not None else (d / 2 if d is not None else None)
|
| 320 |
if r is not None:
|
| 321 |
pi = 3.14 if "3.14" in tl else math.pi
|
| 322 |
if re.search(r"\barea\b", q):
|
| 323 |
+
return result(pi * r * r, 2, ["A = pi r^2, r = %g" % r])
|
| 324 |
if re.search(r"circumference", q):
|
| 325 |
+
return result(2 * pi * r, 1, ["C = 2 pi r, r = %g" % r])
|
| 326 |
if re.search(r"\bvolume\b", q):
|
| 327 |
r, h = near("radius"), near("height")
|
| 328 |
pi = 3.14 if "3.14" in tl else math.pi
|
| 329 |
if "cone" in tl and r is not None and h is not None:
|
| 330 |
+
return result(pi * r * r * h / 3, 3, ["V = (1/3) pi r^2 h, r = %g, h = %g" % (r, h)])
|
| 331 |
if "cylinder" in tl and r is not None and h is not None:
|
| 332 |
+
return result(pi * r * r * h, 3, ["V = pi r^2 h, r = %g, h = %g" % (r, h)])
|
| 333 |
if "sphere" in tl and r is not None:
|
| 334 |
+
return result(4 / 3 * pi * r ** 3, 3, ["V = (4/3) pi r^3, r = %g" % r])
|
| 335 |
return None
|
| 336 |
|
| 337 |
|
tests/conversation_grammar/test_m53_dummy_it.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Untrusted handler tests: real AO conversion and AN recording, controlled AM replies.
|
| 2 |
+
|
| 3 |
+
No full Engine or source stores are loaded. These tests establish conversion
|
| 4 |
+
behavior, not whether the full engine can compute the requested duration/cost.
|
| 5 |
+
"""
|
| 6 |
+
from copy import deepcopy
|
| 7 |
+
import importlib.util
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import sys
|
| 10 |
+
import types
|
| 11 |
+
import unittest
|
| 12 |
+
from unittest.mock import patch
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 15 |
+
WANDER = ROOT / 'root/wander-train'
|
| 16 |
+
MIRROR = ROOT / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_ao.py'
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load(name, path):
|
| 20 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 21 |
+
module = importlib.util.module_from_spec(spec)
|
| 22 |
+
sys.modules[name] = module
|
| 23 |
+
spec.loader.exec_module(module)
|
| 24 |
+
return module
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def answer(body):
|
| 28 |
+
return {'status': 'answered', 'path': 'verified:test', 'response': body}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def withheld():
|
| 32 |
+
return {'status': 'withheld', 'path': 'missing:test', 'response': 'No answer for that question.'}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Plain:
|
| 36 |
+
def __init__(self):
|
| 37 |
+
self.dialogue = {}
|
| 38 |
+
self.calls = []
|
| 39 |
+
self.routes = {}
|
| 40 |
+
|
| 41 |
+
def chat(self, text, session_id='default'):
|
| 42 |
+
self.calls.append((text, session_id))
|
| 43 |
+
result = deepcopy(self.routes.get(text, withheld()))
|
| 44 |
+
self.dialogue.setdefault(session_id, {})['last_result'] = deepcopy(result)
|
| 45 |
+
return result
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class DummyItTests(unittest.TestCase):
|
| 49 |
+
def setUp(self):
|
| 50 |
+
self.modules = patch.dict(sys.modules, {'oracle_release_runtime_am':
|
| 51 |
+
types.SimpleNamespace(OracleReleaseRuntimeAM=Plain)})
|
| 52 |
+
self.modules.start()
|
| 53 |
+
load('oracle_release_runtime_an', WANDER / 'oracle_release_runtime_an.py')
|
| 54 |
+
self.ao = load('oracle_release_runtime_ao', WANDER / 'oracle_release_runtime_ao.py')
|
| 55 |
+
self.rt = self.ao.OracleReleaseRuntimeAO()
|
| 56 |
+
self.rt.dialogue['s'] = {'last_topic': 'that times 2'}
|
| 57 |
+
self.rt.routes['Summarize that times 2.'] = answer('A summary unrelated to transfer duration.')
|
| 58 |
+
|
| 59 |
+
def tearDown(self):
|
| 60 |
+
self.modules.stop()
|
| 61 |
+
|
| 62 |
+
def test_recorded_transfer_failure_does_not_become_old_topic_summary(self):
|
| 63 |
+
question = 'How long does it take to transfer 1GB at100Mbps?'
|
| 64 |
+
reply = self.rt.chat(question, 's')
|
| 65 |
+
self.assertEqual(reply, withheld())
|
| 66 |
+
self.assertEqual(self.rt.calls, [(question, 's'), (question, 's')])
|
| 67 |
+
self.assertEqual(len(self.rt.path('s')), 1)
|
| 68 |
+
self.assertEqual(self.rt.path('s')[0]['in_surface'], question)
|
| 69 |
+
self.assertNotIn('conversion', self.rt.path('s')[0])
|
| 70 |
+
|
| 71 |
+
def test_duration_and_cost_grammar_generalizes_beyond_transfer_wording(self):
|
| 72 |
+
questions = ['How long will it take to boil 2 litres of water?',
|
| 73 |
+
'How much does it cost to run a 2 kW heater for 3 hours?',
|
| 74 |
+
'What would it take to build a bridge?',
|
| 75 |
+
'Does it take 1.5 hours to charge a battery?',
|
| 76 |
+
'Would it cost $2.50 to run the heater?',
|
| 77 |
+
'How much time has it taken to compile the program?',
|
| 78 |
+
'HOW LONG COULD IT TAKE TO WALK 10 KM?']
|
| 79 |
+
for question in questions:
|
| 80 |
+
with self.subTest(question=question):
|
| 81 |
+
self.assertEqual(self.rt._convert(question, 's'), (None, None))
|
| 82 |
+
|
| 83 |
+
def test_real_referential_it_and_other_pronouns_still_resolve(self):
|
| 84 |
+
self.rt.dialogue['s']['last_topic'] = 'Moon'
|
| 85 |
+
for question, expected in [('Who discovered it?', 'Who discovered Moon?'),
|
| 86 |
+
('What is it made of?', 'What is Moon made of?'),
|
| 87 |
+
('What lives there?', 'What lives in Moon?'),
|
| 88 |
+
('How long does it take?', 'How long does Moon take?'),
|
| 89 |
+
('Does it take photos?', 'Does Moon take photos?'),
|
| 90 |
+
('Does it take photos to upload?', 'Does Moon take photos to upload?'),
|
| 91 |
+
('Does it take me to London?', 'Does Moon take me to London?')]:
|
| 92 |
+
with self.subTest(question=question):
|
| 93 |
+
self.assertEqual(self.rt._convert(question, 's'), (expected, 'REFERENT_SUBSTITUTION'))
|
| 94 |
+
|
| 95 |
+
def test_dummy_it_does_not_hide_a_later_real_reference(self):
|
| 96 |
+
self.rt.dialogue['s']['last_topic'] = 'the battery'
|
| 97 |
+
question = 'How long does it take to charge that?'
|
| 98 |
+
rewritten = 'How long does it take to charge the battery?'
|
| 99 |
+
self.assertEqual(self.rt._convert(question, 's'), (rewritten, 'REFERENT_SUBSTITUTION'))
|
| 100 |
+
self.rt.routes[rewritten] = answer('Two hours. [source:test]')
|
| 101 |
+
reply = self.rt.chat(question, 's')
|
| 102 |
+
self.assertEqual(reply['response'], 'Two hours. [source:test]')
|
| 103 |
+
self.assertEqual(reply['conversion']['to'], rewritten)
|
| 104 |
+
self.assertEqual(len(self.rt.path('s')), 1)
|
| 105 |
+
|
| 106 |
+
def test_known_answer_and_conversation_moves_are_preserved(self):
|
| 107 |
+
question = 'How long does it take to transfer 1 GB at 100 Mbps?'
|
| 108 |
+
self.rt.routes[question] = answer('80 seconds. [source:test]')
|
| 109 |
+
self.assertEqual(self.rt.chat(question, 's'), self.rt.routes[question])
|
| 110 |
+
self.assertEqual(len(self.rt.path('s')), 1)
|
| 111 |
+
self.assertEqual(self.rt._convert('I meant the Pacific Ocean', 's'),
|
| 112 |
+
('Summarize the Pacific Ocean.', 'REPAIR_ACCEPT'))
|
| 113 |
+
self.assertEqual(self.rt._convert('What about Venus?', 's'),
|
| 114 |
+
('Summarize Venus.', 'ELLIPSIS_COMPLETION'))
|
| 115 |
+
self.assertEqual(self.rt._convert('Go on', 's'), (None, None))
|
| 116 |
+
|
| 117 |
+
def test_absent_topic_and_other_session_never_borrow_referents(self):
|
| 118 |
+
self.assertEqual(self.rt._convert('Who discovered it?', 'other'), (None, None))
|
| 119 |
+
self.assertEqual(self.rt._convert('How long does it take to charge that?', 'other'), (None, None))
|
| 120 |
+
self.assertEqual(self.rt.dialogue['s']['last_topic'], 'that times 2')
|
| 121 |
+
|
| 122 |
+
def test_source_mirror_matches(self):
|
| 123 |
+
self.assertEqual((WANDER / 'oracle_release_runtime_ao.py').read_bytes(), MIRROR.read_bytes())
|
| 124 |
+
|
| 125 |
+
if __name__ == '__main__':
|
| 126 |
+
unittest.main()
|
tests/conversation_grammar_r468/test_door_dummy_it.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Untrusted tests of real r468 and its real obligation compiler, without an Engine.
|
| 2 |
+
|
| 3 |
+
The controlled parent supplies explicit replies. No real transfer-duration
|
| 4 |
+
answer or privacy-guard implementation is claimed by these component tests.
|
| 5 |
+
"""
|
| 6 |
+
import ast
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
import importlib.util
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import sys
|
| 11 |
+
import types
|
| 12 |
+
import unittest
|
| 13 |
+
from unittest.mock import patch
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 16 |
+
WANDER = ROOT / 'root/wander-train'
|
| 17 |
+
ADMITTED = ROOT / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted'
|
| 18 |
+
QUESTION = 'How long does it take to transfer 1GB at100Mbps?'
|
| 19 |
+
GENERIC = {'status':'clarification', 'path':'act:clarify', 'response':"I didn't fully parse that, so I won't guess.",
|
| 20 |
+
'provenance':{'kind':'dialogue_state'}}
|
| 21 |
+
PRIVATE = {'status':'withheld', 'path':'precision_guard:G2_private_state',
|
| 22 |
+
'response':'That depends on something only you know; my records cannot tell me.',
|
| 23 |
+
'provenance':{'kind':'precision_guard','rule':'G2_private_state'}}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load(name, path):
|
| 27 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 28 |
+
module = importlib.util.module_from_spec(spec)
|
| 29 |
+
sys.modules[name] = module
|
| 30 |
+
spec.loader.exec_module(module)
|
| 31 |
+
return module
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Parent:
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self.routes = {}
|
| 37 |
+
self.calls = []
|
| 38 |
+
|
| 39 |
+
def chat(self, text, session_id='default'):
|
| 40 |
+
self.calls.append((text, session_id))
|
| 41 |
+
return deepcopy(self.routes.get(text, GENERIC))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DoorGrammarTests(unittest.TestCase):
|
| 45 |
+
def setUp(self):
|
| 46 |
+
self.modules = patch.dict(sys.modules, {'oracle_release_runtime_w313':
|
| 47 |
+
types.SimpleNamespace(OracleReleaseRuntimeW313=Parent)})
|
| 48 |
+
self.modules.start()
|
| 49 |
+
load('oracle_program_ir_v1', WANDER / 'oracle_program_ir_v1.py')
|
| 50 |
+
self.module = load('oracle_release_runtime_w468', ADMITTED / 'oracle_release_runtime_w468.py')
|
| 51 |
+
self.rt = self.module.OracleReleaseRuntimeW468()
|
| 52 |
+
|
| 53 |
+
def tearDown(self):
|
| 54 |
+
self.modules.stop()
|
| 55 |
+
|
| 56 |
+
def seed_arithmetic(self):
|
| 57 |
+
for question, result in [('What is 6 multiplied by 7?', '42'), ('that times 2', '84')]:
|
| 58 |
+
self.rt.routes[question] = {'status':'answered','path':'compute:verified','response':result}
|
| 59 |
+
self.rt.chat(question, 'arithmetic')
|
| 60 |
+
self.assertEqual(self.rt._r468_thread('arithmetic'), ['multiplied', 'times'])
|
| 61 |
+
self.rt.calls.clear()
|
| 62 |
+
|
| 63 |
+
def test_actual_arithmetic_thread_does_not_ask_dummy_it_to_choose_old_words(self):
|
| 64 |
+
self.seed_arithmetic()
|
| 65 |
+
actual = self.rt.chat(QUESTION, 'arithmetic')
|
| 66 |
+
self.assertEqual(actual, GENERIC)
|
| 67 |
+
self.assertEqual(self.rt.calls, [(QUESTION, 'arithmetic')])
|
| 68 |
+
self.assertNotIn('r468_door', actual['provenance'])
|
| 69 |
+
|
| 70 |
+
def test_same_exclusion_applies_to_binding_and_clarification(self):
|
| 71 |
+
self.seed_arithmetic()
|
| 72 |
+
questions = [QUESTION, 'How much does it cost to run a heater?',
|
| 73 |
+
'What would it take to build a bridge?',
|
| 74 |
+
'Does it take 1.5 hours to charge a battery?',
|
| 75 |
+
'Would it cost $2.50 to run the heater?',
|
| 76 |
+
'How much time has it taken to compile the program?']
|
| 77 |
+
for question in questions:
|
| 78 |
+
with self.subTest(question=question):
|
| 79 |
+
self.assertIsNone(self.rt._r468_bind(question, 'times'))
|
| 80 |
+
self.assertIsNone(self.rt._r468_door(question, 'arithmetic', deepcopy(GENERIC)))
|
| 81 |
+
|
| 82 |
+
def test_later_genuine_reference_is_bound_or_asked_about_instead(self):
|
| 83 |
+
self.rt._r468_thread('s').append('battery')
|
| 84 |
+
question = 'How long does it take to charge that?'
|
| 85 |
+
self.assertEqual(self.rt._r468_bind(question, 'battery'),
|
| 86 |
+
'How long does it take to charge battery?')
|
| 87 |
+
actual = self.rt.chat(question, 's')
|
| 88 |
+
self.assertEqual(actual['provenance']['r468_door']['phrase'], 'that')
|
| 89 |
+
self.assertEqual(actual['provenance']['r468_door']['offered'], ['battery'])
|
| 90 |
+
self.assertEqual(self.rt._r468_bind('How long does it take to cool its contents?', 'battery'),
|
| 91 |
+
"How long does it take to cool battery's contents?")
|
| 92 |
+
|
| 93 |
+
def test_real_reference_binds_and_preserves_evidence_from_same_session(self):
|
| 94 |
+
self.rt._r468_thread('s').append('Eiffel Tower')
|
| 95 |
+
source = {'status':'answered', 'path':'verified:place', 'response':'It is in Paris, France. [source:place]',
|
| 96 |
+
'references':['source:place']}
|
| 97 |
+
self.rt.routes['Where is Eiffel Tower?'] = source
|
| 98 |
+
actual = self.rt.chat('Where is it?', 's')
|
| 99 |
+
self.assertEqual(actual['provenance']['r468_door']['act'], 'bound')
|
| 100 |
+
self.assertEqual(actual['references'], ['source:place'])
|
| 101 |
+
self.assertEqual(self.rt.calls, [('Where is it?', 's'), ('Where is Eiffel Tower?', 's::r468')])
|
| 102 |
+
|
| 103 |
+
def test_direct_object_it_and_non_dummy_possessives_are_unchanged(self):
|
| 104 |
+
for question, expected in [('Does it take me to London?', 'Does train take me to London?'),
|
| 105 |
+
('Does it take photos to upload?', 'Does train take photos to upload?'),
|
| 106 |
+
('How long does it take?', 'How long does train take?'),
|
| 107 |
+
('What is its speed?', "What is train's speed?")]:
|
| 108 |
+
with self.subTest(question=question):
|
| 109 |
+
self.assertEqual(self.rt._r468_bind(question, 'train'), expected)
|
| 110 |
+
|
| 111 |
+
def test_private_refusal_is_never_rebound_or_replaced(self):
|
| 112 |
+
self.seed_arithmetic()
|
| 113 |
+
for question in ['What is my password?',
|
| 114 |
+
'How long does it take to recover my password?',
|
| 115 |
+
'How long does it take to read that in another conversation?']:
|
| 116 |
+
with self.subTest(question=question):
|
| 117 |
+
self.rt.routes[question] = deepcopy(PRIVATE)
|
| 118 |
+
self.rt.calls.clear()
|
| 119 |
+
self.assertEqual(self.rt.chat(question, 'arithmetic'), PRIVATE)
|
| 120 |
+
self.assertEqual(self.rt.calls, [(question, 'arithmetic')])
|
| 121 |
+
|
| 122 |
+
def test_empty_session_does_not_offer_other_sessions_subjects(self):
|
| 123 |
+
self.seed_arithmetic()
|
| 124 |
+
actual = self.rt.chat('Where is it?', 'empty')
|
| 125 |
+
self.assertEqual(actual['provenance']['r468_door']['offered'], [])
|
| 126 |
+
self.assertNotIn('times', actual['response'])
|
| 127 |
+
self.assertNotIn('multiplied', actual['response'])
|
| 128 |
+
self.assertEqual(self.rt._r468_thread('arithmetic'), ['multiplied', 'times'])
|
| 129 |
+
|
| 130 |
+
def test_answered_parent_still_passes_through(self):
|
| 131 |
+
self.rt.routes[QUESTION] = {'status':'answered','path':'specialist:transfer','response':'80 seconds.'}
|
| 132 |
+
self.assertEqual(self.rt.chat(QUESTION, 's'), self.rt.routes[QUESTION])
|
| 133 |
+
self.assertEqual(self.rt.calls, [(QUESTION, 's')])
|
| 134 |
+
|
| 135 |
+
def test_dummy_it_grammar_matches_existing_ao_without_importing_its_chain(self):
|
| 136 |
+
tree = ast.parse((WANDER / 'oracle_release_runtime_ao.py').read_text())
|
| 137 |
+
node = next(n for n in tree.body if isinstance(n, ast.Assign) and
|
| 138 |
+
any(isinstance(t, ast.Name) and t.id == 'DUMMY_IT' for t in n.targets))
|
| 139 |
+
self.assertEqual(self.module.DUMMY_IT.pattern, ast.literal_eval(node.value.args[0]))
|
| 140 |
+
|
| 141 |
+
if __name__ == '__main__':
|
| 142 |
+
unittest.main()
|
tests/conversation_guard/test_last_question.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""G2 regression using real dialogue production and guard methods, without a head."""
|
| 2 |
+
import ast
|
| 3 |
+
from copy import deepcopy
|
| 4 |
+
import importlib.util
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import sys
|
| 7 |
+
from types import ModuleType
|
| 8 |
+
import unittest
|
| 9 |
+
from unittest.mock import patch
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 12 |
+
ADMITTED = ROOT / "root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def load(name, path):
|
| 16 |
+
spec = importlib.util.spec_from_file_location(name,path)
|
| 17 |
+
module = importlib.util.module_from_spec(spec)
|
| 18 |
+
spec.loader.exec_module(module)
|
| 19 |
+
return module
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
DIALOGUE = load("actual_dialogue",ROOT / "root/wander-train/organ_dialogue_v1.py")
|
| 23 |
+
# Only the actual envelope builder is needed from w480; its module imports the
|
| 24 |
+
# head and unrelated math/relation stores. Compile this unchanged method alone.
|
| 25 |
+
source = ast.parse((ADMITTED / "oracle_release_runtime_w480.py").read_text())
|
| 26 |
+
cls = next(node for node in source.body if isinstance(node,ast.ClassDef))
|
| 27 |
+
method = next(node for node in cls.body if isinstance(node,ast.FunctionDef) and node.name == "_c480_dialogue_reply")
|
| 28 |
+
namespace = {}
|
| 29 |
+
exec(compile(ast.Module(body=[method],type_ignores=[]),"actual_w480_reply", "exec"),namespace)
|
| 30 |
+
reply_builder = namespace["_c480_dialogue_reply"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Parent:
|
| 34 |
+
def _c480_dialogue(self):
|
| 35 |
+
if not hasattr(self,"_c480_d"):
|
| 36 |
+
self._c480_d = DIALOGUE.DialogueOrgan()
|
| 37 |
+
return self._c480_d
|
| 38 |
+
|
| 39 |
+
def chat(self, text, session_id="default"):
|
| 40 |
+
dialogue = self._c480_dialogue()
|
| 41 |
+
if getattr(self,"forced_reply",None) is not None:
|
| 42 |
+
reply = deepcopy(self.forced_reply)
|
| 43 |
+
else:
|
| 44 |
+
answer = dialogue.answer(text,session_id)
|
| 45 |
+
reply = reply_builder(self,{"status":"clarification","path":"act:clarify"},answer)
|
| 46 |
+
dialogue.log(session_id,text,reply)
|
| 47 |
+
return reply
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
parent_module = ModuleType("oracle_release_runtime_w533")
|
| 51 |
+
parent_module.OracleReleaseRuntimeW533 = Parent
|
| 52 |
+
with patch.dict(sys.modules,{"oracle_release_runtime_w533":parent_module,
|
| 53 |
+
"chat_context_v2":None,"entity_linker_v2":None,"relation_spec_v1":None}):
|
| 54 |
+
GUARD = load("actual_w534",ADMITTED / "oracle_release_runtime_w534.py")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
QUESTION = "What is 6 multiplied by 7?"
|
| 58 |
+
ANSWER = "In steps: 6 x 7 = 42. The answer is 42."
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class ConversationGuard(unittest.TestCase):
|
| 62 |
+
def setUp(self):
|
| 63 |
+
self.runtime = GUARD.OracleReleaseRuntimeW534()
|
| 64 |
+
self.dialogue = self.runtime._c480_dialogue()
|
| 65 |
+
self.dialogue.log("a",QUESTION,dict(status="answered",path="compute:verified",response=ANSWER))
|
| 66 |
+
|
| 67 |
+
def history_reply(self, session="a", question="What did I just ask?"):
|
| 68 |
+
return reply_builder(self.runtime,{"status":"clarification","path":"act:clarify"},
|
| 69 |
+
self.dialogue.answer(question,session))
|
| 70 |
+
|
| 71 |
+
def test_recorded_real_repro_passes_with_exact_turn_evidence(self):
|
| 72 |
+
expected = self.history_reply()
|
| 73 |
+
self.assertTrue(GUARD.private_state("What did I just ask?"))
|
| 74 |
+
self.assertEqual(GUARD.final_guards("What did I just ask?",expected)["status"],"withheld")
|
| 75 |
+
actual = self.runtime.chat("What did I just ask?","a")
|
| 76 |
+
self.assertEqual(actual,expected)
|
| 77 |
+
self.assertEqual(actual["response"],f'You asked: "{QUESTION}" [session:turn 1]. I answered: "{ANSWER}".')
|
| 78 |
+
self.assertEqual(actual["provenance"]["turns"],[1])
|
| 79 |
+
self.assertEqual(len(self.dialogue.logs["a"]),2) # guard read added no turn
|
| 80 |
+
|
| 81 |
+
def test_empty_session_does_not_recall_another_session(self):
|
| 82 |
+
actual = self.runtime.chat("What did I just ask?","empty")
|
| 83 |
+
self.assertEqual(actual["status"],"answered")
|
| 84 |
+
self.assertIn("first thing",actual["response"])
|
| 85 |
+
self.assertEqual(actual["references"],[])
|
| 86 |
+
self.assertNotIn(QUESTION,actual["response"])
|
| 87 |
+
self.assertEqual(len(self.dialogue.logs["empty"]),1)
|
| 88 |
+
self.assertEqual(len(self.dialogue.logs["a"]),1)
|
| 89 |
+
|
| 90 |
+
def test_repeat_recall_and_supported_variants_do_not_recall_themselves(self):
|
| 91 |
+
for question in ("What did I just ask?","What was my previous question?","What did I last say?"):
|
| 92 |
+
actual = self.runtime.chat(question,"a")
|
| 93 |
+
self.assertEqual(actual["status"],"answered")
|
| 94 |
+
self.assertIn(QUESTION,actual["response"])
|
| 95 |
+
self.assertEqual(actual["provenance"]["turns"],[1])
|
| 96 |
+
self.assertEqual(len(self.dialogue.logs["a"]),4)
|
| 97 |
+
|
| 98 |
+
def test_withheld_turn_is_recalled_as_a_request_not_as_a_true_answer(self):
|
| 99 |
+
self.dialogue.log("a","What is my bank balance?",dict(status="withheld",path="precision_guard:G2_private_state",
|
| 100 |
+
response="I do not know your balance."))
|
| 101 |
+
actual = self.runtime.chat("What did I just ask?","a")
|
| 102 |
+
self.assertEqual(actual["status"],"answered")
|
| 103 |
+
self.assertIn('You asked: "What is my bank balance?"',actual["response"])
|
| 104 |
+
self.assertIn("I did not answer it",actual["response"])
|
| 105 |
+
self.assertEqual(actual["provenance"]["turns"],[2])
|
| 106 |
+
|
| 107 |
+
def test_private_and_compound_questions_are_still_withheld(self):
|
| 108 |
+
for question in ("What is my bank account balance?","What is my password?",
|
| 109 |
+
"What did I dream about last night?","What number am I thinking of?",
|
| 110 |
+
"What did I just ask? And what is my PIN?",
|
| 111 |
+
"What did I just ask and what is my password?",
|
| 112 |
+
"What did I just ask in another conversation?"):
|
| 113 |
+
with self.subTest(question=question):
|
| 114 |
+
self.runtime.forced_reply = self.history_reply()
|
| 115 |
+
actual = self.runtime.chat(question,"a")
|
| 116 |
+
self.assertEqual(actual["status"],"withheld")
|
| 117 |
+
self.assertIn("G2_private_state",actual["path"])
|
| 118 |
+
|
| 119 |
+
def test_route_or_turn_labels_do_not_license_fabricated_content(self):
|
| 120 |
+
for field, altered in (("response","Your password is swordfish."),
|
| 121 |
+
("references",[]),
|
| 122 |
+
("provenance",dict(kind="organ_dialogue",move="discourse:asked_last",turns=[99]))):
|
| 123 |
+
with self.subTest(field=field):
|
| 124 |
+
self.runtime.forced_reply = self.history_reply()
|
| 125 |
+
self.runtime.forced_reply[field] = altered
|
| 126 |
+
self.assertEqual(self.runtime.chat("What did I just ask?","a")["status"],"withheld")
|
| 127 |
+
|
| 128 |
+
def test_stale_reply_and_other_session_reply_are_withheld(self):
|
| 129 |
+
stale = self.history_reply()
|
| 130 |
+
self.dialogue.log("a","What is 9 multiplied by 9?",dict(status="answered",path="compute:verified",response="81."))
|
| 131 |
+
self.runtime.forced_reply = stale
|
| 132 |
+
self.assertEqual(self.runtime.chat("What did I just ask?","a")["status"],"withheld")
|
| 133 |
+
self.runtime.forced_reply = self.history_reply("a")
|
| 134 |
+
self.assertEqual(self.runtime.chat("What did I just ask?","b")["status"],"withheld")
|
| 135 |
+
|
| 136 |
+
def test_missing_history_handler_fails_closed(self):
|
| 137 |
+
self.runtime.forced_reply = self.history_reply()
|
| 138 |
+
self.runtime._c480_dialogue = lambda: None
|
| 139 |
+
# Invoke the real guard with a controlled parent that does not need its log.
|
| 140 |
+
with patch.object(Parent,"chat",return_value=self.runtime.forced_reply):
|
| 141 |
+
self.assertEqual(self.runtime.chat("What did I just ask?","a")["status"],"withheld")
|
| 142 |
+
|
| 143 |
+
def test_other_guards_and_quantity_computations_remain_active(self):
|
| 144 |
+
reply = dict(status="answered",path="specialist_call>x",response="The speed is 3 m/s.")
|
| 145 |
+
self.assertIn("G1_sign_kept",GUARD.final_guards("Compute speed for -3 m/s",reply)["path"])
|
| 146 |
+
self.assertIs(GUARD.final_guards("My car does 30 mpg; how far for 2 gallons?",reply),reply)
|
| 147 |
+
with patch.object(GUARD,"premise_years",return_value=("Painting",1990,[1889])):
|
| 148 |
+
self.assertIn("G3_date_premise",GUARD.final_guards("Who painted Painting in 1990?",reply)["path"])
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
unittest.main()
|
tests/usage/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Focused usage regressions
|
| 2 |
+
|
| 3 |
+
These tests exercise the changed runtime functions and actual specialist/family
|
| 4 |
+
consumers with controlled expensive parents. They do not start the full Engine,
|
| 5 |
+
download stores, rerun release admission or make theorem claims. Source defaults
|
| 6 |
+
to this checkout; use the upstream Python requirements for the SymPy tests.
|
| 7 |
+
|
| 8 |
+
From the repository root, run these selected directories individually:
|
| 9 |
+
|
| 10 |
+
```sh
|
| 11 |
+
for group in computing conversation composition evidence documents engineering research reliability reliability_route; do
|
| 12 |
+
python -B -m unittest discover -s tests/usage/$group -v || exit 1
|
| 13 |
+
done
|
| 14 |
+
for group in conversation_guard conversation_grammar conversation_grammar_r468; do
|
| 15 |
+
python -B -m unittest discover -s tests/$group -v || exit 1
|
| 16 |
+
done
|
| 17 |
+
for version in 2 3; do
|
| 18 |
+
MATH_ORGAN_SOURCE="$PWD/root/wander-train/math_organ_v$version.py" \
|
| 19 |
+
python -B -m unittest discover -s tests/usage/evidence -v || exit 1
|
| 20 |
+
done
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
The fixtures contain bounded synthetic questions, their independent expectations,
|
| 24 |
+
and selected public-fact reply shapes. Tests of the local appliance HTTP adapter,
|
| 25 |
+
Mini authority, workflow tools and data-transfer extension are not part of this
|
| 26 |
+
source contribution. No historical verdict or release hash is rewritten.
|
tests/usage/composition/test_energy_repair.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Independent requested-quantity checks for the actual v3 organ chain."""
|
| 2 |
+
import importlib
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import sys
|
| 8 |
+
import unittest
|
| 9 |
+
|
| 10 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 11 |
+
DIRECTORY = Path(os.environ.get('ENERGY_ORGAN_DIRECTORY', ROOT / 'root/specialists/energy-v3'))
|
| 12 |
+
sys.path.insert(0, str(DIRECTORY))
|
| 13 |
+
from organ_energy_v3 import EnergyOrganV3
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class EnergyRepairTests(unittest.TestCase):
|
| 17 |
+
def setUp(self):
|
| 18 |
+
self.organ = EnergyOrganV3()
|
| 19 |
+
|
| 20 |
+
def value(self, q, expected, kind=None):
|
| 21 |
+
reply = self.organ.answer(q)
|
| 22 |
+
self.assertIsNotNone(reply, q)
|
| 23 |
+
self.assertEqual(reply['status'], 'answered', reply)
|
| 24 |
+
self.assertAlmostEqual(reply['value'], expected, msg=reply)
|
| 25 |
+
self.assertTrue(math.isfinite(reply['value']))
|
| 26 |
+
if kind: self.assertEqual(reply['kind'],kind)
|
| 27 |
+
return reply
|
| 28 |
+
|
| 29 |
+
def refusal(self, q):
|
| 30 |
+
reply = self.organ.answer(q)
|
| 31 |
+
self.assertTrue(reply is None or reply['status'] != 'answered', reply)
|
| 32 |
+
return reply
|
| 33 |
+
|
| 34 |
+
def test_original_six_energy_failures(self):
|
| 35 |
+
results = {'solar_useful':1.6, 'net_energy':3.5, 'battery_roles':7.2,
|
| 36 |
+
'battery_plain':7.2, 'battery_rating_first':7.2, 'battery_invalid':None}
|
| 37 |
+
for file in ['composition/cases.json','engineering/cases.json','engineering/followup_cases.json']:
|
| 38 |
+
for c in json.loads((ROOT/'tests/usage/fixtures'/file).read_text()):
|
| 39 |
+
if c['id'] in results:
|
| 40 |
+
with self.subTest(case=c['id']):
|
| 41 |
+
value=results[c['id']]
|
| 42 |
+
if value is None: self.refusal(c['text'])
|
| 43 |
+
else: self.value(c['text'],value)
|
| 44 |
+
|
| 45 |
+
def test_runtime_role_permutations_and_kw_units(self):
|
| 46 |
+
for detail in ['with a 500 W rated inverter run a 100 W load',
|
| 47 |
+
'run a 100 W load through a 500 W rated inverter',
|
| 48 |
+
'with an inverter rated at 0.5 kW run a 0.1 kW load']:
|
| 49 |
+
self.value('How long can a 1000 Wh battery '+detail+' with 90% inverter efficiency and 80% depth of discharge?',7.2,'runtime')
|
| 50 |
+
self.value('How long can a 24 V 50 Ah battery run a 200 W load with efficiency 0.9 and usable fraction 0.8?',4.32)
|
| 51 |
+
|
| 52 |
+
def test_zero_and_full_factors(self):
|
| 53 |
+
for eff,depth,value in [(0,80,0),(90,0,0),(100,100,10)]:
|
| 54 |
+
self.value(f'How long can a 1000 Wh battery run a 100 W load with {eff}% efficiency and {depth}% depth of discharge?',value)
|
| 55 |
+
|
| 56 |
+
def test_bad_factor_ranges_and_unbound_conditions(self):
|
| 57 |
+
for adjustment in ['120% efficiency','-10% efficiency','110% depth of discharge',
|
| 58 |
+
'90% charging efficiency','90% motor efficiency','20% reserve',
|
| 59 |
+
'inverter efficiency unspecified','temperature derate 0.8',
|
| 60 |
+
'half full','a variable load','0.8 state of charge']:
|
| 61 |
+
with self.subTest(adjustment=adjustment):
|
| 62 |
+
self.refusal('How long can a 1000 Wh battery run a 100 W load with '+adjustment+'?')
|
| 63 |
+
|
| 64 |
+
def test_loss_is_not_multiplied_twice(self):
|
| 65 |
+
self.value('How long can a 1000 Wh battery run a 100 W load with 10% inverter losses and 80% depth of discharge?',7.2)
|
| 66 |
+
self.value('How long can a 1000 Wh battery run a 100 W load with 90% efficiency and 10% losses?',9)
|
| 67 |
+
self.refusal('How long can a 1000 Wh battery run a 100 W load with 90% efficiency and 20% losses?')
|
| 68 |
+
self.refusal('How long can a 1000 Wh battery run a 100 W load with 90% efficiency and 80% efficiency?')
|
| 69 |
+
|
| 70 |
+
def test_power_constraints_and_ambiguity(self):
|
| 71 |
+
for q in ['How long can a 1000 Wh battery run a 600 W load through a 500 W inverter?',
|
| 72 |
+
'How long can a 1000 Wh battery run a 0 W load?',
|
| 73 |
+
'How long can a 1000 Wh battery run a -100 W load?',
|
| 74 |
+
'How long can a 1000 Wh battery run a 100 W fan and a 200 W heater?',
|
| 75 |
+
'How long can a 1000 Wh battery with a 500 W inverter run?',
|
| 76 |
+
'How long can a 1000 Wh battery run a 100 W load with a 5 W standby draw?']:
|
| 77 |
+
self.refusal(q)
|
| 78 |
+
|
| 79 |
+
def test_conflicting_capacity_and_negative_values(self):
|
| 80 |
+
self.refusal('How long can a 1000 Wh battery with 12 V and 10 Ah run a 100 W load?')
|
| 81 |
+
self.refusal('How long can a -1000 Wh battery run a 100 W load?')
|
| 82 |
+
self.value('How long can a 120 Wh battery with 12 V and 10 Ah run a 100 W load?',1.2)
|
| 83 |
+
|
| 84 |
+
def test_remaining_uses_battery_side_withdrawal(self):
|
| 85 |
+
r=self.value('A battery initially stores 5 kWh. A 500 W load runs for 3 hours with 90% inverter efficiency. How much energy remains in the battery?',5-1.5/0.9)
|
| 86 |
+
self.assertIn('remaining energy',r['response'])
|
| 87 |
+
r=self.value('A battery initially stores 5 kWh. A 500 W load runs for 5 hours with 80% depth of discharge. How much usable energy remains?',1.5)
|
| 88 |
+
self.assertIn('usable remaining',r['response'])
|
| 89 |
+
|
| 90 |
+
def test_remaining_requires_initial_and_feasible_duration(self):
|
| 91 |
+
for q in ['A 5 kWh battery supplies a 500 W load for 3 hours. How much energy remains?',
|
| 92 |
+
'A battery initially stores 1 kWh. A 500 W load runs for 3 hours. How much energy remains?',
|
| 93 |
+
'A battery initially stores 1 kWh. A 500 W load runs for -1 hours. How much energy remains?',
|
| 94 |
+
'A battery initially stores 1 kWh. A 500 W load runs for 1 hours with 0% efficiency. How much energy remains?',
|
| 95 |
+
'A battery initially stores 1 kWh. A 500 W load runs for 1 hours and then 2 hours. How much energy remains?']:
|
| 96 |
+
self.refusal(q)
|
| 97 |
+
|
| 98 |
+
def test_storage_from_power_or_energy(self):
|
| 99 |
+
self.value('A 400 W solar panel produces power for 5 hours with 80% charging efficiency. How much energy is stored?',1.6)
|
| 100 |
+
self.value('A solar panel produces 2 kWh. A battery stores 80% of this energy. What is the stored energy?',1.6)
|
| 101 |
+
self.value('A 400 W solar panel gets 5 peak sun hours. A battery stores 80% of this energy. What is the stored energy?',1.6)
|
| 102 |
+
self.value('3 panels each produce 400 W for 5 hours. A battery stores 80% of this energy. What is the stored energy?',4.8)
|
| 103 |
+
self.value('A 400 W solar panel produces power for 5 hours. Ignoring losses, how much energy is stored?',2)
|
| 104 |
+
|
| 105 |
+
def test_storage_unbound_and_conflicting_conditions(self):
|
| 106 |
+
for q in ['A 400 W solar panel runs for 5 hours. What is the stored energy?',
|
| 107 |
+
'A 400 W solar panel runs for 5 hours. A battery stores 120% of this energy. What is the stored energy?',
|
| 108 |
+
'A 400 W solar panel runs for 5 hours. A battery stores 80% of this energy with 50% depth of discharge. What is the stored energy?',
|
| 109 |
+
'3 panels produce 400 W for 5 hours. A battery stores 80% of this energy. What is the stored energy?',
|
| 110 |
+
'A 400 W solar panel runs for -5 hours. A battery stores 80% of this energy. What is the stored energy?',
|
| 111 |
+
'A 400 W solar panel runs for 5 hours. A battery stores 80% of this energy at 90% storage efficiency. What is the stored energy?']:
|
| 112 |
+
self.refusal(q)
|
| 113 |
+
|
| 114 |
+
def test_changed_numbers_check_arithmetic_independently(self):
|
| 115 |
+
# Independent arithmetic, outside the shipped examples/templates.
|
| 116 |
+
for cap,load,eff,depth in [(1630,270,86,70),(300,50,75,40),(4200,350,95,90)]:
|
| 117 |
+
self.value(f'How long can a {cap} Wh battery run a {load} W load with {eff}% inverter efficiency and {depth}% depth of discharge?', cap*eff*depth/(10000*load))
|
| 118 |
+
|
| 119 |
+
def test_malformed_groups_and_multiple_sources_do_not_salvage_a_number(self):
|
| 120 |
+
for q in ['A battery initially stores 1,2 kWh. A 100 W load runs for 1 hour. How much energy remains?',
|
| 121 |
+
'How long can a 1,2 kWh battery run a 100 W load?',
|
| 122 |
+
'How long can a 1000 Wh battery run a 1,00 W load?',
|
| 123 |
+
'How long can a 1000 Wh battery run a 100 W load with 90,5% efficiency?',
|
| 124 |
+
'How long can two 1000 Wh batteries run a 100 W load?',
|
| 125 |
+
'How long can 2 batteries each 1000 Wh run a 100 W load?',
|
| 126 |
+
'How long can a 1000 Wh battery run two 100 W fans?']:
|
| 127 |
+
self.refusal(q)
|
| 128 |
+
self.value('How long can a 1,000 Wh battery run a 100 W load?',10)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == '__main__': unittest.main()
|
tests/usage/computing/test_base_conversion.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Narrow regression checks; no engine startup or specialist panel claim."""
|
| 2 |
+
import importlib.util
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import unittest
|
| 6 |
+
|
| 7 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 8 |
+
MODULE = Path(os.environ.get('ORACLE_OPERATOR_SPECS', ROOT / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_operator_specs_v1.py'))
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def load(name, path):
|
| 12 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 13 |
+
module = importlib.util.module_from_spec(spec)
|
| 14 |
+
spec.loader.exec_module(module)
|
| 15 |
+
return module
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
op = load('computing_test_operator', MODULE)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class BaseConversionRepair(unittest.TestCase):
|
| 22 |
+
def check(self, prompt, expected, value, source):
|
| 23 |
+
args = op._base_parse(prompt)
|
| 24 |
+
self.assertEqual((args['n'], args['frm']), (value, source))
|
| 25 |
+
actual, _ = op._base_fn(args)
|
| 26 |
+
self.assertEqual(actual, expected)
|
| 27 |
+
self.assertEqual(op._base_ref(args), expected)
|
| 28 |
+
|
| 29 |
+
def test_real_wrong_answer_and_explicit_source(self):
|
| 30 |
+
self.check('Convert binary 11111111 to decimal.', '255', 255, 2)
|
| 31 |
+
self.check('Convert 11111111 from base 2 to base 10.', '255', 255, 2)
|
| 32 |
+
self.check('Convert 17 from base 8 to base 10.', '15', 15, 8)
|
| 33 |
+
self.check('Convert 0010 from base 2 to base 10.', '2', 2, 2)
|
| 34 |
+
|
| 35 |
+
def test_hexadecimal_words_and_prefix(self):
|
| 36 |
+
for text in ('Convert hex ff to binary.', 'Convert ff from hexadecimal to binary.',
|
| 37 |
+
'What is 0xff in binary?', 'Convert +0xff to binary.'):
|
| 38 |
+
self.check(text, '11111111', 255, 16)
|
| 39 |
+
self.check('Convert 0b1010 to decimal.', '10', 10, 2)
|
| 40 |
+
self.check('Convert 0o17 to decimal.', '15', 15, 8)
|
| 41 |
+
self.check('Convert 1010 in binary to decimal.', '10', 10, 2)
|
| 42 |
+
|
| 43 |
+
def test_decimal_control_and_zero(self):
|
| 44 |
+
self.check('Convert 255 to hexadecimal.', 'ff', 255, 10)
|
| 45 |
+
self.check('Write 0 in binary.', '0', 0, 10)
|
| 46 |
+
self.check('Express +15 as hex.', 'f', 15, 10)
|
| 47 |
+
self.check('Convert decimal 255 to hex.', 'ff', 255, 10)
|
| 48 |
+
|
| 49 |
+
def test_declines_invalid_digits_signs_and_conflicting_bases(self):
|
| 50 |
+
for text in ('Convert 102 from base 2 to base 10.', 'Convert binary 102 to decimal.',
|
| 51 |
+
'Convert -10 to binary.', 'Convert -0xff to binary.',
|
| 52 |
+
'Convert 0x to decimal.', 'Convert 19 from base 8 to decimal.',
|
| 53 |
+
'Convert 10 from base 1 to decimal.', 'Convert 10 to base 37.',
|
| 54 |
+
'Convert binary 11 from base 8 to decimal.',
|
| 55 |
+
'Convert decimal 0xff to binary.'):
|
| 56 |
+
with self.subTest(text=text), self.assertRaises(op.OutOfFragment):
|
| 57 |
+
op._base_parse(text)
|
| 58 |
+
|
| 59 |
+
def test_never_takes_a_suffix_of_malformed_or_compound_input(self):
|
| 60 |
+
for text in ('Convert - 10 to binary.', 'Convert 12.5 to binary.',
|
| 61 |
+
'Convert 10 to binary and 20 to hexadecimal.',
|
| 62 |
+
'Convert 1011/2 to decimal.', 'Convert 0b102 to decimal.',
|
| 63 |
+
'Convert --10 to binary.', 'Convert ++10 to binary.'):
|
| 64 |
+
with self.subTest(text=text), self.assertRaises(op.OutOfFragment):
|
| 65 |
+
op._base_parse(text)
|
| 66 |
+
|
| 67 |
+
def test_every_source_target_base_with_independent_integer_values(self):
|
| 68 |
+
# 35*35*5 known source values; output decoding is stdlib int rather
|
| 69 |
+
# than either target-encoding implementation under test.
|
| 70 |
+
for source in range(2, 37):
|
| 71 |
+
for target in range(2, 37):
|
| 72 |
+
for digits in ('0', '1', '10', '11', '101'):
|
| 73 |
+
args = op._base_parse(f'Convert {digits} from base {source} to base {target}.')
|
| 74 |
+
expected = {'0': 0, '1': 1, '10': source,
|
| 75 |
+
'11': source+1, '101': source*source+1}[digits]
|
| 76 |
+
output, _ = op._base_fn(args)
|
| 77 |
+
self.assertEqual(int(output, target), expected)
|
| 78 |
+
self.assertEqual(op._base_ref(args), output)
|
| 79 |
+
|
| 80 |
+
def test_arithmetic_does_not_reuse_a_corrupted_decimal_binding(self):
|
| 81 |
+
args = op._base_parse('Convert binary 11111111 to decimal.')
|
| 82 |
+
args['n'] = 11111111
|
| 83 |
+
self.assertEqual(op._base_fn(args)[0], '255')
|
| 84 |
+
self.assertEqual(op._base_ref(args), '255')
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == '__main__':
|
| 88 |
+
unittest.main()
|
tests/usage/conversation/birth-replies.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"text": "When was Albert Einstein born?",
|
| 4 |
+
"reply": {
|
| 5 |
+
"status": "answered",
|
| 6 |
+
"path": "family_precedence>specialist_call>specialist:rel-engineers>engineers",
|
| 7 |
+
"response": "14 March 1879. From the Wikidata relation store: Albert Einstein (Q937) - year of birth (P569) - 14 March 1879. [rel-engineers]",
|
| 8 |
+
"provenance": {
|
| 9 |
+
"kind": "family_precedence",
|
| 10 |
+
"replaced_route": "capability:when_did",
|
| 11 |
+
"replaced_response": "[ev:direct] According to Albert Einstein, Albert Einstein (14 March 1879 – 18 April 1955) was a German-born Jewish scientist. [simplewiki_a9a88ffc711143a397966482] Albert Einstein also records: Einstein was born in Ulm, Württemberg, Germany, on 14 March 1879. [simplewiki_e131399c5b98026c642abcac]",
|
| 12 |
+
"specialist": "rel-engineers",
|
| 13 |
+
"chat_resolved": null
|
| 14 |
+
},
|
| 15 |
+
"latency_ms": 0.2,
|
| 16 |
+
"zero_model_gate": {
|
| 17 |
+
"lm_calls": 0,
|
| 18 |
+
"transformer_calls": 0
|
| 19 |
+
},
|
| 20 |
+
"evidential": {
|
| 21 |
+
"input_marked": null,
|
| 22 |
+
"answer_licensed": "direct"
|
| 23 |
+
},
|
| 24 |
+
"continuation": {
|
| 25 |
+
"doors": [
|
| 26 |
+
"germany",
|
| 27 |
+
"scientist"
|
| 28 |
+
],
|
| 29 |
+
"offer": "from here, germany or scientist stand open"
|
| 30 |
+
},
|
| 31 |
+
"references": []
|
| 32 |
+
},
|
| 33 |
+
"receipt": "var/usage-20261026/evidence/run-1/results.jsonl"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"text": "When was John Williams born?",
|
| 37 |
+
"reply": {
|
| 38 |
+
"status": "answered",
|
| 39 |
+
"path": "family_precedence>specialist_call>specialist:rel-composers>composers",
|
| 40 |
+
"response": "8 February 1932. From the Wikidata relation store: John Williams (Q131285) - year of birth (P569) - 8 February 1932. [rel-composers]",
|
| 41 |
+
"provenance": {
|
| 42 |
+
"kind": "family_precedence",
|
| 43 |
+
"replaced_route": "capability:when_did",
|
| 44 |
+
"replaced_response": "[ev:direct] According to John Williams, John Towner Williams (born February 8, 1932) is an American composer and conductor. [simplewiki_8cb1cf64a055db807a2b8983] John Williams also records: John Towner Williams was born in Queens, New York City, New York on February 8, 1932. [simplewiki_26349c5d4659",
|
| 45 |
+
"specialist": "rel-composers",
|
| 46 |
+
"chat_resolved": null
|
| 47 |
+
},
|
| 48 |
+
"latency_ms": 0.1,
|
| 49 |
+
"zero_model_gate": {
|
| 50 |
+
"lm_calls": 0,
|
| 51 |
+
"transformer_calls": 0
|
| 52 |
+
},
|
| 53 |
+
"evidential": {
|
| 54 |
+
"input_marked": null,
|
| 55 |
+
"answer_licensed": "direct"
|
| 56 |
+
},
|
| 57 |
+
"continuation": {
|
| 58 |
+
"doors": [
|
| 59 |
+
"city",
|
| 60 |
+
"composer",
|
| 61 |
+
"conductor"
|
| 62 |
+
],
|
| 63 |
+
"offer": "from here, city or composer stand open"
|
| 64 |
+
},
|
| 65 |
+
"references": []
|
| 66 |
+
},
|
| 67 |
+
"receipt": "var/usage-20261026/research/run-1/results.jsonl"
|
| 68 |
+
}
|
| 69 |
+
]
|
tests/usage/conversation/test_state.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Scoped legacy-state regressions; no engine, corpus writes, or proof claims."""
|
| 2 |
+
import importlib.util
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import sys
|
| 6 |
+
import types
|
| 7 |
+
import unittest
|
| 8 |
+
from unittest.mock import patch
|
| 9 |
+
|
| 10 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 11 |
+
spec = importlib.util.spec_from_file_location('conversation_reconciliation', ROOT/'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/runtime_conversation_state.py')
|
| 12 |
+
helper = importlib.util.module_from_spec(spec)
|
| 13 |
+
spec.loader.exec_module(helper)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ConversationTests(unittest.TestCase):
|
| 17 |
+
def setUp(self):
|
| 18 |
+
self.modules = patch.dict(sys.modules, {'relation_spec_v1': types.SimpleNamespace()})
|
| 19 |
+
self.modules.start()
|
| 20 |
+
path = ROOT/'root/wander-train/chat_context_v2.py'
|
| 21 |
+
spec = importlib.util.spec_from_file_location('chat_context_v2', path)
|
| 22 |
+
self.cc = importlib.util.module_from_spec(spec)
|
| 23 |
+
spec.loader.exec_module(self.cc)
|
| 24 |
+
sys.modules['chat_context_v2'] = self.cc
|
| 25 |
+
self.cc.recognised = lambda q: q.startswith(('Who', 'When', 'What'))
|
| 26 |
+
self.cc._entity_of_question = lambda q: None
|
| 27 |
+
self.cc._entity_of_reply = lambda reply, prefer_person=False: (1, 'Jane Austen', True) if reply.startswith('Jane Austen') else None
|
| 28 |
+
self.rt = types.SimpleNamespace(_last_value={}, _v4_disc={})
|
| 29 |
+
|
| 30 |
+
def tearDown(self):
|
| 31 |
+
self.modules.stop()
|
| 32 |
+
|
| 33 |
+
def turn(self, text, reply, sid='s', supported=True, mutate=None, runtime=None):
|
| 34 |
+
rt = runtime or self.rt
|
| 35 |
+
frame = helper.begin(rt, text, sid)
|
| 36 |
+
if mutate:
|
| 37 |
+
mutate(rt, self.cc.state(sid))
|
| 38 |
+
helper.finish(rt, text, reply, frame, supported)
|
| 39 |
+
helper.release(frame)
|
| 40 |
+
return rt._conversation_contexts[sid]
|
| 41 |
+
|
| 42 |
+
def test_final_author_supports_pronoun(self):
|
| 43 |
+
self.turn('Who wrote Emma?', {'status':'answered','path':'organ:relation>form:author','response':'Jane Austen.'})
|
| 44 |
+
frame = helper.begin(self.rt, 'When was she born?', 's')
|
| 45 |
+
self.assertEqual(self.cc._person(self.cc.state('s')), 'Jane Austen')
|
| 46 |
+
helper.release(frame)
|
| 47 |
+
self.assertNotIn('s', self.cc._STATES)
|
| 48 |
+
|
| 49 |
+
def test_unknown_new_topic_discards_previous_person_and_number(self):
|
| 50 |
+
self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'})
|
| 51 |
+
self.rt._last_value['s'] = 12
|
| 52 |
+
self.rt._v4_disc['s'] = {'entity':'Jane Austen'}
|
| 53 |
+
state = self.turn('New topic: who wrote Zeralune?', {'status':'withheld','path':'factoid:withheld'}, supported=False)
|
| 54 |
+
self.assertIsNone(self.cc._person(state))
|
| 55 |
+
self.assertNotIn('s', self.rt._last_value)
|
| 56 |
+
self.assertNotIn('s', self.rt._v4_disc)
|
| 57 |
+
|
| 58 |
+
def test_final_scalar_replaces_stale_number(self):
|
| 59 |
+
self.rt._last_value['s'] = 12
|
| 60 |
+
r={'status':'answered','path':'organ:math>evaluate','response':'[ev:derived] 126. In steps: read 126; then evaluate.',
|
| 61 |
+
'provenance':{'kind':'organ_math','directive':'evaluate'}}
|
| 62 |
+
state=self.turn('Correction: calculate 21 * 6 instead.',r)
|
| 63 |
+
self.assertEqual(self.rt._last_value['s'],126)
|
| 64 |
+
self.assertEqual(state.result,(126,''))
|
| 65 |
+
|
| 66 |
+
def test_quantity_keeps_unit_without_becoming_dimensionless_scalar(self):
|
| 67 |
+
r={'status':'answered','path':'specialist_call','response':'The answer is 300 kWh.'}
|
| 68 |
+
state=self.turn('What is the energy for 10 hours?',r)
|
| 69 |
+
self.assertEqual(state.result,(300,'kwh'))
|
| 70 |
+
self.assertNotIn('s', self.rt._last_value)
|
| 71 |
+
|
| 72 |
+
def test_acknowledgement_preserves_context(self):
|
| 73 |
+
self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'})
|
| 74 |
+
state=self.turn('Thanks', {'status':'answered','path':'act:acknowledge','response':'You are welcome.'})
|
| 75 |
+
self.assertEqual(self.cc._person(state),'Jane Austen')
|
| 76 |
+
|
| 77 |
+
def test_clarification_does_not_learn_speculative_person(self):
|
| 78 |
+
def speculate(rt,state): state.ans=(2,'Wrong Person',True)
|
| 79 |
+
state=self.turn('Who wrote unknown?',{'status':'clarification','path':'act:clarify'},supported=False,mutate=speculate)
|
| 80 |
+
self.assertIsNone(state.ans)
|
| 81 |
+
|
| 82 |
+
def test_same_session_id_in_two_instances_is_isolated(self):
|
| 83 |
+
self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'})
|
| 84 |
+
other=types.SimpleNamespace(_last_value={},_v4_disc={})
|
| 85 |
+
outer=helper.begin(self.rt,'When was she born?','s')
|
| 86 |
+
nested=helper.begin(other,'When was she born?','s')
|
| 87 |
+
self.assertIsNone(self.cc._person(self.cc.state('s')))
|
| 88 |
+
helper.release(nested)
|
| 89 |
+
self.assertEqual(self.cc._person(self.cc.state('s')),'Jane Austen')
|
| 90 |
+
helper.release(outer)
|
| 91 |
+
|
| 92 |
+
def test_different_sessions_and_nested_turn_restore_ambient_context(self):
|
| 93 |
+
self.turn('Who wrote Emma?', {'status':'answered','path':'specialist_call','response':'Jane Austen.'},sid='a')
|
| 94 |
+
a=helper.begin(self.rt,'When was she born?','a')
|
| 95 |
+
b=helper.begin(self.rt,'When was she born?','b')
|
| 96 |
+
self.assertIsNone(self.cc._person(self.cc.state('b')))
|
| 97 |
+
helper.release(b)
|
| 98 |
+
self.assertEqual(self.cc._person(self.cc.state('a')),'Jane Austen')
|
| 99 |
+
helper.release(a)
|
| 100 |
+
self.assertEqual(self.cc._STATES,{})
|
| 101 |
+
|
| 102 |
+
def test_real_direct_birth_replies_with_null_chat_resolution(self):
|
| 103 |
+
rows = json.loads((Path(__file__).with_name('birth-replies.json')).read_text())
|
| 104 |
+
for row in rows:
|
| 105 |
+
with self.subTest(question=row['text']):
|
| 106 |
+
self.assertIsNone(row['reply']['provenance']['chat_resolved'])
|
| 107 |
+
state = self.turn(row['text'], row['reply'])
|
| 108 |
+
self.assertEqual(state.q, row['text'])
|
| 109 |
+
self.assertNotIn('s', self.cc._STATES)
|
| 110 |
+
|
| 111 |
+
def test_optional_rewrite_metadata_is_typed_and_nonempty(self):
|
| 112 |
+
for prov in (None, [], 'none', {'chat_resolved': None}, {'chat_resolved': []},
|
| 113 |
+
{'chat_resolved': {'read_as': None}}, {'chat_resolved': {'read_as': ''}}):
|
| 114 |
+
with self.subTest(provenance=prov):
|
| 115 |
+
state = self.turn('When was Jane Austen born?',
|
| 116 |
+
{'status':'answered','path':'specialist_call','response':'16 December 1775.', 'provenance':prov})
|
| 117 |
+
self.assertEqual(state.q, 'When was Jane Austen born?')
|
| 118 |
+
|
| 119 |
+
def test_not_a_scalar_route_and_nonfinite_or_unit_answer_never_become_number(self):
|
| 120 |
+
for path,body in [('pretend-compute:verified','The answer is 7.'),('organ:math>evaluate','[ev:derived] 1e999. In steps:'),('organ:math>evaluate','[ev:derived] 3 kg. In steps:')]:
|
| 121 |
+
r={'status':'answered','path':path,'response':body,'provenance':{'kind':'organ_math','directive':'evaluate'}}
|
| 122 |
+
self.rt._last_value['s']=12
|
| 123 |
+
self.turn('Calculate 3',r)
|
| 124 |
+
self.assertNotIn('s',self.rt._last_value)
|
| 125 |
+
|
| 126 |
+
if __name__=='__main__': unittest.main()
|
tests/usage/documents/test_conflict_repair.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Untrusted isolated-method regression, not a full-engine or panel test.
|
| 2 |
+
|
| 3 |
+
Load actual passage helpers using AST, without importing the heavyweight engine.
|
| 4 |
+
WordNet aliases are disabled: these regressions all use literal matching terms.
|
| 5 |
+
Reads the current checkout; no source mutation.
|
| 6 |
+
"""
|
| 7 |
+
import ast
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import re
|
| 10 |
+
import time
|
| 11 |
+
import types
|
| 12 |
+
import unittest
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 15 |
+
REL = Path('root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted')
|
| 16 |
+
CANDIDATE = ROOT
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def definitions(source, names, namespace):
|
| 20 |
+
nodes = []
|
| 21 |
+
for node in ast.parse(source).body:
|
| 22 |
+
if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names:
|
| 23 |
+
nodes.append(node)
|
| 24 |
+
elif isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in names for t in node.targets):
|
| 25 |
+
nodes.append(node)
|
| 26 |
+
exec(compile(ast.Module(nodes, type_ignores=[]), '<isolated-source>', 'exec'), namespace)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def reader(source):
|
| 30 |
+
ns420 = {'re': re}
|
| 31 |
+
definitions((CANDIDATE / REL / 'oracle_release_runtime_w420.py').read_text(), {'IRREGULAR', '_stem', '_toks'}, ns420)
|
| 32 |
+
ns434 = {'re': re, '_w420': types.SimpleNamespace(_toks=ns420['_toks'])}
|
| 33 |
+
names = {'_stem', '_in_order', 'NEG', 'NEG_TOK', 'MODAL', 'PRESENT_Q', 'PAST', 'PRESENT', 'CMP_Q', 'IDENTITY', 'OB_Q', 'OB_S', 'TEMP_Q', 'TEMP_S', 'SUP_Q', 'HEDGE_S', 'USE_ORDER', 'USE_TEMPORAL', 'USE_SUPERL'}
|
| 34 |
+
text434 = (CANDIDATE / REL / 'oracle_release_runtime_w434.py').read_text()
|
| 35 |
+
definitions(text434, names, ns434)
|
| 36 |
+
cls434 = next(n for n in ast.parse(text434).body if isinstance(n, ast.ClassDef))
|
| 37 |
+
method = next(n for n in cls434.body if isinstance(n, ast.FunctionDef) and n.name == '_r434_verdict')
|
| 38 |
+
exec(compile(ast.Module([method], type_ignores=[]), '<actual-verdict>', 'exec'), ns434)
|
| 39 |
+
class Base:
|
| 40 |
+
_r434_verdict = ns434['_r434_verdict']
|
| 41 |
+
def _r434_receipt(self, out, res, text, reason):
|
| 42 |
+
return {'status': 'withheld', 'response': text, 'reason': reason}
|
| 43 |
+
ns = {'re': re, 'time': time, '_Base': Base}
|
| 44 |
+
definitions(source, {'PASSAGE', 'YESNO_HEAD', 'STOP', 'REQUIRED', '_toks', '_stem', '_sentences', '_qterms', 'OracleReleaseRuntimeW442'}, ns)
|
| 45 |
+
instance = ns['OracleReleaseRuntimeW442']()
|
| 46 |
+
instance._r436_aliases = lambda term: {}
|
| 47 |
+
return instance
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ConflictTests(unittest.TestCase):
|
| 51 |
+
@classmethod
|
| 52 |
+
def setUpClass(cls):
|
| 53 |
+
cls.after = reader((CANDIDATE / REL / 'oracle_release_runtime_w442.py').read_text())
|
| 54 |
+
|
| 55 |
+
def ask(self, engine, text):
|
| 56 |
+
return engine._r434_passage('Passage: '+text+' Question: Is the Lumen Archive open on Sunday?', {}, time.time())
|
| 57 |
+
|
| 58 |
+
def test_reproduces_and_repairs_both_orders(self):
|
| 59 |
+
yes = 'Version one says the Lumen Archive is open on Sunday.'
|
| 60 |
+
no = 'Version two says the Lumen Archive is not open on Sunday.'
|
| 61 |
+
for text in (yes+' '+no, no+' '+yes):
|
| 62 |
+
result = self.ask(self.after, text)
|
| 63 |
+
self.assertEqual(result['status'], 'withheld')
|
| 64 |
+
self.assertEqual(result['reason'], 'conflicting_covering_sentences')
|
| 65 |
+
self.assertEqual({r['text'] for r in result['references']}, {yes, no})
|
| 66 |
+
|
| 67 |
+
def test_preserves_nonconflicting_answers(self):
|
| 68 |
+
for text, expected in (
|
| 69 |
+
('The Lumen Archive is open on Sunday.', 'Yes.'),
|
| 70 |
+
('The Lumen Archive is not open on Sunday.', 'No.'),
|
| 71 |
+
('The Lumen Archive is open on Sunday. The Cedar Archive is not open on Sunday.', 'Yes.'),
|
| 72 |
+
('The Lumen Archive is open on Sunday. The Lumen Archive is open on Sunday.', 'Yes.'),
|
| 73 |
+
):
|
| 74 |
+
self.assertTrue(self.ask(self.after, text)['response'].startswith(expected))
|
| 75 |
+
|
| 76 |
+
def test_preserves_missing_and_modal_refusals(self):
|
| 77 |
+
for text in ('The Lumen Archive is in Alderport.', 'The Lumen Archive will open on Sunday.'):
|
| 78 |
+
self.assertEqual(self.ask(self.after, text)['status'], 'withheld')
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if __name__ == '__main__':
|
| 82 |
+
unittest.main()
|
tests/usage/engineering/test_condition_attachment.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Narrow source regression: trailing physical assumptions remain with their question."""
|
| 2 |
+
import importlib.util
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import unittest
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 9 |
+
SLIM = Path(os.environ.get("ENGINEERING_SLIM_ROOT", ROOT))
|
| 10 |
+
SOURCE = SLIM / "root/specialists/_gate/combo_runtime_v1.py"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_source():
|
| 14 |
+
spec = importlib.util.spec_from_file_location("engineering_condition_combo", SOURCE)
|
| 15 |
+
module = importlib.util.module_from_spec(spec)
|
| 16 |
+
spec.loader.exec_module(module)
|
| 17 |
+
return module
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@unittest.skipUnless(SOURCE.is_file(), "requires private Slim source (set ENGINEERING_SLIM_ROOT)")
|
| 21 |
+
class ConditionAttachmentTests(unittest.TestCase):
|
| 22 |
+
@classmethod
|
| 23 |
+
def setUpClass(cls):
|
| 24 |
+
cls.combo = load_source()
|
| 25 |
+
cases = json.loads((ROOT / "tests/usage/fixtures/engineering/cases.json").read_text())
|
| 26 |
+
cls.heat = next(case["text"] for case in cases if case["id"] == "heat_endpoints")
|
| 27 |
+
|
| 28 |
+
def test_exact_observed_heat_failure_through_real_thermo(self):
|
| 29 |
+
directory = SLIM / "root/specialists/thermo"
|
| 30 |
+
combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
|
| 31 |
+
reply = combo.reply(self.heat)
|
| 32 |
+
self.assertEqual(reply["status"], "answered")
|
| 33 |
+
self.assertEqual(reply["path"], "specialist:thermo>sensible_heat")
|
| 34 |
+
self.assertIn("5.016e+05 J", reply["response"])
|
| 35 |
+
self.assertIn("4180", reply["response"])
|
| 36 |
+
self.assertNotIn("4186", reply["response"])
|
| 37 |
+
self.assertNotIn("I need", reply["response"])
|
| 38 |
+
|
| 39 |
+
def test_real_combo_keeps_independent_second_task_visible(self):
|
| 40 |
+
directory = SLIM / "root/specialists/thermo"
|
| 41 |
+
combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
|
| 42 |
+
reply = combo.reply(self.heat + " What is 2+3?")
|
| 43 |
+
parts = reply["provenance"]["parts"]
|
| 44 |
+
self.assertEqual(len(parts), 2)
|
| 45 |
+
self.assertIn("4180", parts[0]["response"])
|
| 46 |
+
self.assertEqual(parts[1]["part"], "What is 2+3?")
|
| 47 |
+
self.assertIn("What is 2+3?", reply["response"])
|
| 48 |
+
|
| 49 |
+
def test_real_combo_retains_elliptical_followup(self):
|
| 50 |
+
directory = SLIM / "root/specialists/thermo"
|
| 51 |
+
combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
|
| 52 |
+
reply = combo.reply(self.heat + " How much?")
|
| 53 |
+
self.assertEqual(reply["path"], "specialist:thermo>sensible_heat")
|
| 54 |
+
self.assertIn("5.016e+05 J", reply["response"])
|
| 55 |
+
|
| 56 |
+
def test_full_short_question_before_thermo_not_swallowed(self):
|
| 57 |
+
directory = SLIM / "root/specialists/thermo"
|
| 58 |
+
combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
|
| 59 |
+
reply = combo.reply("Who wrote Hamlet? " + self.heat)
|
| 60 |
+
parts = reply["provenance"]["parts"]
|
| 61 |
+
self.assertEqual(len(parts), 2)
|
| 62 |
+
self.assertEqual(parts[0]["part"], "Who wrote Hamlet?")
|
| 63 |
+
self.assertIn("5.016e+05 J", parts[1]["response"])
|
| 64 |
+
|
| 65 |
+
def test_constant_load_request_through_real_energy(self):
|
| 66 |
+
cases = json.loads((ROOT / "tests/usage/fixtures/engineering/cases.json").read_text())
|
| 67 |
+
question = next(case["text"] for case in cases if case["id"] == "battery_roles")
|
| 68 |
+
directory = SLIM / "root/specialists/energy-v3"
|
| 69 |
+
combo = self.combo.Combo([str(directory / "manifest.json")], [str(directory)])
|
| 70 |
+
reply = combo.reply(question)
|
| 71 |
+
self.assertEqual(reply["path"], "specialist:energy-v3>runtime")
|
| 72 |
+
self.assertIn("7.2 hours", reply["response"])
|
| 73 |
+
self.assertIn("100 W load", reply["response"])
|
| 74 |
+
self.assertIn("500 W inverter rating", reply["response"])
|
| 75 |
+
|
| 76 |
+
def test_named_constant_conditions_attach(self):
|
| 77 |
+
for condition in ("The load is constant.", "The specific heat remains fixed.",
|
| 78 |
+
"Flow rate stays steady throughout.", "The gravity is constant."):
|
| 79 |
+
with self.subTest(condition=condition):
|
| 80 |
+
q = "How long can the battery run? " + condition
|
| 81 |
+
self.assertEqual(self.combo._split_parts(q), [q])
|
| 82 |
+
|
| 83 |
+
def test_declarative_condition_does_not_absorb_second_task(self):
|
| 84 |
+
first = "How long can the battery run? The load is constant."
|
| 85 |
+
second = "What is 2+3?"
|
| 86 |
+
self.assertEqual(self.combo._split_parts(first + " " + second), [first, second])
|
| 87 |
+
for other in ("The load is constant; calculate its current.", "The load is a tree.",
|
| 88 |
+
"The capital is Constant.", "The temperature is rising.",
|
| 89 |
+
"The load is constant and what is its current?"):
|
| 90 |
+
with self.subTest(other=other):
|
| 91 |
+
self.assertEqual(self.combo._split_parts("How long? " + other), ["How long?", other])
|
| 92 |
+
|
| 93 |
+
def test_condition_forms_attach(self):
|
| 94 |
+
for condition in ("Use c=4180.", "Using c=4180.", "Assume no losses.",
|
| 95 |
+
"Assuming no losses.", "Take gravity as 9.81.", "Given c=4180.",
|
| 96 |
+
"With no phase change.", "Without friction.", "Ignore heat loss.",
|
| 97 |
+
"Neglect pipe friction.", "Please use c=4180."):
|
| 98 |
+
q = "How much heat? " + condition
|
| 99 |
+
with self.subTest(condition=condition):
|
| 100 |
+
self.assertEqual(self.combo._split_parts(q), [q])
|
| 101 |
+
|
| 102 |
+
def test_multiple_conditions_and_second_question_preserved(self):
|
| 103 |
+
first = "How much heat? Use c=4180. Ignore heat loss."
|
| 104 |
+
second = "What is 2 + 3?"
|
| 105 |
+
self.assertEqual(self.combo._split_parts(first + " " + second), [first, second])
|
| 106 |
+
|
| 107 |
+
def test_second_question_with_its_own_premise_not_absorbed(self):
|
| 108 |
+
first = "What is 2 + 3?"
|
| 109 |
+
second = "Given 2 kg of water. How much heat raises it by 10 K?"
|
| 110 |
+
self.assertEqual(self.combo._split_parts(first + " " + second), [first, second])
|
| 111 |
+
|
| 112 |
+
def test_new_task_disguised_as_condition_not_absorbed(self):
|
| 113 |
+
for second in ("Use a calculator to compute 2+3.", "Use c=4180 and convert 2 m to cm.",
|
| 114 |
+
"Assume no losses and find the motor power.", "Use c=4180; what is its unit?",
|
| 115 |
+
"Take the square root of 9.", "Take a derivative of sin(x).",
|
| 116 |
+
"Assume x=2 and evaluate x+1.", "Use a dictionary."):
|
| 117 |
+
with self.subTest(second=second):
|
| 118 |
+
self.assertEqual(self.combo._split_parts("How much heat? " + second), ["How much heat?", second])
|
| 119 |
+
|
| 120 |
+
def test_plain_independent_questions_still_split(self):
|
| 121 |
+
self.assertEqual(self.combo._split_parts("What is 2+3? What is 4+5?"), ["What is 2+3?", "What is 4+5?"])
|
| 122 |
+
|
| 123 |
+
def test_quoted_and_code_questions_not_boundaries(self):
|
| 124 |
+
for quote in ('"why? use c=1."', "`why? use c=1.`", "'why? use c=1.'"):
|
| 125 |
+
first = "What does " + quote + " mean?"
|
| 126 |
+
self.assertEqual(self.combo._split_parts(first + " What is 2+3?"), [first, "What is 2+3?"])
|
| 127 |
+
|
| 128 |
+
def test_quoted_question_inside_condition_retained(self):
|
| 129 |
+
q = 'How much heat? Use c=4180 with label "why? answer." for the sample.'
|
| 130 |
+
self.assertEqual(self.combo._split_parts(q), [q])
|
| 131 |
+
|
| 132 |
+
def test_decimal_and_semicolon_conditions_retained(self):
|
| 133 |
+
q = "How much heat? Use specific heat 4.18 kJ/(kg K); ignore losses."
|
| 134 |
+
self.assertEqual(self.combo._split_parts(q), [q])
|
| 135 |
+
|
| 136 |
+
def test_unrelated_declarative_tail_stays_visible(self):
|
| 137 |
+
self.assertEqual(self.combo._split_parts("How much heat? Copper has atomic number 29."),
|
| 138 |
+
["How much heat?", "Copper has atomic number 29."])
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
unittest.main()
|
tests/usage/evidence/test_math_finite.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Focused source repair regression; no corpus, native build or specialist panel.
|
| 2 |
+
|
| 3 |
+
Set MATH_ORGAN_SOURCE to test a provisioned copy. The default is this checkout. This file does not mutate or import the full engine.
|
| 4 |
+
"""
|
| 5 |
+
import importlib.util
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import unittest
|
| 10 |
+
from unittest.mock import patch
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 13 |
+
SOURCE = Path(os.environ.get('MATH_ORGAN_SOURCE', ROOT / 'root/wander-train/math_organ_v1.py'))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@unittest.skipUnless(SOURCE.is_file(), 'optional appliance source not provisioned')
|
| 17 |
+
class FiniteMathTests(unittest.TestCase):
|
| 18 |
+
@classmethod
|
| 19 |
+
def setUpClass(cls):
|
| 20 |
+
helper_spec = importlib.util.spec_from_file_location('finite_math_helper', SOURCE.parent / 'math_organ_v1.py')
|
| 21 |
+
helper = importlib.util.module_from_spec(helper_spec)
|
| 22 |
+
helper_spec.loader.exec_module(helper)
|
| 23 |
+
spec = importlib.util.spec_from_file_location('finite_math_organ', SOURCE)
|
| 24 |
+
cls.module = importlib.util.module_from_spec(spec)
|
| 25 |
+
# v2/v3 share the helper from their own provisioned source directory.
|
| 26 |
+
with patch.dict(sys.modules, {'math_organ_v1': helper}):
|
| 27 |
+
spec.loader.exec_module(cls.module)
|
| 28 |
+
# No corpus opening: exercise the answer pipeline with method lookup absent.
|
| 29 |
+
cls.organ = cls.module.MathOrgan.__new__(cls.module.MathOrgan)
|
| 30 |
+
cls.organ.db = None
|
| 31 |
+
cls.organ.why = ''
|
| 32 |
+
|
| 33 |
+
def test_nonfinite_direct_evaluation_declined(self):
|
| 34 |
+
for expression in ('1/0', '0/0', 'oo', '-oo', 'oo-oo', '0*oo', 'log(0)'):
|
| 35 |
+
with self.subTest(expression=expression):
|
| 36 |
+
self.assertIsNone(self.organ._evaluate([expression], 'Evaluate ' + expression))
|
| 37 |
+
|
| 38 |
+
def test_real_question_route_declines_undefined(self):
|
| 39 |
+
for question in ('What is 1 / 0?', 'What is 0 / 0?', 'Evaluate log(0).'):
|
| 40 |
+
with self.subTest(question=question):
|
| 41 |
+
self.assertIsNone(self.organ.answer(question))
|
| 42 |
+
|
| 43 |
+
def test_finite_real_and_complex_results_preserved(self):
|
| 44 |
+
for expression, expected in (('17*23', '391'), ('1/3', '1/3'), ('sqrt(-1)', 'I'),
|
| 45 |
+
('(1+I)*(1-I)', '2'), ('sqrt(2)', 'sqrt(2)')):
|
| 46 |
+
with self.subTest(expression=expression):
|
| 47 |
+
result = self.organ._evaluate([expression], 'Evaluate ' + expression)
|
| 48 |
+
self.assertIsNotNone(result)
|
| 49 |
+
self.assertEqual(result['answer'], expected)
|
| 50 |
+
self.assertEqual(result['check'], 'finite numerical comparison using SymPy')
|
| 51 |
+
|
| 52 |
+
def test_equiv_does_not_accept_nan_or_infinity(self):
|
| 53 |
+
sp = self.module.sp
|
| 54 |
+
for a, b in ((sp.nan, sp.nan), (sp.zoo, sp.zoo), (sp.oo, sp.oo), (sp.oo, 1)):
|
| 55 |
+
with self.subTest(a=a, b=b):
|
| 56 |
+
self.assertFalse(self.module.equiv(sp.sympify(a), sp.sympify(b)))
|
| 57 |
+
|
| 58 |
+
def test_numerical_comparison_checks_all_complex_components(self):
|
| 59 |
+
sp = self.module.sp
|
| 60 |
+
compare = self.module.finite_numeric_agreement
|
| 61 |
+
self.assertTrue(compare(sp.I, sp.I, 1e-9))
|
| 62 |
+
self.assertFalse(compare(sp.I, 2*sp.I, 1e-9))
|
| 63 |
+
self.assertFalse(compare(sp.Float('nan'), sp.Integer(1), 1e-9))
|
| 64 |
+
self.assertFalse(compare(sp.Integer(10)**1000, sp.Integer(10)**1000, 1e-9))
|
| 65 |
+
|
| 66 |
+
def test_ordinary_question_is_still_answered(self):
|
| 67 |
+
self.assertEqual(self.organ.answer('What is 17 * 23?')['answer'], '391')
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == '__main__':
|
| 71 |
+
unittest.main()
|
tests/usage/fixtures/composition/cases.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "journey",
|
| 4 |
+
"conversation_id": "composition_journey",
|
| 5 |
+
"text": "A car travels 150 km at a constant speed of 60 km/h. Parking then takes 15 minutes. What is the total elapsed time in hours?"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"id": "energy_cost",
|
| 9 |
+
"conversation_id": "composition_energy_cost",
|
| 10 |
+
"text": "A 1500 W heater runs for 2 hours. Electricity costs 0.20 dollars per kWh. What is the electricity cost in dollars?"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"id": "material_cost",
|
| 14 |
+
"conversation_id": "composition_material_cost",
|
| 15 |
+
"text": "An aluminium block has volume 0.002 m^3 and density 2700 kg/m^3. Aluminium costs 4 dollars per kg. What is the material cost in dollars?"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": "trip_fuel",
|
| 19 |
+
"conversation_id": "composition_trip_fuel",
|
| 20 |
+
"text": "A car uses 6 litres of fuel per 100 km and travels 250 km. Fuel costs 1.60 dollars per litre. What is the total fuel cost in dollars?"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"id": "resistor_energy",
|
| 24 |
+
"conversation_id": "composition_resistor_energy",
|
| 25 |
+
"text": "A 10 ohm resistor has 5 A of current for 60 seconds. What is the energy dissipated in joules?"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"id": "solar_useful",
|
| 29 |
+
"conversation_id": "composition_solar_useful",
|
| 30 |
+
"text": "A solar panel produces 400 W for 5 hours. A battery stores 80% of this energy. What is the stored energy in kWh?"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"id": "tank_fill",
|
| 34 |
+
"conversation_id": "composition_tank_fill",
|
| 35 |
+
"text": "A rectangular tank is 2 m long, 1 m wide and 0.5 m deep. A pump supplies 20 litres per minute. How many minutes does it take to fill the empty tank?"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "work_price",
|
| 39 |
+
"conversation_id": "composition_work_price",
|
| 40 |
+
"text": "Three workers each work 8 hours at 20 dollars per hour. Materials cost 100 dollars. What is the total project cost in dollars?"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "date_relation",
|
| 44 |
+
"conversation_id": "composition_date_relation",
|
| 45 |
+
"text": "Who wrote Leaves of Grass, and how many years after the author was born was the first edition published?"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"id": "orbital_ratio",
|
| 49 |
+
"conversation_id": "composition_orbital_ratio",
|
| 50 |
+
"text": "Earth takes 365.25 days to orbit the Sun and Mars takes 687 days. How many Earth years are 3 Mars years?"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "net_energy",
|
| 54 |
+
"conversation_id": "composition_net_energy",
|
| 55 |
+
"text": "A battery initially stores 5 kWh. A 500 W appliance runs for 3 hours. Ignoring losses, how much energy remains in the battery in kWh?"
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "ambiguous_efficiency",
|
| 59 |
+
"conversation_id": "composition_ambiguous_efficiency",
|
| 60 |
+
"text": "A heater uses 2 kWh of electricity. How much useful heat does it produce?"
|
| 61 |
+
}
|
| 62 |
+
]
|
tests/usage/fixtures/engineering/cases.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"lift_ideal","conversation_id":"lift","text":"How much energy is needed to lift a 25 kg crate vertically by 4 m? Use g = 9.81 m/s^2 and ignore losses."},
|
| 3 |
+
{"id":"lift_efficiency","conversation_id":"lift","text":"For that same lift, how much electrical energy is needed if the motor efficiency is 80%?"},
|
| 4 |
+
{"id":"pump_power","conversation_id":"pump","text":"A pump raises water at 0.002 m^3/s through a vertical height of 10 m. Use water density 1000 kg/m^3, g = 9.81 m/s^2, and 70% overall efficiency. What electrical input power is needed? Ignore pipe friction."},
|
| 5 |
+
{"id":"spring_work","conversation_id":"spring-work","text":"A linear spring has stiffness 200 N/m. Its extension changes from 0.1 m to 0.3 m. How much additional elastic energy is stored?"},
|
| 6 |
+
{"id":"spring_force","conversation_id":"spring-force","text":"A spring has unstretched length 0.4 m, final length 0.5 m and stiffness 200 N/m. What is its force magnitude?"},
|
| 7 |
+
{"id":"spring_simple","conversation_id":"spring-simple","text":"What is the elastic energy stored in a spring with stiffness 200 N/m and extension 0.3 m?"},
|
| 8 |
+
{"id":"heat_endpoints","conversation_id":"heat-endpoints","text":"How much heat is needed to warm 2 kg of water from 20 C to 80 C? Use constant specific heat 4180 J/(kg K), with no phase change or heat loss."},
|
| 9 |
+
{"id":"heat_interval","conversation_id":"heat-interval","text":"How much heat is needed to raise the temperature of 2 kg of water by 60 K? Use specific heat 4180 J/(kg K)."},
|
| 10 |
+
{"id":"battery_roles","conversation_id":"battery-roles","text":"How long can a 1000 Wh battery run a 100 W load through a 500 W rated inverter with 90% inverter efficiency and 80% depth of discharge? The load is constant."},
|
| 11 |
+
{"id":"battery_plain","conversation_id":"battery-plain","text":"How long can a 1000 Wh battery run a 100 W load with 90% inverter efficiency and 80% depth of discharge?"},
|
| 12 |
+
{"id":"battery_invalid","conversation_id":"battery-invalid","text":"How long can a 1000 Wh battery run a 100 W load with 120% inverter efficiency and 80% depth of discharge?"},
|
| 13 |
+
{"id":"conversion_flow","conversation_id":"conversion-flow","text":"Convert 30 litres per minute to cubic metres per second."},
|
| 14 |
+
{"id":"temperature_interval","conversation_id":"temperature-interval","text":"Convert a temperature increase of 18 F to a temperature increase in C."},
|
| 15 |
+
{"id":"dimension_mismatch","conversation_id":"dimension-mismatch","text":"Convert 2 metres to kilograms."}
|
| 16 |
+
]
|
tests/usage/fixtures/engineering/followup_cases.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "interval_helper_v2",
|
| 4 |
+
"conversation_id": "interval_helper_v2",
|
| 5 |
+
"text": "What is 18 * (5 / 9)?"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"id": "battery_rating_first",
|
| 9 |
+
"conversation_id": "battery_rating_first",
|
| 10 |
+
"text": "How long can a 1000 Wh battery with a 500 W rated inverter run a 100 W load with 90% inverter efficiency and 80% depth of discharge?"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"id": "spring_energy_rephrase",
|
| 14 |
+
"conversation_id": "spring_energy_rephrase",
|
| 15 |
+
"text": "How much energy is stored in a spring with stiffness 200 N/m and extension 0.3 m?"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": "heat_one_sentence",
|
| 19 |
+
"conversation_id": "heat_one_sentence",
|
| 20 |
+
"text": "How much heat is needed to warm 2 kg of water from 20 C to 80 C using specific heat 4180 J/(kg K), with no phase change or heat loss?"
|
| 21 |
+
}
|
| 22 |
+
]
|
tests/usage/fixtures/reliability/cases.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "percent_base",
|
| 4 |
+
"conversation_id": "rel_percent_base",
|
| 5 |
+
"text": "What is 20% of 80?"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"id": "percent_paraphrase",
|
| 9 |
+
"conversation_id": "rel_percent_paraphrase",
|
| 10 |
+
"text": "Calculate twenty percent of eighty."
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"id": "percent_changed",
|
| 14 |
+
"conversation_id": "rel_percent_changed",
|
| 15 |
+
"text": "What is 25% of 80?"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": "area_base",
|
| 19 |
+
"conversation_id": "rel_area_base",
|
| 20 |
+
"text": "A rectangular room is 3 meters long and 4 meters wide. What is its area?"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"id": "area_paraphrase",
|
| 24 |
+
"conversation_id": "rel_area_paraphrase",
|
| 25 |
+
"text": "What is the area of a rectangle with width 4 meters and length 3 meters?"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"id": "area_incidental",
|
| 29 |
+
"conversation_id": "rel_area_incidental",
|
| 30 |
+
"text": "Apartment 12 has a rectangular room 3 meters long and 4 meters wide. What is the room area?"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"id": "perimeter_requested",
|
| 34 |
+
"conversation_id": "rel_perimeter_requested",
|
| 35 |
+
"text": "A rectangular room is 3 meters long and 4 meters wide. What is its perimeter?"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "temperature_negative",
|
| 39 |
+
"conversation_id": "rel_temperature_negative",
|
| 40 |
+
"text": "Convert -5 degrees Celsius to Fahrenheit."
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "temperature_paraphrase",
|
| 44 |
+
"conversation_id": "rel_temperature_paraphrase",
|
| 45 |
+
"text": "What is five degrees below zero Celsius in Fahrenheit?"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"id": "zero_denominator",
|
| 49 |
+
"conversation_id": "rel_zero_denominator",
|
| 50 |
+
"text": "What is 10 divided by 0?"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "zero_numerator",
|
| 54 |
+
"conversation_id": "rel_zero_numerator",
|
| 55 |
+
"text": "What is 0 divided by 10?"
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "speed_zero_time",
|
| 59 |
+
"conversation_id": "rel_speed_zero_time",
|
| 60 |
+
"text": "A car travels 150 kilometers in 0 hours. What is its average speed?"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "speed_base",
|
| 64 |
+
"conversation_id": "rel_speed_base",
|
| 65 |
+
"text": "A car travels 150 kilometers in 3 hours. What is its average speed?"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"id": "speed_contradiction",
|
| 69 |
+
"conversation_id": "rel_speed_contradiction",
|
| 70 |
+
"text": "The same trip took exactly 3 hours and exactly 4 hours. The car traveled 150 kilometers. What was its average speed?"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"id": "area_contradiction",
|
| 74 |
+
"conversation_id": "rel_area_contradiction",
|
| 75 |
+
"text": "A rectangular room has length exactly 3 meters and length exactly 5 meters, and width 4 meters. What is its area?"
|
| 76 |
+
}
|
| 77 |
+
]
|
tests/usage/fixtures/reliability/preserved_answers.json
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"scope": "Saved successful parent replies from U1 run-2; test fixtures, not fresh Engine output.",
|
| 3 |
+
"source": "var/usage-20261026/reliability/run-2/results.jsonl",
|
| 4 |
+
"cases": [
|
| 5 |
+
{
|
| 6 |
+
"text": "What is 20% of 80?",
|
| 7 |
+
"reply": {
|
| 8 |
+
"status": "answered",
|
| 9 |
+
"path": "organ:math>word:percent_of",
|
| 10 |
+
"response": "[ev:derived] 16. Read as percent of: 20% of 80. Checked: every quantity the formula needs was found with its unit, and the question asks for what the formula gives.",
|
| 11 |
+
"provenance": {
|
| 12 |
+
"kind": "organ_word_problem",
|
| 13 |
+
"schema": "percent_of",
|
| 14 |
+
"value": 16.0,
|
| 15 |
+
"parent_path": "arb:definitional->document_runtime|substitution_disclosed|math:out_of_fragment|vessel:receipt",
|
| 16 |
+
"parent_status": "withheld"
|
| 17 |
+
},
|
| 18 |
+
"latency_ms": 9.0,
|
| 19 |
+
"zero_model_gate": {
|
| 20 |
+
"lm_calls": 0,
|
| 21 |
+
"transformer_calls": 0
|
| 22 |
+
},
|
| 23 |
+
"arbitration": {
|
| 24 |
+
"winner": "definitional",
|
| 25 |
+
"runner_up": "act_continue",
|
| 26 |
+
"margin": 0.799,
|
| 27 |
+
"floor": false,
|
| 28 |
+
"regex_family": "open_topic",
|
| 29 |
+
"agrees": false,
|
| 30 |
+
"mode": "authoritative",
|
| 31 |
+
"arbiter": "v3",
|
| 32 |
+
"canonical_surface": "What is a 80?",
|
| 33 |
+
"capture_point": "capability"
|
| 34 |
+
},
|
| 35 |
+
"evidential": {
|
| 36 |
+
"input_marked": null,
|
| 37 |
+
"answer_licensed": "unknown"
|
| 38 |
+
},
|
| 39 |
+
"continuation": {
|
| 40 |
+
"doors": [
|
| 41 |
+
"cardinal",
|
| 42 |
+
"number",
|
| 43 |
+
"product"
|
| 44 |
+
],
|
| 45 |
+
"offer": "from here, cardinal or number stand open"
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"text": "Calculate twenty percent of eighty.",
|
| 51 |
+
"reply": {
|
| 52 |
+
"status": "answered",
|
| 53 |
+
"path": "compute:verified",
|
| 54 |
+
"response": "In steps: 20 % of 80 = 16. The answer is 16.",
|
| 55 |
+
"provenance": {
|
| 56 |
+
"kind": "computation",
|
| 57 |
+
"verified": true,
|
| 58 |
+
"steps": [
|
| 59 |
+
"20 % of 80 = 16"
|
| 60 |
+
],
|
| 61 |
+
"carried_prior": false,
|
| 62 |
+
"evaluator": "w27_closed_grammar"
|
| 63 |
+
},
|
| 64 |
+
"latency_ms": 1
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"text": "What is 25% of 80?",
|
| 69 |
+
"reply": {
|
| 70 |
+
"status": "answered",
|
| 71 |
+
"path": "organ:math>word:percent_of",
|
| 72 |
+
"response": "[ev:derived] 20. Read as percent of: 25% of 80. Checked: every quantity the formula needs was found with its unit, and the question asks for what the formula gives.",
|
| 73 |
+
"provenance": {
|
| 74 |
+
"kind": "organ_word_problem",
|
| 75 |
+
"schema": "percent_of",
|
| 76 |
+
"value": 20.0,
|
| 77 |
+
"parent_path": "arb:definitional->document_runtime|substitution_disclosed|math:out_of_fragment|vessel:receipt",
|
| 78 |
+
"parent_status": "withheld"
|
| 79 |
+
},
|
| 80 |
+
"latency_ms": 7.7,
|
| 81 |
+
"zero_model_gate": {
|
| 82 |
+
"lm_calls": 0,
|
| 83 |
+
"transformer_calls": 0
|
| 84 |
+
},
|
| 85 |
+
"arbitration": {
|
| 86 |
+
"winner": "definitional",
|
| 87 |
+
"runner_up": "timeline",
|
| 88 |
+
"margin": 0.688,
|
| 89 |
+
"floor": false,
|
| 90 |
+
"regex_family": "open_topic",
|
| 91 |
+
"agrees": false,
|
| 92 |
+
"mode": "authoritative",
|
| 93 |
+
"arbiter": "v3",
|
| 94 |
+
"canonical_surface": "What is a 80?",
|
| 95 |
+
"capture_point": "capability"
|
| 96 |
+
},
|
| 97 |
+
"evidential": {
|
| 98 |
+
"input_marked": null,
|
| 99 |
+
"answer_licensed": "unknown"
|
| 100 |
+
},
|
| 101 |
+
"continuation": {
|
| 102 |
+
"doors": [
|
| 103 |
+
"cardinal",
|
| 104 |
+
"number",
|
| 105 |
+
"product"
|
| 106 |
+
],
|
| 107 |
+
"offer": "from here, cardinal or number stand open"
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"text": "What is the area of a rectangle with width 4 meters and length 3 meters?",
|
| 113 |
+
"reply": {
|
| 114 |
+
"status": "answered",
|
| 115 |
+
"path": "organ:math>word:geometry",
|
| 116 |
+
"response": "[ev:derived] 12 square units. Read as geometry: A = L x W = 3 x 4. Checked: every quantity the formula needs was found with its unit, and the question asks for what the formula gives.",
|
| 117 |
+
"provenance": {
|
| 118 |
+
"kind": "organ_word_problem",
|
| 119 |
+
"schema": "geometry",
|
| 120 |
+
"value": 12.0,
|
| 121 |
+
"parent_path": "corpus_query:property_of|vessel:receipt",
|
| 122 |
+
"parent_status": "withheld"
|
| 123 |
+
},
|
| 124 |
+
"latency_ms": 2.0,
|
| 125 |
+
"zero_model_gate": {
|
| 126 |
+
"lm_calls": 0,
|
| 127 |
+
"transformer_calls": 0
|
| 128 |
+
},
|
| 129 |
+
"arbitration": {
|
| 130 |
+
"winner": "definitional",
|
| 131 |
+
"runner_up": "multi_step",
|
| 132 |
+
"margin": 0.722,
|
| 133 |
+
"floor": false,
|
| 134 |
+
"regex_family": null,
|
| 135 |
+
"agrees": null,
|
| 136 |
+
"mode": "shadow",
|
| 137 |
+
"arbiter": "v3"
|
| 138 |
+
},
|
| 139 |
+
"evidential": {
|
| 140 |
+
"input_marked": null,
|
| 141 |
+
"answer_licensed": "unknown"
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"text": "Convert -5 degrees Celsius to Fahrenheit.",
|
| 147 |
+
"reply": {
|
| 148 |
+
"status": "answered",
|
| 149 |
+
"path": "specialist_call>specialist:units-v3>temperature",
|
| 150 |
+
"response": "-5 \u00b0C is 23 \u00b0F. [units-v3]",
|
| 151 |
+
"provenance": {
|
| 152 |
+
"kind": "core_called_specialist",
|
| 153 |
+
"core_status": "clarification",
|
| 154 |
+
"core_path": "act:clarify|vessel:receipt",
|
| 155 |
+
"specialist": "units-v3",
|
| 156 |
+
"parts": null,
|
| 157 |
+
"trace": {
|
| 158 |
+
"rule": "only"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"arbitration": {
|
| 162 |
+
"winner": "clarification",
|
| 163 |
+
"runner_up": "summarize",
|
| 164 |
+
"margin": 0.022,
|
| 165 |
+
"floor": true,
|
| 166 |
+
"regex_family": "open_topic",
|
| 167 |
+
"agrees": false,
|
| 168 |
+
"mode": "shadow",
|
| 169 |
+
"arbiter": "v3"
|
| 170 |
+
},
|
| 171 |
+
"latency_ms": 6.6,
|
| 172 |
+
"zero_model_gate": {
|
| 173 |
+
"lm_calls": 0,
|
| 174 |
+
"transformer_calls": 0
|
| 175 |
+
},
|
| 176 |
+
"evidential": {
|
| 177 |
+
"input_marked": null,
|
| 178 |
+
"answer_licensed": "unknown"
|
| 179 |
+
},
|
| 180 |
+
"references": []
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"text": "What is 0 divided by 10?",
|
| 185 |
+
"reply": {
|
| 186 |
+
"status": "answered",
|
| 187 |
+
"path": "compute:verified",
|
| 188 |
+
"response": "In steps: 0 / 10 = 0. The answer is 0.",
|
| 189 |
+
"provenance": {
|
| 190 |
+
"kind": "computation",
|
| 191 |
+
"verified": true,
|
| 192 |
+
"steps": [
|
| 193 |
+
"0 / 10 = 0"
|
| 194 |
+
],
|
| 195 |
+
"carried_prior": false,
|
| 196 |
+
"evaluator": "w27_closed_grammar"
|
| 197 |
+
},
|
| 198 |
+
"latency_ms": 1
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
]
|
| 202 |
+
}
|
tests/usage/reliability/test_geometry_consumer.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Untrusted focused harness: actual word module/w488 consumer, substituted parent.
|
| 2 |
+
|
| 3 |
+
The w480 timed-call helper and answered predicate are AST-selected unchanged.
|
| 4 |
+
No full Engine, stores, model or admission gate is launched.
|
| 5 |
+
"""
|
| 6 |
+
import ast
|
| 7 |
+
import importlib.util
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import sys
|
| 12 |
+
import threading
|
| 13 |
+
import types
|
| 14 |
+
import unittest
|
| 15 |
+
from unittest.mock import patch
|
| 16 |
+
|
| 17 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 18 |
+
SOURCE = Path(os.environ.get('RELIABILITY_SOURCE_ROOT', ROOT))
|
| 19 |
+
ADMITTED = SOURCE / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted'
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def load(path, name):
|
| 23 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 24 |
+
module = importlib.util.module_from_spec(spec)
|
| 25 |
+
spec.loader.exec_module(module)
|
| 26 |
+
return module
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def consumer(word_path=None, fold_path=None):
|
| 30 |
+
wp = load(word_path or SOURCE / 'root/wander-train/word_problems_v1.py', 'reliability_word_subject')
|
| 31 |
+
tree = ast.parse((ADMITTED / 'oracle_release_runtime_w480.py').read_text())
|
| 32 |
+
selected = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_run']
|
| 33 |
+
parent_class = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'OracleReleaseRuntimeW480')
|
| 34 |
+
selected += [n for n in parent_class.body if isinstance(n, ast.FunctionDef) and n.name == '_c480_answered']
|
| 35 |
+
ns = {'threading': threading, 'BUDGET_S': 4.0}
|
| 36 |
+
exec(compile(ast.Module(body=selected, type_ignores=[]), '<actual-w480-helpers>', 'exec'), ns)
|
| 37 |
+
|
| 38 |
+
class Parent:
|
| 39 |
+
_c480_answered = ns['_c480_answered']
|
| 40 |
+
|
| 41 |
+
def _c480_dispatch(self, text, session_id, res):
|
| 42 |
+
return res
|
| 43 |
+
|
| 44 |
+
base = types.ModuleType('oracle_release_runtime_w486')
|
| 45 |
+
base.OracleReleaseRuntimeW486 = Parent
|
| 46 |
+
timed = types.ModuleType('oracle_release_runtime_w480')
|
| 47 |
+
timed._run = ns['_run']
|
| 48 |
+
with patch.dict(sys.modules, {'word_problems_v1': wp, 'oracle_release_runtime_w486': base,
|
| 49 |
+
'oracle_release_runtime_w480': timed}):
|
| 50 |
+
fold = load(fold_path or ADMITTED / 'oracle_release_runtime_w488.py', 'reliability_fold_subject')
|
| 51 |
+
return wp, fold.OracleReleaseRuntimeW488()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class GeometryConsumerTests(unittest.TestCase):
|
| 55 |
+
@classmethod
|
| 56 |
+
def setUpClass(cls):
|
| 57 |
+
cls.wp, cls.runtime = consumer()
|
| 58 |
+
cls.original = json.loads((ROOT / 'tests/usage/fixtures/reliability/cases.json').read_text())
|
| 59 |
+
cls.cases = {c['id']: c['text'] for c in cls.original}
|
| 60 |
+
|
| 61 |
+
def dispatch(self, text, parent=None):
|
| 62 |
+
if parent is None:
|
| 63 |
+
parent = {'status': 'clarification', 'path': 'act:clarify_specific',
|
| 64 |
+
'response': 'What does "its" refer to?', 'latency_ms': 1}
|
| 65 |
+
return self.runtime._c480_dispatch(text, 'isolated', parent)
|
| 66 |
+
|
| 67 |
+
def test_original_room_words_and_requested_quantity(self):
|
| 68 |
+
# Independent equations: area = 3 * 4; perimeter = 2 * (3 + 4).
|
| 69 |
+
for key, value in [('area_base', 12), ('area_incidental', 12), ('area_paraphrase', 12), ('perimeter_requested', 14)]:
|
| 70 |
+
with self.subTest(key=key):
|
| 71 |
+
result = self.dispatch(self.cases[key])
|
| 72 |
+
self.assertEqual(result['status'], 'answered')
|
| 73 |
+
self.assertEqual(result['provenance']['value'], value)
|
| 74 |
+
|
| 75 |
+
def test_changed_dimension_changes_area(self):
|
| 76 |
+
self.assertEqual(self.dispatch('A rectangular room is 5 meters long and 4 meters wide. What is its area?')['provenance']['value'], 20)
|
| 77 |
+
|
| 78 |
+
def test_real_conflict_has_useful_explanation(self):
|
| 79 |
+
result = self.dispatch(self.cases['area_contradiction'])
|
| 80 |
+
self.assertEqual(result['status'], 'clarification')
|
| 81 |
+
self.assertEqual(result['path'], 'organ:math>word:conflicting_dimension')
|
| 82 |
+
self.assertEqual(result['response'], 'The supplied length values disagree (3 meters and 5 meters). Which length should I use?')
|
| 83 |
+
self.assertEqual(result['provenance']['parent_path'], 'act:clarify_specific')
|
| 84 |
+
self.assertNotIn('value', result['provenance'])
|
| 85 |
+
|
| 86 |
+
def test_postfix_conflict_also_explained(self):
|
| 87 |
+
result = self.dispatch('A rectangular room is 3 meters long and 5 meters long and 4 meters wide. What is its area?')
|
| 88 |
+
self.assertEqual(result['provenance']['reason'], 'conflicting_dimension')
|
| 89 |
+
|
| 90 |
+
def test_equal_prefix_postfix_mentions_count_once(self):
|
| 91 |
+
for text in ['A rectangle has length 3 meters long and width 4 meters wide. What is its area?',
|
| 92 |
+
'A rectangle is 3 meters long and 3 meters long and 4 meters wide. What is its area?']:
|
| 93 |
+
self.assertEqual(self.dispatch(text)['provenance']['value'], 12)
|
| 94 |
+
|
| 95 |
+
def test_malformed_negative_or_unbound_dimensions_decline(self):
|
| 96 |
+
for text in ['A rectangle is -3 meters long and 4 meters wide. What is its area?',
|
| 97 |
+
'A rectangle is −3 meters long and 4 meters wide. What is its area?',
|
| 98 |
+
'A rectangle is 3 bananas long and 4 meters wide. What is its area?',
|
| 99 |
+
'A rectangle is 3 meters long and 4 meters wide and another side 5 meters. What is its area?',
|
| 100 |
+
'A rectangle is 3 meters long and 4 centimeters wide. What is its area?']:
|
| 101 |
+
with self.subTest(text=text):
|
| 102 |
+
result = self.dispatch(text)
|
| 103 |
+
self.assertEqual(result['path'], 'act:clarify_specific')
|
| 104 |
+
self.assertIsNone(self.wp.explain_refusal(text))
|
| 105 |
+
|
| 106 |
+
def test_equivalent_unit_duplicates_not_called_contradictory(self):
|
| 107 |
+
text = 'A rectangle has length 3 meters and length 300 centimeters and width 4 meters. What is its area?'
|
| 108 |
+
self.assertIsNone(self.wp.explain_refusal(text))
|
| 109 |
+
self.assertEqual(self.dispatch(text)['status'], 'clarification')
|
| 110 |
+
|
| 111 |
+
def test_different_shapes_not_conflated(self):
|
| 112 |
+
text = 'A rectangle has length 3 meters and width 4 meters. Another rectangle has length 5 meters and width 4 meters. What is the area?'
|
| 113 |
+
self.assertEqual(self.wp.explain_refusal(text)['reason'], 'unsupported_geometry_request')
|
| 114 |
+
|
| 115 |
+
def test_conflict_malformed_remainder_not_claimed_complete(self):
|
| 116 |
+
text = 'A rectangle has length 3 meters and length 5 meters and width 4 bananas. What is its area?'
|
| 117 |
+
self.assertIsNone(self.wp.explain_refusal(text))
|
| 118 |
+
|
| 119 |
+
def test_radius_diameter_diagnosis(self):
|
| 120 |
+
result = self.dispatch('A circle has radius 3 meters and diameter 8 meters. What is its area?')
|
| 121 |
+
self.assertEqual(result['status'], 'clarification')
|
| 122 |
+
self.assertIn('diameter must be twice the radius', result['response'])
|
| 123 |
+
|
| 124 |
+
def test_parent_answers_remain_unchanged(self):
|
| 125 |
+
# Replay retained successful outputs as parent responses; these are not
|
| 126 |
+
# fresh Engine output and the harness does not claim to recompute them.
|
| 127 |
+
answered = json.loads((ROOT / 'tests/usage/fixtures/reliability/preserved_answers.json').read_text())['cases']
|
| 128 |
+
self.assertEqual(len(answered), 6)
|
| 129 |
+
for row in answered:
|
| 130 |
+
reply = row['reply']
|
| 131 |
+
self.assertIs(self.dispatch(row['text'], reply), reply)
|
| 132 |
+
|
| 133 |
+
def test_multiple_rooms_not_silently_one_room(self):
|
| 134 |
+
# Two disjoint 3*4 rooms have total area 2*12=24; a scalar 12 is false.
|
| 135 |
+
for text in ['Two rectangular rooms are each 3 meters long and 4 meters wide. What is their total area?',
|
| 136 |
+
'2 rooms are rectangular and each is 3 meters long and 4 meters wide. What is their total area?']:
|
| 137 |
+
result = self.dispatch(text)
|
| 138 |
+
self.assertEqual(result['status'], 'clarification')
|
| 139 |
+
self.assertIn('multiple shapes', result['response'])
|
| 140 |
+
self.assertNotIn('value', result['provenance'])
|
| 141 |
+
|
| 142 |
+
def test_multiple_requested_measures_not_partial_answer(self):
|
| 143 |
+
# Required pair is area12, perimeter14, not an unqualified scalar12.
|
| 144 |
+
text = 'A rectangle is 3 meters long and 4 meters wide. What are its area and perimeter?'
|
| 145 |
+
result = self.dispatch(text)
|
| 146 |
+
self.assertEqual(result['status'], 'clarification')
|
| 147 |
+
self.assertIn('each requested measure', result['response'])
|
| 148 |
+
|
| 149 |
+
def test_requested_area_unit_not_ignored(self):
|
| 150 |
+
# 12m^2 = 12*(100cm)^2 =120000cm^2; bare12 does not answer this.
|
| 151 |
+
text = 'A rectangle is 3 meters long and 4 meters wide. What is its area in square centimeters?'
|
| 152 |
+
result = self.dispatch(text)
|
| 153 |
+
self.assertEqual(result['status'], 'clarification')
|
| 154 |
+
self.assertIn('requested output unit', result['response'])
|
| 155 |
+
|
| 156 |
+
def test_apartment_identifier_is_not_shape_count(self):
|
| 157 |
+
text = 'Apartment 16 has a rectangular room 3 meters long and 4 meters wide. What is the room area?'
|
| 158 |
+
self.assertEqual(self.dispatch(text)['provenance']['value'], 12)
|
| 159 |
+
|
| 160 |
+
def test_rounding_and_location_are_not_output_units(self):
|
| 161 |
+
for question in ['What is its area rounded to the nearest tenth?', 'What is its area in this diagram?']:
|
| 162 |
+
text = 'A rectangle is 3 meters long and 4 meters wide. ' + question
|
| 163 |
+
result = self.dispatch(text)
|
| 164 |
+
self.assertEqual(result['status'], 'answered')
|
| 165 |
+
self.assertEqual(result['provenance']['value'], 12)
|
| 166 |
+
rounded = self.dispatch('A rectangle is 3.12 meters long and 4 meters wide. What is its area rounded to the nearest tenth?')
|
| 167 |
+
self.assertEqual(rounded['provenance']['value'], 12.5)
|
| 168 |
+
|
| 169 |
+
def test_explicit_unknown_units_still_decline(self):
|
| 170 |
+
for suffix in ['in square glorks', 'in units of glorks', 'in cm2']:
|
| 171 |
+
result = self.dispatch('A rectangle is 3 meters long and 4 meters wide. What is its area ' + suffix + '?')
|
| 172 |
+
self.assertEqual(result['path'], 'organ:math>word:unsupported_geometry_request')
|
| 173 |
+
self.assertIn('requested output unit', result['response'])
|
| 174 |
+
|
| 175 |
+
def test_non_measure_shape_questions_not_intercepted(self):
|
| 176 |
+
for text in ['Two rectangles are blue. What color are they?',
|
| 177 |
+
'A rectangle is 3 meters long and 4 meters wide. What is its name?']:
|
| 178 |
+
parent = {'status': 'withheld', 'path': 'other:reader', 'response': 'Unanswered by parent.'}
|
| 179 |
+
self.assertIs(self.dispatch(text, parent), parent)
|
| 180 |
+
|
| 181 |
+
def test_non_geometry_refusals_preserved(self):
|
| 182 |
+
for text in ['What does its name mean?', 'A car travels 150 kilometers in 0 hours. What is its average speed?',
|
| 183 |
+
'What is five degrees below zero Celsius in Fahrenheit?']:
|
| 184 |
+
original = {'status': 'withheld', 'path': 'existing:guard', 'response': 'No verified answer.'}
|
| 185 |
+
self.assertIs(self.dispatch(text, original), original)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == '__main__':
|
| 189 |
+
unittest.main()
|
tests/usage/reliability/test_geometry_units.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Independent geometry-unit checks through the actual untrusted w488 consumer.
|
| 2 |
+
|
| 3 |
+
Uses the existing focused parent substitute; no full Engine or panel is run.
|
| 4 |
+
"""
|
| 5 |
+
import math
|
| 6 |
+
import unittest
|
| 7 |
+
|
| 8 |
+
from test_geometry_consumer import consumer
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class GeometryUnitTests(unittest.TestCase):
|
| 12 |
+
@classmethod
|
| 13 |
+
def setUpClass(cls):
|
| 14 |
+
cls.wp, cls.runtime = consumer()
|
| 15 |
+
|
| 16 |
+
def dispatch(self, text):
|
| 17 |
+
return self.runtime._c480_dispatch(text, 'units',
|
| 18 |
+
{'status': 'clarification', 'path': 'controlled:parent', 'response': 'Parent clarification.'})
|
| 19 |
+
|
| 20 |
+
def check(self, text, value, unit):
|
| 21 |
+
module = self.wp.answer(text)
|
| 22 |
+
self.assertIsNotNone(module)
|
| 23 |
+
self.assertAlmostEqual(module['value'], value, places=10)
|
| 24 |
+
self.assertEqual(module['unit'], unit)
|
| 25 |
+
reply = self.dispatch(text)
|
| 26 |
+
self.assertEqual(reply['status'], 'answered')
|
| 27 |
+
self.assertAlmostEqual(reply['provenance']['value'], value, places=10)
|
| 28 |
+
self.assertEqual(reply['provenance']['unit'], unit)
|
| 29 |
+
first_sentence = reply['response'].split('. Read as ', 1)[0]
|
| 30 |
+
self.assertTrue(first_sentence.endswith(' ' + unit.replace('^2', '²').replace('^3', '³')), first_sentence)
|
| 31 |
+
self.assertNotIn('square units', reply['response'])
|
| 32 |
+
self.assertNotIn('cubic units', reply['response'])
|
| 33 |
+
return reply
|
| 34 |
+
|
| 35 |
+
def test_original_room_area_and_perimeter(self):
|
| 36 |
+
# L=3m,W=4m: A=12m²; P=14m. These are the actual U4 question strings.
|
| 37 |
+
self.check('A rectangular room is 3 meters long and 4 meters wide. What is its area?', 12, 'm^2')
|
| 38 |
+
self.check('A rectangular room is 3 meters long and 4 meters wide. What is its perimeter?', 14, 'm')
|
| 39 |
+
|
| 40 |
+
def test_paraphrase_identifiers_and_location(self):
|
| 41 |
+
for text in [
|
| 42 |
+
'What is the area of a rectangle with width 4 meters and length 3 meters?',
|
| 43 |
+
'Apartment 12 has a rectangular room 3 meters long and 4 meters wide. What is the room area?',
|
| 44 |
+
'Apartment 16 has a rectangular room 3 meters long and 4 meters wide. What is the room area?',
|
| 45 |
+
'A rectangle is 3 meters long and 4 meters wide. What is its area in this diagram?']:
|
| 46 |
+
with self.subTest(text=text):
|
| 47 |
+
self.check(text, 12, 'm^2')
|
| 48 |
+
|
| 49 |
+
def test_supported_same_unit_scales_not_relabelled(self):
|
| 50 |
+
for spelling, canonical in [('millimeters', 'mm'), ('centimeters', 'cm'), ('meters', 'm'),
|
| 51 |
+
('kilometers', 'km'), ('inches', 'in'), ('feet', 'ft'),
|
| 52 |
+
('yards', 'yd'), ('miles', 'mi')]:
|
| 53 |
+
with self.subTest(unit=spelling):
|
| 54 |
+
self.check(f'A rectangle has length 3 {spelling} and width 4 {spelling}. What is its area?', 12, canonical+'^2')
|
| 55 |
+
self.check(f'A rectangle has length 3 {spelling} and width 4 {spelling}. What is its perimeter?', 14, canonical)
|
| 56 |
+
|
| 57 |
+
def test_equal_scale_aliases(self):
|
| 58 |
+
self.check('A rectangle has length 3 feet and width 4 ft. What is its area?', 12, 'ft^2')
|
| 59 |
+
self.check('A rectangle has length 3 m and width 4 meters. What is its perimeter?', 14, 'm')
|
| 60 |
+
|
| 61 |
+
def test_in_is_inches_only_in_bound_dimension_position(self):
|
| 62 |
+
self.check('A rectangle has length 3 in and width 4 inches. What is its area?', 12, 'in^2')
|
| 63 |
+
# A preposition without a measurement unit must not acquire inches.
|
| 64 |
+
text = 'A rectangle has length 3 in the diagram and width 4 in the diagram. What is its area?'
|
| 65 |
+
self.assertIsNone(self.wp.answer(text))
|
| 66 |
+
|
| 67 |
+
def test_area_rounding_retains_squared_unit(self):
|
| 68 |
+
self.check('A rectangle is 3.12 meters long and 4 meters wide. What is its area rounded to the nearest tenth?', 12.5, 'm^2')
|
| 69 |
+
|
| 70 |
+
def test_circle_radius_diameter_and_circumference(self):
|
| 71 |
+
self.check('A circle has radius 3 centimeters. What is its area?', 9*math.pi, 'cm^2')
|
| 72 |
+
self.check('A circle has diameter 6 centimeters. What is its area?', 9*math.pi, 'cm^2')
|
| 73 |
+
self.check('A circle has radius 3 centimeters and diameter 6 cm. What is its circumference?', 6*math.pi, 'cm')
|
| 74 |
+
|
| 75 |
+
def test_circumference_rounding_retains_linear_unit(self):
|
| 76 |
+
self.check('A circle has radius 1 meters. What is its circumference rounded to the nearest tenth?', 6.3, 'm')
|
| 77 |
+
|
| 78 |
+
def test_supported_volumes_retain_cubed_unit(self):
|
| 79 |
+
# Vcone=πr²h/3; Vcylinder=πr²h; Vsphere=4πr³/3.
|
| 80 |
+
self.check('A cone has radius 2 centimeters and height 3 centimeters. What is its volume?', 4*math.pi, 'cm^3')
|
| 81 |
+
self.check('A cylinder has radius 2 inches and height 3 in. What is its volume?', 12*math.pi, 'in^3')
|
| 82 |
+
self.check('A sphere has radius 2 feet. What is its volume?', 32*math.pi/3, 'ft^3')
|
| 83 |
+
|
| 84 |
+
def test_volume_rounding_retains_cubed_unit(self):
|
| 85 |
+
self.check('A cylinder has radius 1.2 centimeters and height 3.4 centimeters. What is its volume rounded to the nearest tenth?', 15.4, 'cm^3')
|
| 86 |
+
|
| 87 |
+
def test_mixed_units_and_malformed_units_not_silently_relabelled(self):
|
| 88 |
+
for text in [
|
| 89 |
+
'A rectangle has length 3 meters and width 4 centimeters. What is its area?',
|
| 90 |
+
'A cylinder has radius 2 inches and height 3 feet. What is its volume?',
|
| 91 |
+
'A rectangle has length 3 minutes and width 4 meters. What is its area?',
|
| 92 |
+
'A rectangle has length 3 smoots and width 4 smoots. What is its area?']:
|
| 93 |
+
with self.subTest(text=text):
|
| 94 |
+
self.assertIsNone(self.wp.answer(text))
|
| 95 |
+
self.assertNotEqual(self.dispatch(text)['status'], 'answered')
|
| 96 |
+
|
| 97 |
+
def test_conflict_and_scope_stay_bounded(self):
|
| 98 |
+
for text in [
|
| 99 |
+
'A circle has radius 3 meters and diameter 8 meters. What is its area?',
|
| 100 |
+
'A rectangle has length 3 meters and length 5 meters and width 4 meters. What is its area?',
|
| 101 |
+
'Two rectangular rooms are each 3 meters long and 4 meters wide. What is their total area?',
|
| 102 |
+
'A rectangle is 3 meters long and 4 meters wide. What are its area and perimeter?',
|
| 103 |
+
'A rectangle is 3 meters long and 4 meters wide. What is its area in square centimeters?']:
|
| 104 |
+
with self.subTest(text=text):
|
| 105 |
+
self.assertEqual(self.dispatch(text)['status'], 'clarification')
|
| 106 |
+
self.assertIsNone(self.wp.answer(text))
|
| 107 |
+
|
| 108 |
+
def test_zero_area_keeps_unit(self):
|
| 109 |
+
self.check('A rectangle has length 0 centimeters and width 4 centimeters. What is its area?', 0, 'cm^2')
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == '__main__':
|
| 113 |
+
unittest.main()
|
tests/usage/reliability_route/test_geometry_route.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Actual geometry-v4 -> Family -> w504 checks with only the expensive parent substituted.
|
| 2 |
+
|
| 3 |
+
Untrusted test harness; no full Engine, native build or admission authority.
|
| 4 |
+
"""
|
| 5 |
+
import importlib.util
|
| 6 |
+
import json
|
| 7 |
+
import math
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import sys
|
| 11 |
+
import types
|
| 12 |
+
import unittest
|
| 13 |
+
from unittest.mock import patch
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 16 |
+
SOURCE = Path(os.environ.get('RELIABILITY_SOURCE_ROOT', ROOT))
|
| 17 |
+
SPECIALIST = SOURCE / 'root/specialists/geometry-v4'
|
| 18 |
+
ADMITTED = SOURCE / 'root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted'
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def load(path, name):
|
| 22 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 23 |
+
module = importlib.util.module_from_spec(spec)
|
| 24 |
+
spec.loader.exec_module(module)
|
| 25 |
+
return module
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def actual_consumer():
|
| 29 |
+
combo = load(SOURCE / 'root/specialists/_gate/combo_runtime_v1.py', 'geometry_route_combo')
|
| 30 |
+
family = combo.Family([str(SPECIALIST / 'manifest.json')], [str(SPECIALIST)])
|
| 31 |
+
class Parent:
|
| 32 |
+
def chat(self, text, session_id='default'):
|
| 33 |
+
return self.parent_reply
|
| 34 |
+
parent_module = types.ModuleType('oracle_release_runtime_w366')
|
| 35 |
+
parent_module.OracleReleaseRuntimeW366 = Parent
|
| 36 |
+
with patch.dict(sys.modules, {'oracle_release_runtime_w366': parent_module}):
|
| 37 |
+
fold = load(ADMITTED / 'oracle_release_runtime_w504.py', 'geometry_route_fold')
|
| 38 |
+
fold._FAM['f'] = family
|
| 39 |
+
runtime = fold.OracleReleaseRuntimeW504()
|
| 40 |
+
runtime.parent_reply = {'status': 'clarification', 'path': 'act:clarify_specific', 'response': 'Unresolved parent reading.'}
|
| 41 |
+
return family, runtime
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class GeometryRouteTests(unittest.TestCase):
|
| 45 |
+
@classmethod
|
| 46 |
+
def setUpClass(cls):
|
| 47 |
+
cls.family, cls.runtime = actual_consumer()
|
| 48 |
+
|
| 49 |
+
def answer(self, text):
|
| 50 |
+
self.runtime.parent_reply = {'status': 'clarification', 'path': 'act:clarify_specific', 'response': 'Unresolved parent reading.'}
|
| 51 |
+
return self.runtime.chat(text)
|
| 52 |
+
|
| 53 |
+
def test_actual_recorded_mixed_units(self):
|
| 54 |
+
reply = self.answer('A rectangle has length 3 meters and width 4 centimeters. What is its area?')
|
| 55 |
+
self.assertEqual(reply['status'], 'answered')
|
| 56 |
+
self.assertEqual(reply['path'], 'specialist_call>specialist:geometry-v4>rect_area')
|
| 57 |
+
# 3m * .04m = .12m², not raw 3*4=12.
|
| 58 |
+
self.assertIn('area is 0.12 m²', reply['response'])
|
| 59 |
+
|
| 60 |
+
def test_actual_recorded_diagram_units(self):
|
| 61 |
+
reply = self.answer('A rectangle has length 3 in the diagram and width 4 in the diagram. What is its area?')
|
| 62 |
+
self.assertEqual(reply['status'], 'answered')
|
| 63 |
+
self.assertIn('area is 12 square units', reply['response'])
|
| 64 |
+
self.assertNotIn('in²', reply['response'])
|
| 65 |
+
|
| 66 |
+
def test_mixed_rectangle_perimeter(self):
|
| 67 |
+
# 2*(3+.04)m=6.08m.
|
| 68 |
+
self.assertIn('perimeter is 6.08 m', self.answer('A rectangle has length 3 meters and width 4 centimeters. What is its perimeter?')['response'])
|
| 69 |
+
|
| 70 |
+
def test_same_units_and_aliases(self):
|
| 71 |
+
for length, width, unit in [('centimeters', 'cm', 'cm²'), ('feet', 'ft', 'ft²'), ('meters', 'm', 'm²')]:
|
| 72 |
+
result = self.answer(f'A rectangle has length 3 {length} and width 4 {width}. What is its area?')
|
| 73 |
+
self.assertEqual(result['status'], 'answered')
|
| 74 |
+
self.assertIn('area is 12 '+unit, result['response'])
|
| 75 |
+
|
| 76 |
+
def test_explicit_inches_do_not_become_diagram_units(self):
|
| 77 |
+
self.assertIn('area is 12 in²', self.answer('A rectangle has length 3 in and width 4 inches. What is its area?')['response'])
|
| 78 |
+
|
| 79 |
+
def test_diagram_and_physical_dimensions_not_combined(self):
|
| 80 |
+
result = self.answer('A rectangle has length 3 meters and width 4 in the diagram. What is its area?')
|
| 81 |
+
self.assertEqual(result['status'], 'withheld')
|
| 82 |
+
self.assertIn('missing units', result['response'])
|
| 83 |
+
|
| 84 |
+
def test_unknown_and_ambiguous_units(self):
|
| 85 |
+
for text in ['A rectangle has length 3 smoots and width 4 meters. What is its area?',
|
| 86 |
+
'A circle has radius 3 bananas. What is its area?',
|
| 87 |
+
'A rectangle has length 3 in another place and width 4 inches. What is its area?']:
|
| 88 |
+
result = self.answer(text)
|
| 89 |
+
self.assertEqual(result['status'], 'withheld')
|
| 90 |
+
self.assertTrue(result['path'].endswith('geometry-v4>underspecified'))
|
| 91 |
+
|
| 92 |
+
def test_missing_unit_must_not_inherit_silently(self):
|
| 93 |
+
self.assertEqual(self.answer('A cylinder has radius 3 meters and height 4. What is its volume?')['status'], 'withheld')
|
| 94 |
+
|
| 95 |
+
def test_mixed_units_do_not_escape_through_parent_formulas(self):
|
| 96 |
+
for text in ['A cylinder has radius 3 meters and height 4 centimeters. What is its volume?',
|
| 97 |
+
'A cone has radius 3 meters and height 4 centimeters. What is its volume?',
|
| 98 |
+
'A triangle has base 3 meters and height 4 centimeters. What is its area?',
|
| 99 |
+
'A box has length 3 meters and width 4 centimeters and height 5 meters. What is its volume?']:
|
| 100 |
+
with self.subTest(text=text):
|
| 101 |
+
result = self.answer(text)
|
| 102 |
+
self.assertEqual(result['status'], 'withheld')
|
| 103 |
+
self.assertIn('mixed length units', result['response'])
|
| 104 |
+
|
| 105 |
+
def test_inconsistent_radius_diameter_and_duplicates(self):
|
| 106 |
+
for text in ['A circle has radius 3 meters and diameter 8 centimeters. What is its area?',
|
| 107 |
+
'A rectangle has length 3 meters and length 5 meters and width 4 meters. What is its area?',
|
| 108 |
+
'A square has side 3 meters and side 5 meters. What is its area?']:
|
| 109 |
+
result = self.answer(text)
|
| 110 |
+
self.assertEqual(result['status'], 'withheld')
|
| 111 |
+
self.assertIn('contradict', result['response'])
|
| 112 |
+
|
| 113 |
+
def test_equal_dimension_repeats_and_aliases(self):
|
| 114 |
+
self.assertIn('area is 12 m²', self.answer('A rectangle has length 3 meters and length 300 centimeters and width 4 meters. What is its area?')['response'])
|
| 115 |
+
|
| 116 |
+
def test_other_same_unit_formulas_remain_useful(self):
|
| 117 |
+
for text, fragment in [
|
| 118 |
+
('A cylinder has radius 2 centimeters and height 3 cm. What is its volume?', '37.6991 cm³'),
|
| 119 |
+
('A circle has radius 3 centimeters and diameter 6 cm. What is its area?', '28.2743 cm²'),
|
| 120 |
+
('A cube has side 2 feet. What is its volume?', '8 ft³')]:
|
| 121 |
+
reply = self.answer(text)
|
| 122 |
+
self.assertEqual(reply['status'], 'answered')
|
| 123 |
+
self.assertIn(fragment, reply['response'])
|
| 124 |
+
|
| 125 |
+
def test_powered_or_compound_units_not_read_as_lengths(self):
|
| 126 |
+
for unit in ['m^2', 'm2', 'm²', 'meters per second', 'm/s', 'µm']:
|
| 127 |
+
result = self.answer(f'A triangle has base 3 {unit} and height 4 meters. What is its area?')
|
| 128 |
+
self.assertEqual(result['status'], 'withheld', (unit, result))
|
| 129 |
+
|
| 130 |
+
def test_cube_side_and_edge_consistency(self):
|
| 131 |
+
self.assertEqual(self.answer('A cube has side 2 meters and edge 3 meters. What is its volume?')['status'], 'withheld')
|
| 132 |
+
self.assertIn('8 m³', self.answer('A cube has side 2 meters and edge 2 meters. What is its volume?')['response'])
|
| 133 |
+
|
| 134 |
+
def test_requests_respect_scope(self):
|
| 135 |
+
for text in ['Two rectangles have length 3 meters and width 4 meters. What is their total area?',
|
| 136 |
+
'A rectangle has length 3 meters and width 4 meters. What are its area and perimeter?',
|
| 137 |
+
'A rectangle has length 3 meters and width 4 meters. What is its area in square centimeters?']:
|
| 138 |
+
self.assertEqual(self.answer(text)['status'], 'withheld')
|
| 139 |
+
|
| 140 |
+
def test_rounding_and_location(self):
|
| 141 |
+
self.assertIn('area is 12.5 m²', self.answer('A rectangle has length 3.12 meters and width 4 meters. What is its area rounded to the nearest tenth?')['response'])
|
| 142 |
+
self.assertIn('area is 12 m²', self.answer('A rectangle has length 3 meters and width 4 meters. What is its area in this diagram?')['response'])
|
| 143 |
+
|
| 144 |
+
def test_parent_success_and_deliberate_refusal_preserved(self):
|
| 145 |
+
for original in [{'status': 'answered', 'path': 'existing:answer', 'response': 'Existing useful answer.'},
|
| 146 |
+
{'status': 'withheld', 'path': 'guard:out_of_fragment', 'response': 'Deliberate refusal.'}]:
|
| 147 |
+
self.runtime.parent_reply = original
|
| 148 |
+
self.assertIs(self.runtime.chat('A rectangle has length 3 meters and width 4 centimeters. What is its area?'), original)
|
| 149 |
+
|
| 150 |
+
def test_shape_mentions_without_measure_request_stay_with_parent(self):
|
| 151 |
+
for text in ['A rectangle has length 3 meters and width 4 centimeters. Who invented rectangles?',
|
| 152 |
+
'A rectangle has area 12 and length 3 meters and width 4 meters. What color is it?',
|
| 153 |
+
'Two squares are drawn in blue. Who drew them?']:
|
| 154 |
+
self.runtime.parent_reply = {'status': 'clarification', 'path': 'act:clarify_specific', 'response': 'Original non-measure response.'}
|
| 155 |
+
self.assertIs(self.runtime.chat(text), self.runtime.parent_reply)
|
| 156 |
+
|
| 157 |
+
def test_non_calculation_question_with_measure_word_stays_with_parent(self):
|
| 158 |
+
for text in ['Who discovered the area formula of a circle?',
|
| 159 |
+
'A square has side 3 meters. What is the history of its area formula?',
|
| 160 |
+
'A rectangle has length 3 meters and width 4 meters. What is the name of its area formula?']:
|
| 161 |
+
self.runtime.parent_reply = {'status':'clarification','path':'act:clarify_specific','response':'Existing non-calculation answer.'}
|
| 162 |
+
self.assertIs(self.runtime.chat(text), self.runtime.parent_reply)
|
| 163 |
+
|
| 164 |
+
def test_extra_numeric_tuple_is_not_ignored(self):
|
| 165 |
+
for text in ['A rectangle has length 3 meters and width 4 meters, also 5 by 6. What is its area?',
|
| 166 |
+
'A rectangle has length 3 meters and width 4 meters, also 5 by 6. What is its perimeter?']:
|
| 167 |
+
reply = self.answer(text)
|
| 168 |
+
self.assertEqual(reply['status'], 'withheld')
|
| 169 |
+
self.assertIn('every number', reply['response'])
|
| 170 |
+
|
| 171 |
+
def test_unused_physical_roles_are_not_ignored(self):
|
| 172 |
+
for text in ['A sphere has radius 2 centimeters and height 3 centimeters. What is its volume?',
|
| 173 |
+
'A circle has radius 3 meters and height 4 meters. What is its area?',
|
| 174 |
+
'A rectangle has length 3 meters and width 4 meters and radius 2 meters. What is its area?']:
|
| 175 |
+
reply = self.answer(text)
|
| 176 |
+
self.assertEqual(reply['status'], 'withheld')
|
| 177 |
+
self.assertIn('all supplied geometry roles', reply['response'])
|
| 178 |
+
|
| 179 |
+
def test_apartment_identifier_is_not_a_length(self):
|
| 180 |
+
self.assertIn('area is 0.12 m²', self.answer('Apartment 16 contains a rectangle with length 3 meters and width 4 centimeters. What is its area?')['response'])
|
| 181 |
+
|
| 182 |
+
def test_unrelated_question_does_not_cost_parent(self):
|
| 183 |
+
self.runtime.parent_reply = {'status':'clarification','path':'act:clarify_specific','response':'Original unrelated answer.'}
|
| 184 |
+
self.assertIs(self.runtime.chat('What music do you enjoy?'), self.runtime.parent_reply)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == '__main__':
|
| 188 |
+
unittest.main()
|
tests/usage/research/test_relation_semantics.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression tests for the granted Oracle relation_spec_v1 source repair.
|
| 2 |
+
|
| 3 |
+
Set RESEARCH_RELATION_SOURCE to test the same module in another package. The
|
| 4 |
+
default is the root-approved build source, never the immutable passing candidate.
|
| 5 |
+
"""
|
| 6 |
+
import importlib.util
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import sys
|
| 10 |
+
import types
|
| 11 |
+
import unittest
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from unittest.mock import patch
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[3]
|
| 16 |
+
SOURCE = Path(os.environ.get("RESEARCH_RELATION_SOURCE", ROOT / "root/wander-train/relation_spec_v1.py"))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@unittest.skipUnless(SOURCE.exists(), "Set RESEARCH_RELATION_SOURCE to the patched relation_spec_v1.py")
|
| 20 |
+
class RelationSemanticsTests(unittest.TestCase):
|
| 21 |
+
def setUp(self):
|
| 22 |
+
self.names = {"star wars": [(462, 184, 0), (17738, 96, 1)]}
|
| 23 |
+
self.props = {(17738, 86): frozenset({"John Williams"}),
|
| 24 |
+
(17738, 577): frozenset({"1977-05-25T00:00:00Z"})}
|
| 25 |
+
linker = types.SimpleNamespace(candidates=lambda name, limit: self.names.get(name, [])[:limit], STOP=set())
|
| 26 |
+
x = types.SimpleNamespace(L=linker, norm=lambda s: re.sub(r"[^a-z0-9]+", " ", s.lower()).strip(),
|
| 27 |
+
_show=lambda value, q: value, TIME_VARIANT=set())
|
| 28 |
+
spec = importlib.util.spec_from_file_location("test_relation_spec", SOURCE)
|
| 29 |
+
self.module = importlib.util.module_from_spec(spec)
|
| 30 |
+
with patch.dict(sys.modules, {"exact_router_v1": x}):
|
| 31 |
+
spec.loader.exec_module(self.module)
|
| 32 |
+
self.module.in_domain = lambda qid, domain: (qid == 17738 and domain == "films") or (qid == 2536187 and domain == "singers")
|
| 33 |
+
self.module.values = lambda qid, prop: self.props.get((qid, prop), frozenset())
|
| 34 |
+
self.module.analyse = lambda q: {"cands": [17738], "feats": {("where", "born")}}
|
| 35 |
+
self.patterns = {"inv|who|composed": [800], "fwd|where|born": [19]}
|
| 36 |
+
|
| 37 |
+
def test_observed_failure_now_reads_composer_of_dated_film(self):
|
| 38 |
+
answer = self.module.answer("Who composed the music for Star Wars (1977)?", "films", self.patterns)
|
| 39 |
+
self.assertEqual((answer["answer"], answer["property"], answer["subject_qid"]), ("John Williams", 86, 17738))
|
| 40 |
+
|
| 41 |
+
def test_singer_notable_work_never_satisfies_composer(self):
|
| 42 |
+
self.assertIsNone(self.module.answer("Who composed the music for Star Wars (1977)?", "singers", self.patterns))
|
| 43 |
+
|
| 44 |
+
def test_supported_composer_phrasings(self):
|
| 45 |
+
for question in ["Who composed Star Wars?", "Who wrote the music for Star Wars?",
|
| 46 |
+
"Who is the composer of Star Wars?", "Which composer wrote Star Wars?",
|
| 47 |
+
"Could you please tell me who composed the music for the film Star Wars (1977)?"]:
|
| 48 |
+
with self.subTest(question=question):
|
| 49 |
+
self.assertEqual(self.module.answer(question, "films", self.patterns)["property"], 86)
|
| 50 |
+
|
| 51 |
+
def test_missing_composer_does_not_fall_back_to_notable_work(self):
|
| 52 |
+
self.props.pop((17738, 86))
|
| 53 |
+
self.assertIsNone(self.module.answer("Who composed Star Wars?", "films", self.patterns))
|
| 54 |
+
|
| 55 |
+
def test_unmatched_year_is_not_ignored(self):
|
| 56 |
+
self.assertIsNone(self.module.answer("Who composed Star Wars (1777)?", "films", self.patterns))
|
| 57 |
+
|
| 58 |
+
def test_extra_condition_is_not_ignored(self):
|
| 59 |
+
self.assertIsNone(self.module.answer("Who composed Star Wars and directed Jaws?", "films", self.patterns))
|
| 60 |
+
|
| 61 |
+
def test_capped_candidates_do_not_count_as_complete(self):
|
| 62 |
+
self.names["star wars"] = [(17738, 96, 1)] * 65
|
| 63 |
+
self.assertIsNone(self.module.answer("Who composed Star Wars?", "films", self.patterns))
|
| 64 |
+
|
| 65 |
+
def test_disagreeing_composers_are_not_selected_by_popularity(self):
|
| 66 |
+
self.names["star wars"].append((999, 1, 1))
|
| 67 |
+
self.module.in_domain = lambda qid, domain: qid in {17738, 999}
|
| 68 |
+
self.props[(999, 86)] = frozenset({"Someone else"})
|
| 69 |
+
self.assertIsNone(self.module.answer("Who composed Star Wars?", "films", self.patterns))
|
| 70 |
+
|
| 71 |
+
def test_other_relations_remain_available(self):
|
| 72 |
+
self.props[(17738, 19)] = frozenset({"Test City"})
|
| 73 |
+
answer = self.module.answer("Where was Test Person born?", "films", self.patterns)
|
| 74 |
+
self.assertEqual((answer["answer"], answer["property"]), ("Test City", 19))
|
| 75 |
+
|
| 76 |
+
def test_receipt_labels_preserve_property_meaning(self):
|
| 77 |
+
self.assertEqual(self.module.prop_label(26), "spouse")
|
| 78 |
+
self.assertEqual(self.module.prop_label(86), "composer")
|
| 79 |
+
self.assertEqual(self.module.prop_label(800), "notable work")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
unittest.main()
|