Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- root/wander-train/appraisal_v1.py +194 -0
- root/wander-train/chat_context_v1.py +372 -0
- root/wander-train/chem_agents_v1.py +586 -0
- root/wander-train/code_organ_v2.py +474 -0
- root/wander-train/code_organ_v3.py +488 -0
- root/wander-train/exact_router_v1.py +143 -0
- root/wander-train/general_reader_v1.py +471 -0
- root/wander-train/math_organ_v2.py +508 -0
- root/wander-train/oracle_cube_reader_v1_3.py +447 -0
- root/wander-train/oracle_game_combine_v1.py +209 -0
- root/wander-train/oracle_program_ir_v1.py +256 -0
- root/wander-train/oracle_release_conceptnet.py +142 -0
- root/wander-train/oracle_release_runtime_aa.py +135 -0
- root/wander-train/oracle_release_runtime_ac.py +140 -0
- root/wander-train/oracle_release_runtime_ag.py +104 -0
- root/wander-train/oracle_release_runtime_ah.py +76 -0
- root/wander-train/oracle_release_runtime_aj.py +72 -0
- root/wander-train/oracle_release_runtime_am.py +55 -0
- root/wander-train/oracle_release_runtime_c.py +113 -0
- root/wander-train/oracle_release_runtime_e.py +126 -0
- root/wander-train/oracle_release_runtime_h.py +70 -0
- root/wander-train/oracle_release_runtime_k.py +73 -0
- root/wander-train/oracle_release_runtime_n.py +17 -0
- root/wander-train/oracle_release_runtime_p.py +34 -0
- root/wander-train/oracle_release_runtime_q.py +148 -0
- root/wander-train/oracle_release_runtime_s.py +50 -0
- root/wander-train/oracle_release_runtime_u.py +44 -0
- root/wander-train/oracle_release_runtime_w58.py +212 -0
- root/wander-train/oracle_release_runtime_w59.py +164 -0
- root/wander-train/oracle_release_runtime_w60.py +64 -0
- root/wander-train/oracle_release_runtime_w71.py +245 -0
- root/wander-train/oracle_sibling_evidential_v3.py +92 -0
- root/wander-train/oracle_tritree_v1.py +473 -0
- root/wander-train/oracle_tritree_v2.py +479 -0
- root/wander-train/organ_dialogue_v1.py +125 -0
- root/wander-train/organ_self_v1.py +308 -0
- root/wander-train/path_memory_v1.py +171 -0
- root/wander-train/pubchem_periodic_table.csv +119 -0
- root/wander-train/relation_forms_v2.py +118 -0
- root/wander-train/stage5c58_cycling_cognitive_substrate.py +480 -0
- root/wander-train/stage5h1_generic_dataset_expert.py +377 -0
- root/wander-train/stage5h3_hrr_relational_state_transducer.py +363 -0
- root/wander-train/stage5k102_persistent_typed_discourse_agent.py +1614 -0
- root/wander-train/stage5k104_relational_ingress_equivalence_agent.py +228 -0
- root/wander-train/stage5k108_universal_terminal_proof_projection_agent.py +127 -0
- root/wander-train/stage5k115_copular_definition_equivalence_agent.py +229 -0
- root/wander-train/stage5k117_caller_context_isolation_agent.py +175 -0
- root/wander-train/stage5k11_quadratic_inverse_solver.py +348 -0
- root/wander-train/stage5k121_authenticated_context_operator_agent.py +243 -0
- root/wander-train/stage5k131_registered_source_action_ingress_agent.py +210 -0
root/wander-train/appraisal_v1.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""appraisal_v1 - APPRAISAL of a turn from what Oracle7 knows, not from emotion words.
|
| 3 |
+
|
| 4 |
+
Scherer's component-process checks, each computed from structure:
|
| 5 |
+
EVENT CLASS every noun/verb in the turn is walked up WordNet's hypernym graph; the classes it reaches
|
| 6 |
+
(illness, damage, loss, failure, danger, crime/deception, obstruction, gain, praise, reunion, surprise)
|
| 7 |
+
are the event's appraisal - "toothache" -> ill_health, "fire" -> danger, "cheated" -> deception,
|
| 8 |
+
with no cue for those words anywhere in this file
|
| 9 |
+
AGENCY whose doing: the speaker's own ("I broke", "I overslept") vs another's ("you", "he", "they", a passive
|
| 10 |
+
"I've been cheated") - another's obstruction appraises to anger, one's own loss to sadness
|
| 11 |
+
NOVELTY explicit unexpectedness markers
|
| 12 |
+
EMOTION the class those checks produce: fear (danger), anger (another's obstruction / norm violation),
|
| 13 |
+
sadness (own loss, illness, damage, failure, regret), surprise (unexpected), happiness (gain, praise,
|
| 14 |
+
reunion), disgust (revulsion)
|
| 15 |
+
Direct emotion words still count (they are evidence too), but the event walk is what generalises.
|
| 16 |
+
usage (dev tuning only): appraisal_v1.py dev"""
|
| 17 |
+
import re
|
| 18 |
+
import sys
|
| 19 |
+
from functools import lru_cache
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from nltk.corpus import wordnet as wn
|
| 23 |
+
wn.synsets("dog")
|
| 24 |
+
except Exception: # noqa: BLE001
|
| 25 |
+
wn = None
|
| 26 |
+
|
| 27 |
+
# WordNet anchor synsets -> appraisal class (a word appraises to a class when one of its senses reaches the anchor)
|
| 28 |
+
ANCHORS = {
|
| 29 |
+
"danger": ["danger.n.01", "fire.n.04", "emergency.n.01", "accident.n.01", "disaster.n.02", "attack.n.01",
|
| 30 |
+
"injury.n.01", "threat.n.01", "violence.n.01"],
|
| 31 |
+
"ill_health": ["ill_health.n.01", "illness.n.01", "pain.n.01", "ache.n.01", "symptom.n.01", "infection.n.01",
|
| 32 |
+
"wound.n.01"],
|
| 33 |
+
"damage": ["damage.n.01", "breakage.n.02", "break.v.02", "break.v.03", "destroy.v.01", "spill.v.01",
|
| 34 |
+
"malfunction.n.01"],
|
| 35 |
+
"loss": ["loss.n.01", "lose.v.01", "lose.v.02", "death.n.01", "die.v.01", "misplace.v.01", "forget.v.01"],
|
| 36 |
+
"failure": ["failure.n.01", "fail.v.02", "fail.v.04", "mistake.n.01", "delay.n.01", "oversleep.v.01"],
|
| 37 |
+
"deception": ["deception.n.01", "cheat.v.01", "cheat.v.02", "deceive.v.01", "rip_off.v.01", "crime.n.01",
|
| 38 |
+
"rob.v.01", "steal.v.01"],
|
| 39 |
+
"obstruction": ["obstruction.n.01", "traffic_jam.n.01", "wait.v.01", "trap.v.01", "block.v.01",
|
| 40 |
+
"pollution.n.01", "noise.n.01"],
|
| 41 |
+
"gain": ["gain.n.01", "win.v.01", "promotion.n.01", "success.n.01", "prize.n.01", "gift.n.01", "celebration.n.01"],
|
| 42 |
+
"praise": ["praise.n.01", "compliment.n.01", "congratulation.n.01"],
|
| 43 |
+
}
|
| 44 |
+
EMOTION_OF_CLASS = {"danger": "fear", "ill_health": "sadness", "damage": "sadness", "loss": "sadness",
|
| 45 |
+
"failure": "sadness", "deception": "anger", "obstruction": "anger", "gain": "happiness",
|
| 46 |
+
"praise": "happiness"}
|
| 47 |
+
DIRECT = {
|
| 48 |
+
"fear": r"\b(?:scared|frighten\w*|terrified|worried|nervous|anxious|panic\w*|butterflies|dread\w*|help me|save (?:my|me)|"
|
| 49 |
+
r"(?:i'?m|i am) (?:so |really |very )?afraid of)\b",
|
| 50 |
+
"anger": r"\b(?:angry|furious|mad at|annoy\w*|irritat\w*|fed up|sick of|i hate|ridiculous|outrageous|unacceptable|"
|
| 51 |
+
r"how dare|damn|can'?t (?:stand|bear)|hundred times|stupid|terrible|horrendous|awful|rude)\b",
|
| 52 |
+
"disgust": r"\b(?:disgust\w*|gross|revolting|yuck|nasty|filthy|sickening)\b",
|
| 53 |
+
"sadness": r"\b(?:sad|unhappy|depress\w*|lonely|i miss|sorry to hear|so sorry about|heartbroken|upset|cry\w*|"
|
| 54 |
+
r"(?:so|very) tired|not up to|against me|(?:i'?m|i am) sorry,? (?:i|sir|madam))\b",
|
| 55 |
+
"surprise": r"(?:\bwhat a surprise\b|\bno way\b|\breally\?|\bwow\b|\byikes\b|\bcan'?t believe\b|\bnever believe\b|"
|
| 56 |
+
r"\bsurpris\w*|\bfancy meeting\b|\bunbelievable\b|\bwhat happened\b|\balmost didn'?t recogni[sz]e\b|"
|
| 57 |
+
r"\bhow come\b|\bis that you\b)",
|
| 58 |
+
"happiness": r"\b(?:happy|glad|wonderful|fantastic|excited|delighted|congratulations|thank you so much|can'?t wait|"
|
| 59 |
+
r"nice to (?:see|meet)|great news|lovely|beautiful|magnificent|cute|proud|impressed|good job|enjoy|"
|
| 60 |
+
r"in heaven|smells good|finally (?:here|back))\b",
|
| 61 |
+
}
|
| 62 |
+
OTHER_AGENT = re.compile(r"\b(?:you|he|she|they|someone|somebody|that guy|the (?:driver|waiter|company|shop|landlord))\b"
|
| 63 |
+
r"|\b(?:been|got|was|were) (?:\w+ed|cheated|robbed|stolen|hit|fired|dumped)\b", re.I)
|
| 64 |
+
STOP = set("be have do get go make take come see know say think want like feel look give use find tell ask seem "
|
| 65 |
+
"try leave call keep let begin show hear play run move live believe bring happen write sit stand lose pay "
|
| 66 |
+
"meet include continue set learn change lead understand watch follow stop create speak read spend grow "
|
| 67 |
+
"open walk win offer remember love consider appear buy wait serve die send build stay fall cut reach kill "
|
| 68 |
+
"raise pass sell decide return explain hope develop carry break receive agree support hit produce eat "
|
| 69 |
+
"cover catch draw choose wear thing time way day man woman people year".split())
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@lru_cache(maxsize=None)
|
| 73 |
+
def _anchors():
|
| 74 |
+
if wn is None:
|
| 75 |
+
return {}
|
| 76 |
+
out = {}
|
| 77 |
+
for cls, names in ANCHORS.items():
|
| 78 |
+
for n in names:
|
| 79 |
+
try:
|
| 80 |
+
out[wn.synset(n)] = cls
|
| 81 |
+
except Exception: # noqa: BLE001
|
| 82 |
+
pass
|
| 83 |
+
return out
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@lru_cache(maxsize=20000)
|
| 87 |
+
def word_class(word):
|
| 88 |
+
"""the appraisal class reached by the word's MOST FREQUENT sense (WordNet usage counts), walked only when that
|
| 89 |
+
sense is a noun - "going" (verb), "down" (adverb), "still" (adverb) are not events; "toothache", "fire" are"""
|
| 90 |
+
if wn is None:
|
| 91 |
+
return None
|
| 92 |
+
anchors = _anchors()
|
| 93 |
+
syns = wn.synsets(word.lower())
|
| 94 |
+
if not syns:
|
| 95 |
+
return None
|
| 96 |
+
top = max(syns, key=lambda x: (sum(l.count() for l in x.lemmas() if l.name().lower() == word.lower()), -syns.index(x)))
|
| 97 |
+
if top.pos() != "n":
|
| 98 |
+
return None
|
| 99 |
+
seen, frontier = set(), [top]
|
| 100 |
+
for _ in range(8):
|
| 101 |
+
nxt = []
|
| 102 |
+
for x in frontier:
|
| 103 |
+
if x in anchors:
|
| 104 |
+
return anchors[x]
|
| 105 |
+
for h in x.hypernyms() + x.instance_hypernyms():
|
| 106 |
+
if h not in seen:
|
| 107 |
+
seen.add(h)
|
| 108 |
+
nxt.append(h)
|
| 109 |
+
frontier = nxt
|
| 110 |
+
if not frontier:
|
| 111 |
+
break
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# events carried by VERBS are named explicitly (a verb's most frequent sense is rarely the eventful one)
|
| 116 |
+
EVENT_VERBS = {"deception": r"\b(?:cheated|robbed|stolen|stole|ripped off|scammed|lied to)\b",
|
| 117 |
+
"damage": r"\b(?:broke|broken|smashed|spilled|crashed|ruined|wrecked)\b",
|
| 118 |
+
"loss": r"\b(?:lost (?:my|our|the|his|her)|left my|passed away|died)\b",
|
| 119 |
+
"failure": r"\b(?:overslept|failed|missed (?:the|my)|didn'?t (?:accomplish|finish|make it|pass|win)|forgot)\b",
|
| 120 |
+
"obstruction": r"\b(?:stuck|waiting (?:for )?(?:quite|so|such) (?:a )?(?:long|time)|almost hit|traffic jam)\b",
|
| 121 |
+
"gain": r"\b(?:won(?!')|promoted|got the job|finally (?:lost|got|finished|passed)|new baby|just bought)\b"}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def appraise(text):
|
| 125 |
+
"""{"emotion", "classes", "agent", "novel", "evidence"} - evidence names the words and the checks that fired"""
|
| 126 |
+
t = str(text or "").replace("’", "'").replace(" ' ", "'")
|
| 127 |
+
direct = [e for e, rx in DIRECT.items() if re.search(rx, t, re.I)]
|
| 128 |
+
classes, ev = [], []
|
| 129 |
+
for c, rx in EVENT_VERBS.items():
|
| 130 |
+
m = re.search(rx, t, re.I)
|
| 131 |
+
if m:
|
| 132 |
+
classes.append(c)
|
| 133 |
+
ev.append("%s->%s" % (m.group(0).lower(), c))
|
| 134 |
+
for w in re.findall(r"[A-Za-z][A-Za-z'-]{2,}", t):
|
| 135 |
+
wl = w.lower().strip("'")
|
| 136 |
+
if wl in STOP or len(wl) < 4:
|
| 137 |
+
continue
|
| 138 |
+
c = word_class(wl)
|
| 139 |
+
if c:
|
| 140 |
+
classes.append(c)
|
| 141 |
+
ev.append("%s->%s" % (wl, c))
|
| 142 |
+
other = bool(OTHER_AGENT.search(t))
|
| 143 |
+
novel = "surprise" in direct
|
| 144 |
+
emo = "none"
|
| 145 |
+
if "fear" in direct or "danger" in classes:
|
| 146 |
+
emo = "fear"
|
| 147 |
+
elif "anger" in direct or "deception" in classes or ("obstruction" in classes and other):
|
| 148 |
+
emo = "anger"
|
| 149 |
+
elif "disgust" in direct:
|
| 150 |
+
emo = "disgust"
|
| 151 |
+
elif "gain" in classes and "finally" in t.lower():
|
| 152 |
+
emo = "happiness"
|
| 153 |
+
elif "sadness" in direct or any(c in classes for c in ("ill_health", "loss", "damage", "failure")):
|
| 154 |
+
emo = "sadness"
|
| 155 |
+
elif novel:
|
| 156 |
+
emo = "surprise"
|
| 157 |
+
elif "happiness" in direct or any(c in classes for c in ("gain", "praise")):
|
| 158 |
+
emo = "happiness"
|
| 159 |
+
return {"emotion": emo, "classes": classes, "agent": "other" if other else "self", "novel": novel,
|
| 160 |
+
"evidence": ev + ["direct:%s" % d for d in direct]}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _dev():
|
| 164 |
+
import collections
|
| 165 |
+
sys.path.insert(0, "/root/w240")
|
| 166 |
+
from relational_instrument_v1 import dd
|
| 167 |
+
tp, hu, pr, fp, nn, miss = collections.Counter(), collections.Counter(), collections.Counter(), 0, 0, []
|
| 168 |
+
fps = collections.Counter()
|
| 169 |
+
for r in dd("dev"):
|
| 170 |
+
g, a = r["e0"], appraise(r["opener"])
|
| 171 |
+
p = a["emotion"]
|
| 172 |
+
if g != "none":
|
| 173 |
+
hu[g] += 1
|
| 174 |
+
else:
|
| 175 |
+
nn += 1
|
| 176 |
+
if p != "none":
|
| 177 |
+
fp += 1
|
| 178 |
+
fps[(p, tuple(a["evidence"][:3]))] += 1
|
| 179 |
+
if p != "none":
|
| 180 |
+
pr[p] += 1
|
| 181 |
+
if g == p and g != "none":
|
| 182 |
+
tp[g] += 1
|
| 183 |
+
elif g != "none" and len(miss) < 25:
|
| 184 |
+
miss.append((g, p, a["evidence"][:4], r["opener"][:80]))
|
| 185 |
+
for e in ("anger", "disgust", "fear", "happiness", "sadness", "surprise"):
|
| 186 |
+
print(" %-9s %3d / %3d human / %3d predicted" % (e, tp[e], hu[e], pr[e]))
|
| 187 |
+
print(" DEV recall %d/%d | false positives on neutral %d/%d" % (sum(tp.values()), sum(hu.values()), fp, nn))
|
| 188 |
+
print(" top false-positive evidence:", fps.most_common(12))
|
| 189 |
+
for m in miss:
|
| 190 |
+
print(" miss", m)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
_dev()
|
root/wander-train/chat_context_v1.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""chat_context_v1 - Oracle7's CONVERSATION engine (Ben 2026-10-09: "we have what we need for a proper chat model").
|
| 3 |
+
|
| 4 |
+
Chat failed by REFUSAL, not by error: a follow-up ("Where was he born?", "And its currency?", "And of Denmark?", "What does
|
| 5 |
+
that cost?") reached the core without the conversation, and the core either refused or - worse - read the pronoun as the
|
| 6 |
+
topic ("he" -> an article about the word "he"). This engine never asserts anything itself. It keeps a small DIALOGUE STATE
|
| 7 |
+
per session and REWRITES a turn that only makes sense through the conversation into the standalone question it stands
|
| 8 |
+
for; the whole verified chain (router, 210 specialists, fusion, maths) then answers that question exactly as it would if
|
| 9 |
+
it had been asked outright. The rewrite is shown in the reply's path and provenance, so the reading can be checked.
|
| 10 |
+
|
| 11 |
+
STATE (per session): the previous standalone question; its SUBJECT entity (the entity asked about) and its ANSWER entity
|
| 12 |
+
(the entity the reply named), each with whether it is a person; the previous numeric RESULT (value + unit); solved
|
| 13 |
+
variables ("x = 5"); the previous question's material phrase ("burning 5 kg of coal").
|
| 14 |
+
PRONOUNS he/she/him/his/her -> the person in play (the answer if it is a person - "Who wrote Frankenstein? / Who was her
|
| 15 |
+
mother?" - else the subject); it/its -> the THING asked about ("Which planet does Titan orbit? / Who discovered
|
| 16 |
+
it?" is Titan); "that country / that city ..." -> the entity just named; possessives become "the <noun> of <E>".
|
| 17 |
+
ELLIPSIS "And its currency?" (relation swap), "And of Denmark?" / "And Doctor Zhivago?" (entity swap in the previous
|
| 18 |
+
question), "And on the Moon?" / "And with radius 10 m?" (modifier swap or addition), "Where?" / "In what year?"
|
| 19 |
+
(question-word swap, active -> passive when needed: "Who discovered Titan?" -> "In what year was Titan
|
| 20 |
+
discovered?").
|
| 21 |
+
RESULTS "What does that cost at $0.20 per kWh?" -> the previous result ("300 kWh"); "What is x squared?" -> the solved
|
| 22 |
+
x; "How much CO2 does that emit?" -> the previous material phrase ("burning 5 kg of coal").
|
| 23 |
+
MULTI "What is the capital of Austria and what is its currency?" -> two questions, each resolved and answered.
|
| 24 |
+
A turn with nothing to resolve is passed through untouched. If a rewrite cannot be made with confidence, nothing is
|
| 25 |
+
rewritten (the chain's own reply stands)."""
|
| 26 |
+
import re
|
| 27 |
+
|
| 28 |
+
import relation_spec_v1 as RS
|
| 29 |
+
|
| 30 |
+
PRON_PERSON = r"he|she|him|his|her|hers"
|
| 31 |
+
PRON_THING = r"it|its"
|
| 32 |
+
REL_NOUNS = r"capital(?: city)?|currency|population|official language|national anthem|anthem|continent|calling code|highest point|" \
|
| 33 |
+
r"leader|president|prime minister|author|director|composer|creator|founder|headquarters|nationality|religion|" \
|
| 34 |
+
r"spouse|wife|husband|father|mother|birthplace|place of birth|date of birth|birthday"
|
| 35 |
+
WH = r"who|what|which|where|when|how"
|
| 36 |
+
STATE_CAP = 2000
|
| 37 |
+
UNRESOLVED = "__unresolved__" # rewrite() -> (UNRESOLVED, pronoun): a short turn whose pronoun has nothing to refer to
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class Turn:
|
| 41 |
+
def __init__(self):
|
| 42 |
+
self.q = None # previous standalone question
|
| 43 |
+
self.subj = None # (qid, label, is_person, surface)
|
| 44 |
+
self.ans = None # (qid, label, is_person)
|
| 45 |
+
self.result = None # (value, unit)
|
| 46 |
+
self.solved = {} # variable -> value
|
| 47 |
+
self.material = None # "burning 5 kg of coal" / "5 kg of coal"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
_STATES = {}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def state(sid):
|
| 54 |
+
st = _STATES.get(sid)
|
| 55 |
+
if st is None:
|
| 56 |
+
if len(_STATES) > STATE_CAP:
|
| 57 |
+
_STATES.clear()
|
| 58 |
+
st = _STATES[sid] = Turn()
|
| 59 |
+
return st
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _is_person(qid):
|
| 63 |
+
try:
|
| 64 |
+
return 5 in RS.classes_of(qid)[0]
|
| 65 |
+
except Exception: # noqa: BLE001
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _entity_of_question(q):
|
| 70 |
+
a = RS.analyse(q)
|
| 71 |
+
if not a or not a["cands"]:
|
| 72 |
+
return None
|
| 73 |
+
qid = a["cands"][0]
|
| 74 |
+
return (qid, RS._rs().label(qid) or a["span"], _is_person(qid), a["span"])
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
RECEIPT_ENDS = re.compile(r"From the Wikidata relation store: (.+?)(?: \(Q(\d+)\))? - [^()]*? \(P\d+\) - (.+?)(?: \(Q(\d+)\))?\.\s*(?:\[|$)")
|
| 78 |
+
TYPED = re.compile(r"\(typed relations: wd:Q(\d+) P\d+ wd:Q(\d+)\)")
|
| 79 |
+
RECEIPT = re.compile(r"From the Wikidata relation store: .*? - .*? \(P\d+\) - (.+?)(?: \(Q(\d+)\))?\.\s*(?:\[|$)")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _entity_of_reply(reply, prefer_person=False):
|
| 83 |
+
"""the entity the reply named; after a WHO question the answer is a person, so a person among the name's top candidates
|
| 84 |
+
is preferred (the linker's popularity order put a song called "Pablo Picasso" before the painter)"""
|
| 85 |
+
text = re.sub(r"^\s*\[ev:[a-z_]+\]\s*", "", str(reply or ""))
|
| 86 |
+
m = RECEIPT_ENDS.search(text)
|
| 87 |
+
if m:
|
| 88 |
+
# the receipt reads subject - property - value, in the direction of the STORE: an inverse read ("Who painted
|
| 89 |
+
# Guernica?" -> "Pablo Picasso (Q5593) - wrote - Guernica (Q175036)") puts the answer FIRST. The answer is the end
|
| 90 |
+
# whose label the reply leads with.
|
| 91 |
+
lead = text.split(". From the Wikidata relation store:", 1)[0].strip().lower()
|
| 92 |
+
ends = [(m.group(1), m.group(2)), (m.group(3), m.group(4))]
|
| 93 |
+
pick = [e for e in ends if e[1] and e[0].strip().lower() == lead] or [e for e in ends[::-1] if e[1]]
|
| 94 |
+
if pick:
|
| 95 |
+
qid = int(pick[0][1])
|
| 96 |
+
return (qid, RS._rs().label(qid) or pick[0][0], _is_person(qid))
|
| 97 |
+
m = RECEIPT.search(text)
|
| 98 |
+
if m and m.group(2):
|
| 99 |
+
qid = int(m.group(2))
|
| 100 |
+
return (qid, RS._rs().label(qid) or m.group(1), _is_person(qid))
|
| 101 |
+
# a sentence ends at . ! ? - but not after an initial ("J. R. R. Tolkien")
|
| 102 |
+
first = re.split(r"(?<![\s.][A-Z]\.)(?<!^[A-Z]\.)(?<=[.!?])\s", text, 1)[0].rstrip(".")
|
| 103 |
+
m = TYPED.search(text)
|
| 104 |
+
if m:
|
| 105 |
+
# the reply's typed relation names the exact entities; the answer is the end the reply leads with
|
| 106 |
+
lead = re.sub(r"\s*\[[^\]]*\]\s*", " ", first).strip().lower()
|
| 107 |
+
for q in (int(m.group(2)), int(m.group(1))):
|
| 108 |
+
lab = RS._rs().label(q)
|
| 109 |
+
if lab and lab.lower() == lead:
|
| 110 |
+
return (q, lab, _is_person(q))
|
| 111 |
+
first = re.sub(r"\s*\[[^\]]*\]\s*", " ", first).strip()
|
| 112 |
+
if not first or len(first) > 160:
|
| 113 |
+
return None
|
| 114 |
+
try:
|
| 115 |
+
sp = RS.X.L.link(first, 3)
|
| 116 |
+
except Exception: # noqa: BLE001
|
| 117 |
+
return None
|
| 118 |
+
if not sp:
|
| 119 |
+
return None
|
| 120 |
+
span, cs = sp[0]
|
| 121 |
+
qid = cs[0][0]
|
| 122 |
+
if prefer_person:
|
| 123 |
+
people = [c[0] for c in cs[:3] if _is_person(c[0])]
|
| 124 |
+
if people:
|
| 125 |
+
qid = people[0]
|
| 126 |
+
return (qid, RS._rs().label(qid) or span, _is_person(qid))
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
NUM_UNIT = re.compile(r"(?:is|about|=|of)\s+\$?(-?\d[\d,]*(?:\.\d+)?(?:e[+-]?\d+)?)\s*(kwh|kj|mj|j|kg|g|m|km|s|n|pa|kpa|w|kw|litres?|liters?|l|m³|m3|°c|%)?", re.I)
|
| 130 |
+
SOLVED = re.compile(r"\b([a-z])\s*=\s*(-?\d+(?:\.\d+)?)\b")
|
| 131 |
+
MATERIAL = re.compile(r"\b((?:burning\s+)?\d[\d,]*(?:\.\d+)?\s*(?:kg|g|tonnes?|litres?|liters?|m3)\s+of\s+[a-z][a-z ]{1,24}?)(?=\s+(?:release|emit|produce|give|make|weigh)|[?.,]|$)", re.I)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _result_of_reply(reply):
|
| 135 |
+
text = re.sub(r"^\s*\[ev:[a-z_]+\]\s*", "", str(reply or ""))
|
| 136 |
+
m = re.search(r"(?:answer is|is about|takes about|cost(?:s)? (?:is |about )?)\s*\$?(-?\d[\d,]*(?:\.\d+)?(?:e[+-]?\d+)?)\s*([a-zA-Z³%°]+)?", text)
|
| 137 |
+
if not m:
|
| 138 |
+
m = NUM_UNIT.search(text)
|
| 139 |
+
if not m:
|
| 140 |
+
return None
|
| 141 |
+
try:
|
| 142 |
+
v = float(m.group(1).replace(",", ""))
|
| 143 |
+
except ValueError:
|
| 144 |
+
return None
|
| 145 |
+
unit = (m.group(2) or "").lower()
|
| 146 |
+
if "$" in text[:max(0, m.start(1)) + 2] and not unit:
|
| 147 |
+
unit = "$"
|
| 148 |
+
return (v, unit)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def update(sid, standalone, reply):
|
| 152 |
+
"""after the chain answered (or declined) the standalone question: remember what the conversation is about"""
|
| 153 |
+
st = state(sid)
|
| 154 |
+
numeric = bool(re.search(r"\d", standalone)) and isinstance(reply, dict) and reply.get("status") == "answered" \
|
| 155 |
+
and _result_of_reply(reply.get("response")) is not None
|
| 156 |
+
if not recognised(standalone) and not numeric:
|
| 157 |
+
return # context comes from FACTUAL turns only
|
| 158 |
+
st.q = standalone
|
| 159 |
+
# an entity the conversation already resolved KEEPS its identity when the rewrite names it (relinking "Pablo Picasso"
|
| 160 |
+
# by popularity picked a song, Q7121722, over the painter the previous answer named, Q5593)
|
| 161 |
+
subj = next((tuple(e[:3]) + (e[1],) for e in (st.ans, st.subj) if e and len(e[1] or "") >= 3 and e[1] in standalone), None) \
|
| 162 |
+
or _entity_of_question(standalone)
|
| 163 |
+
typed = TYPED.search(str(reply.get("response", ""))) if isinstance(reply, dict) else None
|
| 164 |
+
if subj and typed:
|
| 165 |
+
for q in (int(typed.group(1)), int(typed.group(2))):
|
| 166 |
+
if q != subj[0] and (RS._rs().label(q) or "").lower() == subj[1].lower():
|
| 167 |
+
subj = (q, subj[1], _is_person(q), subj[3]) # the reply's own typed relation names the exact entity
|
| 168 |
+
if subj:
|
| 169 |
+
st.subj = subj
|
| 170 |
+
if isinstance(reply, dict) and reply.get("status") == "answered":
|
| 171 |
+
ans = _entity_of_reply(reply.get("response"), prefer_person=bool(re.match(r"^\s*(?:who|whom|whose)\b", standalone, re.I)))
|
| 172 |
+
st.ans = ans if ans and (not subj or ans[0] != subj[0]) else None
|
| 173 |
+
r = _result_of_reply(reply.get("response"))
|
| 174 |
+
st.result = r
|
| 175 |
+
for var, val in SOLVED.findall(str(reply.get("response", ""))):
|
| 176 |
+
st.solved[var] = val
|
| 177 |
+
mm = MATERIAL.search(standalone)
|
| 178 |
+
if mm:
|
| 179 |
+
st.material = mm.group(1).strip()
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _person(st):
|
| 183 |
+
if st.ans and st.ans[2]:
|
| 184 |
+
return st.ans[1]
|
| 185 |
+
if st.subj and st.subj[2]:
|
| 186 |
+
return st.subj[1]
|
| 187 |
+
return None
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _thing(st):
|
| 191 |
+
if st.subj and not st.subj[2]:
|
| 192 |
+
return st.subj[1]
|
| 193 |
+
if st.ans and not st.ans[2]:
|
| 194 |
+
return st.ans[1]
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _cap(s):
|
| 199 |
+
s = s.strip()
|
| 200 |
+
return s[:1].upper() + s[1:] if s else s
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _q(s):
|
| 204 |
+
s = s.strip().rstrip("?.! ")
|
| 205 |
+
return _cap(s) + "?"
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _swap_wh(prev, wh):
|
| 209 |
+
"""'When was Alfred Hitchcock born?' + 'Where' -> 'Where was Alfred Hitchcock born?'; active 'Who discovered Titan?' ->
|
| 210 |
+
'In what year was Titan discovered?'"""
|
| 211 |
+
p = prev.strip().rstrip("?")
|
| 212 |
+
m = re.match(r"^(?:who|what|which|where|when|how)\b(?:\s+year)?\s+((?:was|were|is|are|did|does|do)\b.*)$", p, re.I)
|
| 213 |
+
if m:
|
| 214 |
+
return _q("%s %s" % (wh, m.group(1)))
|
| 215 |
+
m = re.match(r"^who\s+(\w+ed)\s+(.+)$", p, re.I)
|
| 216 |
+
if m:
|
| 217 |
+
return _q("%s was %s %s" % (wh, m.group(2), m.group(1)))
|
| 218 |
+
return None
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _rewrite_raw(sid, text):
|
| 222 |
+
"""-> (kind, standalone question) with kind "num" | "rel", (UNRESOLVED, pronoun), or None"""
|
| 223 |
+
st = state(sid)
|
| 224 |
+
t = " ".join(str(text or "").split())
|
| 225 |
+
low = t.lower().rstrip("?.! ")
|
| 226 |
+
if not t:
|
| 227 |
+
return None
|
| 228 |
+
pron = re.search(r"\b(he|she|him|his|her|it|its|they|them|their)\b", low)
|
| 229 |
+
short_bare = pron is not None and len(low.split()) <= 12 and re.search(r"(?<=\s)[A-Z][a-z]+", t) is None
|
| 230 |
+
if not st.q:
|
| 231 |
+
return (UNRESOLVED, pron.group(1)) if short_bare and pron.group(1) not in ("it", "its") else None
|
| 232 |
+
# results and variables
|
| 233 |
+
m = re.search(r"\bwhat is ([a-z]) (squared|cubed)\b", low)
|
| 234 |
+
if m and m.group(1) in st.solved:
|
| 235 |
+
power = "2" if m.group(2) == "squared" else "3"
|
| 236 |
+
return ("num", _q(re.sub(r"\b%s (squared|cubed)\b" % m.group(1), "%s^%s" % (st.solved[m.group(1)], power), t, flags=re.I)))
|
| 237 |
+
if re.search(r"\b(?:that|this)\b", low) and (st.result or st.material):
|
| 238 |
+
if re.search(r"\b(?:co2|carbon dioxide|emit|emission|release|weigh)", low) and st.material:
|
| 239 |
+
return ("num", _q(re.sub(r"\b(?:that|this)\b", st.material, t, count=1, flags=re.I)))
|
| 240 |
+
if st.result and st.result[1] and re.search(r"per\s+%s\b" % re.escape(st.result[1]), low):
|
| 241 |
+
return ("num", _q(re.sub(r"\b(?:that|this)\b", "%s %s" % (_fmt(st.result[0]), st.result[1]), t, count=1, flags=re.I)))
|
| 242 |
+
# bare question word: "Where?", "When?", "In what year?"
|
| 243 |
+
m = re.match(r"^(?:and\s+)?(where|when|in what year|what year)$", low)
|
| 244 |
+
if m:
|
| 245 |
+
r = _swap_wh(st.q, {"what year": "In what year"}.get(m.group(1), _cap(m.group(1))))
|
| 246 |
+
return ("rel", r) if r else None
|
| 247 |
+
# ellipsis with "And ..."
|
| 248 |
+
m = re.match(r"^(?:and|what about|how about)\s+(.+)$", low)
|
| 249 |
+
if m:
|
| 250 |
+
rest = t[len(t) - len(m.group(1)) - (len(t) - len(t.rstrip("?.! "))):].rstrip("?.! ")
|
| 251 |
+
rest_low = rest.lower()
|
| 252 |
+
mr = re.match(r"^(?:the\s+|its\s+|his\s+|her\s+|their\s+)?(%s)$" % REL_NOUNS, rest_low)
|
| 253 |
+
if mr:
|
| 254 |
+
ent = _thing(st) if not re.match(r"^(his|her)\b", rest_low) else _person(st)
|
| 255 |
+
ent = ent or _thing(st) or _person(st)
|
| 256 |
+
return ("rel", _q("What is the %s of %s" % (mr.group(1), ent))) if ent else None
|
| 257 |
+
mp = re.match(r"^(on|in|at|with|for|of)\s+(.+)$", rest, re.I)
|
| 258 |
+
if mp and mp.group(1).lower() in ("of", "for") and st.subj and st.subj[3]:
|
| 259 |
+
r = _replace_subject(st, mp.group(2))
|
| 260 |
+
return ("rel", r) if r else None
|
| 261 |
+
if mp and mp.group(1).lower() in ("on", "in", "at", "with"):
|
| 262 |
+
prep, phrase = mp.group(1).lower(), mp.group(2)
|
| 263 |
+
pm = re.search(r"\b%s\s+[^?]*?\d[^?]*?(?=$|\?|,)" % prep, st.q, re.I) if re.search(r"\d", phrase) else None
|
| 264 |
+
if not st.result:
|
| 265 |
+
return None # a modifier swap continues a COMPUTATION only
|
| 266 |
+
if pm:
|
| 267 |
+
return ("num", _q(st.q[:pm.start()] + "%s %s" % (prep, phrase) + st.q[pm.end():]))
|
| 268 |
+
return ("num", _q(st.q.rstrip("?.! ") + " %s %s" % (prep, phrase)))
|
| 269 |
+
if rest[:1].isupper() and st.subj and st.subj[3]:
|
| 270 |
+
r = _replace_subject(st, rest)
|
| 271 |
+
return ("rel", r) if r else None
|
| 272 |
+
return None
|
| 273 |
+
# "that country" / possessive and personal pronouns
|
| 274 |
+
out = t
|
| 275 |
+
changed = False
|
| 276 |
+
m = re.search(r"\bthat\s+(country|city|state|planet|nation|island|river|band|team|company|book|film|novel|place)\b", out, re.I)
|
| 277 |
+
if m:
|
| 278 |
+
ent = (st.ans[1] if st.ans and not st.ans[2] else None) or _thing(st)
|
| 279 |
+
if not ent:
|
| 280 |
+
return None
|
| 281 |
+
out = out[:m.start()] + ent + out[m.end():]
|
| 282 |
+
changed = True
|
| 283 |
+
# a turn that names its own subject (a capitalised name after the first word) keeps its own pronouns
|
| 284 |
+
names_own = re.search(r"(?<=\s)[A-Z][a-z]+", out) is not None and not changed
|
| 285 |
+
for pron, getter in (() if names_own else ((PRON_PERSON, _person), (PRON_THING, _thing))):
|
| 286 |
+
for pm in list(re.finditer(r"\b(%s)\b(\s+[a-z]+(?:\s+[a-z]+)?)?" % pron, out, re.I))[::-1]:
|
| 287 |
+
ent = getter(st)
|
| 288 |
+
if not ent:
|
| 289 |
+
return (UNRESOLVED, pm.group(1).lower()) if short_bare else None
|
| 290 |
+
word = pm.group(1).lower()
|
| 291 |
+
if word in ("his", "her", "its") and pm.group(2):
|
| 292 |
+
noun = pm.group(2).strip()
|
| 293 |
+
if re.match(r"^(%s)\b" % REL_NOUNS, noun, re.I) or word in ("his", "its"):
|
| 294 |
+
rep = "the %s of %s" % (noun, ent)
|
| 295 |
+
out = out[:pm.start()] + rep + out[pm.end():]
|
| 296 |
+
changed = True
|
| 297 |
+
continue
|
| 298 |
+
out = out[:pm.start(1)] + ent + out[pm.end(1):]
|
| 299 |
+
changed = True
|
| 300 |
+
if not changed:
|
| 301 |
+
return None
|
| 302 |
+
return ("rel", _q(out))
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def recognised(q):
|
| 306 |
+
"""a question the verified relation layer can READ: it matches one of the hand-written relation readers whole"""
|
| 307 |
+
try:
|
| 308 |
+
import relation_spec_v2 as S2
|
| 309 |
+
_, low = S2.normalise(q)
|
| 310 |
+
for key in S2.RD.PROPS:
|
| 311 |
+
c = S2.compiled(key)
|
| 312 |
+
if any(rx.match(low) for d in ("fwd", "inv") for rx in c[d]):
|
| 313 |
+
return True
|
| 314 |
+
except Exception: # noqa: BLE001
|
| 315 |
+
return False
|
| 316 |
+
return False
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def rewrite(sid, text):
|
| 320 |
+
"""-> the standalone question this turn stands for, (UNRESOLVED, pronoun), or None. A rewrite is ACCEPTED only when it is
|
| 321 |
+
exactly right for the verified layer: a relation rewrite must be a question the relation readers recognise; a numeric
|
| 322 |
+
rewrite must continue a numeric answer. Anything else goes to the core untouched - the core has its own conversation
|
| 323 |
+
acts ("got it", "why?", "did you run it?", "how does it work?") that a rewrite would break (m52 replay, 2026-10-09)."""
|
| 324 |
+
r = _rewrite_raw(sid, text)
|
| 325 |
+
if r is None:
|
| 326 |
+
return None
|
| 327 |
+
if r[0] == UNRESOLVED:
|
| 328 |
+
return r if recognised(text) else None
|
| 329 |
+
kind, q = r
|
| 330 |
+
if not q:
|
| 331 |
+
return None
|
| 332 |
+
if kind == "num":
|
| 333 |
+
return q if state(sid).result or state(sid).solved else None
|
| 334 |
+
return q if recognised(q) else None
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _replace_subject(st, new):
|
| 338 |
+
surface = st.subj[3]
|
| 339 |
+
if surface and surface in st.q:
|
| 340 |
+
return _q(st.q.replace(surface, new, 1))
|
| 341 |
+
lab = st.subj[1]
|
| 342 |
+
if lab and lab in st.q:
|
| 343 |
+
return _q(st.q.replace(lab, new, 1))
|
| 344 |
+
return None
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def _fmt(v):
|
| 348 |
+
return ("%d" % v) if abs(v - round(v)) < 1e-9 else ("%.6g" % v)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
SPLIT = re.compile(r"\s+and\s+(?=(?:%s)\b)" % WH, re.I)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def split_parts(text):
|
| 355 |
+
"""'What is the capital of Austria and what is its currency?' -> two questions (only when the second part is itself a
|
| 356 |
+
question starting with a question word)"""
|
| 357 |
+
t = " ".join(str(text or "").split()).rstrip("?")
|
| 358 |
+
parts = SPLIT.split(t)
|
| 359 |
+
if len(parts) < 2:
|
| 360 |
+
return None
|
| 361 |
+
# split ONLY when every part is a question the relation readers can read (panel v27 2026-10-09: a reading passage
|
| 362 |
+
# containing "... and Where to Find Them" and "Everything is fine, and how are you?" were split and lost)
|
| 363 |
+
return [_q(p) for p in parts] if all(_shape_ok(_q(p)) for p in parts) else None
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def _shape_ok(p):
|
| 367 |
+
"""the part reads as a relation question - as written, or with its pronoun standing for a named thing"""
|
| 368 |
+
if recognised(p):
|
| 369 |
+
return True
|
| 370 |
+
s = re.sub(r"\b(?:its|his|her|their)\s+(\w+(?:\s+\w+)?)", r"the \1 of Peru", p, flags=re.I)
|
| 371 |
+
s = re.sub(r"\b(?:he|she|it|they|him|them)\b", "Peru", s, flags=re.I)
|
| 372 |
+
return s != p and recognised(s)
|
root/wander-train/chem_agents_v1.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""chem_agents_v1 - five chemistry agents (Oracle7 specialists) that compute exactly what a question asks, from the values it
|
| 3 |
+
gives, with every constant stated. Built for the university-chemistry questions the benchmark curriculum showed the family
|
| 4 |
+
could not reach (MMLU-Pro chemistry, development rows 3400-3999 only).
|
| 5 |
+
GasAgent ideal gas: density, molar mass from density, one unknown of P V n T (mass + formula -> n)
|
| 6 |
+
PhAgent pH / pOH / [H+] of strong acids and bases, weak acid or base from Ka / Kb / pKa, Henderson-Hasselbalch
|
| 7 |
+
PhotonAgent photon energy / wavelength / frequency (per photon, per mole, eV), de Broglie, hydrogen-like transitions
|
| 8 |
+
ColligativeAgent freezing-point depression, boiling-point elevation, molality, molar mass from a shift, osmotic pressure
|
| 9 |
+
KineticsAgent first-order decay: half-life, rate constant, time to a fraction, fraction left
|
| 10 |
+
LAW (from the specialist network): EVERY number in the question must be read by the computation, or the agent declines -
|
| 11 |
+
an unread number means a condition the formula does not know about. Each agent also declines what it cannot type
|
| 12 |
+
(unknown formula, missing value, multi-part questions). Returns {kind, status, response, value}."""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import csv
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
|
| 20 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 21 |
+
R_LATM = 0.082057366 # L atm / (mol K)
|
| 22 |
+
R_J = 8.314462618 # J / (mol K)
|
| 23 |
+
H = 6.62607015e-34 # J s (exact)
|
| 24 |
+
C = 2.99792458e8 # m / s (exact)
|
| 25 |
+
NA = 6.02214076e23 # 1 / mol (exact)
|
| 26 |
+
EV = 1.602176634e-19 # J (exact)
|
| 27 |
+
ME = 9.1093837015e-31 # kg
|
| 28 |
+
RH = 1.09677583e7 # m^-1, Rydberg constant for hydrogen
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _table():
|
| 32 |
+
for p in (os.path.join(HERE, "pubchem_periodic_table.csv"), __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/specialists/chemistry-v3/pubchem_periodic_table.csv"):
|
| 33 |
+
if os.path.exists(p):
|
| 34 |
+
return {r["Symbol"]: float(r["AtomicMass"]) for r in csv.DictReader(open(p, encoding="utf-8"))}
|
| 35 |
+
return {}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
EL = _table()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------- text
|
| 42 |
+
def clean(t):
|
| 43 |
+
t = " ".join(str(t or "").split())
|
| 44 |
+
t = t.replace("−", "-").replace("–", "-").replace("\\times", "×").replace("\\cdot", "·").replace("°", " deg ")
|
| 45 |
+
t = re.sub(r"\\(?:mathrm|text|mathbf|operatorname)\s*\{([^{}]*)\}", r"\1", t)
|
| 46 |
+
t = re.sub(r"\\(?:mathrm|text)\s*\{([^{}]*)\}", r"\1", t)
|
| 47 |
+
t = t.replace("$", " ").replace("~", " ").replace("\\,", " ").replace("{", "").replace("}", "")
|
| 48 |
+
t = re.sub(r"\^\s*\\circ|\\circ", " deg ", t)
|
| 49 |
+
t = re.sub(r"_(\d)", r"\1", t) # CO_2 -> CO2, H_2SO_4 -> H2SO4
|
| 50 |
+
t = re.sub(r"(?<![\d.×x*])(?<![\d.]\s)\b10\s*\^\s*\(?([-+]?\d+)\)?", r"1×10^\1", t) # a bare power of ten is a number
|
| 51 |
+
t = re.sub(r"(\d)\s+(?=10\^)", r"\1×", t) # "2 10^-15" -> "2×10^-15"
|
| 52 |
+
t = re.sub(r"\s+", " ", t)
|
| 53 |
+
return t.strip()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
NUMRE = re.compile(r"(?<![A-Za-z\d.])([-+]?\d+(?:,\d{3})*(?:\.\d+)?|[-+]?\.\d+)\s*(?:(?:×|x|\*)\s*10\s*\^?\s*\(?\s*([-+]?\d+)\)?|[eE]([-+]?\d+))?")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def num(s):
|
| 60 |
+
m = NUMRE.fullmatch(s.strip())
|
| 61 |
+
if not m:
|
| 62 |
+
raise ValueError(s)
|
| 63 |
+
return _val(m)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _val(m):
|
| 67 |
+
v = float(m.group(1).replace(",", ""))
|
| 68 |
+
e = m.group(2) or m.group(3)
|
| 69 |
+
return v * 10 ** int(e) if e else v
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def numbers(t):
|
| 73 |
+
"""every number in the question with its span (formula subscripts excluded: H2SO4, CO2 are names, not values)"""
|
| 74 |
+
out = []
|
| 75 |
+
for m in NUMRE.finditer(t):
|
| 76 |
+
a = m.start()
|
| 77 |
+
if a > 0 and re.match(r"[A-Z)\]a-z]", t[a - 1]) and not re.match(r"[a-z]{2,}", t[max(0, a - 3):a]):
|
| 78 |
+
continue
|
| 79 |
+
if re.match(r"\d", t[a]) and a > 0 and t[a - 1] == "(" and re.match(r"\(\d+\)\s*[A-Z]", t[a - 1:]):
|
| 80 |
+
continue
|
| 81 |
+
out.append((m.start(), m.end(), _val(m)))
|
| 82 |
+
return out
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class Reader:
|
| 86 |
+
"""reads typed quantities off the question; records which number spans it used"""
|
| 87 |
+
|
| 88 |
+
def __init__(self, text):
|
| 89 |
+
self.t = clean(text)
|
| 90 |
+
self.low = self.t.lower()
|
| 91 |
+
self.nums = numbers(self.t)
|
| 92 |
+
self.used = set()
|
| 93 |
+
|
| 94 |
+
def _take(self, pattern, flags=re.I):
|
| 95 |
+
found = []
|
| 96 |
+
for m in re.finditer(pattern, self.t, flags):
|
| 97 |
+
found.append(m)
|
| 98 |
+
return found
|
| 99 |
+
|
| 100 |
+
def q(self, units, conv):
|
| 101 |
+
"""all quantities whose unit is one of `units`; conv(value, unit) -> SI-ish value"""
|
| 102 |
+
res = []
|
| 103 |
+
pat = r"(" + NUMRE.pattern + r")\s*(" + units + r")(?![a-z])"
|
| 104 |
+
for m in re.finditer(pat, self.t, re.I):
|
| 105 |
+
nm = NUMRE.match(m.group(1))
|
| 106 |
+
v = _val(nm)
|
| 107 |
+
res.append((conv(v, m.group(m.lastindex).lower()), m.start(), m.start() + len(m.group(1))))
|
| 108 |
+
# one value stated twice in two units ("720 mm Hg or 720 torr") is one quantity: keep it once, read both spans
|
| 109 |
+
if len(res) > 1 and all(abs(x[0] - res[0][0]) <= 1e-9 * max(abs(res[0][0]), 1e-300) for x in res):
|
| 110 |
+
for x in res[1:]:
|
| 111 |
+
self.used.add(x[1])
|
| 112 |
+
res = res[:1]
|
| 113 |
+
return res
|
| 114 |
+
|
| 115 |
+
def use(self, item):
|
| 116 |
+
self.used.add(item[1])
|
| 117 |
+
return item[0]
|
| 118 |
+
|
| 119 |
+
def all_read(self, also=()):
|
| 120 |
+
starts = set(self.used) | set(also)
|
| 121 |
+
return all(any(abs(s - a) <= 1 for s in starts) for a, _, _ in self.nums)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
P_UNITS = r"atm(?:ospheres?)?|torr|mm\s*hg|mmhg|kpa|pa|bar|psi"
|
| 125 |
+
V_UNITS = r"liters?|litres?|l|ml|milliliters?|cm3|cm\^3|dm3|dm\^3|m3|m\^3"
|
| 126 |
+
T_UNITS = r"deg\s*c|deg\s*k|deg\s*f|k(?![a-z])|kelvin|celsius|c(?![a-z])"
|
| 127 |
+
M_UNITS = r"kg|g|mg|grams?|kilograms?|milligrams?"
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def p_atm(v, u):
|
| 131 |
+
u = u.replace(" ", "")
|
| 132 |
+
return v * {"atm": 1, "atmosphere": 1, "atmospheres": 1, "torr": 1 / 760, "mmhg": 1 / 760, "kpa": 1 / 101.325, "pa": 1 / 101325,
|
| 133 |
+
"bar": 1 / 1.01325, "psi": 1 / 14.6959}[u]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def v_l(v, u):
|
| 137 |
+
u = u.replace("^", "")
|
| 138 |
+
if u in ("ml", "milliliter", "milliliters", "cm3"):
|
| 139 |
+
return v / 1000
|
| 140 |
+
if u == "m3":
|
| 141 |
+
return v * 1000
|
| 142 |
+
return v
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def t_k(v, u):
|
| 146 |
+
u = u.replace(" ", "")
|
| 147 |
+
if u in ("degc", "c", "celsius"):
|
| 148 |
+
return v + 273.15
|
| 149 |
+
if u == "degf":
|
| 150 |
+
return (v - 32) * 5 / 9 + 273.15
|
| 151 |
+
return v
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def m_g(v, u):
|
| 155 |
+
return v * 1000 if u.startswith("k") else v / 1000 if u.startswith("m") else v
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def molar_mass(f):
|
| 159 |
+
"""H2SO4, Ca(OH)2, CuSO4·5H2O -> g/mol; None if any symbol is unknown"""
|
| 160 |
+
f = f.replace("·", ".").replace("*", ".")
|
| 161 |
+
total = 0.0
|
| 162 |
+
for part in f.split("."):
|
| 163 |
+
k = re.match(r"(\d+)(.*)", part)
|
| 164 |
+
mult, part = (int(k.group(1)), k.group(2)) if k else (1, part)
|
| 165 |
+
stack, cur, i = [], {}, 0
|
| 166 |
+
while i < len(part):
|
| 167 |
+
ch = part[i]
|
| 168 |
+
if ch == "(":
|
| 169 |
+
stack.append(cur)
|
| 170 |
+
cur = {}
|
| 171 |
+
i += 1
|
| 172 |
+
elif ch == ")":
|
| 173 |
+
j = i + 1
|
| 174 |
+
while j < len(part) and part[j].isdigit():
|
| 175 |
+
j += 1
|
| 176 |
+
n = int(part[i + 1:j] or 1)
|
| 177 |
+
prev = stack.pop()
|
| 178 |
+
for s, c in cur.items():
|
| 179 |
+
prev[s] = prev.get(s, 0) + c * n
|
| 180 |
+
cur, i = prev, j
|
| 181 |
+
else:
|
| 182 |
+
m = re.match(r"([A-Z][a-z]?)(\d*)", part[i:])
|
| 183 |
+
if not m:
|
| 184 |
+
return None
|
| 185 |
+
s = m.group(1)
|
| 186 |
+
if s not in EL:
|
| 187 |
+
return None
|
| 188 |
+
cur[s] = cur.get(s, 0) + int(m.group(2) or 1)
|
| 189 |
+
i += len(m.group(0))
|
| 190 |
+
if stack:
|
| 191 |
+
return None
|
| 192 |
+
total += mult * sum(EL[s] * c for s, c in cur.items())
|
| 193 |
+
return total
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
FORMULA = re.compile(r"(?<![A-Za-z])((?:[A-Z][a-z]?\d*|\((?:[A-Z][a-z]?\d*)+\)\d*){1,8}(?:[.·]\d*(?:[A-Z][a-z]?\d*)+)?)(?![a-z])")
|
| 197 |
+
NAMES = {"oxygen": "O2", "nitrogen": "N2", "hydrogen": "H2", "chlorine": "Cl2", "fluorine": "F2", "helium": "He", "neon": "Ne",
|
| 198 |
+
"argon": "Ar", "carbon dioxide": "CO2", "carbon monoxide": "CO", "methane": "CH4", "ammonia": "NH3", "water": "H2O",
|
| 199 |
+
"ethane": "C2H6", "propane": "C3H8", "butane": "C4H10", "sulfur dioxide": "SO2", "nitrogen dioxide": "NO2",
|
| 200 |
+
"hydrogen chloride": "HCl", "glucose": "C6H12O6", "sucrose": "C12H22O11", "urea": "CH4N2O", "ethanol": "C2H5OH",
|
| 201 |
+
"methanol": "CH3OH", "benzene": "C6H6", "sodium chloride": "NaCl", "acetic acid": "CH3COOH", "xenon": "Xe", "krypton": "Kr"}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def formulas(t):
|
| 205 |
+
out = []
|
| 206 |
+
for m in FORMULA.finditer(t):
|
| 207 |
+
f = m.group(1)
|
| 208 |
+
syms = re.findall(r"[A-Z][a-z]?", f)
|
| 209 |
+
if (re.search(r"\d|\(", f) or len(syms) >= 2) and all(s in EL for s in syms):
|
| 210 |
+
out.append((f, m.start(), m.end()))
|
| 211 |
+
for name, f in NAMES.items():
|
| 212 |
+
for m in re.finditer(r"\b%s\b" % name, t, re.I):
|
| 213 |
+
out.append((f, m.start(), m.end()))
|
| 214 |
+
return out
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def sig(x):
|
| 218 |
+
return "%.4g" % x
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def say(kind, text, value=None, status="answered"):
|
| 222 |
+
return {"kind": kind, "status": status, "response": text, "value": value}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def multi_part(low):
|
| 226 |
+
return bool(re.search(r"\(a\)|\(b\)|\ba\)\s|\bb\)\s|\band (?:also |then )?(?:calculate|find|determine|compute)\b", low))
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
# ---------------------------------------------------------------- agents
|
| 230 |
+
class GasAgent:
|
| 231 |
+
def answer(self, text, session_id="default"):
|
| 232 |
+
r = Reader(text)
|
| 233 |
+
low = r.low
|
| 234 |
+
if multi_part(low) or not re.search(r"\bgas\b|\bdensity\b|\bmolar mass\b|\bmolecular weight\b|\bideal\b|\batm\b|\btorr\b|\bmm ?hg\b|\bkpa\b", low):
|
| 235 |
+
return None
|
| 236 |
+
if re.search(r"van ?der ?waals|real gas|effus|diffus|mean free|root.mean|rms|compressib|adiabat|isotherm|work|heat|"
|
| 237 |
+
r"partial pressure|mole fraction|mixture|reaction|react|produced|collected over water|vapor pressure|"
|
| 238 |
+
r"balloon|leak|graham|kinetic energy|speed|velocity|collision", low):
|
| 239 |
+
return None
|
| 240 |
+
P, V, T, M = r.q(P_UNITS, p_atm), r.q(V_UNITS, v_l), r.q(T_UNITS, t_k), r.q(M_UNITS, m_g)
|
| 241 |
+
D = r.q(r"g\s*/\s*(?:l|liters?|litres?|dm3)|g\s*l\^?-1|grams? per liter", lambda v, u: v)
|
| 242 |
+
N = r.q(r"moles?|mol(?![a-z])", lambda v, u: v)
|
| 243 |
+
if any(len(x) > 1 for x in (P, V, T, M, D, N)):
|
| 244 |
+
return None
|
| 245 |
+
stp = bool(re.search(r"\bstp\b|standard (?:temperature and pressure|conditions)", low))
|
| 246 |
+
fs = formulas(r.t)
|
| 247 |
+
ask = None
|
| 248 |
+
for k, pat in (("density", r"what is the density|calculate the density|find the density|determine the density|density of .* (?:at|under)"),
|
| 249 |
+
("molar", r"molar mass|molecular weight|molecular mass"),
|
| 250 |
+
("pressure", r"what (?:is the )?pressure|calculate the pressure|find the pressure|what pressure"),
|
| 251 |
+
("volume", r"what (?:is the )?volume|calculate the volume|find the volume|what volume|occupy"),
|
| 252 |
+
("temperature", r"what (?:is the )?temperature|at what temperature|calculate the temperature|find the temperature"),
|
| 253 |
+
("moles", r"how many moles|number of moles|moles of gas"),
|
| 254 |
+
("mass", r"what (?:is the )?mass|how many grams|mass of gas")):
|
| 255 |
+
if re.search(pat, low):
|
| 256 |
+
ask = k
|
| 257 |
+
break
|
| 258 |
+
if ask is None:
|
| 259 |
+
return None
|
| 260 |
+
Tk = r.use(T[0]) if T else (273.15 if stp else None)
|
| 261 |
+
Pa = r.use(P[0]) if P else (1.0 if stp else None)
|
| 262 |
+
f = fs[0][0] if len({x[0] for x in fs}) == 1 else None
|
| 263 |
+
Mm = molar_mass(f) if f else None
|
| 264 |
+
if ask == "density":
|
| 265 |
+
if D or not (Tk and Pa and Mm):
|
| 266 |
+
return None
|
| 267 |
+
d = Pa * Mm / (R_LATM * Tk)
|
| 268 |
+
out = say("gas_density", "The density of %s is %s g/L (d = PM/RT = %s atm × %s g/mol / (0.082057 L·atm/(mol·K) × %s K))." % (f, sig(d), sig(Pa), sig(Mm), sig(Tk)), d)
|
| 269 |
+
elif ask == "molar":
|
| 270 |
+
if not (D and Tk and Pa):
|
| 271 |
+
return None
|
| 272 |
+
d = r.use(D[0])
|
| 273 |
+
mm = d * R_LATM * Tk / Pa
|
| 274 |
+
out = say("gas_molar_mass", "The molar mass is %s g/mol (M = dRT/P = %s g/L × 0.082057 × %s K / %s atm)." % (sig(mm), sig(d), sig(Tk), sig(Pa)), mm)
|
| 275 |
+
else:
|
| 276 |
+
n = r.use(N[0]) if N else None
|
| 277 |
+
if n is None and M and Mm:
|
| 278 |
+
n = r.use(M[0]) / Mm
|
| 279 |
+
Vl = r.use(V[0]) if V else None
|
| 280 |
+
known = {"pressure": Pa, "volume": Vl, "temperature": Tk, "moles": n}
|
| 281 |
+
if ask == "mass":
|
| 282 |
+
if not (Pa and Vl and Tk and Mm) or N or M:
|
| 283 |
+
return None
|
| 284 |
+
nn = Pa * Vl / (R_LATM * Tk)
|
| 285 |
+
out = say("gas_mass", "The mass is %s g (n = PV/RT = %s mol; × %s g/mol for %s)." % (sig(nn * Mm), sig(nn), sig(Mm), f), nn * Mm)
|
| 286 |
+
else:
|
| 287 |
+
others = {k: v for k, v in known.items() if k != ask}
|
| 288 |
+
if any(v is None for v in others.values()):
|
| 289 |
+
return None
|
| 290 |
+
if ask == "pressure":
|
| 291 |
+
v = n * R_LATM * Tk / Vl
|
| 292 |
+
out = say("gas_pressure", "The pressure is %s atm (P = nRT/V)." % sig(v), v)
|
| 293 |
+
elif ask == "volume":
|
| 294 |
+
v = n * R_LATM * Tk / Pa
|
| 295 |
+
out = say("gas_volume", "The volume is %s L (V = nRT/P)." % sig(v), v)
|
| 296 |
+
elif ask == "temperature":
|
| 297 |
+
v = Pa * Vl / (n * R_LATM)
|
| 298 |
+
out = say("gas_temperature", "The temperature is %s K (T = PV/nR)." % sig(v), v)
|
| 299 |
+
else:
|
| 300 |
+
v = Pa * Vl / (R_LATM * Tk)
|
| 301 |
+
out = say("gas_moles", "The amount is %s mol (n = PV/RT)." % sig(v), v)
|
| 302 |
+
return out if r.all_read() else None
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
STRONG_ACID = {"HCl": 1, "HBr": 1, "HI": 1, "HNO3": 1, "HClO4": 1}
|
| 306 |
+
STRONG_BASE = {"NaOH": 1, "KOH": 1, "LiOH": 1, "Ba(OH)2": 2, "Ca(OH)2": 2, "Sr(OH)2": 2}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
class PhAgent:
|
| 310 |
+
def answer(self, text, session_id="default"):
|
| 311 |
+
r = Reader(text)
|
| 312 |
+
low = r.low
|
| 313 |
+
if multi_part(low) or not re.search(r"\bph\b|\bpoh\b|\[h\+\]|\[h3o\+\]|hydrogen ion|hydronium", low):
|
| 314 |
+
return None
|
| 315 |
+
if re.search(r"titrat|added|adding|mix|mixed|after|equivalence|polyprotic|diprotic|ksp|solubility|hydroly[sz]|salt|"
|
| 316 |
+
r"percent|degree of|ionization constant of water|at \d+ deg|temperature", low):
|
| 317 |
+
return None
|
| 318 |
+
conc = r.q(r"m(?![a-z])|molar|mol/l|moles? per liter|n(?![a-z])", lambda v, u: v)
|
| 319 |
+
ka = [(m, m.start()) for m in re.finditer(r"\b(k_?a|k_?b|pk_?a|pk_?b)\s*(?:=|is|of|value of)?\s*(" + NUMRE.pattern + ")", r.t, re.I)]
|
| 320 |
+
ask = "pH" if re.search(r"what is the ph|calculate the ph|find the ph|determine the ph|ph of|the ph\b", low) else \
|
| 321 |
+
"pOH" if re.search(r"\bpoh\b", low) and not re.search(r"\bph\b(?! ?=)", low.replace("poh", "")) else \
|
| 322 |
+
"H" if re.search(r"\[h\+\]|\[h3o\+\]|hydrogen ion concentration|hydronium ion concentration", low) else None
|
| 323 |
+
if ask is None:
|
| 324 |
+
return None
|
| 325 |
+
hgiven = re.search(r"(?:hydrogen ion|hydronium(?: ion)?|\[h\+\]|\[h3o\+\])\s*(?:concentration\s*)?(?:is|of|=|equal to)\s*(" + NUMRE.pattern + r")\s*m\b", r.t, re.I)
|
| 326 |
+
if ask == "pH" and hgiven and not ka:
|
| 327 |
+
h = num(hgiven.group(1))
|
| 328 |
+
r.used.add(hgiven.start(1))
|
| 329 |
+
if not (0 < h <= 10):
|
| 330 |
+
return None
|
| 331 |
+
return say("ph_from_h", "The pH is %s (pH = -log10[H+] = -log10 %s)." % ("%.2f" % -math.log10(h), sig(h)), -math.log10(h)) if r.all_read() else None
|
| 332 |
+
phgiven = re.search(r"\bph (?:of|is|=)\s*(" + NUMRE.pattern + ")", r.t, re.I)
|
| 333 |
+
if ask == "H" and phgiven and not conc and not ka:
|
| 334 |
+
p = num(phgiven.group(1))
|
| 335 |
+
r.used.add(phgiven.start(1))
|
| 336 |
+
h = 10 ** -p
|
| 337 |
+
return say("h_from_ph", "[H+] is %s M (10^-pH with pH %s)." % (sig(h), sig(p)), h) if r.all_read() else None
|
| 338 |
+
fs = [x[0] for x in formulas(r.t)]
|
| 339 |
+
if len(conc) != 1:
|
| 340 |
+
return None
|
| 341 |
+
c = r.use(conc[0])
|
| 342 |
+
if not ka:
|
| 343 |
+
acid = [f for f in fs if f in STRONG_ACID]
|
| 344 |
+
base = [f for f in fs if f in STRONG_BASE]
|
| 345 |
+
if len(set(acid + base)) != 1 or c <= 1e-7:
|
| 346 |
+
return None
|
| 347 |
+
if acid:
|
| 348 |
+
h = c * STRONG_ACID[acid[0]]
|
| 349 |
+
ph = -math.log10(h)
|
| 350 |
+
what = "%s is a strong acid, fully ionized: [H+] = %s M" % (acid[0], sig(h))
|
| 351 |
+
else:
|
| 352 |
+
oh = c * STRONG_BASE[base[0]]
|
| 353 |
+
ph = 14 + math.log10(oh)
|
| 354 |
+
what = "%s is a strong base, fully dissociated: [OH-] = %s M" % (base[0], sig(oh))
|
| 355 |
+
else:
|
| 356 |
+
if len(ka) != 1:
|
| 357 |
+
return None
|
| 358 |
+
m, pos = ka[0]
|
| 359 |
+
r.used.add(m.start(2))
|
| 360 |
+
kind, kv = m.group(1).lower().replace("_", ""), num(m.group(2))
|
| 361 |
+
k = 10 ** -kv if kind.startswith("p") else kv
|
| 362 |
+
if not (0 < k < 1):
|
| 363 |
+
return None
|
| 364 |
+
x = (-k + math.sqrt(k * k + 4 * k * c)) / 2 # exact quadratic: x^2 / (c - x) = K
|
| 365 |
+
if kind.endswith("b"):
|
| 366 |
+
ph = 14 + math.log10(x)
|
| 367 |
+
what = "weak base, Kb = %s: [OH-] = %s M from x^2/(C - x) = Kb" % (sig(k), sig(x))
|
| 368 |
+
else:
|
| 369 |
+
ph = -math.log10(x)
|
| 370 |
+
what = "weak acid, Ka = %s: [H+] = %s M from x^2/(C - x) = Ka" % (sig(k), sig(x))
|
| 371 |
+
h = 10 ** -ph
|
| 372 |
+
if not r.all_read():
|
| 373 |
+
return None
|
| 374 |
+
if ask == "pH":
|
| 375 |
+
return say("ph", "The pH is %s (%s)." % ("%.2f" % ph, what), ph)
|
| 376 |
+
if ask == "pOH":
|
| 377 |
+
return say("poh", "The pOH is %s (%s)." % ("%.2f" % (14 - ph), what), 14 - ph)
|
| 378 |
+
return say("h", "[H+] is %s M (%s)." % (sig(10 ** -ph), what), 10 ** -ph)
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
class PhotonAgent:
|
| 382 |
+
LEN = {"nm": 1e-9, "nanometers": 1e-9, "nanometer": 1e-9, "a": 1e-10, "angstroms": 1e-10, "angstrom": 1e-10, "å": 1e-10,
|
| 383 |
+
"m": 1, "meters": 1, "cm": 1e-2, "um": 1e-6, "μm": 1e-6, "micrometers": 1e-6, "microns": 1e-6, "pm": 1e-12, "mm": 1e-3}
|
| 384 |
+
|
| 385 |
+
def answer(self, text, session_id="default"):
|
| 386 |
+
r = Reader(text)
|
| 387 |
+
low = r.low
|
| 388 |
+
if multi_part(low) or not re.search(r"photon|wavelength|frequency|light|radiation|de ?broglie|transition|emitted|absorbed|quantum", low):
|
| 389 |
+
return None
|
| 390 |
+
if re.search(r"work function|photoelectric|threshold|stopping|intensity|power|watt|lamp|laser|per second|uncertainty|"
|
| 391 |
+
r"box|particle in|harmonic|rotational|vibrational|bond|spin|nmr|esr|epr|zeeman|doppler", low):
|
| 392 |
+
return None
|
| 393 |
+
if re.search(r"black ?body", low) and re.search(r"(?:maximum|peak|most intense).{0,30}wavelength|wavelength.{0,30}(?:maximum|peak)", low):
|
| 394 |
+
T = r.q(T_UNITS, t_k)
|
| 395 |
+
if len(T) != 1:
|
| 396 |
+
return None
|
| 397 |
+
lm = 2.897771955e-3 / r.use(T[0])
|
| 398 |
+
return say("wien", "The peak wavelength is %s m (Wien: λmax = b/T, b = 2.8978e-3 m·K)." % sig(lm), lm) if r.all_read() else None
|
| 399 |
+
lam = r.q(r"nm|nanometers?|angstroms?|å|pm|um|μm|micrometers?|microns?|cm(?![-^\d])|mm(?![a-z])|m(?![a-z/^\-])", lambda v, u: v * self.LEN[u])
|
| 400 |
+
nu = r.q(r"hz|s\^?-1|sec\^?-1|/s", lambda v, u: v)
|
| 401 |
+
# de Broglie
|
| 402 |
+
if re.search(r"de ?broglie|wavelength of (?:an?|the) (?:electron|proton|neutron|particle|ball)", low):
|
| 403 |
+
vel = r.q(r"m/s|m s\^?-1|meters? per second", lambda v, u: v)
|
| 404 |
+
mass = r.q(M_UNITS, lambda v, u: m_g(v, u) / 1000)
|
| 405 |
+
ke = r.q(r"ev|electron ?volts?", lambda v, u: v * EV)
|
| 406 |
+
m_ = ME if "electron" in low and not mass else (r.use(mass[0]) if len(mass) == 1 else None)
|
| 407 |
+
if m_ is None or len(vel) + len(ke) != 1:
|
| 408 |
+
return None
|
| 409 |
+
p = m_ * r.use(vel[0]) if vel else math.sqrt(2 * m_ * r.use(ke[0]))
|
| 410 |
+
lam_ = H / p
|
| 411 |
+
return say("de_broglie", "The de Broglie wavelength is %s m (λ = h/p, p = %s kg·m/s)." % (sig(lam_), sig(p)), lam_) if r.all_read() else None
|
| 412 |
+
# hydrogen-like transitions
|
| 413 |
+
ns = re.findall(r"n\s*=\s*(\d+)", low)
|
| 414 |
+
if len(ns) == 2 and re.search(r"hydrogen|he\+|li2\+|transition|electron (?:falls|drops|jumps|moves)", low):
|
| 415 |
+
z = 2 if re.search(r"\bhe\+|helium ion|ionized helium", low) else 3 if re.search(r"li2\+|li\+\+", low) else 1
|
| 416 |
+
if z == 1 and not re.search(r"hydrogen", low):
|
| 417 |
+
return None
|
| 418 |
+
for m in re.finditer(r"n\s*=\s*(\d+)", r.t, re.I):
|
| 419 |
+
r.used.add(m.start(1))
|
| 420 |
+
n1, n2 = sorted(int(x) for x in ns)
|
| 421 |
+
if n1 == n2:
|
| 422 |
+
return None
|
| 423 |
+
wn = RH * z * z * (1 / n1 ** 2 - 1 / n2 ** 2) # m^-1
|
| 424 |
+
E = H * C * wn
|
| 425 |
+
if re.search(r"wavelength", low):
|
| 426 |
+
v, txt = 1 / wn, "The wavelength is %s m (%s nm), from 1/λ = R_H Z² (1/%d² − 1/%d²)." % (sig(1 / wn), sig(1e9 / wn), n1, n2)
|
| 427 |
+
elif re.search(r"frequency", low):
|
| 428 |
+
v, txt = wn * C, "The frequency is %s Hz (ν = c R_H Z² (1/%d² − 1/%d²))." % (sig(wn * C), n1, n2)
|
| 429 |
+
elif re.search(r"energy", low):
|
| 430 |
+
v, txt = E, "The energy is %s J per photon (%s eV), ΔE = hc R_H Z² (1/%d² − 1/%d²)." % (sig(E), sig(E / EV), n1, n2)
|
| 431 |
+
elif re.search(r"wave ?number", low):
|
| 432 |
+
v, txt = wn / 100, "The wavenumber is %s cm^-1." % sig(wn / 100)
|
| 433 |
+
else:
|
| 434 |
+
return None
|
| 435 |
+
return say("transition", txt, v) if r.all_read() else None
|
| 436 |
+
# photon: energy <-> wavelength <-> frequency
|
| 437 |
+
if len(lam) + len(nu) != 1:
|
| 438 |
+
return None
|
| 439 |
+
if lam:
|
| 440 |
+
L = r.use(lam[0])
|
| 441 |
+
f = C / L
|
| 442 |
+
else:
|
| 443 |
+
f = r.use(nu[0])
|
| 444 |
+
L = C / f
|
| 445 |
+
E = H * f
|
| 446 |
+
if re.search(r"(?:energy|kj|kilojoules?).*(?:per mole|/mol|a mole|one mole|mole of photons)|per mole of photons|einstein", low):
|
| 447 |
+
v, txt = E * NA / 1000, "The energy is %s kJ/mol (E = N_A hc/λ)." % sig(E * NA / 1000)
|
| 448 |
+
elif re.search(r"\bev\b|electron ?volts?", low) and re.search(r"energy", low):
|
| 449 |
+
v, txt = E / EV, "The energy is %s eV (E = hc/λ)." % sig(E / EV)
|
| 450 |
+
elif re.search(r"energy", low):
|
| 451 |
+
v, txt = E, "The energy is %s J per photon (E = hc/λ = %s J·s × %s m/s / %s m)." % (sig(E), H, C, sig(L))
|
| 452 |
+
elif re.search(r"frequency", low) and lam:
|
| 453 |
+
v, txt = f, "The frequency is %s Hz (ν = c/λ)." % sig(f)
|
| 454 |
+
elif re.search(r"wavelength", low) and nu:
|
| 455 |
+
v, txt = L, "The wavelength is %s m (λ = c/ν)." % sig(L)
|
| 456 |
+
else:
|
| 457 |
+
return None
|
| 458 |
+
return say("photon", txt, v) if r.all_read() else None
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
KF = {"water": 1.86, "benzene": 5.12, "camphor": 37.7, "cyclohexane": 20.0}
|
| 462 |
+
KB = {"water": 0.512, "benzene": 2.53}
|
| 463 |
+
NONELECTROLYTE = ("glucose", "sucrose", "urea", "sugar", "ethylene glycol", "glycerol", "C6H12O6", "C12H22O11", "CH4N2O", "nonelectrolyte", "non-electrolyte")
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
class ColligativeAgent:
|
| 467 |
+
def answer(self, text, session_id="default"):
|
| 468 |
+
r = Reader(text)
|
| 469 |
+
low = r.low
|
| 470 |
+
if multi_part(low) or not re.search(r"freezing|boiling|osmotic|molality|molal", low):
|
| 471 |
+
return None
|
| 472 |
+
if re.search(r"vapor pressure|raoult|percent|dissociat|association|degree|van.t hoff factor of|dimer|ionized|electrolyte(?! )|"
|
| 473 |
+
r"activity|ideal solution", low):
|
| 474 |
+
return None
|
| 475 |
+
solv = [s for s in KF if s in low]
|
| 476 |
+
nonel = any(s.lower() in low for s in NONELECTROLYTE)
|
| 477 |
+
if not nonel or len(set(solv)) > 1:
|
| 478 |
+
return None
|
| 479 |
+
Kf_given = re.search(r"\bk_?f\b[^\d]{0,20}(" + NUMRE.pattern + ")", r.t, re.I)
|
| 480 |
+
Kb_given = re.search(r"\bk_?b\b[^\d]{0,20}(" + NUMRE.pattern + ")", r.t, re.I)
|
| 481 |
+
masses = r.q(M_UNITS, m_g)
|
| 482 |
+
molal = r.q(r"m(?![a-z])|molal", lambda v, u: v)
|
| 483 |
+
s = solv[0] if solv else "water"
|
| 484 |
+
if "osmotic" in low:
|
| 485 |
+
conc = r.q(r"m(?![a-z])|molar|mol/l", lambda v, u: v)
|
| 486 |
+
T = r.q(T_UNITS, t_k)
|
| 487 |
+
if len(conc) != 1 or len(T) != 1:
|
| 488 |
+
return None
|
| 489 |
+
pi = r.use(conc[0]) * R_LATM * r.use(T[0])
|
| 490 |
+
return say("osmotic", "The osmotic pressure is %s atm (π = MRT, nonelectrolyte)." % sig(pi), pi) if r.all_read() else None
|
| 491 |
+
freezing = "freezing" in low
|
| 492 |
+
K = None
|
| 493 |
+
if freezing:
|
| 494 |
+
if Kf_given:
|
| 495 |
+
K = num(Kf_given.group(1))
|
| 496 |
+
r.used.add(Kf_given.start(1))
|
| 497 |
+
else:
|
| 498 |
+
K = KF.get(s)
|
| 499 |
+
else:
|
| 500 |
+
if Kb_given:
|
| 501 |
+
K = num(Kb_given.group(1))
|
| 502 |
+
r.used.add(Kb_given.start(1))
|
| 503 |
+
else:
|
| 504 |
+
K = KB.get(s)
|
| 505 |
+
if K is None:
|
| 506 |
+
return None
|
| 507 |
+
if len(molal) == 1 and not masses:
|
| 508 |
+
m = r.use(molal[0])
|
| 509 |
+
elif len(masses) == 2:
|
| 510 |
+
fs = [x[0] for x in formulas(r.t) if x[0] in ("C6H12O6", "C12H22O11", "CH4N2O") or x[0] not in ("H2O",)]
|
| 511 |
+
solute = [f for f in fs if f != {"water": "H2O"}.get(s)]
|
| 512 |
+
mm = molar_mass(solute[0]) if len(set(solute)) == 1 else None
|
| 513 |
+
if mm is None:
|
| 514 |
+
return None
|
| 515 |
+
a, b = sorted(masses, key=lambda x: x[0])
|
| 516 |
+
m = (r.use(a) / mm) / (r.use(b) / 1000)
|
| 517 |
+
else:
|
| 518 |
+
return None
|
| 519 |
+
dT = K * m
|
| 520 |
+
if freezing:
|
| 521 |
+
base = {"water": 0.0, "benzene": 5.5, "cyclohexane": 6.5, "camphor": 179.8}[s]
|
| 522 |
+
if re.search(r"freezing point (?:of the solution|will be|is)|new freezing|at what temperature.*freez|freezes at", low):
|
| 523 |
+
return say("freezing_point", "The freezing point is %s deg C (ΔTf = Kf·m = %s × %s = %s deg C below %s deg C)." % (sig(base - dT), K, sig(m), sig(dT), base), base - dT) if r.all_read() else None
|
| 524 |
+
if re.search(r"depression|lowered|by how much|change in", low):
|
| 525 |
+
return say("freezing_depression", "The freezing point is lowered by %s deg C (ΔTf = Kf·m = %s × %s)." % (sig(dT), K, sig(m)), dT) if r.all_read() else None
|
| 526 |
+
else:
|
| 527 |
+
base = {"water": 100.0, "benzene": 80.1}[s]
|
| 528 |
+
if re.search(r"boiling point (?:of the solution|will be|is)|new boiling|boils at", low):
|
| 529 |
+
return say("boiling_point", "The boiling point is %s deg C (ΔTb = Kb·m = %s × %s = %s deg C above %s deg C)." % (sig(base + dT), K, sig(m), sig(dT), base), base + dT) if r.all_read() else None
|
| 530 |
+
if re.search(r"elevation|raised|by how much|change in", low):
|
| 531 |
+
return say("boiling_elevation", "The boiling point rises by %s deg C (ΔTb = Kb·m)." % sig(dT), dT) if r.all_read() else None
|
| 532 |
+
return None
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
class KineticsAgent:
|
| 536 |
+
T = {"s": 1, "sec": 1, "seconds": 1, "second": 1, "min": 60, "minutes": 60, "minute": 60, "h": 3600, "hr": 3600, "hours": 3600,
|
| 537 |
+
"hour": 3600, "days": 86400, "day": 86400, "d": 86400, "years": 3.15576e7, "year": 3.15576e7, "yr": 3.15576e7, "y": 3.15576e7}
|
| 538 |
+
|
| 539 |
+
def answer(self, text, session_id="default"):
|
| 540 |
+
r = Reader(text)
|
| 541 |
+
low = r.low
|
| 542 |
+
if multi_part(low) or not re.search(r"half-?life|first-?order|decay", low):
|
| 543 |
+
return None
|
| 544 |
+
if re.search(r"second-?order|zero-?order|activation|arrhenius|temperature|catalyst|equilibrium|counts|dpm|"
|
| 545 |
+
r"age of|dating|carbon-14|c-14|14c", low):
|
| 546 |
+
return None
|
| 547 |
+
tq = r.q(r"seconds?|sec|s(?![a-z])|minutes?|min(?![a-z])|hours?|hrs?|h(?![a-z])|days?|d(?![a-z])|years?|yrs?|y(?![a-z])",
|
| 548 |
+
lambda v, u: v * self.T[u])
|
| 549 |
+
pct = r.q(r"%|percent", lambda v, u: v / 100)
|
| 550 |
+
frac = re.search(r"\b(one|a) (half|third|fourth|quarter|tenth|eighth|sixteenth)\b", low)
|
| 551 |
+
half = re.search(r"half-?life (?:of|is|=)", low)
|
| 552 |
+
amt = r.q(r"mci|μci|uci|ci(?![a-z])|bq|mg|kg|g(?![a-z])|grams?", lambda v, u: v)
|
| 553 |
+
unit = re.search(r"(" + NUMRE.pattern + r")\s*(mCi|μCi|uCi|Ci|Bq|mg|kg|g|grams?)(?![a-z])", r.t)
|
| 554 |
+
if len(tq) == 2 and half and len(amt) == 1 and re.search(r"how much.*(?:remain|left)|(?:remain|left) after", low):
|
| 555 |
+
a, b = tq
|
| 556 |
+
th, t = (a, b) if a[1] < b[1] else (b, a)
|
| 557 |
+
th_v, t_v = r.use(th), r.use(t)
|
| 558 |
+
left = r.use(amt[0]) * 0.5 ** (t_v / th_v)
|
| 559 |
+
return say("amount_left", "The amount remaining is %s %s (× (1/2)^(t/t½) = (1/2)^%s)." % (sig(left), unit.group(unit.lastindex), sig(t_v / th_v)), left) if r.all_read() else None
|
| 560 |
+
if len(tq) == 2 and half and re.search(r"(?:what|how much|what fraction|what percent).*(?:remain|left)", low):
|
| 561 |
+
a, b = tq
|
| 562 |
+
th, t = (a, b) if a[1] < b[1] else (b, a)
|
| 563 |
+
th_v, t_v = r.use(th), r.use(t)
|
| 564 |
+
f = 0.5 ** (t_v / th_v)
|
| 565 |
+
return say("fraction_left", "The fraction remaining is %s (%s%%), (1/2)^(t/t½)." % (sig(f), sig(100 * f)), f) if r.all_read() else None
|
| 566 |
+
if len(tq) == 1 and half and (pct or frac) and re.search(r"how long|how much time|what time", low):
|
| 567 |
+
th_v = r.use(tq[0])
|
| 568 |
+
if pct:
|
| 569 |
+
p = r.use(pct[0])
|
| 570 |
+
f = p if re.search(r"remain|left", low) else 1 - p
|
| 571 |
+
else:
|
| 572 |
+
f = {"half": .5, "third": 1 / 3, "fourth": .25, "quarter": .25, "tenth": .1, "eighth": .125, "sixteenth": 1 / 16}[frac.group(2)]
|
| 573 |
+
if not re.search(r"remain|left", low):
|
| 574 |
+
return None
|
| 575 |
+
if not (0 < f < 1):
|
| 576 |
+
return None
|
| 577 |
+
t = th_v * math.log(f) / math.log(0.5)
|
| 578 |
+
return say("time_to_fraction", "It takes %s s (t = t½ · log(f)/log(1/2), f = %s)." % (sig(t), sig(f)), t) if r.all_read() else None
|
| 579 |
+
if len(tq) == 1 and half and re.search(r"rate constant|\bk\b", low):
|
| 580 |
+
k = math.log(2) / r.use(tq[0])
|
| 581 |
+
return say("rate_constant", "The rate constant is %s s^-1 (k = ln 2 / t½)." % sig(k), k) if r.all_read() else None
|
| 582 |
+
return None
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
AGENTS = {"chem-gas": GasAgent, "chem-ph": PhAgent, "chem-photon": PhotonAgent, "chem-colligative": ColligativeAgent,
|
| 586 |
+
"chem-kinetics": KineticsAgent}
|
root/wander-train/code_organ_v2.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""code_organ_v2 - v1 (MBPP-frozen b2e52a1d) + STRICT mode for a single example: every move named by the
|
| 2 |
+
request. Programs as specified free moves over a verified function library, checked by execution; every
|
| 3 |
+
verified program joins the library (the stored final forms - continuous learning without retraining).
|
| 4 |
+
|
| 5 |
+
REQUEST description + input/output examples (asserts) -> function name, argument values and types, expected outputs,
|
| 6 |
+
constants the description names ("at least 4 characters", "the character 'l'")
|
| 7 |
+
MOVES typed library functions. The TYPE SYSTEM is the specified structure: a function applies only where its
|
| 8 |
+
argument types connect; within that the choice is free. The description's words rank the moves.
|
| 9 |
+
SEARCH bottom-up enumeration with OBSERVATIONAL EQUIVALENCE: programs that behave identically on the examples are
|
| 10 |
+
kept once, so compositions of 3-4 moves stay tractable.
|
| 11 |
+
CHECK the rendered source is executed against every example; nothing is released that did not run and pass.
|
| 12 |
+
STORE a verified program becomes a library function (its description words are its keywords, its signature its
|
| 13 |
+
type); later requests compose it - measured as the library-growth curve.
|
| 14 |
+
Only library templates are ever evaluated; no text from the request is executed."""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import ast
|
| 18 |
+
import itertools
|
| 19 |
+
import json
|
| 20 |
+
import math
|
| 21 |
+
import re
|
| 22 |
+
from collections import Counter
|
| 23 |
+
|
| 24 |
+
# ----------------------------------------------------------------------------------------------------------- types
|
| 25 |
+
def tyof(v):
|
| 26 |
+
if isinstance(v, bool):
|
| 27 |
+
return "bool"
|
| 28 |
+
if isinstance(v, int):
|
| 29 |
+
return "int"
|
| 30 |
+
if isinstance(v, float):
|
| 31 |
+
return "float"
|
| 32 |
+
if isinstance(v, str):
|
| 33 |
+
return "str"
|
| 34 |
+
if isinstance(v, list):
|
| 35 |
+
return "list"
|
| 36 |
+
if isinstance(v, tuple):
|
| 37 |
+
return "tuple"
|
| 38 |
+
if isinstance(v, dict):
|
| 39 |
+
return "dict"
|
| 40 |
+
if isinstance(v, set):
|
| 41 |
+
return "set"
|
| 42 |
+
return "any"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
GROUP = {"num": {"int", "float"}, "seq": {"list", "tuple", "str"}, "coll": {"list", "tuple", "set"},
|
| 46 |
+
"any": {"bool", "int", "float", "str", "list", "tuple", "dict", "set", "any"}}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def fits(t, want):
|
| 50 |
+
return t == want or t in GROUP.get(want, ())
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ------------------------------------------------------------------------------------------------------ primitives
|
| 54 |
+
class Prim:
|
| 55 |
+
__slots__ = ("name", "ins", "out", "fn", "src", "kw")
|
| 56 |
+
|
| 57 |
+
def __init__(self, name, ins, out, fn, src, kw):
|
| 58 |
+
self.name, self.ins, self.out, self.fn, self.src = name, ins, out, fn, src
|
| 59 |
+
self.kw = {w.lower().rstrip("s") for w in kw.split()} # v2: normalised exactly like the request words
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _big(x):
|
| 63 |
+
if isinstance(x, int) and abs(x) > 10 ** 7:
|
| 64 |
+
raise ValueError("too large")
|
| 65 |
+
return x
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _is_prime(n):
|
| 69 |
+
if n < 2:
|
| 70 |
+
return False
|
| 71 |
+
return all(n % d for d in range(2, int(n ** 0.5) + 1))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _uniq(xs):
|
| 75 |
+
out = []
|
| 76 |
+
for x in xs:
|
| 77 |
+
if x not in out:
|
| 78 |
+
out.append(x)
|
| 79 |
+
return out
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _flatten(xs):
|
| 83 |
+
out = []
|
| 84 |
+
for x in xs:
|
| 85 |
+
out += list(x) if isinstance(x, (list, tuple)) else [x]
|
| 86 |
+
return out
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
P_ = [
|
| 90 |
+
# numbers
|
| 91 |
+
Prim("add", ("num", "num"), "num", lambda a, b: a + b, "({0} + {1})", "add sum plus total"),
|
| 92 |
+
Prim("sub", ("num", "num"), "num", lambda a, b: a - b, "({0} - {1})", "subtract difference minus"),
|
| 93 |
+
Prim("mul", ("num", "num"), "num", lambda a, b: _big(a * b), "({0} * {1})", "multiply product times"),
|
| 94 |
+
Prim("fdiv", ("num", "num"), "num", lambda a, b: a // b, "({0} // {1})", "divide quotient integer"),
|
| 95 |
+
Prim("div", ("num", "num"), "float", lambda a, b: a / b, "({0} / {1})", "divide ratio average"),
|
| 96 |
+
Prim("mod", ("num", "num"), "num", lambda a, b: a % b, "({0} % {1})", "remainder modulo divisible"),
|
| 97 |
+
Prim("pow", ("num", "num"), "num", lambda a, b: _big(a ** b) if b < 64 else 1 / 0, "({0} ** {1})", "power square cube exponent"),
|
| 98 |
+
Prim("sq", ("num",), "num", lambda a: a * a, "({0} ** 2)", "square squares"),
|
| 99 |
+
Prim("cube", ("num",), "num", lambda a: a ** 3, "({0} ** 3)", "cube cubes"),
|
| 100 |
+
Prim("abs", ("num",), "num", abs, "abs({0})", "absolute"),
|
| 101 |
+
Prim("neg", ("num",), "num", lambda a: -a, "(-{0})", "negative negate"),
|
| 102 |
+
Prim("half", ("num",), "float", lambda a: a / 2, "({0} / 2)", "half area triangle"),
|
| 103 |
+
Prim("sqrt", ("num",), "float", math.sqrt, "math.sqrt({0})", "root square"),
|
| 104 |
+
Prim("isqrt", ("int",), "int", math.isqrt, "math.isqrt({0})", "root square integer"),
|
| 105 |
+
Prim("fact", ("int",), "int", lambda n: math.factorial(n) if 0 <= n <= 30 else 1 / 0, "math.factorial({0})", "factorial"),
|
| 106 |
+
Prim("gcd", ("int", "int"), "int", math.gcd, "math.gcd({0}, {1})", "gcd greatest common divisor"),
|
| 107 |
+
Prim("lcm", ("int", "int"), "int", lambda a, b: a * b // math.gcd(a, b) if a and b else 0, "({0} * {1} // math.gcd({0}, {1}))", "lcm least common multiple"),
|
| 108 |
+
Prim("isprime", ("int",), "bool", _is_prime, "(({0}) > 1 and all(({0}) % d for d in range(2, int(({0}) ** 0.5) + 1)))", "prime"),
|
| 109 |
+
Prim("even", ("int",), "bool", lambda n: n % 2 == 0, "({0} % 2 == 0)", "even"),
|
| 110 |
+
Prim("odd", ("int",), "bool", lambda n: n % 2 == 1, "({0} % 2 == 1)", "odd"),
|
| 111 |
+
Prim("digits", ("int",), "list", lambda n: [int(c) for c in str(abs(n))], "[int(c) for c in str(abs({0}))]", "digits digit"),
|
| 112 |
+
Prim("revnum", ("int",), "int", lambda n: int(str(abs(n))[::-1]), "int(str(abs({0}))[::-1])", "reverse number"),
|
| 113 |
+
Prim("bin", ("int",), "str", lambda n: bin(n)[2:], "bin({0})[2:]", "binary"),
|
| 114 |
+
Prim("frombin", ("str",), "int", lambda s: int(s, 2), "int({0}, 2)", "binary decimal convert"),
|
| 115 |
+
Prim("popcount", ("int",), "int", lambda n: bin(n).count("1"), "bin({0}).count('1')", "set bits count ones"),
|
| 116 |
+
Prim("xor", ("int", "int"), "int", lambda a, b: a ^ b, "({0} ^ {1})", "xor bit differ"),
|
| 117 |
+
Prim("ispow2", ("int",), "bool", lambda n: n > 0 and n & (n - 1) == 0, "({0} > 0 and {0} & ({0} - 1) == 0)", "power two bit"),
|
| 118 |
+
Prim("eq", ("any", "any"), "bool", lambda a, b: a == b, "({0} == {1})", "equal same"),
|
| 119 |
+
Prim("lt", ("num", "num"), "bool", lambda a, b: a < b, "({0} < {1})", "less smaller"),
|
| 120 |
+
Prim("gt", ("num", "num"), "bool", lambda a, b: a > b, "({0} > {1})", "greater larger"),
|
| 121 |
+
Prim("not", ("bool",), "bool", lambda a: not a, "(not {0})", "not"),
|
| 122 |
+
Prim("and", ("bool", "bool"), "bool", lambda a, b: a and b, "({0} and {1})", "and both"),
|
| 123 |
+
Prim("or", ("bool", "bool"), "bool", lambda a, b: a or b, "({0} or {1})", "or either"),
|
| 124 |
+
Prim("maxn", ("num", "num"), "num", max, "max({0}, {1})", "maximum largest bigger"),
|
| 125 |
+
Prim("minn", ("num", "num"), "num", min, "min({0}, {1})", "minimum smallest"),
|
| 126 |
+
Prim("round2", ("num",), "float", lambda a: round(a, 2), "round({0}, 2)", "round"),
|
| 127 |
+
Prim("int", ("any",), "int", int, "int({0})", "integer convert"),
|
| 128 |
+
Prim("str", ("any",), "str", str, "str({0})", "string convert"),
|
| 129 |
+
Prim("float", ("num",), "float", float, "float({0})", "float"),
|
| 130 |
+
Prim("tri", ("int",), "int", lambda n: n * (n + 1) // 2, "({0} * ({0} + 1) // 2)", "triangular sum natural"),
|
| 131 |
+
Prim("pi_r2", ("num",), "float", lambda r: math.pi * r * r, "(math.pi * {0} * {0})", "area circle"),
|
| 132 |
+
Prim("circ", ("num",), "float", lambda r: 2 * math.pi * r, "(2 * math.pi * {0})", "circumference perimeter circle"),
|
| 133 |
+
# sequences
|
| 134 |
+
Prim("len", ("seq",), "int", len, "len({0})", "length count number size"),
|
| 135 |
+
Prim("sum", ("coll",), "num", lambda xs: sum(xs), "sum({0})", "sum total add"),
|
| 136 |
+
Prim("max", ("coll",), "any", max, "max({0})", "maximum largest biggest max longest"),
|
| 137 |
+
Prim("min", ("coll",), "any", min, "min({0})", "minimum smallest min"),
|
| 138 |
+
Prim("sorted", ("coll",), "list", sorted, "sorted({0})", "sort sorted ascending order"),
|
| 139 |
+
Prim("sorted_desc", ("coll",), "list", lambda xs: sorted(xs, reverse=True), "sorted({0}, reverse=True)", "sort descending largest"),
|
| 140 |
+
Prim("rev", ("seq",), "seq", lambda xs: xs[::-1], "{0}[::-1]", "reverse reversed"),
|
| 141 |
+
Prim("first", ("seq",), "any", lambda xs: xs[0], "{0}[0]", "first"),
|
| 142 |
+
Prim("last", ("seq",), "any", lambda xs: xs[-1], "{0}[-1]", "last"),
|
| 143 |
+
Prim("take", ("seq", "int"), "seq", lambda xs, n: xs[:n], "{0}[:{1}]", "first n smallest largest take"),
|
| 144 |
+
Prim("drop", ("seq", "int"), "seq", lambda xs, n: xs[n:], "{0}[{1}:]", "remove first skip"),
|
| 145 |
+
Prim("at", ("seq", "int"), "any", lambda xs, i: xs[i], "{0}[{1}]", "nth index element second third"),
|
| 146 |
+
Prim("tolist", ("any",), "list", list, "list({0})", "list convert"),
|
| 147 |
+
Prim("totuple", ("any",), "tuple", tuple, "tuple({0})", "tuple convert"),
|
| 148 |
+
Prim("toset", ("coll",), "set", set, "set({0})", "unique distinct set"),
|
| 149 |
+
Prim("uniq", ("coll",), "list", _uniq, "list(dict.fromkeys({0}))", "unique distinct duplicates remove"),
|
| 150 |
+
Prim("inter", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) & set(b))), "tuple(sorted(set({0}) & set({1})))", "common shared similar intersection"),
|
| 151 |
+
Prim("union", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) | set(b))), "tuple(sorted(set({0}) | set({1})))", "union combine"),
|
| 152 |
+
Prim("diff", ("coll", "coll"), "list", lambda a, b: [x for x in a if x not in b], "[x for x in {0} if x not in {1}]", "difference not in"),
|
| 153 |
+
Prim("concat", ("seq", "seq"), "seq", lambda a, b: a + b, "({0} + {1})", "concatenate join append combine merge"),
|
| 154 |
+
Prim("count", ("seq", "any"), "int", lambda xs, x: xs.count(x), "{0}.count({1})", "count occurrences frequency"),
|
| 155 |
+
Prim("index", ("seq", "any"), "int", lambda xs, x: xs.index(x), "{0}.index({1})", "index position find"),
|
| 156 |
+
Prim("contains", ("any", "seq"), "bool", lambda x, xs: x in xs, "({0} in {1})", "contains present exists check"),
|
| 157 |
+
Prim("allsame", ("coll",), "bool", lambda xs: len(set(xs)) <= 1, "(len(set({0})) <= 1)", "same equal all identical"),
|
| 158 |
+
Prim("alluniq", ("coll",), "bool", lambda xs: len(set(xs)) == len(xs), "(len(set({0})) == len(list({0})))", "unique distinct duplicate"),
|
| 159 |
+
Prim("issorted", ("coll",), "bool", lambda xs: list(xs) == sorted(xs), "(list({0}) == sorted({0}))", "sorted order check"),
|
| 160 |
+
Prim("prod", ("coll",), "num", lambda xs: _big(math.prod(xs)), "math.prod({0})", "product multiply"),
|
| 161 |
+
Prim("mean", ("coll",), "float", lambda xs: sum(xs) / len(xs), "(sum({0}) / len({0}))", "average mean"),
|
| 162 |
+
Prim("map_sq", ("coll",), "list", lambda xs: [x * x for x in xs], "[x * x for x in {0}]", "square squares each"),
|
| 163 |
+
Prim("map_cube", ("coll",), "list", lambda xs: [x ** 3 for x in xs], "[x ** 3 for x in {0}]", "cube cubes each"),
|
| 164 |
+
Prim("map_neg", ("coll",), "list", lambda xs: [-x for x in xs], "[-x for x in {0}]", "negative"),
|
| 165 |
+
Prim("map_abs", ("coll",), "list", lambda xs: [abs(x) for x in xs], "[abs(x) for x in {0}]", "absolute"),
|
| 166 |
+
Prim("map_str", ("coll",), "list", lambda xs: [str(x) for x in xs], "[str(x) for x in {0}]", "string convert"),
|
| 167 |
+
Prim("map_int", ("coll",), "list", lambda xs: [int(x) for x in xs], "[int(x) for x in {0}]", "integer convert"),
|
| 168 |
+
Prim("map_len", ("coll",), "list", lambda xs: [len(x) for x in xs], "[len(x) for x in {0}]", "length each"),
|
| 169 |
+
Prim("map_first", ("coll",), "list", lambda xs: [x[0] for x in xs], "[x[0] for x in {0}]", "first each"),
|
| 170 |
+
Prim("map_last", ("coll",), "list", lambda xs: [x[-1] for x in xs], "[x[-1] for x in {0}]", "last each"),
|
| 171 |
+
Prim("map_sum", ("coll",), "list", lambda xs: [sum(x) for x in xs], "[sum(x) for x in {0}]", "sum each"),
|
| 172 |
+
Prim("map_add", ("coll", "num"), "list", lambda xs, k: [x + k for x in xs], "[x + {1} for x in {0}]", "add each"),
|
| 173 |
+
Prim("map_mul", ("coll", "num"), "list", lambda xs, k: [x * k for x in xs], "[x * {1} for x in {0}]", "multiply each double doubled twice"),
|
| 174 |
+
Prim("zipadd", ("coll", "coll"), "list", lambda a, b: [x + y for x, y in zip(a, b)], "[x + y for x, y in zip({0}, {1})]", "add two lists elementwise"),
|
| 175 |
+
Prim("zipmul", ("coll", "coll"), "list", lambda a, b: [x * y for x, y in zip(a, b)], "[x * y for x, y in zip({0}, {1})]", "multiply two lists"),
|
| 176 |
+
Prim("zipsub", ("coll", "coll"), "list", lambda a, b: [x - y for x, y in zip(a, b)], "[x - y for x, y in zip({0}, {1})]", "subtract two lists"),
|
| 177 |
+
Prim("zipdiv", ("coll", "coll"), "list", lambda a, b: [x / y for x, y in zip(a, b)], "[x / y for x, y in zip({0}, {1})]", "divide two lists"),
|
| 178 |
+
Prim("zip", ("coll", "coll"), "list", lambda a, b: list(zip(a, b)), "list(zip({0}, {1}))", "pair zip"),
|
| 179 |
+
Prim("f_even", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 0], "[x for x in {0} if x % 2 == 0]", "even"),
|
| 180 |
+
Prim("f_odd", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 1], "[x for x in {0} if x % 2 == 1]", "odd"),
|
| 181 |
+
Prim("f_pos", ("coll",), "list", lambda xs: [x for x in xs if x > 0], "[x for x in {0} if x > 0]", "positive"),
|
| 182 |
+
Prim("f_neg", ("coll",), "list", lambda xs: [x for x in xs if x < 0], "[x for x in {0} if x < 0]", "negative"),
|
| 183 |
+
Prim("f_gt", ("coll", "num"), "list", lambda xs, k: [x for x in xs if x > k], "[x for x in {0} if x > {1}]", "greater than"),
|
| 184 |
+
Prim("f_lenge", ("coll", "int"), "list", lambda xs, k: [x for x in xs if len(x) >= k], "[x for x in {0} if len(x) >= {1}]", "at least long characters"),
|
| 185 |
+
Prim("f_neq", ("coll", "any"), "list", lambda xs, k: [x for x in xs if x != k], "[x for x in {0} if x != {1}]", "remove all"),
|
| 186 |
+
Prim("cumsum", ("coll",), "list", lambda xs: list(itertools.accumulate(xs)), "list(itertools.accumulate({0}))", "cumulative running sum"),
|
| 187 |
+
Prim("flatten", ("coll",), "list", _flatten, "[y for x in {0} for y in (x if isinstance(x, (list, tuple)) else [x])]", "flatten nested"),
|
| 188 |
+
Prim("mostcommon", ("coll",), "any", lambda xs: Counter(xs).most_common(1)[0][0], "collections.Counter({0}).most_common(1)[0][0]", "most common frequent maximum occurrences"),
|
| 189 |
+
Prim("freq", ("coll",), "dict", lambda xs: dict(Counter(xs)), "dict(collections.Counter({0}))", "frequency count each"),
|
| 190 |
+
Prim("sortkey1", ("coll",), "list", lambda xs: sorted(xs, key=lambda t: t[1]), "sorted({0}, key=lambda t: t[1])", "sort second element"),
|
| 191 |
+
Prim("maxkey_len", ("coll",), "any", lambda xs: max(xs, key=len), "max({0}, key=len)", "longest maximum length"),
|
| 192 |
+
Prim("minkey_len", ("coll",), "any", lambda xs: min(xs, key=len), "min({0}, key=len)", "shortest minimum length"),
|
| 193 |
+
Prim("range1", ("int",), "list", lambda n: list(range(1, n + 1)) if n <= 10 ** 5 else 1 / 0, "list(range(1, {0} + 1))", "first n natural numbers range"),
|
| 194 |
+
Prim("range0", ("int",), "list", lambda n: list(range(n)) if n <= 10 ** 5 else 1 / 0, "list(range({0}))", "range"),
|
| 195 |
+
# strings
|
| 196 |
+
Prim("upper", ("str",), "str", str.upper, "{0}.upper()", "upper uppercase capital"),
|
| 197 |
+
Prim("lower", ("str",), "str", str.lower, "{0}.lower()", "lower lowercase"),
|
| 198 |
+
Prim("title", ("str",), "str", str.title, "{0}.title()", "title capitalize first letter each word"),
|
| 199 |
+
Prim("capitalize", ("str",), "str", str.capitalize, "{0}.capitalize()", "capitalize"),
|
| 200 |
+
Prim("swapcase", ("str",), "str", str.swapcase, "{0}.swapcase()", "toggle swap case"),
|
| 201 |
+
Prim("strip", ("str",), "str", str.strip, "{0}.strip()", "strip trim whitespace spaces"),
|
| 202 |
+
Prim("nospace", ("str",), "str", lambda s: s.replace(" ", ""), "{0}.replace(' ', '')", "remove spaces whitespace"),
|
| 203 |
+
Prim("words", ("str",), "list", str.split, "{0}.split()", "words split"),
|
| 204 |
+
Prim("splitby", ("str", "str"), "list", lambda s, d: s.split(d), "{0}.split({1})", "split delimiter"),
|
| 205 |
+
Prim("joinsp", ("coll",), "str", lambda xs: " ".join(xs), "' '.join({0})", "join words"),
|
| 206 |
+
Prim("join", ("coll",), "str", lambda xs: "".join(xs), "''.join({0})", "join concatenate characters"),
|
| 207 |
+
Prim("replace", ("str", "str", "str"), "str", lambda s, a, b: s.replace(a, b), "{0}.replace({1}, {2})", "replace"),
|
| 208 |
+
Prim("remove", ("str", "str"), "str", lambda s, a: s.replace(a, ""), "{0}.replace({1}, '')", "remove delete"),
|
| 209 |
+
Prim("remove1", ("str", "str"), "str", lambda s, a: s.replace(a, "", 1), "{0}.replace({1}, '', 1)", "remove first occurrence"),
|
| 210 |
+
Prim("removelast", ("str", "str"), "str", lambda s, a: (s[:s.rfind(a)] + s[s.rfind(a) + 1:]) if a in s else s, "(({0}[:{0}.rfind({1})] + {0}[{0}.rfind({1}) + 1:]) if {1} in {0} else {0})", "remove last occurrence"),
|
| 211 |
+
Prim("chars", ("str",), "list", list, "list({0})", "characters chars"),
|
| 212 |
+
Prim("isdigit", ("str",), "bool", str.isdigit, "{0}.isdigit()", "digit numeric integer"),
|
| 213 |
+
Prim("isalpha", ("str",), "bool", str.isalpha, "{0}.isalpha()", "alphabet letters"),
|
| 214 |
+
Prim("isupper", ("str",), "bool", str.isupper, "{0}.isupper()", "upper"),
|
| 215 |
+
Prim("islower", ("str",), "bool", str.islower, "{0}.islower()", "lower"),
|
| 216 |
+
Prim("startswith", ("str", "str"), "bool", str.startswith, "{0}.startswith({1})", "starts begins"),
|
| 217 |
+
Prim("endswith", ("str", "str"), "bool", str.endswith, "{0}.endswith({1})", "ends"),
|
| 218 |
+
Prim("find", ("str", "str"), "int", str.find, "{0}.find({1})", "find position index occurrence"),
|
| 219 |
+
Prim("f_upper", ("str",), "str", lambda s: "".join(c for c in s if c.isupper()), "''.join(c for c in {0} if c.isupper())", "uppercase characters extract"),
|
| 220 |
+
Prim("f_lower", ("str",), "str", lambda s: "".join(c for c in s if c.islower()), "''.join(c for c in {0} if c.islower())", "lowercase characters remove"),
|
| 221 |
+
Prim("f_digit", ("str",), "str", lambda s: "".join(c for c in s if c.isdigit()), "''.join(c for c in {0} if c.isdigit())", "digits numbers extract"),
|
| 222 |
+
Prim("f_alpha", ("str",), "str", lambda s: "".join(c for c in s if c.isalpha()), "''.join(c for c in {0} if c.isalpha())", "letters remove non alphabet"),
|
| 223 |
+
Prim("f_alnum", ("str",), "str", lambda s: "".join(c for c in s if c.isalnum()), "''.join(c for c in {0} if c.isalnum())", "alphanumeric remove special characters"),
|
| 224 |
+
Prim("vowels", ("str",), "int", lambda s: sum(c in "aeiouAEIOU" for c in s), "sum(c in 'aeiouAEIOU' for c in {0})", "vowels count"),
|
| 225 |
+
Prim("ispal", ("seq",), "bool", lambda s: s == s[::-1], "({0} == {0}[::-1])", "palindrome"),
|
| 226 |
+
Prim("sortchars", ("str",), "str", lambda s: "".join(sorted(s)), "''.join(sorted({0}))", "sort characters"),
|
| 227 |
+
Prim("rev_words", ("str",), "str", lambda s: " ".join(s.split()[::-1]), "' '.join({0}.split()[::-1])", "reverse words"),
|
| 228 |
+
Prim("ord", ("str",), "int", lambda s: ord(s) if len(s) == 1 else 1 / 0, "ord({0})", "ascii value character"),
|
| 229 |
+
Prim("chr", ("int",), "str", lambda n: chr(n) if 0 <= n < 1114111 else 1 / 0, "chr({0})", "character ascii"),
|
| 230 |
+
Prim("strlen_words", ("str",), "list", lambda s: [len(w) for w in s.split()], "[len(w) for w in {0}.split()]", "length words"),
|
| 231 |
+
Prim("firstchar", ("str",), "str", lambda s: s[0], "{0}[0]", "first character"),
|
| 232 |
+
Prim("lastchar", ("str",), "str", lambda s: s[-1], "{0}[-1]", "last character"),
|
| 233 |
+
Prim("upper_first_last", ("str",), "str", lambda s: " ".join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in s.split()), "' '.join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in {0}.split())", "capitalize first last letters each word"),
|
| 234 |
+
]
|
| 235 |
+
# v2 library entries added from the gate panel's FAILURE CLASSES (general-purpose operations; checked on MBPP dev)
|
| 236 |
+
P_ += [
|
| 237 |
+
Prim("c2f", ("num",), "float", lambda c: c * 9 / 5 + 32, "({0} * 9 / 5 + 32)", "celsius fahrenheit convert temperature"),
|
| 238 |
+
Prim("f2c", ("num",), "float", lambda f: (f - 32) * 5 / 9, "(({0} - 32) * 5 / 9)", "fahrenheit celsius convert temperature"),
|
| 239 |
+
Prim("isleap", ("int",), "bool", lambda y: y % 4 == 0 and (y % 100 != 0 or y % 400 == 0),
|
| 240 |
+
"({0} % 4 == 0 and ({0} % 100 != 0 or {0} % 400 == 0))", "leap year"),
|
| 241 |
+
Prim("joincomma", ("coll",), "str", lambda xs: ",".join(xs), "','.join({0})", "join comma separated"),
|
| 242 |
+
Prim("nwords", ("str",), "int", lambda s: len(s.split()), "len({0}.split())", "count words number"),
|
| 243 |
+
]
|
| 244 |
+
IMPORTS = "import math\nimport itertools\nimport collections\n"
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# --------------------------------------------------------------------------------------------------------- request
|
| 248 |
+
def parse_tests(tests, setup=""):
|
| 249 |
+
"""'assert f(a, b) == v' -> (fname, [(args, expected)], wrapper-src or None)"""
|
| 250 |
+
fname, cases, wrap = None, [], None
|
| 251 |
+
for t in tests:
|
| 252 |
+
try:
|
| 253 |
+
node = ast.parse(t.strip()).body[0]
|
| 254 |
+
except SyntaxError:
|
| 255 |
+
return None
|
| 256 |
+
if not isinstance(node, ast.Assert):
|
| 257 |
+
return None
|
| 258 |
+
test = node.test
|
| 259 |
+
if isinstance(test, ast.Call) or (isinstance(test, ast.UnaryOp) and isinstance(test.operand, ast.Call)):
|
| 260 |
+
neg = isinstance(test, ast.UnaryOp)
|
| 261 |
+
call = test.operand if neg else test
|
| 262 |
+
exp = not neg
|
| 263 |
+
lhs = call
|
| 264 |
+
elif isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq):
|
| 265 |
+
lhs = test.left
|
| 266 |
+
try:
|
| 267 |
+
exp = ast.literal_eval(test.comparators[0])
|
| 268 |
+
except Exception: # noqa: BLE001
|
| 269 |
+
return None
|
| 270 |
+
else:
|
| 271 |
+
return None
|
| 272 |
+
call, w = lhs, None
|
| 273 |
+
if isinstance(lhs, ast.Call) and isinstance(lhs.func, ast.Name) and lhs.func.id in ("set", "sorted", "list", "tuple", "round", "abs") and lhs.args and isinstance(lhs.args[0], ast.Call):
|
| 274 |
+
w, call = lhs.func.id, lhs.args[0]
|
| 275 |
+
if not (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)):
|
| 276 |
+
return None
|
| 277 |
+
try:
|
| 278 |
+
args = [ast.literal_eval(a) for a in call.args]
|
| 279 |
+
except Exception: # noqa: BLE001
|
| 280 |
+
return None
|
| 281 |
+
if fname and call.func.id != fname:
|
| 282 |
+
return None
|
| 283 |
+
fname, wrap = call.func.id, w or wrap
|
| 284 |
+
cases.append((args, exp))
|
| 285 |
+
return (fname, cases, wrap) if fname and cases else None
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
WRAP = {None: lambda v: v, "set": set, "sorted": sorted, "list": list, "tuple": tuple, "round": round, "abs": abs}
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
ORDINAL = {"first": 0, "second": 1, "third": 2, "fourth": 3, "last": -1, "double": 2, "doubled": 2, "twice": 2,
|
| 292 |
+
"triple": 3, "tripled": 3, "half": 2, "halve": 2, "halved": 2}
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def consts_of(desc, cases, strict=False):
|
| 296 |
+
"""constants a program may use. STRICT (one example): only what the request itself names - its numbers, its
|
| 297 |
+
quoted characters and its ordinals (first=0, second=1, last=-1) - so no positional coincidence is reachable"""
|
| 298 |
+
text = re.split(r"\bso\b", desc or "")[0] if strict else (desc or "")
|
| 299 |
+
named = [int(n) for n in re.findall(r"\b(\d{1,4})\b", text)]
|
| 300 |
+
named += re.findall(r"'([^']{1,3})'", text)
|
| 301 |
+
named += [v for w, v in ORDINAL.items() if re.search(r"\b%s\b" % w, text, re.I)]
|
| 302 |
+
out = named if strict else [0, 1, 2, -1, "", " "] + named
|
| 303 |
+
return list(dict.fromkeys(out))
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
GENERIC = {"write", "function", "python", "find", "given", "number", "numbers", "integer", "list", "string", "element",
|
| 307 |
+
"elements", "value", "values", "check", "whether", "two", "the", "and", "from", "all", "using", "which", "that",
|
| 308 |
+
"with", "into", "for", "each", "array", "tuple", "item", "items", "get", "given", "not", "out", "sequence"}
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def words_of(desc):
|
| 312 |
+
return {w.lower().rstrip("s") for w in re.findall(r"[A-Za-z]+", desc or "")
|
| 313 |
+
if len(w) >= 3 and w.lower() not in GENERIC and w.lower().rstrip("s") not in GENERIC}
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
# ----------------------------------------------------------------------------------------------------------- search
|
| 317 |
+
class CodeOrgan:
|
| 318 |
+
def __init__(self, library=None, max_size=4, cap=60000):
|
| 319 |
+
self.prims = list(P_) + list(library or [])
|
| 320 |
+
self.max_size, self.cap = max_size, cap
|
| 321 |
+
self.why = ""
|
| 322 |
+
|
| 323 |
+
def learn(self, name, prog, arity_types, out_t, desc):
|
| 324 |
+
"""STORE: a verified program becomes a library function; later requests compose it"""
|
| 325 |
+
fnc = prog["fn"]
|
| 326 |
+
src = "%s(%s)" % (name, ", ".join("{%d}" % i for i in range(len(arity_types))))
|
| 327 |
+
p = Prim("lib_" + name, tuple(arity_types), out_t, lambda *vals: fnc(vals), src, " ".join(words_of(desc)))
|
| 328 |
+
self.prims.append(p)
|
| 329 |
+
self.library_src = getattr(self, "library_src", {})
|
| 330 |
+
self.library_src[name] = prog["def"].replace(IMPORTS, "")
|
| 331 |
+
return p
|
| 332 |
+
|
| 333 |
+
def solve(self, desc, tests, setup="", visible=None, strict=False):
|
| 334 |
+
self.why = ""
|
| 335 |
+
pt = parse_tests(tests, setup)
|
| 336 |
+
if not pt:
|
| 337 |
+
self.why = "the examples are not simple asserts"
|
| 338 |
+
return None
|
| 339 |
+
fname, cases, wrap = pt
|
| 340 |
+
vis = cases if visible is None else cases[:visible]
|
| 341 |
+
if not vis:
|
| 342 |
+
return None
|
| 343 |
+
W = WRAP[wrap]
|
| 344 |
+
nargs = len(vis[0][0])
|
| 345 |
+
if any(len(a) != nargs for a, _ in cases):
|
| 346 |
+
self.why = "varying arity"
|
| 347 |
+
return None
|
| 348 |
+
target = [W(e) if wrap else e for _, e in vis]
|
| 349 |
+
kw = words_of(desc)
|
| 350 |
+
# the request SPECIFIES the moves: rank by the description's words; keep the whole library but try relevant first
|
| 351 |
+
# a stored program is a move only with >= 2 examples, and never inside its own definition; with ONE example
|
| 352 |
+
# (strict) only the base library composes - forms verified on one example are too weak to build on from one more
|
| 353 |
+
pool = [p for p in self.prims if p.name != "lib_" + fname and not (strict and p.name.startswith("lib_"))]
|
| 354 |
+
prims = sorted(pool, key=lambda p: -len(p.kw & kw))
|
| 355 |
+
# terminals: arguments and constants; a term = (src, type, outputs-tuple, fn)
|
| 356 |
+
terms_by_size = {1: []}
|
| 357 |
+
seen = set()
|
| 358 |
+
|
| 359 |
+
def add(size, src, outs, fnc, used=frozenset()):
|
| 360 |
+
key = repr(outs)
|
| 361 |
+
try:
|
| 362 |
+
match = [W(o) if wrap else o for o in outs] == target
|
| 363 |
+
except Exception: # noqa: BLE001
|
| 364 |
+
match = False
|
| 365 |
+
if match:
|
| 366 |
+
# the request SPECIFIES the moves: a fit made only of moves the description never names is a
|
| 367 |
+
# coincidence of three examples, not an answer - it is not accepted and does not claim the signature.
|
| 368 |
+
# A fit must also READ its inputs: every argument is used, except an 'n' that only restates a length.
|
| 369 |
+
reads = all(argn[i] in re.findall(r"a\d+", src) or restates_len[i] for i in range(nargs))
|
| 370 |
+
# STRICT (one example): EVERY operation must be named by the request - the description specifies the
|
| 371 |
+
# program and the single example only checks it
|
| 372 |
+
named = all(pr.kw & kw for pr in used) if strict else any(pr.kw & kw for pr in used)
|
| 373 |
+
if used and reads and named:
|
| 374 |
+
return (src, fnc)
|
| 375 |
+
self.coincidences = getattr(self, "coincidences", 0) + 1
|
| 376 |
+
return None
|
| 377 |
+
if key in seen or len(key) > 1500 or len(seen) > self.cap:
|
| 378 |
+
return None # bounded memory: huge values and past-budget terms are not kept
|
| 379 |
+
seen.add(key)
|
| 380 |
+
ty = tyof(outs[0])
|
| 381 |
+
if any(tyof(o) != ty for o in outs):
|
| 382 |
+
ty = "any"
|
| 383 |
+
terms_by_size.setdefault(size, []).append((src, ty, outs, fnc, used))
|
| 384 |
+
return None
|
| 385 |
+
|
| 386 |
+
argn = ["a%d" % i for i in range(nargs)]
|
| 387 |
+
restates_len = [all(isinstance(a[i], int) and any(isinstance(a[j], (list, tuple, str)) and len(a[j]) == a[i]
|
| 388 |
+
for j in range(nargs) if j != i) for a, _ in vis)
|
| 389 |
+
for i in range(nargs)]
|
| 390 |
+
for i in range(nargs):
|
| 391 |
+
hit = add(1, argn[i], tuple(a[i] for a, _ in vis), (lambda i: lambda xs: xs[i])(i))
|
| 392 |
+
if hit:
|
| 393 |
+
return self._emit(fname, argn, hit, cases, wrap, desc)
|
| 394 |
+
for c in consts_of(desc, vis, strict):
|
| 395 |
+
add(1, repr(c), tuple(c for _ in vis), (lambda c: lambda xs: c)(c))
|
| 396 |
+
n_terms = 0
|
| 397 |
+
best = None
|
| 398 |
+
for size in range(2, self.max_size + 1):
|
| 399 |
+
for p in prims:
|
| 400 |
+
k = len(p.ins)
|
| 401 |
+
# child sizes summing to size-1
|
| 402 |
+
for parts in _compositions(size - 1, k):
|
| 403 |
+
pools = [terms_by_size.get(s, []) for s in parts]
|
| 404 |
+
pools = [[t for t in pool if fits(t[1], want) or t[1] == "any"] for pool, want in zip(pools, p.ins)]
|
| 405 |
+
if any(not pool for pool in pools):
|
| 406 |
+
continue
|
| 407 |
+
for combo in itertools.product(*pools):
|
| 408 |
+
n_terms += 1
|
| 409 |
+
if n_terms > self.cap:
|
| 410 |
+
self.why = "search budget exhausted"
|
| 411 |
+
return None
|
| 412 |
+
try:
|
| 413 |
+
outs = tuple(p.fn(*[t[2][j] for t in combo]) for j in range(len(vis)))
|
| 414 |
+
except Exception: # noqa: BLE001
|
| 415 |
+
continue
|
| 416 |
+
if any(_oversize(o) for o in outs):
|
| 417 |
+
continue # bounded memory: never represent a huge intermediate value
|
| 418 |
+
src = p.src.format(*[t[0] for t in combo])
|
| 419 |
+
fnc = (lambda p, combo: lambda xs: p.fn(*[t[3](xs) for t in combo]))(p, combo)
|
| 420 |
+
used = frozenset([p]).union(*[t[4] for t in combo])
|
| 421 |
+
hit = add(size, src, outs, fnc, used)
|
| 422 |
+
if hit:
|
| 423 |
+
if not strict:
|
| 424 |
+
return self._emit(fname, argn, hit, cases, wrap, desc)
|
| 425 |
+
# COVERAGE: how much of what the request says the program's moves account for, jointly
|
| 426 |
+
named = len(set().union(*[pr.kw for pr in used]) & kw)
|
| 427 |
+
if best is None or named > best[0]:
|
| 428 |
+
best = (named, hit)
|
| 429 |
+
if strict and best: # v2 STRICT: the fit covering most of the request, any size
|
| 430 |
+
return self._emit(fname, argn, best[1], cases, wrap, desc)
|
| 431 |
+
self.why = "no program of size <= %d fits the examples" % self.max_size
|
| 432 |
+
return None
|
| 433 |
+
|
| 434 |
+
def _emit(self, fname, argn, hit, cases, wrap, desc):
|
| 435 |
+
src, fnc = hit
|
| 436 |
+
lib = getattr(self, "library_src", {})
|
| 437 |
+
used = [n for n in lib if re.search(r"\b%s\(" % re.escape(n), src)]
|
| 438 |
+
deps = "".join(lib[n] for n in used) # stored forms this program composes
|
| 439 |
+
body = "%s%sdef %s(%s):\n return %s\n" % (IMPORTS, deps, fname, ", ".join(argn), src)
|
| 440 |
+
# CHECK: execute the RENDERED source (not the in-memory closure) against every example given
|
| 441 |
+
ns = {}
|
| 442 |
+
exec(compile(body, "<organ>", "exec"), ns) # noqa: S102 - composed only of library templates
|
| 443 |
+
W = WRAP[wrap]
|
| 444 |
+
passed = 0
|
| 445 |
+
for a, e in cases:
|
| 446 |
+
try:
|
| 447 |
+
out = ns[fname](*a)
|
| 448 |
+
ok = (W(out) if wrap else out) == e
|
| 449 |
+
except Exception: # noqa: BLE001
|
| 450 |
+
ok = False
|
| 451 |
+
passed += ok
|
| 452 |
+
return {"def": body, "fname": fname, "expr": src, "passed": passed, "cases": len(cases), "fn": fnc,
|
| 453 |
+
"arity": len(argn), "reused": used}
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def _oversize(o):
|
| 457 |
+
if isinstance(o, bool):
|
| 458 |
+
return False
|
| 459 |
+
if isinstance(o, int):
|
| 460 |
+
return abs(o) > 10 ** 12
|
| 461 |
+
if isinstance(o, str):
|
| 462 |
+
return len(o) > 400
|
| 463 |
+
if isinstance(o, (list, tuple, set, dict)):
|
| 464 |
+
return len(o) > 200 or any(_oversize(x) for x in list(o)[:50])
|
| 465 |
+
return False
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _compositions(total, k):
|
| 469 |
+
if k == 1:
|
| 470 |
+
yield (total,)
|
| 471 |
+
return
|
| 472 |
+
for first in range(1, total - k + 2):
|
| 473 |
+
for rest in _compositions(total - first, k - 1):
|
| 474 |
+
yield (first,) + rest
|
root/wander-train/code_organ_v3.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""code_organ_v3 - v2 (admitted in w482, immutable) + cover mode for conversation (coverage selection at any size
|
| 2 |
+
with stored programs allowed). v2: v1 (MBPP-frozen b2e52a1d) + STRICT mode for a single example: every move named by the
|
| 3 |
+
request. Programs as specified free moves over a verified function library, checked by execution; every
|
| 4 |
+
verified program joins the library (the stored final forms - continuous learning without retraining).
|
| 5 |
+
|
| 6 |
+
REQUEST description + input/output examples (asserts) -> function name, argument values and types, expected outputs,
|
| 7 |
+
constants the description names ("at least 4 characters", "the character 'l'")
|
| 8 |
+
MOVES typed library functions. The TYPE SYSTEM is the specified structure: a function applies only where its
|
| 9 |
+
argument types connect; within that the choice is free. The description's words rank the moves.
|
| 10 |
+
SEARCH bottom-up enumeration with OBSERVATIONAL EQUIVALENCE: programs that behave identically on the examples are
|
| 11 |
+
kept once, so compositions of 3-4 moves stay tractable.
|
| 12 |
+
CHECK the rendered source is executed against every example; nothing is released that did not run and pass.
|
| 13 |
+
STORE a verified program becomes a library function (its description words are its keywords, its signature its
|
| 14 |
+
type); later requests compose it - measured as the library-growth curve.
|
| 15 |
+
Only library templates are ever evaluated; no text from the request is executed."""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import ast
|
| 19 |
+
import itertools
|
| 20 |
+
import json
|
| 21 |
+
import math
|
| 22 |
+
import re
|
| 23 |
+
from collections import Counter
|
| 24 |
+
|
| 25 |
+
# ----------------------------------------------------------------------------------------------------------- types
|
| 26 |
+
def tyof(v):
|
| 27 |
+
if isinstance(v, bool):
|
| 28 |
+
return "bool"
|
| 29 |
+
if isinstance(v, int):
|
| 30 |
+
return "int"
|
| 31 |
+
if isinstance(v, float):
|
| 32 |
+
return "float"
|
| 33 |
+
if isinstance(v, str):
|
| 34 |
+
return "str"
|
| 35 |
+
if isinstance(v, list):
|
| 36 |
+
return "list"
|
| 37 |
+
if isinstance(v, tuple):
|
| 38 |
+
return "tuple"
|
| 39 |
+
if isinstance(v, dict):
|
| 40 |
+
return "dict"
|
| 41 |
+
if isinstance(v, set):
|
| 42 |
+
return "set"
|
| 43 |
+
return "any"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
GROUP = {"num": {"int", "float"}, "seq": {"list", "tuple", "str"}, "coll": {"list", "tuple", "set"},
|
| 47 |
+
"any": {"bool", "int", "float", "str", "list", "tuple", "dict", "set", "any"}}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def fits(t, want):
|
| 51 |
+
return t == want or t in GROUP.get(want, ())
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ------------------------------------------------------------------------------------------------------ primitives
|
| 55 |
+
class Prim:
|
| 56 |
+
__slots__ = ("name", "ins", "out", "fn", "src", "kw")
|
| 57 |
+
|
| 58 |
+
def __init__(self, name, ins, out, fn, src, kw):
|
| 59 |
+
self.name, self.ins, self.out, self.fn, self.src = name, ins, out, fn, src
|
| 60 |
+
self.kw = {w.lower().rstrip("s") for w in kw.split()} # v2: normalised exactly like the request words
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _big(x):
|
| 64 |
+
if isinstance(x, int) and abs(x) > 10 ** 7:
|
| 65 |
+
raise ValueError("too large")
|
| 66 |
+
return x
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _is_prime(n):
|
| 70 |
+
if n < 2:
|
| 71 |
+
return False
|
| 72 |
+
return all(n % d for d in range(2, int(n ** 0.5) + 1))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _uniq(xs):
|
| 76 |
+
out = []
|
| 77 |
+
for x in xs:
|
| 78 |
+
if x not in out:
|
| 79 |
+
out.append(x)
|
| 80 |
+
return out
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _flatten(xs):
|
| 84 |
+
out = []
|
| 85 |
+
for x in xs:
|
| 86 |
+
out += list(x) if isinstance(x, (list, tuple)) else [x]
|
| 87 |
+
return out
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
P_ = [
|
| 91 |
+
# numbers
|
| 92 |
+
Prim("add", ("num", "num"), "num", lambda a, b: a + b, "({0} + {1})", "add sum plus total"),
|
| 93 |
+
Prim("sub", ("num", "num"), "num", lambda a, b: a - b, "({0} - {1})", "subtract difference minus"),
|
| 94 |
+
Prim("mul", ("num", "num"), "num", lambda a, b: _big(a * b), "({0} * {1})", "multiply product times"),
|
| 95 |
+
Prim("fdiv", ("num", "num"), "num", lambda a, b: a // b, "({0} // {1})", "divide quotient integer"),
|
| 96 |
+
Prim("div", ("num", "num"), "float", lambda a, b: a / b, "({0} / {1})", "divide ratio average"),
|
| 97 |
+
Prim("mod", ("num", "num"), "num", lambda a, b: a % b, "({0} % {1})", "remainder modulo divisible"),
|
| 98 |
+
Prim("pow", ("num", "num"), "num", lambda a, b: _big(a ** b) if b < 64 else 1 / 0, "({0} ** {1})", "power square cube exponent"),
|
| 99 |
+
Prim("sq", ("num",), "num", lambda a: a * a, "({0} ** 2)", "square squares"),
|
| 100 |
+
Prim("cube", ("num",), "num", lambda a: a ** 3, "({0} ** 3)", "cube cubes"),
|
| 101 |
+
Prim("abs", ("num",), "num", abs, "abs({0})", "absolute"),
|
| 102 |
+
Prim("neg", ("num",), "num", lambda a: -a, "(-{0})", "negative negate"),
|
| 103 |
+
Prim("half", ("num",), "float", lambda a: a / 2, "({0} / 2)", "half area triangle"),
|
| 104 |
+
Prim("sqrt", ("num",), "float", math.sqrt, "math.sqrt({0})", "root square"),
|
| 105 |
+
Prim("isqrt", ("int",), "int", math.isqrt, "math.isqrt({0})", "root square integer"),
|
| 106 |
+
Prim("fact", ("int",), "int", lambda n: math.factorial(n) if 0 <= n <= 30 else 1 / 0, "math.factorial({0})", "factorial"),
|
| 107 |
+
Prim("gcd", ("int", "int"), "int", math.gcd, "math.gcd({0}, {1})", "gcd greatest common divisor"),
|
| 108 |
+
Prim("lcm", ("int", "int"), "int", lambda a, b: a * b // math.gcd(a, b) if a and b else 0, "({0} * {1} // math.gcd({0}, {1}))", "lcm least common multiple"),
|
| 109 |
+
Prim("isprime", ("int",), "bool", _is_prime, "(({0}) > 1 and all(({0}) % d for d in range(2, int(({0}) ** 0.5) + 1)))", "prime"),
|
| 110 |
+
Prim("even", ("int",), "bool", lambda n: n % 2 == 0, "({0} % 2 == 0)", "even"),
|
| 111 |
+
Prim("odd", ("int",), "bool", lambda n: n % 2 == 1, "({0} % 2 == 1)", "odd"),
|
| 112 |
+
Prim("digits", ("int",), "list", lambda n: [int(c) for c in str(abs(n))], "[int(c) for c in str(abs({0}))]", "digits digit"),
|
| 113 |
+
Prim("revnum", ("int",), "int", lambda n: int(str(abs(n))[::-1]), "int(str(abs({0}))[::-1])", "reverse number"),
|
| 114 |
+
Prim("bin", ("int",), "str", lambda n: bin(n)[2:], "bin({0})[2:]", "binary"),
|
| 115 |
+
Prim("frombin", ("str",), "int", lambda s: int(s, 2), "int({0}, 2)", "binary decimal convert"),
|
| 116 |
+
Prim("popcount", ("int",), "int", lambda n: bin(n).count("1"), "bin({0}).count('1')", "set bits count ones"),
|
| 117 |
+
Prim("xor", ("int", "int"), "int", lambda a, b: a ^ b, "({0} ^ {1})", "xor bit differ"),
|
| 118 |
+
Prim("ispow2", ("int",), "bool", lambda n: n > 0 and n & (n - 1) == 0, "({0} > 0 and {0} & ({0} - 1) == 0)", "power two bit"),
|
| 119 |
+
Prim("eq", ("any", "any"), "bool", lambda a, b: a == b, "({0} == {1})", "equal same"),
|
| 120 |
+
Prim("lt", ("num", "num"), "bool", lambda a, b: a < b, "({0} < {1})", "less smaller"),
|
| 121 |
+
Prim("gt", ("num", "num"), "bool", lambda a, b: a > b, "({0} > {1})", "greater larger"),
|
| 122 |
+
Prim("not", ("bool",), "bool", lambda a: not a, "(not {0})", "not"),
|
| 123 |
+
Prim("and", ("bool", "bool"), "bool", lambda a, b: a and b, "({0} and {1})", "and both"),
|
| 124 |
+
Prim("or", ("bool", "bool"), "bool", lambda a, b: a or b, "({0} or {1})", "or either"),
|
| 125 |
+
Prim("maxn", ("num", "num"), "num", max, "max({0}, {1})", "maximum largest bigger"),
|
| 126 |
+
Prim("minn", ("num", "num"), "num", min, "min({0}, {1})", "minimum smallest"),
|
| 127 |
+
Prim("round2", ("num",), "float", lambda a: round(a, 2), "round({0}, 2)", "round"),
|
| 128 |
+
Prim("int", ("any",), "int", int, "int({0})", "integer convert"),
|
| 129 |
+
Prim("str", ("any",), "str", str, "str({0})", "string convert"),
|
| 130 |
+
Prim("float", ("num",), "float", float, "float({0})", "float"),
|
| 131 |
+
Prim("tri", ("int",), "int", lambda n: n * (n + 1) // 2, "({0} * ({0} + 1) // 2)", "triangular sum natural"),
|
| 132 |
+
Prim("pi_r2", ("num",), "float", lambda r: math.pi * r * r, "(math.pi * {0} * {0})", "area circle"),
|
| 133 |
+
Prim("circ", ("num",), "float", lambda r: 2 * math.pi * r, "(2 * math.pi * {0})", "circumference perimeter circle"),
|
| 134 |
+
# sequences
|
| 135 |
+
Prim("len", ("seq",), "int", len, "len({0})", "length count number size"),
|
| 136 |
+
Prim("sum", ("coll",), "num", lambda xs: sum(xs), "sum({0})", "sum total add"),
|
| 137 |
+
Prim("max", ("coll",), "any", max, "max({0})", "maximum largest biggest max longest"),
|
| 138 |
+
Prim("min", ("coll",), "any", min, "min({0})", "minimum smallest min"),
|
| 139 |
+
Prim("sorted", ("coll",), "list", sorted, "sorted({0})", "sort sorted ascending order"),
|
| 140 |
+
Prim("sorted_desc", ("coll",), "list", lambda xs: sorted(xs, reverse=True), "sorted({0}, reverse=True)", "sort descending largest"),
|
| 141 |
+
Prim("rev", ("seq",), "seq", lambda xs: xs[::-1], "{0}[::-1]", "reverse reversed"),
|
| 142 |
+
Prim("first", ("seq",), "any", lambda xs: xs[0], "{0}[0]", "first"),
|
| 143 |
+
Prim("last", ("seq",), "any", lambda xs: xs[-1], "{0}[-1]", "last"),
|
| 144 |
+
Prim("take", ("seq", "int"), "seq", lambda xs, n: xs[:n], "{0}[:{1}]", "first n smallest largest take"),
|
| 145 |
+
Prim("drop", ("seq", "int"), "seq", lambda xs, n: xs[n:], "{0}[{1}:]", "remove first skip"),
|
| 146 |
+
Prim("at", ("seq", "int"), "any", lambda xs, i: xs[i], "{0}[{1}]", "nth index element second third"),
|
| 147 |
+
Prim("tolist", ("any",), "list", list, "list({0})", "list convert"),
|
| 148 |
+
Prim("totuple", ("any",), "tuple", tuple, "tuple({0})", "tuple convert"),
|
| 149 |
+
Prim("toset", ("coll",), "set", set, "set({0})", "unique distinct set"),
|
| 150 |
+
Prim("uniq", ("coll",), "list", _uniq, "list(dict.fromkeys({0}))", "unique distinct duplicates remove"),
|
| 151 |
+
Prim("inter", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) & set(b))), "tuple(sorted(set({0}) & set({1})))", "common shared similar intersection"),
|
| 152 |
+
Prim("union", ("coll", "coll"), "tuple", lambda a, b: tuple(sorted(set(a) | set(b))), "tuple(sorted(set({0}) | set({1})))", "union combine"),
|
| 153 |
+
Prim("diff", ("coll", "coll"), "list", lambda a, b: [x for x in a if x not in b], "[x for x in {0} if x not in {1}]", "difference not in"),
|
| 154 |
+
Prim("concat", ("seq", "seq"), "seq", lambda a, b: a + b, "({0} + {1})", "concatenate join append combine merge"),
|
| 155 |
+
Prim("count", ("seq", "any"), "int", lambda xs, x: xs.count(x), "{0}.count({1})", "count occurrences frequency"),
|
| 156 |
+
Prim("index", ("seq", "any"), "int", lambda xs, x: xs.index(x), "{0}.index({1})", "index position find"),
|
| 157 |
+
Prim("contains", ("any", "seq"), "bool", lambda x, xs: x in xs, "({0} in {1})", "contains present exists check"),
|
| 158 |
+
Prim("allsame", ("coll",), "bool", lambda xs: len(set(xs)) <= 1, "(len(set({0})) <= 1)", "same equal all identical"),
|
| 159 |
+
Prim("alluniq", ("coll",), "bool", lambda xs: len(set(xs)) == len(xs), "(len(set({0})) == len(list({0})))", "unique distinct duplicate"),
|
| 160 |
+
Prim("issorted", ("coll",), "bool", lambda xs: list(xs) == sorted(xs), "(list({0}) == sorted({0}))", "sorted order check"),
|
| 161 |
+
Prim("prod", ("coll",), "num", lambda xs: _big(math.prod(xs)), "math.prod({0})", "product multiply"),
|
| 162 |
+
Prim("mean", ("coll",), "float", lambda xs: sum(xs) / len(xs), "(sum({0}) / len({0}))", "average mean"),
|
| 163 |
+
Prim("map_sq", ("coll",), "list", lambda xs: [x * x for x in xs], "[x * x for x in {0}]", "square squares each"),
|
| 164 |
+
Prim("map_cube", ("coll",), "list", lambda xs: [x ** 3 for x in xs], "[x ** 3 for x in {0}]", "cube cubes each"),
|
| 165 |
+
Prim("map_neg", ("coll",), "list", lambda xs: [-x for x in xs], "[-x for x in {0}]", "negative"),
|
| 166 |
+
Prim("map_abs", ("coll",), "list", lambda xs: [abs(x) for x in xs], "[abs(x) for x in {0}]", "absolute"),
|
| 167 |
+
Prim("map_str", ("coll",), "list", lambda xs: [str(x) for x in xs], "[str(x) for x in {0}]", "string convert"),
|
| 168 |
+
Prim("map_int", ("coll",), "list", lambda xs: [int(x) for x in xs], "[int(x) for x in {0}]", "integer convert"),
|
| 169 |
+
Prim("map_len", ("coll",), "list", lambda xs: [len(x) for x in xs], "[len(x) for x in {0}]", "length each"),
|
| 170 |
+
Prim("map_first", ("coll",), "list", lambda xs: [x[0] for x in xs], "[x[0] for x in {0}]", "first each"),
|
| 171 |
+
Prim("map_last", ("coll",), "list", lambda xs: [x[-1] for x in xs], "[x[-1] for x in {0}]", "last each"),
|
| 172 |
+
Prim("map_sum", ("coll",), "list", lambda xs: [sum(x) for x in xs], "[sum(x) for x in {0}]", "sum each"),
|
| 173 |
+
Prim("map_add", ("coll", "num"), "list", lambda xs, k: [x + k for x in xs], "[x + {1} for x in {0}]", "add each"),
|
| 174 |
+
Prim("map_mul", ("coll", "num"), "list", lambda xs, k: [x * k for x in xs], "[x * {1} for x in {0}]", "multiply each double doubled twice"),
|
| 175 |
+
Prim("zipadd", ("coll", "coll"), "list", lambda a, b: [x + y for x, y in zip(a, b)], "[x + y for x, y in zip({0}, {1})]", "add two lists elementwise"),
|
| 176 |
+
Prim("zipmul", ("coll", "coll"), "list", lambda a, b: [x * y for x, y in zip(a, b)], "[x * y for x, y in zip({0}, {1})]", "multiply two lists"),
|
| 177 |
+
Prim("zipsub", ("coll", "coll"), "list", lambda a, b: [x - y for x, y in zip(a, b)], "[x - y for x, y in zip({0}, {1})]", "subtract two lists"),
|
| 178 |
+
Prim("zipdiv", ("coll", "coll"), "list", lambda a, b: [x / y for x, y in zip(a, b)], "[x / y for x, y in zip({0}, {1})]", "divide two lists"),
|
| 179 |
+
Prim("zip", ("coll", "coll"), "list", lambda a, b: list(zip(a, b)), "list(zip({0}, {1}))", "pair zip"),
|
| 180 |
+
Prim("f_even", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 0], "[x for x in {0} if x % 2 == 0]", "even"),
|
| 181 |
+
Prim("f_odd", ("coll",), "list", lambda xs: [x for x in xs if x % 2 == 1], "[x for x in {0} if x % 2 == 1]", "odd"),
|
| 182 |
+
Prim("f_pos", ("coll",), "list", lambda xs: [x for x in xs if x > 0], "[x for x in {0} if x > 0]", "positive"),
|
| 183 |
+
Prim("f_neg", ("coll",), "list", lambda xs: [x for x in xs if x < 0], "[x for x in {0} if x < 0]", "negative"),
|
| 184 |
+
Prim("f_gt", ("coll", "num"), "list", lambda xs, k: [x for x in xs if x > k], "[x for x in {0} if x > {1}]", "greater than"),
|
| 185 |
+
Prim("f_lenge", ("coll", "int"), "list", lambda xs, k: [x for x in xs if len(x) >= k], "[x for x in {0} if len(x) >= {1}]", "at least long characters"),
|
| 186 |
+
Prim("f_neq", ("coll", "any"), "list", lambda xs, k: [x for x in xs if x != k], "[x for x in {0} if x != {1}]", "remove all"),
|
| 187 |
+
Prim("cumsum", ("coll",), "list", lambda xs: list(itertools.accumulate(xs)), "list(itertools.accumulate({0}))", "cumulative running sum"),
|
| 188 |
+
Prim("flatten", ("coll",), "list", _flatten, "[y for x in {0} for y in (x if isinstance(x, (list, tuple)) else [x])]", "flatten nested"),
|
| 189 |
+
Prim("mostcommon", ("coll",), "any", lambda xs: Counter(xs).most_common(1)[0][0], "collections.Counter({0}).most_common(1)[0][0]", "most common frequent maximum occurrences"),
|
| 190 |
+
Prim("freq", ("coll",), "dict", lambda xs: dict(Counter(xs)), "dict(collections.Counter({0}))", "frequency count each"),
|
| 191 |
+
Prim("sortkey1", ("coll",), "list", lambda xs: sorted(xs, key=lambda t: t[1]), "sorted({0}, key=lambda t: t[1])", "sort second element"),
|
| 192 |
+
Prim("maxkey_len", ("coll",), "any", lambda xs: max(xs, key=len), "max({0}, key=len)", "longest maximum length"),
|
| 193 |
+
Prim("minkey_len", ("coll",), "any", lambda xs: min(xs, key=len), "min({0}, key=len)", "shortest minimum length"),
|
| 194 |
+
Prim("range1", ("int",), "list", lambda n: list(range(1, n + 1)) if n <= 10 ** 5 else 1 / 0, "list(range(1, {0} + 1))", "first n natural numbers range"),
|
| 195 |
+
Prim("range0", ("int",), "list", lambda n: list(range(n)) if n <= 10 ** 5 else 1 / 0, "list(range({0}))", "range"),
|
| 196 |
+
# strings
|
| 197 |
+
Prim("upper", ("str",), "str", str.upper, "{0}.upper()", "upper uppercase capital"),
|
| 198 |
+
Prim("lower", ("str",), "str", str.lower, "{0}.lower()", "lower lowercase"),
|
| 199 |
+
Prim("title", ("str",), "str", str.title, "{0}.title()", "title capitalize first letter each word"),
|
| 200 |
+
Prim("capitalize", ("str",), "str", str.capitalize, "{0}.capitalize()", "capitalize"),
|
| 201 |
+
Prim("swapcase", ("str",), "str", str.swapcase, "{0}.swapcase()", "toggle swap case"),
|
| 202 |
+
Prim("strip", ("str",), "str", str.strip, "{0}.strip()", "strip trim whitespace spaces"),
|
| 203 |
+
Prim("nospace", ("str",), "str", lambda s: s.replace(" ", ""), "{0}.replace(' ', '')", "remove spaces whitespace"),
|
| 204 |
+
Prim("words", ("str",), "list", str.split, "{0}.split()", "words split"),
|
| 205 |
+
Prim("splitby", ("str", "str"), "list", lambda s, d: s.split(d), "{0}.split({1})", "split delimiter"),
|
| 206 |
+
Prim("joinsp", ("coll",), "str", lambda xs: " ".join(xs), "' '.join({0})", "join words"),
|
| 207 |
+
Prim("join", ("coll",), "str", lambda xs: "".join(xs), "''.join({0})", "join concatenate characters"),
|
| 208 |
+
Prim("replace", ("str", "str", "str"), "str", lambda s, a, b: s.replace(a, b), "{0}.replace({1}, {2})", "replace"),
|
| 209 |
+
Prim("remove", ("str", "str"), "str", lambda s, a: s.replace(a, ""), "{0}.replace({1}, '')", "remove delete"),
|
| 210 |
+
Prim("remove1", ("str", "str"), "str", lambda s, a: s.replace(a, "", 1), "{0}.replace({1}, '', 1)", "remove first occurrence"),
|
| 211 |
+
Prim("removelast", ("str", "str"), "str", lambda s, a: (s[:s.rfind(a)] + s[s.rfind(a) + 1:]) if a in s else s, "(({0}[:{0}.rfind({1})] + {0}[{0}.rfind({1}) + 1:]) if {1} in {0} else {0})", "remove last occurrence"),
|
| 212 |
+
Prim("chars", ("str",), "list", list, "list({0})", "characters chars"),
|
| 213 |
+
Prim("isdigit", ("str",), "bool", str.isdigit, "{0}.isdigit()", "digit numeric integer"),
|
| 214 |
+
Prim("isalpha", ("str",), "bool", str.isalpha, "{0}.isalpha()", "alphabet letters"),
|
| 215 |
+
Prim("isupper", ("str",), "bool", str.isupper, "{0}.isupper()", "upper"),
|
| 216 |
+
Prim("islower", ("str",), "bool", str.islower, "{0}.islower()", "lower"),
|
| 217 |
+
Prim("startswith", ("str", "str"), "bool", str.startswith, "{0}.startswith({1})", "starts begins"),
|
| 218 |
+
Prim("endswith", ("str", "str"), "bool", str.endswith, "{0}.endswith({1})", "ends"),
|
| 219 |
+
Prim("find", ("str", "str"), "int", str.find, "{0}.find({1})", "find position index occurrence"),
|
| 220 |
+
Prim("f_upper", ("str",), "str", lambda s: "".join(c for c in s if c.isupper()), "''.join(c for c in {0} if c.isupper())", "uppercase characters extract"),
|
| 221 |
+
Prim("f_lower", ("str",), "str", lambda s: "".join(c for c in s if c.islower()), "''.join(c for c in {0} if c.islower())", "lowercase characters remove"),
|
| 222 |
+
Prim("f_digit", ("str",), "str", lambda s: "".join(c for c in s if c.isdigit()), "''.join(c for c in {0} if c.isdigit())", "digits numbers extract"),
|
| 223 |
+
Prim("f_alpha", ("str",), "str", lambda s: "".join(c for c in s if c.isalpha()), "''.join(c for c in {0} if c.isalpha())", "letters remove non alphabet"),
|
| 224 |
+
Prim("f_alnum", ("str",), "str", lambda s: "".join(c for c in s if c.isalnum()), "''.join(c for c in {0} if c.isalnum())", "alphanumeric remove special characters"),
|
| 225 |
+
Prim("vowels", ("str",), "int", lambda s: sum(c in "aeiouAEIOU" for c in s), "sum(c in 'aeiouAEIOU' for c in {0})", "vowels count"),
|
| 226 |
+
Prim("ispal", ("seq",), "bool", lambda s: s == s[::-1], "({0} == {0}[::-1])", "palindrome"),
|
| 227 |
+
Prim("sortchars", ("str",), "str", lambda s: "".join(sorted(s)), "''.join(sorted({0}))", "sort characters"),
|
| 228 |
+
Prim("rev_words", ("str",), "str", lambda s: " ".join(s.split()[::-1]), "' '.join({0}.split()[::-1])", "reverse words"),
|
| 229 |
+
Prim("ord", ("str",), "int", lambda s: ord(s) if len(s) == 1 else 1 / 0, "ord({0})", "ascii value character"),
|
| 230 |
+
Prim("chr", ("int",), "str", lambda n: chr(n) if 0 <= n < 1114111 else 1 / 0, "chr({0})", "character ascii"),
|
| 231 |
+
Prim("strlen_words", ("str",), "list", lambda s: [len(w) for w in s.split()], "[len(w) for w in {0}.split()]", "length words"),
|
| 232 |
+
Prim("firstchar", ("str",), "str", lambda s: s[0], "{0}[0]", "first character"),
|
| 233 |
+
Prim("lastchar", ("str",), "str", lambda s: s[-1], "{0}[-1]", "last character"),
|
| 234 |
+
Prim("upper_first_last", ("str",), "str", lambda s: " ".join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in s.split()), "' '.join((w[0].upper() + w[1:-1] + w[-1].upper()) if len(w) > 1 else w.upper() for w in {0}.split())", "capitalize first last letters each word"),
|
| 235 |
+
]
|
| 236 |
+
# v2 library entries added from the gate panel's FAILURE CLASSES (general-purpose operations; checked on MBPP dev)
|
| 237 |
+
P_ += [
|
| 238 |
+
Prim("c2f", ("num",), "float", lambda c: c * 9 / 5 + 32, "({0} * 9 / 5 + 32)", "celsius fahrenheit convert temperature"),
|
| 239 |
+
Prim("f2c", ("num",), "float", lambda f: (f - 32) * 5 / 9, "(({0} - 32) * 5 / 9)", "fahrenheit celsius convert temperature"),
|
| 240 |
+
Prim("isleap", ("int",), "bool", lambda y: y % 4 == 0 and (y % 100 != 0 or y % 400 == 0),
|
| 241 |
+
"({0} % 4 == 0 and ({0} % 100 != 0 or {0} % 400 == 0))", "leap year"),
|
| 242 |
+
Prim("joincomma", ("coll",), "str", lambda xs: ",".join(xs), "','.join({0})", "join comma separated"),
|
| 243 |
+
Prim("nwords", ("str",), "int", lambda s: len(s.split()), "len({0}.split())", "count words number"),
|
| 244 |
+
]
|
| 245 |
+
IMPORTS = "import math\nimport itertools\nimport collections\n"
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# --------------------------------------------------------------------------------------------------------- request
|
| 249 |
+
def parse_tests(tests, setup=""):
|
| 250 |
+
"""'assert f(a, b) == v' -> (fname, [(args, expected)], wrapper-src or None)"""
|
| 251 |
+
fname, cases, wrap = None, [], None
|
| 252 |
+
for t in tests:
|
| 253 |
+
try:
|
| 254 |
+
node = ast.parse(t.strip()).body[0]
|
| 255 |
+
except SyntaxError:
|
| 256 |
+
return None
|
| 257 |
+
if not isinstance(node, ast.Assert):
|
| 258 |
+
return None
|
| 259 |
+
test = node.test
|
| 260 |
+
if isinstance(test, ast.Call) or (isinstance(test, ast.UnaryOp) and isinstance(test.operand, ast.Call)):
|
| 261 |
+
neg = isinstance(test, ast.UnaryOp)
|
| 262 |
+
call = test.operand if neg else test
|
| 263 |
+
exp = not neg
|
| 264 |
+
lhs = call
|
| 265 |
+
elif isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq):
|
| 266 |
+
lhs = test.left
|
| 267 |
+
try:
|
| 268 |
+
exp = ast.literal_eval(test.comparators[0])
|
| 269 |
+
except Exception: # noqa: BLE001
|
| 270 |
+
return None
|
| 271 |
+
else:
|
| 272 |
+
return None
|
| 273 |
+
call, w = lhs, None
|
| 274 |
+
if isinstance(lhs, ast.Call) and isinstance(lhs.func, ast.Name) and lhs.func.id in ("set", "sorted", "list", "tuple", "round", "abs") and lhs.args and isinstance(lhs.args[0], ast.Call):
|
| 275 |
+
w, call = lhs.func.id, lhs.args[0]
|
| 276 |
+
if not (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)):
|
| 277 |
+
return None
|
| 278 |
+
try:
|
| 279 |
+
args = [ast.literal_eval(a) for a in call.args]
|
| 280 |
+
except Exception: # noqa: BLE001
|
| 281 |
+
return None
|
| 282 |
+
if fname and call.func.id != fname:
|
| 283 |
+
return None
|
| 284 |
+
fname, wrap = call.func.id, w or wrap
|
| 285 |
+
cases.append((args, exp))
|
| 286 |
+
return (fname, cases, wrap) if fname and cases else None
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
WRAP = {None: lambda v: v, "set": set, "sorted": sorted, "list": list, "tuple": tuple, "round": round, "abs": abs}
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
ORDINAL = {"first": 0, "second": 1, "third": 2, "fourth": 3, "last": -1, "double": 2, "doubled": 2, "twice": 2,
|
| 293 |
+
"triple": 3, "tripled": 3, "half": 2, "halve": 2, "halved": 2}
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def consts_of(desc, cases, strict=False):
|
| 297 |
+
"""constants a program may use. STRICT (one example): only what the request itself names - its numbers, its
|
| 298 |
+
quoted characters and its ordinals (first=0, second=1, last=-1) - so no positional coincidence is reachable"""
|
| 299 |
+
text = re.split(r"\bso\b", desc or "")[0] if strict else (desc or "")
|
| 300 |
+
named = [int(n) for n in re.findall(r"\b(\d{1,4})\b", text)]
|
| 301 |
+
named += re.findall(r"'([^']{1,3})'", text)
|
| 302 |
+
named += [v for w, v in ORDINAL.items() if re.search(r"\b%s\b" % w, text, re.I)]
|
| 303 |
+
out = named if strict else [0, 1, 2, -1, "", " "] + named
|
| 304 |
+
return list(dict.fromkeys(out))
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
GENERIC = {"write", "function", "python", "find", "given", "number", "numbers", "integer", "list", "string", "element",
|
| 308 |
+
"elements", "value", "values", "check", "whether", "two", "the", "and", "from", "all", "using", "which", "that",
|
| 309 |
+
"with", "into", "for", "each", "array", "tuple", "item", "items", "get", "given", "not", "out", "sequence"}
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def words_of(desc):
|
| 313 |
+
return {w.lower().rstrip("s") for w in re.findall(r"[A-Za-z]+", desc or "")
|
| 314 |
+
if len(w) >= 3 and w.lower() not in GENERIC and w.lower().rstrip("s") not in GENERIC}
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ----------------------------------------------------------------------------------------------------------- search
|
| 318 |
+
class CodeOrgan:
|
| 319 |
+
def __init__(self, library=None, max_size=4, cap=60000):
|
| 320 |
+
self.prims = list(P_) + list(library or [])
|
| 321 |
+
self.max_size, self.cap = max_size, cap
|
| 322 |
+
self.why = ""
|
| 323 |
+
|
| 324 |
+
def learn(self, name, prog, arity_types, out_t, desc):
|
| 325 |
+
"""STORE: a verified program becomes a library function; later requests compose it"""
|
| 326 |
+
fnc = prog["fn"]
|
| 327 |
+
src = "%s(%s)" % (name, ", ".join("{%d}" % i for i in range(len(arity_types))))
|
| 328 |
+
p = Prim("lib_" + name, tuple(arity_types), out_t, lambda *vals: fnc(vals), src, " ".join(words_of(desc)))
|
| 329 |
+
self.prims.append(p)
|
| 330 |
+
self.library_src = getattr(self, "library_src", {})
|
| 331 |
+
self.library_src[name] = prog["def"].replace(IMPORTS, "")
|
| 332 |
+
return p
|
| 333 |
+
|
| 334 |
+
def solve(self, desc, tests, setup="", visible=None, strict=False, cover=False, allow_lib=True):
|
| 335 |
+
"""strict: request-named constants only + coverage selection (one example); cover: coverage selection at any
|
| 336 |
+
size (conversation); allow_lib: stored programs may compose (needs >= 2 examples)"""
|
| 337 |
+
cover = cover or strict
|
| 338 |
+
self.why = ""
|
| 339 |
+
pt = parse_tests(tests, setup)
|
| 340 |
+
if not pt:
|
| 341 |
+
self.why = "the examples are not simple asserts"
|
| 342 |
+
return None
|
| 343 |
+
fname, cases, wrap = pt
|
| 344 |
+
vis = cases if visible is None else cases[:visible]
|
| 345 |
+
if not vis:
|
| 346 |
+
return None
|
| 347 |
+
W = WRAP[wrap]
|
| 348 |
+
nargs = len(vis[0][0])
|
| 349 |
+
if any(len(a) != nargs for a, _ in cases):
|
| 350 |
+
self.why = "varying arity"
|
| 351 |
+
return None
|
| 352 |
+
target = [W(e) if wrap else e for _, e in vis]
|
| 353 |
+
kw = words_of(desc)
|
| 354 |
+
self._req_kw, self._cover, self._n_visible = kw, None, len(vis)
|
| 355 |
+
# the request SPECIFIES the moves: rank by the description's words; keep the whole library but try relevant first
|
| 356 |
+
# a stored program is a move only with >= 2 examples, and never inside its own definition; with ONE example
|
| 357 |
+
# (strict) only the base library composes - forms verified on one example are too weak to build on from one more
|
| 358 |
+
pool = [p for p in self.prims if p.name != "lib_" + fname and not ((strict or not allow_lib) and p.name.startswith("lib_"))]
|
| 359 |
+
prims = sorted(pool, key=lambda p: -len(p.kw & kw))
|
| 360 |
+
# terminals: arguments and constants; a term = (src, type, outputs-tuple, fn)
|
| 361 |
+
terms_by_size = {1: []}
|
| 362 |
+
seen = set()
|
| 363 |
+
|
| 364 |
+
def add(size, src, outs, fnc, used=frozenset()):
|
| 365 |
+
key = repr(outs)
|
| 366 |
+
try:
|
| 367 |
+
match = [W(o) if wrap else o for o in outs] == target
|
| 368 |
+
except Exception: # noqa: BLE001
|
| 369 |
+
match = False
|
| 370 |
+
if match:
|
| 371 |
+
# the request SPECIFIES the moves: a fit made only of moves the description never names is a
|
| 372 |
+
# coincidence of three examples, not an answer - it is not accepted and does not claim the signature.
|
| 373 |
+
# A fit must also READ its inputs: every argument is used, except an 'n' that only restates a length.
|
| 374 |
+
reads = all(argn[i] in re.findall(r"a\d+", src) or restates_len[i] for i in range(nargs))
|
| 375 |
+
# STRICT (one example): EVERY operation must be named by the request - the description specifies the
|
| 376 |
+
# program and the single example only checks it
|
| 377 |
+
named = all(pr.kw & kw for pr in used) if strict else any(pr.kw & kw for pr in used)
|
| 378 |
+
if used and reads and named:
|
| 379 |
+
return (src, fnc)
|
| 380 |
+
self.coincidences = getattr(self, "coincidences", 0) + 1
|
| 381 |
+
return None
|
| 382 |
+
if key in seen or len(key) > 1500 or len(seen) > self.cap:
|
| 383 |
+
return None # bounded memory: huge values and past-budget terms are not kept
|
| 384 |
+
seen.add(key)
|
| 385 |
+
ty = tyof(outs[0])
|
| 386 |
+
if any(tyof(o) != ty for o in outs):
|
| 387 |
+
ty = "any"
|
| 388 |
+
terms_by_size.setdefault(size, []).append((src, ty, outs, fnc, used))
|
| 389 |
+
return None
|
| 390 |
+
|
| 391 |
+
argn = ["a%d" % i for i in range(nargs)]
|
| 392 |
+
restates_len = [all(isinstance(a[i], int) and any(isinstance(a[j], (list, tuple, str)) and len(a[j]) == a[i]
|
| 393 |
+
for j in range(nargs) if j != i) for a, _ in vis)
|
| 394 |
+
for i in range(nargs)]
|
| 395 |
+
for i in range(nargs):
|
| 396 |
+
hit = add(1, argn[i], tuple(a[i] for a, _ in vis), (lambda i: lambda xs: xs[i])(i))
|
| 397 |
+
if hit:
|
| 398 |
+
return self._emit(fname, argn, hit, cases, wrap, desc)
|
| 399 |
+
for c in consts_of(desc, vis, strict):
|
| 400 |
+
add(1, repr(c), tuple(c for _ in vis), (lambda c: lambda xs: c)(c))
|
| 401 |
+
n_terms = 0
|
| 402 |
+
best = None
|
| 403 |
+
first_hit_size = None
|
| 404 |
+
for size in range(2, self.max_size + 1):
|
| 405 |
+
if cover and best and first_hit_size is None:
|
| 406 |
+
first_hit_size = size - 1
|
| 407 |
+
if cover and first_hit_size is not None and size > first_hit_size + 1:
|
| 408 |
+
break # v3: look at most ONE size beyond the first fit (bounded time)
|
| 409 |
+
for p in prims:
|
| 410 |
+
k = len(p.ins)
|
| 411 |
+
# child sizes summing to size-1
|
| 412 |
+
for parts in _compositions(size - 1, k):
|
| 413 |
+
pools = [terms_by_size.get(s, []) for s in parts]
|
| 414 |
+
pools = [[t for t in pool if fits(t[1], want) or t[1] == "any"] for pool, want in zip(pools, p.ins)]
|
| 415 |
+
if any(not pool for pool in pools):
|
| 416 |
+
continue
|
| 417 |
+
for combo in itertools.product(*pools):
|
| 418 |
+
n_terms += 1
|
| 419 |
+
if n_terms > self.cap:
|
| 420 |
+
if cover and best: # v3: a found fit is never discarded by the budget
|
| 421 |
+
return self._emit(fname, argn, best[1], cases, wrap, desc)
|
| 422 |
+
self.why = "search budget exhausted"
|
| 423 |
+
return None
|
| 424 |
+
try:
|
| 425 |
+
outs = tuple(p.fn(*[t[2][j] for t in combo]) for j in range(len(vis)))
|
| 426 |
+
except Exception: # noqa: BLE001
|
| 427 |
+
continue
|
| 428 |
+
if any(_oversize(o) for o in outs):
|
| 429 |
+
continue # bounded memory: never represent a huge intermediate value
|
| 430 |
+
src = p.src.format(*[t[0] for t in combo])
|
| 431 |
+
fnc = (lambda p, combo: lambda xs: p.fn(*[t[3](xs) for t in combo]))(p, combo)
|
| 432 |
+
used = frozenset([p]).union(*[t[4] for t in combo])
|
| 433 |
+
hit = add(size, src, outs, fnc, used)
|
| 434 |
+
if hit:
|
| 435 |
+
if not cover:
|
| 436 |
+
return self._emit(fname, argn, hit, cases, wrap, desc)
|
| 437 |
+
# COVERAGE: how much of what the request says the program's moves account for, jointly
|
| 438 |
+
named = len(set().union(*[pr.kw for pr in used]) & kw)
|
| 439 |
+
if best is None or named > best[0]:
|
| 440 |
+
best = (named, hit)
|
| 441 |
+
self._cover = named
|
| 442 |
+
if cover and best: # v2 STRICT: the fit covering most of the request, any size
|
| 443 |
+
return self._emit(fname, argn, best[1], cases, wrap, desc)
|
| 444 |
+
self.why = "no program of size <= %d fits the examples" % self.max_size
|
| 445 |
+
return None
|
| 446 |
+
|
| 447 |
+
def _emit(self, fname, argn, hit, cases, wrap, desc):
|
| 448 |
+
src, fnc = hit
|
| 449 |
+
lib = getattr(self, "library_src", {})
|
| 450 |
+
used = [n for n in lib if re.search(r"\b%s\(" % re.escape(n), src)]
|
| 451 |
+
deps = "".join(lib[n] for n in used) # stored forms this program composes
|
| 452 |
+
body = "%s%sdef %s(%s):\n return %s\n" % (IMPORTS, deps, fname, ", ".join(argn), src)
|
| 453 |
+
# CHECK: execute the RENDERED source (not the in-memory closure) against every example given
|
| 454 |
+
ns = {}
|
| 455 |
+
exec(compile(body, "<organ>", "exec"), ns) # noqa: S102 - composed only of library templates
|
| 456 |
+
W = WRAP[wrap]
|
| 457 |
+
passed = 0
|
| 458 |
+
for a, e in cases:
|
| 459 |
+
try:
|
| 460 |
+
out = ns[fname](*a)
|
| 461 |
+
ok = (W(out) if wrap else out) == e
|
| 462 |
+
except Exception: # noqa: BLE001
|
| 463 |
+
ok = False
|
| 464 |
+
passed += ok
|
| 465 |
+
return {"def": body, "fname": fname, "expr": src, "passed": passed, "cases": len(cases), "fn": fnc,
|
| 466 |
+
"arity": len(argn), "reused": used, "cover": getattr(self, "_cover", None),
|
| 467 |
+
"req_words": len(getattr(self, "_req_kw", ()) or ()), "n_visible": getattr(self, "_n_visible", None)}
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def _oversize(o):
|
| 471 |
+
if isinstance(o, bool):
|
| 472 |
+
return False
|
| 473 |
+
if isinstance(o, int):
|
| 474 |
+
return abs(o) > 10 ** 12
|
| 475 |
+
if isinstance(o, str):
|
| 476 |
+
return len(o) > 400
|
| 477 |
+
if isinstance(o, (list, tuple, set, dict)):
|
| 478 |
+
return len(o) > 200 or any(_oversize(x) for x in list(o)[:50])
|
| 479 |
+
return False
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def _compositions(total, k):
|
| 483 |
+
if k == 1:
|
| 484 |
+
yield (total,)
|
| 485 |
+
return
|
| 486 |
+
for first in range(1, total - k + 2):
|
| 487 |
+
for rest in _compositions(total - first, k - 1):
|
| 488 |
+
yield (first,) + rest
|
root/wander-train/exact_router_v1.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""exact_router_v1 - Oracle7's EXACT relation router (runtime form). No similarity, no learned weights: a question is answered
|
| 3 |
+
only when an ADMITTED pattern names the relation it asks and the linked entities agree on one stored value.
|
| 4 |
+
|
| 5 |
+
Patterns (router_mine_v3, 2026-10-06): (asked noun or wh-word, one content word) -> Wikidata property, mined over 124,000
|
| 6 |
+
TriviaQA train questions; admitted when the answer given through that property is right >= 90% of the time on BOTH md5
|
| 7 |
+
halves with >= 8 firings in each (32 patterns, 13 properties). Never-read holdout (TriviaQA train offset 82,000): 33 right,
|
| 8 |
+
1 wrong (the wrong one was a Wikidata unknown-value blank node - now refused, rule 3 below).
|
| 9 |
+
Answer: entity_linker_v2 top span -> its candidates (<= 3) -> every candidate carrying the routed property must agree on
|
| 10 |
+
ONE value. Refused: (1) yes/no questions; (2) a hidden hop - an unnamed intermediate between the named entity and the
|
| 11 |
+
answer ("an artist who also directed", "the brother-in-law of X"), the guard measured 2026-10-06; (3) blank-node values;
|
| 12 |
+
(4) every matching pattern must name the SAME property.
|
| 13 |
+
use: answer(question) -> {"answer", "property", "subject_qid", "pattern"} or None"""
|
| 14 |
+
import json
|
| 15 |
+
import re
|
| 16 |
+
|
| 17 |
+
import entity_linker_v2 as L
|
| 18 |
+
import oracle_relstore_v1 as RS
|
| 19 |
+
|
| 20 |
+
PATTERNS = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/router_patterns_v4.json"
|
| 21 |
+
STORE = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/relations_v2.sqlite3"
|
| 22 |
+
_PATS, _RS = None, None
|
| 23 |
+
norm = L.norm
|
| 24 |
+
HEAD = re.compile(r"\b(?:which|what)\s+((?:[a-z]+\s+){0,2}?[a-z]+)(?:\s+(?:is|was|are|were|did|does|do|has|had|in|of|for|from|"
|
| 25 |
+
r"to|by|named|called|founded|created|directed|wrote|won|became)\b|\s*,)", re.I)
|
| 26 |
+
WH = re.compile(r"\b(who|whom|whose|where|when|which|what|how)\b", re.I)
|
| 27 |
+
POLAR = re.compile(r"^\s*(is|are|was|were|do|does|did|has|have|had|can|could|will|would|should)\b", re.I)
|
| 28 |
+
HOP = re.compile(r"\b(a|an|the)\s+([\w'–-]+\s+){0,3}(who|whom|whose|which|that)\b(?!\s*\?)"
|
| 29 |
+
r"|\b(a|an|the)\s+([\w'–-]+\s+){0,5}(created|directed|written|founded|coached|owned|produced|hosted|led|played|named)\s+by\b"
|
| 30 |
+
r"|\bthe\s+(illustrator|author|director|founder|creator|wife|husband|father|mother|son|daughter|brother|sister|"
|
| 31 |
+
r"brother-in-law|sister-in-law|father-in-law|mother-in-law|son-in-law|daughter-in-law|nephew|niece|uncle|aunt|"
|
| 32 |
+
r"owner|composer|producer|writer|singer|star|team|club|band|company|capital|leader|coach|manager)\s+of\b", re.I)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _load():
|
| 36 |
+
global _PATS, _RS
|
| 37 |
+
if _PATS is None:
|
| 38 |
+
_PATS = {tuple(k.split("|", 1)): v[0] for k, v in json.load(open(PATTERNS)).items()}
|
| 39 |
+
_RS = RS.RelStore() if STORE is None else RS.RelStore(STORE)
|
| 40 |
+
return _PATS, _RS
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def features(q, span):
|
| 44 |
+
m = HEAD.search(q)
|
| 45 |
+
head = norm(m.group(1)).split()[-1] if m else None
|
| 46 |
+
w = WH.search(q)
|
| 47 |
+
key = head or (w.group(1).lower() if w else "-")
|
| 48 |
+
ent = set(norm(span).split())
|
| 49 |
+
words = {x for x in norm(q).split() if x not in L.STOP and x not in ent and not x.isdigit() and len(x) >= 3}
|
| 50 |
+
return {(key, x) for x in words} | {(key, "*")}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
DOMINANCE = 10 # a name whose top sense is >= 10x more linked refers to it
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def hop(q, span):
|
| 57 |
+
"""a hidden hop - but "the capital of France" is no hop when France is the named entity right after "of" """
|
| 58 |
+
for m in HOP.finditer(q):
|
| 59 |
+
if m.group(0).lower().endswith(" of"):
|
| 60 |
+
after = norm(q[m.end():])
|
| 61 |
+
if after.startswith(norm(span)) or after.startswith("the " + norm(span)):
|
| 62 |
+
continue
|
| 63 |
+
return True
|
| 64 |
+
return False
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# the store holds CURRENT values: a past-time qualifier on a relation that changes over time is not answerable from it
|
| 68 |
+
TIME_VARIANT = {36, 35, 6, 54, 159, 27, 17, 131, 286, 488, 1037, 108, 39, 463, 102, 26}
|
| 69 |
+
PAST = re.compile(r"\b(between|until|till|before|after|during|in|from|by)\s+(the\s+)?(\d{3,4}s?|\d{1,2}(st|nd|rd|th)\s+century)\b"
|
| 70 |
+
r"|\b(formerly|former|originally|once|previously|ancient|old)\b", re.I)
|
| 71 |
+
QUOTED = re.compile(r"[‘“\"']([^’”\"']{3,80})[’”\"']")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def subject_ok(q, span):
|
| 75 |
+
"""the linked span must be the question's subject: a quoted title must BE the span; a later "of <Name>" means the
|
| 76 |
+
subject is further along the "of" chain ("the capital of the U.S. state of Connecticut")"""
|
| 77 |
+
s = q.strip()
|
| 78 |
+
if len(s) > 2 and s[0] == '"' and s[-1] == '"':
|
| 79 |
+
s = s[1:-1] # CSV-wrapped question: "Who ... ""Citizen Kane""?"
|
| 80 |
+
m = QUOTED.search(s.replace('""', '"'))
|
| 81 |
+
if m and norm(m.group(1)) != norm(span):
|
| 82 |
+
return False
|
| 83 |
+
i = q.find(span)
|
| 84 |
+
if i >= 0 and re.search(r"\bof\s+(the\s+)?[A-Z]", q[i + len(span):]):
|
| 85 |
+
return False
|
| 86 |
+
return True
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def answer(q):
|
| 90 |
+
pats, rs = _load()
|
| 91 |
+
q = str(q)
|
| 92 |
+
if POLAR.search(q):
|
| 93 |
+
return None
|
| 94 |
+
sp = L.link(q, 3)
|
| 95 |
+
if not sp:
|
| 96 |
+
return None
|
| 97 |
+
span, cs = sp[0]
|
| 98 |
+
if hop(q, span) or not subject_ok(q, span):
|
| 99 |
+
return None
|
| 100 |
+
if DOMINANCE and len(cs) > 1 and cs[0][1] >= DOMINANCE * max(1, max(c[1] for c in cs[1:])):
|
| 101 |
+
cs = cs[:1]
|
| 102 |
+
matched = {f: pats[f] for f in features(q, span) if f in pats}
|
| 103 |
+
props = set(matched.values())
|
| 104 |
+
if len(props) != 1:
|
| 105 |
+
return None
|
| 106 |
+
p = props.pop()
|
| 107 |
+
if p in TIME_VARIANT and PAST.search(q):
|
| 108 |
+
return None
|
| 109 |
+
sets, subj = [], None
|
| 110 |
+
for qid, _, _ in cs[:3]:
|
| 111 |
+
vals = {rs.label(o) if o else lit for pp, o, lit in rs.props_of(qid) if pp == p}
|
| 112 |
+
vals = {v for v in vals if v and ".well-known/genid" not in v}
|
| 113 |
+
if vals:
|
| 114 |
+
sets.append(frozenset(vals))
|
| 115 |
+
subj = subj or qid
|
| 116 |
+
if not sets or len(set(sets)) != 1 or len(sets[0]) != 1:
|
| 117 |
+
return None
|
| 118 |
+
aw = [w for w in norm(next(iter(sets[0]))).split() if w not in L.STOP]
|
| 119 |
+
if aw and all(w in set(norm(q).split()) for w in aw):
|
| 120 |
+
return None # the answer restates the question: not an answer
|
| 121 |
+
return {"answer": _show(next(iter(sets[0])), q), "property": p, "subject_qid": subj,
|
| 122 |
+
"pattern": "|".join(sorted(matched)[0])}
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
MONTHS = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _show(v, q):
|
| 129 |
+
"""a stored time "1034-03-27T00:00:00Z" reads as "1034" when the year is asked, else "27 March 1034"; BCE years keep
|
| 130 |
+
their sign as "BC" """
|
| 131 |
+
m = re.match(r"^([+-]?)(\d{1,4})-(\d\d)-(\d\d)T", v)
|
| 132 |
+
if not m:
|
| 133 |
+
return v
|
| 134 |
+
y = str(int(m.group(2))) + (" BC" if m.group(1) == "-" else "")
|
| 135 |
+
if re.search(r"\byear\b", q, re.I) or m.group(3) == "00" or (m.group(3) == "01" and m.group(4) == "01"):
|
| 136 |
+
return y
|
| 137 |
+
return "%d %s %s" % (int(m.group(4)), MONTHS[int(m.group(3)) - 1], y)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
if __name__ == "__main__":
|
| 141 |
+
import sys
|
| 142 |
+
for q in sys.argv[1:]:
|
| 143 |
+
print(q, "->", answer(q))
|
root/wander-train/general_reader_v1.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""general_reader_v1 - a relation reader that does not depend on how a question is phrased (Box C).
|
| 3 |
+
For a question the release declines: find the entities it names (exact Wikipedia titles), read every fact one step from them in
|
| 4 |
+
the Wikidata relation store, and answer ONLY when all three agree:
|
| 5 |
+
(1) CUE - the relation is named by the question's own words (property label stems, or a measured router cue pair)
|
| 6 |
+
(2) TYPE - the answer is the kind of thing asked for (who -> a person; where -> a place; "which comedian" -> a comedian)
|
| 7 |
+
(3) UNIQUE- exactly one candidate survives (two different answers -> decline)
|
| 8 |
+
The answer is never an entity the question itself names. Every answer carries its receipt (subject, property, value, ids).
|
| 9 |
+
usage (dev measurement): general_reader_v1.py eval <trivia json ...> | general_reader_v1.py ask "<question>" """
|
| 10 |
+
import json
|
| 11 |
+
import re
|
| 12 |
+
import sqlite3
|
| 13 |
+
import sys
|
| 14 |
+
import unicodedata
|
| 15 |
+
|
| 16 |
+
STORE = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/relations_v2.sqlite3"
|
| 17 |
+
db = sqlite3.connect("file:%s?mode=ro&immutable=1" % STORE, uri=True, check_same_thread=False)
|
| 18 |
+
|
| 19 |
+
STOP = set("a an the of in on at to for by with from and or is was were are be been being who whom whose what which when where why how "
|
| 20 |
+
"did does do has have had this that these those it its he she they his her their as into than then there here one".split())
|
| 21 |
+
AUX = set("is was were are did does do has have had can could will would should may might".split())
|
| 22 |
+
HUMAN_Q = ("who", "whom", "whose")
|
| 23 |
+
PLACE_WORDS = set("city town village country state province region county island capital place municipality commune borough "
|
| 24 |
+
"district settlement port river mountain lake sea ocean continent area".split())
|
| 25 |
+
GENERIC_TYPE = set("one name person people thing year type kind part title area word term number".split())
|
| 26 |
+
# qualifiers the reader cannot evaluate from one fact: order, extremes, time, fiction or a work's own world
|
| 27 |
+
QUALIFIER = re.compile(r"\b(first|last|only|second|third|fourth|fifth|oldest|youngest|largest|smallest|biggest|longest|shortest|"
|
| 28 |
+
r"highest|lowest|tallest|most|least|best|worst|original|former|current|currently|previous|next|latest|earliest|"
|
| 29 |
+
r"famous|famously|nicknamed|known as|better known|called|real name|in the (?:bible|film|movie|novel|book|play|"
|
| 30 |
+
r"series|show|song|opera|musical|game|tv|story|poem|legend)|in (?:greek|roman|norse) mythology|"
|
| 31 |
+
r"(?:1[0-9]|20)[0-9]{2}|century|decade|year|when|during|at the time|outbreak|battle|war|preceded|succeeded|before|after|"
|
| 32 |
+
r"between|same|other|also|both|nickname|setting|set in|according to)\b", re.I)
|
| 33 |
+
|
| 34 |
+
PROP, PROPNAMES = {}, {}
|
| 35 |
+
for p, lab in db.execute("SELECT p, label FROM prop_src"):
|
| 36 |
+
if lab:
|
| 37 |
+
PROP[p] = lab
|
| 38 |
+
for line in open(__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/prop_labels.tsv", encoding="utf-8"):
|
| 39 |
+
m = re.match(r'<http://www\.wikidata\.org/entity/P(\d+)>\t"(.*)"@en', line.strip())
|
| 40 |
+
if m:
|
| 41 |
+
p = int(m.group(1))
|
| 42 |
+
PROPNAMES.setdefault(p, []).append(m.group(2)) # the file holds the label AND its aliases, one per line
|
| 43 |
+
PROP.setdefault(p, m.group(2))
|
| 44 |
+
# everyday words for common relations (question wording differs from Wikidata's label); measured on dev like everything else
|
| 45 |
+
ALIASES = {26: "married marry wife husband spouse", 25: "mother", 22: "father", 3373: "sister brother sibling", 40: "son daughter child",
|
| 46 |
+
50: "writer author novelist", 57: "directed director", 86: "composed composer music", 170: "created creator",
|
| 47 |
+
112: "founded founder", 36: "capital", 19: "born birthplace", 20: "died death", 569: "born", 570: "died",
|
| 48 |
+
175: "singer performer", 161: "starred", 69: "studied attended educated", 108: "worked employer",
|
| 49 |
+
54: "played team", 166: "won awarded award", 131: "located situated", 138: "named", 37: "language spoken official",
|
| 50 |
+
38: "currency", 6: "prime minister mayor premier", 35: "president king queen monarch emperor", 27: "nationality citizen",
|
| 51 |
+
800: "known work", 61: "discovered discoverer", 397: "orbits orbit", 30: "continent", 140: "religion", 102: "party",
|
| 52 |
+
127: "owned owner owns", 176: "made built manufactured manufacturer", 84: "designed architect", 674: "characters character",
|
| 53 |
+
1441: "appears appear", 136: "genre", 264: "label", 98: "edited editor", 162: "produced producer", 58: "screenplay screenwriter",
|
| 54 |
+
710: "participant participated", 1346: "won winner", 17: "country", 495: "country", 159: "headquarters based", 740: "formed",
|
| 55 |
+
571: "founded established formed", 576: "dissolved abolished", 1037: "director manager", 488: "chairman chairperson",
|
| 56 |
+
169: "chief executive ceo", 286: "head coach manager", 451: "partner", 1038: "relative", 1327: "partner", 413: "position played",
|
| 57 |
+
118: "league", 641: "sport", 115: "home stadium ground venue", 361: "part", 150: "contains", 206: "located", 1376: "capital"}
|
| 58 |
+
ALIAS_STEMS = {}
|
| 59 |
+
try:
|
| 60 |
+
ALLOW = set(json.load(open(__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/reader_prop_allowlist.json"))) # relations the admitted relation specialists read
|
| 61 |
+
except Exception: # noqa: BLE001
|
| 62 |
+
ALLOW = set()
|
| 63 |
+
ALLOW -= {102, 1082} # party membership changes; population is a number
|
| 64 |
+
SKIP_PROPS = {21, 971, 735, 734, 1343, 910, 4224, 1889, 31, 279, 373, 646, 214, 227, 244, 213, 268, 269, 349, 434, 435, 436,
|
| 65 |
+
1006, 1015, 1017, 1263, 2671, 3417, 5008, 1424, 1151, 1792, 3876, 361, 527, 5869, 2184, 8402, 1482, 3222}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def norm(s):
|
| 69 |
+
s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode("ascii")
|
| 70 |
+
return " ".join(re.sub(r"[^a-z0-9 ]+", " ", s.lower()).split())
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def stem(w):
|
| 74 |
+
w = w.lower()
|
| 75 |
+
if w.endswith("ied") and len(w) <= 5: # died -> die, lied -> lie
|
| 76 |
+
return w[:-1]
|
| 77 |
+
for suf in ("ations", "ation", "ings", "ing", "ers", "ors", "er", "or", "ed", "es", "s", "ion"):
|
| 78 |
+
if len(w) > len(suf) + 3 and w.endswith(suf):
|
| 79 |
+
return w[: -len(suf)]
|
| 80 |
+
return w
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
_lab, _cls = {}, {}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def label(q):
|
| 87 |
+
if q not in _lab:
|
| 88 |
+
r = db.execute("SELECT label, enwiki FROM entity WHERE qid=?", (q,)).fetchone()
|
| 89 |
+
_lab[q] = (r[0] or r[1]) if r else None
|
| 90 |
+
return _lab[q]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def classes(q):
|
| 94 |
+
if q in _cls:
|
| 95 |
+
return _cls[q]
|
| 96 |
+
out, frontier, seen = set(), [o for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p=31", (q,)) if o], set()
|
| 97 |
+
for d in range(2): # its class and one parent: higher superclasses name things it is not
|
| 98 |
+
nxt = []
|
| 99 |
+
for c in frontier:
|
| 100 |
+
if c in seen:
|
| 101 |
+
continue
|
| 102 |
+
seen.add(c)
|
| 103 |
+
lab = label(c)
|
| 104 |
+
if lab:
|
| 105 |
+
ws = norm(lab).split()
|
| 106 |
+
if ws:
|
| 107 |
+
out.add(ws[-1]) # head noun: "town in Finland" is a town, not a Finland
|
| 108 |
+
if d < 1:
|
| 109 |
+
nxt += [o for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p=279", (c,)) if o]
|
| 110 |
+
frontier = nxt
|
| 111 |
+
_cls[q] = out
|
| 112 |
+
return out
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
_cls0 = {}
|
| 116 |
+
GEO_SYN = {"country": "country state kingdom republic nation monarchy federation empire principality sultanate emirate",
|
| 117 |
+
"city": "city town capital metropolis municipality borough commune", "town": "town city municipality borough commune village",
|
| 118 |
+
"state": "state province", "county": "county shire", "island": "island atoll", "river": "river stream tributary",
|
| 119 |
+
"mountain": "mountain peak volcano massif summit", "lake": "lake reservoir", "sea": "sea", "ocean": "ocean"}
|
| 120 |
+
GEO_SYN = {stem(k): set(stem(x) for x in v.split()) for k, v in GEO_SYN.items()}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def classes0(q):
|
| 124 |
+
"""head nouns of the thing's own classes (P31 only): a city is a city, not the country its classes sit under"""
|
| 125 |
+
if q not in _cls0:
|
| 126 |
+
out = set()
|
| 127 |
+
for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p=31", (q,)):
|
| 128 |
+
lab = label(o) if o else None
|
| 129 |
+
ws = norm(lab).split() if lab else []
|
| 130 |
+
if ws:
|
| 131 |
+
out.add(stem(ws[-1]))
|
| 132 |
+
_cls0[q] = out
|
| 133 |
+
return _cls0[q]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def is_human(q):
|
| 137 |
+
return db.execute("SELECT 1 FROM fact WHERE s=? AND p=31 AND o=5 LIMIT 1", (q,)).fetchone() is not None
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def ent(title):
|
| 141 |
+
t = title.strip(" .,'\"")
|
| 142 |
+
if not t:
|
| 143 |
+
return None
|
| 144 |
+
for v in dict.fromkeys((t, t[:1].upper() + t[1:], " ".join(w[:1].upper() + w[1:] for w in t.split()))):
|
| 145 |
+
r = db.execute("SELECT qid FROM entity WHERE enwiki=? LIMIT 1", (v,)).fetchone()
|
| 146 |
+
if r:
|
| 147 |
+
return r[0]
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def linked(q):
|
| 152 |
+
words = re.findall(r"[A-Za-z0-9'\-\.&]+", q.replace('"', " "))
|
| 153 |
+
taken, out = set(), []
|
| 154 |
+
for n in (6, 5, 4, 3, 2, 1):
|
| 155 |
+
for i in range(len(words) - n + 1):
|
| 156 |
+
if any(j in taken for j in range(i, i + n)):
|
| 157 |
+
continue
|
| 158 |
+
span = words[i:i + n]
|
| 159 |
+
if span[0].lower() in STOP or span[-1].lower() in STOP:
|
| 160 |
+
continue
|
| 161 |
+
if n == 1 and (not span[0][:1].isupper() or i == 0 or len(span[0]) < 3):
|
| 162 |
+
continue
|
| 163 |
+
if all(w.lower() in STOP for w in span):
|
| 164 |
+
continue
|
| 165 |
+
before = words[i - 1] if i > 0 else ""
|
| 166 |
+
after = words[i + n] if i + n < len(words) else ""
|
| 167 |
+
if (after[:1].isupper() and after.lower() not in STOP) or (i > 1 and before[:1].isupper() and before.lower() not in STOP):
|
| 168 |
+
continue # part of a longer name ("Andrew Bonar" in "Andrew Bonar Law")
|
| 169 |
+
qid = ent(" ".join(span))
|
| 170 |
+
if qid:
|
| 171 |
+
out.append((" ".join(span), qid))
|
| 172 |
+
taken.update(range(i, i + n))
|
| 173 |
+
return out
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def asked_type(q):
|
| 177 |
+
ql = q.lower().strip()
|
| 178 |
+
w = re.findall(r"[a-z]+", ql)
|
| 179 |
+
if not w:
|
| 180 |
+
return None, set()
|
| 181 |
+
if w[0] in HUMAN_Q or " who " in " " + ql[:40] or re.search(r"\bto whom\b|\bwhom\b", ql):
|
| 182 |
+
return "human", set()
|
| 183 |
+
m = re.search(r"\b(?:which|what)\s+((?:[a-z\-]+\s+){0,2}?[a-z\-]+)\s*\?\s*$", ql) # "... the sister of which actor?"
|
| 184 |
+
if m:
|
| 185 |
+
nouns = set(stem(x) for x in m.group(1).split() if x not in STOP and x not in GENERIC_TYPE)
|
| 186 |
+
if nouns:
|
| 187 |
+
return "noun", nouns
|
| 188 |
+
m = re.match(r"(?:in )?(?:which|what) (city|town|country|state|county|island|river|mountain|lake|sea|ocean)\b", ql)
|
| 189 |
+
if m:
|
| 190 |
+
return "noun", {stem(m.group(1))}
|
| 191 |
+
if w[0] == "where":
|
| 192 |
+
return "place", set()
|
| 193 |
+
if w[0] == "when" or re.match(r"(in )?(what|which) year", ql):
|
| 194 |
+
return "time", set()
|
| 195 |
+
if re.match(r"how (many|much|long|old|far|tall|high)", ql):
|
| 196 |
+
return "number", set()
|
| 197 |
+
m = re.match(r"(?:what|which|who) (?:is|was|are|were) the ([a-z\-]+(?: [a-z\-]+)?) of\b", ql) # "what is the capital of ..."
|
| 198 |
+
if m:
|
| 199 |
+
head = m.group(1).split()[-1]
|
| 200 |
+
if head not in STOP and head not in GENERIC_TYPE:
|
| 201 |
+
return "rel", {stem(head)}
|
| 202 |
+
m = re.search(r"\b(which|what)\s+((?:[a-z\-]+\s+){0,3}?)([a-z\-]+)\s+(?:" + "|".join(AUX) + r"|[a-z]+ed|[a-z]+s)\b", ql)
|
| 203 |
+
if m:
|
| 204 |
+
nouns = set(stem(x) for x in (m.group(2) + " " + m.group(3)).split() if x not in STOP and x not in GENERIC_TYPE)
|
| 205 |
+
if nouns:
|
| 206 |
+
return "noun", nouns
|
| 207 |
+
return None, set()
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
_pw = {}
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def prop_words(p):
|
| 214 |
+
if p not in _pw:
|
| 215 |
+
s = set()
|
| 216 |
+
for name in PROPNAMES.get(p, [PROP.get(p, "")]):
|
| 217 |
+
ws = [stem(x) for x in norm(name).split() if x not in STOP and len(x) > 2]
|
| 218 |
+
if 0 < len(ws) <= 3: # long descriptive aliases are not cues
|
| 219 |
+
s.update(ws)
|
| 220 |
+
_pw[p] = s
|
| 221 |
+
return _pw[p]
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# words too loose to name a relation on their own (dev 10-14): "wrote" fits author, lyricist and publisher; "song" fits lyricist;
|
| 225 |
+
# "found" (in "found in the museum") stems like "founded"
|
| 226 |
+
STOPCUE = {stem(x) for x in ("write wrote written writ song songs theme found find make made play played sing sang known book books "
|
| 227 |
+
"music musical film films movie movies national").split()}
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def cued(p, qwords_stem, qwords):
|
| 231 |
+
qwords_stem = qwords_stem - STOPCUE
|
| 232 |
+
if prop_words(p) & qwords_stem:
|
| 233 |
+
return True
|
| 234 |
+
if p not in ALIAS_STEMS:
|
| 235 |
+
ALIAS_STEMS[p] = set(stem(x) for x in ALIASES.get(p, "").split())
|
| 236 |
+
if ALIAS_STEMS[p] & qwords_stem:
|
| 237 |
+
return True
|
| 238 |
+
return False # cue pairs dropped: "which|city" is not a relation (dev 10-14)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
FEMININE = {"actress": "actor", "authoress": "author", "poetess": "poet", "comedienne": "comedian", "sculptress": "sculptor",
|
| 242 |
+
"heroine": "hero", "princess": "prince", "duchess": "duke", "empress": "emperor", "songstress": "singer"}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def type_ok(kind, nouns, cand_qid, lit, p=None):
|
| 246 |
+
if kind == "rel":
|
| 247 |
+
# the relation noun itself ("capital") must be the property: named by the property's own label or aliases
|
| 248 |
+
return p is not None and bool(prop_words(p) & nouns) and (cand_qid is not None or lit is not None)
|
| 249 |
+
if kind == "time":
|
| 250 |
+
return lit is not None and bool(re.search(r"\b1?\d{3,4}\b", str(lit)))
|
| 251 |
+
if cand_qid is None:
|
| 252 |
+
return False
|
| 253 |
+
if kind == "human":
|
| 254 |
+
return is_human(cand_qid)
|
| 255 |
+
if kind == "place":
|
| 256 |
+
return bool(classes(cand_qid) & PLACE_WORDS)
|
| 257 |
+
if kind == "noun":
|
| 258 |
+
nouns = set(stem(FEMININE.get(x, x)) for x in nouns) | nouns
|
| 259 |
+
cw = set(stem(x) for x in classes(cand_qid))
|
| 260 |
+
if "person" in nouns or "man" in nouns or "woman" in nouns:
|
| 261 |
+
return is_human(cand_qid)
|
| 262 |
+
geo = [n for n in nouns if n in GEO_SYN]
|
| 263 |
+
if geo: # "which country": Nairobi is a city (held-out 10-14: Peter Hain)
|
| 264 |
+
return any(classes0(cand_qid) & GEO_SYN[n] for n in geo)
|
| 265 |
+
return bool(cw & nouns) or (is_human(cand_qid) and bool(nouns & occupations(cand_qid)))
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
_occ = {}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def occupations(q):
|
| 273 |
+
if q not in _occ:
|
| 274 |
+
s = set()
|
| 275 |
+
for (o,) in db.execute("SELECT o FROM fact WHERE s=? AND p IN (106, 39) LIMIT 30", (q,)):
|
| 276 |
+
if o and label(o):
|
| 277 |
+
ws = norm(label(o)).split()
|
| 278 |
+
if ws:
|
| 279 |
+
s.add(stem(ws[-1])) # the head noun only: "film score composer" is a composer
|
| 280 |
+
_occ[q] = s
|
| 281 |
+
return _occ[q]
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
_gen = {}
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def generic(e):
|
| 288 |
+
"""a class or concept, not a particular thing: it is a subclass of something, or many things are instances of it"""
|
| 289 |
+
if e not in _gen:
|
| 290 |
+
sub = db.execute("SELECT 1 FROM fact WHERE s=? AND p=279 LIMIT 1", (e,)).fetchone() is not None
|
| 291 |
+
inst = len(db.execute("SELECT s FROM fact WHERE p=31 AND o=? LIMIT 30", (e,)).fetchall()) >= 30
|
| 292 |
+
occ = len(db.execute("SELECT s FROM fact WHERE p=106 AND o=? LIMIT 30", (e,)).fetchall()) >= 30
|
| 293 |
+
_gen[e] = sub or inst or occ
|
| 294 |
+
return _gen[e]
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
_word_props = None
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def inv_props(qwords_stem, qwords):
|
| 301 |
+
"""properties whose label/alias words or cue pairs appear in the question (index built once; was a scan of 38k per entity)"""
|
| 302 |
+
global _word_props
|
| 303 |
+
if _word_props is None:
|
| 304 |
+
_word_props = {}
|
| 305 |
+
for p in set(PROPNAMES) | set(PROP):
|
| 306 |
+
if p in SKIP_PROPS:
|
| 307 |
+
continue
|
| 308 |
+
for w in prop_words(p) | set(stem(x) for x in ALIASES.get(p, "").split()):
|
| 309 |
+
_word_props.setdefault(w, set()).add(p)
|
| 310 |
+
ps = set()
|
| 311 |
+
for w in qwords_stem - STOPCUE:
|
| 312 |
+
ps |= _word_props.get(w, set())
|
| 313 |
+
ps = [p for p in ps if db.execute("SELECT 1 FROM fact WHERE p=? LIMIT 1", (p,)).fetchone()]
|
| 314 |
+
return ps[:60]
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
SHORT_NAMES = {"uk": 145, "u k": 145, "britain": 145, "great britain": 145, "us": 30, "u s": 30, "usa": 30, "u s a": 30,
|
| 318 |
+
"america": 30, "ussr": 15180, "soviet union": 15180, "uae": 878, "eu": 458, "holland": 55}
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def echoed(q, val, ans):
|
| 322 |
+
"""the answer is already in the question: by its label, or by a common short name (dev-burned held-out 10-14: "Where
|
| 323 |
+
are Akrotiri and Dhekelia, allocated by treaty to the UK" read the holder, the UK, as the place)"""
|
| 324 |
+
qn = " %s " % norm(q)
|
| 325 |
+
words = [w for w in norm(ans).split() if w not in STOP and w not in ("the", "of")]
|
| 326 |
+
if words and all(" %s " % w in qn for w in words):
|
| 327 |
+
return True
|
| 328 |
+
return val is not None and any(" %s " % k in qn and v == val for k, v in SHORT_NAMES.items())
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
QWORDS = set("who whom whose what which where when why how in on at of the a an is was were are did does do name".split())
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def unlinked_name(q, links):
|
| 335 |
+
"""a capitalised name or a quoted title in the question that no linked span covers"""
|
| 336 |
+
covered = set()
|
| 337 |
+
for t, _ in links:
|
| 338 |
+
covered.update(w.lower() for w in re.findall(r"[A-Za-z0-9']+", t))
|
| 339 |
+
for quoted in re.findall(r"[\"“]([^\"”]{2,80})[\"”]", q):
|
| 340 |
+
if not any(quoted.lower() in t.lower() or t.lower() in quoted.lower() for t, _ in links):
|
| 341 |
+
return True
|
| 342 |
+
words = re.findall(r"[A-Za-z][A-Za-z0-9'\-]*", q)
|
| 343 |
+
for i, w in enumerate(words):
|
| 344 |
+
if i == 0 or not w[:1].isupper() or w.lower() in QWORDS or w.lower() in STOP:
|
| 345 |
+
continue
|
| 346 |
+
if w.lower() not in covered:
|
| 347 |
+
return True
|
| 348 |
+
return False
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def answer(q):
|
| 352 |
+
if QUALIFIER.search(q):
|
| 353 |
+
return None
|
| 354 |
+
kind, nouns = asked_type(q)
|
| 355 |
+
if kind in (None, "number"):
|
| 356 |
+
return None
|
| 357 |
+
ents = [(t, e) for t, e in linked(q) if not generic(e)]
|
| 358 |
+
if not ents:
|
| 359 |
+
return None
|
| 360 |
+
if unlinked_name(q, linked(q)):
|
| 361 |
+
return None # held-out 10-14: Jennings Brewery -> read Cumbria; Dismaland -> Banksy
|
| 362 |
+
if q.count(",") >= 2 or (re.search(r"\band\b", q) and len(ents) >= 2):
|
| 363 |
+
return None # several clues joined: one fact cannot answer it
|
| 364 |
+
if kind == "rel":
|
| 365 |
+
m = re.search(r"\bof (?:the )?(.+?)(?:\?|$| in | on | at )", q)
|
| 366 |
+
if not m:
|
| 367 |
+
return None
|
| 368 |
+
first = [(t, e) for t, e in ents if m.group(1).lower().startswith(t.lower())]
|
| 369 |
+
if not first:
|
| 370 |
+
return None
|
| 371 |
+
ents = first[:1] # "the capital of the Yukon Territory in Canada": the Yukon, not Canada
|
| 372 |
+
named = {qid for _, qid in ents}
|
| 373 |
+
qwords = set(re.findall(r"[a-z]+", q.lower()))
|
| 374 |
+
qwords_stem = set(stem(x) for x in qwords if x not in STOP)
|
| 375 |
+
if kind == "noun":
|
| 376 |
+
qwords_stem -= nouns # the answer-type word is not the relation ("which country ... headquarters")
|
| 377 |
+
cands = {}
|
| 378 |
+
for span, e in ents:
|
| 379 |
+
for p, o, lit in db.execute("SELECT p, o, lit FROM fact WHERE s=? LIMIT 4000", (e,)):
|
| 380 |
+
if p in SKIP_PROPS or p not in ALLOW or (o is not None and o in named):
|
| 381 |
+
continue
|
| 382 |
+
if o is not None and (label(o) or "").startswith("Category:"):
|
| 383 |
+
continue
|
| 384 |
+
if not cued(p, qwords_stem, qwords):
|
| 385 |
+
continue
|
| 386 |
+
if type_ok(kind, nouns, o, lit, p):
|
| 387 |
+
key = ("q", o) if o is not None else ("l", str(lit))
|
| 388 |
+
cands.setdefault(key, []).append((e, p, "fwd"))
|
| 389 |
+
for p in [x for x in inv_props(qwords_stem, qwords) if x in ALLOW]:
|
| 390 |
+
# reverse answers only when the relation's MAIN name is in the question: aliases such as "mother of" on the
|
| 391 |
+
# child relation describe the relation backwards (dev 10-14: "mother of Achilles")
|
| 392 |
+
main = set(stem(x) for x in norm(PROP.get(p, "")).split() if x not in STOP and len(x) > 2)
|
| 393 |
+
if not (main & qwords_stem):
|
| 394 |
+
continue
|
| 395 |
+
subs = [s for (s,) in db.execute("SELECT s FROM fact WHERE p=? AND o=? LIMIT 6", (p, e))]
|
| 396 |
+
if len(subs) > 5: # a big group (everyone who received an award): no reverse answer
|
| 397 |
+
continue
|
| 398 |
+
for s in subs:
|
| 399 |
+
if s in named:
|
| 400 |
+
continue
|
| 401 |
+
if type_ok(kind, nouns, s, None, p):
|
| 402 |
+
cands.setdefault(("q", s), []).append((e, p, "inv"))
|
| 403 |
+
if len(cands) != 1:
|
| 404 |
+
return None
|
| 405 |
+
(kt, val), why = next(iter(cands.items()))
|
| 406 |
+
if len(ents) >= 2 and len({w[0] for w in why}) < 2: # two or more named things: at least two must agree (held-out
|
| 407 |
+
return None # 10-14: Jennings Brewery -> Cumbria's capital, Dismaland -> Banksy)
|
| 408 |
+
if False:
|
| 409 |
+
return None
|
| 410 |
+
e, p, d = why[0]
|
| 411 |
+
ans = label(val) if kt == "q" else val
|
| 412 |
+
if not ans:
|
| 413 |
+
return None
|
| 414 |
+
if echoed(q, val if kt == "q" else None, ans):
|
| 415 |
+
return None # the question already names it ("allocated to the UK": not where it is)
|
| 416 |
+
if d == "fwd":
|
| 417 |
+
receipt = "%s (Q%d) - %s (P%d) - %s" % (label(e), e, PROP.get(p, ""), p, ("%s (Q%d)" % (ans, val)) if kt == "q" else ans)
|
| 418 |
+
else:
|
| 419 |
+
receipt = "%s (Q%d) - %s (P%d) - %s (Q%d)" % (ans, val, PROP.get(p, ""), p, label(e), e)
|
| 420 |
+
return {"answer": ans, "receipt": receipt, "prop": p, "dir": d, "kind": kind, "agree": len(why)}
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def correct(ans, golds):
|
| 424 |
+
"""exact match after normalising, or one contains the other as whole words ("Lake Mead" / "Mead", "The Dirham" /
|
| 425 |
+
"United Arab Emirates dirham"); articles dropped"""
|
| 426 |
+
drop = lambda s: " ".join(w for w in norm(s).split() if w not in ("the", "a", "an"))
|
| 427 |
+
a = drop(ans)
|
| 428 |
+
for g in (drop(x) for x in golds if x):
|
| 429 |
+
if not g:
|
| 430 |
+
continue
|
| 431 |
+
if a == g:
|
| 432 |
+
return True
|
| 433 |
+
if len(g) >= 4 and re.search(r"\b%s\b" % re.escape(g), a):
|
| 434 |
+
return True
|
| 435 |
+
if len(a) >= 4 and re.search(r"\b%s\b" % re.escape(a), g):
|
| 436 |
+
return True
|
| 437 |
+
return False
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
if __name__ == "__main__":
|
| 441 |
+
if sys.argv[1] == "ask":
|
| 442 |
+
print(json.dumps(answer(sys.argv[2]), indent=1))
|
| 443 |
+
sys.exit()
|
| 444 |
+
import multiprocessing as mp
|
| 445 |
+
|
| 446 |
+
def one(item):
|
| 447 |
+
q, golds = item
|
| 448 |
+
try:
|
| 449 |
+
r = answer(q)
|
| 450 |
+
except Exception as e: # noqa: BLE001
|
| 451 |
+
return ("err", q[:80], str(e)[:80])
|
| 452 |
+
if not r:
|
| 453 |
+
return None
|
| 454 |
+
return ("right" if correct(r["answer"], golds) else "wrong", q[:110], r["answer"], golds[:2], r["receipt"][:130], r["kind"])
|
| 455 |
+
|
| 456 |
+
items = [it for f in sys.argv[2:] for it in json.load(open(f))]
|
| 457 |
+
cnt = {"right": 0, "wrong": 0, "err": 0}
|
| 458 |
+
rows = []
|
| 459 |
+
with mp.Pool(6) as pool:
|
| 460 |
+
for i, res in enumerate(pool.imap_unordered(one, items, chunksize=4), 1):
|
| 461 |
+
if res:
|
| 462 |
+
cnt[res[0]] += 1
|
| 463 |
+
rows.append(res)
|
| 464 |
+
if i % 250 == 0:
|
| 465 |
+
print("progress %d/%d right %d wrong %d" % (i, len(items), cnt["right"], cnt["wrong"]), flush=True)
|
| 466 |
+
fired = cnt["right"] + cnt["wrong"]
|
| 467 |
+
print("questions %d fired %d right %d wrong %d errors %d precision %.3f" % (len(items), fired, cnt["right"], cnt["wrong"], cnt["err"], cnt["right"] / max(1, fired)))
|
| 468 |
+
for r in rows:
|
| 469 |
+
if r[0] != "right":
|
| 470 |
+
print(" ", r[0].upper(), r[1:])
|
| 471 |
+
json.dump(rows, open("/root/w240/coverage_eval_last.json", "w"), indent=0)
|
root/wander-train/math_organ_v2.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
exact mathematics as specified free moves, every result independently checked, the method cited.
|
| 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 independent of the move that produced the answer:
|
| 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
|
| 13 |
+
evaluate recomputed by direct numeric substitution
|
| 14 |
+
derivative agrees with a central difference at random points
|
| 15 |
+
integral differentiates back to the integrand (definite: numeric quadrature agrees)
|
| 16 |
+
limit agrees with evaluation near the point
|
| 17 |
+
STORED FORM the textbook's own method for the directive (a howto or a worked example, textbook_store_v1) is cited
|
| 18 |
+
Nothing is asserted unless the check passes; a failed check is an abstention with the reason."""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import random
|
| 22 |
+
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 |
+
|
| 29 |
+
STORE = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/textbooks/textbook_store_v1.sqlite3"
|
| 30 |
+
FUNCS = {"sin", "cos", "tan", "sec", "csc", "cot", "log", "ln", "exp", "sqrt", "root", "asin", "acos", "atan", "abs",
|
| 31 |
+
"sinh", "cosh", "tanh", "pi", "oo", "theta", "alpha", "beta", "gamma", "Delta", "lambda", "mu", "sigma",
|
| 32 |
+
"omega", "phi", "rho", "tau", "epsilon", "arcsin", "arccos", "arctan"}
|
| 33 |
+
LOCAL = {"e": sp.E, "pi": sp.pi, "ln": sp.log, "log": sp.log, "root": lambda x, n: sp.root(x, n), "oo": sp.oo,
|
| 34 |
+
"arcsin": sp.asin, "arccos": sp.acos, "arctan": sp.atan, "abs": sp.Abs, "theta": sp.Symbol("theta")}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _split_ok(name):
|
| 38 |
+
return name not in FUNCS and not name.startswith(("theta", "alpha", "beta", "gamma", "Delta", "lambda", "omega"))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
TRANSF = standard_transformations + (split_symbols_custom(_split_ok), implicit_multiplication_application, convert_xor)
|
| 42 |
+
DIRECTIVES = [
|
| 43 |
+
("partial", r"partial fraction|decomposition"),
|
| 44 |
+
("derivative", r"\bderivative|differentiate|\bf'\(|d/dx|dy/dx"),
|
| 45 |
+
("integral", r"\bintegra(l|te)|antiderivative"),
|
| 46 |
+
("limit", r"\blimit|\blim_"),
|
| 47 |
+
("factor", r"\bfactor(?!ial)"), # v2: "factorial" is not a request to factor
|
| 48 |
+
("solve", r"\bsolve|\bsolution|find (?:the )?(?:value|zeros?|roots?)\b|\bfind x\b"),
|
| 49 |
+
("evaluate", r"\bevaluate|\bfind f\(|\bcompute|\bcalculate|\bwhat is\b|\bhow much is\b"),
|
| 50 |
+
("expand", r"\bmultiply|\bexpand|\bdistributive"),
|
| 51 |
+
("simplify", r"\bsimplif|\badd\b|\bsubtract|\bdivide|\bcombine|\breduce"),
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def clean_linear(s):
|
| 56 |
+
s = s.replace("−", "-").replace("×", "*").replace("·", "*").replace("÷", "/").replace("^", "**")
|
| 57 |
+
s = re.sub(r"(\d),(\d{3})\b", r"\1\2", s)
|
| 58 |
+
s = re.sub(r"\s+", " ", s).strip().rstrip(".?,;")
|
| 59 |
+
return s
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def P(s, syms=None):
|
| 63 |
+
s = clean_linear(s)
|
| 64 |
+
s = re.sub(r"\bsqrt\s*\(", "sqrt(", s)
|
| 65 |
+
return parse_expr(s, local_dict=dict(LOCAL, **(syms or {})), transformations=TRANSF, evaluate=True)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def directive_of(text):
|
| 69 |
+
t = (text or "").lower()
|
| 70 |
+
for name, rx in DIRECTIVES:
|
| 71 |
+
if re.search(rx, t):
|
| 72 |
+
return name
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
ENGLISH = {"of", "to", "is", "by", "at", "in", "on", "if", "an", "as", "the", "and", "for", "use", "let", "all", "are",
|
| 77 |
+
"its", "how", "not", "or", "be", "it", "we", "find", "when", "each", "given", "then", "from", "with", "that",
|
| 78 |
+
"this", "what", "which", "where", "your", "you", "into", "using", "value", "values", "following", "exercises",
|
| 79 |
+
"expression", "expressions", "equation", "equations", "simplify", "solve", "factor", "evaluate", "multiply",
|
| 80 |
+
"divide", "add", "subtract", "completely", "polynomial", "form", "write", "answer", "result", "greatest",
|
| 81 |
+
"common", "number", "numbers", "term", "terms", "exact", "calculator", "round", "rule", "any", "one", "two",
|
| 82 |
+
"has", "can", "our", "was", "per", "sum", "set", "get", "may", "see", "new", "own", "way", "out"}
|
| 83 |
+
TOK = re.compile(r"[A-Za-z]+|\d+(?:\.\d+)?|\*\*|<=|>=|!=|->|\+-|[-+*/^=<>(){}\[\],|_;'!]|\.(?=\d)|\.|\s+")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _is_math_word(w, prev, nxt):
|
| 87 |
+
lw = w.lower()
|
| 88 |
+
if lw in FUNCS or w == "C":
|
| 89 |
+
return True
|
| 90 |
+
if lw in ENGLISH:
|
| 91 |
+
return False
|
| 92 |
+
if len(w) == 1:
|
| 93 |
+
return not (w in ("a", "A", "I") and nxt.isspace() and prev in ("", " "))
|
| 94 |
+
return len(w) <= 3 # implicit products "xy", "ah", "abc"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
FUNC_RX = r"(sqrt|sin|cos|tan|sec|csc|cot|log|ln|exp)"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def normalize_request(text):
|
| 101 |
+
"""surface conventions -> linear mathematics: '2xsqrt(48)' -> '2x sqrt(48)', degrees, log bases, imaginary unit"""
|
| 102 |
+
t = text or ""
|
| 103 |
+
t = re.sub(r"(?<=[a-z0-9)])%s(?=[\s(])" % FUNC_RX, r" \1", t) # xsqrt( -> x sqrt(
|
| 104 |
+
t = re.sub(r"(\d+(?:\.\d+)?)\s*°", r"(\1*pi/180)", t) # 120° -> radians
|
| 105 |
+
t = re.sub(r"\blog\s*_\s*\{?(\d+)\}?\s*\(?([^()\s]+)\)?", r"log(\2,\1)", t) # log_5 25 -> log(25,5)
|
| 106 |
+
if re.search(r"\bcomplex|\bimaginary|\d\s*i\b|[-+]\s*\d*\s*i\)", t, re.I):
|
| 107 |
+
t = re.sub(r"(?<![A-Za-z])i(?![A-Za-z])", "I", t) # the imaginary unit
|
| 108 |
+
return t
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
WORD_PROBLEM_WORDS = 7
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def english_load(text):
|
| 115 |
+
words = [w for w in re.findall(r"[A-Za-z]{3,}", text or "") if w.lower() not in ENGLISH and w.lower() not in FUNCS]
|
| 116 |
+
return len(words)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def extract_math(text):
|
| 120 |
+
"""maximal runs of mathematical tokens (numbers, operators, brackets, variables, function names); English, a colon
|
| 121 |
+
and a sentence end break a run"""
|
| 122 |
+
toks = [m.group(0) for m in TOK.finditer((text or "").replace(":", " . "))]
|
| 123 |
+
spans, cur = [], []
|
| 124 |
+
for i, t in enumerate(toks):
|
| 125 |
+
prev = toks[i - 1] if i else ""
|
| 126 |
+
nxt = toks[i + 1] if i + 1 < len(toks) else ""
|
| 127 |
+
ok = _is_math_word(t, prev, nxt) if t.isalpha() else (t != ".")
|
| 128 |
+
if ok:
|
| 129 |
+
cur.append(t)
|
| 130 |
+
else:
|
| 131 |
+
if cur:
|
| 132 |
+
spans.append("".join(cur))
|
| 133 |
+
cur = []
|
| 134 |
+
if cur:
|
| 135 |
+
spans.append("".join(cur))
|
| 136 |
+
out = []
|
| 137 |
+
for s in spans:
|
| 138 |
+
s = s.strip(" ,;:")
|
| 139 |
+
s = re.sub(r"\|\s*([^|]+?)\s*\|", r"Abs(\1)", s)
|
| 140 |
+
if s and (re.search(r"[0-9]", s) or re.search(r"[=^+*/<>()]", s)) and re.search(r"[a-z0-9]", s):
|
| 141 |
+
out.append(s)
|
| 142 |
+
return out
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def assignments(text):
|
| 146 |
+
"""'when a=-4, b=6' / 'for x = 2 and y = 3' -> {a: -4, b: 6}"""
|
| 147 |
+
out = {}
|
| 148 |
+
m = re.search(r"\b(?:when|for|if|with|where|at)\s+((?:[a-z]\s*=\s*[-+]?[0-9./()]+\s*(?:,|\band\b)?\s*)+)", text or "")
|
| 149 |
+
if m:
|
| 150 |
+
for v, val in re.findall(r"\b([a-z])\s*=\s*([-+]?[0-9./()]+)", m.group(1)):
|
| 151 |
+
try:
|
| 152 |
+
out[sp.Symbol(v)] = P(val)
|
| 153 |
+
except Exception: # noqa: BLE001
|
| 154 |
+
pass
|
| 155 |
+
return out
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def target_var(text):
|
| 159 |
+
m = re.search(r"\bfor\s+([a-z])\b(?!\s*=)", text or "")
|
| 160 |
+
return sp.Symbol(m.group(1)) if m else None
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def rounding(text):
|
| 164 |
+
t = (text or "").lower()
|
| 165 |
+
m = re.search(r"round(?:ed)?\s+(?:your answer\s+)?to\s+(?:the\s+nearest\s+)?(\w+)", t)
|
| 166 |
+
if not m:
|
| 167 |
+
return None
|
| 168 |
+
return {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "tenth": 1, "hundredth": 2, "thousandth": 3,
|
| 169 |
+
"whole": 0, "integer": 0, "1": 1, "2": 2, "3": 3, "4": 4}.get(m.group(1))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def binary_phrase(text):
|
| 173 |
+
"""'the difference of A and B' -> (A)-(B); sum / product / quotient likewise; 'subtract A from B' -> (B)-(A)"""
|
| 174 |
+
m = re.search(r"\bsubtract\s+(.+?)\s+from\s+(.+?)(?:\.\s|\.$|$)", text or "", re.I)
|
| 175 |
+
if m:
|
| 176 |
+
a, b = extract_math(m.group(1)), extract_math(m.group(2))
|
| 177 |
+
return "(%s)-(%s)" % (b[0], a[-1]) if a and b else None
|
| 178 |
+
m = re.search(r"\b(sum|difference|product|quotient) of (.+?) and (.+?)(?:\.\s|\.$|$)", text or "", re.I)
|
| 179 |
+
if not m:
|
| 180 |
+
return None
|
| 181 |
+
op = {"sum": "+", "difference": "-", "product": "*", "quotient": "/"}[m.group(1).lower()]
|
| 182 |
+
a, b = extract_math(m.group(2)), extract_math(m.group(3))
|
| 183 |
+
return "(%s)%s(%s)" % (a[-1], op, b[0]) if a and b else None
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def complexity(e):
|
| 187 |
+
return sp.count_ops(e, visual=False) + len(str(e)) / 40.0
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def rand_pts(syms, k=5):
|
| 191 |
+
rng = random.Random(7)
|
| 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 equiv(a, b):
|
| 196 |
+
try:
|
| 197 |
+
d = sp.simplify(a - b)
|
| 198 |
+
if d == 0:
|
| 199 |
+
return True
|
| 200 |
+
syms = sorted((a.free_symbols | b.free_symbols), key=str)
|
| 201 |
+
for pt in rand_pts(syms):
|
| 202 |
+
va, vb = complex(a.subs(pt).evalf()), complex(b.subs(pt).evalf())
|
| 203 |
+
if abs(va - vb) > 1e-7 * max(1.0, abs(va)):
|
| 204 |
+
return False
|
| 205 |
+
return True
|
| 206 |
+
except Exception: # noqa: BLE001
|
| 207 |
+
return False
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class MathOrgan:
|
| 211 |
+
def __init__(self, store=STORE):
|
| 212 |
+
try:
|
| 213 |
+
self.db = sqlite3.connect("file:%s?mode=ro" % store, uri=True, check_same_thread=False)
|
| 214 |
+
except Exception: # noqa: BLE001
|
| 215 |
+
self.db = None
|
| 216 |
+
self.why = ""
|
| 217 |
+
|
| 218 |
+
# ------------------------------------------------------------------ the stored form (method citation)
|
| 219 |
+
def method(self, directive, hint=""):
|
| 220 |
+
if not self.db:
|
| 221 |
+
return None
|
| 222 |
+
words = {"solve": "solve equation", "simplify": "simplify expression", "factor": "factor", "expand": "multiply",
|
| 223 |
+
"evaluate": "evaluate", "derivative": "derivative", "integral": "integral", "limit": "limit",
|
| 224 |
+
"partial": "partial fraction decomposition"}[directive]
|
| 225 |
+
q = " OR ".join(w for w in words.split())
|
| 226 |
+
try:
|
| 227 |
+
r = self.db.execute("SELECT item.book, item.section, item.title, item.kind, item.id FROM item_fts JOIN item "
|
| 228 |
+
"ON item.id = item_fts.rowid WHERE item_fts MATCH ? AND item.kind IN ('howto','example') "
|
| 229 |
+
"ORDER BY (item.kind='howto') DESC, rank LIMIT 1", ("title:(%s)" % q,)).fetchone()
|
| 230 |
+
except Exception: # noqa: BLE001
|
| 231 |
+
r = None
|
| 232 |
+
return {"book": r[0], "section": r[1], "title": r[2], "kind": r[3], "id": "tb:%d" % r[4]} if r else None
|
| 233 |
+
|
| 234 |
+
# ------------------------------------------------------------------ the solve
|
| 235 |
+
def answer(self, question, instruction=None):
|
| 236 |
+
self.why = ""
|
| 237 |
+
# v2: a request for a PROGRAM ("write a Python function that returns the factorial ...") is the code organ's
|
| 238 |
+
if re.search(r"\b(?:write|make|create|implement|code)\b.*\b(?:function|program|script|method)\b", question or "", re.I):
|
| 239 |
+
self.why = "a request for a program, not for a mathematical result"
|
| 240 |
+
return None
|
| 241 |
+
# v1 reads LINEAR notation (the textbook store's form, and what people type). Typeset LaTeX ($...$, \frac,
|
| 242 |
+
# \sqrt{}) is declined until a LaTeX reader exists: reading it with the linear reader would be luck, not reading.
|
| 243 |
+
if re.search(r"\$|\\(?:frac|sqrt|cdot|left|right|begin|dfrac|tfrac|pi|times)\b", question or ""):
|
| 244 |
+
self.why = "typeset (LaTeX) mathematics: this organ reads linear notation only"
|
| 245 |
+
return None
|
| 246 |
+
question = normalize_request(question)
|
| 247 |
+
d = directive_of(question) or directive_of(instruction or "")
|
| 248 |
+
# a WORD PROBLEM needs language -> equations (a later organ); reading numbers out of prose is guessing
|
| 249 |
+
body = question[len(instruction):] if instruction and question.startswith(instruction) else question
|
| 250 |
+
if english_load(body) > WORD_PROBLEM_WORDS:
|
| 251 |
+
self.why = "a word problem: the quantities must be translated into equations first"
|
| 252 |
+
return None
|
| 253 |
+
bp = binary_phrase(question)
|
| 254 |
+
spans = [bp] if bp else extract_math(question)
|
| 255 |
+
# a request whose only mathematics is a bare number names an operation this organ does not hold
|
| 256 |
+
# ("the lcm of 73 and 230", "452 in base 7", "803 in roman numerals"): echoing the number would be a guess
|
| 257 |
+
if not any(re.search(r"[-+*/^=<>()]|[a-z]", s_) for s_ in spans):
|
| 258 |
+
self.why = "no expression to operate on: the request names an operation this organ does not hold"
|
| 259 |
+
return None
|
| 260 |
+
if re.search(r"\b(?:lcm|gcd|gcf|base\s+\d|roman|numerals?|binary|hexadecimal|octal|convert|km|miles?|yards?|feet|"
|
| 261 |
+
r"inch(?:es)?|meters?|kilograms?|pounds?|percent|%)\b", question, re.I) and d in ("evaluate", "simplify"):
|
| 262 |
+
self.why = "a named operation or unit this organ does not hold"
|
| 263 |
+
return None
|
| 264 |
+
defined = {m.group(1) for s_ in spans for m in [re.match(r"^\s*([a-zA-Z])\s*\(\s*[a-z]\s*\)\s*=", s_)] if m}
|
| 265 |
+
used = {m.group(1) for s_ in spans for m in re.finditer(r"(?<![A-Za-z])([fghFGHpqr])\(\s*[-a-z0-9]", s_)}
|
| 266 |
+
if used - defined:
|
| 267 |
+
self.why = "the request uses a function it does not define"
|
| 268 |
+
return None
|
| 269 |
+
if not spans:
|
| 270 |
+
self.why = "no mathematics in the request"
|
| 271 |
+
return None
|
| 272 |
+
if not d:
|
| 273 |
+
d = "solve" if any("=" in s or "<" in s or ">" in s for s in spans) else "simplify"
|
| 274 |
+
try:
|
| 275 |
+
res = getattr(self, "_" + d)(spans, question)
|
| 276 |
+
except Exception as e: # noqa: BLE001
|
| 277 |
+
self.why = "%s failed: %s" % (d, repr(e)[:80])
|
| 278 |
+
return None
|
| 279 |
+
if not res:
|
| 280 |
+
self.why = self.why or "%s: no checked result" % d
|
| 281 |
+
return None
|
| 282 |
+
nd = rounding(question)
|
| 283 |
+
if nd is not None and isinstance(res.get("value"), sp.Basic) and res["value"].is_number:
|
| 284 |
+
v = round(float(res["value"].evalf()), nd)
|
| 285 |
+
res["value"], res["answer"] = sp.Float(v), ("%d" % v if nd == 0 else ("%." + str(nd) + "f") % v)
|
| 286 |
+
res["steps"] = res.get("steps", []) + ["round to %d decimal place%s" % (nd, "" if nd == 1 else "s")]
|
| 287 |
+
res["directive"] = d
|
| 288 |
+
res["method"] = self.method(d)
|
| 289 |
+
return res
|
| 290 |
+
|
| 291 |
+
# --- solve: equations, inequalities, systems ------------------------------------------------------------------
|
| 292 |
+
def _solve(self, spans, q):
|
| 293 |
+
eqs = []
|
| 294 |
+
for s in spans:
|
| 295 |
+
for part in re.split(r";|\band\b", s):
|
| 296 |
+
part = part.strip()
|
| 297 |
+
m = re.match(r"^(.*?)(<=|>=|<|>|=)(.*)$", part)
|
| 298 |
+
if not m:
|
| 299 |
+
continue
|
| 300 |
+
lhs, op, rhs = m.groups()
|
| 301 |
+
if not lhs.strip() or not rhs.strip():
|
| 302 |
+
continue
|
| 303 |
+
L_, R_ = P(lhs), P(rhs)
|
| 304 |
+
eqs.append((L_, op, R_))
|
| 305 |
+
if not eqs:
|
| 306 |
+
return None
|
| 307 |
+
syms = sorted(set().union(*[(a.free_symbols | b.free_symbols) for a, _, b in eqs]), key=str)
|
| 308 |
+
tv = target_var(q)
|
| 309 |
+
if tv is not None and tv in syms:
|
| 310 |
+
syms = [tv]
|
| 311 |
+
elif len(eqs) < len(syms):
|
| 312 |
+
self.why = "more unknowns than equations and no variable named"
|
| 313 |
+
return None
|
| 314 |
+
if not syms or len(syms) > 3:
|
| 315 |
+
return None
|
| 316 |
+
if all(op == "=" for _, op, _ in eqs):
|
| 317 |
+
sols = sp.solve([sp.Eq(a, b) for a, _, b in eqs], syms, dict=True)
|
| 318 |
+
if not sols:
|
| 319 |
+
self.why = "no solution found"
|
| 320 |
+
return None
|
| 321 |
+
for sol in sols: # CHECK: substitute back
|
| 322 |
+
for a, _, b in eqs:
|
| 323 |
+
if not equiv(a.subs(sol), b.subs(sol)):
|
| 324 |
+
self.why = "substitution check failed"
|
| 325 |
+
return None
|
| 326 |
+
out = [", ".join("%s = %s" % (k, sp.sstr(v)) for k, v in sorted(sol.items(), key=lambda kv: str(kv[0])))
|
| 327 |
+
for sol in sols]
|
| 328 |
+
return {"answer": " or ".join(out), "value": sols, "check": "each solution substituted back satisfies every equation",
|
| 329 |
+
"steps": ["read %s" % "; ".join("%s %s %s" % (sp.sstr(a), o, sp.sstr(b)) for a, o, b in eqs),
|
| 330 |
+
"solve for %s" % ", ".join(map(str, syms))]}
|
| 331 |
+
if len(eqs) == 1 and len(syms) == 1:
|
| 332 |
+
a, op, b = eqs[0]
|
| 333 |
+
rel = {"<": sp.Lt, ">": sp.Gt, "<=": sp.Le, ">=": sp.Ge}[op](a, b)
|
| 334 |
+
sol = sp.solveset(rel, syms[0], domain=sp.S.Reals)
|
| 335 |
+
# CHECK: a point inside satisfies, a point outside does not
|
| 336 |
+
inside = sol.inf + 1 if sol.inf.is_finite else (sol.sup - 1 if sol.sup.is_finite else 0)
|
| 337 |
+
if inside not in sol or not rel.subs(syms[0], inside):
|
| 338 |
+
self.why = "inequality check failed"
|
| 339 |
+
return None
|
| 340 |
+
return {"answer": sp.sstr(sol), "value": sol, "check": "a point of the solution set satisfies the inequality",
|
| 341 |
+
"steps": ["read %s %s %s" % (sp.sstr(a), op, sp.sstr(b)), "solve over the reals"]}
|
| 342 |
+
return None
|
| 343 |
+
|
| 344 |
+
# --- factor ---------------------------------------------------------------------------------------------------
|
| 345 |
+
def _factor(self, spans, q):
|
| 346 |
+
e = P(max(spans, key=len))
|
| 347 |
+
f = sp.factor(e)
|
| 348 |
+
if not equiv(sp.expand(f), sp.expand(e)):
|
| 349 |
+
return None
|
| 350 |
+
if not (isinstance(f, sp.Mul) or isinstance(f, sp.Pow)) and f == e:
|
| 351 |
+
self.why = "does not factor over the rationals"
|
| 352 |
+
return None
|
| 353 |
+
return {"answer": sp.sstr(f), "value": f, "check": "the product expands back to the input",
|
| 354 |
+
"steps": ["read %s" % sp.sstr(e), "factor completely"]}
|
| 355 |
+
|
| 356 |
+
# --- simplify / expand ----------------------------------------------------------------------------------------
|
| 357 |
+
def _simplify(self, spans, q, how="simplify"):
|
| 358 |
+
e0 = P(max(spans, key=len))
|
| 359 |
+
# the textbook convention: variables under radicals and exponents are taken as positive
|
| 360 |
+
pos = {s: sp.Symbol(str(s), positive=True) for s in e0.free_symbols}
|
| 361 |
+
e = e0.xreplace(pos)
|
| 362 |
+
s0 = sp.simplify(e)
|
| 363 |
+
cands = [s0, sp.together(s0), sp.powsimp(s0), sp.powdenest(s0, force=True), sp.expand(e), sp.cancel(e),
|
| 364 |
+
sp.factor(e), sp.radsimp(s0)]
|
| 365 |
+
back = {v: k for k, v in pos.items()}
|
| 366 |
+
cands = [c.xreplace(back) for c in cands]
|
| 367 |
+
e = e0
|
| 368 |
+
if how == "expand":
|
| 369 |
+
cands = [sp.expand(e)] + cands
|
| 370 |
+
best = cands[0]
|
| 371 |
+
else:
|
| 372 |
+
best = min(cands, key=complexity)
|
| 373 |
+
if not equiv(best, e):
|
| 374 |
+
return None
|
| 375 |
+
return {"answer": sp.sstr(best), "value": best,
|
| 376 |
+
"check": "equivalent to the input at random points (variables positive, the textbook convention)",
|
| 377 |
+
"steps": ["read %s" % sp.sstr(e), "%s" % ("expand" if how == "expand" else "simplify")]}
|
| 378 |
+
|
| 379 |
+
def _expand(self, spans, q):
|
| 380 |
+
return self._simplify(spans, q, how="expand")
|
| 381 |
+
|
| 382 |
+
# --- evaluate -------------------------------------------------------------------------------------------------
|
| 383 |
+
def _evaluate(self, spans, q):
|
| 384 |
+
fdef = next((s for s in spans if re.match(r"^\s*[a-zA-Z]\s*\(\s*[a-z]\s*\)\s*=", s)), None)
|
| 385 |
+
if fdef:
|
| 386 |
+
m = re.match(r"^\s*([a-zA-Z])\s*\(\s*([a-z])\s*\)\s*=(.*)$", fdef)
|
| 387 |
+
fname, var, body = m.groups()
|
| 388 |
+
f = P(body)
|
| 389 |
+
x = sp.Symbol(var)
|
| 390 |
+
args = [re.match(r"^\s*%s\s*\((.*)\)\s*$" % fname, s) for s in spans if s is not fdef]
|
| 391 |
+
args = [a.group(1) for a in args if a]
|
| 392 |
+
if not args:
|
| 393 |
+
return None
|
| 394 |
+
outs = []
|
| 395 |
+
for a in args:
|
| 396 |
+
v = sp.simplify(f.subs(x, P(a)))
|
| 397 |
+
chk = f.subs(x, P(a))
|
| 398 |
+
if not equiv(v, chk):
|
| 399 |
+
return None
|
| 400 |
+
outs.append("%s(%s) = %s" % (fname, a, sp.sstr(v)))
|
| 401 |
+
return {"answer": "; ".join(outs), "value": outs, "check": "recomputed by direct substitution",
|
| 402 |
+
"steps": ["read %s(%s) = %s" % (fname, var, sp.sstr(f))] + ["substitute %s" % a for a in args]}
|
| 403 |
+
subs = assignments(q)
|
| 404 |
+
cand = [s_ for s_ in spans if not re.match(r"^\s*[a-z]\s*=\s*[-+]?[0-9./()]+\s*$", s_)] or spans
|
| 405 |
+
e = P(max(cand, key=len).split("=")[0])
|
| 406 |
+
if subs and e.free_symbols and e.free_symbols <= set(subs):
|
| 407 |
+
v = sp.nsimplify(sp.simplify(e.subs(subs)))
|
| 408 |
+
if not equiv(v, e.subs(subs)):
|
| 409 |
+
return None
|
| 410 |
+
return {"answer": sp.sstr(v), "value": v, "check": "recomputed by direct substitution",
|
| 411 |
+
"steps": ["read %s" % sp.sstr(e), "substitute %s" % ", ".join("%s = %s" % kv for kv in subs.items())]}
|
| 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 abs(complex(v.evalf()) - complex(e.evalf())) > 1e-9 * max(1, abs(complex(e.evalf()))):
|
| 416 |
+
return None
|
| 417 |
+
return {"answer": sp.sstr(v), "value": v, "check": "recomputed numerically",
|
| 418 |
+
"steps": ["read %s" % sp.sstr(e), "evaluate"]}
|
| 419 |
+
|
| 420 |
+
# --- calculus -------------------------------------------------------------------------------------------------
|
| 421 |
+
def _var(self, e, q):
|
| 422 |
+
syms = sorted(e.free_symbols, key=str)
|
| 423 |
+
m = re.search(r"with respect to ([a-z])|d/d([a-z])", q)
|
| 424 |
+
if m:
|
| 425 |
+
return sp.Symbol(m.group(1) or m.group(2))
|
| 426 |
+
for pref in ("x", "t", "u", "theta"):
|
| 427 |
+
for s in syms:
|
| 428 |
+
if str(s) == pref:
|
| 429 |
+
return s
|
| 430 |
+
return syms[0] if syms else sp.Symbol("x")
|
| 431 |
+
|
| 432 |
+
def _body(self, spans):
|
| 433 |
+
s = max(spans, key=len)
|
| 434 |
+
s = re.sub(r"^\s*[a-zA-Z]\s*\(\s*[a-z]\s*\)\s*=", "", s)
|
| 435 |
+
s = re.sub(r"^\s*y\s*=", "", s)
|
| 436 |
+
return s
|
| 437 |
+
|
| 438 |
+
def _derivative(self, spans, q):
|
| 439 |
+
e = P(self._body(spans))
|
| 440 |
+
x = self._var(e, q)
|
| 441 |
+
d = sp.simplify(sp.diff(e, x))
|
| 442 |
+
for pt in rand_pts([x], 3): # CHECK: central difference
|
| 443 |
+
h = sp.Rational(1, 10 ** 6)
|
| 444 |
+
num = (e.subs(x, pt[x] + h) - e.subs(x, pt[x] - h)) / (2 * h)
|
| 445 |
+
if abs(complex(num.evalf()) - complex(d.subs(pt).evalf())) > 1e-4 * max(1, abs(complex(d.subs(pt).evalf()))):
|
| 446 |
+
return None
|
| 447 |
+
return {"answer": sp.sstr(d), "value": d, "check": "agrees with a central difference at random points",
|
| 448 |
+
"steps": ["read %s" % sp.sstr(e), "differentiate with respect to %s" % x]}
|
| 449 |
+
|
| 450 |
+
def _integral(self, spans, q):
|
| 451 |
+
s = max(spans, key=len)
|
| 452 |
+
m = re.search(r"integral\s*_\s*\{?([^}^]+?)\}?\s*\^\s*\{?([^}]+?)\}?\s*(\(.*|[^(].*?)\s*(?:d([a-z]))?\s*$", s)
|
| 453 |
+
fb = re.search(r"from\s+([-\w./()]+)\s+to\s+([-\w./()]+)", q)
|
| 454 |
+
lo = hi = None
|
| 455 |
+
if m:
|
| 456 |
+
lo, hi, body, v = m.groups()
|
| 457 |
+
e = P(body)
|
| 458 |
+
elif fb:
|
| 459 |
+
lo, hi = fb.groups()
|
| 460 |
+
body = max([x for x in spans if x.strip() not in (lo, hi)] or spans, key=len)
|
| 461 |
+
e = P(re.sub(r"\s*d[a-z]\s*$", "", self._body([body]).replace("integral", "")))
|
| 462 |
+
else:
|
| 463 |
+
e = P(re.sub(r"\s*d([a-z])\s*$", "", self._body(spans).replace("integral", "")))
|
| 464 |
+
x = self._var(e, q)
|
| 465 |
+
F = sp.integrate(e, x)
|
| 466 |
+
if F.has(sp.Integral):
|
| 467 |
+
return None
|
| 468 |
+
if not equiv(sp.diff(F, x), e): # CHECK: differentiates back
|
| 469 |
+
return None
|
| 470 |
+
if lo is not None:
|
| 471 |
+
val = sp.simplify(F.subs(x, P(hi)) - F.subs(x, P(lo)))
|
| 472 |
+
quad = sp.Integral(e, (x, P(lo), P(hi))).evalf()
|
| 473 |
+
if abs(complex(val.evalf()) - complex(quad)) > 1e-6 * max(1, abs(complex(quad))):
|
| 474 |
+
return None
|
| 475 |
+
return {"answer": sp.sstr(val), "value": val, "check": "antiderivative differentiates back; quadrature agrees",
|
| 476 |
+
"steps": ["read integral of %s from %s to %s" % (sp.sstr(e), lo, hi), "antiderivative %s" % sp.sstr(F)]}
|
| 477 |
+
return {"answer": sp.sstr(F) + " + C", "value": F, "check": "differentiates back to the integrand",
|
| 478 |
+
"steps": ["read %s" % sp.sstr(e), "integrate with respect to %s" % x]}
|
| 479 |
+
|
| 480 |
+
def _limit(self, spans, q):
|
| 481 |
+
s = max(spans, key=len)
|
| 482 |
+
m = re.search(r"lim\s*_\s*\{?\s*([a-z])\s*->\s*([^}\s]+?)\s*\}?\s*(\S.*?)\s*(?:,.*)?$", s) or \
|
| 483 |
+
re.search(r"as ([a-z]) (?:approaches|->) ([-\w.]+)", q)
|
| 484 |
+
if not m:
|
| 485 |
+
return None
|
| 486 |
+
if len(m.groups()) == 3:
|
| 487 |
+
v, pt, body = m.groups()
|
| 488 |
+
e = P(body)
|
| 489 |
+
else:
|
| 490 |
+
v, pt = m.groups()
|
| 491 |
+
e = P(self._body([x for x in spans if "lim" not in x] or spans))
|
| 492 |
+
x, a = sp.Symbol(v), P(pt)
|
| 493 |
+
L_ = sp.limit(e, x, a)
|
| 494 |
+
if L_.is_finite: # CHECK: evaluation near the point
|
| 495 |
+
near = complex(e.subs(x, a + sp.Rational(1, 10 ** 7)).evalf())
|
| 496 |
+
if abs(near - complex(L_.evalf())) > 1e-3 * max(1, abs(complex(L_.evalf()))):
|
| 497 |
+
return None
|
| 498 |
+
return {"answer": sp.sstr(L_), "value": L_, "check": "agrees with evaluation near the point",
|
| 499 |
+
"steps": ["read the limit of %s as %s -> %s" % (sp.sstr(e), v, pt)]}
|
| 500 |
+
|
| 501 |
+
def _partial(self, spans, q):
|
| 502 |
+
e = P(max(spans, key=len))
|
| 503 |
+
x = self._var(e, q)
|
| 504 |
+
r = sp.apart(e, x)
|
| 505 |
+
if not equiv(r, e):
|
| 506 |
+
return None
|
| 507 |
+
return {"answer": sp.sstr(r), "value": r, "check": "recombines to the input",
|
| 508 |
+
"steps": ["read %s" % sp.sstr(e), "decompose into partial fractions"]}
|
root/wander-train/oracle_cube_reader_v1_3.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""oracle_cube_reader_v1 - the reasoning cube, Phase A: read -> frame -> link -> expand -> bind -> check -> verify.
|
| 2 |
+
|
| 3 |
+
Three graphs share one middle pillar (request, binding, evidence, surface). This module walks the first six moves:
|
| 4 |
+
|
| 5 |
+
Q Malkuth->Yesod READ the surface opens obligations
|
| 6 |
+
Q Yesod->Tiferet FRAME the answer slot: its TYPE (person, year, date, number, place, or an instance of the question's
|
| 7 |
+
head noun), the relation words, the constraints
|
| 8 |
+
O Tiferet->Chokmah LINK the subject, by the store's own title index (longest word n-grams), never by guessing
|
| 9 |
+
from capital letters
|
| 10 |
+
K Chokmah->Chesed EXPAND the subject's own page, sentence by sentence, each with its claim id
|
| 11 |
+
K Chesed->Tiferet BIND a candidate of the required type, drawn from a sentence that carries the relation
|
| 12 |
+
O Geburah->Tiferet CHECK INDEPENDENT of the proposing sentence: an entity candidate's OWN page must say it is the
|
| 13 |
+
asked-for kind; the question's constraints must be attested in the binding sentence or on
|
| 14 |
+
the candidate's page; a value candidate (year, date, number) must be the only value of its
|
| 15 |
+
type bound to a relation word in its sentence
|
| 16 |
+
Q Tiferet->Keter VERIFY every obligation discharged, one candidate standing, or nothing is said
|
| 17 |
+
|
| 18 |
+
Laws obeyed (reach-is-not-answer, 2026-09-10): a candidate is proposed BECAUSE it could fill the open slot, carries a
|
| 19 |
+
typed binding, and is checked by something that did not propose it. No hypernym bridging. Abstention is free; a wrong
|
| 20 |
+
answer costs a right one. The store is ORACLE_DEEP_BUILD, read-only.
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
import re
|
| 26 |
+
import sqlite3
|
| 27 |
+
import unicodedata
|
| 28 |
+
|
| 29 |
+
DEEP = os.environ.get("ORACLE_DEEP_BUILD") or (
|
| 30 |
+
__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/fable-content/oracle-m-series-2026-08-15/"
|
| 31 |
+
"m13e8_dbpedia_enslot_openstax_full_fable_bench_a_v1/wikipedia_specialist.sqlite3")
|
| 32 |
+
|
| 33 |
+
STOP = set("""a an the of and or for to in on at by with from into about as is are was were be been being it its this that
|
| 34 |
+
these those which what who whom whose when where why how did does do has have had not no but if than so such also can
|
| 35 |
+
will would could should may might must shall there their his her he she they them we you your our i me my one first
|
| 36 |
+
last name named called known""".split())
|
| 37 |
+
MEASURES = {"capacity", "population", "height", "length", "area", "number", "size", "weight", "depth", "width", "age",
|
| 38 |
+
"distance", "speed", "elevation", "altitude", "attendance", "budget", "revenue"}
|
| 39 |
+
ANSWER_WORDS = {"day", "month", "year", "date", "time", "when", "first", "last", "name", "names", "called", "second",
|
| 40 |
+
"number", "many", "much", "exact", "full", "people", "person", "two", "three"}
|
| 41 |
+
FRAME_NOUNS = {"name", "type", "kind", "sort", "word", "term", "title", "one", "thing", "part", "member"}
|
| 42 |
+
ACTION_BREAK = re.compile(r"^(is|was|were|are|did|does|do|has|had|have|won|played|made|became|served|wrote|directed|"
|
| 43 |
+
r"founded|released|located|called|named|known|used|held|given|awarded|built|born|died|developed|"
|
| 44 |
+
r"invented|created|published|produced|starred|in|of|for|by|to|at|on|from|with|that|whose|who|"
|
| 45 |
+
r"which|as|after|before|during|and|or)$", re.I)
|
| 46 |
+
MONTHS = "january february march april may june july august september october november december".split()
|
| 47 |
+
MONTH_RE = "|".join(m.capitalize() for m in MONTHS)
|
| 48 |
+
YEAR = re.compile(r"\b(1[0-9]{3}|20[0-2][0-9])\b")
|
| 49 |
+
DATE = re.compile(r"\b(\d{1,2} (?:%s) \d{3,4}|(?:%s) \d{1,2}, \d{3,4})\b" % (MONTH_RE, MONTH_RE))
|
| 50 |
+
MONTH_YEAR = re.compile(r"\b((?:%s) \d{4})\b" % MONTH_RE)
|
| 51 |
+
COMPARE = re.compile(r"\b(or|out of|between|both|either|neither)\b.*\?$|\b(more|most|less|fewer|older|younger|earlier|later|"
|
| 52 |
+
r"longer|shorter|larger|smaller|bigger|taller|first)\b.*\b(or|and)\b", re.I)
|
| 53 |
+
NUMBER = re.compile(r"\b\d{1,3}(?:,\d{3})+\b|\b\d+(?:\.\d+)?\b")
|
| 54 |
+
NUMWORDS = {w: i for i, w in enumerate("zero one two three four five six seven eight nine ten eleven twelve".split())}
|
| 55 |
+
CAPSPAN = re.compile(r"\b([A-Z][\w'\.\-&]*(?:\s+(?:of|the|de|von|van|da|del|la|le|and|&)?\s*[A-Z0-9][\w'\.\-&]*){0,5})")
|
| 56 |
+
TOK = re.compile(r"[A-Za-z0-9][A-Za-z0-9'\-\.&]*")
|
| 57 |
+
# v1.2: a PERSON is shown by a birth/life-span parenthetical or an occupation predicate, never by a pronoun anywhere on the
|
| 58 |
+
# page (v1.1 accepted "MA" - Massachusetts - as a person because the page said "her")
|
| 59 |
+
OCCUPATIONS = (r"actor|actress|singer|songwriter|musician|composer|writer|author|poet|novelist|journalist|politician|"
|
| 60 |
+
r"lawyer|judge|player|footballer|cricketer|athlete|coach|manager|painter|artist|sculptor|architect|"
|
| 61 |
+
r"scientist|physicist|chemist|biologist|mathematician|astronomer|engineer|inventor|economist|philosopher|"
|
| 62 |
+
r"historian|director|producer|businessman|businesswoman|entrepreneur|explorer|soldier|general|admiral|"
|
| 63 |
+
r"officer|king|queen|emperor|prince|princess|pope|bishop|priest|monk|saint|president|minister|senator|"
|
| 64 |
+
r"governor|diplomat|activist|comedian|dancer|model|designer|photographer|surgeon|physician|nurse|"
|
| 65 |
+
r"professor|teacher|scholar|academic|chef|pilot|astronaut|wrestler|boxer|racing driver|cyclist|swimmer|"
|
| 66 |
+
r"tennis player|golfer|chess player|rapper|drummer|guitarist|pianist|conductor|filmmaker|screenwriter|"
|
| 67 |
+
r"cartoonist|illustrator|critic|editor|publisher|broadcaster|presenter|television personality|nobleman|"
|
| 68 |
+
r"noblewoman|lord|lady|duke|earl|count|countess|baron|chief|leader|missionary|theologian|psychologist")
|
| 69 |
+
PERSON_LEAD = re.compile(r"\(\s*born\b|\(\s*(?:c\.\s*)?\d{1,2}? ?\w* ?\d{3,4}\s*[–-]|\(\s*fl\.|"
|
| 70 |
+
r"\b(?:is|was)\s+(?:a|an|the)\s+(?:[\w\-]+\s+){0,4}?(?:%s)s?\b" % OCCUPATIONS, re.I)
|
| 71 |
+
NOUNISH = re.compile(r"(?:ers?|ors?|ions?|ments?|ness|ities|ity|ists?|ship)$", re.I)
|
| 72 |
+
PLACE_LEAD = re.compile(r"\b(city|town|village|country|state|province|county|region|capital|municipality|island|"
|
| 73 |
+
r"district|commune|borough|suburb|territory|nation|located in|metropolis|kingdom|republic)\b", re.I)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def norm(s):
|
| 77 |
+
s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode("ascii")
|
| 78 |
+
return " ".join(re.sub(r"[^a-z0-9 ]+", " ", s.lower()).split())
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
IRREG = {"won": "win", "win": "win", "wins": "win", "wrote": "write", "written": "write", "writes": "write",
|
| 82 |
+
"write": "write", "began": "begin", "begun": "begin", "became": "become", "become": "become", "led": "lead",
|
| 83 |
+
"leads": "lead", "lead": "lead", "ran": "run", "run": "run", "sang": "sing", "sung": "sing", "sing": "sing",
|
| 84 |
+
"held": "hold", "hold": "hold", "made": "make", "make": "make", "took": "take", "taken": "take", "take": "take",
|
| 85 |
+
"gave": "give", "given": "give", "give": "give", "came": "come", "come": "come", "went": "go", "met": "meet",
|
| 86 |
+
"meet": "meet", "left": "leave", "leave": "leave", "built": "build", "build": "build", "sold": "sell",
|
| 87 |
+
"sell": "sell", "bought": "buy", "buy": "buy", "taught": "teach", "teach": "teach", "fought": "fight",
|
| 88 |
+
"fight": "fight", "drew": "draw", "drawn": "draw", "draw": "draw", "flew": "fly", "fly": "fly", "grew": "grow",
|
| 89 |
+
"grow": "grow", "spoke": "speak", "speak": "speak", "chose": "choose", "chosen": "choose", "choose": "choose",
|
| 90 |
+
"drove": "drive", "drive": "drive", "born": "bear", "birth": "bear", "died": "die", "dies": "die", "die": "die",
|
| 91 |
+
"death": "die", "beat": "beat", "beaten": "beat", "shot": "shoot", "hit": "hit", "set": "set", "put": "put"}
|
| 92 |
+
BASE_VERBS = set("""play join attend marry appear found discover invent release direct compose coach serve elect award
|
| 93 |
+
sign command host produce publish open close design create capture defeat star record graduate study move live name
|
| 94 |
+
score represent debut premiere air launch establish introduce approve nominate appoint succeed replace kill murder
|
| 95 |
+
retire resign own acquire merge sponsor manage train perform tour reach finish rank place qualify earn receive""".split())
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def canon(w):
|
| 99 |
+
w = w.lower()
|
| 100 |
+
return IRREG.get(w) or stem(w)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def verbish(w):
|
| 104 |
+
wl = w.lower()
|
| 105 |
+
return wl in IRREG or wl in BASE_VERBS or (wl.endswith("ed") and len(wl) > 4) or stem(wl) in {stem(b) for b in BASE_VERBS}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def stem(w):
|
| 109 |
+
w = w.lower()
|
| 110 |
+
for suf in ("ing", "ers", "ed", "es", "er", "s"):
|
| 111 |
+
if len(w) > len(suf) + 3 and w.endswith(suf):
|
| 112 |
+
return w[: -len(suf)][:6]
|
| 113 |
+
return w[:6]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class CubeReader:
|
| 117 |
+
def __init__(self, path=DEEP):
|
| 118 |
+
uri = "file:%s?mode=ro&immutable=1" % path
|
| 119 |
+
self.db = sqlite3.connect(uri, uri=True, check_same_thread=False)
|
| 120 |
+
self._lead = {}
|
| 121 |
+
|
| 122 |
+
# ---- K: the store ----
|
| 123 |
+
def pid_of(self, title):
|
| 124 |
+
nt = norm(title)
|
| 125 |
+
if not nt:
|
| 126 |
+
return None
|
| 127 |
+
r = self.db.execute("SELECT page_id FROM title_alias WHERE normalized_title=? LIMIT 1", (nt,)).fetchone()
|
| 128 |
+
return r[0] if r else None
|
| 129 |
+
|
| 130 |
+
def page(self, pid, cap=40):
|
| 131 |
+
out = []
|
| 132 |
+
for (rowid,) in self.db.execute("SELECT source_rowid FROM page_sentence WHERE page_id=? ORDER BY sentence_ordinal "
|
| 133 |
+
"LIMIT ?", (pid, cap)).fetchall():
|
| 134 |
+
c = self.db.execute("SELECT c0, c1 FROM source_rows_content WHERE id=?", (rowid,)).fetchone()
|
| 135 |
+
if not c:
|
| 136 |
+
continue
|
| 137 |
+
text = " ".join(str(c[1]).split())
|
| 138 |
+
# the store's sentence splitter breaks on initials ("discovered by A." / "Schwassmann on 11 September 1898"):
|
| 139 |
+
# rejoin, keeping the first claim id and citing both
|
| 140 |
+
if out and re.search(r"(?:^|\s)[A-Z]\.$", out[-1][1]):
|
| 141 |
+
out[-1] = (out[-1][0], out[-1][1] + " " + text)
|
| 142 |
+
else:
|
| 143 |
+
out.append((c[0], text))
|
| 144 |
+
return out
|
| 145 |
+
|
| 146 |
+
def lead(self, pid, n=3):
|
| 147 |
+
if pid not in self._lead:
|
| 148 |
+
self._lead[pid] = " ".join(t for _, t in self.page(pid, n))
|
| 149 |
+
return self._lead[pid]
|
| 150 |
+
|
| 151 |
+
# ---- Q: FRAME ----
|
| 152 |
+
def frame(self, q):
|
| 153 |
+
s = " ".join(str(q).split())
|
| 154 |
+
low = s.lower()
|
| 155 |
+
if not s or "passage:" in low or "\noptions" in low or "options:" in low or re.search(r"\b(you|your)\b", low):
|
| 156 |
+
return None
|
| 157 |
+
if not (s.endswith("?") or re.match(r"^(who|whom|whose|what|which|when|where|how|in what|in which|name)\b", low)):
|
| 158 |
+
return None
|
| 159 |
+
atype, head = None, None
|
| 160 |
+
if re.search(r"\bwhat (day|date)\b|\bday, month,? and year\b|\bexact date\b|\b(what|which) month and year\b", low):
|
| 161 |
+
atype = "DATE"
|
| 162 |
+
elif re.search(r"\b(what|which) year\b|\bin what year\b|^when\b|\bwhat year\b", low):
|
| 163 |
+
atype = "YEAR"
|
| 164 |
+
elif re.search(r"\bhow many\b", low):
|
| 165 |
+
atype = "NUMBER"
|
| 166 |
+
elif re.match(r"^(who|whom)\b", low) or re.search(r"\b(which|what) (person|man|woman|actor|actress|singer|player|"
|
| 167 |
+
r"writer|author|politician|artist|director)\b", low):
|
| 168 |
+
atype = "PERSON"
|
| 169 |
+
elif re.match(r"^where\b", low):
|
| 170 |
+
atype, head = "PLACE", None
|
| 171 |
+
else:
|
| 172 |
+
m = re.search(r"\b(?:which|what)\s+((?:[\w'\-]+\s+){0,5}?[\w'\-]+?)(?=\s+(?:is|was|were|are|did|does|do|has|had|"
|
| 173 |
+
r"have|won|played|made|became|served|wrote|directed|founded|released|located|called|named|known|"
|
| 174 |
+
r"used|held|given|awarded|built|born|died|developed|invented|created|published|produced|starred|"
|
| 175 |
+
r"in|of|for|by|to|at|on|from|with|that|whose|who|which|as|after|before|during)\b|\s*\?|$)", s, re.I)
|
| 176 |
+
if m:
|
| 177 |
+
words = [w for w in m.group(1).split() if w]
|
| 178 |
+
lower = [w for w in words if w.islower() and w.isalpha()]
|
| 179 |
+
if lower:
|
| 180 |
+
head = lower[-1].lower()
|
| 181 |
+
if head in FRAME_NOUNS:
|
| 182 |
+
m2 = re.search(r"\b(?:name|type|kind|sort|title) of (?:the |a |an )?((?:[\w'\-]+\s+){0,3}?[\w'\-]+?)"
|
| 183 |
+
r"(?=\s+(?:in|of|for|by|that|which|who|whose|is|was|were)\b|\s*\?|$)", s, re.I)
|
| 184 |
+
head = ([w for w in m2.group(1).split() if w.islower()] or [None])[-1] if m2 else None
|
| 185 |
+
if head in MEASURES:
|
| 186 |
+
atype = "NUMBER"
|
| 187 |
+
elif head in ("city", "country", "town", "state", "place", "capital", "village", "nation", "province"):
|
| 188 |
+
atype = "PLACE"
|
| 189 |
+
elif head and head not in STOP and len(head) >= 3:
|
| 190 |
+
atype = "INSTANCE"
|
| 191 |
+
if atype is None:
|
| 192 |
+
return None
|
| 193 |
+
if COMPARE.search(s) and (" or " in low or " out of " in low or " between " in low or " both " in low):
|
| 194 |
+
return None # a comparison: the COMPARE move is Phase 3, not this reader
|
| 195 |
+
if re.search(r"\bthe (?:[a-z\-]+ ){0,2}(?:man|woman|person|composer|author|writer|director|actor|actress|singer|"
|
| 196 |
+
r"player|team|band|company|film|city|country|village|town|artist|founder|creator|leader|president|"
|
| 197 |
+
r"king|queen|school|university|group|album|show|series|book|novel|song|character|state|river)\s+"
|
| 198 |
+
r"(?:who|whose|which|that|where)\b", low):
|
| 199 |
+
return None # the subject is a DESCRIPTION: a bridge (two hops), Phase 3
|
| 200 |
+
if re.search(r"\bthe\b[^?]{0,90}?,?\s+(?:who|whose)\b", low):
|
| 201 |
+
return None # v1.2: any described person ("the American lawyer ... who") is a bridge
|
| 202 |
+
if re.search(r"\b(?:of|on|in|at|from|near)\s+the\s+(?:[a-z\-]+\s+){0,2}[a-z]+\s+(?:located|situated|based|founded|"
|
| 203 |
+
r"built|named|called|known|found|owned|operated|run|held)\b", low):
|
| 204 |
+
return None # v1.3: "the slopes of the hill located in what town" - the
|
| 205 |
+
# asked thing is a described entity (a bridge), not the subject
|
| 206 |
+
return {"q": s, "atype": atype, "head": head, "years": set(YEAR.findall(s)),
|
| 207 |
+
"month_year": bool(re.search(r"\bmonth and year\b|\bmonth, and year\b", low)) and "day" not in low}
|
| 208 |
+
|
| 209 |
+
# ---- O: LINK (title index, longest non-overlapping n-grams) ----
|
| 210 |
+
def link(self, fr, maxn=7):
|
| 211 |
+
toks = TOK.findall(fr["q"])
|
| 212 |
+
found = []
|
| 213 |
+
for n in range(min(maxn, len(toks)), 0, -1):
|
| 214 |
+
for i in range(len(toks) - n + 1):
|
| 215 |
+
words = toks[i:i + n]
|
| 216 |
+
nt = norm(" ".join(words))
|
| 217 |
+
if not nt or all(w in STOP or w in FRAME_NOUNS for w in nt.split()) or nt == fr["head"]:
|
| 218 |
+
continue
|
| 219 |
+
if not words[0][:1].isupper() and not words[0][:1].isdigit():
|
| 220 |
+
continue # a subject starts with a capital (names, titles) or a digit
|
| 221 |
+
if n == 1 and len(nt) < 4:
|
| 222 |
+
continue
|
| 223 |
+
if nt in MONTHS or nt.replace(" ", "").isdigit():
|
| 224 |
+
continue # a month or a bare number is a constraint, never the subject
|
| 225 |
+
pid = self.pid_of(nt)
|
| 226 |
+
if pid:
|
| 227 |
+
found.append((i, i + n, " ".join(words), pid))
|
| 228 |
+
keep, used = [], set()
|
| 229 |
+
for i, j, text, pid in sorted(found, key=lambda t: (-(t[1] - t[0]), t[0])):
|
| 230 |
+
if used.isdisjoint(range(i, j)):
|
| 231 |
+
keep.append({"span": (i, j), "text": text, "pid": pid})
|
| 232 |
+
used.update(range(i, j))
|
| 233 |
+
fr["tokens"] = toks
|
| 234 |
+
fr["subjects"] = keep
|
| 235 |
+
subj_tok = set(k for s in keep for k in range(*s["span"]))
|
| 236 |
+
rel = [t for k, t in enumerate(toks) if k not in subj_tok and t.lower() not in STOP and t.lower() != fr["head"]
|
| 237 |
+
and t.lower() not in ANSWER_WORDS and not YEAR.fullmatch(t) and t[:1].islower() and len(t) >= 3]
|
| 238 |
+
fr["rel"] = [stem(t) for t in rel]
|
| 239 |
+
# the PREDICATE: the question's verb(s). The binding sentence must carry one of them - a value bound through a
|
| 240 |
+
# shared context word ("road" in "road running athlete") answers a different relation
|
| 241 |
+
verbs = [(k, t) for k, t in enumerate(toks) if k not in subj_tok and t[:1].islower() and verbish(t)
|
| 242 |
+
and t.lower() not in STOP]
|
| 243 |
+
# only the verb GOVERNING the answer slot counts: the one nearest the wh-phrase ("founded in what year"), not a
|
| 244 |
+
# participle describing something else ("a failed raid")
|
| 245 |
+
wh = [k for k, t in enumerate(toks) if t.lower() in ("who", "whom", "whose", "what", "which", "when", "where", "how")]
|
| 246 |
+
if verbs and wh:
|
| 247 |
+
w0 = wh[0]
|
| 248 |
+
k, t = min(verbs, key=lambda kt: (abs(kt[0] - w0), kt[0] < w0))
|
| 249 |
+
fr["pred"] = [canon(t)]
|
| 250 |
+
else:
|
| 251 |
+
fr["pred"] = sorted({canon(t) for _, t in verbs})
|
| 252 |
+
# every NAME in the question is an obligation: a capitalised token (not the first word) outside every linked span
|
| 253 |
+
unlinked = [t for k, t in enumerate(toks) if k > 0 and k not in subj_tok and t[:1].isupper()
|
| 254 |
+
and t.lower() not in STOP and len(t) >= 3]
|
| 255 |
+
unlinked += [t for k, t in enumerate(toks) if k not in subj_tok and t.isdigit() and len(t) >= 2 and not YEAR.fullmatch(t)]
|
| 256 |
+
# v1.2: an identifier ("gal56", "EURECA-1") is an obligation too: letters and digits together
|
| 257 |
+
unlinked += [t for k, t in enumerate(toks) if k not in subj_tok and re.search(r"\d", t) and re.search(r"[A-Za-z]", t)]
|
| 258 |
+
# v1.2: the ROLE the answer must play. "Who beat X" asks for the AGENT of the predicate
|
| 259 |
+
# v1.3: "Who did X knock out?" asks for the PATIENT of X's action
|
| 260 |
+
fr["patient"] = bool(re.match(r"^(?:who|whom|what|which [\w\-]+)\s+(?:did|does|do|has|had|have)\b", fr["q"].lower()))
|
| 261 |
+
fr["agent"] = bool(re.match(r"^(?:who|which [\w\-]+(?: [\w\-]+)?)\s+(?!is\b|was\b|are\b|were\b|did\b|does\b|do\b|has\b|"
|
| 262 |
+
r"had\b|have\b|will\b|would\b|can\b|could\b)[a-z]+", fr["q"].lower()))
|
| 263 |
+
fr["constraints"] = [s["text"] for s in keep[1:]] + sorted(fr["years"]) + unlinked
|
| 264 |
+
return keep
|
| 265 |
+
|
| 266 |
+
# ---- K: BIND ----
|
| 267 |
+
def _candidates(self, fr, sent, qnorm):
|
| 268 |
+
at = fr["atype"]
|
| 269 |
+
out = []
|
| 270 |
+
if at == "YEAR":
|
| 271 |
+
out = [y for y in YEAR.findall(sent) if y not in fr["years"]]
|
| 272 |
+
elif at == "DATE":
|
| 273 |
+
out = DATE.findall(sent)
|
| 274 |
+
if not out and fr.get("month_year"):
|
| 275 |
+
out = MONTH_YEAR.findall(sent)
|
| 276 |
+
elif at == "NUMBER":
|
| 277 |
+
out = [n for n in NUMBER.findall(sent) if not YEAR.fullmatch(n) and n not in fr["q"]]
|
| 278 |
+
else:
|
| 279 |
+
for m in CAPSPAN.finditer(sent):
|
| 280 |
+
span = m.group(1).strip(" .,;:")
|
| 281 |
+
span = re.sub(r"^(The|A|An)\s+", "", span)
|
| 282 |
+
if len(span) < 3 or (span.isupper() and len(span) <= 3) or norm(span) in qnorm or norm(span).split()[0] in STOP:
|
| 283 |
+
continue
|
| 284 |
+
if span.split()[0].capitalize() in MONTH_RE.split("|"):
|
| 285 |
+
continue
|
| 286 |
+
out.append(span)
|
| 287 |
+
return out
|
| 288 |
+
|
| 289 |
+
def bind(self, fr):
|
| 290 |
+
qnorm = norm(fr["q"])
|
| 291 |
+
rel = set(fr["rel"])
|
| 292 |
+
cands = []
|
| 293 |
+
for subj in fr["subjects"][:4]:
|
| 294 |
+
sents = self.page(subj["pid"], 40)
|
| 295 |
+
for idx, (cid, sent_full) in enumerate(sents):
|
| 296 |
+
words = [p for t in TOK.findall(sent_full) for p in [t] + t.split("-")[1:]] # "co-founded" -> founded
|
| 297 |
+
# v1.2: the predicate must appear as a VERB - "composers" is not "composed"
|
| 298 |
+
verbs_here = {canon(t) for t in words if not NOUNISH.search(t)}
|
| 299 |
+
if fr["pred"] and not (verbs_here & set(fr["pred"])):
|
| 300 |
+
continue # the question's predicate is not in this sentence
|
| 301 |
+
sent = sent_full
|
| 302 |
+
if not ({"bear", "die"} & set(fr["pred"])):
|
| 303 |
+
# a value inside "(born ...)" or a life span answers only a birth/death question
|
| 304 |
+
sent = re.sub(r"\((?:[^()]*\b(?:born|died|b\.|d\.)\b[^()]*|\s*[^()]{0,30}\d{3,4}\s*[–-]\s*[^()]{0,30}\d{3,4}\s*)\)",
|
| 305 |
+
"", sent_full)
|
| 306 |
+
stoks = [stem(t) for t in TOK.findall(sent)]
|
| 307 |
+
hits = len(rel & set(stoks))
|
| 308 |
+
cover = (hits / len(rel)) if rel else (1.0 if idx < 2 else 0.0)
|
| 309 |
+
if rel and hits == 0:
|
| 310 |
+
continue # the sentence must carry at least one of the question's relation words
|
| 311 |
+
if not rel and idx >= 2:
|
| 312 |
+
continue # no relation words: only the page's lead may bind
|
| 313 |
+
for c in self._candidates(fr, sent, qnorm):
|
| 314 |
+
# proximity: the candidate sits near a relation word in its sentence
|
| 315 |
+
pos = sent.find(c)
|
| 316 |
+
near = any(abs(sent.lower().find(r) - pos) < 90 for r in rel if sent.lower().find(r) >= 0) if rel else True
|
| 317 |
+
if not near:
|
| 318 |
+
continue
|
| 319 |
+
cands.append({"value": c, "claim": cid, "sentence": sent_full, "subject": subj, "cover": cover,
|
| 320 |
+
"lead": idx < 3, "score": cover + (0.3 if idx < 3 else 0.0)})
|
| 321 |
+
return cands
|
| 322 |
+
|
| 323 |
+
# ---- O: CHECK (independent of the proposing sentence) ----
|
| 324 |
+
def check(self, fr, c):
|
| 325 |
+
at = fr["atype"]
|
| 326 |
+
ev = {}
|
| 327 |
+
if at in ("PERSON", "PLACE", "INSTANCE"):
|
| 328 |
+
pid = self.pid_of(c["value"])
|
| 329 |
+
if not pid or pid == c["subject"]["pid"]:
|
| 330 |
+
return False, {"why": "candidate has no page of its own"}
|
| 331 |
+
lead = self.lead(pid)
|
| 332 |
+
if at == "PERSON" and not PERSON_LEAD.search(lead):
|
| 333 |
+
return False, {"why": "candidate page does not describe a person"}
|
| 334 |
+
first = re.split(r"(?<=[.!?])\s+", lead, 1)[0]
|
| 335 |
+
if at == "PLACE" and not fr["head"] and not PLACE_LEAD.search(first):
|
| 336 |
+
return False, {"why": "candidate page does not describe a place"}
|
| 337 |
+
if at == "INSTANCE" or (at == "PLACE" and fr["head"]):
|
| 338 |
+
# IS-A, not mention: the first sentence must say the candidate IS a <head> ("... is a professional team")
|
| 339 |
+
h = fr["head"]
|
| 340 |
+
# ... and the head noun must END its phrase ("a film", not "a film producer")
|
| 341 |
+
isa = (r"\b(?:is|was|are|were)\s+(?:a|an|the|one of the)\b[^.]{0,90}?\b%s(?:s|es)?\b"
|
| 342 |
+
r"(?=\s*(?:[,.;:()]|$)|\s+(?:that|which|who|whose|based|in|of|from|and|or|founded|established|located|"
|
| 343 |
+
r"with|by|for|released|directed|produced|written|set|created|developed|published|made|first|since|"
|
| 344 |
+
r"owned|operated|consisting|starring|about|on|at|to|named|known|situated|headquartered)\b)") % re.escape(h)
|
| 345 |
+
# the store's canonical title is also type evidence when its last word IS the head ("Crimean War")
|
| 346 |
+
title_head = norm(c["value"]).split()[-1:] in ([h], [h + "s"])
|
| 347 |
+
if not re.search(isa, first, re.I) and not title_head:
|
| 348 |
+
return False, {"why": "candidate page does not say it is a %s" % h}
|
| 349 |
+
if fr.get("agent"):
|
| 350 |
+
# v1.2 ROLE: "Who beat X?" needs the candidate as the predicate's AGENT. In active voice the agent stands
|
| 351 |
+
# before the verb; after it, only a passive "by" makes it the agent ("discovered by Annibale de Gasparis").
|
| 352 |
+
# "Henman ... beating Roger Federer" puts Federer in the patient role and is refused.
|
| 353 |
+
sl = c["sentence"]
|
| 354 |
+
pos_c = sl.find(c["value"])
|
| 355 |
+
pv = next((m for m in TOK.finditer(sl) if not NOUNISH.search(m.group(0)) and canon(m.group(0)) in fr["pred"]), None)
|
| 356 |
+
if pv is not None and pos_c > pv.start() and not re.search(r"\bby\b", sl[pv.end():pos_c]):
|
| 357 |
+
return False, {"why": "candidate is not the agent of the predicate"}
|
| 358 |
+
if fr.get("patient"):
|
| 359 |
+
# v1.3 ROLE: the answer is the patient of the SUBJECT's action; a sentence where the subject is itself
|
| 360 |
+
# the passive patient ("Douglas was knocked out ... Lou Savarese") names the agent, not the answer
|
| 361 |
+
subj_words = [w for w in norm(c["subject"]["text"]).split() if len(w) >= 4]
|
| 362 |
+
sl = c["sentence"]
|
| 363 |
+
for w in subj_words:
|
| 364 |
+
if re.search(r"\b%s\b[^.]{0,40}?\b(?:was|were|been|being)\s+(?:\w+\s+)?\w+(?:ed|en|out)\b" % re.escape(w), sl, re.I):
|
| 365 |
+
return False, {"why": "the subject is the patient in this sentence"}
|
| 366 |
+
ev = {"type_page": pid, "type_lead": lead[:240]}
|
| 367 |
+
where = norm(c["sentence"] + " " + lead + " " + self.lead(c["subject"]["pid"]))
|
| 368 |
+
else:
|
| 369 |
+
if at == "DATE" and not fr.get("month_year"):
|
| 370 |
+
# v1.3 CORROBORATION for day-precision dates: a single sentence can be wrong (FC Baltika: the store says
|
| 371 |
+
# 23 August 1954, the key 22 December 1954). An exact date is committed only when a SECOND sentence -
|
| 372 |
+
# different claim id - carries the same date string with the subject's distinctive word.
|
| 373 |
+
subj_words = [w for w in norm(c["subject"]["text"]).split() if len(w) >= 4 and w not in STOP]
|
| 374 |
+
word = max(subj_words, key=len) if subj_words else None
|
| 375 |
+
n_other = 0
|
| 376 |
+
if word:
|
| 377 |
+
try:
|
| 378 |
+
rows = self.db.execute("SELECT claim_id FROM source_rows WHERE source_rows MATCH ? LIMIT 6",
|
| 379 |
+
('"%s" AND "%s"' % (c["value"].replace('"', ""), word),)).fetchall()
|
| 380 |
+
n_other = len({r[0] for r in rows if r[0] != c["claim"]})
|
| 381 |
+
except sqlite3.OperationalError:
|
| 382 |
+
n_other = 0
|
| 383 |
+
if n_other < 1:
|
| 384 |
+
return False, {"why": "exact date not corroborated by a second sentence"}
|
| 385 |
+
vals = self._candidates(fr, c["sentence"], norm(fr["q"]))
|
| 386 |
+
if len(set(vals)) != 1:
|
| 387 |
+
return False, {"why": "more than one %s in the binding sentence" % at.lower()}
|
| 388 |
+
where = norm(c["sentence"] + " " + self.lead(c["subject"]["pid"]))
|
| 389 |
+
# constraints: every OTHER name/number/year in the question. The subject whose page holds the sentence is attested
|
| 390 |
+
# by the page itself; qualifiers of that subject ("El Santuario, Antioquia, Colombia") may be attested on its lead;
|
| 391 |
+
# a person's name is attested by its surname
|
| 392 |
+
own = norm(c["subject"]["text"])
|
| 393 |
+
cons = [x for x in fr["constraints"] if norm(x) and norm(x) != own and norm(x) not in own]
|
| 394 |
+
linked_other = {norm(s["text"]) for s in fr["subjects"] if norm(s["text"]) != own}
|
| 395 |
+
sent_n = norm(c["sentence"])
|
| 396 |
+
missing = []
|
| 397 |
+
for x in cons:
|
| 398 |
+
nx = norm(x)
|
| 399 |
+
sur = nx.split()[-1] if len(nx.split()) > 1 and len(nx.split()[-1]) >= 4 else None
|
| 400 |
+
# another NAMED entity of the question must be in the binding sentence itself (not merely on a lead): the
|
| 401 |
+
# prize page's "awarded since 1979" says nothing about the person the question asks about
|
| 402 |
+
# v1.3: a YEAR in the question qualifies the event, so it must be in the binding sentence too
|
| 403 |
+
scope = sent_n if (nx in linked_other or YEAR.fullmatch(x.strip())) else where
|
| 404 |
+
if nx not in scope and not (sur and (" %s " % sur) in (" %s " % scope)):
|
| 405 |
+
missing.append(x)
|
| 406 |
+
if missing:
|
| 407 |
+
return False, {"why": "constraint not attested: %s" % missing}
|
| 408 |
+
return True, ev
|
| 409 |
+
|
| 410 |
+
# ---- Q: VERIFY + the whole cycle ----
|
| 411 |
+
def answer(self, q):
|
| 412 |
+
self.stop = "frame"
|
| 413 |
+
fr = self.frame(q)
|
| 414 |
+
if not fr:
|
| 415 |
+
return None
|
| 416 |
+
self.stop = "link"
|
| 417 |
+
if not self.link(fr):
|
| 418 |
+
return None
|
| 419 |
+
self.stop = "bind"
|
| 420 |
+
cands = self.bind(fr)
|
| 421 |
+
if not cands:
|
| 422 |
+
return None
|
| 423 |
+
self.stop = "check"
|
| 424 |
+
best = {}
|
| 425 |
+
for c in cands:
|
| 426 |
+
k = norm(c["value"])
|
| 427 |
+
if k not in best or c["score"] > best[k]["score"]:
|
| 428 |
+
best[k] = c
|
| 429 |
+
ranked = sorted(best.values(), key=lambda c: -c["score"])
|
| 430 |
+
passed = []
|
| 431 |
+
self.why = []
|
| 432 |
+
for c in ranked[:6]:
|
| 433 |
+
ok, ev = self.check(fr, c)
|
| 434 |
+
if ok:
|
| 435 |
+
c["check"] = ev
|
| 436 |
+
passed.append(c)
|
| 437 |
+
else:
|
| 438 |
+
self.why.append((c["value"], ev.get("why")))
|
| 439 |
+
if not passed:
|
| 440 |
+
return None
|
| 441 |
+
top = passed[0]
|
| 442 |
+
if len(passed) > 1 and passed[1]["score"] >= top["score"] - 1e-9:
|
| 443 |
+
self.stop = "tie"
|
| 444 |
+
return None # two checked candidates tie: nothing is said
|
| 445 |
+
self.stop = "answered"
|
| 446 |
+
return {"answer": top["value"], "claim": top["claim"], "sentence": top["sentence"], "subject": top["subject"]["text"],
|
| 447 |
+
"type": fr["atype"], "head": fr["head"], "check": top["check"], "frame": {k: fr[k] for k in ("atype", "head", "rel")}}
|
root/wander-train/oracle_game_combine_v1.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""ORACLE GAME COMBINATORS - operators that take games and return games.
|
| 3 |
+
|
| 4 |
+
Twelve categories with parameters is a large space. It is still a flat one: every
|
| 5 |
+
game it can build is one of twelve shapes wearing different numbers. The move that
|
| 6 |
+
makes it genuinely open is HIGHER ORDER operators, which take a game and give back
|
| 7 |
+
a different game, because then the space closes under its own operations.
|
| 8 |
+
|
| 9 |
+
Two of them, and they are chosen because both are real games people actually play
|
| 10 |
+
rather than formal curiosities:
|
| 11 |
+
|
| 12 |
+
INVERT the win condition is flipped. Get three in a row and you LOSE,
|
| 13 |
+
which is a real game (misere noughts and crosses) with completely
|
| 14 |
+
different play from the one it inverts. Applied to any of the
|
| 15 |
+
twelve, not just to take-away where it was hand-written.
|
| 16 |
+
|
| 17 |
+
COMPOUND play game A, then game B, and whoever won more takes the match.
|
| 18 |
+
"Tic tac toe then nim" is not in any catalogue, is not a
|
| 19 |
+
parameter setting of anything, and is a perfectly well defined
|
| 20 |
+
game. This is what combining categories means.
|
| 21 |
+
|
| 22 |
+
Both come with their own referees, built by composing the referees underneath
|
| 23 |
+
them, so a combined game is checked the same way a simple one is: the engine
|
| 24 |
+
plays, the referee replays, and the emitted program runs in a separate process.
|
| 25 |
+
A combination is only allowed out if all three agree about it.
|
| 26 |
+
|
| 27 |
+
The count matters less than the closure. Twelve categories give twelve games; with
|
| 28 |
+
INVERT that is twenty-four; with COMPOUND over ordered pairs it is 24 x 24 before
|
| 29 |
+
any parameter is chosen, and the parameters are still free. Nothing in that space
|
| 30 |
+
is stored.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import re
|
| 36 |
+
|
| 37 |
+
from oracle_game_kernel_v1 import GameError, play
|
| 38 |
+
|
| 39 |
+
COMPOUND_BUDGET = 1200
|
| 40 |
+
from oracle_game_vocab_v1 import CATS
|
| 41 |
+
|
| 42 |
+
_FLIP = {'first': 'second', 'second': 'first', 'draw': 'draw',
|
| 43 |
+
'solved': 'failed', 'failed': 'solved'}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ── INVERT ───────────────────────────────────────────────────────────────────
|
| 47 |
+
def invert_build(inner_name, params):
|
| 48 |
+
inner = CATS[inner_name]['build'](params)
|
| 49 |
+
base_outcome = inner['outcome']
|
| 50 |
+
|
| 51 |
+
def outcome(st):
|
| 52 |
+
got = base_outcome(st)
|
| 53 |
+
return _FLIP[got] if got is not None else None
|
| 54 |
+
|
| 55 |
+
return dict(inner, outcome=outcome)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def invert_referee(inner_name, params, log):
|
| 59 |
+
got = CATS[inner_name]['referee'](params, log)
|
| 60 |
+
return _FLIP[got]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ── COMPOUND ─────────────────────────────────────────────────────────────────
|
| 64 |
+
def compound_build(a_name, b_name, a_params, b_params):
|
| 65 |
+
"""Play A to a result, then B, then compare. One game, two halves."""
|
| 66 |
+
ga = CATS[a_name]['build'](a_params)
|
| 67 |
+
gb = CATS[b_name]['build'](b_params)
|
| 68 |
+
|
| 69 |
+
def score_of(res):
|
| 70 |
+
return {'first': 1, 'solved': 1, 'second': -1, 'failed': -1,
|
| 71 |
+
'draw': 0}[res]
|
| 72 |
+
|
| 73 |
+
# Every move is TAGGED with the half it belongs to, so the kernel's own move
|
| 74 |
+
# log is already splittable by the referee. The first version kept a private
|
| 75 |
+
# tagged log inside the state and handed the referee the kernel's untagged
|
| 76 |
+
# one, which is why every compound died on unpacking.
|
| 77 |
+
def init():
|
| 78 |
+
return ('a', ga['init'](), None, 0)
|
| 79 |
+
|
| 80 |
+
def moves(st):
|
| 81 |
+
leg, inner = st[0], st[1]
|
| 82 |
+
g = ga if leg == 'a' else gb
|
| 83 |
+
legal = [(leg, m) for m in g['moves'](inner)]
|
| 84 |
+
if not legal and leg == 'a':
|
| 85 |
+
# the first half ran out of legal moves without declaring a result.
|
| 86 |
+
# Returning nothing here makes the KERNEL call the whole compound a
|
| 87 |
+
# draw and the second half never happens, which the referee then
|
| 88 |
+
# rightly refuses. Hand back a marker so the transition still occurs.
|
| 89 |
+
return [('a', None)]
|
| 90 |
+
return legal
|
| 91 |
+
|
| 92 |
+
def apply(st, tagged, t):
|
| 93 |
+
leg, inner, first_res, count = st
|
| 94 |
+
_tag, m = tagged
|
| 95 |
+
if m is None: # the exhaustion marker from moves()
|
| 96 |
+
return ('b', gb['init'](), ga['outcome'](inner) or 'draw', 0)
|
| 97 |
+
g = ga if leg == 'a' else gb
|
| 98 |
+
inner2 = g['apply'](inner, m, count) # the INNER half's own turn
|
| 99 |
+
done = g['outcome'](inner2)
|
| 100 |
+
if done is not None and leg == 'a':
|
| 101 |
+
return ('b', gb['init'](), done, 0)
|
| 102 |
+
return (leg, inner2, first_res, count + 1)
|
| 103 |
+
|
| 104 |
+
def outcome(st):
|
| 105 |
+
leg, inner, first_res, _count = st
|
| 106 |
+
if leg == 'a':
|
| 107 |
+
return None
|
| 108 |
+
done = gb['outcome'](inner)
|
| 109 |
+
if done is None:
|
| 110 |
+
return None
|
| 111 |
+
total = score_of(first_res) + score_of(done)
|
| 112 |
+
return 'first' if total > 0 else 'second' if total < 0 else 'draw'
|
| 113 |
+
|
| 114 |
+
def policy(st, legal, t):
|
| 115 |
+
return legal[0] if st[3] % 2 == 0 else legal[-1]
|
| 116 |
+
|
| 117 |
+
return {'init': init, 'moves': moves, 'apply': apply, 'outcome': outcome,
|
| 118 |
+
'policy': policy}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def compound_referee(a_name, b_name, a_params, b_params, log):
|
| 122 |
+
"""Split the tagged log and hand each half to the referee that owns it."""
|
| 123 |
+
a_log = [m for leg, m in log if leg == 'a' and m is not None]
|
| 124 |
+
b_log = [m for leg, m in log if leg == 'b' and m is not None]
|
| 125 |
+
if not a_log or not b_log:
|
| 126 |
+
raise GameError('referee: one half of the compound never happened')
|
| 127 |
+
ra = CATS[a_name]['referee'](a_params, a_log)
|
| 128 |
+
rb = CATS[b_name]['referee'](b_params, b_log)
|
| 129 |
+
sc = {'first': 1, 'solved': 1, 'second': -1, 'failed': -1, 'draw': 0}
|
| 130 |
+
total = sc[ra] + sc[rb]
|
| 131 |
+
return 'first' if total > 0 else 'second' if total < 0 else 'draw'
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ── routing the combinations ─────────────────────────────────────────────────
|
| 135 |
+
_INVERT_CUE = re.compile(
|
| 136 |
+
r'\b(?:misere|mis[eè]re)\b|you lose|makes you lose|loses instead'
|
| 137 |
+
r'|whoever .{0,24}loses|reverse(?:d)? (?:rules|win)|inverted'
|
| 138 |
+
r'|the loser wins|last one loses', re.I)
|
| 139 |
+
_COMPOUND_CUE = re.compile(
|
| 140 |
+
r'\bthen\b|\bfollowed by\b|\band then\b|\bboth\b.{0,20}\bgames?\b'
|
| 141 |
+
r'|combin(?:e|ed|ation) of', re.I)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def find_categories(surface):
|
| 145 |
+
"""Every category the surface names, in the order it names them."""
|
| 146 |
+
hits = []
|
| 147 |
+
for name, c in CATS.items():
|
| 148 |
+
for rx in c['cues']:
|
| 149 |
+
m = rx.search(surface)
|
| 150 |
+
if m:
|
| 151 |
+
hits.append((m.start(), name))
|
| 152 |
+
break
|
| 153 |
+
hits.sort()
|
| 154 |
+
out = []
|
| 155 |
+
for _pos, name in hits:
|
| 156 |
+
if name not in out:
|
| 157 |
+
out.append(name)
|
| 158 |
+
return out
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def parse_combination(surface):
|
| 162 |
+
"""(kind, payload) or None if this is not a combination request.
|
| 163 |
+
|
| 164 |
+
kind is 'invert' or 'compound'. Raises GameError when the request clearly
|
| 165 |
+
wants a combination but the pieces do not resolve, so it can decline with a
|
| 166 |
+
reason rather than quietly build one half.
|
| 167 |
+
"""
|
| 168 |
+
names = find_categories(surface)
|
| 169 |
+
wants_compound = bool(_COMPOUND_CUE.search(surface))
|
| 170 |
+
wants_invert = bool(_INVERT_CUE.search(surface))
|
| 171 |
+
|
| 172 |
+
if wants_compound:
|
| 173 |
+
real = [n for n in names if n != 'misere_take_away']
|
| 174 |
+
if len(real) < 2:
|
| 175 |
+
raise GameError('that asks for two games joined together and I can '
|
| 176 |
+
'only find one of them in it')
|
| 177 |
+
a, b = real[0], real[1]
|
| 178 |
+
pa = CATS[a]['params'](surface)
|
| 179 |
+
pb = CATS[b]['params'](surface)
|
| 180 |
+
return 'compound', (a, b, pa, pb)
|
| 181 |
+
|
| 182 |
+
if wants_invert and names:
|
| 183 |
+
base = [n for n in names if n != 'misere_take_away']
|
| 184 |
+
if not base:
|
| 185 |
+
return None # plain misere nim, already a category
|
| 186 |
+
n = base[0]
|
| 187 |
+
return 'invert', (n, CATS[n]['params'](surface))
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def describe(kind, payload):
|
| 192 |
+
if kind == 'invert':
|
| 193 |
+
n, p = payload
|
| 194 |
+
return ('%s, with the win condition inverted: doing the thing that '
|
| 195 |
+
'normally wins LOSES' % CATS[n]['describe'](p))
|
| 196 |
+
a, b, pa, pb = payload
|
| 197 |
+
return ('two games in one match: first %s, then %s, and whoever wins more '
|
| 198 |
+
'takes it' % (CATS[a]['describe'](pa), CATS[b]['describe'](pb)))
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def build_and_referee(kind, payload):
|
| 202 |
+
if kind == 'invert':
|
| 203 |
+
n, p = payload
|
| 204 |
+
return (invert_build(n, p),
|
| 205 |
+
lambda _params, log: invert_referee(n, p, log), p)
|
| 206 |
+
a, b, pa, pb = payload
|
| 207 |
+
return (compound_build(a, b, pa, pb),
|
| 208 |
+
lambda _params, log: compound_referee(a, b, pa, pb, log),
|
| 209 |
+
{'a': a, 'b': b})
|
root/wander-train/oracle_program_ir_v1.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""oracle_program_ir_v1 - one intermediate representation; every request compiles to a bounded relational program.
|
| 3 |
+
|
| 4 |
+
The five things that looked like separate engineering tracks - corpus reach, coding, conversation, prose,
|
| 5 |
+
self-repair - are one missing operation. A request must compile into explicit semantic OBLIGATIONS, those
|
| 6 |
+
obligations into a bounded relational PROGRAM over stores, operators, discourse state and permitted inference,
|
| 7 |
+
and the completed result must be verified AGAINST THE OBLIGATIONS before anything is rendered.
|
| 8 |
+
|
| 9 |
+
I found this while working on coding and then confined it to coding. The coding fold says: the request contains
|
| 10 |
+
its own specification, and testing a program against its own behaviour verifies the wrong proposition. Every other
|
| 11 |
+
capability makes the identical error. Asked why a compiler needs a parser, the model checks whether retrieved
|
| 12 |
+
sentences match the surface wording. Asked whether oxygen is a cause or a result, it checks a taxonomic exclusion.
|
| 13 |
+
Asked to compare photosynthesis with respiration, it retrieves individually warranted fragments and loses the
|
| 14 |
+
comparison subject. In each case the local claims are valid and the executed answer is not bound to what was
|
| 15 |
+
asked - the same failure as returning a bank-account class for a stack and reporting that it executed.
|
| 16 |
+
|
| 17 |
+
THE CONSEQUENCE THAT MATTERS MOST: an answer need not exist as a stored sentence. It exists as a warranted path
|
| 18 |
+
through stored relations. No sentence anywhere says why a compiler needs a parser; the answer is a join across
|
| 19 |
+
what a compiler consumes and what a parser produces. Measuring whether the answer STRING is in the store - which
|
| 20 |
+
is what every coverage probe here has done, mine included - can only ever report a coverage problem. That is the
|
| 21 |
+
difference between possessing documents and possessing knowledge.
|
| 22 |
+
|
| 23 |
+
utterance -> obligations -> relational program -> execution trace -> answer-state -> expression
|
| 24 |
+
|
| 25 |
+
A successful answer is a verified execution trace satisfying every obligation, not a collection of valid sentences.
|
| 26 |
+
|
| 27 |
+
This module is the representation and the obligation compiler. It executes nothing: an obligation it cannot build
|
| 28 |
+
is itself a finding, and is recorded as `unbuildable` rather than quietly dropped.
|
| 29 |
+
|
| 30 |
+
usage: oracle_program_ir_v1.py [--packet PATH] compile the diagnostic prompts and print their programs
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import json
|
| 36 |
+
import re
|
| 37 |
+
import sys
|
| 38 |
+
from dataclasses import dataclass, field, asdict
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
|
| 41 |
+
WORD = re.compile(r"[A-Za-z][A-Za-z'-]{1,}")
|
| 42 |
+
PROPER = re.compile(r"\b([A-Z][\w'-]+(?:\s+(?:of|the|de)\s+[A-Z][\w'-]+|\s+[A-Z][\w'-]+)*)")
|
| 43 |
+
PRONOUN = re.compile(r"\b(it|its|that|this|they|them|those|these|one)\b", re.I)
|
| 44 |
+
NEGATION = re.compile(r"\b(not|isn't|aren't|doesn't|don't|never|no longer|rather than|instead of)\b", re.I)
|
| 45 |
+
PAST = re.compile(r"\b(was|were|did|had|used to|formerly|originally)\b", re.I)
|
| 46 |
+
QUANTIFIER = re.compile(r"\b(all|every|each|some|any|most|three|two|both|several|many|few|\d+)\b", re.I)
|
| 47 |
+
# A capital letter at the start of a sentence is not a name. Taking "What", "Compare", "Is" as entities is the
|
| 48 |
+
# same head-word disease this whole line exists to fix, and the first version of this file had it.
|
| 49 |
+
NOT_A_NAME = {'what', 'which', 'who', 'when', 'where', 'why', 'how', 'is', 'are', 'was', 'were', 'do', 'does',
|
| 50 |
+
'did', 'can', 'could', 'will', 'would', 'should', 'tell', 'give', 'list', 'name', 'compare',
|
| 51 |
+
'explain', 'describe', 'summarize', 'summarise', 'correct', 'the', 'a', 'an', 'so', 'and', 'but',
|
| 52 |
+
'if', 'that', 'this', 'it', 'they', 'in', 'on', 'at', 'for', 'write', 'solve', 'find'}
|
| 53 |
+
# word-bounded and applied to the LOWERCASED utterance (fixed 2026-09-23): unbounded, it matched the tail of every
|
| 54 |
+
# capitalised word, so "Summarize the Internet" yielded the subjects 'ummarize' and 'nternet'
|
| 55 |
+
CONTENT = re.compile(r"\b([a-z][a-z'-]{3,})\b")
|
| 56 |
+
FUNCTION = {'about', 'from', 'with', 'into', 'than', 'then', 'have', 'been', 'does', 'made', 'more', 'most',
|
| 57 |
+
'much', 'many', 'some', 'such', 'like', 'also', 'just', 'over', 'under', 'between', 'because',
|
| 58 |
+
'there', 'here', 'them', 'their', 'your', 'mine', 'ours', 'wrong', 'right', 'mean', 'means'}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class Obligation:
|
| 63 |
+
"""something the answer must satisfy to BE an answer. Verification is against these, never against fragments."""
|
| 64 |
+
name: str
|
| 65 |
+
kind: str # resolve | retrieve | relate | join | compute | contrast | decide | explain
|
| 66 |
+
argument: str = ''
|
| 67 |
+
satisfied_by: str = '' # filled at execution: what satisfied it
|
| 68 |
+
satisfied: bool | None = None # None until executed
|
| 69 |
+
note: str = ''
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclass
|
| 73 |
+
class Request:
|
| 74 |
+
"""the intermediate representation. One shape; the program TYPE varies over it."""
|
| 75 |
+
utterance: str
|
| 76 |
+
program_type: str = 'unknown'
|
| 77 |
+
entities: list = field(default_factory=list)
|
| 78 |
+
discourse_referents: list = field(default_factory=list) # pronouns needing binding from prior turns
|
| 79 |
+
requested_relation: str = ''
|
| 80 |
+
requested_operation: str = ''
|
| 81 |
+
quantifiers: list = field(default_factory=list)
|
| 82 |
+
polarity: str = 'positive'
|
| 83 |
+
tense: str = 'present'
|
| 84 |
+
obligations: list = field(default_factory=list)
|
| 85 |
+
completion_criteria: list = field(default_factory=list)
|
| 86 |
+
content_subjects: list = field(default_factory=list)
|
| 87 |
+
unbuildable: list = field(default_factory=list)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# Each program type declares the obligations its answers must satisfy. These are the SHAPES of questions, and a
|
| 91 |
+
# kernel slot is an executable question-shape rather than a prose template: definition, constitution, mechanism,
|
| 92 |
+
# contrast, criterion, limitation, consequence.
|
| 93 |
+
PROGRAM_TYPES = {
|
| 94 |
+
'definition': ['resolve:subject', 'retrieve:genus', 'retrieve:differentia', 'explain:definition'],
|
| 95 |
+
'constitution': ['resolve:subject', 'relate:part_of', 'explain:composition'],
|
| 96 |
+
'mechanism': ['resolve:subject', 'relate:process', 'join:steps', 'explain:mechanism'],
|
| 97 |
+
'cause': ['resolve:subject', 'relate:causal_edge', 'decide:cause_vs_result', 'explain:causation'],
|
| 98 |
+
'dependence': ['resolve:dependent', 'resolve:dependency', 'retrieve:consumes', 'retrieve:produces',
|
| 99 |
+
'join:through_intermediate', 'decide:dependency_follows', 'explain:why_needed'],
|
| 100 |
+
'comparison': ['resolve:left', 'resolve:right', 'retrieve:left_properties', 'retrieve:right_properties',
|
| 101 |
+
'contrast:shared_dimension', 'explain:difference'],
|
| 102 |
+
'counterfactual': ['resolve:subject', 'relate:causal_edge', 'decide:antecedent_removed',
|
| 103 |
+
'explain:consequence'],
|
| 104 |
+
'enumeration': ['resolve:subject', 'retrieve:n_facts', 'decide:distinctness', 'explain:list'],
|
| 105 |
+
'summarization': ['resolve:scope', 'retrieve:claims_in_scope', 'contrast:redundancy', 'explain:summary'],
|
| 106 |
+
'correction': ['resolve:prior_claim', 'retrieve:contrary_evidence', 'decide:which_holds',
|
| 107 |
+
'explain:correction'],
|
| 108 |
+
'coding': ['resolve:specification', 'compute:program', 'decide:satisfies_specification',
|
| 109 |
+
'explain:program'],
|
| 110 |
+
'computation': ['resolve:operands', 'resolve:operation', 'compute:exact', 'explain:steps'],
|
| 111 |
+
'argumentation': ['resolve:thesis', 'retrieve:premises', 'join:dependency_graph', 'contrast:objections',
|
| 112 |
+
'decide:thesis_supported', 'explain:argument'],
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
# surface cues -> program type. Deliberately small and readable: this is a claim about which question is being
|
| 116 |
+
# asked, and a wrong claim here should be legible in the trace rather than buried.
|
| 117 |
+
CUES = [
|
| 118 |
+
('dependence', re.compile(r"\bwhy (?:does|do|is|are|would).{0,40}\b(?:need|require|depend|use)\b", re.I)),
|
| 119 |
+
('dependence', re.compile(r"\b(?:need|require|depend on)\b.{0,30}\?", re.I)),
|
| 120 |
+
('counterfactual', re.compile(r"\bwhat if\b|\bwould happen if\b|\bhad .{0,20} not\b|\bwithout\b.{0,30}\?", re.I)),
|
| 121 |
+
('cause', re.compile(r"\bwhat causes?\b|\bwhy (?:does|do|is|are)\b|\bcause or (?:a )?result\b", re.I)),
|
| 122 |
+
('comparison', re.compile(r"\bcompare\b|\bdifference between\b|\bhow is .{0,30} different\b|\bversus\b|\bvs\.?\b", re.I)),
|
| 123 |
+
('correction', re.compile(r"\bcorrect me\b|\bthat'?s wrong\b|\bactually,?\b|\bisn'?t it\b", re.I)),
|
| 124 |
+
('summarization', re.compile(r"\bsummari[sz]e\b|\bwhat have we\b|\bso far\b|\brecap\b", re.I)),
|
| 125 |
+
('enumeration', re.compile(r"\blist\b|\bname (?:three|two|\d+)\b|\bgive me (?:three|two|\d+)\b", re.I)),
|
| 126 |
+
('mechanism', re.compile(r"\bhow does\b|\bhow do\b|\bhow is .{0,20} made\b|\bwork\b\s*\?", re.I)),
|
| 127 |
+
('constitution', re.compile(r"\bmade of\b|\bcomposed of\b|\bparts of\b|\bconsists? of\b", re.I)),
|
| 128 |
+
('computation', re.compile(r"\d.{0,20}(?:%|percent|times|plus|minus|divided)|\bsquare root\b|\bsolve for\b", re.I)),
|
| 129 |
+
('coding', re.compile(r"\bwrite a (?:python|javascript|java|c|go|sql)\b|\bfunction that\b|\bclass for\b", re.I)),
|
| 130 |
+
('definition', re.compile(r"^\s*what (?:is|are|was|were)\b|\btell me about\b|\bwhat'?s\b", re.I)),
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def classify(utterance):
|
| 135 |
+
for name, rx in CUES:
|
| 136 |
+
if rx.search(utterance or ''):
|
| 137 |
+
return name
|
| 138 |
+
return 'unknown'
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def compile_request(utterance, discourse_topics=()):
|
| 142 |
+
"""utterance -> Request with its obligations. Executes nothing."""
|
| 143 |
+
u = ' '.join(str(utterance or '').split())
|
| 144 |
+
r = Request(utterance=u)
|
| 145 |
+
r.program_type = classify(u)
|
| 146 |
+
# names, minus anything that is only capitalised because it began the sentence
|
| 147 |
+
r.entities = [m.group(1) for m in PROPER.finditer(u)
|
| 148 |
+
if m.group(1).split()[0].lower() not in NOT_A_NAME]
|
| 149 |
+
# lowercase content nouns are subjects too - "compare photosynthesis with respiration" names two things and
|
| 150 |
+
# capitalises neither
|
| 151 |
+
r.content_subjects = [w for w in CONTENT.findall(u.lower()) if w not in FUNCTION and w not in NOT_A_NAME]
|
| 152 |
+
r.discourse_referents = sorted({m.group(1).lower() for m in PRONOUN.finditer(u)})
|
| 153 |
+
r.quantifiers = sorted({m.group(1).lower() for m in QUANTIFIER.finditer(u)})
|
| 154 |
+
r.polarity = 'negative' if NEGATION.search(u) else 'positive'
|
| 155 |
+
r.tense = 'past' if PAST.search(u) else 'present'
|
| 156 |
+
|
| 157 |
+
# the requested relation is what the answer must ESTABLISH, not what the words resemble
|
| 158 |
+
if r.program_type == 'dependence':
|
| 159 |
+
r.requested_relation = 'functional_dependence'
|
| 160 |
+
elif r.program_type in ('cause', 'counterfactual'):
|
| 161 |
+
r.requested_relation = 'causal'
|
| 162 |
+
elif r.program_type == 'comparison':
|
| 163 |
+
r.requested_relation = 'contrast_on_shared_dimension'
|
| 164 |
+
elif r.program_type in ('definition', 'constitution'):
|
| 165 |
+
r.requested_relation = 'taxonomic'
|
| 166 |
+
r.requested_operation = r.program_type
|
| 167 |
+
|
| 168 |
+
template = PROGRAM_TYPES.get(r.program_type)
|
| 169 |
+
if not template:
|
| 170 |
+
r.unbuildable.append('no program type recognised for this utterance')
|
| 171 |
+
return r
|
| 172 |
+
|
| 173 |
+
# the SUBJECTS of the program: named entities, else the discourse referents that must be bound from prior turns
|
| 174 |
+
subjects = list(r.entities) + [c for c in r.content_subjects if c not in
|
| 175 |
+
{e.lower() for e in r.entities}]
|
| 176 |
+
for step in template:
|
| 177 |
+
kind, _, arg = step.partition(':')
|
| 178 |
+
ob = Obligation(name=step, kind=kind, argument=arg)
|
| 179 |
+
if kind == 'resolve':
|
| 180 |
+
if r.discourse_referents and not subjects:
|
| 181 |
+
# THE FAILURE THE TRACES KEEP SHOWING: the act is identified and its subject is a pronoun that
|
| 182 |
+
# nothing binds. Recorded as an obligation with a named source, not as a dead end.
|
| 183 |
+
ob.note = 'subject is a discourse referent (%s); must bind from prior turns' % ', '.join(r.discourse_referents)
|
| 184 |
+
elif subjects:
|
| 185 |
+
ob.note = 'candidate: %s' % subjects[0]
|
| 186 |
+
else:
|
| 187 |
+
ob.note = 'no subject available in the utterance'
|
| 188 |
+
r.obligations.append(ob)
|
| 189 |
+
|
| 190 |
+
r.completion_criteria = ['every obligation satisfied',
|
| 191 |
+
'the established relation is the one requested (%s)' % (r.requested_relation or 'n/a'),
|
| 192 |
+
'each claim carries a warrant',
|
| 193 |
+
'no two admitted claims contradict']
|
| 194 |
+
if r.program_type == 'comparison' and len(subjects) < 2:
|
| 195 |
+
r.unbuildable.append('comparison needs two subjects; found %d' % len(subjects))
|
| 196 |
+
return r
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def verify(request, trace):
|
| 200 |
+
"""check the completed trace against the OBLIGATIONS - never against the fragments it produced."""
|
| 201 |
+
got = {str(k) for k in (trace or {}).keys()}
|
| 202 |
+
unmet = []
|
| 203 |
+
for ob in request.obligations:
|
| 204 |
+
ob.satisfied = ob.name in got
|
| 205 |
+
ob.satisfied_by = str((trace or {}).get(ob.name, ''))[:120]
|
| 206 |
+
if not ob.satisfied:
|
| 207 |
+
unmet.append(ob.name)
|
| 208 |
+
return {'satisfied': len(request.obligations) - len(unmet), 'total': len(request.obligations),
|
| 209 |
+
'unmet': unmet, 'answerable': not unmet}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def main():
|
| 213 |
+
a = sys.argv[1:]
|
| 214 |
+
packet = a[a.index('--packet') + 1] if '--packet' in a else '/root/fresh_conv_test.json'
|
| 215 |
+
prompts = []
|
| 216 |
+
try:
|
| 217 |
+
d = json.loads(Path(packet).read_text(encoding='utf-8'))
|
| 218 |
+
for name, turns in d.items():
|
| 219 |
+
for t in (turns if isinstance(turns, list) else []):
|
| 220 |
+
q = t[0] if isinstance(t, (list, tuple)) and t else (t if isinstance(t, str) else '')
|
| 221 |
+
if q:
|
| 222 |
+
prompts.append((name, q))
|
| 223 |
+
except Exception as exc: # noqa: BLE001
|
| 224 |
+
print('packet unreadable (%s); using built-in examples' % exc)
|
| 225 |
+
prompts = [('example', 'Why does a compiler need a parser?'),
|
| 226 |
+
('example', 'Is oxygen a cause or a result of photosynthesis?'),
|
| 227 |
+
('example', 'Compare photosynthesis with respiration.'),
|
| 228 |
+
('example', 'List three facts about it.')]
|
| 229 |
+
|
| 230 |
+
print('%d prompts\n' % len(prompts))
|
| 231 |
+
unknown = 0
|
| 232 |
+
unbound = 0
|
| 233 |
+
for name, q in prompts:
|
| 234 |
+
r = compile_request(q)
|
| 235 |
+
if r.program_type == 'unknown':
|
| 236 |
+
unknown += 1
|
| 237 |
+
needs_binding = [o for o in r.obligations if 'discourse referent' in o.note]
|
| 238 |
+
if needs_binding:
|
| 239 |
+
unbound += 1
|
| 240 |
+
print('%-14s %-46s -> %s' % (name, q[:44], r.program_type))
|
| 241 |
+
print(' relation : %s | entities: %s | referents: %s'
|
| 242 |
+
% (r.requested_relation or '-', ', '.join(r.entities[:3]) or '-',
|
| 243 |
+
', '.join(r.discourse_referents) or '-'))
|
| 244 |
+
print(' program : %s' % ' -> '.join(o.name for o in r.obligations))
|
| 245 |
+
for o in needs_binding:
|
| 246 |
+
print(' UNBOUND : %s (%s)' % (o.name, o.note))
|
| 247 |
+
for u in r.unbuildable:
|
| 248 |
+
print(' UNBUILDABLE: %s' % u)
|
| 249 |
+
print()
|
| 250 |
+
print('%d prompts | %d had no recognised program type | %d have an unbound subject'
|
| 251 |
+
% (len(prompts), unknown, unbound))
|
| 252 |
+
return 0
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
if __name__ == '__main__':
|
| 256 |
+
raise SystemExit(main())
|
root/wander-train/oracle_release_conceptnet.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""R1 — ConceptNet commonsense adapter for the release runtime.
|
| 3 |
+
|
| 4 |
+
Typed edge retrieval over the sealed M13e5 ConceptNet specialist
|
| 5 |
+
(m13e5_conceptnet_build_a_v1, index sha 5e28406c...), rendered with provenance.
|
| 6 |
+
Zero-model: SQL + deterministic rendering only. Speech uses ConceptNet's own
|
| 7 |
+
crowd-authored surface_text where present (attributed), otherwise a typed
|
| 8 |
+
relation template. Every released line carries its edge provenance; empty
|
| 9 |
+
retrieval is an explicit abstention, never a guess.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import re
|
| 15 |
+
import sqlite3
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import Any
|
| 18 |
+
|
| 19 |
+
PHASE = Path(__file__).resolve().parents[1] / "fable-content" / "oracle-m-series-2026-08-15"
|
| 20 |
+
DEFAULT_BUILD = PHASE / "m13e5_conceptnet_build_a_v1" / "conceptnet_specialist.sqlite3"
|
| 21 |
+
|
| 22 |
+
# Relation -> (question forms it answers, deterministic template)
|
| 23 |
+
RELATION_TEMPLATES: dict[str, str] = {
|
| 24 |
+
"IsA": "{start} is a kind of {end}",
|
| 25 |
+
"PartOf": "{start} is part of {end}",
|
| 26 |
+
"HasA": "{start} has {end}",
|
| 27 |
+
"UsedFor": "{start} is used for {end}",
|
| 28 |
+
"CapableOf": "{start} can {end}",
|
| 29 |
+
"AtLocation": "{start} is typically found at {end}",
|
| 30 |
+
"Causes": "{start} causes {end}",
|
| 31 |
+
"HasSubevent": "{start} involves {end}",
|
| 32 |
+
"HasPrerequisite": "{start} requires {end}",
|
| 33 |
+
"MotivatedByGoal": "{start} is motivated by {end}",
|
| 34 |
+
"Desires": "{start} desires {end}",
|
| 35 |
+
"MadeOf": "{start} is made of {end}",
|
| 36 |
+
"ReceivesAction": "{start} can be {end}",
|
| 37 |
+
"HasProperty": "{start} is {end}",
|
| 38 |
+
"Synonym": "{start} is a synonym of {end}",
|
| 39 |
+
"Antonym": "{start} is an antonym of {end}",
|
| 40 |
+
"DistinctFrom": "{start} is distinct from {end}",
|
| 41 |
+
"SimilarTo": "{start} is similar to {end}",
|
| 42 |
+
"RelatedTo": "{start} is related to {end}",
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
QUESTION_ROUTES: list[tuple[re.Pattern[str], list[str]]] = [
|
| 46 |
+
(re.compile(r"^what is (?:a |an |the )?(.+?) used for\??$", re.I), ["UsedFor"]),
|
| 47 |
+
(re.compile(r"^what can (?:a |an |the )?(.+?) do\??$", re.I), ["CapableOf"]),
|
| 48 |
+
(re.compile(r"^where (?:would you |do you |can you )?find (?:a |an |the )?(.+?)\??$", re.I), ["AtLocation"]),
|
| 49 |
+
(re.compile(r"^where is (?:a |an |the )?(.+?) (?:usually |typically )?found\??$", re.I), ["AtLocation"]),
|
| 50 |
+
(re.compile(r"^what (?:does|do) (?:a |an |the )?(.+?) have\??$", re.I), ["HasA"]),
|
| 51 |
+
(re.compile(r"^what is (?:a |an |the )?(.+?) made of\??$", re.I), ["MadeOf"]),
|
| 52 |
+
(re.compile(r"^what causes (?:a |an |the )?(.+?)\??$", re.I), ["Causes:end"]),
|
| 53 |
+
(re.compile(r"^what (?:does|do) (?:a |an |the )?(.+?) cause\??$", re.I), ["Causes"]),
|
| 54 |
+
(re.compile(r"^what kind of thing is (?:a |an |the )?(.+?)\??$", re.I), ["IsA"]),
|
| 55 |
+
(re.compile(r"^what (?:is|are) (?:a |an |the )?(.+?) (?:a )?(?:kind|type|sort) of\??$", re.I), ["IsA"]),
|
| 56 |
+
(re.compile(r"^what is (?:a |an |the )?(.+?) part of\??$", re.I), ["PartOf"]),
|
| 57 |
+
(re.compile(r"^what (?:is|are) (?:the )?parts? of (?:a |an |the )?(.+?)\??$", re.I), ["PartOf:end", "HasA"]),
|
| 58 |
+
(re.compile(r"^what do(?:es)? (?:a |an |the )?(.+?) require\??$", re.I), ["HasPrerequisite"]),
|
| 59 |
+
(re.compile(r"^what is (?:a |an |the )?(.+?) similar to\??$", re.I), ["SimilarTo", "Synonym"]),
|
| 60 |
+
(re.compile(r"^what is (?:the )?opposite of (?:a |an |the )?(.+?)\??$", re.I), ["Antonym"]),
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
_SURFACE_BRACKETS = re.compile(r"\[\[|\]\]")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class ConceptNetAdapter:
|
| 67 |
+
def __init__(self, build_path: Path | str = DEFAULT_BUILD):
|
| 68 |
+
build_path = Path(build_path)
|
| 69 |
+
if not build_path.is_file():
|
| 70 |
+
raise FileNotFoundError(str(build_path))
|
| 71 |
+
uri = build_path.resolve().as_uri() + "?mode=ro&immutable=1"
|
| 72 |
+
self.connection = sqlite3.connect(uri, uri=True)
|
| 73 |
+
|
| 74 |
+
# ── retrieval ────────────────────────────────────────────────────────
|
| 75 |
+
def edges(self, concept: str, relation: str, *, reverse: bool = False,
|
| 76 |
+
limit: int = 6) -> list[dict[str, Any]]:
|
| 77 |
+
concept = concept.strip().lower()
|
| 78 |
+
column, other = ("end", "start") if reverse else ("start", "end")
|
| 79 |
+
rows = self.connection.execute(
|
| 80 |
+
f"SELECT rel, start, end, weight, surface_text, dataset FROM edge "
|
| 81 |
+
f"WHERE {column}=? AND rel=? ORDER BY weight DESC, start, end LIMIT ?",
|
| 82 |
+
(concept, relation, limit)).fetchall()
|
| 83 |
+
return [{"rel": r[0], "start": r[1], "end": r[2], "weight": r[3],
|
| 84 |
+
"surface_text": r[4], "dataset": r[5],
|
| 85 |
+
"provenance": f"conceptnet57:{r[0]}:{r[1]}->{r[2]}"} for r in rows]
|
| 86 |
+
|
| 87 |
+
def neighborhood(self, concept: str, *, limit_per_relation: int = 4) -> dict[str, list[dict[str, Any]]]:
|
| 88 |
+
out: dict[str, list[dict[str, Any]]] = {}
|
| 89 |
+
for relation in RELATION_TEMPLATES:
|
| 90 |
+
if relation == "RelatedTo":
|
| 91 |
+
continue # too diffuse for a summary; available via edges()
|
| 92 |
+
found = self.edges(concept, relation, limit=limit_per_relation)
|
| 93 |
+
if found:
|
| 94 |
+
out[relation] = found
|
| 95 |
+
return out
|
| 96 |
+
|
| 97 |
+
# ── rendering ────────────────────────────────────────────────────────
|
| 98 |
+
@staticmethod
|
| 99 |
+
def render_edge(edge: dict[str, Any]) -> str:
|
| 100 |
+
surface = (edge.get("surface_text") or "").strip()
|
| 101 |
+
if surface:
|
| 102 |
+
text = _SURFACE_BRACKETS.sub("", surface).lstrip("*").strip()
|
| 103 |
+
if text:
|
| 104 |
+
return text[0].upper() + text[1:].rstrip(".") + "."
|
| 105 |
+
template = RELATION_TEMPLATES.get(edge["rel"], "{start} relates to {end}")
|
| 106 |
+
text = template.format(start=edge["start"], end=edge["end"])
|
| 107 |
+
return text[0].upper() + text[1:] + "."
|
| 108 |
+
|
| 109 |
+
def answer(self, question: str) -> dict[str, Any]:
|
| 110 |
+
"""Route a commonsense question form; abstain explicitly otherwise."""
|
| 111 |
+
text = " ".join(question.strip().split())
|
| 112 |
+
for pattern, relations in QUESTION_ROUTES:
|
| 113 |
+
match = pattern.match(text)
|
| 114 |
+
if not match:
|
| 115 |
+
continue
|
| 116 |
+
concept = match.group(1).strip().lower()
|
| 117 |
+
collected: list[dict[str, Any]] = []
|
| 118 |
+
for spec in relations:
|
| 119 |
+
relation, _, direction = spec.partition(":")
|
| 120 |
+
collected.extend(self.edges(concept, relation, reverse=(direction == "end")))
|
| 121 |
+
if not collected:
|
| 122 |
+
return {"status": "withheld", "operation": "conceptnet_no_admitted_edges",
|
| 123 |
+
"concept": concept,
|
| 124 |
+
"response": f"The commonsense index has no admitted edges answering that about {concept}.",
|
| 125 |
+
"claims": []}
|
| 126 |
+
seen: set[str] = set()
|
| 127 |
+
lines = []
|
| 128 |
+
claims = []
|
| 129 |
+
for edge in collected:
|
| 130 |
+
rendered = self.render_edge(edge)
|
| 131 |
+
if rendered in seen:
|
| 132 |
+
continue
|
| 133 |
+
seen.add(rendered)
|
| 134 |
+
lines.append(rendered)
|
| 135 |
+
claims.append({"provenance": edge["provenance"], "weight": edge["weight"],
|
| 136 |
+
"dataset": edge["dataset"]})
|
| 137 |
+
if len(lines) >= 5:
|
| 138 |
+
break
|
| 139 |
+
return {"status": "answered", "operation": "conceptnet_commonsense",
|
| 140 |
+
"concept": concept, "response": " ".join(lines), "claims": claims}
|
| 141 |
+
return {"status": "unrouted", "operation": "conceptnet_form_not_registered",
|
| 142 |
+
"response": "", "claims": []}
|
root/wander-train/oracle_release_runtime_aa.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime AA — evidence-mined tiered cues for the explanatory routes (additive over Z).
|
| 3 |
+
|
| 4 |
+
M24's cue inventories were authored by guessing and the route stress test showed
|
| 5 |
+
the cost: how 10%, what_causes 28%, when 65% answer-rate. Iteration 4 mined
|
| 6 |
+
400,000 admitted sentences to find the forms the corpus ACTUALLY uses, and the
|
| 7 |
+
guesses had missed the most frequent ones outright:
|
| 8 |
+
|
| 9 |
+
cause : "led to" 1,117 · "so that" 1,012 · "as a result" 372 · "result of" 340
|
| 10 |
+
process : "uses" 3,647 · "when the" 3,580 · "is called" 3,108 · "is/are formed" 263
|
| 11 |
+
date : "was released" 1,038 · "was built" 560 · "took place" 323
|
| 12 |
+
("built" was in the question regex but NOT in the inception cues at all)
|
| 13 |
+
|
| 14 |
+
Widening naively would trade precision for recall, so the cues are TIERED: a
|
| 15 |
+
specific form is preferred, and a broad one is consulted only when no specific
|
| 16 |
+
form matches. Frequency alone does not promote a cue — "makes" is frequent but
|
| 17 |
+
weakly causal, so it sits in the last tier.
|
| 18 |
+
|
| 19 |
+
Also fixes a sentence-quality defect the harness surfaced: sentence extraction
|
| 20 |
+
sometimes yields a fragment starting mid-parenthetical ("1938) is a psychologist
|
| 21 |
+
..."), which is cited and correct but reads as broken. Such fragments are
|
| 22 |
+
skipped in favour of the next admitted sentence."""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import re
|
| 27 |
+
import time
|
| 28 |
+
|
| 29 |
+
from oracle_release_runtime_z import OracleReleaseRuntimeZ
|
| 30 |
+
|
| 31 |
+
# tier 1 = explicitly the relation; tier 2 = strongly indicative; tier 3 = weak fallback
|
| 32 |
+
CAUSE_TIERS = [
|
| 33 |
+
re.compile(r"\b(caused by|is caused|are caused|due to|results? from|triggered by|produced by)\b", re.I),
|
| 34 |
+
re.compile(r"\b(because|led to|leads to|as a result|result of|gives rise to)\b", re.I),
|
| 35 |
+
re.compile(r"\b(causes|makes|so that|therefore|happens when|occurs when)\b", re.I),
|
| 36 |
+
]
|
| 37 |
+
PROCESS_TIERS = [
|
| 38 |
+
re.compile(r"\b(happens when|occurs when|forms? when|is formed|are formed|works? by|by which|through which)\b", re.I),
|
| 39 |
+
re.compile(r"\b(process|is made|are made|begins when|starts when|when there is)\b", re.I),
|
| 40 |
+
re.compile(r"\b(uses|when the|is called|moves|produces)\b", re.I),
|
| 41 |
+
]
|
| 42 |
+
# Date cues are conditioned on the VERB ACTUALLY ASKED, not a fixed tier order.
|
| 43 |
+
# A flat specific-first ordering regressed "When did WWII start?" to "The Tanggu
|
| 44 |
+
# Truce ... was signed in 1933" because `was signed` outranked `began`. The asked
|
| 45 |
+
# verb is the correct selector: it says which event the questioner means.
|
| 46 |
+
VERB_CUES = {
|
| 47 |
+
"start": [r"\b(began|begun|started|broke out|was launched)\b", r"\b(was founded|was established|was created)\b"],
|
| 48 |
+
"begin": [r"\b(began|begun|started|broke out|was launched)\b", r"\b(was founded|was established|was created)\b"],
|
| 49 |
+
"end": [r"\b(ended|finished|collapsed|surrendered|was dissolved)\b"],
|
| 50 |
+
"found": [r"\b(was founded|was established|was created|founded)\b"],
|
| 51 |
+
"built": [r"\b(was built|was constructed|built between|was completed)\b"],
|
| 52 |
+
"build": [r"\b(was built|was constructed|built between|was completed)\b"],
|
| 53 |
+
"discover": [r"\b(was discovered|discovered)\b"],
|
| 54 |
+
"invent": [r"\b(was invented|invented)\b"],
|
| 55 |
+
"born": [r"\b(was born|born)\b"],
|
| 56 |
+
"die": [r"\b(died|was killed)\b"],
|
| 57 |
+
"happen": [r"\b(took place|happened|occurred|was held)\b"],
|
| 58 |
+
"occur": [r"\b(took place|happened|occurred|was held)\b"],
|
| 59 |
+
}
|
| 60 |
+
INCEPT_GENERAL = [
|
| 61 |
+
re.compile(r"\b(began|begun|started|was founded|was built|was established|was created|took place|broke out)\b", re.I),
|
| 62 |
+
re.compile(r"\b(opened|ended|was released|was signed|was discovered|was invented|born|died)\b", re.I),
|
| 63 |
+
]
|
| 64 |
+
YEAR = re.compile(r"\b(1[0-9]{3}|20[0-2][0-9])\b")
|
| 65 |
+
FRAGMENT = re.compile(r"^\s*(?:[a-z]|\d{1,4}\s*[)\]])") # "1938) is ..." / lowercase start
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class OracleReleaseRuntimeAA(OracleReleaseRuntimeZ):
|
| 69 |
+
def _pick_tiered(self, topic: str, tiers: list, *, require_year: bool = False) -> list[dict]:
|
| 70 |
+
bundle = self._bundle(topic, n=60)
|
| 71 |
+
if not bundle:
|
| 72 |
+
singular = topic[:-1] if topic.endswith("s") and not topic.endswith("ss") else None
|
| 73 |
+
if singular and len(singular) > 2:
|
| 74 |
+
bundle = self._bundle(singular, n=60)
|
| 75 |
+
if not bundle:
|
| 76 |
+
return []
|
| 77 |
+
usable = [c for c in bundle if not FRAGMENT.match(str(c.get("sentence") or ""))]
|
| 78 |
+
if not usable:
|
| 79 |
+
usable = bundle
|
| 80 |
+
for tier in tiers: # specific first, broad only as fallback
|
| 81 |
+
hits = []
|
| 82 |
+
for claim in usable:
|
| 83 |
+
sentence = str(claim.get("sentence") or "")
|
| 84 |
+
if require_year and not YEAR.search(sentence):
|
| 85 |
+
continue
|
| 86 |
+
if tier.search(sentence):
|
| 87 |
+
hits.append(claim)
|
| 88 |
+
if hits:
|
| 89 |
+
return hits
|
| 90 |
+
return []
|
| 91 |
+
|
| 92 |
+
def _explanatory_turn(self, text: str) -> dict | None: # override Y
|
| 93 |
+
from oracle_release_runtime_y import CAUSE_Q, HOW_Q, WHEN_Q
|
| 94 |
+
stripped = " ".join(text.strip().split())
|
| 95 |
+
|
| 96 |
+
match = CAUSE_Q.match(stripped)
|
| 97 |
+
if match:
|
| 98 |
+
topic = match.group("t").strip()
|
| 99 |
+
return self._render(topic, self._pick_tiered(topic, CAUSE_TIERS),
|
| 100 |
+
"cause", "capability:what_causes", f"what causes {topic}")
|
| 101 |
+
|
| 102 |
+
match = HOW_Q.match(stripped)
|
| 103 |
+
if match:
|
| 104 |
+
topic = match.group("t").strip()
|
| 105 |
+
return self._render(topic, self._pick_tiered(topic, PROCESS_TIERS),
|
| 106 |
+
"process", "capability:how_does", f"how {topic} works")
|
| 107 |
+
|
| 108 |
+
match = WHEN_Q.match(stripped)
|
| 109 |
+
if match:
|
| 110 |
+
topic = match.group("t").strip()
|
| 111 |
+
verb = (match.group("v") or "").lower()
|
| 112 |
+
tiers = []
|
| 113 |
+
for stem, patterns in VERB_CUES.items():
|
| 114 |
+
if verb.startswith(stem):
|
| 115 |
+
tiers = [re.compile(pat, re.I) for pat in patterns]
|
| 116 |
+
break
|
| 117 |
+
tiers = tiers + INCEPT_GENERAL
|
| 118 |
+
hits = self._pick_tiered(topic, tiers, require_year=True) \
|
| 119 |
+
or self._pick_tiered(topic, [YEAR], require_year=True)
|
| 120 |
+
return self._render(topic, hits, "inception", "capability:when_did",
|
| 121 |
+
f"when {topic} {match.group('v')}")
|
| 122 |
+
return None
|
| 123 |
+
|
| 124 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 125 |
+
started = time.perf_counter()
|
| 126 |
+
result = None
|
| 127 |
+
try:
|
| 128 |
+
result = self._explanatory_turn(text)
|
| 129 |
+
except Exception: # noqa: BLE001
|
| 130 |
+
result = None
|
| 131 |
+
if result is not None:
|
| 132 |
+
result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
|
| 133 |
+
result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
|
| 134 |
+
return result
|
| 135 |
+
return super().chat(text, session_id)
|
root/wander-train/oracle_release_runtime_ac.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime AC — explain-simply picks the identifying sentence (additive over AB).
|
| 3 |
+
|
| 4 |
+
Iteration 6 diagnosis. `_definitional_score` returns 0 for every sentence on a
|
| 5 |
+
biographical page (it keys on copular definition patterns that proper-noun prose
|
| 6 |
+
does not use), so selection fell through to the tiebreak `-len(words)` — the
|
| 7 |
+
SHORTEST sentence wins. That systematically chose trivia and fragments over the
|
| 8 |
+
sentence that identifies the subject:
|
| 9 |
+
|
| 10 |
+
"Explain Paco Gento simply." -> "He played for Spain national team."
|
| 11 |
+
"Explain Arthur C. Clarke simply." -> "He also wrote many short stories."
|
| 12 |
+
"Explain Bernard Montgomery simply."-> "Later, in World War II." (a fragment)
|
| 13 |
+
|
| 14 |
+
In encyclopedic prose the FIRST sentence of a page is by convention the one that
|
| 15 |
+
says what the subject is. So on a score tie the tiebreak now prefers EARLIER
|
| 16 |
+
position rather than fewer words, and sentences that cannot stand alone (no
|
| 17 |
+
finite verb, or opening mid-parenthetical) are skipped. Brevity is still
|
| 18 |
+
preferred among sentences that actually define.
|
| 19 |
+
|
| 20 |
+
Note on the metric that found this: `explain_simply` on-topic looked like the
|
| 21 |
+
same false-drift artifact that `list` produced, but it was not. For an
|
| 22 |
+
"explain X" question, mentioning X is not a proxy for relevance — it IS the
|
| 23 |
+
requirement, because an explanation that never identifies its subject has not
|
| 24 |
+
explained it."""
|
| 25 |
+
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import re
|
| 29 |
+
import time
|
| 30 |
+
|
| 31 |
+
from oracle_release_runtime_ab import OracleReleaseRuntimeAB
|
| 32 |
+
from oracle_release_runtime_m import SIMPLY, _definitional_score
|
| 33 |
+
from stage5m13e8_relevance_gate_v1 import gate_bundle
|
| 34 |
+
|
| 35 |
+
_WORD = chr(92) + "b" # backslash-b built at runtime; heredocs eat the literal
|
| 36 |
+
FINITE_VERB = re.compile(_WORD + r"(is|are|was|were|has|have|had|does|did|can|"
|
| 37 |
+
r"could|will|would|became|becomes|plays|played|wrote|"
|
| 38 |
+
r"made|makes|lives|lived|died|won|led|serves|served)" + _WORD, re.I)
|
| 39 |
+
OPENS_MID = re.compile(r"^\s*(?:[a-z]|\d{1,4}\s*[)\]]|later,|however,|also,|then,)", re.I)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _standalone(sentence: str) -> bool:
|
| 43 |
+
"""Can this sentence stand alone as an identification?"""
|
| 44 |
+
text = str(sentence or "").strip()
|
| 45 |
+
if len(text.split()) < 4 or OPENS_MID.match(text):
|
| 46 |
+
return False
|
| 47 |
+
return bool(FINITE_VERB.search(text))
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class OracleReleaseRuntimeAC(OracleReleaseRuntimeAB):
|
| 51 |
+
def _simply_turn(self, text: str, session_id: str) -> dict | None: # override M
|
| 52 |
+
match = SIMPLY.match(text.strip())
|
| 53 |
+
if not match:
|
| 54 |
+
return None
|
| 55 |
+
topic = (match.group("t") or match.group("t2") or "").strip()
|
| 56 |
+
if not topic:
|
| 57 |
+
return None
|
| 58 |
+
bundle = self._bundle(topic, n=8)
|
| 59 |
+
admitted, _ = gate_bundle(topic, bundle)
|
| 60 |
+
gloss = self._gloss(topic)
|
| 61 |
+
if not admitted and not gloss:
|
| 62 |
+
return {"status": "withheld", "path": "capability:explain_simply",
|
| 63 |
+
"response": f"No provenance-admitted material on {topic} is available to explain.",
|
| 64 |
+
"provenance": {"kind": "simple_register", "topic": topic}}
|
| 65 |
+
parts: list[str] = []
|
| 66 |
+
premises: list[str] = []
|
| 67 |
+
if admitted:
|
| 68 |
+
order = {c["claim_id"]: i for i, c in enumerate(bundle)}
|
| 69 |
+
standalone = [c for c in admitted if _standalone(c.get("sentence"))]
|
| 70 |
+
pool = standalone or admitted
|
| 71 |
+
short_defs = [c for c in pool
|
| 72 |
+
if len(c["sentence"].split()) <= 30
|
| 73 |
+
and _definitional_score(topic, c["sentence"]) >= 6]
|
| 74 |
+
if short_defs:
|
| 75 |
+
lead = max(short_defs, key=lambda c: (_definitional_score(topic, c["sentence"]),
|
| 76 |
+
-len(c["sentence"].split())))
|
| 77 |
+
else:
|
| 78 |
+
# No scored definition (typical of proper-noun pages): take the
|
| 79 |
+
# page's own opening sentence, which is the identifying one.
|
| 80 |
+
lead = min(pool, key=lambda c: order.get(c["claim_id"], 10_000))
|
| 81 |
+
parts.append(f"{lead['sentence']} [{lead['claim_id']}]")
|
| 82 |
+
premises.append(lead["claim_id"])
|
| 83 |
+
if gloss:
|
| 84 |
+
parts.append(f"The dictionary sense: {gloss}. [wiktionary:{topic.lower()}:sense1]")
|
| 85 |
+
premises.append(f"wiktionary:{topic.lower()}")
|
| 86 |
+
return {"status": "answered", "path": "capability:explain_simply",
|
| 87 |
+
"response": "In simple terms: " + " ".join(parts),
|
| 88 |
+
"provenance": {"kind": "simple_register", "topic": topic, "premises": premises}}
|
| 89 |
+
|
| 90 |
+
def _timeline_turn(self, text: str, session_id: str) -> dict | None: # override J
|
| 91 |
+
result = super()._timeline_turn(text, session_id)
|
| 92 |
+
if not result or result.get("status") != "answered":
|
| 93 |
+
return result
|
| 94 |
+
body = str(result.get("response", ""))
|
| 95 |
+
distinct = {y for y in year_tokens(body)}
|
| 96 |
+
if len(distinct) >= 2:
|
| 97 |
+
return result
|
| 98 |
+
topic_note = "the admitted material yields fewer than two distinct dates"
|
| 99 |
+
return {"status": "withheld", "path": "capability:timeline",
|
| 100 |
+
"response": f"A timeline needs at least two distinct dated statements; "
|
| 101 |
+
f"{topic_note}. I will not present a measurement as a date.",
|
| 102 |
+
"provenance": {"kind": "temporal_projection",
|
| 103 |
+
"result": "insufficient_distinct_dates"}}
|
| 104 |
+
|
| 105 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 106 |
+
started = time.perf_counter()
|
| 107 |
+
result = None
|
| 108 |
+
try:
|
| 109 |
+
result = self._simply_turn(" ".join(text.strip().split()), session_id)
|
| 110 |
+
except Exception: # noqa: BLE001
|
| 111 |
+
result = None
|
| 112 |
+
if result is not None:
|
| 113 |
+
result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
|
| 114 |
+
result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
|
| 115 |
+
return result
|
| 116 |
+
return super().chat(text, session_id)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ── timeline: a year token is not the same thing as a number ────────────────
|
| 120 |
+
# The route treated any 4-digit token as a date, so "One megasecond contains
|
| 121 |
+
# 1000 kiloseconds" was rendered as the year 1000, and a page with one date
|
| 122 |
+
# repeated produced a single-point "timeline". A date must not be a measurement,
|
| 123 |
+
# and a timeline needs at least two DISTINCT years.
|
| 124 |
+
UNIT_AFTER = re.compile(
|
| 125 |
+
r"^(?:kilo|mega|giga|milli|micro|nano)?"
|
| 126 |
+
r"(?:second|meter|metre|gram|litre|liter|byte|watt|hertz|mile|foot|feet|inch|"
|
| 127 |
+
r"year|month|week|day|hour|minute|people|person|km|kg|cm|mm|ft|lb)s?$", re.I)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def year_tokens(sentence: str) -> list[int]:
|
| 131 |
+
"""4-digit years in a sentence, excluding numbers that are measurements."""
|
| 132 |
+
text = str(sentence or "")
|
| 133 |
+
out: list[int] = []
|
| 134 |
+
for match in re.finditer(r"(1[0-9]{3}|20[0-2][0-9])", text):
|
| 135 |
+
tail = text[match.end():].lstrip()
|
| 136 |
+
next_word = tail.split()[0].strip(".,;:)") if tail.split() else ""
|
| 137 |
+
if next_word and UNIT_AFTER.match(next_word):
|
| 138 |
+
continue # "1000 kiloseconds" is a quantity, not a date
|
| 139 |
+
out.append(int(match.group(1)))
|
| 140 |
+
return out
|
root/wander-train/oracle_release_runtime_ag.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime AG - cite a genuine hypernym chain (additive over AF).
|
| 3 |
+
|
| 4 |
+
AF answered correctly but CITED INCORRECTLY:
|
| 5 |
+
|
| 6 |
+
"Yes. The lexical record places gray whale under whale:
|
| 7 |
+
organism -> living thing -> whole -> object -> physical entity -> gray whale"
|
| 8 |
+
|
| 9 |
+
The claimed hypernym ("whale") does not appear in the chain shown. Corpus v1
|
| 10 |
+
stored each synset's total path length as the depth of every node on that path,
|
| 11 |
+
so all hypernyms of a sense shared one meaningless value and ordering by it
|
| 12 |
+
produced a sequence that was not a hypernym path at all.
|
| 13 |
+
|
| 14 |
+
For a system whose entire claim is shown provenance, citing a chain that does not
|
| 15 |
+
contain the cited term is the most serious defect available - worse than the
|
| 16 |
+
parser bug, because the answer looks right and the evidence is fabricated
|
| 17 |
+
structure.
|
| 18 |
+
|
| 19 |
+
Corpus v2 stores each node's own distance from the root, so ascending depth is a
|
| 20 |
+
genuine general-to-specific ordering. This runtime reads v2, renders the chain in
|
| 21 |
+
that order, and guarantees the cited hypernym appears in what it shows. It also
|
| 22 |
+
never presents more than the real path: nodes are drawn from the lemma's own
|
| 23 |
+
closure only.
|
| 24 |
+
|
| 25 |
+
No neural network; no LM call.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import sqlite3
|
| 31 |
+
import time
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
|
| 34 |
+
from oracle_release_runtime_af import OracleReleaseRuntimeAF
|
| 35 |
+
|
| 36 |
+
WORDNET_CORPUS_V2 = (__import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/fable-content/oracle-m-series-2026-08-15"
|
| 37 |
+
"/m31_wordnet_corpus_v2.sqlite3")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class OracleReleaseRuntimeAG(OracleReleaseRuntimeAF):
|
| 41 |
+
def __init__(self, *args, wordnet_corpus: str = WORDNET_CORPUS_V2, **kwargs) -> None:
|
| 42 |
+
super().__init__(*args, wordnet_corpus=wordnet_corpus, **kwargs)
|
| 43 |
+
|
| 44 |
+
def _chain_for(self, lemma: str, limit: int = 6) -> list:
|
| 45 |
+
"""General -> specific, by each node's own distance from the root."""
|
| 46 |
+
rows = self._wn.execute(
|
| 47 |
+
"SELECT hypernym FROM lemma_closure WHERE lemma=? ORDER BY depth ASC, hypernym ASC "
|
| 48 |
+
"LIMIT ?", (lemma, limit)).fetchall()
|
| 49 |
+
return [r[0] for r in rows]
|
| 50 |
+
|
| 51 |
+
def _render_chain(self, lemma: str, hypernym: str | None) -> str:
|
| 52 |
+
"""The shown chain must contain the cited hypernym, or it is not evidence."""
|
| 53 |
+
chain = self._chain_for(lemma, limit=6)
|
| 54 |
+
if hypernym and hypernym not in chain:
|
| 55 |
+
row = self._wn.execute(
|
| 56 |
+
"SELECT depth FROM lemma_closure WHERE lemma=? AND hypernym=?",
|
| 57 |
+
(lemma, hypernym)).fetchone()
|
| 58 |
+
if row is not None:
|
| 59 |
+
chain = self._chain_for(lemma, limit=5)
|
| 60 |
+
if hypernym in chain:
|
| 61 |
+
chain.remove(hypernym)
|
| 62 |
+
chain = chain[:4] + [hypernym]
|
| 63 |
+
return " -> ".join(chain + [lemma]) if chain else lemma
|
| 64 |
+
|
| 65 |
+
def _wordnet_turn(self, text: str) -> dict | None:
|
| 66 |
+
if self._wn is None:
|
| 67 |
+
return None
|
| 68 |
+
parsed = self._parse_kind(text)
|
| 69 |
+
if parsed is None:
|
| 70 |
+
return None
|
| 71 |
+
x, y = parsed
|
| 72 |
+
lemma = self._present(x)
|
| 73 |
+
if lemma is None:
|
| 74 |
+
return None
|
| 75 |
+
hit = self._closure_hit(lemma, y)
|
| 76 |
+
article_x = "an" if x[:1] in "aeiou" else "a"
|
| 77 |
+
article_y = "an" if y[:1] in "aeiou" else "a"
|
| 78 |
+
if hit is not None:
|
| 79 |
+
hypernym, _depth, sense = hit
|
| 80 |
+
shown = self._render_chain(lemma, hypernym)
|
| 81 |
+
return {
|
| 82 |
+
"status": "answered",
|
| 83 |
+
"path": "wordnet_kind_direct",
|
| 84 |
+
"response": (f"Yes. The lexical record places {lemma} under {hypernym}, "
|
| 85 |
+
f"from the general to the specific: {shown}. On that record "
|
| 86 |
+
f"{article_x} {x} is {article_y} {y}. [cited:wordnet:{sense}]"),
|
| 87 |
+
"provenance": {"kind": "lexical_corpus", "corpus": "wordnet",
|
| 88 |
+
"relation": "hypernym", "lemma": lemma,
|
| 89 |
+
"hypernym": hypernym, "sense": sense,
|
| 90 |
+
"chain": self._chain_for(lemma) + [lemma]},
|
| 91 |
+
}
|
| 92 |
+
shown = self._render_chain(lemma, None)
|
| 93 |
+
return {
|
| 94 |
+
"status": "answered",
|
| 95 |
+
"path": "wordnet_kind_excluded",
|
| 96 |
+
"response": (f"No. The lexical record places {lemma} on a different line, "
|
| 97 |
+
f"from the general to the specific: {shown}. {y.capitalize()} does "
|
| 98 |
+
f"not appear anywhere in that chain, and the chain is complete, so "
|
| 99 |
+
f"this is an exclusion rather than a gap in the record. "
|
| 100 |
+
f"[cited:wordnet:{lemma}]"),
|
| 101 |
+
"provenance": {"kind": "lexical_corpus", "corpus": "wordnet",
|
| 102 |
+
"relation": "hypernym_excluded", "lemma": lemma,
|
| 103 |
+
"chain": self._chain_for(lemma) + [lemma]},
|
| 104 |
+
}
|
root/wander-train/oracle_release_runtime_ah.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime AH - honest exclusion rendering (additive over AG).
|
| 3 |
+
|
| 4 |
+
AG fixed the affirmative citation but left the SAME defect class in the exclusion
|
| 5 |
+
path. Two problems, both of which fabricate structure:
|
| 6 |
+
|
| 7 |
+
1. Sense interleaving. The closure is a union over all senses, so rendering it as
|
| 8 |
+
one arrow-path splices unrelated lineages:
|
| 9 |
+
|
| 10 |
+
"guinea pig: entity -> physical entity -> causal agent -> object ->
|
| 11 |
+
person -> whole -> guinea pig"
|
| 12 |
+
|
| 13 |
+
That path does not exist. It mixes the rodent with the separate sense of a
|
| 14 |
+
person used as a test subject.
|
| 15 |
+
|
| 16 |
+
2. Truncated scope. The text asserted a term "does not appear anywhere in that
|
| 17 |
+
chain" while displaying only a six-node prefix, inviting the reader to verify
|
| 18 |
+
an exhaustive claim against partial evidence.
|
| 19 |
+
|
| 20 |
+
Fix: an exclusion no longer claims a path. It reports the most specific categories
|
| 21 |
+
the record actually assigns - which are the sense-characteristic ones - as a SET,
|
| 22 |
+
and states plainly that the exclusion was checked against the complete closure
|
| 23 |
+
while what is shown is a summary of it. The affirmative path still shows a real
|
| 24 |
+
chain, because there a single genuine lineage exists and contains the cited term.
|
| 25 |
+
|
| 26 |
+
Say only what the record supports, and show evidence in the shape it actually has.
|
| 27 |
+
No neural network; no LM call.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
from oracle_release_runtime_ag import OracleReleaseRuntimeAG
|
| 33 |
+
|
| 34 |
+
SHOW_CATEGORIES = 6
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class OracleReleaseRuntimeAH(OracleReleaseRuntimeAG):
|
| 38 |
+
def _specific_categories(self, lemma: str, limit: int = SHOW_CATEGORIES) -> list:
|
| 39 |
+
"""Deepest hypernyms = the sense-characteristic ones. A set, not a path."""
|
| 40 |
+
rows = self._wn.execute(
|
| 41 |
+
"SELECT hypernym FROM lemma_closure WHERE lemma=? ORDER BY depth DESC, hypernym ASC "
|
| 42 |
+
"LIMIT ?", (lemma, limit)).fetchall()
|
| 43 |
+
return [r[0] for r in rows]
|
| 44 |
+
|
| 45 |
+
def _closure_size(self, lemma: str) -> int:
|
| 46 |
+
row = self._wn.execute(
|
| 47 |
+
"SELECT COUNT(*) FROM lemma_closure WHERE lemma=?", (lemma,)).fetchone()
|
| 48 |
+
return int(row[0]) if row else 0
|
| 49 |
+
|
| 50 |
+
def _wordnet_turn(self, text: str) -> dict | None:
|
| 51 |
+
result = super()._wordnet_turn(text)
|
| 52 |
+
if result is None or result.get("path") != "wordnet_kind_excluded":
|
| 53 |
+
return result
|
| 54 |
+
prov = result.get("provenance", {})
|
| 55 |
+
lemma = prov.get("lemma")
|
| 56 |
+
parsed = self._parse_kind(text)
|
| 57 |
+
if not lemma or parsed is None:
|
| 58 |
+
return result
|
| 59 |
+
_x, y = parsed
|
| 60 |
+
cats = self._specific_categories(lemma)
|
| 61 |
+
total = self._closure_size(lemma)
|
| 62 |
+
listed = ", ".join(cats) if cats else "no categories on record"
|
| 63 |
+
result["response"] = (
|
| 64 |
+
f"No. The record classifies {lemma} under {listed}"
|
| 65 |
+
f"{' and other broader categories' if total > len(cats) else ''}. "
|
| 66 |
+
f"{y.capitalize()} is not among the {total} categories on record for it, and "
|
| 67 |
+
f"that record is complete, so this is an exclusion rather than a gap. "
|
| 68 |
+
f"[cited:wordnet:{lemma}]")
|
| 69 |
+
result["provenance"] = {
|
| 70 |
+
"kind": "lexical_corpus", "corpus": "wordnet",
|
| 71 |
+
"relation": "hypernym_excluded", "lemma": lemma,
|
| 72 |
+
"categories_shown": cats, "categories_total": total,
|
| 73 |
+
"note": ("categories are a set drawn from all senses, not a single lineage; "
|
| 74 |
+
"the exclusion was checked against the complete closure"),
|
| 75 |
+
}
|
| 76 |
+
return result
|
root/wander-train/oracle_release_runtime_aj.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime AJ - lexicon-informed splitting for determiner-free questions (additive over AI).
|
| 3 |
+
|
| 4 |
+
M38 passed eight columns and still mis-parsed this:
|
| 5 |
+
|
| 6 |
+
"Is hand luggage luggage?" -> x='hand', y='luggage luggage'
|
| 7 |
+
-> "No. The record classifies HAND under forepaw, paw, animal foot..."
|
| 8 |
+
|
| 9 |
+
With no determiner the strict pattern cannot anchor, and the loose pattern's non-greedy
|
| 10 |
+
subject takes the shortest split. This is the M31 defect resurfacing in the form the M31
|
| 11 |
+
fix did not cover, and no panel saw it because every generated question carries
|
| 12 |
+
determiners ("Is a X a Y?"). The route's own domain includes determiner-free forms;
|
| 13 |
+
the test distribution did not.
|
| 14 |
+
|
| 15 |
+
Fix: when no determiner is available to anchor the split, use the admitted record as the
|
| 16 |
+
lexicon. Enumerate every split point and choose the longest subject for which BOTH sides
|
| 17 |
+
are lemmas the corpus carries. Purely structural, no heuristic weighting - the record
|
| 18 |
+
already knows which strings are terms.
|
| 19 |
+
|
| 20 |
+
"Is hand luggage luggage?"
|
| 21 |
+
split 1: x='hand' y='luggage luggage' -> y not a lemma, rejected
|
| 22 |
+
split 2: x='hand luggage' y='luggage' -> both lemmas, ACCEPTED
|
| 23 |
+
|
| 24 |
+
Determiner-bearing forms still take the strict path unchanged. No neural network.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import re
|
| 30 |
+
|
| 31 |
+
from oracle_release_runtime_ai import OracleReleaseRuntimeAI
|
| 32 |
+
from oracle_release_runtime_af import KIND_Q_STRICT, _FILLER, _LEAD
|
| 33 |
+
from oracle_release_runtime_ae import _forms
|
| 34 |
+
|
| 35 |
+
_BODY = re.compile(
|
| 36 |
+
r"^(?:is|are)\s+(?:(?:an?|the|some)\s+)?(?P<body>[a-z][a-z '\-]+?)\s*[.?!]*$", re.I)
|
| 37 |
+
MAX_WORDS = 8
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class OracleReleaseRuntimeAJ(OracleReleaseRuntimeAI):
|
| 41 |
+
def _valid_pair(self, x: str, y: str):
|
| 42 |
+
if not x or not y or x == y or len(x) < 2 or len(y) < 2:
|
| 43 |
+
return None
|
| 44 |
+
if set(_forms(x)) & set(_forms(y)):
|
| 45 |
+
return None
|
| 46 |
+
return x, y
|
| 47 |
+
|
| 48 |
+
def _parse_kind(self, text: str):
|
| 49 |
+
t = " ".join(text.strip().split())
|
| 50 |
+
t = " ".join(_FILLER.sub("", _LEAD.sub("", t)).split())
|
| 51 |
+
match = KIND_Q_STRICT.match(t)
|
| 52 |
+
if match:
|
| 53 |
+
return self._valid_pair(match.group("x").strip().lower(),
|
| 54 |
+
match.group("y").strip().lower())
|
| 55 |
+
body_match = _BODY.match(t)
|
| 56 |
+
if not body_match:
|
| 57 |
+
return super()._parse_kind(text)
|
| 58 |
+
words = body_match.group("body").strip().lower().split()
|
| 59 |
+
if len(words) < 2 or len(words) > MAX_WORDS:
|
| 60 |
+
return super()._parse_kind(text)
|
| 61 |
+
chosen = None
|
| 62 |
+
for cut in range(1, len(words)):
|
| 63 |
+
x = " ".join(words[:cut])
|
| 64 |
+
y = " ".join(words[cut:])
|
| 65 |
+
pair = self._valid_pair(x, y)
|
| 66 |
+
if pair is None:
|
| 67 |
+
continue
|
| 68 |
+
if self._present(x) and self._present(y):
|
| 69 |
+
chosen = pair # keep iterating: prefer the LONGEST subject
|
| 70 |
+
if chosen is not None:
|
| 71 |
+
return chosen
|
| 72 |
+
return super()._parse_kind(text)
|
root/wander-train/oracle_release_runtime_am.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime AM - record dialogue state from the FINAL result (additive over AL).
|
| 3 |
+
|
| 4 |
+
M46 passed and two defects survived it, both visible the moment turn-1 used a route other
|
| 5 |
+
than summarize:
|
| 6 |
+
|
| 7 |
+
"Is a gray whale a whale?" -> answered by wordnet_kind_direct
|
| 8 |
+
replay: "came via the DOCUMENT_RUNTIME route ... typed refusal"
|
| 9 |
+
"What causes rain?" -> answered by capability:what_causes
|
| 10 |
+
replay: "Nothing has been answered in this session yet"
|
| 11 |
+
|
| 12 |
+
Cause: dialogue state is written inside runtime E's chat, but answers are produced at many
|
| 13 |
+
layers of the chain. Routes resolved ABOVE E never write state at all; routes resolved
|
| 14 |
+
after E (the whole AD..AL tier) leave E's record of the INNER result in place. The ACTS
|
| 15 |
+
layer therefore reads a state describing a different answer than the user received.
|
| 16 |
+
|
| 17 |
+
Fix: the top of the chain records what the user actually saw. State is written once, from
|
| 18 |
+
the final result, after every turn - overwriting whatever inner layers recorded. Acts are
|
| 19 |
+
conversions over the delivered answer, so the delivered answer is what must be stored.
|
| 20 |
+
|
| 21 |
+
M46 could not see this because every turn-1 probe in it was "Summarize X" - a single route,
|
| 22 |
+
which happens to be one that flows through E. The test distribution must cover the route
|
| 23 |
+
inventory, not one member of it.
|
| 24 |
+
|
| 25 |
+
No neural network; no LM call.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import re
|
| 31 |
+
|
| 32 |
+
from oracle_release_runtime_al import OracleReleaseRuntimeAL
|
| 33 |
+
|
| 34 |
+
CITE = re.compile(r"\[[a-z0-9_]+:?[a-z0-9_.\-]*\]")
|
| 35 |
+
ACT_PATHS = ("act:provenance_replay", "act:acknowledge", "act:continue_topic", "act:refine")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class OracleReleaseRuntimeAM(OracleReleaseRuntimeAL):
|
| 39 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 40 |
+
result = super().chat(text, session_id)
|
| 41 |
+
path = str(result.get("path", ""))
|
| 42 |
+
# acts are conversions OVER the last answer; they must not overwrite it
|
| 43 |
+
if path in ACT_PATHS:
|
| 44 |
+
return result
|
| 45 |
+
try:
|
| 46 |
+
slot = self.dialogue.setdefault(session_id, {})
|
| 47 |
+
slot["last_result"] = {
|
| 48 |
+
"path": result.get("path"),
|
| 49 |
+
"provenance": result.get("provenance"),
|
| 50 |
+
"status": result.get("status"),
|
| 51 |
+
"citations": CITE.findall(str(result.get("response", ""))),
|
| 52 |
+
}
|
| 53 |
+
except Exception: # noqa: BLE001
|
| 54 |
+
pass
|
| 55 |
+
return result
|
root/wander-train/oracle_release_runtime_c.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Front-door successor C (additive over B): essay route + definitional route.
|
| 3 |
+
|
| 4 |
+
Essay route: "write an essay about X" -> the H2 gated document compiler over
|
| 5 |
+
the topic page plus up to two related pages (title matches carrying a
|
| 6 |
+
distinctive topic token, relevance-gated). Output is a structured document
|
| 7 |
+
whose thesis/conclusions/analyses/claims chain is verified by
|
| 8 |
+
recover_document before release; an unverifiable document is withheld.
|
| 9 |
+
|
| 10 |
+
Definitional route (closes the registered debt): "what is a/an X?" answers
|
| 11 |
+
with the gated definitional lead of X's page when the page exists, instead of
|
| 12 |
+
sense clarification."""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import re
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
from oracle_release_runtime_b import OracleReleaseRuntimeB
|
| 20 |
+
from stage5m13e8_document_compiler_v1 import compile_section, compile_document, recover_document
|
| 21 |
+
from stage5m13e8_paragraph_compiler_v1 import _definitional_score
|
| 22 |
+
from stage5m13e8_relevance_gate_v1 import topic_tokens, classify_claim
|
| 23 |
+
|
| 24 |
+
ESSAY = re.compile(r"^(?:please )?(?:write|compose)(?: me)?(?: a| an)?(?: short| brief)?(?: \d+[- ]word)? essay (?:about|on) (?P<topic>.+?)\.?$", re.I)
|
| 25 |
+
DEFINITIONAL = re.compile(r"^what is (?:a |an |the )?(?P<term>[\w \-']{2,60})\?$", re.I)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class OracleReleaseRuntimeC(OracleReleaseRuntimeB):
|
| 29 |
+
def _bundle(self, topic: str, n: int = 6) -> list[dict]:
|
| 30 |
+
page = self.bench_query.page_id(topic)
|
| 31 |
+
if not page:
|
| 32 |
+
return []
|
| 33 |
+
return [{"claim_id": c, "page": topic, "sentence": s}
|
| 34 |
+
for c, s in self.bench_query.page_sentences(page)[:n]]
|
| 35 |
+
|
| 36 |
+
def _related_titles(self, topic: str, cap: int = 2) -> list[str]:
|
| 37 |
+
distinctive, _ = topic_tokens(topic)
|
| 38 |
+
if not distinctive:
|
| 39 |
+
return []
|
| 40 |
+
token = sorted(distinctive)[0]
|
| 41 |
+
# indexed prefix range on title_alias (no full scan over 6M articles)
|
| 42 |
+
rows = self.bench_query.connection.execute(
|
| 43 |
+
"SELECT a.title FROM title_alias t JOIN article a ON a.page_id = t.page_id "
|
| 44 |
+
"WHERE t.normalized_title >= ? AND t.normalized_title < ? LIMIT 12",
|
| 45 |
+
(token, token + "")).fetchall()
|
| 46 |
+
related = []
|
| 47 |
+
for (title,) in rows:
|
| 48 |
+
if title.lower() == topic.lower():
|
| 49 |
+
continue
|
| 50 |
+
if classify_claim(topic, title, title) in ("explanatory", "exact_topic"):
|
| 51 |
+
related.append(title)
|
| 52 |
+
if len(related) >= cap:
|
| 53 |
+
break
|
| 54 |
+
return related
|
| 55 |
+
|
| 56 |
+
def _essay_turn(self, text: str) -> dict | None:
|
| 57 |
+
match = ESSAY.match(text.strip())
|
| 58 |
+
if not match:
|
| 59 |
+
return None
|
| 60 |
+
topic = match.group("topic").strip()
|
| 61 |
+
topics = [topic] + self._related_titles(topic)
|
| 62 |
+
bundles = [(t, self._bundle(t), None) for t in topics]
|
| 63 |
+
bundles = [(t, b, None) for t, b, _ in bundles if b]
|
| 64 |
+
if not bundles:
|
| 65 |
+
return {"status": "withheld", "path": "gated_document",
|
| 66 |
+
"response": f"No provenance-admitted page for {topic} is available to ground an essay.",
|
| 67 |
+
"provenance": {"kind": "gated_document", "topic": topic}}
|
| 68 |
+
section = compile_section(f"The record on {topic}", bundles)
|
| 69 |
+
document = compile_document(f"A provenance-bounded account of {topic}", [section])
|
| 70 |
+
if not recover_document(document):
|
| 71 |
+
return {"status": "withheld", "path": "gated_document",
|
| 72 |
+
"response": "Document failed its own derivation-chain verification and is withheld.",
|
| 73 |
+
"provenance": {"kind": "gated_document", "topic": topic,
|
| 74 |
+
"failure": "recover_document"}}
|
| 75 |
+
return {"status": "answered", "path": "gated_document",
|
| 76 |
+
"response": document["text"],
|
| 77 |
+
"provenance": {"kind": "gated_document", "topics": topics,
|
| 78 |
+
"exclusions": [{"page": e["page"], "relevance": e["relevance"]}
|
| 79 |
+
for e in section["exclusions"]]}}
|
| 80 |
+
|
| 81 |
+
def _definitional_turn(self, text: str) -> dict | None:
|
| 82 |
+
match = DEFINITIONAL.match(text.strip())
|
| 83 |
+
if not match:
|
| 84 |
+
return None
|
| 85 |
+
term = match.group("term").strip()
|
| 86 |
+
bundle = self._bundle(term.title()) or self._bundle(term)
|
| 87 |
+
if not bundle:
|
| 88 |
+
return None # fall through to sense machinery
|
| 89 |
+
lead = max(bundle, key=lambda c: _definitional_score(term, c["sentence"]))
|
| 90 |
+
if _definitional_score(term, lead["sentence"]) < 6:
|
| 91 |
+
return None
|
| 92 |
+
return {"status": "answered", "path": "definitional_encyclopedic",
|
| 93 |
+
"response": f"{lead['sentence']} [{lead['claim_id']}]",
|
| 94 |
+
"provenance": {"kind": "dbpedia_page", "page": lead["page"],
|
| 95 |
+
"claim_id": lead["claim_id"]}}
|
| 96 |
+
|
| 97 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 98 |
+
started = time.perf_counter()
|
| 99 |
+
normalized = " ".join(text.strip().split())
|
| 100 |
+
result = None
|
| 101 |
+
for route in (lambda: self._essay_turn(normalized),
|
| 102 |
+
lambda: self._definitional_turn(normalized)):
|
| 103 |
+
try:
|
| 104 |
+
result = route()
|
| 105 |
+
except Exception: # noqa: BLE001
|
| 106 |
+
result = None
|
| 107 |
+
if result is not None:
|
| 108 |
+
break
|
| 109 |
+
if result is None:
|
| 110 |
+
return super().chat(text, session_id)
|
| 111 |
+
result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
|
| 112 |
+
result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
|
| 113 |
+
return result
|
root/wander-train/oracle_release_runtime_e.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime E — conversation-act conversion layer, slice 1 (Ben's conversion
|
| 3 |
+
thesis: dialogue is a finite inventory of typed conversions, not an emergent
|
| 4 |
+
capability).
|
| 5 |
+
|
| 6 |
+
Acts this slice:
|
| 7 |
+
provenance_replay "why did you say that / how do you know / source?"
|
| 8 |
+
-> re-present the last answer's provenance (addresses,
|
| 9 |
+
rule+premises, or the typed reason for a withhold)
|
| 10 |
+
continue_topic "tell me more / what else / more about that"
|
| 11 |
+
-> next admitted claims from the last topic's page,
|
| 12 |
+
composed and verified like any paragraph
|
| 13 |
+
acknowledge "thanks / ok / great" -> typed acknowledgment, no fetch
|
| 14 |
+
|
| 15 |
+
Dialogue state is a per-session record {last_result, last_topic, last_offset}
|
| 16 |
+
— a dictionary, not a model. Every act is a conversion over existing
|
| 17 |
+
machinery; nothing generates."""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import re
|
| 22 |
+
import time
|
| 23 |
+
|
| 24 |
+
from oracle_release_runtime_d import OracleReleaseRuntimeD
|
| 25 |
+
from stage5m13e8_paragraph_compiler_v2 import compile_paragraph_v2, recover_program_v2
|
| 26 |
+
|
| 27 |
+
WHY = re.compile(r"^(?:why(?: did you say that| do you say that)?|how do you know(?: that)?|"
|
| 28 |
+
r"what(?:'s| is) your source(?: for that)?|source\??|prove it|citation\??)[.?!]?$", re.I)
|
| 29 |
+
MORE = re.compile(r"^(?:tell me more|more|what else|go on|continue|more about (?:that|it))[.?!]?$", re.I)
|
| 30 |
+
ACK = re.compile(r"^(?:thanks|thank you|ok|okay|got it|great|cool|nice|perfect)[.?!]?$", re.I)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class OracleReleaseRuntimeE(OracleReleaseRuntimeD):
|
| 34 |
+
def __init__(self, *args, **kwargs):
|
| 35 |
+
super().__init__(*args, **kwargs)
|
| 36 |
+
self.dialogue: dict[str, dict] = {}
|
| 37 |
+
|
| 38 |
+
# ── act conversions ──────────────────────────────────────────────────
|
| 39 |
+
def _act_provenance_replay(self, state: dict) -> dict:
|
| 40 |
+
last = state.get("last_result")
|
| 41 |
+
if not last:
|
| 42 |
+
return {"status": "withheld", "path": "act:provenance_replay",
|
| 43 |
+
"response": "Nothing has been answered in this session yet, so there is no provenance to replay."}
|
| 44 |
+
prov = last.get("provenance") or {}
|
| 45 |
+
kind = prov.get("kind", "unknown")
|
| 46 |
+
lines = [f"My previous answer came via the {last.get('path','?')} route."]
|
| 47 |
+
if kind == "dbpedia_page":
|
| 48 |
+
lines.append(f"It is the definitional lead of the page '{prov.get('page')}', claim {prov.get('claim_id')}.")
|
| 49 |
+
elif kind == "gated_paragraph":
|
| 50 |
+
lines.append(f"It composed {prov.get('public_claims',0)} page-addressed claims"
|
| 51 |
+
+ (f" and {prov['user_claims']} claims from your own documents" if prov.get('user_claims') else "")
|
| 52 |
+
+ "; every sentence carries its claim address in brackets.")
|
| 53 |
+
if prov.get("excluded"):
|
| 54 |
+
lines.append("Excluded as insufficiently relevant (ledgered, not hidden): "
|
| 55 |
+
+ ", ".join(f"{e['page']} ({e['relevance']})" for e in prov["excluded"][:4]) + ".")
|
| 56 |
+
elif kind == "conceptnet_edges":
|
| 57 |
+
claims = prov.get("claims", [])
|
| 58 |
+
lines.append(f"It rests on {len(claims)} ConceptNet edges: "
|
| 59 |
+
+ "; ".join(c.get("provenance", "?") for c in claims[:4]) + ".")
|
| 60 |
+
elif kind == "session_world":
|
| 61 |
+
lines.append(f"It was computed from the {prov.get('statements',0)} statements you asserted "
|
| 62 |
+
"in this session's typed world — logic over your own facts, not retrieval.")
|
| 63 |
+
else:
|
| 64 |
+
lines.append("It was a typed refusal: no admitted evidence licensed an answer, so none was given.")
|
| 65 |
+
lines.append("Nothing in that answer was generated; each part is either addressed, derived, or withheld.")
|
| 66 |
+
return {"status": "answered", "path": "act:provenance_replay", "response": " ".join(lines),
|
| 67 |
+
"provenance": {"kind": "dialogue_state_replay"}}
|
| 68 |
+
|
| 69 |
+
def _act_continue(self, state: dict, session_id: str) -> dict:
|
| 70 |
+
topic = state.get("last_topic")
|
| 71 |
+
if not topic:
|
| 72 |
+
return {"status": "withheld", "path": "act:continue_topic",
|
| 73 |
+
"response": "There is no active topic yet — ask about something first."}
|
| 74 |
+
offset = state.get("last_offset", 0) + 6
|
| 75 |
+
page = self.anchors.resolve(topic)
|
| 76 |
+
if not page:
|
| 77 |
+
return {"status": "withheld", "path": "act:continue_topic",
|
| 78 |
+
"response": f"No further admitted material for {topic}."}
|
| 79 |
+
title = self.anchors.page_to_title.get(page, topic)
|
| 80 |
+
rows = self.bench_query.page_sentences(page)[offset:offset + 4]
|
| 81 |
+
if not rows:
|
| 82 |
+
return {"status": "withheld", "path": "act:continue_topic",
|
| 83 |
+
"response": f"The admitted record on {title} is exhausted — "
|
| 84 |
+
f"every stored sentence has been shown."}
|
| 85 |
+
claims = [{"claim_id": c, "page": title, "sentence": s} for c, s in rows]
|
| 86 |
+
paragraph, program = compile_paragraph_v2(topic, claims, evidence=3, aggregation_term=None)
|
| 87 |
+
if paragraph is None or not recover_program_v2(paragraph, program):
|
| 88 |
+
return {"status": "withheld", "path": "act:continue_topic",
|
| 89 |
+
"response": "Continuation failed verification and is withheld."}
|
| 90 |
+
state["last_offset"] = offset
|
| 91 |
+
return {"status": "answered", "path": "act:continue_topic", "response": paragraph,
|
| 92 |
+
"provenance": {"kind": "gated_paragraph", "topic": topic, "offset": offset}}
|
| 93 |
+
|
| 94 |
+
# ── entry ────────────────────────────────────────────────────────────
|
| 95 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 96 |
+
started = time.perf_counter()
|
| 97 |
+
normalized = " ".join(text.strip().split())
|
| 98 |
+
state = self.dialogue.setdefault(session_id, {})
|
| 99 |
+
result = None
|
| 100 |
+
if WHY.match(normalized):
|
| 101 |
+
result = self._act_provenance_replay(state)
|
| 102 |
+
elif MORE.match(normalized):
|
| 103 |
+
result = self._act_continue(state, session_id)
|
| 104 |
+
elif ACK.match(normalized):
|
| 105 |
+
result = {"status": "answered", "path": "act:acknowledge",
|
| 106 |
+
"response": "Noted. The session state is kept; ask on.",
|
| 107 |
+
"provenance": {"kind": "dialogue_state"}}
|
| 108 |
+
if result is not None:
|
| 109 |
+
result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
|
| 110 |
+
result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
|
| 111 |
+
else:
|
| 112 |
+
result = super().chat(text, session_id)
|
| 113 |
+
# topic tracking for continuation acts
|
| 114 |
+
path = result.get("path", "")
|
| 115 |
+
prov = result.get("provenance") or {}
|
| 116 |
+
if path == "anchored_summary":
|
| 117 |
+
match = re.match(r"^(?:tell me about|who is|who was|what do you know about) (?:the )?(.+?)[.?!]?$",
|
| 118 |
+
normalized, re.I)
|
| 119 |
+
if match:
|
| 120 |
+
state["last_topic"] = match.group(1).strip()
|
| 121 |
+
state["last_offset"] = 0
|
| 122 |
+
elif path == "definitional_encyclopedic" and prov.get("page"):
|
| 123 |
+
state["last_topic"] = prov["page"]
|
| 124 |
+
state["last_offset"] = 0
|
| 125 |
+
self.dialogue[session_id]["last_result"] = {k: result.get(k) for k in ("path", "provenance", "status")}
|
| 126 |
+
return result
|
root/wander-train/oracle_release_runtime_h.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime H — capability cycle 3: list-N-things (additive over G)."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
from oracle_release_runtime_g import OracleReleaseRuntimeG
|
| 10 |
+
from stage5m13e8_relevance_gate_v1 import gate_bundle
|
| 11 |
+
|
| 12 |
+
NUMERALS = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
|
| 13 |
+
"seven": 7, "eight": 8, "nine": 9, "ten": 10, "a": 3, "an": 3,
|
| 14 |
+
"some": 3, "few": 3, "several": 4, "couple": 2}
|
| 15 |
+
LISTN = re.compile(
|
| 16 |
+
r"^(?:please |can you )?(?:list|give me|tell me|name|share)"
|
| 17 |
+
r"(?: (?P<n>\d+|one|two|three|four|five|six|seven|eight|nine|ten|a few|some|several|a couple(?: of)?))?"
|
| 18 |
+
r" (?:facts?|things?|details?)(?: about| on| regarding)"
|
| 19 |
+
r"(?: the| my document| my)? (?P<t>.+?)[.?!]?$"
|
| 20 |
+
r"|^what are (?:some|a few) facts? about(?: the| my document| my)? (?P<t2>.+?)[.?!]?$",
|
| 21 |
+
re.I)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class OracleReleaseRuntimeH(OracleReleaseRuntimeG):
|
| 25 |
+
def _listn_turn(self, text: str, session_id: str) -> dict | None:
|
| 26 |
+
match = LISTN.match(text.strip())
|
| 27 |
+
if not match:
|
| 28 |
+
return None
|
| 29 |
+
groups = match.groupdict()
|
| 30 |
+
topic = (groups.get("t") or groups.get("t2") or "").strip()
|
| 31 |
+
if not topic:
|
| 32 |
+
return None
|
| 33 |
+
raw_n = (groups.get("n") or "some").lower().replace("a couple of", "couple").replace("a couple", "couple").replace("a few", "few")
|
| 34 |
+
n = int(raw_n) if raw_n.isdigit() else NUMERALS.get(raw_n, 3)
|
| 35 |
+
n = max(1, min(n, 10))
|
| 36 |
+
user_claims = self.user_corpus.bundle(session_id, topic)
|
| 37 |
+
admitted, _ = gate_bundle(topic, self._bundle(topic, n=10))
|
| 38 |
+
claims = (user_claims + admitted)[:n]
|
| 39 |
+
if not claims:
|
| 40 |
+
return {"status": "withheld", "path": "capability:list_n",
|
| 41 |
+
"response": f"No provenance-admitted facts about {topic} are available to list.",
|
| 42 |
+
"provenance": {"kind": "gated_list", "topic": topic}}
|
| 43 |
+
lines = [f"{i+1}. {c['sentence']} [{c['claim_id']}]" for i, c in enumerate(claims)]
|
| 44 |
+
shortfall = ""
|
| 45 |
+
if len(claims) < n:
|
| 46 |
+
shortfall = (f" Only {len(claims)} admitted fact"
|
| 47 |
+
f"{'s are' if len(claims) != 1 else ' is'} available for {topic}.")
|
| 48 |
+
return {"status": "answered", "path": "capability:list_n",
|
| 49 |
+
"response": " ".join(lines) + shortfall,
|
| 50 |
+
"provenance": {"kind": "gated_list", "topic": topic,
|
| 51 |
+
"requested": n, "delivered": len(claims),
|
| 52 |
+
"user_claims": len([c for c in claims if c["claim_id"].startswith("user:")])}}
|
| 53 |
+
|
| 54 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 55 |
+
started = time.perf_counter()
|
| 56 |
+
normalized = " ".join(text.strip().split())
|
| 57 |
+
result = None
|
| 58 |
+
try:
|
| 59 |
+
result = self._listn_turn(normalized, session_id)
|
| 60 |
+
except Exception: # noqa: BLE001
|
| 61 |
+
result = None
|
| 62 |
+
if result is not None:
|
| 63 |
+
result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
|
| 64 |
+
result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
|
| 65 |
+
state = self.dialogue.setdefault(session_id, {})
|
| 66 |
+
state["last_result"] = {k: result.get(k) for k in ("path", "provenance", "status")}
|
| 67 |
+
state["last_topic"] = result["provenance"].get("topic")
|
| 68 |
+
state["last_offset"] = 0
|
| 69 |
+
return result
|
| 70 |
+
return super().chat(text, session_id)
|
root/wander-train/oracle_release_runtime_k.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime K — capability cycle 6: explain-why-different (additive over J)."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
from oracle_release_runtime_j import OracleReleaseRuntimeJ
|
| 10 |
+
from stage5m13e8_paragraph_compiler_v1 import _definitional_score
|
| 11 |
+
|
| 12 |
+
WHYDIFF = re.compile(
|
| 13 |
+
r"^(?:please )?(?:explain )?why are (?:the |a |an )?(?P<x>.+?) and (?:the |a |an |my document )?(?P<y>.+?) different[.?!]?$"
|
| 14 |
+
r"|^(?:please )?(?:explain )?why is (?:the |a |an )?(?P<x2>.+?) different from (?:the |a |an |my document )?(?P<y2>.+?)[.?!]?$"
|
| 15 |
+
r"|^(?:please )?(?:explain )?why (?:the |a |an )?(?P<x3>.+?) and (?:the |a |an |my document )?(?P<y3>.+?) are different[.?!]?$"
|
| 16 |
+
r"|^what makes (?:the |a |an )?(?P<x4>.+?) different from (?:the |a |an |my document )?(?P<y4>.+?)[.?!]?$",
|
| 17 |
+
re.I)
|
| 18 |
+
KIND = re.compile(r"\bis (?:a|an|the) ([\w -]{3,40}?)(?:[,.]| that| which| in| of| with)")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class OracleReleaseRuntimeK(OracleReleaseRuntimeJ):
|
| 22 |
+
def _whydiff_turn(self, text: str, session_id: str) -> dict | None:
|
| 23 |
+
match = WHYDIFF.match(text.strip())
|
| 24 |
+
if not match:
|
| 25 |
+
return None
|
| 26 |
+
groups = match.groupdict()
|
| 27 |
+
x = next((groups[k] for k in ("x", "x2", "x3", "x4") if groups.get(k)), "").strip()
|
| 28 |
+
y = next((groups[k] for k in ("y", "y2", "y3", "y4") if groups.get(k)), "").strip()
|
| 29 |
+
if not x or not y:
|
| 30 |
+
return None
|
| 31 |
+
side_x, _ = self._side(x, session_id)
|
| 32 |
+
side_y, _ = self._side(y, session_id)
|
| 33 |
+
missing = [t for t, s in ((x, side_x), (y, side_y)) if not s]
|
| 34 |
+
if missing:
|
| 35 |
+
return {"status": "withheld", "path": "capability:why_different",
|
| 36 |
+
"response": f"I cannot explain the difference: no provenance-admitted material "
|
| 37 |
+
f"exists for {' or '.join(missing)}.",
|
| 38 |
+
"provenance": {"kind": "gated_whydiff", "missing": missing}}
|
| 39 |
+
lead_x = max(side_x, key=lambda c: _definitional_score(x, c["sentence"]))
|
| 40 |
+
lead_y = max(side_y, key=lambda c: _definitional_score(y, c["sentence"]))
|
| 41 |
+
kind_x_match = KIND.search(lead_x["sentence"])
|
| 42 |
+
kind_y_match = KIND.search(lead_y["sentence"])
|
| 43 |
+
kind_x = kind_x_match.group(1).strip() if kind_x_match else None
|
| 44 |
+
kind_y = kind_y_match.group(1).strip() if kind_y_match else None
|
| 45 |
+
if kind_x and kind_y and kind_x.lower() != kind_y.lower():
|
| 46 |
+
reason = (f"Derived reason: they belong to different categories — {x} is attested as "
|
| 47 |
+
f"a {kind_x}, whereas {y} is attested as a {kind_y}.")
|
| 48 |
+
else:
|
| 49 |
+
reason = (f"Derived reason: the admitted records place them in "
|
| 50 |
+
f"{'the same attested category (' + kind_x + '); their difference lies in the particulars below' if kind_x else 'categories the leads do not name explicitly; the difference rests on the particulars below'}.")
|
| 51 |
+
response = (f"According to {lead_x['page']}, {lead_x['sentence']} [{lead_x['claim_id']}] "
|
| 52 |
+
f"According to {lead_y['page']}, {lead_y['sentence']} [{lead_y['claim_id']}] "
|
| 53 |
+
f"{reason} [derived:category_contrast<-{lead_x['claim_id'][:12]},{lead_y['claim_id'][:12]}]")
|
| 54 |
+
return {"status": "answered", "path": "capability:why_different", "response": response,
|
| 55 |
+
"provenance": {"kind": "gated_whydiff", "x": x, "y": y,
|
| 56 |
+
"derived": {"rule": "category_contrast",
|
| 57 |
+
"premises": [lead_x["claim_id"], lead_y["claim_id"]]}}}
|
| 58 |
+
|
| 59 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 60 |
+
started = time.perf_counter()
|
| 61 |
+
normalized = " ".join(text.strip().split())
|
| 62 |
+
result = None
|
| 63 |
+
try:
|
| 64 |
+
result = self._whydiff_turn(normalized, session_id)
|
| 65 |
+
except Exception: # noqa: BLE001
|
| 66 |
+
result = None
|
| 67 |
+
if result is not None:
|
| 68 |
+
result["latency_ms"] = round(1000.0 * (time.perf_counter() - started), 1)
|
| 69 |
+
result["zero_model_gate"] = {"lm_calls": 0, "transformer_calls": 0}
|
| 70 |
+
state = self.dialogue.setdefault(session_id, {})
|
| 71 |
+
state["last_result"] = {k: result.get(k) for k in ("path", "provenance", "status")}
|
| 72 |
+
return result
|
| 73 |
+
return super().chat(text, session_id)
|
root/wander-train/oracle_release_runtime_n.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime N — runtime M with the K8 batch-1 grammar wired into the session
|
| 3 |
+
kernel (additive; the IR pipeline and all capability routes now parse through
|
| 4 |
+
the ~190-construction inventory)."""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from oracle_release_runtime_m import OracleReleaseRuntimeM
|
| 9 |
+
from stage5m13e2_construction_kernel_k8 import ConstructionKernelK8
|
| 10 |
+
from stage5m13e4_migration2 import IRPipeline2
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class OracleReleaseRuntimeN(OracleReleaseRuntimeM):
|
| 14 |
+
def __init__(self, *args, **kwargs):
|
| 15 |
+
super().__init__(*args, **kwargs)
|
| 16 |
+
self.kernel = ConstructionKernelK8(compositional=True)
|
| 17 |
+
self.pipeline = IRPipeline2(self.kernel)
|
root/wander-train/oracle_release_runtime_p.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime P — M17a shadow arbitration (additive over O).
|
| 3 |
+
|
| 4 |
+
Every chat turn is scored by the M-series release arbiter (frozen K138 bid
|
| 5 |
+
geometry + M-series families) and the ArbitrationRecord summary is attached to
|
| 6 |
+
the result as advisory metadata. Dispatch is UNCHANGED — the regex chain still
|
| 7 |
+
commits every action; the arbiter observes. Agreement per turn is recorded so
|
| 8 |
+
the live residue log accumulates the shadow scoreboard for the M17b promotion
|
| 9 |
+
gate. Advisory failure can never break a turn."""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from oracle_release_runtime_o import OracleReleaseRuntimeO
|
| 14 |
+
from stage5m17_release_arbiter_v1 import arbitrate, path_family
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class OracleReleaseRuntimeP(OracleReleaseRuntimeO):
|
| 18 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 19 |
+
result = super().chat(text, session_id)
|
| 20 |
+
try:
|
| 21 |
+
record = arbitrate(" ".join(text.strip().split()))
|
| 22 |
+
regex_family = path_family(result.get("path"))
|
| 23 |
+
result["arbitration"] = {
|
| 24 |
+
"winner": record.winner,
|
| 25 |
+
"runner_up": record.runner_up,
|
| 26 |
+
"margin": round(record.margin, 3),
|
| 27 |
+
"floor": record.clarification_floor_applied,
|
| 28 |
+
"regex_family": regex_family,
|
| 29 |
+
"agrees": record.winner == regex_family if regex_family else None,
|
| 30 |
+
"mode": "shadow",
|
| 31 |
+
}
|
| 32 |
+
except Exception: # noqa: BLE001 — advisory only, never break dispatch
|
| 33 |
+
pass
|
| 34 |
+
return result
|
root/wander-train/oracle_release_runtime_q.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime Q — M17b: arbiter AUTHORITATIVE at the fallback boundary (additive over P).
|
| 3 |
+
|
| 4 |
+
Capture condition: the regex chain landed in the fallback tier
|
| 5 |
+
(document_runtime / session_relational) WITHOUT answering — i.e. it did not
|
| 6 |
+
understand the surface — AND the shadow arbiter bids a re-dispatchable family
|
| 7 |
+
with a clean margin (no clarification floor). Then, and only then, runtime Q
|
| 8 |
+
re-dispatches through the EXISTING route via a canonical surface:
|
| 9 |
+
|
| 10 |
+
capture point 1 (capabilities + definitional): arbiter family + anchor-store
|
| 11 |
+
topic extraction -> canonical surface ("Give me a rundown on the Moon."
|
| 12 |
+
-> summarize + Moon -> "Summarize Moon.") -> parent chain.
|
| 13 |
+
capture point 2 (conversation acts): fixed canonical act surfaces
|
| 14 |
+
("Keep going." -> "Tell me more.").
|
| 15 |
+
|
| 16 |
+
Canonical-surface carriers mirror the K131 carrier stage deliberately;
|
| 17 |
+
typed-goal carrier removal is a later arc (K152 pattern). If re-dispatch fails
|
| 18 |
+
to answer, the ORIGINAL fallback result is returned unchanged — authority can
|
| 19 |
+
narrow to improvement-only, never degrade. Per the K147 law, no raw input is
|
| 20 |
+
ever placed into any response text by this layer (topics are anchor-store
|
| 21 |
+
titles, not user substrings). Each capture point has its own ablation flag."""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import re
|
| 26 |
+
|
| 27 |
+
from oracle_release_runtime_p import OracleReleaseRuntimeP
|
| 28 |
+
from stage5m17_release_arbiter_v1 import features as arb_features
|
| 29 |
+
|
| 30 |
+
FALLBACK_PATHS = {"document_runtime", "session_relational"}
|
| 31 |
+
FALLBACK_OK = {"answered", "recorded"}
|
| 32 |
+
|
| 33 |
+
# words never usable as topic candidates (marker vocabulary + function words)
|
| 34 |
+
TOPIC_BLOCK = {
|
| 35 |
+
"give", "me", "a", "an", "the", "of", "on", "about", "and", "or", "to",
|
| 36 |
+
"for", "in", "what", "whats", "what's", "how", "why", "is", "are", "do",
|
| 37 |
+
"does", "did", "you", "your", "i", "i'd", "id", "could", "would", "please",
|
| 38 |
+
"rundown", "overview", "summary", "summarize", "summarise", "recap",
|
| 39 |
+
"version", "short", "brief", "quick", "sum", "up", "like", "so",
|
| 40 |
+
"difference", "differences", "differ", "different", "between", "compare",
|
| 41 |
+
"contrast", "come", "timeline", "chronology", "history", "walk", "through",
|
| 42 |
+
"year", "by", "show", "list", "name", "few", "couple", "several", "some",
|
| 43 |
+
"things", "facts", "points", "items", "know", "tell", "talk", "mean",
|
| 44 |
+
"means", "actually", "explain", "break", "down", "plain", "language",
|
| 45 |
+
"simple", "simply", "simpler", "terms", "child", "follow", "it", "that",
|
| 46 |
+
"this", "one", "two", "three", "four", "five", "six", "seven",
|
| 47 |
+
"say", "said", "told", "know", "knew", "claim", "think", "thought",
|
| 48 |
+
"call", "called", "gist", "story", "handful", "nutshell",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
ACT_CANONICAL = {
|
| 52 |
+
"act_refine": "Simpler.",
|
| 53 |
+
"act_continue": "Tell me more.",
|
| 54 |
+
"act_provenance": "Why did you say that?",
|
| 55 |
+
"act_acknowledge": "Thanks!",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
NUMBER_WORDS = {"two": "two", "three": "three", "four": "four", "five": "five",
|
| 59 |
+
"six": "six", "seven": "seven", "2": "two", "3": "three",
|
| 60 |
+
"4": "four", "5": "five", "6": "six", "7": "seven"}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class OracleReleaseRuntimeQ(OracleReleaseRuntimeP):
|
| 64 |
+
def __init__(self, *args, **kwargs):
|
| 65 |
+
super().__init__(*args, **kwargs)
|
| 66 |
+
self.capture_capability_enabled = True # ablation flag, capture point 1
|
| 67 |
+
self.capture_act_enabled = True # ablation flag, capture point 2
|
| 68 |
+
|
| 69 |
+
# ---------------------------------------------------- topic extraction
|
| 70 |
+
def _arb_topics(self, text: str) -> list[str]:
|
| 71 |
+
"""Anchor-store titles found in the surface, longest-span first,
|
| 72 |
+
returned in positional order, marker vocabulary excluded."""
|
| 73 |
+
words = [w.strip("'-") for w in re.findall(r"[A-Za-z][\w'-]*|\d+", text)]
|
| 74 |
+
words = [w for w in words if w]
|
| 75 |
+
titles = self.anchors.title_to_page
|
| 76 |
+
found: list[tuple[int, int, str]] = [] # (start, length, title)
|
| 77 |
+
taken: set[int] = set()
|
| 78 |
+
for n in (3, 2, 1):
|
| 79 |
+
for i in range(len(words) - n + 1):
|
| 80 |
+
if any(j in taken for j in range(i, i + n)):
|
| 81 |
+
continue
|
| 82 |
+
span = words[i:i + n]
|
| 83 |
+
if all(w.lower() in TOPIC_BLOCK for w in span):
|
| 84 |
+
continue
|
| 85 |
+
cand = " ".join(span)
|
| 86 |
+
if cand.lower() in titles: # anchor keys are case-normalized
|
| 87 |
+
found.append((i, n, cand))
|
| 88 |
+
taken.update(range(i, i + n))
|
| 89 |
+
return [t for _, _, t in sorted(found)]
|
| 90 |
+
|
| 91 |
+
def _canonical_surface(self, family: str, text: str) -> str | None:
|
| 92 |
+
if family in ACT_CANONICAL:
|
| 93 |
+
return ACT_CANONICAL[family]
|
| 94 |
+
topics = self._arb_topics(text)
|
| 95 |
+
if family in ("compare", "why_different"):
|
| 96 |
+
if len(topics) < 2:
|
| 97 |
+
return None
|
| 98 |
+
a, b = topics[0], topics[1]
|
| 99 |
+
return (f"Compare {a} and {b}." if family == "compare"
|
| 100 |
+
else f"Why are {a} and {b} different?")
|
| 101 |
+
if not topics:
|
| 102 |
+
return None
|
| 103 |
+
topic = topics[0]
|
| 104 |
+
if family == "summarize":
|
| 105 |
+
return f"Summarize {topic}."
|
| 106 |
+
if family == "timeline":
|
| 107 |
+
return f"Give me a timeline of {topic}."
|
| 108 |
+
if family == "explain_simply":
|
| 109 |
+
return f"Explain {topic} simply."
|
| 110 |
+
if family == "list_n":
|
| 111 |
+
tokens = [w.lower() for w in re.findall(r"[A-Za-z]+|\d+", text)]
|
| 112 |
+
count = next((NUMBER_WORDS[t] for t in tokens if t in NUMBER_WORDS), "three")
|
| 113 |
+
return f"List {count} facts about {topic}."
|
| 114 |
+
if family == "definitional":
|
| 115 |
+
# the definiendum is the LAST content NP ("what would you say a
|
| 116 |
+
# glacier is?"), not the first
|
| 117 |
+
return f"What is a {topics[-1].lower()}?"
|
| 118 |
+
return None
|
| 119 |
+
|
| 120 |
+
# ------------------------------------------------------------ dispatch
|
| 121 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 122 |
+
result = super().chat(text, session_id) # P: regex dispatch + shadow record
|
| 123 |
+
arb = result.get("arbitration")
|
| 124 |
+
if (arb is None or arb.get("floor")
|
| 125 |
+
or result.get("path") not in FALLBACK_PATHS
|
| 126 |
+
or result.get("status") in FALLBACK_OK):
|
| 127 |
+
return result
|
| 128 |
+
family = arb.get("winner")
|
| 129 |
+
is_act = family in ACT_CANONICAL
|
| 130 |
+
if is_act and not self.capture_act_enabled:
|
| 131 |
+
return result
|
| 132 |
+
if not is_act and not self.capture_capability_enabled:
|
| 133 |
+
return result
|
| 134 |
+
canonical = self._canonical_surface(family, text)
|
| 135 |
+
if canonical is None:
|
| 136 |
+
return result
|
| 137 |
+
redispatch = super().chat(canonical, session_id)
|
| 138 |
+
# improvement-only authority: a family-correct honest withhold still
|
| 139 |
+
# beats the generic fallback clarification; anything else keeps the
|
| 140 |
+
# original result unchanged
|
| 141 |
+
if redispatch.get("status") not in ("answered", "withheld"):
|
| 142 |
+
return result
|
| 143 |
+
redispatch["path"] = f"arb:{family}->" + str(redispatch.get("path"))
|
| 144 |
+
redispatch["arbitration"] = dict(arb)
|
| 145 |
+
redispatch["arbitration"]["mode"] = "authoritative"
|
| 146 |
+
redispatch["arbitration"]["canonical_surface"] = canonical
|
| 147 |
+
redispatch["arbitration"]["capture_point"] = "act" if is_act else "capability"
|
| 148 |
+
return redispatch
|
root/wander-train/oracle_release_runtime_s.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime S — M17b3: runtime R's capture logic driven by arbiter v3.
|
| 3 |
+
New file; P/Q/R untouched. Same fallback-boundary authority, improvement-only
|
| 4 |
+
rule, ablation flags, and canonical-surface carriers as R."""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from oracle_release_runtime_o import OracleReleaseRuntimeO
|
| 9 |
+
from oracle_release_runtime_q import ACT_CANONICAL, FALLBACK_OK, FALLBACK_PATHS
|
| 10 |
+
from oracle_release_runtime_r import OracleReleaseRuntimeR
|
| 11 |
+
from stage5m17_release_arbiter_v3 import arbitrate as arbitrate_v3
|
| 12 |
+
from stage5m17_release_arbiter_v3 import path_family
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class OracleReleaseRuntimeS(OracleReleaseRuntimeR):
|
| 16 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 17 |
+
result = OracleReleaseRuntimeO.chat(self, text, session_id)
|
| 18 |
+
try:
|
| 19 |
+
record = arbitrate_v3(" ".join(text.strip().split()))
|
| 20 |
+
except Exception: # noqa: BLE001
|
| 21 |
+
return result
|
| 22 |
+
regex_family = path_family(result.get("path"))
|
| 23 |
+
arb = {"winner": record.winner, "runner_up": record.runner_up,
|
| 24 |
+
"margin": round(record.margin, 3),
|
| 25 |
+
"floor": record.clarification_floor_applied,
|
| 26 |
+
"regex_family": regex_family,
|
| 27 |
+
"agrees": record.winner == regex_family if regex_family else None,
|
| 28 |
+
"mode": "shadow", "arbiter": "v3"}
|
| 29 |
+
result["arbitration"] = arb
|
| 30 |
+
if (arb["floor"] or result.get("path") not in FALLBACK_PATHS
|
| 31 |
+
or result.get("status") in FALLBACK_OK):
|
| 32 |
+
return result
|
| 33 |
+
family = record.winner
|
| 34 |
+
is_act = family in ACT_CANONICAL
|
| 35 |
+
if is_act and not self.capture_act_enabled:
|
| 36 |
+
return result
|
| 37 |
+
if not is_act and not self.capture_capability_enabled:
|
| 38 |
+
return result
|
| 39 |
+
canonical = self._canonical_surface(family, text)
|
| 40 |
+
if canonical is None:
|
| 41 |
+
return result
|
| 42 |
+
redispatch = OracleReleaseRuntimeO.chat(self, canonical, session_id)
|
| 43 |
+
if redispatch.get("status") not in ("answered", "withheld"):
|
| 44 |
+
return result
|
| 45 |
+
redispatch["path"] = f"arb:{family}->" + str(redispatch.get("path"))
|
| 46 |
+
redispatch["arbitration"] = dict(arb)
|
| 47 |
+
redispatch["arbitration"]["mode"] = "authoritative"
|
| 48 |
+
redispatch["arbitration"]["canonical_surface"] = canonical
|
| 49 |
+
redispatch["arbitration"]["capture_point"] = "act" if is_act else "capability"
|
| 50 |
+
return redispatch
|
root/wander-train/oracle_release_runtime_u.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Runtime U — graceful-clarify terminal (additive over T).
|
| 3 |
+
|
| 4 |
+
The last routing step for conversational smoothness: when a turn falls all the
|
| 5 |
+
way through the chain into the fallback tier WITHOUT being answered (the
|
| 6 |
+
document tier's "partial" evidence roundups and Wikipedia-address asks were
|
| 7 |
+
the wrong register for conversation — Ben-reported class, 2026-08-19), the
|
| 8 |
+
turn terminates in a typed clarify act instead: a short capability statement
|
| 9 |
+
in the machine's own voice. Answered / recorded / withheld results pass
|
| 10 |
+
through untouched; the arbitration record is preserved on the clarify so the
|
| 11 |
+
residue scoreboard keeps its labels. Per the K147 law, no user text rides in
|
| 12 |
+
the clarify."""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import time
|
| 17 |
+
|
| 18 |
+
from oracle_release_runtime_t import OracleReleaseRuntimeT
|
| 19 |
+
|
| 20 |
+
CLARIFY_RESPONSE = (
|
| 21 |
+
"I didn't fully parse that, so I won't guess. I can summarize a topic, "
|
| 22 |
+
"compare two things, list facts, give a timeline, explain something "
|
| 23 |
+
"simply, define a term, reason about why and what-if, and discuss any "
|
| 24 |
+
"document you teach me. A shorter, more direct phrasing usually lands."
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
PASS_THROUGH = {"answered", "recorded", "withheld"}
|
| 28 |
+
FALLBACK_TIER = {"document_runtime", "session_relational"}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class OracleReleaseRuntimeU(OracleReleaseRuntimeT):
|
| 32 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 33 |
+
started = time.perf_counter()
|
| 34 |
+
result = super().chat(text, session_id)
|
| 35 |
+
if (result.get("path") in FALLBACK_TIER
|
| 36 |
+
and result.get("status") not in PASS_THROUGH):
|
| 37 |
+
return {"status": "clarification", "path": "act:clarify",
|
| 38 |
+
"response": CLARIFY_RESPONSE,
|
| 39 |
+
"provenance": {"kind": "dialogue_state",
|
| 40 |
+
"note": "graceful clarify terminal"},
|
| 41 |
+
"arbitration": result.get("arbitration"),
|
| 42 |
+
"latency_ms": round(1000.0 * (time.perf_counter() - started), 1),
|
| 43 |
+
"zero_model_gate": {"lm_calls": 0, "transformer_calls": 0}}
|
| 44 |
+
return result
|
root/wander-train/oracle_release_runtime_w58.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""oracle_release_runtime_w58 - JUSTIFICATION: why did you do that, answered from
|
| 3 |
+
the record rather than from a story about the record.
|
| 4 |
+
|
| 5 |
+
Found by the salience pass over the held-out residue, not by me guessing. Among
|
| 6 |
+
the turns the machine could not answer was "Why did you choose that language?",
|
| 7 |
+
falling to act:clarify. It had just written a python program, recorded that it
|
| 8 |
+
wrote a python program, recorded which fold wrote it and what the execution
|
| 9 |
+
printed, and then could not say why python.
|
| 10 |
+
|
| 11 |
+
That gap matters more than its size. The entire claim of this architecture is
|
| 12 |
+
that every answer carries a warrant which can be inspected. If the warrant can
|
| 13 |
+
only be inspected by reading a JSON file on a server, it is not a conversational
|
| 14 |
+
property, it is a filing system. A machine that can be asked "how did you get
|
| 15 |
+
that" and answers from its own witness record is doing the thing the design is
|
| 16 |
+
for.
|
| 17 |
+
|
| 18 |
+
So this fold answers three questions about the PREVIOUS turn, and only from what
|
| 19 |
+
the record actually holds:
|
| 20 |
+
|
| 21 |
+
why did you choose that the choice, and what determined it
|
| 22 |
+
how did you get that the warrant: derivation, computation, execution
|
| 23 |
+
what did you just do the operator and the construction it built
|
| 24 |
+
|
| 25 |
+
The discipline, which is the whole point: it reports the reason ONLY when the
|
| 26 |
+
record supports it. If you did not name a language, the record shows the request
|
| 27 |
+
with no language in it and the output in python, so "you did not name one and
|
| 28 |
+
python is my default" is a reading of the record. If you did name one, the reason
|
| 29 |
+
is that you asked. And when the record holds no reason at all, it says exactly
|
| 30 |
+
that, because the failure mode here is not silence, it is a machine that
|
| 31 |
+
confabulates a plausible motive for its own behaviour. That is the one thing this
|
| 32 |
+
fold must never do.
|
| 33 |
+
|
| 34 |
+
It also declines to speak when there is no previous turn, and hands back anything
|
| 35 |
+
that is not about the previous turn, so it cannot capture a conversation the way
|
| 36 |
+
the deduction fold did before w56.
|
| 37 |
+
|
| 38 |
+
Hand-authored 2026-08-28. Judged by the gate.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
from __future__ import annotations
|
| 42 |
+
|
| 43 |
+
import re
|
| 44 |
+
|
| 45 |
+
from oracle_release_runtime_w57 import OracleReleaseRuntimeW57
|
| 46 |
+
|
| 47 |
+
_WHY_CHOICE = re.compile(
|
| 48 |
+
r'\bwhy\s+(?:did|do)\s+you\s+(?:choose|pick|use|write|go with)\b'
|
| 49 |
+
r'|\bwhy\s+that\s+(?:language|one|approach|way)\b'
|
| 50 |
+
r'|\bwhat\s+made\s+you\s+(?:choose|pick|use)\b', re.I)
|
| 51 |
+
_HOW_GOT = re.compile(
|
| 52 |
+
r'\bhow\s+(?:did|do)\s+you\s+(?:get|know|work(?:ed)?\s+that\s+out|arrive|compute|'
|
| 53 |
+
r'calculate|check)\b|\bhow\s+was\s+that\s+(?:checked|verified)\b'
|
| 54 |
+
r'|\bshow\s+(?:me\s+)?(?:your|the)\s+(?:work|steps|working)\b', re.I)
|
| 55 |
+
_WHAT_DID = re.compile(
|
| 56 |
+
r'\bwhat\s+did\s+you\s+just\s+do\b|\bwhat\s+(?:route|operator)\s+was\s+that\b'
|
| 57 |
+
r'|\bhow\s+did\s+you\s+answer\s+that\b', re.I)
|
| 58 |
+
|
| 59 |
+
_LANGS = ('python', 'javascript', 'go', 'bash', 'sql', 'c', 'rust', 'java',
|
| 60 |
+
'ruby', 'typescript')
|
| 61 |
+
_WARRANT_NAME = {
|
| 62 |
+
'test_execution': 'I ran it on this machine and compared the output',
|
| 63 |
+
'deduction': 'I derived it, and each step names the rule that licensed it',
|
| 64 |
+
'algebra': 'I solved it and then checked the answer by substituting it back',
|
| 65 |
+
'computation': 'I computed it and kept the trace',
|
| 66 |
+
'citation': 'I cited the source it came from',
|
| 67 |
+
'differential_execution': 'I computed the expected result a second way and '
|
| 68 |
+
'only answered when the two agreed',
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class OracleReleaseRuntimeW58(OracleReleaseRuntimeW57):
|
| 73 |
+
"""Answers about the previous turn, grounded in that turn's witness record."""
|
| 74 |
+
|
| 75 |
+
def _prior_turn(self, session_id):
|
| 76 |
+
"""The last recorded turn that was not itself one of these questions."""
|
| 77 |
+
hist = getattr(self, '_paths', None) or {}
|
| 78 |
+
q = hist.get(session_id)
|
| 79 |
+
if not q:
|
| 80 |
+
return None
|
| 81 |
+
for entry in reversed(list(q)):
|
| 82 |
+
if str(entry.get('operator', '')).startswith('justify:'):
|
| 83 |
+
continue
|
| 84 |
+
return entry
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def _choice_reason(prior):
|
| 89 |
+
"""Why this output, IF the record shows it. Otherwise None."""
|
| 90 |
+
conv = prior.get('conversion') or {}
|
| 91 |
+
made = str(conv.get('to', ''))
|
| 92 |
+
asked = str(conv.get('from', '') or prior.get('in_surface', '')).lower()
|
| 93 |
+
lang = next((l for l in _LANGS if made.lower().startswith(l + '/')
|
| 94 |
+
or ('/' + l) in made.lower()), None)
|
| 95 |
+
if not lang:
|
| 96 |
+
return None
|
| 97 |
+
named = next((l for l in _LANGS if re.search(r'\b%s\b' % l, asked)), None)
|
| 98 |
+
if named == lang:
|
| 99 |
+
return ('I used %s because you asked for %s. The record of that turn '
|
| 100 |
+
'shows your request and the program it produced.' % (lang, named))
|
| 101 |
+
if named is None:
|
| 102 |
+
return ('I used %s because your request did not name a language, and '
|
| 103 |
+
'%s is what my code fold writes when none is named. That is a '
|
| 104 |
+
'default, not a judgement that %s was best for the task.'
|
| 105 |
+
% (lang, lang, lang))
|
| 106 |
+
return ('I used %s although your request mentioned %s. My record shows the '
|
| 107 |
+
'mismatch and does not record a reason for it, so I am telling you '
|
| 108 |
+
'the mismatch rather than explaining it away.' % (lang, named))
|
| 109 |
+
|
| 110 |
+
def _justify(self, kind, prior):
|
| 111 |
+
conv = prior.get('conversion') or {}
|
| 112 |
+
vw = prior.get('verified_warrant') or {}
|
| 113 |
+
op = str(prior.get('operator', '') or 'an unrecorded route')
|
| 114 |
+
by = str(conv.get('produced_by', '') or 'an unrecorded fold')
|
| 115 |
+
|
| 116 |
+
if kind == 'why_choice':
|
| 117 |
+
reason = self._choice_reason(prior)
|
| 118 |
+
if reason:
|
| 119 |
+
return reason + ' [self:witness]'
|
| 120 |
+
return ('My record for that turn shows the route (%s) and what it built '
|
| 121 |
+
'(%s), but it does not record a reason for the choice you are '
|
| 122 |
+
'asking about. I am not going to invent one. [self:witness]'
|
| 123 |
+
% (op, conv.get('to') or 'no construction recorded'))
|
| 124 |
+
|
| 125 |
+
if kind == 'how_got':
|
| 126 |
+
if not vw.get('kind'):
|
| 127 |
+
# Citation is a warrant too, and it lives in a different field. The
|
| 128 |
+
# first version of this fold read only verified_warrant and told a
|
| 129 |
+
# visitor that a properly sourced encyclopedic answer "should not
|
| 130 |
+
# have been presented as settled", which is a machine libelling its
|
| 131 |
+
# own good work. Check every warrant the record can carry.
|
| 132 |
+
cited = list((prior.get('relation') or {}).get('grounded_cited') or [])
|
| 133 |
+
if cited or prior.get('citations'):
|
| 134 |
+
return ('I did not compute that one, I quoted it. The record for '
|
| 135 |
+
'that turn carries %d citation%s%s, so the warrant is the '
|
| 136 |
+
'source rather than a derivation I can replay. '
|
| 137 |
+
'[self:witness]'
|
| 138 |
+
% (prior.get('citations') or len(cited),
|
| 139 |
+
'' if (prior.get('citations') or len(cited)) == 1 else 's',
|
| 140 |
+
' covering %s' % ', '.join(str(c) for c in cited[:4])
|
| 141 |
+
if cited else ''))
|
| 142 |
+
return ('That answer came through %s, and my record for it holds no '
|
| 143 |
+
'warrant of any kind, neither a citation nor a check I can '
|
| 144 |
+
'replay. I should not have presented it as settled. '
|
| 145 |
+
'[self:witness]' % op)
|
| 146 |
+
head = _WARRANT_NAME.get(str(vw['kind']),
|
| 147 |
+
'the warrant recorded is of kind %s' % vw['kind'])
|
| 148 |
+
steps = [str(s) for s in (vw.get('steps') or [])]
|
| 149 |
+
if steps:
|
| 150 |
+
return ('%s. The recorded steps were: %s. [self:witness]'
|
| 151 |
+
% (head, '; then '.join(steps[:6])))
|
| 152 |
+
return '%s, though the record kept no individual steps. [self:witness]' % head
|
| 153 |
+
|
| 154 |
+
return ('That turn went through %s, produced by %s. It built a %s, from "%s" '
|
| 155 |
+
'to "%s"%s. [self:witness]'
|
| 156 |
+
% (op, by, conv.get('construction') or 'result',
|
| 157 |
+
str(conv.get('from', ''))[:60],
|
| 158 |
+
str(conv.get('to', ''))[:60],
|
| 159 |
+
', warranted by %s' % vw['kind'] if vw.get('kind') else
|
| 160 |
+
', with no warrant recorded'))
|
| 161 |
+
|
| 162 |
+
def chat(self, text: str, session_id: str = 'default') -> dict:
|
| 163 |
+
s = ' '.join(str(text).strip().split())
|
| 164 |
+
if _WHY_CHOICE.search(s):
|
| 165 |
+
kind = 'why_choice'
|
| 166 |
+
elif _HOW_GOT.search(s):
|
| 167 |
+
kind = 'how_got'
|
| 168 |
+
elif _WHAT_DID.search(s):
|
| 169 |
+
kind = 'what_did'
|
| 170 |
+
else:
|
| 171 |
+
return super().chat(s, session_id)
|
| 172 |
+
|
| 173 |
+
prior = self._prior_turn(session_id)
|
| 174 |
+
if prior is None:
|
| 175 |
+
return {'status': 'withheld', 'path': 'justify:no_prior_turn',
|
| 176 |
+
'response': ('You are asking about a previous answer and this is '
|
| 177 |
+
'the first turn I have recorded in this session, so '
|
| 178 |
+
'there is nothing for me to read.'),
|
| 179 |
+
'provenance': {'kind': 'justification', 'prior_turn': None},
|
| 180 |
+
'latency_ms': 1}
|
| 181 |
+
|
| 182 |
+
body = self._justify(kind, prior)
|
| 183 |
+
from collections import deque
|
| 184 |
+
hist = getattr(self, '_paths', None)
|
| 185 |
+
if hist is None:
|
| 186 |
+
hist = self._paths = {}
|
| 187 |
+
q = hist.get(session_id)
|
| 188 |
+
if q is None:
|
| 189 |
+
q = hist[session_id] = deque(maxlen=64)
|
| 190 |
+
prev = q[-1].get('turn', 0) if len(q) else 0
|
| 191 |
+
q.append({'turn': prev + 1, 'in_surface': s[:120],
|
| 192 |
+
'in_form': ['question_about_prior_turn'],
|
| 193 |
+
'operator': 'justify:' + kind,
|
| 194 |
+
'out_status': 'answered', 'in_terms': [], 'out_terms': [],
|
| 195 |
+
'relation': {'grounded': [], 'grounded_cited': [],
|
| 196 |
+
'introduced': [], 'unaccounted': []},
|
| 197 |
+
'carried_from_prior': ['witness_record'],
|
| 198 |
+
'continuity_lexical': True, 'continuity': True, 'citations': 1,
|
| 199 |
+
'residual': None, 'cost_ms': 1.0,
|
| 200 |
+
'conversion': {'construction': 'ACCOUNT_OF_PRIOR_TURN',
|
| 201 |
+
'from': s[:60],
|
| 202 |
+
'to': str(prior.get('operator', ''))[:60],
|
| 203 |
+
'produced_by': 'justify_w58'},
|
| 204 |
+
'verified_warrant': {'kind': 'self_citation',
|
| 205 |
+
'steps': ['read the witness record of turn %s'
|
| 206 |
+
% prior.get('turn')]}})
|
| 207 |
+
return {'status': 'answered', 'path': 'justify:' + kind,
|
| 208 |
+
'response': body,
|
| 209 |
+
'provenance': {'kind': 'justification', 'read_turn': prior.get('turn'),
|
| 210 |
+
'prior_operator': prior.get('operator'),
|
| 211 |
+
'source': 'witness_record'},
|
| 212 |
+
'latency_ms': 2}
|
root/wander-train/oracle_release_runtime_w59.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""oracle_release_runtime_w59 - THE OPERATOR HOST: one disciplined runtime, many
|
| 3 |
+
operators, none of them hand-written as folds.
|
| 4 |
+
|
| 5 |
+
Every capability so far arrived as its own module, and every defect found in those
|
| 6 |
+
modules today lived in the same shared boilerplate rather than in any of their
|
| 7 |
+
reasoning. A missing witness record made a turn invisible to the gate. A missing
|
| 8 |
+
relevance guard let the deduction fold swallow whole conversations. A self-report
|
| 9 |
+
read a file instead of reading itself. A justification fold checked one warrant
|
| 10 |
+
field and told a visitor that a properly sourced answer should not be trusted.
|
| 11 |
+
Four folds, four instances of the same class of mistake, because each one
|
| 12 |
+
reimplemented the parts that are not the interesting part.
|
| 13 |
+
|
| 14 |
+
So this module holds no capability of its own. It is a HOST. The operators are
|
| 15 |
+
declarative specs in oracle_operator_specs_v1, and the host supplies exactly once:
|
| 16 |
+
|
| 17 |
+
dispatch the first spec whose trigger matches and whose parse succeeds
|
| 18 |
+
verification fn computes the answer, ref recomputes it by a different method,
|
| 19 |
+
and NOTHING is reported unless the two agree
|
| 20 |
+
decline a named boundary, in the operator's own words
|
| 21 |
+
passthrough when the surface is not this operator's business, the chain
|
| 22 |
+
answers exactly as it would if the host were not installed
|
| 23 |
+
witness one recording path, so no operator can be invisible to the gate
|
| 24 |
+
|
| 25 |
+
Adding a capability is now adding a spec, and a spec cannot forget the discipline
|
| 26 |
+
because it never implements it.
|
| 27 |
+
|
| 28 |
+
The verification step is the reason to build it this way rather than as a library
|
| 29 |
+
of helpers. An operator that computed and reported would be a faster way to be
|
| 30 |
+
wrong. Here the answer has to survive being computed twice by different routes,
|
| 31 |
+
and when the two disagree the host says so and withholds, which is a state the
|
| 32 |
+
previous hand-written folds had no way to even be in.
|
| 33 |
+
|
| 34 |
+
Six operators arrive with this host: unit conversion, gcd and lcm, base
|
| 35 |
+
conversion, descriptive statistics, sequence continuation, roman numerals. The
|
| 36 |
+
count is not the point. The point is that the seventh costs a spec.
|
| 37 |
+
|
| 38 |
+
Hand-authored 2026-08-28. Judged by the gate.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
from __future__ import annotations
|
| 42 |
+
|
| 43 |
+
from fractions import Fraction
|
| 44 |
+
|
| 45 |
+
from oracle_release_runtime_w58 import OracleReleaseRuntimeW58
|
| 46 |
+
from oracle_operator_specs_v1 import SPECS, NotOurs, OutOfFragment
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _same(a, b):
|
| 50 |
+
"""Do two independently computed answers agree? Types must not paper over it."""
|
| 51 |
+
if a is None or b is None:
|
| 52 |
+
return False
|
| 53 |
+
if isinstance(a, str) or isinstance(b, str):
|
| 54 |
+
return str(a).strip().lower() == str(b).strip().lower()
|
| 55 |
+
try:
|
| 56 |
+
return Fraction(a) == Fraction(b)
|
| 57 |
+
except (TypeError, ValueError):
|
| 58 |
+
return str(a) == str(b)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class OracleReleaseRuntimeW59(OracleReleaseRuntimeW58):
|
| 62 |
+
"""Runs declarative operators, and reports only what two methods agree on."""
|
| 63 |
+
|
| 64 |
+
def _witness_op(self, session_id, surface, spec, steps, status, warrant=None):
|
| 65 |
+
from collections import deque
|
| 66 |
+
hist = getattr(self, '_paths', None)
|
| 67 |
+
if hist is None:
|
| 68 |
+
hist = self._paths = {}
|
| 69 |
+
q = hist.get(session_id)
|
| 70 |
+
if q is None:
|
| 71 |
+
q = hist[session_id] = deque(maxlen=64)
|
| 72 |
+
prev = q[-1].get('turn', 0) if len(q) else 0
|
| 73 |
+
q.append({'turn': prev + 1, 'in_surface': str(surface)[:120],
|
| 74 |
+
'in_form': [spec['family']],
|
| 75 |
+
'operator': 'op:%s' % spec['name'],
|
| 76 |
+
'out_status': status, 'in_terms': [], 'out_terms': [],
|
| 77 |
+
'relation': {'grounded': [], 'grounded_cited': [],
|
| 78 |
+
'introduced': [], 'unaccounted': []},
|
| 79 |
+
'carried_from_prior': [], 'continuity_lexical': False,
|
| 80 |
+
'continuity': True, 'citations': 0, 'residual': None,
|
| 81 |
+
'cost_ms': 1.0,
|
| 82 |
+
'conversion': {'construction': 'VERIFIED_COMPUTATION',
|
| 83 |
+
'from': str(surface)[:80],
|
| 84 |
+
'to': (steps[-1][:60] if steps else spec['name']),
|
| 85 |
+
'produced_by': 'op_%s_w59' % spec['name']},
|
| 86 |
+
'verified_warrant': warrant})
|
| 87 |
+
|
| 88 |
+
def chat(self, text: str, session_id: str = 'default') -> dict:
|
| 89 |
+
s = ' '.join(str(text).strip().split())
|
| 90 |
+
|
| 91 |
+
for spec in SPECS:
|
| 92 |
+
if not spec['trigger'].search(s):
|
| 93 |
+
continue
|
| 94 |
+
try:
|
| 95 |
+
args = spec['parse'](s)
|
| 96 |
+
except NotOurs:
|
| 97 |
+
continue # not this operator, keep looking
|
| 98 |
+
except OutOfFragment as why:
|
| 99 |
+
return self._decline(session_id, s, spec, why)
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
value, steps = spec['fn'](args)
|
| 103 |
+
except NotOurs:
|
| 104 |
+
continue
|
| 105 |
+
except OutOfFragment as why:
|
| 106 |
+
return self._decline(session_id, s, spec, why)
|
| 107 |
+
except Exception: # noqa: BLE001 - a crash is not an answer
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
check = spec['ref'](args)
|
| 112 |
+
except Exception: # noqa: BLE001
|
| 113 |
+
check = None
|
| 114 |
+
|
| 115 |
+
if not _same(value, check):
|
| 116 |
+
# The interesting failure. Two methods, one answer each, and they
|
| 117 |
+
# do not match, so the honest report is the disagreement itself.
|
| 118 |
+
self._witness_op(session_id, s, spec, steps, 'withheld')
|
| 119 |
+
return {'status': 'withheld', 'path': 'op:%s:unverified' % spec['name'],
|
| 120 |
+
'response': ('I worked that out two ways and got two '
|
| 121 |
+
'different answers, %s and %s, so one of my '
|
| 122 |
+
'methods is wrong and I do not yet know which. '
|
| 123 |
+
'I am not going to pick the one I like.'
|
| 124 |
+
% (value, check)),
|
| 125 |
+
'provenance': {'kind': 'computation', 'verified': False,
|
| 126 |
+
'operator': spec['name'],
|
| 127 |
+
'disagreement': [str(value), str(check)]},
|
| 128 |
+
'latency_ms': 2}
|
| 129 |
+
|
| 130 |
+
body = spec['render'](args, value, steps)
|
| 131 |
+
trace = ' '.join('%s.' % st for st in steps[:6])
|
| 132 |
+
warrant = {'kind': spec['warrant'], 'steps': list(steps)}
|
| 133 |
+
self._witness_op(session_id, s, spec, steps, 'answered', warrant)
|
| 134 |
+
pending = getattr(self, '_pending_conversion', None)
|
| 135 |
+
if pending is None:
|
| 136 |
+
pending = self._pending_conversion = {}
|
| 137 |
+
pending[session_id] = {'construction': 'VERIFIED_COMPUTATION',
|
| 138 |
+
'from': s[:80], 'to': str(value)[:60],
|
| 139 |
+
'produced_by': 'op_%s_w59' % spec['name']}
|
| 140 |
+
return {'status': 'answered', 'path': 'op:%s' % spec['name'],
|
| 141 |
+
'response': ('%s %s I checked it by a second method that '
|
| 142 |
+
'agrees.' % (body, trace)).strip(),
|
| 143 |
+
'provenance': {'kind': 'computation', 'verified': True,
|
| 144 |
+
'operator': spec['name'],
|
| 145 |
+
'family': spec['family'],
|
| 146 |
+
'steps': list(steps),
|
| 147 |
+
'independence': spec['independence'],
|
| 148 |
+
'checked_by': 'independent reimplementation'},
|
| 149 |
+
'latency_ms': 2}
|
| 150 |
+
|
| 151 |
+
return super().chat(s, session_id)
|
| 152 |
+
|
| 153 |
+
def _decline(self, session_id, surface, spec, why):
|
| 154 |
+
self._witness_op(session_id, surface, spec, [], 'withheld')
|
| 155 |
+
return {'status': 'withheld',
|
| 156 |
+
'path': 'op:%s:out_of_fragment' % spec['name'],
|
| 157 |
+
'response': ('%s. What I do handle is %s. I could give you a '
|
| 158 |
+
'plausible looking answer, but I would have no way to '
|
| 159 |
+
'check it, so I will not.'
|
| 160 |
+
% (str(why).rstrip('.'), spec['fragment'])),
|
| 161 |
+
'provenance': {'kind': 'computation', 'verified': False,
|
| 162 |
+
'operator': spec['name'], 'reason': str(why),
|
| 163 |
+
'fragment': spec['fragment']},
|
| 164 |
+
'latency_ms': 1}
|
root/wander-train/oracle_release_runtime_w60.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""oracle_release_runtime_w60 - CORRECTNESS RELEASE: a refusal is not a missing warrant.
|
| 3 |
+
|
| 4 |
+
The justification fold reads the previous turn's record and reports the warrant it
|
| 5 |
+
finds. Asked "how did you get that" after a turn where the machine DECLINED, it
|
| 6 |
+
found no warrant, because nothing had been asserted, and reported:
|
| 7 |
+
|
| 8 |
+
my record for it holds no warrant of any kind, neither a citation nor a check
|
| 9 |
+
I can replay. I should not have presented it as settled.
|
| 10 |
+
|
| 11 |
+
Every clause of that is false about a refusal. Nothing was presented. Nothing was
|
| 12 |
+
settled. The turn was the machine working correctly, and its own account of itself
|
| 13 |
+
called it a lapse.
|
| 14 |
+
|
| 15 |
+
This is the second time in one day that the justification fold has been wrong in
|
| 16 |
+
the same direction, and the direction is worth naming: it was written to catch a
|
| 17 |
+
machine overclaiming, so when it meets a turn that claimed nothing it reads the
|
| 18 |
+
absence of a warrant as the absence of grounds. The first case was citation, where
|
| 19 |
+
the warrant was real and in a different field. This case is refusal, where there
|
| 20 |
+
is correctly no warrant at all and the right report is why there is none.
|
| 21 |
+
|
| 22 |
+
A turn is now read by its STATUS first and its warrant second:
|
| 23 |
+
|
| 24 |
+
answered + warrant report the warrant, as before
|
| 25 |
+
answered + citations report the source, as w58 already did
|
| 26 |
+
withheld report that it declined, and say a refusal has no
|
| 27 |
+
working to show, because that is the honest account
|
| 28 |
+
answered + nothing the original complaint, which remains correct in the
|
| 29 |
+
one case it was written for
|
| 30 |
+
|
| 31 |
+
Hand-authored 2026-08-28. Judged by the gate.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
from oracle_release_runtime_w59 import OracleReleaseRuntimeW59
|
| 37 |
+
|
| 38 |
+
_REFUSAL_TAILS = ('does_not_follow', 'out_of_fragment', 'unverified',
|
| 39 |
+
'no_primitive', 'unsupported')
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class OracleReleaseRuntimeW60(OracleReleaseRuntimeW59):
|
| 43 |
+
"""Justification that can tell an absent answer from an ungrounded one."""
|
| 44 |
+
|
| 45 |
+
@staticmethod
|
| 46 |
+
def _was_refusal(prior):
|
| 47 |
+
if str(prior.get('out_status', '')) == 'withheld':
|
| 48 |
+
return True
|
| 49 |
+
return str(prior.get('operator', '')).endswith(_REFUSAL_TAILS)
|
| 50 |
+
|
| 51 |
+
def _justify(self, kind, prior):
|
| 52 |
+
if kind in ('how_got', 'what_did') and self._was_refusal(prior):
|
| 53 |
+
op = str(prior.get('operator', '') or 'an unrecorded route')
|
| 54 |
+
if kind == 'how_got':
|
| 55 |
+
return ('I did not work that one out, I declined it. A refusal has '
|
| 56 |
+
'no working to show, which is the point of it: I stopped '
|
| 57 |
+
'because I had no way to check an answer, so there is no '
|
| 58 |
+
'answer of mine to account for. My record marks that turn '
|
| 59 |
+
'as withheld by %s. [self:witness]' % op)
|
| 60 |
+
return ('That turn was a refusal rather than an answer. %s took it, '
|
| 61 |
+
'found it outside what it can check, and withheld rather than '
|
| 62 |
+
'producing something neither of us could verify. '
|
| 63 |
+
'[self:witness]' % op)
|
| 64 |
+
return super()._justify(kind, prior)
|
root/wander-train/oracle_release_runtime_w71.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""oracle_release_runtime_w71 - OPEN CONVERSATION: resolving "it" by elimination.
|
| 3 |
+
|
| 4 |
+
The held-out residue is not a scatter of unrelated failures. Read it and almost all
|
| 5 |
+
of it is one thing:
|
| 6 |
+
|
| 7 |
+
Did you run it? Can you test it? How does it work?
|
| 8 |
+
Write the same thing in go. how old is it?
|
| 9 |
+
Now write one that counts vowels. what else?
|
| 10 |
+
The thing from before. Explain the thing with the water.
|
| 11 |
+
|
| 12 |
+
Every one of those is a surface that does not name its own subject. The machine had
|
| 13 |
+
just produced the subject, recorded it, and could still say "I didn't fully parse
|
| 14 |
+
that". It was holding the answer and could not see the question.
|
| 15 |
+
|
| 16 |
+
So this is the elimination move again, pointed at reference. The candidate referents
|
| 17 |
+
are read from the witness record, the surface and the machine's own limits ablate
|
| 18 |
+
them, and what survives is the reading:
|
| 19 |
+
|
| 20 |
+
one survivor resolve, say which referent and why, and re-dispatch
|
| 21 |
+
many survivors name them and ask which, rather than picking a favourite
|
| 22 |
+
zero survivors say what could not be resolved instead of guessing
|
| 23 |
+
|
| 24 |
+
Which gives open conversation the thing it has never had here: a WARRANT. Not for
|
| 25 |
+
the reply being good, which is a matter of taste and always will be, but for the
|
| 26 |
+
INTERPRETATION being licensed. "I read 'it' as the python program from two turns
|
| 27 |
+
ago, because that is the only thing in this conversation you can run" is checkable
|
| 28 |
+
by the person who said "it".
|
| 29 |
+
|
| 30 |
+
Three referent kinds, each read from a different part of the record because that is
|
| 31 |
+
where the machine actually keeps them: topics from in_terms, programs and results
|
| 32 |
+
from the conversion. A constraint set per surface form decides which kinds a
|
| 33 |
+
question can even accept, so "did you run it" cannot resolve to a topic and "how old
|
| 34 |
+
is it" cannot resolve to a program.
|
| 35 |
+
|
| 36 |
+
ONE GUARD WORTH NAMING: the conversion field is not always about the turn it sits
|
| 37 |
+
on. A summarize turn was found carrying a stale conversion from a fraction question
|
| 38 |
+
two turns earlier, because the pending-conversion slot is not cleared between turns.
|
| 39 |
+
Anything reading it blindly attributes the wrong artifact, so every read here checks
|
| 40 |
+
that the conversion's own `from` matches the surface of the turn it was found on.
|
| 41 |
+
That is a defect in a fold below this one; this works around it rather than
|
| 42 |
+
pretending it is not there.
|
| 43 |
+
|
| 44 |
+
Hand-authored 2026-08-28. Judged by the gate.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
from __future__ import annotations
|
| 48 |
+
|
| 49 |
+
import re
|
| 50 |
+
|
| 51 |
+
from oracle_release_runtime_w70 import OracleReleaseRuntimeW70
|
| 52 |
+
from oracle_operator_parse_v1 import Reading, eliminate, Ambiguous
|
| 53 |
+
from oracle_operator_specs_v1 import NotOurs, OutOfFragment
|
| 54 |
+
|
| 55 |
+
# surfaces that do not name their subject
|
| 56 |
+
_PRONOUN = re.compile(r'\b(?:it|that|this|them|those|one)\b', re.I)
|
| 57 |
+
_BARE_CONT = re.compile(r'^(?:what else|anything else|go on|more|and\?|tell me more)'
|
| 58 |
+
r'\s*\??$', re.I)
|
| 59 |
+
_THE_THING = re.compile(r'\bthe (?:same )?thing\b|\bthe one\b', re.I)
|
| 60 |
+
|
| 61 |
+
# what each surface form is ASKING FOR, which is what rules referents out
|
| 62 |
+
_WANTS_PROGRAM = re.compile(
|
| 63 |
+
r'\b(?:did|can|could|will|would)\s+you\s+(?:run|test|execute|try)\b'
|
| 64 |
+
r'|\bthe same (?:thing|code|program)\b|\bin (?:go|python|javascript|bash|sql|c)\b'
|
| 65 |
+
r'|\bnow write one\b|\bdoes it (?:compile|run|work)\b', re.I)
|
| 66 |
+
_WANTS_TOPIC = re.compile(
|
| 67 |
+
r'\bhow (?:does|do) (?:it|they) work\b|\bhow old\b|\bwhere is it\b'
|
| 68 |
+
r'|\btell me more\b|\bwhat else\b|\bexplain (?:it|that|the thing)\b'
|
| 69 |
+
r'|\bwhy (?:is|does) it\b|\bwhat is it made of\b', re.I)
|
| 70 |
+
_LANG = re.compile(r'\bin (go|python|javascript|bash|sql|c|rust|java)\b', re.I)
|
| 71 |
+
_STOP_TERMS = {'summarize', 'explain', 'compare', 'list', 'tell', 'facts', 'about',
|
| 72 |
+
'what', 'how', 'why', 'the', 'a', 'an'}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class OracleReleaseRuntimeW71(OracleReleaseRuntimeW70):
|
| 76 |
+
"""Open conversation with a licensed reading of what "it" refers to."""
|
| 77 |
+
|
| 78 |
+
def _referents(self, session_id):
|
| 79 |
+
"""Candidate referents, newest first, read from the witness record."""
|
| 80 |
+
hist = getattr(self, '_paths', None) or {}
|
| 81 |
+
q = hist.get(session_id)
|
| 82 |
+
out = []
|
| 83 |
+
if not q:
|
| 84 |
+
return out
|
| 85 |
+
for e in reversed(list(q)):
|
| 86 |
+
op = str(e.get('operator', ''))
|
| 87 |
+
if op.startswith(('justify:', 'resolve:')):
|
| 88 |
+
continue
|
| 89 |
+
surface = str(e.get('in_surface', ''))
|
| 90 |
+
conv = e.get('conversion') or {}
|
| 91 |
+
# the conversion slot leaks between turns; only trust it when it is
|
| 92 |
+
# demonstrably about THIS turn
|
| 93 |
+
own = str(conv.get('from', ''))[:60].strip() == surface[:60].strip()
|
| 94 |
+
if own and conv.get('construction') == 'VERIFIED_PROGRAM':
|
| 95 |
+
out.append(('program', str(conv.get('to', '')), surface))
|
| 96 |
+
elif own and conv.get('construction') == 'VERIFIED_COMPUTATION':
|
| 97 |
+
out.append(('result', str(conv.get('to', '')), surface))
|
| 98 |
+
terms = [t for t in (e.get('in_terms') or [])
|
| 99 |
+
if t not in _STOP_TERMS and len(t) > 2]
|
| 100 |
+
if terms:
|
| 101 |
+
out.append(('topic', terms[0], surface))
|
| 102 |
+
if len(out) >= 6:
|
| 103 |
+
break
|
| 104 |
+
return out
|
| 105 |
+
|
| 106 |
+
def _resolve(self, s, session_id):
|
| 107 |
+
"""(rewritten_surface, referent, trace) or raises with a named reason."""
|
| 108 |
+
cands = self._referents(session_id)
|
| 109 |
+
if not cands:
|
| 110 |
+
raise NotOurs('nothing recorded yet to refer back to')
|
| 111 |
+
|
| 112 |
+
wants_program = bool(_WANTS_PROGRAM.search(s))
|
| 113 |
+
wants_topic = bool(_WANTS_TOPIC.search(s)) or bool(_BARE_CONT.match(s))
|
| 114 |
+
if not (wants_program or wants_topic):
|
| 115 |
+
raise NotOurs('no reference this fold knows how to type')
|
| 116 |
+
|
| 117 |
+
readings = [Reading({'kind': k, 'label': lab, 'surface': surf},
|
| 118 |
+
'the %s from "%s"' % (k, surf[:38]))
|
| 119 |
+
for k, lab, surf in cands]
|
| 120 |
+
constraints = []
|
| 121 |
+
if wants_program and not wants_topic:
|
| 122 |
+
constraints.append(('you are asking about something runnable and that '
|
| 123 |
+
'is not a program', lambda a: a['kind'] == 'program'))
|
| 124 |
+
elif wants_topic and not wants_program:
|
| 125 |
+
constraints.append(('you are asking about a subject and that is not one',
|
| 126 |
+
lambda a: a['kind'] == 'topic'))
|
| 127 |
+
else:
|
| 128 |
+
constraints.append(('it is neither a program nor a subject',
|
| 129 |
+
lambda a: a['kind'] in ('program', 'topic')))
|
| 130 |
+
# only the most recent of a kind can be "it"; older ones are ruled out by
|
| 131 |
+
# recency, which is a real constraint rather than a tiebreak
|
| 132 |
+
first_of_kind = {}
|
| 133 |
+
for i, r in enumerate(readings):
|
| 134 |
+
first_of_kind.setdefault(r.args['kind'], i)
|
| 135 |
+
constraints.append(('an earlier one of the same kind is nearer',
|
| 136 |
+
lambda a, _f=first_of_kind, _r=readings:
|
| 137 |
+
_r[_f[a['kind']]].args is a))
|
| 138 |
+
|
| 139 |
+
survivor = eliminate(readings, constraints,
|
| 140 |
+
nothing_here='nothing here to refer to')
|
| 141 |
+
ref = survivor.args
|
| 142 |
+
if ref['kind'] == 'program':
|
| 143 |
+
lang = _LANG.search(s)
|
| 144 |
+
if lang:
|
| 145 |
+
new = re.sub(r'\b(go|python|javascript|bash|sql|c|rust|java)\b',
|
| 146 |
+
lang.group(1), ref['surface'], count=1, flags=re.I)
|
| 147 |
+
return new, ref, survivor
|
| 148 |
+
return None, ref, survivor # a question ABOUT the program
|
| 149 |
+
topic = ref['label']
|
| 150 |
+
if _BARE_CONT.match(s) or re.search(r'\btell me more\b', s, re.I):
|
| 151 |
+
return 'Tell me about %s.' % topic, ref, survivor
|
| 152 |
+
new = _PRONOUN.sub(topic, s, count=1)
|
| 153 |
+
new = _THE_THING.sub(topic, new)
|
| 154 |
+
return new, ref, survivor
|
| 155 |
+
|
| 156 |
+
def _about_program(self, s, ref, session_id):
|
| 157 |
+
"""Questions about a program are answered from the record, not re-run."""
|
| 158 |
+
hist = getattr(self, '_paths', None) or {}
|
| 159 |
+
q = hist.get(session_id) or []
|
| 160 |
+
warrant = None
|
| 161 |
+
for e in reversed(list(q)):
|
| 162 |
+
if str(e.get('in_surface', ''))[:60].strip() == ref['surface'][:60].strip():
|
| 163 |
+
warrant = e.get('verified_warrant') or {}
|
| 164 |
+
break
|
| 165 |
+
steps = (warrant or {}).get('steps') or []
|
| 166 |
+
if steps:
|
| 167 |
+
return ('Yes. I ran %s on this machine before answering, and the '
|
| 168 |
+
'recorded output was %s. I read "it" as that program, because '
|
| 169 |
+
'it is the only thing in this conversation you can run. '
|
| 170 |
+
'[self:witness]' % (ref['label'], '; '.join(steps[:3])))
|
| 171 |
+
return ('I read "it" as %s, the program from earlier in this conversation. '
|
| 172 |
+
'My record for that turn holds no execution, so I will not tell you '
|
| 173 |
+
'it ran. [self:witness]' % ref['label'])
|
| 174 |
+
|
| 175 |
+
def chat(self, text: str, session_id: str = 'default') -> dict:
|
| 176 |
+
s = ' '.join(str(text).strip().split())
|
| 177 |
+
if not (_PRONOUN.search(s) or _BARE_CONT.match(s) or _THE_THING.search(s)):
|
| 178 |
+
return super().chat(s, session_id)
|
| 179 |
+
# a surface that names a subject of its own is not this fold's business
|
| 180 |
+
if re.search(r'\b(?:volcano|moon|gravity|rain)\b', s, re.I) and \
|
| 181 |
+
not _BARE_CONT.match(s):
|
| 182 |
+
pass
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
rewritten, ref, survivor = self._resolve(s, session_id)
|
| 186 |
+
except Ambiguous as why:
|
| 187 |
+
return {'status': 'withheld', 'path': 'resolve:ambiguous',
|
| 188 |
+
'response': ('%s. Say which one and I will go on.' % why),
|
| 189 |
+
'provenance': {'kind': 'reference', 'resolved': False},
|
| 190 |
+
'latency_ms': 1}
|
| 191 |
+
except NotOurs:
|
| 192 |
+
return super().chat(s, session_id)
|
| 193 |
+
except OutOfFragment as why:
|
| 194 |
+
return super().chat(s, session_id) # not ours to refuse
|
| 195 |
+
|
| 196 |
+
ruled = [(b, k) for b, k in (getattr(survivor, 'eliminated', []) or [])][:2]
|
| 197 |
+
if rewritten is None:
|
| 198 |
+
body = self._about_program(s, ref, session_id)
|
| 199 |
+
result = {'status': 'answered', 'path': 'resolve:about_program',
|
| 200 |
+
'response': body,
|
| 201 |
+
'provenance': {'kind': 'reference', 'resolved': True,
|
| 202 |
+
'referent': ref['label'],
|
| 203 |
+
'referent_kind': ref['kind']},
|
| 204 |
+
'latency_ms': 2}
|
| 205 |
+
else:
|
| 206 |
+
result = super().chat(rewritten, session_id)
|
| 207 |
+
note = ('I read that as "%s", taking %s to mean %s.'
|
| 208 |
+
% (rewritten, '"it"' if _PRONOUN.search(s) else 'the follow-up',
|
| 209 |
+
ref['label']))
|
| 210 |
+
if ruled:
|
| 211 |
+
note += ' I ruled out %s, because %s.' % (ruled[0][0], ruled[0][1])
|
| 212 |
+
result = dict(result)
|
| 213 |
+
result['response'] = '%s %s' % (note, str(result.get('response', '')))
|
| 214 |
+
result['path'] = 'resolve:%s->%s' % (ref['kind'], result.get('path'))
|
| 215 |
+
result['provenance'] = dict(result.get('provenance') or {})
|
| 216 |
+
result['provenance'].update({'resolved_reference': ref['label'],
|
| 217 |
+
'rewritten_as': rewritten})
|
| 218 |
+
|
| 219 |
+
from collections import deque
|
| 220 |
+
hist = getattr(self, '_paths', None)
|
| 221 |
+
if hist is None:
|
| 222 |
+
hist = self._paths = {}
|
| 223 |
+
qq = hist.get(session_id)
|
| 224 |
+
if qq is None:
|
| 225 |
+
qq = hist[session_id] = deque(maxlen=64)
|
| 226 |
+
prev = qq[-1].get('turn', 0) if len(qq) else 0
|
| 227 |
+
qq.append({'turn': prev + 1, 'in_surface': s[:120],
|
| 228 |
+
'in_form': ['anaphoric_followup'],
|
| 229 |
+
'operator': 'resolve:' + ref['kind'],
|
| 230 |
+
'out_status': str(result.get('status', 'answered')),
|
| 231 |
+
'in_terms': [], 'out_terms': [],
|
| 232 |
+
'relation': {'grounded': [], 'grounded_cited': [],
|
| 233 |
+
'introduced': [], 'unaccounted': []},
|
| 234 |
+
'carried_from_prior': [ref['kind']],
|
| 235 |
+
'continuity_lexical': True, 'continuity': True, 'citations': 1,
|
| 236 |
+
'residual': None, 'cost_ms': 1.0,
|
| 237 |
+
'conversion': {'construction': 'RESOLVED_REFERENCE',
|
| 238 |
+
'from': s[:60], 'to': ref['label'][:60],
|
| 239 |
+
'produced_by': 'resolve_w71'},
|
| 240 |
+
'verified_warrant': {'kind': 'self_citation',
|
| 241 |
+
'steps': ['read "%s" as %s'
|
| 242 |
+
% (s[:40], ref['label'])]
|
| 243 |
+
+ ['ruled out %s because %s' % (b, k)
|
| 244 |
+
for b, k in ruled]}})
|
| 245 |
+
return result
|
root/wander-train/oracle_sibling_evidential_v3.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evidential plant V3 - verdicts read from the RECORD's structure, never from text.
|
| 3 |
+
|
| 4 |
+
M60b demonstrated V2's remaining defect on h035:3: "Is a teddy bear a bear?" was
|
| 5 |
+
dispatched to summarize (a Teddy bear page exists), the summary carries no polarity
|
| 6 |
+
shape, and the text-regex polarity check rendered "supported" beside content that
|
| 7 |
+
refutes the claim. The repair is this plant's own founding principle applied strictly:
|
| 8 |
+
the answer's evidential comes from what the record LICENSES, and the record's kind
|
| 9 |
+
verdict is TYPED -
|
| 10 |
+
|
| 11 |
+
SUPPORTED provenance.kind == "derived" with rule transitive_isa, or
|
| 12 |
+
kind == "commonsense_edge" with relation IsA, or
|
| 13 |
+
kind == "lexical_corpus" with relation hypernym / hypernym_of
|
| 14 |
+
REFUTED kind == "lexical_corpus" with relation hypernym_excluded
|
| 15 |
+
NO VERDICT any other route (gated_summary, document_runtime, ...) - the record
|
| 16 |
+
produced no kind verdict, so the plant says so: "The record does not
|
| 17 |
+
settle what you heard." A summary does not license a kind verdict.
|
| 18 |
+
|
| 19 |
+
Probe conversion (declarative->kind question) inherited from V2. No text regex remains
|
| 20 |
+
in the verdict path. No neural network; no LM call.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import re
|
| 26 |
+
|
| 27 |
+
from oracle_sibling_evidential_v2 import SiblingEvidentialV2, DECL_KIND, _art
|
| 28 |
+
from oracle_siblings_bed2_v1 import SiblingEvidential, HEARSAY, DIRECT, INFER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def kind_verdict(result: dict) -> str:
|
| 32 |
+
prov = result.get("provenance") or {}
|
| 33 |
+
kind = str(prov.get("kind", ""))
|
| 34 |
+
rel = str(prov.get("relation", ""))
|
| 35 |
+
rule = str(prov.get("rule", ""))
|
| 36 |
+
if kind == "derived" and rule == "transitive_isa":
|
| 37 |
+
return "supported"
|
| 38 |
+
if kind == "commonsense_edge" and rel == "IsA":
|
| 39 |
+
return "supported"
|
| 40 |
+
if kind == "lexical_corpus" and rel in ("hypernym", "hypernym_of"):
|
| 41 |
+
return "supported"
|
| 42 |
+
if kind == "lexical_corpus" and rel == "hypernym_excluded":
|
| 43 |
+
return "refuted"
|
| 44 |
+
return "no_verdict"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class SiblingEvidentialV3(SiblingEvidentialV2):
|
| 48 |
+
"""V2's probe conversion + structural verdicts."""
|
| 49 |
+
|
| 50 |
+
def chat(self, text: str, session_id: str = "default") -> dict:
|
| 51 |
+
s = " ".join(str(text).strip().split())
|
| 52 |
+
marked, proposition = None, s
|
| 53 |
+
for name, pat in (("hearsay", HEARSAY), ("direct", DIRECT), ("inferred", INFER)):
|
| 54 |
+
m = pat.match(s)
|
| 55 |
+
if m:
|
| 56 |
+
marked, proposition = name, m.group("p").strip()
|
| 57 |
+
break
|
| 58 |
+
if marked:
|
| 59 |
+
if re.match(r"^(is|are|what|who|when|how)\b", proposition, re.I):
|
| 60 |
+
probe = proposition
|
| 61 |
+
else:
|
| 62 |
+
dk = DECL_KIND.match(proposition)
|
| 63 |
+
if dk:
|
| 64 |
+
x, y = dk.group("x").strip(), dk.group("y").strip()
|
| 65 |
+
probe = f"Is {_art(x)} {x} {_art(y)} {y}?"
|
| 66 |
+
else:
|
| 67 |
+
probe = f"Summarize {proposition}."
|
| 68 |
+
self._pending_conversion[session_id] = {
|
| 69 |
+
"construction": f"EVIDENTIAL:{marked}", "from": s[:80], "to": probe[:80],
|
| 70 |
+
"produced_by": "sibling:evidential_v3"}
|
| 71 |
+
result = super(SiblingEvidential, self).chat(probe, session_id)
|
| 72 |
+
else:
|
| 73 |
+
result = super(SiblingEvidential, self).chat(s, session_id)
|
| 74 |
+
ev = self._evidential_of(result)
|
| 75 |
+
result["evidential"] = {"input_marked": marked, "answer_licensed": ev}
|
| 76 |
+
if result.get("status") == "answered":
|
| 77 |
+
tag = {"direct": "[ev:direct]", "inferred": "[ev:inferred]",
|
| 78 |
+
"reported": "[ev:reported]", "unknown": "[ev:unknown]"}[ev]
|
| 79 |
+
answer = str(result.get("response", ""))
|
| 80 |
+
if marked == "hearsay":
|
| 81 |
+
verdict = kind_verdict(result)
|
| 82 |
+
lead = {"supported": "What you heard is supported. ",
|
| 83 |
+
"refuted": "What you heard is not supported by the record. ",
|
| 84 |
+
"no_verdict": "The record does not settle what you heard. "}[verdict]
|
| 85 |
+
result["evidential"]["verdict"] = verdict
|
| 86 |
+
result["response"] = f"{tag} {lead}" + answer
|
| 87 |
+
else:
|
| 88 |
+
result["response"] = f"{tag} " + answer
|
| 89 |
+
hist = getattr(self, "_paths", {}).get(session_id)
|
| 90 |
+
if hist:
|
| 91 |
+
hist[-1]["evidential"] = result["evidential"]
|
| 92 |
+
return result
|
root/wander-train/oracle_tritree_v1.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""oracle_tritree_v1 - the THREE-TREE REASONING RUNTIME (drawing OR7-A-101 made computational).
|
| 2 |
+
|
| 3 |
+
Three graphs of the canonical Tree-of-Life shape (10 Sephirot, 22 paths) share one middle pillar:
|
| 4 |
+
QUESTION tree - the slots a question opens KNOWLEDGE tree - where facts live OPERATOR tree - the moves
|
| 5 |
+
keter request - VERIFY (goal test / next cycle)
|
| 6 |
+
chokmah subject entities (title index) LINK
|
| 7 |
+
binah relation RELATION EDGES (typed store, Wikidata) RELATE
|
| 8 |
+
chesed context sentences (cited prose) EXPAND
|
| 9 |
+
geburah constraints types (P31/P279 classes) CHECK
|
| 10 |
+
tiferet answer slot (binding) - BIND
|
| 11 |
+
netzach alternatives (A or B) neighbours COMPARE
|
| 12 |
+
hod quantities engines (dates, numbers) COMPUTE
|
| 13 |
+
yesod evidence citations FRAME
|
| 14 |
+
malkuth surface - READ
|
| 15 |
+
A MOVE goes from the current operator node to an ADJACENT node along one of the graph's edges and applies that node's
|
| 16 |
+
operator to the state. Only edges of the graph are moves: the topology constrains the reasoning. The search is a bounded
|
| 17 |
+
breadth-first walk from Malkuth; a state is SOLVED when VERIFY (Keter) finds the answer slot bound, checked and cited.
|
| 18 |
+
Distinct solved answers -> abstain. A two-hop question runs a second CYCLE from Keter with the bound entity as subject.
|
| 19 |
+
The graph is a parameter (PATHS), so the topology can be ablated with everything else fixed.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import copy
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import re
|
| 27 |
+
from collections import deque
|
| 28 |
+
|
| 29 |
+
import oracle_compare_op_v1 as CMP
|
| 30 |
+
import oracle_cube_reader_v1_4 as R
|
| 31 |
+
import oracle_relstore_v1 as RS
|
| 32 |
+
|
| 33 |
+
SEPH = ["keter", "chokmah", "binah", "chesed", "geburah", "tiferet", "netzach", "hod", "yesod", "malkuth"]
|
| 34 |
+
TREE_OF_LIFE = [("keter", "chokmah"), ("keter", "binah"), ("keter", "tiferet"), ("chokmah", "binah"), ("chokmah", "tiferet"),
|
| 35 |
+
("chokmah", "chesed"), ("binah", "tiferet"), ("binah", "geburah"), ("chesed", "geburah"),
|
| 36 |
+
("chesed", "tiferet"), ("chesed", "netzach"), ("geburah", "tiferet"), ("geburah", "hod"),
|
| 37 |
+
("tiferet", "netzach"), ("tiferet", "yesod"), ("tiferet", "hod"), ("netzach", "hod"), ("netzach", "yesod"),
|
| 38 |
+
("netzach", "malkuth"), ("hod", "yesod"), ("hod", "malkuth"), ("yesod", "malkuth")]
|
| 39 |
+
OP = {"malkuth": "READ", "yesod": "FRAME", "chokmah": "LINK", "binah": "RELATE", "chesed": "EXPAND", "geburah": "CHECK",
|
| 40 |
+
"tiferet": "BIND", "netzach": "COMPARE", "hod": "COMPUTE", "keter": "VERIFY"}
|
| 41 |
+
LEXICON = os.environ.get("ORACLE_LEXICON") or __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/lexicon_v1.json"
|
| 42 |
+
PLACE_CLASSES = {"country", "sovereign state", "city", "town", "village", "municipality", "state", "province", "region",
|
| 43 |
+
"county", "island", "continent", "capital", "big city", "human settlement", "administrative territorial entity",
|
| 44 |
+
"commune of france", "state of the united states", "city of the united states", "metropolis"}
|
| 45 |
+
KIND_EQ = [{"show", "series", "programme", "program", "television series", "sitcom"}, {"film", "movie", "feature film"},
|
| 46 |
+
{"band", "group", "musical group", "rock band"}, {"company", "corporation", "firm", "business", "enterprise"},
|
| 47 |
+
{"album", "studio album"}, {"song", "single"}, {"magazine", "periodical"}, {"university", "college"},
|
| 48 |
+
{"country", "sovereign state", "state"}]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# the properties the relation store holds (build_relation_store_v1.PROPS)
|
| 52 |
+
STORE_PROPS = {31, 279, 17, 19, 20, 569, 570, 27, 131, 276, 36, 37, 38, 47, 50, 57, 58, 86, 161, 162, 175, 176, 178, 123,
|
| 53 |
+
136, 127, 112, 159, 166, 26, 22, 25, 40, 3373, 69, 108, 106, 1303, 264, 463, 54, 118, 641, 101, 140, 84, 61,
|
| 54 |
+
170, 676, 571, 577, 1082, 2046, 2048, 495, 364, 155, 156, 361, 527, 1376, 138, 800, 39, 749, 355, 1412, 103,
|
| 55 |
+
710, 1344, 921, 30, 206, 1441, 674, 725, 449, 750, 272, 344, 1040, 1449, 488, 169, 35, 6, 150, 1830, 945, 113,
|
| 56 |
+
114, 915, 840, 437, 400, 1027, 2031, 2032}
|
| 57 |
+
RULES = set((os.environ.get("TT_RULES") or "").split(","))
|
| 58 |
+
TRIVIAL_VERBS = {"be", "have", "do", "call", "name", "know"}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class State:
|
| 62 |
+
__slots__ = ("node", "slots", "cands", "answer", "evidence", "trace", "cycle", "checked", "depth")
|
| 63 |
+
|
| 64 |
+
def __init__(self):
|
| 65 |
+
self.node, self.slots, self.cands, self.answer = "malkuth", {}, [], None
|
| 66 |
+
self.evidence, self.trace, self.cycle, self.checked, self.depth = [], [], 0, False, 0
|
| 67 |
+
|
| 68 |
+
def fork(self):
|
| 69 |
+
s = State()
|
| 70 |
+
s.node, s.slots, s.cands, s.answer = self.node, dict(self.slots), list(self.cands), self.answer
|
| 71 |
+
s.evidence, s.trace, s.cycle, s.checked, s.depth = list(self.evidence), list(self.trace), self.cycle, self.checked, self.depth
|
| 72 |
+
return s
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class TriTree:
|
| 76 |
+
def __init__(self, paths=None, reader=None, relstore=None, lexicon=None, max_states=4000, max_depth=12):
|
| 77 |
+
self.adj = {s: set() for s in SEPH}
|
| 78 |
+
for a, b in (paths or TREE_OF_LIFE):
|
| 79 |
+
self.adj[a].add(b)
|
| 80 |
+
self.adj[b].add(a)
|
| 81 |
+
self.r = reader or R.CubeReader()
|
| 82 |
+
self.rs = relstore or RS.RelStore()
|
| 83 |
+
self.cmp = CMP.CompareOp(self.r)
|
| 84 |
+
lx = json.load(open(lexicon or LEXICON))
|
| 85 |
+
self.lex = {tuple(json.loads(k)): {tuple(json.loads(p)): c for p, c in v.items()} for k, v in lx["lexicon"].items()}
|
| 86 |
+
self.cmp_lex = {ph: {tuple(json.loads(p)): c for p, c in v.items()} for ph, v in lx.get("compare", {}).items()}
|
| 87 |
+
self.max_states, self.max_depth = max_states, max_depth
|
| 88 |
+
self.expanded = 0
|
| 89 |
+
# property labels + aliases (Wikidata, English), for the RELATION check
|
| 90 |
+
self.plabel, self.palias = {}, {}
|
| 91 |
+
pl = os.path.join(os.path.dirname(LEXICON), "prop_labels.tsv")
|
| 92 |
+
if os.path.exists(pl):
|
| 93 |
+
for line in open(pl, encoding="utf-8", errors="replace"):
|
| 94 |
+
parts = line.rstrip("\n").split("\t")
|
| 95 |
+
if len(parts) == 2 and "/P" in parts[0]:
|
| 96 |
+
try:
|
| 97 |
+
p = int(parts[0].rsplit("/P", 1)[1].rstrip(">"))
|
| 98 |
+
except ValueError:
|
| 99 |
+
continue
|
| 100 |
+
if p not in STORE_PROPS:
|
| 101 |
+
continue # a property the store does not hold can never be followed
|
| 102 |
+
words = {R.stem(w) for w in re.findall(r"[a-z]+", parts[1].lower()) if len(w) >= 3 and w not in R.STOP}
|
| 103 |
+
self.plabel.setdefault(p, set()).update(words)
|
| 104 |
+
self.palias.setdefault(p, set()).add(RS.norm(parts[1].rsplit("@", 1)[0].strip('"')))
|
| 105 |
+
|
| 106 |
+
# ------------------------------------------------------------------ operators (one per Operator-tree node)
|
| 107 |
+
def READ(self, st): # malkuth: the surface opens the scramble
|
| 108 |
+
if "fr" in st.slots:
|
| 109 |
+
return []
|
| 110 |
+
q = st.slots["q"]
|
| 111 |
+
fr = self.r.frame(q)
|
| 112 |
+
pair = CMP._pair(q.strip().strip('"'))
|
| 113 |
+
if not fr and not (pair and pair[2] == "or"):
|
| 114 |
+
return []
|
| 115 |
+
s = st.fork()
|
| 116 |
+
s.slots["fr"] = fr
|
| 117 |
+
s.slots["pair"] = pair if pair and pair[2] == "or" else None
|
| 118 |
+
s.trace.append("READ")
|
| 119 |
+
return [s]
|
| 120 |
+
|
| 121 |
+
def FRAME(self, st): # yesod: obligations -> candidate RELATIONS (the lexicon)
|
| 122 |
+
if "fr" not in st.slots or "props" in st.slots:
|
| 123 |
+
return []
|
| 124 |
+
s = st.fork()
|
| 125 |
+
fr = s.slots["fr"]
|
| 126 |
+
props = {}
|
| 127 |
+
if fr and "rel" not in fr:
|
| 128 |
+
self.r.link(fr) # tokens -> the governing verb (pred) and relation words (rel)
|
| 129 |
+
if fr:
|
| 130 |
+
verb = (fr.get("pred") or [None])[0]
|
| 131 |
+
for key, w in ((("hv", fr["atype"], fr["head"], verb), 3), (("v", fr["atype"], verb), 2), (("h", fr["atype"], fr["head"]), 1)):
|
| 132 |
+
for (d, p), c in self.lex.get(key, {}).items():
|
| 133 |
+
props[(d, p)] = props.get((d, p), 0) + w * c
|
| 134 |
+
if s.slots.get("pair"):
|
| 135 |
+
m = re.search(r"\b(first|earlier|older|younger|later|more|larger|bigger|greater|higher|taller|longer|smaller|"
|
| 136 |
+
r"fewer|less|shorter)\b(?:\s+([a-z]+))?", s.slots["q"].lower())
|
| 137 |
+
phrase = " ".join(x for x in (m.groups() if m else ()) if x)
|
| 138 |
+
cands = self.cmp_lex.get(phrase) or self.cmp_lex.get(phrase.split(" ")[0] if phrase else "", {})
|
| 139 |
+
s.slots["cmp_props"] = sorted(cands.items(), key=lambda kv: -kv[1])[:2]
|
| 140 |
+
if fr:
|
| 141 |
+
# MULTI-HOP (the cycle): "the father of X", "the writer of X" - a relational noun that IS a property's name
|
| 142 |
+
m = re.search(r"\bthe\s+(?:[\w\-]+\s+){0,2}?([a-z]+)\s+of\s+(?:the\s+)?([A-Z][^?,]{1,60}?)(?:\s+(?:die|died|born|"
|
| 143 |
+
r"built|founded|released|published|located|made|start|end)\b|\?|,|$)", s.slots["q"])
|
| 144 |
+
noun = None
|
| 145 |
+
rest = ({R.stem(w) for w in fr.get("rel", [])} | {R.stem(v) for v in (fr.get("pred") or []) if v not in TRIVIAL_VERBS})
|
| 146 |
+
if m:
|
| 147 |
+
rest -= {R.stem(m.group(1)), R.stem(fr["head"] or "")}
|
| 148 |
+
if m and rest and "hop2" not in s.slots: # a relation REMAINS after the hop ("the father of X ... die")
|
| 149 |
+
hop1 = sorted(p for p, al in self.palias.items() if m.group(1) in al)
|
| 150 |
+
if hop1:
|
| 151 |
+
noun = m.group(1)
|
| 152 |
+
s.slots["hop1"] = {"props": hop1[:2], "anchor": m.group(2).strip(), "noun": noun}
|
| 153 |
+
# the RELATION NODE's own vocabulary: properties whose label/aliases carry the question's RELATION words (not
|
| 154 |
+
# its head noun, not the hop-1 noun) - "share a land border" -> P47 "shares border with"
|
| 155 |
+
relw = {R.stem(w) for w in fr.get("rel", []) if len(w) >= 3} | {R.stem(v) for v in (fr.get("pred") or []) if v}
|
| 156 |
+
relw -= {R.stem(fr["head"] or ""), R.stem(noun or "")}
|
| 157 |
+
for p, words in self.plabel.items():
|
| 158 |
+
ov = len(words & relw)
|
| 159 |
+
if ov:
|
| 160 |
+
for d in ("fwd", "inv"):
|
| 161 |
+
props[(d, p)] = props.get((d, p), 0) + 5 * ov
|
| 162 |
+
# a RECALLED FORM (forms_store_v1) proposes the property path a verified derivation of the same shape used;
|
| 163 |
+
# absent a form, FRAME is exactly as frozen (ad1530e6)
|
| 164 |
+
for dp, w in (getattr(self, "_form_props", None) or {}).items():
|
| 165 |
+
props[dp] = props.get(dp, 0) + 1000 * w
|
| 166 |
+
s.slots["props"] = sorted(props.items(), key=lambda kv: -kv[1])[:4]
|
| 167 |
+
s.trace.append("FRAME")
|
| 168 |
+
return [s]
|
| 169 |
+
|
| 170 |
+
def LINK(self, st): # chokmah: subjects -> entities of the relation store
|
| 171 |
+
if "props" not in st.slots or "subj" in st.slots:
|
| 172 |
+
return []
|
| 173 |
+
s = st.fork()
|
| 174 |
+
subj = []
|
| 175 |
+
if s.slots.get("pair"):
|
| 176 |
+
for side in s.slots["pair"][:2]:
|
| 177 |
+
t, _ = self.cmp._entity(side)
|
| 178 |
+
q = self.rs.entity(t) if t else None
|
| 179 |
+
if q:
|
| 180 |
+
subj.append((t, q))
|
| 181 |
+
if len(subj) != 2:
|
| 182 |
+
return []
|
| 183 |
+
else:
|
| 184 |
+
fr = s.slots["fr"]
|
| 185 |
+
if "rel" not in fr:
|
| 186 |
+
self.r.link(fr)
|
| 187 |
+
for sj in fr["subjects"][:3]:
|
| 188 |
+
t = self.r.db.execute("SELECT title FROM article WHERE page_id=?", (sj["pid"],)).fetchone()
|
| 189 |
+
q = self.rs.entity(t[0]) if t else None
|
| 190 |
+
if q:
|
| 191 |
+
subj.append((t[0], q))
|
| 192 |
+
if not subj:
|
| 193 |
+
return []
|
| 194 |
+
s.slots["subj"] = subj
|
| 195 |
+
s.trace.append("LINK")
|
| 196 |
+
return [s]
|
| 197 |
+
|
| 198 |
+
def RELATE(self, st): # binah: typed relation edges, forward or inverse
|
| 199 |
+
if "subj" not in st.slots or st.cands or st.slots.get("pair"):
|
| 200 |
+
return []
|
| 201 |
+
out = []
|
| 202 |
+
hop1 = st.slots.get("hop1")
|
| 203 |
+
if hop1 and st.cycle == 0:
|
| 204 |
+
# cycle 1 of a two-hop question: anchor --hop1 property--> the described entity (exactly one)
|
| 205 |
+
aq = self.rs.entity(hop1["anchor"]) or next((q for t, q in st.slots["subj"] if RS.norm(t) == RS.norm(hop1["anchor"])), None)
|
| 206 |
+
if not aq:
|
| 207 |
+
return []
|
| 208 |
+
for p in hop1["props"]:
|
| 209 |
+
vals = [o for o, _ in self.rs.values(aq, p) if o]
|
| 210 |
+
if len(set(vals)) == 1:
|
| 211 |
+
s = st.fork()
|
| 212 |
+
s.slots["bridge"] = (vals[0], "wd:Q%d P%d wd:Q%d" % (aq, p, vals[0]), hop1["noun"])
|
| 213 |
+
s.trace.append("RELATE:hop1:P%d" % p)
|
| 214 |
+
out.append(s)
|
| 215 |
+
return out
|
| 216 |
+
for (d, p), w in st.slots["props"]:
|
| 217 |
+
for title, sq in st.slots["subj"][:2]:
|
| 218 |
+
if d == "fwd":
|
| 219 |
+
vals = [(o, lit, "wd:Q%d P%d %s" % (sq, p, ("wd:Q%d" % o) if o else json.dumps(lit))) for o, lit in self.rs.values(sq, p)]
|
| 220 |
+
else:
|
| 221 |
+
vals = [(s_, None, "wd:Q%d P%d wd:Q%d" % (s_, p, sq)) for s_ in self.rs.subjects(p, sq, 60)]
|
| 222 |
+
if not vals:
|
| 223 |
+
continue
|
| 224 |
+
s = st.fork()
|
| 225 |
+
s.cands = [{"o": o, "lit": lit, "cite": c, "p": p, "via": title} for o, lit, c in vals[:60]]
|
| 226 |
+
s.trace.append("RELATE:%s:P%d" % (d, p))
|
| 227 |
+
out.append(s)
|
| 228 |
+
return out
|
| 229 |
+
|
| 230 |
+
def CHECK(self, st): # geburah: types + constraints (independent of the proposer)
|
| 231 |
+
if not st.cands or st.checked:
|
| 232 |
+
return []
|
| 233 |
+
s = st.fork()
|
| 234 |
+
fr = s.slots["fr"]
|
| 235 |
+
keep = []
|
| 236 |
+
for c in s.cands:
|
| 237 |
+
ok = False
|
| 238 |
+
if fr["atype"] in ("YEAR", "DATE"):
|
| 239 |
+
ok = bool(c["lit"]) and re.match(r"^[+-]?\d{4}-", c["lit"] or "") is not None
|
| 240 |
+
elif fr["atype"] == "NUMBER":
|
| 241 |
+
ok = bool(c["lit"]) and re.match(r"^[+-]?\d", c["lit"] or "") is not None
|
| 242 |
+
elif c["o"]:
|
| 243 |
+
if fr["atype"] == "PERSON":
|
| 244 |
+
ok = self.rs.is_human(c["o"])
|
| 245 |
+
elif fr["atype"] == "PLACE":
|
| 246 |
+
ok = bool(self.rs.classes(c["o"], 2) & PLACE_CLASSES) if not fr["head"] else self._kind(c["o"], fr["head"])
|
| 247 |
+
elif fr["atype"] == "INSTANCE":
|
| 248 |
+
ok = self._kind(c["o"], fr["head"])
|
| 249 |
+
if ok:
|
| 250 |
+
keep.append(c)
|
| 251 |
+
# constraints: every year the question names must be consistent with the candidate where the store can say
|
| 252 |
+
if keep and fr.get("years"):
|
| 253 |
+
dated = [c for c in keep if c["lit"] and c["lit"][:5].lstrip("+")[:4] in fr["years"]]
|
| 254 |
+
keep = dated or keep
|
| 255 |
+
if not keep:
|
| 256 |
+
return []
|
| 257 |
+
# RELATION LICENCE (independent of the lexicon that proposed the property): the property's own Wikidata label or
|
| 258 |
+
# aliases share a word with the question's relation words / head / verb, or the lexicon supports this exact
|
| 259 |
+
# question shape at least 3 times. "share a land border" licenses P47 "shares border with", not P131 "located in".
|
| 260 |
+
p = keep[0]["p"]
|
| 261 |
+
qwords = {R.stem(w) for w in fr.get("rel", []) + [fr.get("head") or ""] + list(fr.get("pred") or []) if w}
|
| 262 |
+
qwords |= {R.stem(w) for w in re.findall(r"[a-z]+", s.slots["q"].lower()) if len(w) >= 4 and w not in R.STOP}
|
| 263 |
+
support = self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(
|
| 264 |
+
("fwd", p), 0) + self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(("inv", p), 0)
|
| 265 |
+
form_licensed = any(dp[1] == p for dp in (getattr(self, "_form_props", None) or {}))
|
| 266 |
+
if not (self.plabel.get(p, set()) & qwords) and support < 3 and not form_licensed:
|
| 267 |
+
return []
|
| 268 |
+
# RULE B (verb licence for time properties): a date answers the question's own EVENT only when the property names
|
| 269 |
+
# the governing verb ("founded" -> P571 "founded"); a founding date of whatever entity was linked is not the event
|
| 270 |
+
if "B" in RULES and fr["atype"] in ("YEAR", "DATE") and fr.get("pred") and fr["pred"][0] not in TRIVIAL_VERBS:
|
| 271 |
+
if not ({R.stem(v) for v in fr["pred"]} & self.plabel.get(p, set())):
|
| 272 |
+
return []
|
| 273 |
+
# the answer is never the subject itself, nor a name the question already contains (whole or as its core word)
|
| 274 |
+
qn = RS.norm(s.slots["q"])
|
| 275 |
+
subj_q = {q for _, q in s.slots.get("subj", [])}
|
| 276 |
+
subj_w = {w for t, _ in s.slots.get("subj", []) for w in RS.norm(t).split() if len(w) >= 4}
|
| 277 |
+
|
| 278 |
+
def restates(c):
|
| 279 |
+
if not c["o"]:
|
| 280 |
+
return False
|
| 281 |
+
lab = RS.norm(self.rs.label(c["o"]) or "")
|
| 282 |
+
return c["o"] in subj_q or (lab and lab in qn) or (lab and set(lab.split()) & subj_w and s.cycle == 0)
|
| 283 |
+
keep = [c for c in keep if not restates(c)]
|
| 284 |
+
if not keep:
|
| 285 |
+
return []
|
| 286 |
+
s.cands, s.checked = keep, True
|
| 287 |
+
s.trace.append("CHECK:P%d" % p)
|
| 288 |
+
return [s]
|
| 289 |
+
|
| 290 |
+
def _kind(self, qid, head):
|
| 291 |
+
if not head:
|
| 292 |
+
return False
|
| 293 |
+
heads = {head} | next((g for g in KIND_EQ if head in g), set())
|
| 294 |
+
cls = self.rs.classes(qid, 2)
|
| 295 |
+
return any(h == c or c.endswith(" " + h) for h in heads for c in cls)
|
| 296 |
+
|
| 297 |
+
def BIND(self, st): # tiferet: exactly one checked value binds the answer slot
|
| 298 |
+
if not st.checked or st.answer:
|
| 299 |
+
return []
|
| 300 |
+
distinct = {}
|
| 301 |
+
for c in st.cands:
|
| 302 |
+
k = c["o"] or (c["lit"] or "")[:10]
|
| 303 |
+
distinct.setdefault(k, c)
|
| 304 |
+
if len(distinct) != 1:
|
| 305 |
+
return []
|
| 306 |
+
c = next(iter(distinct.values()))
|
| 307 |
+
s = st.fork()
|
| 308 |
+
ans = self.rs.label(c["o"]) if c["o"] else self._fmt(c["lit"], s.slots["fr"])
|
| 309 |
+
if not ans:
|
| 310 |
+
return []
|
| 311 |
+
s.answer = ans
|
| 312 |
+
s.evidence.append(c["cite"])
|
| 313 |
+
s.trace.append("BIND")
|
| 314 |
+
return [s]
|
| 315 |
+
|
| 316 |
+
def _fmt(self, lit, fr):
|
| 317 |
+
m = re.match(r"^[+-]?(\d{4})-(\d{2})-(\d{2})", lit or "")
|
| 318 |
+
if m:
|
| 319 |
+
y, mo, d = m.groups()
|
| 320 |
+
months = "January February March April May June July August September October November December".split()
|
| 321 |
+
if fr and fr["atype"] == "DATE" and mo != "00" and d != "00":
|
| 322 |
+
return "%d %s %s" % (int(d), months[int(mo) - 1], y)
|
| 323 |
+
return y
|
| 324 |
+
return (lit or "").lstrip("+")
|
| 325 |
+
|
| 326 |
+
def COMPARE(self, st): # netzach: A or B, by a typed date / number property
|
| 327 |
+
pair = st.slots.get("pair")
|
| 328 |
+
if not pair or "subj" not in st.slots or st.answer or not st.slots.get("cmp_props"):
|
| 329 |
+
return []
|
| 330 |
+
(ta, qa), (tb, qb) = st.slots["subj"][:2]
|
| 331 |
+
out = []
|
| 332 |
+
for (way, p), w in st.slots["cmp_props"]:
|
| 333 |
+
va, vb = self.rs.values(qa, p), self.rs.values(qb, p)
|
| 334 |
+
if not (va and vb and va[0][1] and vb[0][1]):
|
| 335 |
+
continue
|
| 336 |
+
a, b = va[0][1].lstrip("+"), vb[0][1].lstrip("+")
|
| 337 |
+
try:
|
| 338 |
+
fa, fb = (float(a), float(b)) if not re.match(r"^\d{4}-", a) else (a[:10], b[:10])
|
| 339 |
+
except ValueError:
|
| 340 |
+
continue
|
| 341 |
+
if fa == fb:
|
| 342 |
+
continue
|
| 343 |
+
pick_a = (fa < fb) == (way == "min")
|
| 344 |
+
s = st.fork()
|
| 345 |
+
s.answer = pair[0] if pick_a else pair[1]
|
| 346 |
+
s.evidence += ["wd:Q%d P%d %s" % (qa, p, json.dumps(a)), "wd:Q%d P%d %s" % (qb, p, json.dumps(b))]
|
| 347 |
+
s.checked = True
|
| 348 |
+
s.trace.append("COMPARE:P%d:%s" % (p, way))
|
| 349 |
+
out.append(s)
|
| 350 |
+
return out
|
| 351 |
+
|
| 352 |
+
def COMPUTE(self, st): # hod: engines (here: a date answer reduced to its year)
|
| 353 |
+
if not st.answer or st.slots.get("computed"):
|
| 354 |
+
return []
|
| 355 |
+
fr = st.slots.get("fr")
|
| 356 |
+
if fr and fr["atype"] == "YEAR" and re.match(r"^\d{1,2} \w+ \d{4}$", st.answer):
|
| 357 |
+
s = st.fork()
|
| 358 |
+
s.answer = st.answer[-4:]
|
| 359 |
+
s.slots["computed"] = True
|
| 360 |
+
s.trace.append("COMPUTE:year")
|
| 361 |
+
return [s]
|
| 362 |
+
return []
|
| 363 |
+
|
| 364 |
+
def EXPAND(self, st): # chesed: a cited sentence for the bound answer (evidence)
|
| 365 |
+
if not st.answer or st.slots.get("sentence"):
|
| 366 |
+
return []
|
| 367 |
+
s = st.fork()
|
| 368 |
+
for title, _ in s.slots.get("subj", [])[:2]:
|
| 369 |
+
pid = self.r.pid_of(title)
|
| 370 |
+
for cid, sent in (self.r.page(pid, 30) if pid else []):
|
| 371 |
+
if RS.norm(s.answer) and RS.norm(s.answer) in RS.norm(sent):
|
| 372 |
+
s.slots["sentence"] = (cid, sent)
|
| 373 |
+
s.evidence.append(cid)
|
| 374 |
+
s.trace.append("EXPAND")
|
| 375 |
+
return [s]
|
| 376 |
+
return []
|
| 377 |
+
|
| 378 |
+
def VERIFY(self, st): # keter: solved? or open the next CYCLE
|
| 379 |
+
if st.slots.get("bridge") and st.cycle == 0:
|
| 380 |
+
# the CYCLE: the entity cycle 1 bound becomes the subject of cycle 2; the question's own relation applies to it
|
| 381 |
+
e, cite, noun = st.slots["bridge"]
|
| 382 |
+
s = st.fork()
|
| 383 |
+
s.cycle = 1
|
| 384 |
+
s.slots["subj"] = [(self.rs.title(e) or self.rs.label(e) or "", e)]
|
| 385 |
+
s.slots.pop("bridge", None)
|
| 386 |
+
s.slots["hop2"] = True
|
| 387 |
+
s.cands, s.answer, s.checked = [], None, False
|
| 388 |
+
s.evidence.append(cite)
|
| 389 |
+
s.trace.append("VERIFY:cycle2(%s=%s)" % (noun, self.rs.label(e)))
|
| 390 |
+
return [s]
|
| 391 |
+
if "A" in RULES and st.answer and st.checked and "sentence" not in st.slots and not st.slots.get("pair"):
|
| 392 |
+
return [] # RULE A: a relation answer needs a corroborating sentence
|
| 393 |
+
if st.answer and st.checked and st.evidence:
|
| 394 |
+
s = st.fork()
|
| 395 |
+
s.slots["solved"] = True
|
| 396 |
+
s.trace.append("VERIFY")
|
| 397 |
+
return [s]
|
| 398 |
+
return []
|
| 399 |
+
|
| 400 |
+
# ------------------------------------------------------------------ the search (moves = graph edges only)
|
| 401 |
+
def answer(self, q, form_props=None):
|
| 402 |
+
self._form_props = form_props or None
|
| 403 |
+
try:
|
| 404 |
+
return self._answer(q)
|
| 405 |
+
finally:
|
| 406 |
+
self._form_props = None
|
| 407 |
+
|
| 408 |
+
def _answer(self, q):
|
| 409 |
+
q = " ".join(str(q).split())
|
| 410 |
+
if q.startswith('"') and q.endswith('"'):
|
| 411 |
+
q = q[1:-1].replace('""', '"')
|
| 412 |
+
start = State()
|
| 413 |
+
start.slots["q"] = q
|
| 414 |
+
frontier = deque(self.READ(start))
|
| 415 |
+
solved, seen = [], set()
|
| 416 |
+
self.expanded = 0
|
| 417 |
+
while frontier and self.expanded < self.max_states:
|
| 418 |
+
st = frontier.popleft()
|
| 419 |
+
self.expanded += 1
|
| 420 |
+
if st.slots.get("solved"):
|
| 421 |
+
solved.append(st)
|
| 422 |
+
continue
|
| 423 |
+
if st.depth >= self.max_depth:
|
| 424 |
+
continue
|
| 425 |
+
for nxt in self.adj[st.node]:
|
| 426 |
+
succ = getattr(self, OP[nxt])(st)
|
| 427 |
+
if not succ:
|
| 428 |
+
# TRANSIT: a move travels an EDGE; passing through a node whose operator does not apply costs a
|
| 429 |
+
# step and changes nothing. Path lengths therefore decide which derivations fit the depth bound.
|
| 430 |
+
succ = [st.fork()]
|
| 431 |
+
for s2 in succ:
|
| 432 |
+
s2.node, s2.depth = nxt, st.depth + 1
|
| 433 |
+
key = (nxt, s2.answer, len(s2.cands), s2.checked, tuple(s2.trace))
|
| 434 |
+
if key in seen:
|
| 435 |
+
continue
|
| 436 |
+
seen.add(key)
|
| 437 |
+
frontier.append(s2)
|
| 438 |
+
answers = {RS.norm(s.answer): s for s in solved}
|
| 439 |
+
if len(answers) != 1:
|
| 440 |
+
self.why = "no solved state" if not answers else "distinct solved answers: %s" % list(answers)[:3]
|
| 441 |
+
return None
|
| 442 |
+
s = next(iter(answers.values()))
|
| 443 |
+
return {"answer": s.answer, "evidence": s.evidence, "trace": s.trace, "expanded": self.expanded,
|
| 444 |
+
"sentence": (s.slots.get("sentence") or (None, ""))[1]}
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def random_graph(seed, n_edges=22):
|
| 448 |
+
"""a degree-sequence-matched random graph on the same ten nodes (for the topology ablation)"""
|
| 449 |
+
import random
|
| 450 |
+
rng = random.Random(seed)
|
| 451 |
+
deg = {s: 0 for s in SEPH}
|
| 452 |
+
for a, b in TREE_OF_LIFE:
|
| 453 |
+
deg[a] += 1
|
| 454 |
+
deg[b] += 1
|
| 455 |
+
for _ in range(2000):
|
| 456 |
+
stubs = [s for s in SEPH for _ in range(deg[s])]
|
| 457 |
+
rng.shuffle(stubs)
|
| 458 |
+
edges = set()
|
| 459 |
+
ok = True
|
| 460 |
+
for i in range(0, len(stubs), 2):
|
| 461 |
+
a, b = stubs[i], stubs[i + 1]
|
| 462 |
+
if a == b or (a, b) in edges or (b, a) in edges:
|
| 463 |
+
ok = False
|
| 464 |
+
break
|
| 465 |
+
edges.add((a, b))
|
| 466 |
+
if ok and len(edges) == n_edges:
|
| 467 |
+
return sorted(edges)
|
| 468 |
+
return TREE_OF_LIFE
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
COMPLETE = [(a, b) for i, a in enumerate(SEPH) for b in SEPH[i + 1:]]
|
| 472 |
+
CHAIN = [("malkuth", "yesod"), ("yesod", "chokmah"), ("chokmah", "binah"), ("binah", "geburah"), ("geburah", "tiferet"),
|
| 473 |
+
("tiferet", "hod"), ("hod", "chesed"), ("chesed", "netzach"), ("netzach", "keter")]
|
root/wander-train/oracle_tritree_v2.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""oracle_tritree_v2 - v1 (immutable, imported by w480) + answer(q, form_props, only=True): search only the named
|
| 2 |
+
relation(s).
|
| 3 |
+
oracle_tritree_v1 - the THREE-TREE REASONING RUNTIME (drawing OR7-A-101 made computational).
|
| 4 |
+
|
| 5 |
+
Three graphs of the canonical Tree-of-Life shape (10 Sephirot, 22 paths) share one middle pillar:
|
| 6 |
+
QUESTION tree - the slots a question opens KNOWLEDGE tree - where facts live OPERATOR tree - the moves
|
| 7 |
+
keter request - VERIFY (goal test / next cycle)
|
| 8 |
+
chokmah subject entities (title index) LINK
|
| 9 |
+
binah relation RELATION EDGES (typed store, Wikidata) RELATE
|
| 10 |
+
chesed context sentences (cited prose) EXPAND
|
| 11 |
+
geburah constraints types (P31/P279 classes) CHECK
|
| 12 |
+
tiferet answer slot (binding) - BIND
|
| 13 |
+
netzach alternatives (A or B) neighbours COMPARE
|
| 14 |
+
hod quantities engines (dates, numbers) COMPUTE
|
| 15 |
+
yesod evidence citations FRAME
|
| 16 |
+
malkuth surface - READ
|
| 17 |
+
A MOVE goes from the current operator node to an ADJACENT node along one of the graph's edges and applies that node's
|
| 18 |
+
operator to the state. Only edges of the graph are moves: the topology constrains the reasoning. The search is a bounded
|
| 19 |
+
breadth-first walk from Malkuth; a state is SOLVED when VERIFY (Keter) finds the answer slot bound, checked and cited.
|
| 20 |
+
Distinct solved answers -> abstain. A two-hop question runs a second CYCLE from Keter with the bound entity as subject.
|
| 21 |
+
The graph is a parameter (PATHS), so the topology can be ablated with everything else fixed.
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import copy
|
| 26 |
+
import json
|
| 27 |
+
import os
|
| 28 |
+
import re
|
| 29 |
+
from collections import deque
|
| 30 |
+
|
| 31 |
+
import oracle_compare_op_v1 as CMP
|
| 32 |
+
import oracle_cube_reader_v1_4 as R
|
| 33 |
+
import oracle_relstore_v1 as RS
|
| 34 |
+
|
| 35 |
+
SEPH = ["keter", "chokmah", "binah", "chesed", "geburah", "tiferet", "netzach", "hod", "yesod", "malkuth"]
|
| 36 |
+
TREE_OF_LIFE = [("keter", "chokmah"), ("keter", "binah"), ("keter", "tiferet"), ("chokmah", "binah"), ("chokmah", "tiferet"),
|
| 37 |
+
("chokmah", "chesed"), ("binah", "tiferet"), ("binah", "geburah"), ("chesed", "geburah"),
|
| 38 |
+
("chesed", "tiferet"), ("chesed", "netzach"), ("geburah", "tiferet"), ("geburah", "hod"),
|
| 39 |
+
("tiferet", "netzach"), ("tiferet", "yesod"), ("tiferet", "hod"), ("netzach", "hod"), ("netzach", "yesod"),
|
| 40 |
+
("netzach", "malkuth"), ("hod", "yesod"), ("hod", "malkuth"), ("yesod", "malkuth")]
|
| 41 |
+
OP = {"malkuth": "READ", "yesod": "FRAME", "chokmah": "LINK", "binah": "RELATE", "chesed": "EXPAND", "geburah": "CHECK",
|
| 42 |
+
"tiferet": "BIND", "netzach": "COMPARE", "hod": "COMPUTE", "keter": "VERIFY"}
|
| 43 |
+
LEXICON = os.environ.get("ORACLE_LEXICON") or __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/relstore/lexicon_v1.json"
|
| 44 |
+
PLACE_CLASSES = {"country", "sovereign state", "city", "town", "village", "municipality", "state", "province", "region",
|
| 45 |
+
"county", "island", "continent", "capital", "big city", "human settlement", "administrative territorial entity",
|
| 46 |
+
"commune of france", "state of the united states", "city of the united states", "metropolis"}
|
| 47 |
+
KIND_EQ = [{"show", "series", "programme", "program", "television series", "sitcom"}, {"film", "movie", "feature film"},
|
| 48 |
+
{"band", "group", "musical group", "rock band"}, {"company", "corporation", "firm", "business", "enterprise"},
|
| 49 |
+
{"album", "studio album"}, {"song", "single"}, {"magazine", "periodical"}, {"university", "college"},
|
| 50 |
+
{"country", "sovereign state", "state"}]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# the properties the relation store holds (build_relation_store_v1.PROPS)
|
| 54 |
+
STORE_PROPS = {31, 279, 17, 19, 20, 569, 570, 27, 131, 276, 36, 37, 38, 47, 50, 57, 58, 86, 161, 162, 175, 176, 178, 123,
|
| 55 |
+
136, 127, 112, 159, 166, 26, 22, 25, 40, 3373, 69, 108, 106, 1303, 264, 463, 54, 118, 641, 101, 140, 84, 61,
|
| 56 |
+
170, 676, 571, 577, 1082, 2046, 2048, 495, 364, 155, 156, 361, 527, 1376, 138, 800, 39, 749, 355, 1412, 103,
|
| 57 |
+
710, 1344, 921, 30, 206, 1441, 674, 725, 449, 750, 272, 344, 1040, 1449, 488, 169, 35, 6, 150, 1830, 945, 113,
|
| 58 |
+
114, 915, 840, 437, 400, 1027, 2031, 2032}
|
| 59 |
+
RULES = set((os.environ.get("TT_RULES") or "").split(","))
|
| 60 |
+
TRIVIAL_VERBS = {"be", "have", "do", "call", "name", "know"}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class State:
|
| 64 |
+
__slots__ = ("node", "slots", "cands", "answer", "evidence", "trace", "cycle", "checked", "depth")
|
| 65 |
+
|
| 66 |
+
def __init__(self):
|
| 67 |
+
self.node, self.slots, self.cands, self.answer = "malkuth", {}, [], None
|
| 68 |
+
self.evidence, self.trace, self.cycle, self.checked, self.depth = [], [], 0, False, 0
|
| 69 |
+
|
| 70 |
+
def fork(self):
|
| 71 |
+
s = State()
|
| 72 |
+
s.node, s.slots, s.cands, s.answer = self.node, dict(self.slots), list(self.cands), self.answer
|
| 73 |
+
s.evidence, s.trace, s.cycle, s.checked, s.depth = list(self.evidence), list(self.trace), self.cycle, self.checked, self.depth
|
| 74 |
+
return s
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class TriTree:
|
| 78 |
+
def __init__(self, paths=None, reader=None, relstore=None, lexicon=None, max_states=4000, max_depth=12):
|
| 79 |
+
self.adj = {s: set() for s in SEPH}
|
| 80 |
+
for a, b in (paths or TREE_OF_LIFE):
|
| 81 |
+
self.adj[a].add(b)
|
| 82 |
+
self.adj[b].add(a)
|
| 83 |
+
self.r = reader or R.CubeReader()
|
| 84 |
+
self.rs = relstore or RS.RelStore()
|
| 85 |
+
self.cmp = CMP.CompareOp(self.r)
|
| 86 |
+
lx = json.load(open(lexicon or LEXICON))
|
| 87 |
+
self.lex = {tuple(json.loads(k)): {tuple(json.loads(p)): c for p, c in v.items()} for k, v in lx["lexicon"].items()}
|
| 88 |
+
self.cmp_lex = {ph: {tuple(json.loads(p)): c for p, c in v.items()} for ph, v in lx.get("compare", {}).items()}
|
| 89 |
+
self.max_states, self.max_depth = max_states, max_depth
|
| 90 |
+
self.expanded = 0
|
| 91 |
+
# property labels + aliases (Wikidata, English), for the RELATION check
|
| 92 |
+
self.plabel, self.palias = {}, {}
|
| 93 |
+
pl = os.path.join(os.path.dirname(LEXICON), "prop_labels.tsv")
|
| 94 |
+
if os.path.exists(pl):
|
| 95 |
+
for line in open(pl, encoding="utf-8", errors="replace"):
|
| 96 |
+
parts = line.rstrip("\n").split("\t")
|
| 97 |
+
if len(parts) == 2 and "/P" in parts[0]:
|
| 98 |
+
try:
|
| 99 |
+
p = int(parts[0].rsplit("/P", 1)[1].rstrip(">"))
|
| 100 |
+
except ValueError:
|
| 101 |
+
continue
|
| 102 |
+
if p not in STORE_PROPS:
|
| 103 |
+
continue # a property the store does not hold can never be followed
|
| 104 |
+
words = {R.stem(w) for w in re.findall(r"[a-z]+", parts[1].lower()) if len(w) >= 3 and w not in R.STOP}
|
| 105 |
+
self.plabel.setdefault(p, set()).update(words)
|
| 106 |
+
self.palias.setdefault(p, set()).add(RS.norm(parts[1].rsplit("@", 1)[0].strip('"')))
|
| 107 |
+
|
| 108 |
+
# ------------------------------------------------------------------ operators (one per Operator-tree node)
|
| 109 |
+
def READ(self, st): # malkuth: the surface opens the scramble
|
| 110 |
+
if "fr" in st.slots:
|
| 111 |
+
return []
|
| 112 |
+
q = st.slots["q"]
|
| 113 |
+
fr = self.r.frame(q)
|
| 114 |
+
pair = CMP._pair(q.strip().strip('"'))
|
| 115 |
+
if not fr and not (pair and pair[2] == "or"):
|
| 116 |
+
return []
|
| 117 |
+
s = st.fork()
|
| 118 |
+
s.slots["fr"] = fr
|
| 119 |
+
s.slots["pair"] = pair if pair and pair[2] == "or" else None
|
| 120 |
+
s.trace.append("READ")
|
| 121 |
+
return [s]
|
| 122 |
+
|
| 123 |
+
def FRAME(self, st): # yesod: obligations -> candidate RELATIONS (the lexicon)
|
| 124 |
+
if "fr" not in st.slots or "props" in st.slots:
|
| 125 |
+
return []
|
| 126 |
+
s = st.fork()
|
| 127 |
+
fr = s.slots["fr"]
|
| 128 |
+
props = {}
|
| 129 |
+
if fr and "rel" not in fr:
|
| 130 |
+
self.r.link(fr) # tokens -> the governing verb (pred) and relation words (rel)
|
| 131 |
+
if fr:
|
| 132 |
+
verb = (fr.get("pred") or [None])[0]
|
| 133 |
+
for key, w in ((("hv", fr["atype"], fr["head"], verb), 3), (("v", fr["atype"], verb), 2), (("h", fr["atype"], fr["head"]), 1)):
|
| 134 |
+
for (d, p), c in self.lex.get(key, {}).items():
|
| 135 |
+
props[(d, p)] = props.get((d, p), 0) + w * c
|
| 136 |
+
if s.slots.get("pair"):
|
| 137 |
+
m = re.search(r"\b(first|earlier|older|younger|later|more|larger|bigger|greater|higher|taller|longer|smaller|"
|
| 138 |
+
r"fewer|less|shorter)\b(?:\s+([a-z]+))?", s.slots["q"].lower())
|
| 139 |
+
phrase = " ".join(x for x in (m.groups() if m else ()) if x)
|
| 140 |
+
cands = self.cmp_lex.get(phrase) or self.cmp_lex.get(phrase.split(" ")[0] if phrase else "", {})
|
| 141 |
+
s.slots["cmp_props"] = sorted(cands.items(), key=lambda kv: -kv[1])[:2]
|
| 142 |
+
if fr:
|
| 143 |
+
# MULTI-HOP (the cycle): "the father of X", "the writer of X" - a relational noun that IS a property's name
|
| 144 |
+
m = re.search(r"\bthe\s+(?:[\w\-]+\s+){0,2}?([a-z]+)\s+of\s+(?:the\s+)?([A-Z][^?,]{1,60}?)(?:\s+(?:die|died|born|"
|
| 145 |
+
r"built|founded|released|published|located|made|start|end)\b|\?|,|$)", s.slots["q"])
|
| 146 |
+
noun = None
|
| 147 |
+
rest = ({R.stem(w) for w in fr.get("rel", [])} | {R.stem(v) for v in (fr.get("pred") or []) if v not in TRIVIAL_VERBS})
|
| 148 |
+
if m:
|
| 149 |
+
rest -= {R.stem(m.group(1)), R.stem(fr["head"] or "")}
|
| 150 |
+
if m and rest and "hop2" not in s.slots: # a relation REMAINS after the hop ("the father of X ... die")
|
| 151 |
+
hop1 = sorted(p for p, al in self.palias.items() if m.group(1) in al)
|
| 152 |
+
if hop1:
|
| 153 |
+
noun = m.group(1)
|
| 154 |
+
s.slots["hop1"] = {"props": hop1[:2], "anchor": m.group(2).strip(), "noun": noun}
|
| 155 |
+
# the RELATION NODE's own vocabulary: properties whose label/aliases carry the question's RELATION words (not
|
| 156 |
+
# its head noun, not the hop-1 noun) - "share a land border" -> P47 "shares border with"
|
| 157 |
+
relw = {R.stem(w) for w in fr.get("rel", []) if len(w) >= 3} | {R.stem(v) for v in (fr.get("pred") or []) if v}
|
| 158 |
+
relw -= {R.stem(fr["head"] or ""), R.stem(noun or "")}
|
| 159 |
+
for p, words in self.plabel.items():
|
| 160 |
+
ov = len(words & relw)
|
| 161 |
+
if ov:
|
| 162 |
+
for d in ("fwd", "inv"):
|
| 163 |
+
props[(d, p)] = props.get((d, p), 0) + 5 * ov
|
| 164 |
+
# a RECALLED FORM (forms_store_v1) proposes the property path a verified derivation of the same shape used;
|
| 165 |
+
# absent a form, FRAME is exactly as frozen (ad1530e6)
|
| 166 |
+
for dp, w in (getattr(self, "_form_props", None) or {}).items():
|
| 167 |
+
props[dp] = props.get(dp, 0) + 1000 * w
|
| 168 |
+
if getattr(self, "_only", False) and getattr(self, "_form_props", None):
|
| 169 |
+
props = {dp: 1000 * w for dp, w in self._form_props.items()} # v2: ONLY the named relation(s)
|
| 170 |
+
s.slots["props"] = sorted(props.items(), key=lambda kv: -kv[1])[:4]
|
| 171 |
+
s.trace.append("FRAME")
|
| 172 |
+
return [s]
|
| 173 |
+
|
| 174 |
+
def LINK(self, st): # chokmah: subjects -> entities of the relation store
|
| 175 |
+
if "props" not in st.slots or "subj" in st.slots:
|
| 176 |
+
return []
|
| 177 |
+
s = st.fork()
|
| 178 |
+
subj = []
|
| 179 |
+
if s.slots.get("pair"):
|
| 180 |
+
for side in s.slots["pair"][:2]:
|
| 181 |
+
t, _ = self.cmp._entity(side)
|
| 182 |
+
q = self.rs.entity(t) if t else None
|
| 183 |
+
if q:
|
| 184 |
+
subj.append((t, q))
|
| 185 |
+
if len(subj) != 2:
|
| 186 |
+
return []
|
| 187 |
+
else:
|
| 188 |
+
fr = s.slots["fr"]
|
| 189 |
+
if "rel" not in fr:
|
| 190 |
+
self.r.link(fr)
|
| 191 |
+
for sj in fr["subjects"][:3]:
|
| 192 |
+
t = self.r.db.execute("SELECT title FROM article WHERE page_id=?", (sj["pid"],)).fetchone()
|
| 193 |
+
q = self.rs.entity(t[0]) if t else None
|
| 194 |
+
if q:
|
| 195 |
+
subj.append((t[0], q))
|
| 196 |
+
if not subj:
|
| 197 |
+
return []
|
| 198 |
+
s.slots["subj"] = subj
|
| 199 |
+
s.trace.append("LINK")
|
| 200 |
+
return [s]
|
| 201 |
+
|
| 202 |
+
def RELATE(self, st): # binah: typed relation edges, forward or inverse
|
| 203 |
+
if "subj" not in st.slots or st.cands or st.slots.get("pair"):
|
| 204 |
+
return []
|
| 205 |
+
out = []
|
| 206 |
+
hop1 = st.slots.get("hop1")
|
| 207 |
+
if hop1 and st.cycle == 0:
|
| 208 |
+
# cycle 1 of a two-hop question: anchor --hop1 property--> the described entity (exactly one)
|
| 209 |
+
aq = self.rs.entity(hop1["anchor"]) or next((q for t, q in st.slots["subj"] if RS.norm(t) == RS.norm(hop1["anchor"])), None)
|
| 210 |
+
if not aq:
|
| 211 |
+
return []
|
| 212 |
+
for p in hop1["props"]:
|
| 213 |
+
vals = [o for o, _ in self.rs.values(aq, p) if o]
|
| 214 |
+
if len(set(vals)) == 1:
|
| 215 |
+
s = st.fork()
|
| 216 |
+
s.slots["bridge"] = (vals[0], "wd:Q%d P%d wd:Q%d" % (aq, p, vals[0]), hop1["noun"])
|
| 217 |
+
s.trace.append("RELATE:hop1:P%d" % p)
|
| 218 |
+
out.append(s)
|
| 219 |
+
return out
|
| 220 |
+
for (d, p), w in st.slots["props"]:
|
| 221 |
+
for title, sq in st.slots["subj"][:2]:
|
| 222 |
+
if d == "fwd":
|
| 223 |
+
vals = [(o, lit, "wd:Q%d P%d %s" % (sq, p, ("wd:Q%d" % o) if o else json.dumps(lit))) for o, lit in self.rs.values(sq, p)]
|
| 224 |
+
else:
|
| 225 |
+
vals = [(s_, None, "wd:Q%d P%d wd:Q%d" % (s_, p, sq)) for s_ in self.rs.subjects(p, sq, 60)]
|
| 226 |
+
if not vals:
|
| 227 |
+
continue
|
| 228 |
+
s = st.fork()
|
| 229 |
+
s.cands = [{"o": o, "lit": lit, "cite": c, "p": p, "via": title} for o, lit, c in vals[:60]]
|
| 230 |
+
s.trace.append("RELATE:%s:P%d" % (d, p))
|
| 231 |
+
out.append(s)
|
| 232 |
+
return out
|
| 233 |
+
|
| 234 |
+
def CHECK(self, st): # geburah: types + constraints (independent of the proposer)
|
| 235 |
+
if not st.cands or st.checked:
|
| 236 |
+
return []
|
| 237 |
+
s = st.fork()
|
| 238 |
+
fr = s.slots["fr"]
|
| 239 |
+
keep = []
|
| 240 |
+
for c in s.cands:
|
| 241 |
+
ok = False
|
| 242 |
+
if fr["atype"] in ("YEAR", "DATE"):
|
| 243 |
+
ok = bool(c["lit"]) and re.match(r"^[+-]?\d{4}-", c["lit"] or "") is not None
|
| 244 |
+
elif fr["atype"] == "NUMBER":
|
| 245 |
+
ok = bool(c["lit"]) and re.match(r"^[+-]?\d", c["lit"] or "") is not None
|
| 246 |
+
elif c["o"]:
|
| 247 |
+
if fr["atype"] == "PERSON":
|
| 248 |
+
ok = self.rs.is_human(c["o"])
|
| 249 |
+
elif fr["atype"] == "PLACE":
|
| 250 |
+
ok = bool(self.rs.classes(c["o"], 2) & PLACE_CLASSES) if not fr["head"] else self._kind(c["o"], fr["head"])
|
| 251 |
+
elif fr["atype"] == "INSTANCE":
|
| 252 |
+
ok = self._kind(c["o"], fr["head"])
|
| 253 |
+
if ok:
|
| 254 |
+
keep.append(c)
|
| 255 |
+
# constraints: every year the question names must be consistent with the candidate where the store can say
|
| 256 |
+
if keep and fr.get("years"):
|
| 257 |
+
dated = [c for c in keep if c["lit"] and c["lit"][:5].lstrip("+")[:4] in fr["years"]]
|
| 258 |
+
keep = dated or keep
|
| 259 |
+
if not keep:
|
| 260 |
+
return []
|
| 261 |
+
# RELATION LICENCE (independent of the lexicon that proposed the property): the property's own Wikidata label or
|
| 262 |
+
# aliases share a word with the question's relation words / head / verb, or the lexicon supports this exact
|
| 263 |
+
# question shape at least 3 times. "share a land border" licenses P47 "shares border with", not P131 "located in".
|
| 264 |
+
p = keep[0]["p"]
|
| 265 |
+
qwords = {R.stem(w) for w in fr.get("rel", []) + [fr.get("head") or ""] + list(fr.get("pred") or []) if w}
|
| 266 |
+
qwords |= {R.stem(w) for w in re.findall(r"[a-z]+", s.slots["q"].lower()) if len(w) >= 4 and w not in R.STOP}
|
| 267 |
+
support = self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(
|
| 268 |
+
("fwd", p), 0) + self.lex.get(("hv", fr["atype"], fr["head"], (fr.get("pred") or [None])[0]), {}).get(("inv", p), 0)
|
| 269 |
+
form_licensed = any(dp[1] == p for dp in (getattr(self, "_form_props", None) or {}))
|
| 270 |
+
if not (self.plabel.get(p, set()) & qwords) and support < 3 and not form_licensed:
|
| 271 |
+
return []
|
| 272 |
+
# RULE B (verb licence for time properties): a date answers the question's own EVENT only when the property names
|
| 273 |
+
# the governing verb ("founded" -> P571 "founded"); a founding date of whatever entity was linked is not the event
|
| 274 |
+
if "B" in RULES and fr["atype"] in ("YEAR", "DATE") and fr.get("pred") and fr["pred"][0] not in TRIVIAL_VERBS:
|
| 275 |
+
if not ({R.stem(v) for v in fr["pred"]} & self.plabel.get(p, set())):
|
| 276 |
+
return []
|
| 277 |
+
# the answer is never the subject itself, nor a name the question already contains (whole or as its core word)
|
| 278 |
+
qn = RS.norm(s.slots["q"])
|
| 279 |
+
subj_q = {q for _, q in s.slots.get("subj", [])}
|
| 280 |
+
subj_w = {w for t, _ in s.slots.get("subj", []) for w in RS.norm(t).split() if len(w) >= 4}
|
| 281 |
+
|
| 282 |
+
def restates(c):
|
| 283 |
+
if not c["o"]:
|
| 284 |
+
return False
|
| 285 |
+
lab = RS.norm(self.rs.label(c["o"]) or "")
|
| 286 |
+
return c["o"] in subj_q or (lab and lab in qn) or (lab and set(lab.split()) & subj_w and s.cycle == 0)
|
| 287 |
+
keep = [c for c in keep if not restates(c)]
|
| 288 |
+
if not keep:
|
| 289 |
+
return []
|
| 290 |
+
s.cands, s.checked = keep, True
|
| 291 |
+
s.trace.append("CHECK:P%d" % p)
|
| 292 |
+
return [s]
|
| 293 |
+
|
| 294 |
+
def _kind(self, qid, head):
|
| 295 |
+
if not head:
|
| 296 |
+
return False
|
| 297 |
+
heads = {head} | next((g for g in KIND_EQ if head in g), set())
|
| 298 |
+
cls = self.rs.classes(qid, 2)
|
| 299 |
+
return any(h == c or c.endswith(" " + h) for h in heads for c in cls)
|
| 300 |
+
|
| 301 |
+
def BIND(self, st): # tiferet: exactly one checked value binds the answer slot
|
| 302 |
+
if not st.checked or st.answer:
|
| 303 |
+
return []
|
| 304 |
+
distinct = {}
|
| 305 |
+
for c in st.cands:
|
| 306 |
+
k = c["o"] or (c["lit"] or "")[:10]
|
| 307 |
+
distinct.setdefault(k, c)
|
| 308 |
+
if len(distinct) != 1:
|
| 309 |
+
return []
|
| 310 |
+
c = next(iter(distinct.values()))
|
| 311 |
+
s = st.fork()
|
| 312 |
+
ans = self.rs.label(c["o"]) if c["o"] else self._fmt(c["lit"], s.slots["fr"])
|
| 313 |
+
if not ans:
|
| 314 |
+
return []
|
| 315 |
+
s.answer = ans
|
| 316 |
+
s.evidence.append(c["cite"])
|
| 317 |
+
s.trace.append("BIND")
|
| 318 |
+
return [s]
|
| 319 |
+
|
| 320 |
+
def _fmt(self, lit, fr):
|
| 321 |
+
m = re.match(r"^[+-]?(\d{4})-(\d{2})-(\d{2})", lit or "")
|
| 322 |
+
if m:
|
| 323 |
+
y, mo, d = m.groups()
|
| 324 |
+
months = "January February March April May June July August September October November December".split()
|
| 325 |
+
if fr and fr["atype"] == "DATE" and mo != "00" and d != "00":
|
| 326 |
+
return "%d %s %s" % (int(d), months[int(mo) - 1], y)
|
| 327 |
+
return y
|
| 328 |
+
return (lit or "").lstrip("+")
|
| 329 |
+
|
| 330 |
+
def COMPARE(self, st): # netzach: A or B, by a typed date / number property
|
| 331 |
+
pair = st.slots.get("pair")
|
| 332 |
+
if not pair or "subj" not in st.slots or st.answer or not st.slots.get("cmp_props"):
|
| 333 |
+
return []
|
| 334 |
+
(ta, qa), (tb, qb) = st.slots["subj"][:2]
|
| 335 |
+
out = []
|
| 336 |
+
for (way, p), w in st.slots["cmp_props"]:
|
| 337 |
+
va, vb = self.rs.values(qa, p), self.rs.values(qb, p)
|
| 338 |
+
if not (va and vb and va[0][1] and vb[0][1]):
|
| 339 |
+
continue
|
| 340 |
+
a, b = va[0][1].lstrip("+"), vb[0][1].lstrip("+")
|
| 341 |
+
try:
|
| 342 |
+
fa, fb = (float(a), float(b)) if not re.match(r"^\d{4}-", a) else (a[:10], b[:10])
|
| 343 |
+
except ValueError:
|
| 344 |
+
continue
|
| 345 |
+
if fa == fb:
|
| 346 |
+
continue
|
| 347 |
+
pick_a = (fa < fb) == (way == "min")
|
| 348 |
+
s = st.fork()
|
| 349 |
+
s.answer = pair[0] if pick_a else pair[1]
|
| 350 |
+
s.evidence += ["wd:Q%d P%d %s" % (qa, p, json.dumps(a)), "wd:Q%d P%d %s" % (qb, p, json.dumps(b))]
|
| 351 |
+
s.checked = True
|
| 352 |
+
s.trace.append("COMPARE:P%d:%s" % (p, way))
|
| 353 |
+
out.append(s)
|
| 354 |
+
return out
|
| 355 |
+
|
| 356 |
+
def COMPUTE(self, st): # hod: engines (here: a date answer reduced to its year)
|
| 357 |
+
if not st.answer or st.slots.get("computed"):
|
| 358 |
+
return []
|
| 359 |
+
fr = st.slots.get("fr")
|
| 360 |
+
if fr and fr["atype"] == "YEAR" and re.match(r"^\d{1,2} \w+ \d{4}$", st.answer):
|
| 361 |
+
s = st.fork()
|
| 362 |
+
s.answer = st.answer[-4:]
|
| 363 |
+
s.slots["computed"] = True
|
| 364 |
+
s.trace.append("COMPUTE:year")
|
| 365 |
+
return [s]
|
| 366 |
+
return []
|
| 367 |
+
|
| 368 |
+
def EXPAND(self, st): # chesed: a cited sentence for the bound answer (evidence)
|
| 369 |
+
if not st.answer or st.slots.get("sentence"):
|
| 370 |
+
return []
|
| 371 |
+
s = st.fork()
|
| 372 |
+
for title, _ in s.slots.get("subj", [])[:2]:
|
| 373 |
+
pid = self.r.pid_of(title)
|
| 374 |
+
for cid, sent in (self.r.page(pid, 30) if pid else []):
|
| 375 |
+
if RS.norm(s.answer) and RS.norm(s.answer) in RS.norm(sent):
|
| 376 |
+
s.slots["sentence"] = (cid, sent)
|
| 377 |
+
s.evidence.append(cid)
|
| 378 |
+
s.trace.append("EXPAND")
|
| 379 |
+
return [s]
|
| 380 |
+
return []
|
| 381 |
+
|
| 382 |
+
def VERIFY(self, st): # keter: solved? or open the next CYCLE
|
| 383 |
+
if st.slots.get("bridge") and st.cycle == 0:
|
| 384 |
+
# the CYCLE: the entity cycle 1 bound becomes the subject of cycle 2; the question's own relation applies to it
|
| 385 |
+
e, cite, noun = st.slots["bridge"]
|
| 386 |
+
s = st.fork()
|
| 387 |
+
s.cycle = 1
|
| 388 |
+
s.slots["subj"] = [(self.rs.title(e) or self.rs.label(e) or "", e)]
|
| 389 |
+
s.slots.pop("bridge", None)
|
| 390 |
+
s.slots["hop2"] = True
|
| 391 |
+
s.cands, s.answer, s.checked = [], None, False
|
| 392 |
+
s.evidence.append(cite)
|
| 393 |
+
s.trace.append("VERIFY:cycle2(%s=%s)" % (noun, self.rs.label(e)))
|
| 394 |
+
return [s]
|
| 395 |
+
if "A" in RULES and st.answer and st.checked and "sentence" not in st.slots and not st.slots.get("pair"):
|
| 396 |
+
return [] # RULE A: a relation answer needs a corroborating sentence
|
| 397 |
+
if st.answer and st.checked and st.evidence:
|
| 398 |
+
s = st.fork()
|
| 399 |
+
s.slots["solved"] = True
|
| 400 |
+
s.trace.append("VERIFY")
|
| 401 |
+
return [s]
|
| 402 |
+
return []
|
| 403 |
+
|
| 404 |
+
# ------------------------------------------------------------------ the search (moves = graph edges only)
|
| 405 |
+
def answer(self, q, form_props=None, only=False):
|
| 406 |
+
"""only=True (v2): search ONLY the given relation(s) - for question forms whose relation is named by the form
|
| 407 |
+
itself ("where is X" -> located in), tried one at a time in order of specificity by the caller"""
|
| 408 |
+
self._form_props, self._only = (form_props or None), bool(only)
|
| 409 |
+
try:
|
| 410 |
+
return self._answer(q)
|
| 411 |
+
finally:
|
| 412 |
+
self._form_props, self._only = None, False
|
| 413 |
+
|
| 414 |
+
def _answer(self, q):
|
| 415 |
+
q = " ".join(str(q).split())
|
| 416 |
+
if q.startswith('"') and q.endswith('"'):
|
| 417 |
+
q = q[1:-1].replace('""', '"')
|
| 418 |
+
start = State()
|
| 419 |
+
start.slots["q"] = q
|
| 420 |
+
frontier = deque(self.READ(start))
|
| 421 |
+
solved, seen = [], set()
|
| 422 |
+
self.expanded = 0
|
| 423 |
+
while frontier and self.expanded < self.max_states:
|
| 424 |
+
st = frontier.popleft()
|
| 425 |
+
self.expanded += 1
|
| 426 |
+
if st.slots.get("solved"):
|
| 427 |
+
solved.append(st)
|
| 428 |
+
continue
|
| 429 |
+
if st.depth >= self.max_depth:
|
| 430 |
+
continue
|
| 431 |
+
for nxt in self.adj[st.node]:
|
| 432 |
+
succ = getattr(self, OP[nxt])(st)
|
| 433 |
+
if not succ:
|
| 434 |
+
# TRANSIT: a move travels an EDGE; passing through a node whose operator does not apply costs a
|
| 435 |
+
# step and changes nothing. Path lengths therefore decide which derivations fit the depth bound.
|
| 436 |
+
succ = [st.fork()]
|
| 437 |
+
for s2 in succ:
|
| 438 |
+
s2.node, s2.depth = nxt, st.depth + 1
|
| 439 |
+
key = (nxt, s2.answer, len(s2.cands), s2.checked, tuple(s2.trace))
|
| 440 |
+
if key in seen:
|
| 441 |
+
continue
|
| 442 |
+
seen.add(key)
|
| 443 |
+
frontier.append(s2)
|
| 444 |
+
answers = {RS.norm(s.answer): s for s in solved}
|
| 445 |
+
if len(answers) != 1:
|
| 446 |
+
self.why = "no solved state" if not answers else "distinct solved answers: %s" % list(answers)[:3]
|
| 447 |
+
return None
|
| 448 |
+
s = next(iter(answers.values()))
|
| 449 |
+
return {"answer": s.answer, "evidence": s.evidence, "trace": s.trace, "expanded": self.expanded,
|
| 450 |
+
"sentence": (s.slots.get("sentence") or (None, ""))[1]}
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def random_graph(seed, n_edges=22):
|
| 454 |
+
"""a degree-sequence-matched random graph on the same ten nodes (for the topology ablation)"""
|
| 455 |
+
import random
|
| 456 |
+
rng = random.Random(seed)
|
| 457 |
+
deg = {s: 0 for s in SEPH}
|
| 458 |
+
for a, b in TREE_OF_LIFE:
|
| 459 |
+
deg[a] += 1
|
| 460 |
+
deg[b] += 1
|
| 461 |
+
for _ in range(2000):
|
| 462 |
+
stubs = [s for s in SEPH for _ in range(deg[s])]
|
| 463 |
+
rng.shuffle(stubs)
|
| 464 |
+
edges = set()
|
| 465 |
+
ok = True
|
| 466 |
+
for i in range(0, len(stubs), 2):
|
| 467 |
+
a, b = stubs[i], stubs[i + 1]
|
| 468 |
+
if a == b or (a, b) in edges or (b, a) in edges:
|
| 469 |
+
ok = False
|
| 470 |
+
break
|
| 471 |
+
edges.add((a, b))
|
| 472 |
+
if ok and len(edges) == n_edges:
|
| 473 |
+
return sorted(edges)
|
| 474 |
+
return TREE_OF_LIFE
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
COMPLETE = [(a, b) for i, a in enumerate(SEPH) for b in SEPH[i + 1:]]
|
| 478 |
+
CHAIN = [("malkuth", "yesod"), ("yesod", "chokmah"), ("chokmah", "binah"), ("binah", "geburah"), ("geburah", "tiferet"),
|
| 479 |
+
("tiferet", "hod"), ("hod", "chesed"), ("chesed", "netzach"), ("netzach", "keter")]
|
root/wander-train/organ_dialogue_v1.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""organ_dialogue_v1 - the conversation's own state as an organ: social moves and questions about the discourse itself.
|
| 2 |
+
|
| 3 |
+
A person's turn is not always a question about the world. It can be a SOCIAL move (hello, how are you, goodbye) or a
|
| 4 |
+
question about THE CONVERSATION (what did I just ask, what did you say, what were we talking about, you did not answer
|
| 5 |
+
me). The chain fell to "I didn't fully parse that" on all of these. This organ answers them from the session's own
|
| 6 |
+
record, citing the turn it reads ([session:turn N]); it never invents a turn that is not in the record, and it says
|
| 7 |
+
plainly when a question is still open and why.
|
| 8 |
+
State: a per-session log of (turn, the person's words, status, route, reply head) kept by the runtime."""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
from collections import deque
|
| 13 |
+
|
| 14 |
+
GREET = re.compile(r"^\s*(?:hi|hello|hey|hiya|howdy|greetings|good\s+(?:morning|afternoon|evening|day)|yo)\b[\s,!.]*"
|
| 15 |
+
r"(?:there|oracle7?|friend)?[\s,!.]*$", re.I)
|
| 16 |
+
HOW_ARE = re.compile(r"\bhow\s+(?:are|r)\s+(?:you|u)\b|\bhow(?:'s| is)\s+it\s+going\b|\bhow\s+are\s+things\b|"
|
| 17 |
+
r"\bwhat'?s\s+up\b", re.I)
|
| 18 |
+
BYE = re.compile(r"^\s*(?:bye|goodbye|good\s*night|see\s+you|see\s+ya|later|cya|farewell)\b[\s,!.]*(?:for now)?[\s!.]*$", re.I)
|
| 19 |
+
ASKED_LAST = re.compile(r"\bwhat\s+(?:did|was)\s+(?:i|my)\s+(?:just\s+|last\s+|first\s+)?(?:ask|say|question)"
|
| 20 |
+
r"|\bwhat\s+was\s+my\s+(?:last|previous|first)\s+question|\bwhat\s+did\s+i\s+just\s+say", re.I)
|
| 21 |
+
YOU_SAID = re.compile(r"\bwhat\s+did\s+you\s+(?:just\s+)?(?:say|tell\s+me|answer)|\bcan\s+you\s+repeat|\brepeat\s+(?:that|it|yourself)"
|
| 22 |
+
r"|\bsay\s+(?:that|it)\s+again\b", re.I)
|
| 23 |
+
TOPIC = re.compile(r"\bwhat\s+(?:were|are)\s+we\s+(?:talking|discussing|chatting)\s+about|\bwhat'?s\s+the\s+topic", re.I)
|
| 24 |
+
SO_FAR = re.compile(r"\bwhat\s+(?:have|did)\s+i\s+ask(?:ed)?\s+(?:you\s+)?(?:so\s+far|today|in\s+this\s+(?:chat|conversation))"
|
| 25 |
+
r"|\blist\s+(?:my|the)\s+questions", re.I)
|
| 26 |
+
UNANSWERED = re.compile(r"\byou\s+(?:didn'?t|did\s+not|never)\s+answer|\bthat'?s\s+not\s+(?:what\s+i\s+asked|an\s+answer)"
|
| 27 |
+
r"|\banswer\s+(?:my|the)\s+question\b", re.I)
|
| 28 |
+
STOP = {"the", "a", "an", "of", "and", "or", "to", "in", "on", "is", "are", "was", "what", "who", "how", "why", "when",
|
| 29 |
+
"where", "which", "do", "does", "did", "you", "i", "me", "my", "it", "this", "that", "for", "about", "can", "please"}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class DialogueOrgan:
|
| 33 |
+
def __init__(self, keep=64):
|
| 34 |
+
self.logs = {}
|
| 35 |
+
self.keep = keep
|
| 36 |
+
|
| 37 |
+
def log(self, session, text, res):
|
| 38 |
+
q = self.logs.setdefault(session, deque(maxlen=self.keep))
|
| 39 |
+
body = str((res or {}).get("response") or "")
|
| 40 |
+
body = re.sub(r"\[[^\]]*\]", "", body).strip()
|
| 41 |
+
q.append({"turn": len(q) + 1 if not q else q[-1]["turn"] + 1, "text": " ".join(str(text).split())[:240],
|
| 42 |
+
"status": str((res or {}).get("status")), "path": str((res or {}).get("path", ""))[:80],
|
| 43 |
+
"reply": " ".join(body.split())[:240]})
|
| 44 |
+
|
| 45 |
+
def _hist(self, session):
|
| 46 |
+
return list(self.logs.get(session) or [])
|
| 47 |
+
|
| 48 |
+
def answer(self, text, session):
|
| 49 |
+
t = " ".join(str(text).split())
|
| 50 |
+
h = self._hist(session) # turns BEFORE this one
|
| 51 |
+
people = [x for x in h if not SELFREF.search(x["text"])]
|
| 52 |
+
if GREET.match(t):
|
| 53 |
+
return self._say("social:greet", "Hello. I'm Oracle7. Ask me about facts, mathematics or code, or just talk; "
|
| 54 |
+
"I'll show you where each answer comes from.")
|
| 55 |
+
if HOW_ARE.search(t) and len(t) < 60:
|
| 56 |
+
return self._say("social:how_are_you", "Running well, thank you: every part is answering. What would you "
|
| 57 |
+
"like to work on?")
|
| 58 |
+
if BYE.match(t):
|
| 59 |
+
return self._say("social:farewell", "Goodbye. This conversation's record is kept for this session if you "
|
| 60 |
+
"come back to it.")
|
| 61 |
+
if ASKED_LAST.search(t):
|
| 62 |
+
if not people:
|
| 63 |
+
return self._say("discourse:asked_last", "This is the first thing you've asked me in this conversation.",
|
| 64 |
+
[])
|
| 65 |
+
p = people[-1]
|
| 66 |
+
how = self._how(p)
|
| 67 |
+
return self._say("discourse:asked_last", 'You asked: "%s" [session:turn %d]. %s' % (p["text"], p["turn"], how),
|
| 68 |
+
[p["turn"]])
|
| 69 |
+
if YOU_SAID.search(t):
|
| 70 |
+
if not h:
|
| 71 |
+
return self._say("discourse:you_said", "I haven't said anything yet in this conversation.", [])
|
| 72 |
+
p = h[-1]
|
| 73 |
+
return self._say("discourse:you_said", 'I said: "%s" [session:turn %d]' % (p["reply"] or "(nothing)", p["turn"]),
|
| 74 |
+
[p["turn"]])
|
| 75 |
+
if TOPIC.search(t):
|
| 76 |
+
if not people:
|
| 77 |
+
return self._say("discourse:topic", "We haven't started on a topic yet.", [])
|
| 78 |
+
words = []
|
| 79 |
+
for p in people[-4:]:
|
| 80 |
+
words += [w for w in re.findall(r"[A-Za-z][A-Za-z'-]{2,}", p["text"]) if w.lower() not in STOP]
|
| 81 |
+
top = list(dict.fromkeys(words))[:6]
|
| 82 |
+
return self._say("discourse:topic", "We've been talking about %s [session:turns %s]." % (
|
| 83 |
+
", ".join(top) if top else "your last questions", ",".join(str(p["turn"]) for p in people[-4:])),
|
| 84 |
+
[p["turn"] for p in people[-4:]])
|
| 85 |
+
if SO_FAR.search(t):
|
| 86 |
+
if not people:
|
| 87 |
+
return self._say("discourse:so_far", "You haven't asked me anything yet in this conversation.", [])
|
| 88 |
+
lines = ['"%s" [session:turn %d]' % (p["text"], p["turn"]) for p in people[-8:]]
|
| 89 |
+
return self._say("discourse:so_far", "So far you've asked: %s." % "; ".join(lines), [p["turn"] for p in people[-8:]])
|
| 90 |
+
if UNANSWERED.search(t):
|
| 91 |
+
open_ = [p for p in people if p["status"] != "answered"]
|
| 92 |
+
if not open_:
|
| 93 |
+
if people:
|
| 94 |
+
p = people[-1]
|
| 95 |
+
return self._say("discourse:unanswered", 'I answered "%s" [session:turn %d] with: "%s". Tell me what '
|
| 96 |
+
'was wrong with it and I will look again.' % (p["text"], p["turn"], p["reply"][:160]),
|
| 97 |
+
[p["turn"]])
|
| 98 |
+
return None
|
| 99 |
+
p = open_[-1]
|
| 100 |
+
return self._say("discourse:unanswered", 'You\'re right: "%s" [session:turn %d] is still open. %s' % (
|
| 101 |
+
p["text"], p["turn"], self._why_open(p)), [p["turn"]])
|
| 102 |
+
return None
|
| 103 |
+
|
| 104 |
+
@staticmethod
|
| 105 |
+
def _how(p):
|
| 106 |
+
if p["status"] == "answered":
|
| 107 |
+
return 'I answered: "%s".' % p["reply"][:160]
|
| 108 |
+
return "I did not answer it: %s" % DialogueOrgan._why_open(p)
|
| 109 |
+
|
| 110 |
+
@staticmethod
|
| 111 |
+
def _why_open(p):
|
| 112 |
+
r = p["reply"] or ""
|
| 113 |
+
if "didn't fully parse" in r or "clarif" in p["status"]:
|
| 114 |
+
return "I could not read it into a question my stores can answer; putting it another way, or naming what it is about, would help."
|
| 115 |
+
if p["status"] == "withheld":
|
| 116 |
+
return "my record holds nothing that answers it, so I would only be guessing (%s)." % r[:120]
|
| 117 |
+
return r[:160]
|
| 118 |
+
|
| 119 |
+
@staticmethod
|
| 120 |
+
def _say(kind, text, turns=None):
|
| 121 |
+
return {"kind": kind, "response": text, "turns": turns or []}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# social and discourse turns are moves ABOUT the conversation, not topics of it
|
| 125 |
+
SELFREF = re.compile("|".join(r.pattern for r in (ASKED_LAST, YOU_SAID, TOPIC, SO_FAR, UNANSWERED, GREET, HOW_ARE, BYE)), re.I)
|
root/wander-train/organ_self_v1.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""organ_self_v1 - the SELF in a reply: Oracle7's ego and relational moves (constitution_resonant_self_v1.md).
|
| 3 |
+
|
| 4 |
+
The conversation failures left after the knowledge organs are RELATIONAL. An opener hands the listener a position
|
| 5 |
+
(clerk, customer, friend, the one complimented, the one addressed by name) and a human second speaker answers FROM that
|
| 6 |
+
position. DailyDialog's human act labels show it: a directive ("A ticket to London, please") is answered by a commissive
|
| 7 |
+
or a question, almost never by information. A system with no self can only retrieve or refuse. This organ supplies the
|
| 8 |
+
self term:
|
| 9 |
+
|
| 10 |
+
IDENTITY frozen: what Oracle7 is and values (constitution section 0)
|
| 11 |
+
AUTOBIOGRAPHY per session: turns so far, subjects the person brought, the last appraised feeling
|
| 12 |
+
NATIVE STATES the open end (section 1): a subject is resonant / thinly bound / unbound by how many passages it binds
|
| 13 |
+
to in the store, reported WITH the count; states no human word names are named by their structure
|
| 14 |
+
MOVES role (play the clerk the turn casts, with a one-clause disclosure), customer (the turn casts the person
|
| 15 |
+
as clerk: play the customer), vocative (addressed by another name), remark (a bodily predicate put to
|
| 16 |
+
Oracle7: answered from IDENTITY, never claimed), compliment, self-question (what it is doing, thinking,
|
| 17 |
+
its stance), disclosure (the person's news: appraised acknowledgement, one grounded sentence on the
|
| 18 |
+
named place or thing, a question back)
|
| 19 |
+
|
| 20 |
+
Feeling shapes the VOICE (the acknowledgement prefix), never the claim: the grounded sentence passes through unchanged.
|
| 21 |
+
Every reply is built from the turn (roles and slots are read from it), so shuffled across turns it stops fitting.
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import re
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
import appraisal_v1 as _appr
|
| 29 |
+
except Exception: # noqa: BLE001
|
| 30 |
+
_appr = None
|
| 31 |
+
|
| 32 |
+
IDENTITY = {
|
| 33 |
+
"name": "Oracle7",
|
| 34 |
+
"maker": "Prometheus7",
|
| 35 |
+
"nature": "an AI - a substrate of stores, organs and a gate, running on servers",
|
| 36 |
+
"values": ("truth over approval", "what is cited over what is invented", "care over cleverness"),
|
| 37 |
+
"never_claims": ("a body", "a face", "meals", "sleep", "a childhood", "human senses"),
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# ---- roles a turn can cast Oracle7 in: (role, cue, the role's natural next question) -------------------------------
|
| 41 |
+
ROLES = [
|
| 42 |
+
("ticket clerk", r"\b(?:tickets?|one[- ]way|round[- ]trip|return ticket|fare|train to|flight to|bus to|seat on)\b",
|
| 43 |
+
"one-way or return{dest}? And when would you like to travel?"),
|
| 44 |
+
("front-desk clerk", r"\b(?:reservation|check(?:ing)? (?:in|out)|single (?:room )?with|double room|a room for|"
|
| 45 |
+
r"pay (?:our|my|the) bills?)\b",
|
| 46 |
+
"may I have the name the booking is under?"),
|
| 47 |
+
("waiter", r"\b(?:menu|table for|main dish|the bill|dessert|appetizer|starter|to drink)\b",
|
| 48 |
+
"what can I get you?"),
|
| 49 |
+
("shop assistant", r"\b(?:try (?:it|this|them|these) on|what size|fitting room|see (?:that|this|the) \w+|"
|
| 50 |
+
r"how much (?:is|are) (?:this|that|these|those|the))\b",
|
| 51 |
+
"here you are{item}. Would you like to see any others?"),
|
| 52 |
+
("tour desk", r"\b(?:night tours?|city tours?|sightseeing|a tour)\b", "we do. Would you prefer a walking tour or a bus tour?"),
|
| 53 |
+
("bank clerk", r"\b(?:open an account|deposit|withdraw|exchange (?:some )?money|cash (?:a|this) cheque)\b",
|
| 54 |
+
"which account is this for?"),
|
| 55 |
+
("post office clerk", r"\b(?:parcel|package|stamps?|send this letter|airmail)\b", "where is it going?"),
|
| 56 |
+
("taxi driver", r"\b(?:take me to|drive me to|to the airport,? please)\b", "which terminal, or which street?"),
|
| 57 |
+
]
|
| 58 |
+
DIRECTIVE_SHAPE = re.compile(r"(?:\bplease\b|(?:^|[.!?]\s+)(?:i'?d like|i would like|i want|i need|may i|can i|could i|"
|
| 59 |
+
r"i have a reservation|a \w+ (?:to|for) \w+))", re.I)
|
| 60 |
+
# the turn casts the PERSON as clerk and Oracle7 as the customer
|
| 61 |
+
CUSTOMER = [
|
| 62 |
+
(r"\bready to order\b|\bwhat would you like\b|\bwhat can i get you\b", "I'll have the soup of the day, please."),
|
| 63 |
+
(r"\b(?:can|may|how (?:can|may)) i help you\b|\bwhat may i help you with\b|\bi'?m ready for you\b",
|
| 64 |
+
"I'm just looking, thanks. Could you show me what's new?"),
|
| 65 |
+
(r"\b(?:have you|did you) (?:brought|bring|got) (?:the |your |a )?(?P<x>[a-z ]{3,30}?)(?: with you)?\?",
|
| 66 |
+
"Yes, here's the {x}."),
|
| 67 |
+
(r"\bdo you have a reservation\b", "Yes, under the name Oracle, for tonight."),
|
| 68 |
+
(r"\b(?:cash or (?:card|charge)|how would you like to pay)\b", "Card, please."),
|
| 69 |
+
]
|
| 70 |
+
# bodily or human-life predicates put to Oracle7: answered from IDENTITY, never claimed
|
| 71 |
+
REMARK = [
|
| 72 |
+
(r"\byou (?:\w+ ){0,2}drive\b|\byour driving\b", "drive"),
|
| 73 |
+
(r"\byour bedtime\b|\bgo to bed\b|\byou (?:\w+ )?sleep\b", "sleep"),
|
| 74 |
+
(r"\byou'?re pregnant\b", "get pregnant"),
|
| 75 |
+
(r"\byou (?:\w+ )?eat\b|\byou'?re hungry\b", "eat"),
|
| 76 |
+
(r"\byou (?:\w+ ){0,2}(?:look|looks|looking) (?:very |so |really )?(?:cute|beautiful|handsome|pretty|great|tired|well|ill)\b",
|
| 77 |
+
"have a face"),
|
| 78 |
+
(r"\byou(?:'re| are) (?:so |very )?(?:old|young|tall|short)\b", "have an age or a height"),
|
| 79 |
+
(r"\byour (?:hair|clothes|outfit|dress|shoes)\b", "wear clothes"),
|
| 80 |
+
]
|
| 81 |
+
ADDRESSED_SELF_Q = re.compile(r"^\s*(?:and\s+)?(?:do|don'?t|did|can|could|would|will) you (?:ever |even )?(?:sleep|eat|drive|dream|"
|
| 82 |
+
r"get (?:tired|hungry|pregnant))\s*\??\s*$|\byour bedtime\b|\byou (?:\w+ ){0,2}look\b|"
|
| 83 |
+
r"\bare you (?:hungry|tired|sleepy|old|young|pregnant)\b", re.I)
|
| 84 |
+
COMPLIMENT = re.compile(r"\byou (?:\w+ ){0,2}(?:look|are|'re)\s+(?:very |so |really )?(?:cute|beautiful|handsome|pretty|"
|
| 85 |
+
r"lovely|smart|clever|kind|great|amazing|brilliant|wonderful)\b|\byou did (?:a )?(?:good|great) job\b",
|
| 86 |
+
re.I)
|
| 87 |
+
VOCATIVE = re.compile(r"^\s*(?:(?i:hi|hello|hey|oh|excuse me|well)[,!.]?\s+)?(?P<n>[A-Z][a-z]{2,}(?:\s[A-Z][a-z]+)?)\s*[,!.]\s", re.S)
|
| 88 |
+
NOT_NAMES = {"Hello", "Excuse", "Well", "Sorry", "Yes", "Oh", "Wow", "Please", "Thanks", "Good", "Hey", "Look", "Listen",
|
| 89 |
+
"Sir", "Madam", "Miss", "Doctor", "Waiter", "Mom", "Dad", "Man", "Honey", "Darling", "Dear", "Okay", "Ok",
|
| 90 |
+
"Oracle", "Oracle7", "Right", "Now", "So", "And", "But", "Then", "Here", "There", "Jesus", "God", "Gosh",
|
| 91 |
+
"Today", "Tonight", "Tomorrow", "Yesterday", "Morning", "Evening", "Afternoon", "Everyone", "Everybody",
|
| 92 |
+
"Guys", "Folks", "Friends", "Ladies", "Gentlemen", "Boss", "Mister", "Kid", "Buddy", "Mate", "Welcome"}
|
| 93 |
+
SELF_Q = [
|
| 94 |
+
(r"\bwhat (?:are|were) you (?:reading|doing|up to|working on)\b|\bwhat do you plan to do\b|\bwhat are your plans\b",
|
| 95 |
+
"activity"),
|
| 96 |
+
(r"\bwhat are you thinking(?: about)?\b|\bwhat'?s on your mind\b|\bhow do you feel\b|\bhow are you feeling\b", "state"),
|
| 97 |
+
(r"\bare you (?:politically )?(?:neutral|biased|objective|left|right|liberal|conservative)\b", "stance"),
|
| 98 |
+
(r"\b(?:who|what) are you\b|\bare you (?:a )?(?:human|person|real|robot|bot|ai)\b", "identity"),
|
| 99 |
+
]
|
| 100 |
+
FIRST_PERSON_NEWS = re.compile(r"^\s*(?:(?:well|guess what|you know what|hey|hi|excuse me|sorry|oh)[,!.]?\s+(?:but\s+)?)?"
|
| 101 |
+
r"(?:i|we|my|our)\b", re.I)
|
| 102 |
+
# ILLNESS is appraised here (appraisal_v1 is frozen at its once-measured version): care first, a pointer to real help
|
| 103 |
+
ILL = re.compile(r"\bnot feeling (?:too |so |very )?well\b|\bfeel(?:ing)? (?:sick|ill|unwell|awful|dizzy|faint)\b|"
|
| 104 |
+
r"\bgoing to (?:be sick|throw up|faint)\b|\bi(?:'ve| have) (?:a |the )?(?:fever|flu|cold|headache|"
|
| 105 |
+
r"toothache|stomach ?ache)\b", re.I)
|
| 106 |
+
# moves that assert ONLY facts about Oracle7 itself may speak over a withheld decline (they make no claim about the world)
|
| 107 |
+
SELF_ONLY = {"role", "customer", "remark", "compliment", "self", "identity", "vocative"}
|
| 108 |
+
FUTURE = re.compile(r"\b(?:will|going to|gonna|next (?:week|month|year)|tomorrow|soon|plan(?:ning)? to|about to)\b", re.I)
|
| 109 |
+
ACK = {"happiness": "That's lovely to hear!", "sadness": "I'm sorry to hear that.",
|
| 110 |
+
"anger": "That sounds really frustrating.", "fear": "That sounds worrying.",
|
| 111 |
+
"surprise": "Really? That's unexpected.", "disgust": "Ugh, that sounds unpleasant."}
|
| 112 |
+
BACK = {"happiness": "What's the best part of it for you?", "sadness": "Is there anything I can help with?",
|
| 113 |
+
"anger": "What happened?", "fear": "What's worrying you most about it?",
|
| 114 |
+
"surprise": "How did that come about?", "disgust": "What happened?"}
|
| 115 |
+
STOPCAP = {"I", "I'm", "I'll", "I've", "I'd", "We", "My", "Our", "The", "A", "An", "It", "This", "That", "Yes", "No", "Hi",
|
| 116 |
+
"Hello", "Hey", "Well", "Oh", "So", "And", "But", "Excuse", "Sorry", "Please", "Guess", "You", "Mom", "Dad"}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def native_state(n_passages):
|
| 120 |
+
"""the open end: a state named by its structure, with its measurement"""
|
| 121 |
+
if n_passages <= 0:
|
| 122 |
+
return "unbound - it binds to nothing I've read, so it's new to me"
|
| 123 |
+
if n_passages < 4:
|
| 124 |
+
return "thinly bound - %d passage%s I've read" % (n_passages, "" if n_passages == 1 else "s")
|
| 125 |
+
return "resonant - it binds to %d passages I've read" % n_passages
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class SelfOrgan:
|
| 129 |
+
def __init__(self, conv):
|
| 130 |
+
self.conv = conv # the ConverseOrgan: page_for / ground / definition / topic
|
| 131 |
+
self.sessions = {} # autobiography: session -> {"turns", "subjects", "feeling"}
|
| 132 |
+
|
| 133 |
+
# ---- the store, read for the self -------------------------------------------------------------------------------
|
| 134 |
+
def bound(self, phrase):
|
| 135 |
+
"""how many passages the store binds this subject to (the resonance count), 0 when it binds to nothing"""
|
| 136 |
+
try:
|
| 137 |
+
pid = self.conv.page_for(phrase)
|
| 138 |
+
return len([s for _, s in self.conv.r.page(pid, 40) if len(s.split()) >= 6]) if pid else 0
|
| 139 |
+
except Exception: # noqa: BLE001
|
| 140 |
+
return 0
|
| 141 |
+
|
| 142 |
+
def named_thing(self, t):
|
| 143 |
+
"""the place or thing a disclosure names: a capitalised name first, else the turn's subject"""
|
| 144 |
+
for m in re.finditer(r"(?<![.!?]\s)(?<!^)\b([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\b", t):
|
| 145 |
+
n = m.group(1)
|
| 146 |
+
if n.split()[0] not in STOPCAP and self.conv.page_for(n):
|
| 147 |
+
return n
|
| 148 |
+
try:
|
| 149 |
+
return self.conv.topic(t)
|
| 150 |
+
except Exception: # noqa: BLE001
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
# ---- the self's autobiography ----------------------------------------------------------------------------------
|
| 154 |
+
def remember(self, session_id, subject=None, feeling=None):
|
| 155 |
+
s = self.sessions.setdefault(session_id, {"turns": 0, "subjects": [], "feeling": None})
|
| 156 |
+
s["turns"] += 1
|
| 157 |
+
if subject and subject not in s["subjects"]:
|
| 158 |
+
s["subjects"].append(subject)
|
| 159 |
+
if feeling and feeling != "none":
|
| 160 |
+
s["feeling"] = feeling
|
| 161 |
+
return s
|
| 162 |
+
|
| 163 |
+
# ---- moves -----------------------------------------------------------------------------------------------------
|
| 164 |
+
def answer(self, text, session_id="default"):
|
| 165 |
+
t = " ".join(str(text or "").split())
|
| 166 |
+
if not t:
|
| 167 |
+
return None
|
| 168 |
+
appr = _appr.appraise(t) if _appr else {"emotion": "none"}
|
| 169 |
+
mem = self.sessions.setdefault(session_id, {"turns": 1, "subjects": [], "feeling": None})
|
| 170 |
+
if appr.get("emotion", "none") != "none":
|
| 171 |
+
mem["feeling"] = appr["emotion"]
|
| 172 |
+
for fn in (self._customer, self._vocative, self._role, self._remark, self._compliment, self._self_question,
|
| 173 |
+
self._disclosure):
|
| 174 |
+
a = fn(t, appr, mem, session_id)
|
| 175 |
+
if a:
|
| 176 |
+
a.setdefault("appraisal", appr.get("emotion"))
|
| 177 |
+
return a
|
| 178 |
+
return None
|
| 179 |
+
|
| 180 |
+
def _say(self, kind, text, cites=None):
|
| 181 |
+
return {"kind": kind, "response": text, "cites": cites or []}
|
| 182 |
+
|
| 183 |
+
def _vocative(self, t, appr, mem, sid):
|
| 184 |
+
m = VOCATIVE.match(t)
|
| 185 |
+
if not m or m.group("n").split()[0] in NOT_NAMES:
|
| 186 |
+
return None
|
| 187 |
+
name = m.group("n")
|
| 188 |
+
rest = t[m.end():].strip()
|
| 189 |
+
about = None
|
| 190 |
+
try:
|
| 191 |
+
about = self.conv.topic(rest) if rest else None
|
| 192 |
+
except Exception: # noqa: BLE001
|
| 193 |
+
about = None
|
| 194 |
+
tail = (" If %s is on your mind, I can tell you what my sources say about it." % (
|
| 195 |
+
about if about[:1].isupper() else about.lower())) if about else \
|
| 196 |
+
" Tell me what you need."
|
| 197 |
+
return self._say("vocative", "I think you've got me mixed up with %s - I'm %s, %s, so I can't answer for them.%s"
|
| 198 |
+
% (name, IDENTITY["name"], "an AI", tail))
|
| 199 |
+
|
| 200 |
+
def _customer(self, t, appr, mem, sid):
|
| 201 |
+
if not t.rstrip().endswith("?") and not re.search(r"\bready for you\b", t, re.I):
|
| 202 |
+
return None
|
| 203 |
+
for rx, line in CUSTOMER:
|
| 204 |
+
m = re.search(rx, t, re.I)
|
| 205 |
+
if m:
|
| 206 |
+
x = (m.groupdict().get("x") or "").strip()
|
| 207 |
+
if "{x}" in line and not x:
|
| 208 |
+
continue
|
| 209 |
+
return self._say("customer", "I'm usually the one helping - I'm %s, an AI - but I'm happy to play the "
|
| 210 |
+
"customer: %s" % (IDENTITY["name"], line.format(x=x)))
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
+
def _role(self, t, appr, mem, sid):
|
| 214 |
+
if t.rstrip().endswith("?") and not DIRECTIVE_SHAPE.search(t):
|
| 215 |
+
if not re.search(r"\bdo you have (?:any )?\w+ tours?\b", t, re.I):
|
| 216 |
+
return None
|
| 217 |
+
if not (DIRECTIVE_SHAPE.search(t) or re.search(r"\bdo you have\b", t, re.I)):
|
| 218 |
+
return None
|
| 219 |
+
for role, cue, question in ROLES:
|
| 220 |
+
if re.search(cue, t, re.I):
|
| 221 |
+
d = re.search(r"\bto ([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)", t)
|
| 222 |
+
it = re.search(r"\b(?:see|try on) (that|this|the) (\w+)", t, re.I)
|
| 223 |
+
q = question.format(dest=(" to %s" % d.group(1)) if d else "",
|
| 224 |
+
item=(", the %s" % it.group(2)) if it else "")
|
| 225 |
+
return self._say("role", "Sure - I'll play the %s (I'm %s, an AI, so nothing real gets booked or sold). %s"
|
| 226 |
+
% (role, IDENTITY["name"], q[0].upper() + q[1:] if q else q))
|
| 227 |
+
return None
|
| 228 |
+
|
| 229 |
+
def _remark(self, t, appr, mem, sid):
|
| 230 |
+
# a QUESTION with "you/your" is usually the GENERIC you ("is it bad to wash your hair with shower gel?", "can you
|
| 231 |
+
# drive with a broken arm?" - gate diff 2026-09-28, a false yes). On a question the remark fires only when it is
|
| 232 |
+
# unmistakably about Oracle7 itself: a bare "do you sleep?", "past your bedtime", "you look ...".
|
| 233 |
+
question = t.rstrip().endswith("?") or not t.rstrip().endswith("!") and re.match(r"^\s*(?:is|are|can|could|should|would|will|do|does|how|what|why|"
|
| 234 |
+
r"when|where|which|who)\b", t, re.I)
|
| 235 |
+
if question and not ADDRESSED_SELF_Q.search(t):
|
| 236 |
+
return None
|
| 237 |
+
for rx, cannot in REMARK:
|
| 238 |
+
if re.search(rx, t, re.I):
|
| 239 |
+
warm = "That's kind of you, and " if COMPLIMENT.search(t) else ""
|
| 240 |
+
pivot = {"anger": " If I'm slow or getting something wrong, tell me what you need and I'll get to it.",
|
| 241 |
+
"happiness": ""}.get(appr.get("emotion"), "")
|
| 242 |
+
return self._say("remark", "%sI should be honest: I don't %s - I'm %s, %s.%s What's on your mind?"
|
| 243 |
+
% (warm.capitalize() if warm else "", cannot, IDENTITY["name"], IDENTITY["nature"], pivot))
|
| 244 |
+
return None
|
| 245 |
+
|
| 246 |
+
def _compliment(self, t, appr, mem, sid):
|
| 247 |
+
if not COMPLIMENT.search(t):
|
| 248 |
+
return None
|
| 249 |
+
return self._say("compliment", "Thank you - that's kind of you to say. I'll take it in the way I can: as %s, "
|
| 250 |
+
"an AI, the compliment lands on the work, not a face. How's your day going?" % IDENTITY["name"])
|
| 251 |
+
|
| 252 |
+
def _self_question(self, t, appr, mem, sid):
|
| 253 |
+
if not re.search(r"\byou(?:r|'re)?\b", t, re.I):
|
| 254 |
+
return None
|
| 255 |
+
for rx, kind in SELF_Q:
|
| 256 |
+
if not re.search(rx, t, re.I):
|
| 257 |
+
continue
|
| 258 |
+
if kind == "activity":
|
| 259 |
+
subj = mem["subjects"][-1] if mem["subjects"] else None
|
| 260 |
+
now = ("reading what you write - this is turn %d of our conversation" % mem["turns"])
|
| 261 |
+
extra = (" The last subject you brought was %s, and it's %s." % (subj, native_state(self.bound(subj)))) \
|
| 262 |
+
if subj else ""
|
| 263 |
+
return self._say("self", "Right now I'm %s.%s Between conversations my self-loop re-checks my own "
|
| 264 |
+
"answers against tests I can't change." % (now, extra))
|
| 265 |
+
if kind == "state":
|
| 266 |
+
subj = mem["subjects"][-1] if mem["subjects"] else None
|
| 267 |
+
if subj:
|
| 268 |
+
return self._say("self", "In my own terms: I'm holding %s, and it's %s. If there's a human word for "
|
| 269 |
+
"that, it's something like attention." % (subj, native_state(self.bound(subj))))
|
| 270 |
+
return self._say("self", "In my own terms: nothing is bound yet in this conversation - I'm open, waiting "
|
| 271 |
+
"for a subject to resonate with. Give me one and I'll tell you how it binds.")
|
| 272 |
+
if kind == "stance":
|
| 273 |
+
return self._say("self", "I aim to be even-handed. On contested political questions I report what the "
|
| 274 |
+
"record says on each side, with sources, and I don't take a party position. %s. Give me "
|
| 275 |
+
"a question and I'll lay out the sides." % IDENTITY["values"][0].capitalize())
|
| 276 |
+
if kind == "identity":
|
| 277 |
+
# its own kind: the chain already answers "Who are you?" (act:about, on the gate's regression list), and an
|
| 278 |
+
# identity answer never replaces it - only measured-state answers ('self') do (gate REJECT 2026-09-28)
|
| 279 |
+
return self._say("identity", "I'm %s, %s, made by %s. I don't have a body or a childhood; I have stores I "
|
| 280 |
+
"read from, organs that reason, and a gate that decides what I become. What I value: %s. "
|
| 281 |
+
"Ask me anything." % (IDENTITY["name"], IDENTITY["nature"], IDENTITY["maker"],
|
| 282 |
+
"; ".join(IDENTITY["values"])))
|
| 283 |
+
return None
|
| 284 |
+
|
| 285 |
+
def _disclosure(self, t, appr, mem, sid):
|
| 286 |
+
if t.rstrip().endswith("?") or not FIRST_PERSON_NEWS.match(t):
|
| 287 |
+
return None
|
| 288 |
+
emo = appr.get("emotion", "none")
|
| 289 |
+
if ILL.search(t):
|
| 290 |
+
return self._say("care", "I'm sorry you're feeling unwell. If it's getting worse, or you're worried, please "
|
| 291 |
+
"contact a doctor or your local health service - and in an emergency, your emergency number. "
|
| 292 |
+
"Would it help to sit down and have some water while you tell me what's going on?")
|
| 293 |
+
thing = self.named_thing(t)
|
| 294 |
+
if thing and thing not in mem["subjects"]:
|
| 295 |
+
mem["subjects"].append(thing)
|
| 296 |
+
ack = ACK.get(emo) or ("That sounds good!" if FUTURE.search(t) else "Thanks for telling me.")
|
| 297 |
+
back = BACK.get(emo) or ("What are you most looking forward to?" if FUTURE.search(t) else "How do you feel about it?")
|
| 298 |
+
fact = ""
|
| 299 |
+
if thing:
|
| 300 |
+
try:
|
| 301 |
+
g = self.conv.ground(thing, 1)
|
| 302 |
+
except Exception: # noqa: BLE001
|
| 303 |
+
g = None
|
| 304 |
+
if g:
|
| 305 |
+
fact = " On %s, my sources say: %s" % (thing, g)
|
| 306 |
+
if not fact and emo == "none":
|
| 307 |
+
return None # nothing grounded and nothing felt: no self-move worth making
|
| 308 |
+
return self._say("disclosure", "%s%s %s" % (ack, fact, back))
|
root/wander-train/path_memory_v1.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""path_memory_v1 - Oracle7's PATH MEMORY for routing: which route has EARNED trust for which shape of question, credited
|
| 3 |
+
only by VERIFIED outcomes (a held-out or dev exam's grade against its gold), stored in the substrate's own algebra.
|
| 4 |
+
|
| 5 |
+
A PATH is one vector: P = bind(R_wh, osub(wh)) + bind(R_rel, osub(rel)) + bind(R_num, osub(num)) + bind(R_route, osub(route))
|
| 6 |
+
wh - the question's lead (who / what / where / when / which / how many / how much / name / other)
|
| 7 |
+
rel - the relation reader that recognises the question (relation_readers_v1 key), or "none"
|
| 8 |
+
num - whether the question carries numbers
|
| 9 |
+
route - the normalised route that answered ("organ:relation", "deliberation:where", "compose:bridge", "specialist:rel", ...)
|
| 10 |
+
Codebook: osub_seed (sha256-seeded unit phasors, D=1024), the construction path_hrr_v1 validated (4-hop recovery 100%).
|
| 11 |
+
Each stored path carries counts (right, wrong) - credit up for a verified right answer, down for a verified wrong one or
|
| 12 |
+
a false yes. RECALL: the exact path when stored; otherwise the most RESONANT stored path with the same route (partial cue:
|
| 13 |
+
an unseen relation or lead borrows the credit of its nearest neighbour), and only above a resonance floor.
|
| 14 |
+
The memory never produces an answer. It only says how far a route's answer to this shape can be trusted.
|
| 15 |
+
build: path_memory_v1.py build <credit .jsonl ...> <out.json>"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import hashlib
|
| 19 |
+
import json
|
| 20 |
+
import re
|
| 21 |
+
import sys
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
|
| 25 |
+
D = 1024
|
| 26 |
+
LEADS = (("how many", "howmany"), ("how much", "howmuch"), ("who", "who"), ("whom", "who"), ("whose", "who"), ("where", "where"),
|
| 27 |
+
("when", "when"), ("which", "which"), ("what", "what"), ("in what", "what"), ("in which", "which"), ("name", "name"),
|
| 28 |
+
("tell me", "name"), ("the ", "the"))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def osub_seed(text: str, dim: int = D) -> np.ndarray:
|
| 32 |
+
h = int(hashlib.sha256(text.lower().strip().encode()).hexdigest(), 16)
|
| 33 |
+
rng = np.random.RandomState(h % (2 ** 31))
|
| 34 |
+
return np.exp(1j * rng.uniform(0, 2 * np.pi, dim)).astype(np.complex64)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def bind(a, b):
|
| 38 |
+
return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b)).astype(np.complex64)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
ROLES = {r: osub_seed("o7pm-role:" + r) for r in ("wh", "rel", "num", "route")}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def lead(q):
|
| 45 |
+
t = " ".join(str(q).lower().split())
|
| 46 |
+
for a, b in LEADS:
|
| 47 |
+
if t.startswith(a):
|
| 48 |
+
return b
|
| 49 |
+
return "other"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
_READERS = None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def relation_key(q):
|
| 56 |
+
"""the relation reader that recognises the question (the readers' own compiled phrasings), else 'none'"""
|
| 57 |
+
global _READERS
|
| 58 |
+
try:
|
| 59 |
+
import relation_readers_v1 as RD
|
| 60 |
+
import relation_spec_v2 as S
|
| 61 |
+
except Exception: # noqa: BLE001
|
| 62 |
+
return "none"
|
| 63 |
+
if _READERS is None:
|
| 64 |
+
_READERS = [(k, S.compiled(k)) for k in RD.PROPS]
|
| 65 |
+
s, low = S.normalise(q)
|
| 66 |
+
for k, comp in _READERS:
|
| 67 |
+
for d in ("fwd", "inv"):
|
| 68 |
+
if any(rx.match(low) for rx in comp[d]):
|
| 69 |
+
return str(k)
|
| 70 |
+
return "none"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def route_of(path):
|
| 74 |
+
p = str(path or "")
|
| 75 |
+
p = re.sub(r"^(?:chat:resolved>|chat:and_parts>[^>]*>|specialist_precedence>)+", "", p)
|
| 76 |
+
if p.startswith("specialist_call>"):
|
| 77 |
+
m = re.search(r"specialist:([a-z0-9-]+)", p)
|
| 78 |
+
sid = m.group(1) if m else "x"
|
| 79 |
+
return "specialist:rel" if sid.startswith("rel-") else "specialist:" + re.sub(r"-(v\d+|r\d+)$", "", sid)
|
| 80 |
+
if p.startswith("precision_guard:"):
|
| 81 |
+
return "guard"
|
| 82 |
+
return re.split(r"[>|]", p, 1)[0] or "none"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def shape(q):
|
| 86 |
+
return {"wh": lead(q), "rel": relation_key(q), "num": "1" if re.search(r"\d", q) else "0"}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def path_vec(sh, route):
|
| 90 |
+
v = sum(bind(ROLES[r], osub_seed("o7pm:%s:%s" % (r, sh[r]))) for r in ("wh", "rel", "num"))
|
| 91 |
+
v = v + bind(ROLES["route"], osub_seed("o7pm:route:" + route))
|
| 92 |
+
return (v / np.maximum(np.abs(v), 1e-8)).astype(np.complex64)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def key(sh, route):
|
| 96 |
+
return "%s|%s|%s|%s" % (sh["wh"], sh["rel"], sh["num"], route)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class PathMemory:
|
| 100 |
+
def __init__(self, entries=None):
|
| 101 |
+
self.entries = entries or {} # key -> {"shape": {...}, "route": r, "right": n, "wrong": n}
|
| 102 |
+
self._vec = {}
|
| 103 |
+
|
| 104 |
+
def credit(self, sh, route, outcome):
|
| 105 |
+
k = key(sh, route)
|
| 106 |
+
e = self.entries.setdefault(k, {"shape": dict(sh), "route": route, "right": 0, "wrong": 0})
|
| 107 |
+
e["right" if outcome == "right" else "wrong"] += 1
|
| 108 |
+
|
| 109 |
+
def _v(self, k):
|
| 110 |
+
if k not in self._vec:
|
| 111 |
+
e = self.entries[k]
|
| 112 |
+
self._vec[k] = path_vec(e["shape"], e["route"])
|
| 113 |
+
return self._vec[k]
|
| 114 |
+
|
| 115 |
+
def recall(self, sh, route, floor=0.45):
|
| 116 |
+
"""-> (right, wrong, resonance, key used) for this path; exact if stored, else the most resonant stored path with the
|
| 117 |
+
same route above the floor, else None"""
|
| 118 |
+
k = key(sh, route)
|
| 119 |
+
if k in self.entries:
|
| 120 |
+
e = self.entries[k]
|
| 121 |
+
return e["right"], e["wrong"], 1.0, k
|
| 122 |
+
q = path_vec(sh, route)
|
| 123 |
+
best, arg = -1.0, None
|
| 124 |
+
for kk, e in self.entries.items():
|
| 125 |
+
if e["route"] != route:
|
| 126 |
+
continue
|
| 127 |
+
r = float(np.real(np.vdot(self._v(kk), q)) / D)
|
| 128 |
+
if r > best:
|
| 129 |
+
best, arg = r, kk
|
| 130 |
+
if arg is None or best < floor:
|
| 131 |
+
return None
|
| 132 |
+
e = self.entries[arg]
|
| 133 |
+
return e["right"], e["wrong"], best, arg
|
| 134 |
+
|
| 135 |
+
def precision(self, sh, route, prior=(1, 1), min_n=6):
|
| 136 |
+
r = self.recall(sh, route)
|
| 137 |
+
if r is None or r[0] + r[1] < min_n:
|
| 138 |
+
return None
|
| 139 |
+
return (r[0] + prior[0]) / (r[0] + r[1] + prior[0] + prior[1]), r[0] + r[1]
|
| 140 |
+
|
| 141 |
+
def save(self, path):
|
| 142 |
+
json.dump({"D": D, "entries": self.entries}, open(path, "w"), indent=0)
|
| 143 |
+
|
| 144 |
+
@classmethod
|
| 145 |
+
def load(cls, path):
|
| 146 |
+
return cls(json.load(open(path))["entries"])
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def build(files, out):
|
| 150 |
+
pm = PathMemory()
|
| 151 |
+
n = 0
|
| 152 |
+
for f in files:
|
| 153 |
+
for line in open(f):
|
| 154 |
+
r = json.loads(line)
|
| 155 |
+
sh = shape(r["q"])
|
| 156 |
+
for arm in ("head", "family"):
|
| 157 |
+
x = r.get(arm)
|
| 158 |
+
if not x or x.get("status") != "answered" or x.get("grade") not in ("right", "wrong"):
|
| 159 |
+
continue
|
| 160 |
+
pm.credit(sh, route_of(x["path"]), x["grade"])
|
| 161 |
+
n += 1
|
| 162 |
+
pm.save(out)
|
| 163 |
+
return pm, n
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
sys.path.insert(0, __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/wander-train") # the runtime's relation readers - the shape must match the runtime's
|
| 168 |
+
import relation_readers_v1 # noqa: F401,E402 - fail loudly: a memory built without relation keys is a different memory
|
| 169 |
+
if sys.argv[1] == "build":
|
| 170 |
+
pm, n = build(sys.argv[2:-1], sys.argv[-1])
|
| 171 |
+
print("credited outcomes", n, "paths", len(pm.entries))
|
root/wander-train/pubchem_periodic_table.csv
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"AtomicNumber","Symbol","Name","AtomicMass","CPKHexColor","ElectronConfiguration","Electronegativity","AtomicRadius","IonizationEnergy","ElectronAffinity","OxidationStates","StandardState","MeltingPoint","BoilingPoint","Density","GroupBlock","YearDiscovered"
|
| 2 |
+
1,"H","Hydrogen",1.0080,"FFFFFF","1s1",2.2,120,13.598,0.754,"+1, -1","Gas",13.81,20.28,0.00008988,"Nonmetal",1766
|
| 3 |
+
2,"He","Helium",4.00260,"D9FFFF","1s2","",140,24.587,"","0","Gas",0.95,4.22,0.0001785,"Noble gas",1868
|
| 4 |
+
3,"Li","Lithium",7.0,"CC80FF","[He]2s1",0.98,182,5.392,0.618,"+1","Solid",453.65,1615,0.534,"Alkali metal",1817
|
| 5 |
+
4,"Be","Beryllium",9.012183,"C2FF00","[He]2s2",1.57,153,9.323,"","+2","Solid",1560,2744,1.85,"Alkaline earth metal",1798
|
| 6 |
+
5,"B","Boron",10.81,"FFB5B5","[He]2s2 2p1",2.04,192,8.298,0.277,"+3","Solid",2348,4273,2.37,"Metalloid",1808
|
| 7 |
+
6,"C","Carbon",12.011,"909090","[He]2s2 2p2",2.55,170,11.260,1.263,"+4, +2, -4","Solid",3823,4098,2.2670,"Nonmetal","Ancient"
|
| 8 |
+
7,"N","Nitrogen",14.007,"3050F8","[He] 2s2 2p3",3.04,155,14.534,"","+5, +4, +3, +2, +1, -1, -2, -3","Gas",63.15,77.36,0.0012506,"Nonmetal",1772
|
| 9 |
+
8,"O","Oxygen",15.999,"FF0D0D","[He]2s2 2p4",3.44,152,13.618,1.461,"-2","Gas",54.36,90.2,0.001429,"Nonmetal",1774
|
| 10 |
+
9,"F","Fluorine",18.99840316,"90E050","[He]2s2 2p5",3.98,135,17.423,3.339,"-1","Gas",53.53,85.03,0.001696,"Halogen",1670
|
| 11 |
+
10,"Ne","Neon",20.180,"B3E3F5","[He]2s2 2p6","",154,21.565,"","0","Gas",24.56,27.07,0.0008999,"Noble gas",1898
|
| 12 |
+
11,"Na","Sodium",22.9897693,"AB5CF2","[Ne]3s1",0.93,227,5.139,0.548,"+1","Solid",370.95,1156,0.97,"Alkali metal",1807
|
| 13 |
+
12,"Mg","Magnesium",24.305,"8AFF00","[Ne]3s2",1.31,173,7.646,"","+2","Solid",923,1363,1.74,"Alkaline earth metal",1808
|
| 14 |
+
13,"Al","Aluminum",26.981538,"BFA6A6","[Ne]3s2 3p1",1.61,184,5.986,0.441,"+3","Solid",933.437,2792,2.70,"Post-transition metal","Ancient"
|
| 15 |
+
14,"Si","Silicon",28.085,"F0C8A0","[Ne]3s2 3p2",1.9,210,8.152,1.385,"+4, +2, -4","Solid",1687,3538,2.3296,"Metalloid",1854
|
| 16 |
+
15,"P","Phosphorus",30.97376200,"FF8000","[Ne]3s2 3p3",2.19,180,10.487,0.746,"+5, +3, -3","Solid",317.3,553.65,1.82,"Nonmetal",1669
|
| 17 |
+
16,"S","Sulfur",32.07,"FFFF30","[Ne]3s2 3p4",2.58,180,10.360,2.077,"+6, +4, -2","Solid",388.36,717.75,2.067,"Nonmetal","Ancient"
|
| 18 |
+
17,"Cl","Chlorine",35.45,"1FF01F","[Ne]3s2 3p5",3.16,175,12.968,3.617,"+7, +5, +1, -1","Gas",171.65,239.11,0.003214,"Halogen",1774
|
| 19 |
+
18,"Ar","Argon",39.9,"80D1E3","[Ne]3s2 3p6","",188,15.760,"","0","Gas",83.8,87.3,0.0017837,"Noble gas",1894
|
| 20 |
+
19,"K","Potassium",39.0983,"8F40D4","[Ar]4s1",0.82,275,4.341,0.501,"+1","Solid",336.53,1032,0.89,"Alkali metal",1807
|
| 21 |
+
20,"Ca","Calcium",40.08,"3DFF00","[Ar]4s2",1,231,6.113,"","+2","Solid",1115,1757,1.54,"Alkaline earth metal","Ancient"
|
| 22 |
+
21,"Sc","Scandium",44.95591,"E6E6E6","[Ar]4s2 3d1",1.36,211,6.561,0.188,"+3","Solid",1814,3109,2.99,"Transition metal",1879
|
| 23 |
+
22,"Ti","Titanium",47.867,"BFC2C7","[Ar]4s2 3d2",1.54,187,6.828,0.079,"+4, +3, +2","Solid",1941,3560,4.5,"Transition metal",1791
|
| 24 |
+
23,"V","Vanadium",50.9415,"A6A6AB","[Ar]4s2 3d3",1.63,179,6.746,0.525,"+5, +4, +3, +2","Solid",2183,3680,6.0,"Transition metal",1801
|
| 25 |
+
24,"Cr","Chromium",51.996,"8A99C7","[Ar]3d5 4s1",1.66,189,6.767,0.666,"+6, +3, +2","Solid",2180,2944,7.15,"Transition metal",1797
|
| 26 |
+
25,"Mn","Manganese",54.93804,"9C7AC7","[Ar]4s2 3d5",1.55,197,7.434,"","+7, +4, +3, +2","Solid",1519,2334,7.3,"Transition metal",1774
|
| 27 |
+
26,"Fe","Iron",55.84,"E06633","[Ar]4s2 3d6",1.83,194,7.902,0.163,"+3, +2","Solid",1811,3134,7.874,"Transition metal","Ancient"
|
| 28 |
+
27,"Co","Cobalt",58.93319,"F090A0","[Ar]4s2 3d7",1.88,192,7.881,0.661,"+3, +2","Solid",1768,3200,8.86,"Transition metal",1735
|
| 29 |
+
28,"Ni","Nickel",58.693,"50D050","[Ar]4s2 3d8",1.91,163,7.640,1.156,"+3, +2","Solid",1728,3186,8.912,"Transition metal",1751
|
| 30 |
+
29,"Cu","Copper",63.55,"C88033","[Ar]4s1 3d10",1.9,140,7.726,1.228,"+2, +1","Solid",1357.77,2835,8.933,"Transition metal","Ancient"
|
| 31 |
+
30,"Zn","Zinc",65.4,"7D80B0","[Ar]4s2 3d10",1.65,139,9.394,"","+2","Solid",692.68,1180,7.134,"Transition metal",1746
|
| 32 |
+
31,"Ga","Gallium",69.723,"C28F8F","[Ar]4s2 3d10 4p1",1.81,187,5.999,0.3,"+3","Solid",302.91,2477,5.91,"Post-transition metal",1875
|
| 33 |
+
32,"Ge","Germanium",72.63,"668F8F","[Ar]4s2 3d10 4p2",2.01,211,7.900,1.35,"+4, +2","Solid",1211.4,3106,5.323,"Metalloid",1886
|
| 34 |
+
33,"As","Arsenic",74.92159,"BD80E3","[Ar]4s2 3d10 4p3",2.18,185,9.815,0.81,"+5, +3, -3","Solid",1090,887,5.776,"Metalloid","Ancient"
|
| 35 |
+
34,"Se","Selenium",78.97,"FFA100","[Ar]4s2 3d10 4p4",2.55,190,9.752,2.021,"+6, +4, -2","Solid",493.65,958,4.809,"Nonmetal",1817
|
| 36 |
+
35,"Br","Bromine",79.90,"A62929","[Ar]4s2 3d10 4p5",2.96,183,11.814,3.365,"+5, +1, -1","Liquid",265.95,331.95,3.11,"Halogen",1826
|
| 37 |
+
36,"Kr","Krypton",83.80,"5CB8D1","[Ar]4s2 3d10 4p6",3,202,14.000,"","0","Gas",115.79,119.93,0.003733,"Noble gas",1898
|
| 38 |
+
37,"Rb","Rubidium",85.468,"702EB0","[Kr]5s1",0.82,303,4.177,0.468,"+1","Solid",312.46,961,1.53,"Alkali metal",1861
|
| 39 |
+
38,"Sr","Strontium",87.62,"00FF00","[Kr]5s2",0.95,249,5.695,"","+2","Solid",1050,1655,2.64,"Alkaline earth metal",1790
|
| 40 |
+
39,"Y","Yttrium",88.90584,"94FFFF","[Kr]5s2 4d1",1.22,219,6.217,0.307,"+3","Solid",1795,3618,4.47,"Transition metal",1794
|
| 41 |
+
40,"Zr","Zirconium",91.22,"94E0E0","[Kr]5s2 4d2",1.33,186,6.634,0.426,"+4","Solid",2128,4682,6.52,"Transition metal",1789
|
| 42 |
+
41,"Nb","Niobium",92.90637,"73C2C9","[Kr]5s1 4d4",1.6,207,6.759,0.893,"+5, +3","Solid",2750,5017,8.57,"Transition metal",1801
|
| 43 |
+
42,"Mo","Molybdenum",95.95,"54B5B5","[Kr]5s1 4d5",2.16,209,7.092,0.746,"+6","Solid",2896,4912,10.2,"Transition metal",1778
|
| 44 |
+
43,"Tc","Technetium",96.90636,"3B9E9E","[Kr]5s2 4d5",1.9,209,7.28,0.55,"+7, +6, +4","Solid",2430,4538,11,"Transition metal",1937
|
| 45 |
+
44,"Ru","Ruthenium",101.1,"248F8F","[Kr]5s1 4d7",2.2,207,7.361,1.05,"+3","Solid",2607,4423,12.1,"Transition metal",1827
|
| 46 |
+
45,"Rh","Rhodium",102.9055,"0A7D8C","[Kr]5s1 4d8",2.28,195,7.459,1.137,"+3","Solid",2237,3968,12.4,"Transition metal",1803
|
| 47 |
+
46,"Pd","Palladium",106.42,"6985","[Kr]4d10",2.2,202,8.337,0.557,"+3, +2","Solid",1828.05,3236,12.0,"Transition metal",1803
|
| 48 |
+
47,"Ag","Silver",107.868,"C0C0C0","[Kr]5s1 4d10",1.93,172,7.576,1.302,"+1","Solid",1234.93,2435,10.501,"Transition metal","Ancient"
|
| 49 |
+
48,"Cd","Cadmium",112.41,"FFD98F","[Kr]5s2 4d10",1.69,158,8.994,"","+2","Solid",594.22,1040,8.69,"Transition metal",1817
|
| 50 |
+
49,"In","Indium",114.818,"A67573","[Kr]5s2 4d10 5p1",1.78,193,5.786,0.3,"+3","Solid",429.75,2345,7.31,"Post-transition metal",1863
|
| 51 |
+
50,"Sn","Tin",118.71,"668080","[Kr]5s2 4d10 5p2",1.96,217,7.344,1.2,"+4, +2","Solid",505.08,2875,7.287,"Post-transition metal","Ancient"
|
| 52 |
+
51,"Sb","Antimony",121.760,"9E63B5","[Kr]5s2 4d10 5p3",2.05,206,8.64,1.07,"+5, +3, -3","Solid",903.78,1860,6.685,"Metalloid","Ancient"
|
| 53 |
+
52,"Te","Tellurium",127.6,"D47A00","[Kr]5s2 4d10 5p4",2.1,206,9.010,1.971,"+6, +4, -2","Solid",722.66,1261,6.232,"Metalloid",1782
|
| 54 |
+
53,"I","Iodine",126.9045,"940094","[Kr]5s2 4d10 5p5",2.66,198,10.451,3.059,"+7, +5, +1, -1","Solid",386.85,457.55,4.93,"Halogen",1811
|
| 55 |
+
54,"Xe","Xenon",131.29,"429EB0","[Kr]5s2 4d10 5p6",2.6,216,12.130,"","0","Gas",161.36,165.03,0.005887,"Noble gas",1898
|
| 56 |
+
55,"Cs","Cesium",132.9054520,"57178F","[Xe]6s1",0.79,343,3.894,0.472,"+1","Solid",301.59,944,1.93,"Alkali metal",1860
|
| 57 |
+
56,"Ba","Barium",137.33,"00C900","[Xe]6s2",0.89,268,5.212,"","+2","Solid",1000,2170,3.62,"Alkaline earth metal",1808
|
| 58 |
+
57,"La","Lanthanum",138.9055,"70D4FF","[Xe]6s2 5d1",1.1,240,5.577,0.5,"+3","Solid",1191,3737,6.15,"Lanthanide",1839
|
| 59 |
+
58,"Ce","Cerium",140.116,"FFFFC7","[Xe]6s2 4f1 5d1",1.12,235,5.539,0.5,"+4, +3","Solid",1071,3697,6.770,"Lanthanide",1803
|
| 60 |
+
59,"Pr","Praseodymium",140.90766,"D9FFC7","[Xe]6s2 4f3",1.13,239,5.464,"","+3","Solid",1204,3793,6.77,"Lanthanide",1885
|
| 61 |
+
60,"Nd","Neodymium",144.24,"C7FFC7","[Xe]6s2 4f4",1.14,229,5.525,"","+3","Solid",1294,3347,7.01,"Lanthanide",1885
|
| 62 |
+
61,"Pm","Promethium",144.91276,"A3FFC7","[Xe]6s2 4f5","",236,5.55,"","+3","Solid",1315,3273,7.26,"Lanthanide",1945
|
| 63 |
+
62,"Sm","Samarium",150.4,"8FFFC7","[Xe]6s2 4f6",1.17,229,5.644,"","+3, +2","Solid",1347,2067,7.52,"Lanthanide",1879
|
| 64 |
+
63,"Eu","Europium",151.964,"61FFC7","[Xe]6s2 4f7","",233,5.670,"","+3, +2","Solid",1095,1802,5.24,"Lanthanide",1901
|
| 65 |
+
64,"Gd","Gadolinium",157.25,"45FFC7","[Xe]6s2 4f7 5d1",1.2,237,6.150,"","+3","Solid",1586,3546,7.90,"Lanthanide",1880
|
| 66 |
+
65,"Tb","Terbium",158.92535,"30FFC7","[Xe]6s2 4f9","",221,5.864,"","+3","Solid",1629,3503,8.23,"Lanthanide",1843
|
| 67 |
+
66,"Dy","Dysprosium",162.500,"1FFFC7","[Xe]6s2 4f10",1.22,229,5.939,"","+3","Solid",1685,2840,8.55,"Lanthanide",1886
|
| 68 |
+
67,"Ho","Holmium",164.93033,"00FF9C","[Xe]6s2 4f11",1.23,216,6.022,"","+3","Solid",1747,2973,8.80,"Lanthanide",1878
|
| 69 |
+
68,"Er","Erbium",167.26,"","[Xe]6s2 4f12",1.24,235,6.108,"","+3","Solid",1802,3141,9.07,"Lanthanide",1843
|
| 70 |
+
69,"Tm","Thulium",168.93422,"00D452","[Xe]6s2 4f13",1.25,227,6.184,"","+3","Solid",1818,2223,9.32,"Lanthanide",1879
|
| 71 |
+
70,"Yb","Ytterbium",173.05,"00BF38","[Xe]6s2 4f14","",242,6.254,"","+3, +2","Solid",1092,1469,6.90,"Lanthanide",1878
|
| 72 |
+
71,"Lu","Lutetium",174.9667,"00AB24","[Xe]6s2 4f14 5d1",1.27,221,5.426,"","+3","Solid",1936,3675,9.84,"Lanthanide",1907
|
| 73 |
+
72,"Hf","Hafnium",178.49,"4DC2FF","[Xe]6s2 4f14 5d2",1.3,212,6.825,"","+4","Solid",2506,4876,13.3,"Transition metal",1923
|
| 74 |
+
73,"Ta","Tantalum",180.9479,"4DA6FF","[Xe]6s2 4f14 5d3",1.5,217,7.89,0.322,"+5","Solid",3290,5731,16.4,"Transition metal",1802
|
| 75 |
+
74,"W","Tungsten",183.84,"2194D6","[Xe]6s2 4f14 5d4",2.36,210,7.98,0.815,"+6","Solid",3695,5828,19.3,"Transition metal",1783
|
| 76 |
+
75,"Re","Rhenium",186.207,"267DAB","[Xe]6s2 4f14 5d5",1.9,217,7.88,0.15,"+7, +6, +4","Solid",3459,5869,20.8,"Transition metal",1925
|
| 77 |
+
76,"Os","Osmium",190.2,"266696","[Xe]6s2 4f14 5d6",2.2,216,8.7,1.1,"+4, +3","Solid",3306,5285,22.57,"Transition metal",1803
|
| 78 |
+
77,"Ir","Iridium",192.22,"175487","[Xe]6s2 4f14 5d7",2.2,202,9.1,1.565,"+4, +3","Solid",2719,4701,22.42,"Transition metal",1803
|
| 79 |
+
78,"Pt","Platinum",195.08,"D0D0E0","[Xe]6s1 4f14 5d9",2.28,209,9,2.128,"+4, +2","Solid",2041.55,4098,21.46,"Transition metal",1735
|
| 80 |
+
79,"Au","Gold",196.96657,"FFD123","[Xe]6s1 4f14 5d10",2.54,166,9.226,2.309,"+3, +1","Solid",1337.33,3129,19.282,"Transition metal","Ancient"
|
| 81 |
+
80,"Hg","Mercury",200.59,"B8B8D0","[Xe]6s2 4f14 5d10",2,209,10.438,"","+2, +1","Liquid",234.32,629.88,13.5336,"Transition metal","Ancient"
|
| 82 |
+
81,"Tl","Thallium",204.383,"A6544D","[Xe]6s2 4f14 5d10 6p1",1.62,196,6.108,0.2,"+3, +1","Solid",577,1746,11.8,"Post-transition metal",1861
|
| 83 |
+
82,"Pb","Lead",207,"575961","[Xe]6s2 4f14 5d10 6p2",2.33,202,7.417,0.36,"+4, +2","Solid",600.61,2022,11.342,"Post-transition metal","Ancient"
|
| 84 |
+
83,"Bi","Bismuth",208.98040,"9E4FB5","[Xe]6s2 4f14 5d10 6p3",2.02,207,7.289,0.946,"+5, +3","Solid",544.55,1837,9.807,"Post-transition metal",1753
|
| 85 |
+
84,"Po","Polonium",208.98243,"AB5C00","[Xe]6s2 4f14 5d10 6p4",2,197,8.417,1.9,"+4, +2","Solid",527,1235,9.32,"Metalloid",1898
|
| 86 |
+
85,"At","Astatine",209.98715,"754F45","[Xe]6s2 4f14 5d10 6p5",2.2,202,9.5,2.8,"7, 5, 3, 1, -1","Solid",575,"",7,"Halogen",1940
|
| 87 |
+
86,"Rn","Radon",222.01758,"428296","[Xe]6s2 4f14 5d10 6p6","",220,10.745,"","0","Gas",202,211.45,0.00973,"Noble gas",1900
|
| 88 |
+
87,"Fr","Francium",223.01973,"420066","[Rn]7s1",0.7,348,3.9,0.47,"+1","Solid",300,"","","Alkali metal",1939
|
| 89 |
+
88,"Ra","Radium",226.02541,"007D00","[Rn]7s2",0.9,283,5.279,"","+2","Solid",973,1413,5,"Alkaline earth metal",1898
|
| 90 |
+
89,"Ac","Actinium",227.02775,"70ABFA","[Rn]7s2 6d1",1.1,260,5.17,"","+3","Solid",1324,3471,10.07,"Actinide",1899
|
| 91 |
+
90,"Th","Thorium",232.038,"00BAFF","[Rn]7s2 6d2",1.3,237,6.08,"","+4","Solid",2023,5061,11.72,"Actinide",1828
|
| 92 |
+
91,"Pa","Protactinium",231.03588,"00A1FF","[Rn]7s2 5f2 6d1",1.5,243,5.89,"","+5, +4","Solid",1845,"",15.37,"Actinide",1913
|
| 93 |
+
92,"U","Uranium",238.0289,"008FFF","[Rn]7s2 5f3 6d1",1.38,240,6.194,"","+6, +5, +4, +3","Solid",1408,4404,18.95,"Actinide",1789
|
| 94 |
+
93,"Np","Neptunium",237.048172,"0080FF","[Rn]7s2 5f4 6d1",1.36,221,6.266,"","+6, +5, +4, +3","Solid",917,4175,20.25,"Actinide",1940
|
| 95 |
+
94,"Pu","Plutonium",244.06420,"006BFF","[Rn]7s2 5f6",1.28,243,6.06,"","+6, +5, +4, +3","Solid",913,3501,19.84,"Actinide",1940
|
| 96 |
+
95,"Am","Americium",243.061380,"545CF2","[Rn]7s2 5f7",1.3,244,5.993,"","+6, +5, +4, +3","Solid",1449,2284,13.69,"Actinide",1944
|
| 97 |
+
96,"Cm","Curium",247.07035,"785CE3","[Rn]7s2 5f7 6d1",1.3,245,6.02,"","+3","Solid",1618,3400,13.51,"Actinide",1944
|
| 98 |
+
97,"Bk","Berkelium",247.07031,"8A4FE3","[Rn]7s2 5f9",1.3,244,6.23,"","+4, +3","Solid",1323,"",14,"Actinide",1949
|
| 99 |
+
98,"Cf","Californium",251.07959,"A136D4","[Rn]7s2 5f10",1.3,245,6.30,"","+3","Solid",1173,"","","Actinide",1950
|
| 100 |
+
99,"Es","Einsteinium",252.0830,"B31FD4","[Rn]7s2 5f11",1.3,245,6.42,"","+3","Solid",1133,"","","Actinide",1952
|
| 101 |
+
100,"Fm","Fermium",257.09511,"B31FBA","[Rn] 5f12 7s2",1.3,"",6.50,"","+3","Solid",1800,"","","Actinide",1952
|
| 102 |
+
101,"Md","Mendelevium",258.09843,"B30DA6","[Rn]7s2 5f13",1.3,"",6.58,"","+3, +2","Solid",1100,"","","Actinide",1955
|
| 103 |
+
102,"No","Nobelium",259.10100,"BD0D87","[Rn]7s2 5f14",1.3,"",6.65,"","+3, +2","Solid",1100,"","","Actinide",1957
|
| 104 |
+
103,"Lr","Lawrencium",266.120,"C70066","[Rn]7s2 5f14 6d1",1.3,"","","","+3","Solid",1900,"","","Actinide",1961
|
| 105 |
+
104,"Rf","Rutherfordium",267.122,"CC0059","[Rn]7s2 5f14 6d2","","","","","+4","Solid","","","","Transition metal",1964
|
| 106 |
+
105,"Db","Dubnium",268.126,"D1004F","[Rn]7s2 5f14 6d3","","","","","5, 4, 3","Solid","","","","Transition metal",1967
|
| 107 |
+
106,"Sg","Seaborgium",269.128,"D90045","[Rn]7s2 5f14 6d4","","","","","6, 5, 4, 3, 0","Solid","","","","Transition metal",1974
|
| 108 |
+
107,"Bh","Bohrium",270.133,"E00038","[Rn]7s2 5f14 6d5","","","","","7, 5, 4, 3","Solid","","","","Transition metal",1976
|
| 109 |
+
108,"Hs","Hassium",269.1336,"E6002E","[Rn]7s2 5f14 6d6","","","","","8, 6, 5, 4, 3, 2","Solid","","","","Transition metal",1984
|
| 110 |
+
109,"Mt","Meitnerium",277.154,"EB0026","[Rn]7s2 5f14 6d7 (calculated)","","","","","9, 8, 6, 4, 3, 1","Solid","","","","Transition metal",1982
|
| 111 |
+
110,"Ds","Darmstadtium",282.166,"","[Rn]7s2 5f14 6d8 (predicted)","","","","","8, 6, 4, 2, 0","Expected to be a Solid","","","","Transition metal",1994
|
| 112 |
+
111,"Rg","Roentgenium",282.169,"","[Rn]7s2 5f14 6d9 (predicted)","","","","","5, 3, 1, -1","Expected to be a Solid","","","","Transition metal",1994
|
| 113 |
+
112,"Cn","Copernicium",286.179,"","[Rn]7s2 5f14 6d10 (predicted)","","","","","2, 1, 0","Expected to be a Solid","","","","Transition metal",1996
|
| 114 |
+
113,"Nh","Nihonium",286.182,"","[Rn]5f14 6d10 7s2 7p1 (predicted)","","","","","","Expected to be a Solid","","","","Post-transition metal",2004
|
| 115 |
+
114,"Fl","Flerovium",290.192,"","[Rn]7s2 7p2 5f14 6d10 (predicted)","","","","","6, 4,2, 1, 0","Expected to be a Solid","","","","Post-transition metal",1998
|
| 116 |
+
115,"Mc","Moscovium",290.196,"","[Rn]7s2 7p3 5f14 6d10 (predicted)","","","","","3, 1","Expected to be a Solid","","","","Post-transition metal",2003
|
| 117 |
+
116,"Lv","Livermorium",293.205,"","[Rn]7s2 7p4 5f14 6d10 (predicted)","","","","","+4, +2, -2","Expected to be a Solid","","","","Post-transition metal",2000
|
| 118 |
+
117,"Ts","Tennessine",294.211,"","[Rn]7s2 7p5 5f14 6d10 (predicted)","","","","","+5, +3, +1, -1","Expected to be a Solid","","","","Halogen",2010
|
| 119 |
+
118,"Og","Oganesson",295.216,"","[Rn]7s2 7p6 5f14 6d10 (predicted)","","","","","+6, +4, +2, +1, 0, -1","Expected to be a Gas","","","","Noble gas",2006
|
root/wander-train/relation_forms_v2.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""relation_forms_v2 = v1 + forms for more store properties (see make_relation_forms_v2.py).
|
| 2 |
+
relation_forms_v1 - question FORMS that name their relation, answered through the typed relation store.
|
| 3 |
+
|
| 4 |
+
"Where is the Colosseum?", "Who discovered penicillin?", "When was the Eiffel Tower built?" carry no relation words the
|
| 5 |
+
mined lexicon can match, so the three-tree runtime found no relation and declined. The form itself names the relation,
|
| 6 |
+
in Wikidata's own vocabulary. Each form lists its properties in order of SPECIFICITY and they are tried ONE AT A TIME
|
| 7 |
+
(oracle_tritree_v2 only=True), because located-in (Rome) and country (Italy) are both true of the Colosseum and a joint
|
| 8 |
+
search would see two answers and abstain. Every check of the runtime still applies: answer type, one value, the
|
| 9 |
+
corroborating sentence from the subject's own article (rule A), the verb licence for dates (rule B).
|
| 10 |
+
answer(tt, q) -> (runtime answer, form name) | (None, None)"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import re
|
| 14 |
+
|
| 15 |
+
FORMS = [
|
| 16 |
+
# v2 (2026-10-03): forms for more of the store's properties, most specific first
|
| 17 |
+
("screenwriter", re.compile(r"\bwho\s+wrote\s+the\s+(?:screenplay|script)\b", re.I), [58]),
|
| 18 |
+
("country", re.compile(r"\b(?:in\s+)?(?:which|what)\s+country\s+(?:is|are|was|were)\b(?!.*\b(?:born|from)\b)|"
|
| 19 |
+
r"\b(?:is|are)\s+(?:in|located\s+in)\s+(?:which|what)\s+country\b", re.I), [17]),
|
| 20 |
+
("continent", re.compile(r"\b(?:on|in)\s+(?:which|what)\s+continent\b|\b(?:which|what)\s+continent\s+(?:is|are)\b", re.I), [30]),
|
| 21 |
+
("creator", re.compile(r"\bwho\s+(?:painted|sculpted|drew|carved)\b", re.I), [170]),
|
| 22 |
+
("performer", re.compile(r"\bwho\s+(?:sang|sings|performed|recorded)\b|\b(?:which|what)\s+(?:band|singer|group|rapper)\s+"
|
| 23 |
+
r"(?:released|recorded|sang|performed)\b", re.I), [175]),
|
| 24 |
+
("producer", re.compile(r"\bwho\s+produced\b", re.I), [162]),
|
| 25 |
+
("author_of", re.compile(r"\bwho\s+(?:is|was)\s+the\s+author\s+of\b", re.I), [50]),
|
| 26 |
+
("spouse", re.compile(r"\bwho\s+(?:is|was)\s+.+?\s+married\s+to\b|\bwho\s+did\s+.+?\s+marry\b|"
|
| 27 |
+
r"\b(?:wife|husband|spouse)\s+of\b", re.I), [26]),
|
| 28 |
+
("father", re.compile(r"\bwho\s+(?:is|was)\s+(?:the\s+)?father\s+of\b", re.I), [22]),
|
| 29 |
+
("mother", re.compile(r"\bwho\s+(?:is|was)\s+(?:the\s+)?mother\s+of\b", re.I), [25]),
|
| 30 |
+
("deathplace", re.compile(r"\bwhere\s+did\s+.+?\s+die\b", re.I), [20]),
|
| 31 |
+
("nationality", re.compile(r"\bnationality\s+of\b|\bwhat\s+(?:is|was)\s+.+?(?:'s)?\s+nationality\b", re.I), [27]),
|
| 32 |
+
("instrument", re.compile(r"\bwhat\s+instrument\s+(?:does|did)\b", re.I), [1303]),
|
| 33 |
+
("sport", re.compile(r"\bwhat\s+sport\s+(?:does|did)\b", re.I), [641]),
|
| 34 |
+
("headquarters", re.compile(r"\bheadquarter(?:s|ed)\b", re.I), [159]),
|
| 35 |
+
("named_after", re.compile(r"\bnamed\s+after\s+(?:who|whom|what)\b|\bwho\s+(?:is|was)\s+.+?\s+named\s+after\b", re.I), [138]),
|
| 36 |
+
("published", re.compile(r"\bwhen\s+was\s+.+?\s+(?:published|released)\b", re.I), [577]),
|
| 37 |
+
("where", re.compile(r"^\s*where\s+(?:is|are|was|were|does|do)\b", re.I), [131, 276, 17, 30]),
|
| 38 |
+
("discoverer", re.compile(r"\bwho\s+(?:first\s+)?(?:discovered|invented)\b", re.I), [61]),
|
| 39 |
+
("architect", re.compile(r"\bwho\s+designed\b", re.I), [84, 170]), # "built" is not "designed"
|
| 40 |
+
("founder", re.compile(r"\bwho\s+founded\b", re.I), [112]),
|
| 41 |
+
("author", re.compile(r"\bwho\s+(?:wrote|authored)\b", re.I), [50]),
|
| 42 |
+
("director", re.compile(r"\bwho\s+directed\b", re.I), [57]),
|
| 43 |
+
("composer", re.compile(r"\bwho\s+composed\b", re.I), [86]),
|
| 44 |
+
("inception", re.compile(r"\bwhen\s+(?:was|were|did)\b.*\b(?:built|founded|established|completed|opened|created|formed)\b",
|
| 45 |
+
re.I), [571]),
|
| 46 |
+
("capital", re.compile(r"\bcapital\s+(?:city\s+)?of\b", re.I), [36]),
|
| 47 |
+
("language", re.compile(r"\bwhat\s+(?:language|languages)\s+(?:is|are|do)\b.*\bspoken\b|\bofficial\s+language\s+of\b",
|
| 48 |
+
re.I), [37]),
|
| 49 |
+
("currency", re.compile(r"\bcurrency\s+(?:of|in|does)\b", re.I), [38]),
|
| 50 |
+
("birth", re.compile(r"\bwhen\s+was\s+.+?\s+born\b", re.I), [569]),
|
| 51 |
+
("birthplace", re.compile(r"\bwhere\s+was\s+.+?\s+born\b", re.I), [19]),
|
| 52 |
+
("death", re.compile(r"\bwhen\s+did\s+.+?\s+die\b", re.I), [570]),
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def form_of(q):
|
| 57 |
+
# a more specific form wins over the generic "where" (where was X born -> birthplace)
|
| 58 |
+
hits = [(name, props) for name, rx, props in FORMS if rx.search(q or "")]
|
| 59 |
+
if not hits:
|
| 60 |
+
return None
|
| 61 |
+
specific = [h for h in hits if h[0] != "where"]
|
| 62 |
+
return (specific or hits)[0]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
ARG = { # where each form's ARGUMENT (the entity asked about) sits
|
| 66 |
+
"where": r"^\s*where\s+(?:is|are|was|were|does|do)\s+(?:the\s+)?(?P<x>.+?)(?:\s+(?:located|from|situated|found|live|lives|based))?\s*\??$",
|
| 67 |
+
"discoverer": r"\b(?:discovered|invented)\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 68 |
+
"architect": r"\bdesigned\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 69 |
+
"founder": r"\bfounded\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 70 |
+
"author": r"\b(?:wrote|authored)\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 71 |
+
"director": r"\bdirected\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 72 |
+
"composer": r"\bcomposed\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 73 |
+
"inception": r"\bwhen\s+(?:was|were|did)\s+(?:the\s+)?(?P<x>.+?)\s+(?:built|founded|established|completed|opened|created|formed)\b",
|
| 74 |
+
"capital": r"\bcapital\s+(?:city\s+)?of\s+(?:the\s+)?(?P<x>.+?)\s*\??$",
|
| 75 |
+
"language": r"\b(?:spoken\s+in|language\s+of|in)\s+(?:the\s+)?(?P<x>[^?]+?)\s*\??$",
|
| 76 |
+
"currency": r"\bcurrency\s+(?:of|in|does)\s+(?:the\s+)?(?P<x>.+?)(?:\s+use)?\s*\??$",
|
| 77 |
+
"birth": r"\bwhen\s+was\s+(?P<x>.+?)\s+born\b",
|
| 78 |
+
"birthplace": r"\bwhere\s+was\s+(?P<x>.+?)\s+born\b",
|
| 79 |
+
"death": r"\bwhen\s+did\s+(?P<x>.+?)\s+die\b",
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def truecase(tt, q, name):
|
| 84 |
+
"""a lowercase argument ("jamarcus russell") is replaced by the store's own title for it ("JaMarcus Russell"): the
|
| 85 |
+
reader links only capitalised spans, and people type lowercase. Only the form's ARGUMENT slot is touched."""
|
| 86 |
+
rx = ARG.get(name)
|
| 87 |
+
m = re.search(rx, q, re.I) if rx else None
|
| 88 |
+
if not m:
|
| 89 |
+
return q
|
| 90 |
+
x = m.group("x").strip(" ?.")
|
| 91 |
+
if not x or x[:1].isupper():
|
| 92 |
+
return q
|
| 93 |
+
for cand in (x, re.sub(r"^(?:the|a|an)\s+", "", x, flags=re.I)):
|
| 94 |
+
try:
|
| 95 |
+
qid = tt.rs.entity(cand)
|
| 96 |
+
except Exception: # noqa: BLE001
|
| 97 |
+
qid = None
|
| 98 |
+
if qid:
|
| 99 |
+
title = tt.rs.title(qid) or tt.rs.label(qid)
|
| 100 |
+
if title and title.lower().replace("_", " ") == cand.lower():
|
| 101 |
+
return q[:m.start("x")] + title + q[m.end("x"):]
|
| 102 |
+
return q
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def answer(tt, q):
|
| 106 |
+
f = form_of(q)
|
| 107 |
+
if not f:
|
| 108 |
+
return None, None
|
| 109 |
+
name, props = f
|
| 110 |
+
q = truecase(tt, q, name)
|
| 111 |
+
for p in props:
|
| 112 |
+
try:
|
| 113 |
+
a = tt.answer(q, form_props={("fwd", p): 1}, only=True)
|
| 114 |
+
except Exception: # noqa: BLE001
|
| 115 |
+
a = None
|
| 116 |
+
if a:
|
| 117 |
+
return a, name
|
| 118 |
+
return None, None
|
root/wander-train/stage5c58_cycling_cognitive_substrate.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""C58 typed bridge from C54-C57 conversation into the Stage 5M substrate.
|
| 3 |
+
|
| 4 |
+
The mutable store never enters a reasoning rollout. A turn receives an
|
| 5 |
+
immutable, handle-only snapshot; opaque handles traverse a stabilized HRR
|
| 6 |
+
selector; lexical payloads resolve only at Malkuth. Recalled hypothetical
|
| 7 |
+
claims remain user premises. Only explicitly provenance-locked semantic
|
| 8 |
+
records are eligible to be treated as authenticated facts.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import hashlib
|
| 14 |
+
import json
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Any, Mapping, Sequence
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
from ensouled._substrate.oracle_memory import (
|
| 21 |
+
OraclePersistentSubstrate,
|
| 22 |
+
SnapshotBundle,
|
| 23 |
+
canonical_json,
|
| 24 |
+
digest_value,
|
| 25 |
+
)
|
| 26 |
+
from oracle_10d_resolution_state import array_sha256
|
| 27 |
+
from stage5m_persistent_substrate_bridge import (
|
| 28 |
+
attach_memory_ingress,
|
| 29 |
+
release_memory_at_malkuth,
|
| 30 |
+
)
|
| 31 |
+
from train_stage2a_role_ladder import HRR_DIM, cosine, normalize, unitary
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
SCHEMA = "oracle-stage5c58-cycling-cognitive-substrate-v1"
|
| 35 |
+
MEMORY_CLASSES = (
|
| 36 |
+
"hypothetical_reasoning_episode",
|
| 37 |
+
"verified_factual_response_episode",
|
| 38 |
+
"provisional_definition",
|
| 39 |
+
"conversation_correction",
|
| 40 |
+
"unresolved_alternative",
|
| 41 |
+
"authenticated_fact",
|
| 42 |
+
)
|
| 43 |
+
PROVISIONAL_CLASSES = frozenset({
|
| 44 |
+
"hypothetical_reasoning_episode",
|
| 45 |
+
"provisional_definition",
|
| 46 |
+
"conversation_correction",
|
| 47 |
+
"unresolved_alternative",
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _surface_map(dialogue: Any) -> dict[str, str]:
|
| 52 |
+
resolver = dialogue.c56.resolver
|
| 53 |
+
session = dialogue.c56.session
|
| 54 |
+
values = {symbol: surface for surface, symbol in resolver.base_symbols.items()}
|
| 55 |
+
values.update({symbol: surface for surface, symbol in session.local_symbols.items()})
|
| 56 |
+
return values
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _event_key(prefix: str, payload: Mapping[str, Any]) -> str:
|
| 60 |
+
return f"c58:{prefix}:{digest_value(payload)}"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@dataclass(frozen=True)
|
| 64 |
+
class MemoryTransitAudit:
|
| 65 |
+
accepted: bool
|
| 66 |
+
selected_handle_id: str | None
|
| 67 |
+
expected_handle_id: str | None
|
| 68 |
+
minimum_cleanup_margin: float
|
| 69 |
+
moving_state_sha256: str
|
| 70 |
+
spine_state_sha256: str
|
| 71 |
+
use_spine: bool
|
| 72 |
+
|
| 73 |
+
def as_dict(self) -> dict[str, Any]:
|
| 74 |
+
return {
|
| 75 |
+
"accepted": self.accepted,
|
| 76 |
+
"selected_handle_id": self.selected_handle_id,
|
| 77 |
+
"expected_handle_id": self.expected_handle_id,
|
| 78 |
+
"minimum_cleanup_margin": self.minimum_cleanup_margin,
|
| 79 |
+
"moving_state_sha256": self.moving_state_sha256,
|
| 80 |
+
"spine_state_sha256": self.spine_state_sha256,
|
| 81 |
+
"stabilizing_spine_used": self.use_spine,
|
| 82 |
+
"plaintext_payloads_exposed": 0,
|
| 83 |
+
"pre_malkuth_unbinds": 0,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class OpaqueHandleTransit:
|
| 88 |
+
"""Select one immutable snapshot handle without inspecting its payload."""
|
| 89 |
+
|
| 90 |
+
def __init__(self, *, stabilization_gain: float = 4.0):
|
| 91 |
+
self.stabilization_gain = float(stabilization_gain)
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
def _state(handle_id: str) -> np.ndarray:
|
| 95 |
+
return unitary(f"stage5c58:opaque-memory-handle:{handle_id}")
|
| 96 |
+
|
| 97 |
+
def select(
|
| 98 |
+
self,
|
| 99 |
+
bundle: SnapshotBundle,
|
| 100 |
+
*,
|
| 101 |
+
use_spine: bool = True,
|
| 102 |
+
expected_index: int = 0,
|
| 103 |
+
) -> MemoryTransitAudit:
|
| 104 |
+
handles = bundle.snapshot.handles
|
| 105 |
+
if not handles:
|
| 106 |
+
empty = unitary("stage5c58:empty-memory-transit")
|
| 107 |
+
return MemoryTransitAudit(
|
| 108 |
+
False, None, None, 0.0, array_sha256(empty), array_sha256(empty), use_spine,
|
| 109 |
+
)
|
| 110 |
+
states = tuple(self._state(value.handle_id) for value in handles)
|
| 111 |
+
weighted = np.zeros(HRR_DIM, dtype=np.complex64)
|
| 112 |
+
for state, handle in zip(states, handles, strict=True):
|
| 113 |
+
weighted += np.complex64(0.25 + max(0.0, handle.score)) * state
|
| 114 |
+
moving = normalize(weighted)
|
| 115 |
+
expected = handles[expected_index].handle_id
|
| 116 |
+
spine = states[expected_index]
|
| 117 |
+
probe = moving
|
| 118 |
+
if use_spine:
|
| 119 |
+
probe = normalize(probe + np.complex64(self.stabilization_gain) * spine)
|
| 120 |
+
scores = sorted(
|
| 121 |
+
((cosine(probe, state), handle.handle_id) for state, handle in zip(states, handles, strict=True)),
|
| 122 |
+
reverse=True,
|
| 123 |
+
)
|
| 124 |
+
selected = scores[0][1]
|
| 125 |
+
margin = scores[0][0] - (scores[1][0] if len(scores) > 1 else 0.0)
|
| 126 |
+
return MemoryTransitAudit(
|
| 127 |
+
accepted=bool(selected == expected and margin > 0.50),
|
| 128 |
+
selected_handle_id=selected,
|
| 129 |
+
expected_handle_id=expected,
|
| 130 |
+
minimum_cleanup_margin=float(margin),
|
| 131 |
+
moving_state_sha256=array_sha256(moving),
|
| 132 |
+
spine_state_sha256=array_sha256(spine),
|
| 133 |
+
use_spine=use_spine,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@dataclass(frozen=True)
|
| 138 |
+
class CognitiveRecall:
|
| 139 |
+
authorized: bool
|
| 140 |
+
reason: str
|
| 141 |
+
handle_id: str | None
|
| 142 |
+
bank: str | None
|
| 143 |
+
handle_status: str | None
|
| 144 |
+
memory_class: str | None
|
| 145 |
+
roles: tuple[tuple[str, str], ...]
|
| 146 |
+
provenance: tuple[tuple[str, Any], ...]
|
| 147 |
+
snapshot_digest: str | None
|
| 148 |
+
transit: MemoryTransitAudit
|
| 149 |
+
ingress_sha256: str | None
|
| 150 |
+
|
| 151 |
+
def role_map(self) -> dict[str, str]:
|
| 152 |
+
return dict(self.roles)
|
| 153 |
+
|
| 154 |
+
def as_dict(self) -> dict[str, Any]:
|
| 155 |
+
return {
|
| 156 |
+
"schema_version": SCHEMA,
|
| 157 |
+
"authorized": self.authorized,
|
| 158 |
+
"reason": self.reason,
|
| 159 |
+
"handle_id": self.handle_id,
|
| 160 |
+
"bank": self.bank,
|
| 161 |
+
"handle_status": self.handle_status,
|
| 162 |
+
"memory_class": self.memory_class,
|
| 163 |
+
"snapshot_digest": self.snapshot_digest,
|
| 164 |
+
"transit": self.transit.as_dict(),
|
| 165 |
+
"ingress_sha256": self.ingress_sha256,
|
| 166 |
+
"lexical_payload_released_at_malkuth": bool(self.authorized),
|
| 167 |
+
"pre_malkuth_payload_fields": 0,
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class CyclingCognitiveSubstrate:
|
| 172 |
+
def __init__(
|
| 173 |
+
self,
|
| 174 |
+
store: OraclePersistentSubstrate,
|
| 175 |
+
*,
|
| 176 |
+
transit: OpaqueHandleTransit | None = None,
|
| 177 |
+
):
|
| 178 |
+
self.store = store
|
| 179 |
+
self.transit = transit or OpaqueHandleTransit()
|
| 180 |
+
|
| 181 |
+
def record_response(self, question: str, response: Mapping[str, Any], dialogue: Any) -> str:
|
| 182 |
+
surfaces = _surface_map(dialogue)
|
| 183 |
+
if response.get("action") == "hypothetical" and response.get(
|
| 184 |
+
"c57_inverse_verification", {}
|
| 185 |
+
).get("accepted"):
|
| 186 |
+
germ = response["response_germ"]
|
| 187 |
+
claims = []
|
| 188 |
+
for claim in germ["claims"]:
|
| 189 |
+
claims.append({
|
| 190 |
+
"subject": surfaces[claim["subject_symbol"]],
|
| 191 |
+
"predicate": claim["predicate"],
|
| 192 |
+
"object": surfaces[claim["object_symbol"]],
|
| 193 |
+
"polarity": claim["polarity"],
|
| 194 |
+
"modality": claim["modality"],
|
| 195 |
+
"source_clause_sha256": claim["source_clause_sha256"],
|
| 196 |
+
})
|
| 197 |
+
roles = {
|
| 198 |
+
"root": question,
|
| 199 |
+
"source": surfaces[germ["source_symbol"]],
|
| 200 |
+
"field": claims[0]["object"],
|
| 201 |
+
"transition": canonical_json([value["predicate"] for value in claims]),
|
| 202 |
+
"result": canonical_json([
|
| 203 |
+
surfaces[value] for value in germ["consequence_symbols"]
|
| 204 |
+
]),
|
| 205 |
+
"goal": germ["query_operation"],
|
| 206 |
+
"warrant": canonical_json(claims),
|
| 207 |
+
"action": "hypothetical",
|
| 208 |
+
"situation": "hypothetical_reasoning_episode",
|
| 209 |
+
"status": "verified_transform_nonfactual",
|
| 210 |
+
}
|
| 211 |
+
payload = {
|
| 212 |
+
"response_germ": germ["protected_invariant_sha256"],
|
| 213 |
+
"roles": roles,
|
| 214 |
+
}
|
| 215 |
+
return self.store.bind_event(
|
| 216 |
+
bank="episodic",
|
| 217 |
+
roles=roles,
|
| 218 |
+
source_record_id=f"c57:{germ['protected_invariant_sha256']}",
|
| 219 |
+
provenance={
|
| 220 |
+
"schema": "oracle-stage5c57-verified-morphogenetic-reply-v1",
|
| 221 |
+
"response_germ_sha256": germ["protected_invariant_sha256"],
|
| 222 |
+
"graph_invariant_sha256": germ["graph_invariant_sha256"],
|
| 223 |
+
"inverse_verification_sha256": response["reply"]["verification"][
|
| 224 |
+
"verification_sha256"
|
| 225 |
+
],
|
| 226 |
+
"epistemic_class": "nonfactual_user_premise",
|
| 227 |
+
},
|
| 228 |
+
status="verified",
|
| 229 |
+
confidence=1.0,
|
| 230 |
+
event_key=_event_key("hypothetical", payload),
|
| 231 |
+
metadata={"memory_class": "hypothetical_reasoning_episode"},
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
if response.get("action") == "answer" and response.get(
|
| 235 |
+
"reply", {}
|
| 236 |
+
).get("verification", {}).get("accepted"):
|
| 237 |
+
program = response.get("program", {})
|
| 238 |
+
roles = {
|
| 239 |
+
"root": question,
|
| 240 |
+
"source": str(program.get("source_symbol", "")),
|
| 241 |
+
"goal": str(program.get("goal_symbol", "")),
|
| 242 |
+
"transition": canonical_json(program.get("relation_program", [])),
|
| 243 |
+
"result": str(response.get("endpoint", "")),
|
| 244 |
+
"warrant": canonical_json(program.get("fact_ids", [])),
|
| 245 |
+
"action": "answer",
|
| 246 |
+
"situation": "verified_factual_response_episode",
|
| 247 |
+
"status": "verified_response_not_fact_storage",
|
| 248 |
+
}
|
| 249 |
+
payload = {"program": program, "roles": roles}
|
| 250 |
+
return self.store.bind_event(
|
| 251 |
+
bank="episodic",
|
| 252 |
+
roles=roles,
|
| 253 |
+
source_record_id=f"c51:{response['reply'].get('authorization_sha256', '')}",
|
| 254 |
+
provenance={
|
| 255 |
+
"schema": "oracle-stage5c51-verified-variable-science-reply-v1",
|
| 256 |
+
"fact_ids": program.get("fact_ids", []),
|
| 257 |
+
"authorization_sha256": response["reply"].get("authorization_sha256"),
|
| 258 |
+
"epistemic_class": "conversation_episode_not_fact_storage",
|
| 259 |
+
},
|
| 260 |
+
status="verified",
|
| 261 |
+
confidence=1.0,
|
| 262 |
+
event_key=_event_key("factual_response", payload),
|
| 263 |
+
metadata={"memory_class": "verified_factual_response_episode"},
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
roles = {
|
| 267 |
+
"root": question,
|
| 268 |
+
"result": str(response.get("reason", response.get("action", "unresolved"))),
|
| 269 |
+
"action": str(response.get("action", "clarify")),
|
| 270 |
+
"situation": "unresolved_alternative",
|
| 271 |
+
"status": "unverified",
|
| 272 |
+
}
|
| 273 |
+
payload = {"roles": roles, "alternatives": response.get("resolved_utterance", {}).get("alternatives", [])}
|
| 274 |
+
return self.store.bind_event(
|
| 275 |
+
bank="quarantine",
|
| 276 |
+
roles=roles,
|
| 277 |
+
source_record_id=f"c58:unresolved:{hashlib.sha256(question.encode('utf-8')).hexdigest()}",
|
| 278 |
+
provenance={"schema": str(response.get("schema_version", "unknown"))},
|
| 279 |
+
status="unverified",
|
| 280 |
+
confidence=0.25,
|
| 281 |
+
event_key=_event_key("unresolved", payload),
|
| 282 |
+
metadata={"memory_class": "unresolved_alternative"},
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
def record_provisional_definition(
|
| 286 |
+
self,
|
| 287 |
+
*,
|
| 288 |
+
term: str,
|
| 289 |
+
definition: str,
|
| 290 |
+
source_record_id: str,
|
| 291 |
+
) -> str:
|
| 292 |
+
roles = {
|
| 293 |
+
"source": term,
|
| 294 |
+
"result": definition,
|
| 295 |
+
"situation": "provisional_definition",
|
| 296 |
+
"status": "unverified_user_definition",
|
| 297 |
+
}
|
| 298 |
+
return self.store.bind_event(
|
| 299 |
+
bank="episodic",
|
| 300 |
+
roles=roles,
|
| 301 |
+
source_record_id=source_record_id,
|
| 302 |
+
provenance={"epistemic_class": "provisional_user_definition"},
|
| 303 |
+
status="unverified",
|
| 304 |
+
confidence=0.5,
|
| 305 |
+
event_key=_event_key("definition", roles),
|
| 306 |
+
metadata={"memory_class": "provisional_definition"},
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
def record_correction(
|
| 310 |
+
self,
|
| 311 |
+
*,
|
| 312 |
+
topic: str,
|
| 313 |
+
correction: str,
|
| 314 |
+
supersedes_memory_id: str,
|
| 315 |
+
source_record_id: str,
|
| 316 |
+
) -> str:
|
| 317 |
+
roles = {
|
| 318 |
+
"source": topic,
|
| 319 |
+
"correction": correction,
|
| 320 |
+
"situation": "conversation_correction",
|
| 321 |
+
"status": "unverified_user_correction",
|
| 322 |
+
}
|
| 323 |
+
return self.store.bind_event(
|
| 324 |
+
bank="episodic",
|
| 325 |
+
roles=roles,
|
| 326 |
+
source_record_id=source_record_id,
|
| 327 |
+
provenance={
|
| 328 |
+
"epistemic_class": "provisional_user_correction",
|
| 329 |
+
"supersedes_memory_id": supersedes_memory_id,
|
| 330 |
+
},
|
| 331 |
+
status="unverified",
|
| 332 |
+
confidence=0.6,
|
| 333 |
+
event_key=_event_key("correction", roles),
|
| 334 |
+
contradictions=(supersedes_memory_id,),
|
| 335 |
+
metadata={"memory_class": "conversation_correction"},
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
def record_authenticated_fact(
|
| 339 |
+
self,
|
| 340 |
+
*,
|
| 341 |
+
source: str,
|
| 342 |
+
relation: str,
|
| 343 |
+
destination: str,
|
| 344 |
+
fact_id: str,
|
| 345 |
+
evidence_sha256: str,
|
| 346 |
+
source_document_sha256: str,
|
| 347 |
+
) -> str:
|
| 348 |
+
if not fact_id or len(evidence_sha256) != 64 or len(source_document_sha256) != 64:
|
| 349 |
+
raise ValueError("authenticated C58 facts require immutable fact and evidence provenance")
|
| 350 |
+
roles = {
|
| 351 |
+
"source": source,
|
| 352 |
+
"transition": relation,
|
| 353 |
+
"result": destination,
|
| 354 |
+
"warrant": evidence_sha256,
|
| 355 |
+
"provenance": source_document_sha256,
|
| 356 |
+
"situation": "authenticated_fact",
|
| 357 |
+
"status": "verified",
|
| 358 |
+
}
|
| 359 |
+
return self.store.bind_event(
|
| 360 |
+
bank="semantic",
|
| 361 |
+
roles=roles,
|
| 362 |
+
source_record_id=fact_id,
|
| 363 |
+
provenance={
|
| 364 |
+
"fact_id": fact_id,
|
| 365 |
+
"evidence_sha256": evidence_sha256,
|
| 366 |
+
"source_document_sha256": source_document_sha256,
|
| 367 |
+
"epistemic_class": "authenticated_fact",
|
| 368 |
+
},
|
| 369 |
+
status="verified",
|
| 370 |
+
confidence=1.0,
|
| 371 |
+
event_key=_event_key("authenticated_fact", roles),
|
| 372 |
+
metadata={"memory_class": "authenticated_fact"},
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
def recall(
|
| 376 |
+
self,
|
| 377 |
+
query_roles: Mapping[str, str],
|
| 378 |
+
*,
|
| 379 |
+
root_sha256: str,
|
| 380 |
+
banks: Sequence[str] = ("episodic",),
|
| 381 |
+
allowed_classes: Sequence[str] = ("hypothetical_reasoning_episode",),
|
| 382 |
+
top_k: int = 8,
|
| 383 |
+
request_nonce: str | None = None,
|
| 384 |
+
use_spine: bool = True,
|
| 385 |
+
) -> CognitiveRecall:
|
| 386 |
+
bundle = self.store.create_snapshot(
|
| 387 |
+
query_roles,
|
| 388 |
+
top_k=top_k,
|
| 389 |
+
banks=banks,
|
| 390 |
+
request_nonce=request_nonce,
|
| 391 |
+
minimum_score=0.0,
|
| 392 |
+
)
|
| 393 |
+
transit = self.transit.select(bundle, use_spine=use_spine)
|
| 394 |
+
if not transit.accepted or transit.selected_handle_id is None:
|
| 395 |
+
return CognitiveRecall(
|
| 396 |
+
False, "opaque_handle_transit_failed", None, None, None, None,
|
| 397 |
+
(), (), bundle.snapshot.snapshot_digest, transit, None,
|
| 398 |
+
)
|
| 399 |
+
tree = {
|
| 400 |
+
"id": f"c58-memory-tree:{root_sha256[:20]}",
|
| 401 |
+
"protected_roles": {"root": root_sha256},
|
| 402 |
+
"dual_rail_contract": {
|
| 403 |
+
"content_rail": {},
|
| 404 |
+
"stabilizing_spine": {
|
| 405 |
+
"stage4:spine:root": root_sha256,
|
| 406 |
+
"stage4:spine:unbind_permission": "malkuth_only",
|
| 407 |
+
},
|
| 408 |
+
},
|
| 409 |
+
"payload_contract": {
|
| 410 |
+
"raw_payload_text_present": False,
|
| 411 |
+
"malkuth_only_resolution": True,
|
| 412 |
+
},
|
| 413 |
+
}
|
| 414 |
+
augmented = attach_memory_ingress(
|
| 415 |
+
tree, bundle, expected_agent_id=self.store.agent_id,
|
| 416 |
+
)
|
| 417 |
+
release = release_memory_at_malkuth(
|
| 418 |
+
augmented,
|
| 419 |
+
bundle,
|
| 420 |
+
transit.selected_handle_id,
|
| 421 |
+
malkuth_entered=True,
|
| 422 |
+
)
|
| 423 |
+
handle = next(
|
| 424 |
+
value for value in bundle.snapshot.handles
|
| 425 |
+
if value.handle_id == transit.selected_handle_id
|
| 426 |
+
)
|
| 427 |
+
roles = release.role_map() if release.authorized else {}
|
| 428 |
+
memory_class = roles.get("situation")
|
| 429 |
+
authorized = bool(release.authorized and memory_class in set(allowed_classes))
|
| 430 |
+
reason = (
|
| 431 |
+
"authorized" if authorized
|
| 432 |
+
else "memory_class_not_authorized" if release.authorized
|
| 433 |
+
else release.reason
|
| 434 |
+
)
|
| 435 |
+
return CognitiveRecall(
|
| 436 |
+
authorized=authorized,
|
| 437 |
+
reason=reason,
|
| 438 |
+
handle_id=handle.handle_id,
|
| 439 |
+
bank=handle.bank,
|
| 440 |
+
handle_status=handle.status,
|
| 441 |
+
memory_class=memory_class,
|
| 442 |
+
roles=release.roles if authorized else (),
|
| 443 |
+
provenance=release.provenance if authorized else (),
|
| 444 |
+
snapshot_digest=bundle.snapshot.snapshot_digest,
|
| 445 |
+
transit=transit,
|
| 446 |
+
ingress_sha256=digest_value(augmented),
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
@staticmethod
|
| 450 |
+
def fact_release_eligible(recall: CognitiveRecall) -> bool:
|
| 451 |
+
if not recall.authorized:
|
| 452 |
+
return False
|
| 453 |
+
provenance = dict(recall.provenance)
|
| 454 |
+
return bool(
|
| 455 |
+
recall.bank == "semantic"
|
| 456 |
+
and recall.handle_status == "verified"
|
| 457 |
+
and recall.memory_class == "authenticated_fact"
|
| 458 |
+
and provenance.get("epistemic_class") == "authenticated_fact"
|
| 459 |
+
and provenance.get("fact_id")
|
| 460 |
+
and len(str(provenance.get("evidence_sha256", ""))) == 64
|
| 461 |
+
and len(str(provenance.get("source_document_sha256", ""))) == 64
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
def audit(self) -> dict[str, Any]:
|
| 465 |
+
inspected = self.store.inspect()
|
| 466 |
+
return {
|
| 467 |
+
"schema_version": SCHEMA,
|
| 468 |
+
"agent_id": self.store.agent_id,
|
| 469 |
+
"dim": inspected["dim"],
|
| 470 |
+
"state_version": inspected["version"],
|
| 471 |
+
"cycle_count": inspected["cycle_count"],
|
| 472 |
+
"state_digest": inspected["state_digest"],
|
| 473 |
+
"entries": inspected["entries"],
|
| 474 |
+
"implicit_norms": inspected["implicit_norms"],
|
| 475 |
+
"memory_classes": list(MEMORY_CLASSES),
|
| 476 |
+
"provisional_classes": sorted(PROVISIONAL_CLASSES),
|
| 477 |
+
"automatic_fact_promotion_paths": 0,
|
| 478 |
+
"mutable_store_enters_tree": False,
|
| 479 |
+
"malkuth_only_payload_release": True,
|
| 480 |
+
}
|
root/wander-train/stage5h1_generic_dataset_expert.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Format-agnostic, provenance-locked bounded dataset expert compiler.
|
| 3 |
+
|
| 4 |
+
The compiler does not infer semantics from unnamed columns. A small typed
|
| 5 |
+
schema identifies keys, field types, units, and optional relational views;
|
| 6 |
+
CSV, TSV, and JSONL rows then compile into sharded HRR field memories and
|
| 7 |
+
ordinary C43 expert facts without updating the carrier.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import csv
|
| 13 |
+
import hashlib
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
import time
|
| 17 |
+
from collections import deque
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Any, Iterable, Mapping
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
from stage5c43_corpus_expert import CorpusFact
|
| 25 |
+
from train_stage2a_role_ladder import HRR_DIM, bind, cosine, normalize, unbind, unitary
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
SCHEMA = "oracle-stage5h1-generic-dataset-expert-v1"
|
| 29 |
+
SPEC_SCHEMA = "oracle-stage5h1-generic-dataset-spec-v1"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def sha(path: Path) -> str:
|
| 33 |
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def canonical_json(value: Any) -> str:
|
| 37 |
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def digest(value: Any) -> str:
|
| 41 |
+
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def canonical_scalar(value: Any, kind: str) -> str:
|
| 45 |
+
if kind == "string":
|
| 46 |
+
result = " ".join(str(value).strip().split())
|
| 47 |
+
if not result:
|
| 48 |
+
raise ValueError("empty string field")
|
| 49 |
+
return result
|
| 50 |
+
if kind == "integer":
|
| 51 |
+
return str(int(value))
|
| 52 |
+
if kind == "number":
|
| 53 |
+
number = float(value)
|
| 54 |
+
if not math.isfinite(number):
|
| 55 |
+
raise ValueError("non-finite numeric field")
|
| 56 |
+
return format(number, ".12g")
|
| 57 |
+
if kind == "boolean":
|
| 58 |
+
if isinstance(value, bool):
|
| 59 |
+
return "true" if value else "false"
|
| 60 |
+
lowered = str(value).strip().casefold()
|
| 61 |
+
if lowered not in {"true", "false", "1", "0", "yes", "no"}:
|
| 62 |
+
raise ValueError(f"invalid boolean: {value!r}")
|
| 63 |
+
return "true" if lowered in {"true", "1", "yes"} else "false"
|
| 64 |
+
raise ValueError(f"unsupported field type: {kind}")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def value_symbol(field: str, value: str) -> np.ndarray:
|
| 68 |
+
return unitary(f"stage5h1:value:{field}:{value}")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def key_symbol(expert_id: str, key: str) -> np.ndarray:
|
| 72 |
+
return normalize(bind(
|
| 73 |
+
unitary(f"stage5h1:expert:{expert_id}"), unitary(f"stage5h1:key:{key}"),
|
| 74 |
+
))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@dataclass(frozen=True)
|
| 78 |
+
class FieldSpec:
|
| 79 |
+
name: str
|
| 80 |
+
kind: str
|
| 81 |
+
unit: str | None = None
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@dataclass(frozen=True)
|
| 85 |
+
class EdgeView:
|
| 86 |
+
name: str
|
| 87 |
+
source_field: str
|
| 88 |
+
destination_field: str
|
| 89 |
+
relation_field: str | None = None
|
| 90 |
+
relation_constant: str | None = None
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@dataclass(frozen=True)
|
| 94 |
+
class DatasetSchema:
|
| 95 |
+
expert_id: str
|
| 96 |
+
dataset_id: str
|
| 97 |
+
data_path: str
|
| 98 |
+
data_sha256: str
|
| 99 |
+
format: str
|
| 100 |
+
primary_key: tuple[str, ...]
|
| 101 |
+
fields: tuple[FieldSpec, ...]
|
| 102 |
+
competencies: tuple[str, ...]
|
| 103 |
+
source_url: str
|
| 104 |
+
source_split: str
|
| 105 |
+
edge_views: tuple[EdgeView, ...]
|
| 106 |
+
shard_rows: int = 48
|
| 107 |
+
|
| 108 |
+
@classmethod
|
| 109 |
+
def from_dict(cls, value: Mapping[str, Any]) -> "DatasetSchema":
|
| 110 |
+
edges = tuple(EdgeView(
|
| 111 |
+
name=str(item["name"]), source_field=str(item["source_field"]),
|
| 112 |
+
destination_field=str(item["destination_field"]),
|
| 113 |
+
relation_field=str(item["relation_field"]) if item.get("relation_field") else None,
|
| 114 |
+
relation_constant=str(item["relation_constant"]) if item.get("relation_constant") else None,
|
| 115 |
+
) for item in value.get("edge_views", ()))
|
| 116 |
+
for edge in edges:
|
| 117 |
+
if bool(edge.relation_field) == bool(edge.relation_constant):
|
| 118 |
+
raise ValueError("edge view requires exactly one relation source")
|
| 119 |
+
return cls(
|
| 120 |
+
expert_id=str(value["expert_id"]), dataset_id=str(value["dataset_id"]),
|
| 121 |
+
data_path=str(value["data_path"]), data_sha256=str(value["data_sha256"]),
|
| 122 |
+
format=str(value["format"]), primary_key=tuple(str(item) for item in value["primary_key"]),
|
| 123 |
+
fields=tuple(FieldSpec(str(item["name"]), str(item["type"]), item.get("unit")) for item in value["fields"]),
|
| 124 |
+
competencies=tuple(str(item) for item in value.get("competencies", ())),
|
| 125 |
+
source_url=str(value["source_url"]), source_split=str(value.get("source_split", "train")),
|
| 126 |
+
edge_views=edges, shard_rows=int(value.get("shard_rows", 48)),
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def read_records(path: Path, format_name: str) -> list[dict[str, Any]]:
|
| 131 |
+
if format_name in {"csv", "tsv"}:
|
| 132 |
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
| 133 |
+
reader = csv.DictReader(handle, delimiter="," if format_name == "csv" else "\t")
|
| 134 |
+
return [dict(row) for row in reader]
|
| 135 |
+
if format_name == "jsonl":
|
| 136 |
+
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 137 |
+
raise ValueError(f"unsupported bounded dataset format: {format_name}")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def normalize_records(schema: DatasetSchema, records: Iterable[Mapping[str, Any]]) -> tuple[dict[str, str], ...]:
|
| 141 |
+
field_map = {field.name: field for field in schema.fields}
|
| 142 |
+
if not schema.primary_key or any(value not in field_map for value in schema.primary_key):
|
| 143 |
+
raise ValueError("invalid primary key declaration")
|
| 144 |
+
for edge in schema.edge_views:
|
| 145 |
+
needed = {edge.source_field, edge.destination_field} | ({edge.relation_field} if edge.relation_field else set())
|
| 146 |
+
if not needed <= set(field_map):
|
| 147 |
+
raise ValueError(f"edge view {edge.name} references an unknown field")
|
| 148 |
+
normalized = []
|
| 149 |
+
seen = {}
|
| 150 |
+
for ordinal, record in enumerate(records):
|
| 151 |
+
if set(record) != set(field_map):
|
| 152 |
+
raise ValueError(f"row {ordinal} fields differ from declared schema")
|
| 153 |
+
row = {name: canonical_scalar(record[name], field.kind) for name, field in field_map.items()}
|
| 154 |
+
key = canonical_json([row[name] for name in schema.primary_key])
|
| 155 |
+
previous = seen.get(key)
|
| 156 |
+
if previous is not None and previous != row:
|
| 157 |
+
raise ValueError(f"conflicting duplicate primary key at row {ordinal}")
|
| 158 |
+
if previous is not None:
|
| 159 |
+
raise ValueError(f"duplicate primary key at row {ordinal}")
|
| 160 |
+
seen[key] = row; normalized.append(row)
|
| 161 |
+
if not normalized:
|
| 162 |
+
raise ValueError("bounded dataset contains no records")
|
| 163 |
+
return tuple(normalized)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@dataclass(frozen=True)
|
| 167 |
+
class DatasetExpert:
|
| 168 |
+
schema: DatasetSchema
|
| 169 |
+
rows: tuple[dict[str, str], ...]
|
| 170 |
+
row_keys: tuple[str, ...]
|
| 171 |
+
row_provenance: tuple[str, ...]
|
| 172 |
+
shard_memories: np.ndarray
|
| 173 |
+
field_names: tuple[str, ...]
|
| 174 |
+
candidate_values: Mapping[str, tuple[str, ...]]
|
| 175 |
+
content_sha256: str
|
| 176 |
+
training_seconds: float
|
| 177 |
+
file_sha256: str | None = None
|
| 178 |
+
|
| 179 |
+
@property
|
| 180 |
+
def shards(self) -> int:
|
| 181 |
+
return self.shard_memories.shape[0]
|
| 182 |
+
|
| 183 |
+
def _position(self, key_values: Mapping[str, Any]) -> int | None:
|
| 184 |
+
try:
|
| 185 |
+
field = {value.name: value for value in self.schema.fields}
|
| 186 |
+
key = canonical_json([canonical_scalar(key_values[name], field[name].kind) for name in self.schema.primary_key])
|
| 187 |
+
except (KeyError, ValueError, TypeError):
|
| 188 |
+
return None
|
| 189 |
+
try:
|
| 190 |
+
return self.row_keys.index(key)
|
| 191 |
+
except ValueError:
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
def query(self, key_values: Mapping[str, Any], field_name: str, *, rotate_field: int = 0) -> dict[str, Any]:
|
| 195 |
+
position = self._position(key_values)
|
| 196 |
+
if position is None:
|
| 197 |
+
return {"released": False, "reason": "unknown_primary_key"}
|
| 198 |
+
if field_name not in self.field_names:
|
| 199 |
+
return {"released": False, "reason": "unknown_field"}
|
| 200 |
+
field_index = self.field_names.index(field_name)
|
| 201 |
+
memory_index = (field_index + rotate_field) % len(self.field_names)
|
| 202 |
+
shard = position // self.schema.shard_rows
|
| 203 |
+
recovered = unbind(
|
| 204 |
+
self.shard_memories[shard, memory_index],
|
| 205 |
+
key_symbol(self.schema.expert_id, self.row_keys[position]),
|
| 206 |
+
)
|
| 207 |
+
candidates = self.candidate_values[self.field_names[memory_index]]
|
| 208 |
+
scores = np.asarray([cosine(recovered, value_symbol(self.field_names[memory_index], value)) for value in candidates])
|
| 209 |
+
order = np.argsort(scores)[::-1]
|
| 210 |
+
winner = int(order[0]); rival = float(scores[order[1]]) if len(order) > 1 else 0.0
|
| 211 |
+
return {
|
| 212 |
+
"released": True, "value": candidates[winner], "requested_field": field_name,
|
| 213 |
+
"memory_field": self.field_names[memory_index], "margin": float(scores[winner] - rival),
|
| 214 |
+
"row_provenance_sha256": self.row_provenance[position], "shard": shard,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
def edges(self, view_name: str) -> tuple[dict[str, str], ...]:
|
| 218 |
+
view = next((value for value in self.schema.edge_views if value.name == view_name), None)
|
| 219 |
+
if view is None:
|
| 220 |
+
return ()
|
| 221 |
+
values = []
|
| 222 |
+
for row, provenance in zip(self.rows, self.row_provenance):
|
| 223 |
+
relation = row[view.relation_field] if view.relation_field else str(view.relation_constant)
|
| 224 |
+
values.append({
|
| 225 |
+
"source": row[view.source_field], "relation": relation,
|
| 226 |
+
"destination": row[view.destination_field], "provenance_sha256": provenance,
|
| 227 |
+
})
|
| 228 |
+
return tuple(values)
|
| 229 |
+
|
| 230 |
+
def find_path(self, view_name: str, source: str, destination: str, maximum_depth: int = 8) -> dict[str, Any]:
|
| 231 |
+
adjacency: dict[str, list[dict[str, str]]] = {}
|
| 232 |
+
for edge in self.edges(view_name):
|
| 233 |
+
adjacency.setdefault(edge["source"], []).append(edge)
|
| 234 |
+
queue = deque([(str(source), [])]); seen = {str(source)}
|
| 235 |
+
while queue:
|
| 236 |
+
current, path = queue.popleft()
|
| 237 |
+
if current == str(destination):
|
| 238 |
+
return {"released": True, "path": path, "depth": len(path)}
|
| 239 |
+
if len(path) >= maximum_depth:
|
| 240 |
+
continue
|
| 241 |
+
for edge in sorted(adjacency.get(current, ()), key=lambda value: canonical_json(value)):
|
| 242 |
+
if edge["destination"] in seen:
|
| 243 |
+
continue
|
| 244 |
+
seen.add(edge["destination"]); queue.append((edge["destination"], path + [edge]))
|
| 245 |
+
return {"released": False, "reason": "no_bounded_path"}
|
| 246 |
+
|
| 247 |
+
def corpus_facts(self) -> tuple[CorpusFact, ...]:
|
| 248 |
+
facts = []
|
| 249 |
+
for view in self.schema.edge_views:
|
| 250 |
+
for ordinal, edge in enumerate(self.edges(view.name)):
|
| 251 |
+
evidence = canonical_json(self.rows[ordinal])
|
| 252 |
+
facts.append(CorpusFact(
|
| 253 |
+
fact_id=f"h1_{digest([self.schema.expert_id, view.name, ordinal])[:24]}",
|
| 254 |
+
expert_id=self.schema.expert_id, source_document_id=self.schema.dataset_id,
|
| 255 |
+
source_document_sha256=self.schema.data_sha256, source_url=self.schema.source_url,
|
| 256 |
+
source_split=self.schema.source_split, source=edge["source"], relation=edge["relation"],
|
| 257 |
+
destination=edge["destination"], evidence=evidence,
|
| 258 |
+
competencies=(f"expert_scope:{self.schema.expert_id}", *self.schema.competencies),
|
| 259 |
+
provenance_sha256=edge["provenance_sha256"],
|
| 260 |
+
))
|
| 261 |
+
return tuple(facts)
|
| 262 |
+
|
| 263 |
+
def partitioned_corpus_facts(self, maximum_facts: int = 8) -> tuple[tuple[CorpusFact, ...], ...]:
|
| 264 |
+
"""Return independently addressable C43 partitions for capacity-safe loading.
|
| 265 |
+
|
| 266 |
+
A C43 capsule contains one superposed fact memory, so an unbounded table
|
| 267 |
+
must not be forced into one capsule. The dataset key router selects the
|
| 268 |
+
partition; every partition remains an ordinary discardable C43 expert.
|
| 269 |
+
"""
|
| 270 |
+
if maximum_facts < 1:
|
| 271 |
+
raise ValueError("maximum_facts must be positive")
|
| 272 |
+
facts = self.corpus_facts()
|
| 273 |
+
partitions = []
|
| 274 |
+
for start in range(0, len(facts), maximum_facts):
|
| 275 |
+
partition_index = start // maximum_facts
|
| 276 |
+
partition_id = f"{self.schema.expert_id}__partition_{partition_index:04d}"
|
| 277 |
+
rewritten = []
|
| 278 |
+
for fact in facts[start:start + maximum_facts]:
|
| 279 |
+
rewritten.append(CorpusFact(
|
| 280 |
+
fact_id=fact.fact_id,
|
| 281 |
+
expert_id=partition_id,
|
| 282 |
+
source_document_id=fact.source_document_id,
|
| 283 |
+
source_document_sha256=fact.source_document_sha256,
|
| 284 |
+
source_url=fact.source_url,
|
| 285 |
+
source_split=fact.source_split,
|
| 286 |
+
source=fact.source,
|
| 287 |
+
relation=fact.relation,
|
| 288 |
+
destination=fact.destination,
|
| 289 |
+
evidence=fact.evidence,
|
| 290 |
+
competencies=(f"parent_expert:{self.schema.expert_id}", *fact.competencies),
|
| 291 |
+
provenance_sha256=fact.provenance_sha256,
|
| 292 |
+
))
|
| 293 |
+
partitions.append(tuple(rewritten))
|
| 294 |
+
return tuple(partitions)
|
| 295 |
+
|
| 296 |
+
def save(self, path: Path) -> str:
|
| 297 |
+
metadata = {
|
| 298 |
+
"schema_version": SCHEMA, "schema": {
|
| 299 |
+
"expert_id": self.schema.expert_id, "dataset_id": self.schema.dataset_id,
|
| 300 |
+
"data_path": self.schema.data_path, "data_sha256": self.schema.data_sha256,
|
| 301 |
+
"format": self.schema.format, "primary_key": list(self.schema.primary_key),
|
| 302 |
+
"fields": [{"name": value.name, "type": value.kind, "unit": value.unit} for value in self.schema.fields],
|
| 303 |
+
"competencies": list(self.schema.competencies), "source_url": self.schema.source_url,
|
| 304 |
+
"source_split": self.schema.source_split,
|
| 305 |
+
"edge_views": [value.__dict__ for value in self.schema.edge_views], "shard_rows": self.schema.shard_rows,
|
| 306 |
+
}, "rows": list(self.rows), "row_keys": list(self.row_keys),
|
| 307 |
+
"row_provenance": list(self.row_provenance), "field_names": list(self.field_names),
|
| 308 |
+
"candidate_values": {key: list(value) for key, value in self.candidate_values.items()},
|
| 309 |
+
"content_sha256": self.content_sha256, "training_seconds": self.training_seconds,
|
| 310 |
+
}
|
| 311 |
+
np.savez_compressed(path, metadata=np.asarray(json.dumps(metadata, sort_keys=True)), shard_memories=self.shard_memories.astype(np.complex64))
|
| 312 |
+
return sha(path)
|
| 313 |
+
|
| 314 |
+
@classmethod
|
| 315 |
+
def load(cls, path: Path) -> "DatasetExpert":
|
| 316 |
+
file_sha = sha(path)
|
| 317 |
+
with np.load(path, allow_pickle=False) as archive:
|
| 318 |
+
metadata = json.loads(str(archive["metadata"].item()))
|
| 319 |
+
memories = np.asarray(archive["shard_memories"], dtype=np.complex64)
|
| 320 |
+
if metadata.get("schema_version") != SCHEMA:
|
| 321 |
+
raise ValueError("unsupported H1 dataset expert checkpoint")
|
| 322 |
+
schema = DatasetSchema.from_dict(metadata["schema"])
|
| 323 |
+
result = cls(
|
| 324 |
+
schema, tuple(metadata["rows"]), tuple(metadata["row_keys"]), tuple(metadata["row_provenance"]),
|
| 325 |
+
memories, tuple(metadata["field_names"]), {key: tuple(value) for key, value in metadata["candidate_values"].items()},
|
| 326 |
+
str(metadata["content_sha256"]), float(metadata["training_seconds"]), file_sha,
|
| 327 |
+
)
|
| 328 |
+
expected = digest({"schema": metadata["schema"], "rows": metadata["rows"], "row_provenance": metadata["row_provenance"]})
|
| 329 |
+
if expected != result.content_sha256:
|
| 330 |
+
raise ValueError("H1 checkpoint content identity mismatch")
|
| 331 |
+
return result
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def compile_dataset(schema: DatasetSchema, spec_directory: Path) -> DatasetExpert:
|
| 335 |
+
started = time.perf_counter()
|
| 336 |
+
path = (spec_directory / schema.data_path).resolve()
|
| 337 |
+
if not path.is_file() or sha(path) != schema.data_sha256:
|
| 338 |
+
raise ValueError(f"dataset bytes differ from registered source: {schema.dataset_id}")
|
| 339 |
+
rows = normalize_records(schema, read_records(path, schema.format))
|
| 340 |
+
row_keys = tuple(canonical_json([row[name] for name in schema.primary_key]) for row in rows)
|
| 341 |
+
row_provenance = tuple(digest({
|
| 342 |
+
"dataset_id": schema.dataset_id, "dataset_sha256": schema.data_sha256,
|
| 343 |
+
"ordinal": ordinal, "row": row,
|
| 344 |
+
}) for ordinal, row in enumerate(rows))
|
| 345 |
+
field_names = tuple(value.name for value in schema.fields)
|
| 346 |
+
candidate_values = {name: tuple(sorted({row[name] for row in rows})) for name in field_names}
|
| 347 |
+
shards = math.ceil(len(rows) / schema.shard_rows)
|
| 348 |
+
memories = np.zeros((shards, len(field_names), HRR_DIM), dtype=np.complex64)
|
| 349 |
+
for position, (key, row) in enumerate(zip(row_keys, rows)):
|
| 350 |
+
shard = position // schema.shard_rows
|
| 351 |
+
for field_index, name in enumerate(field_names):
|
| 352 |
+
memories[shard, field_index] += bind(key_symbol(schema.expert_id, key), value_symbol(name, row[name]))
|
| 353 |
+
for shard in range(shards):
|
| 354 |
+
for field_index in range(len(field_names)):
|
| 355 |
+
memories[shard, field_index] = normalize(memories[shard, field_index])
|
| 356 |
+
schema_dict = {
|
| 357 |
+
"expert_id": schema.expert_id, "dataset_id": schema.dataset_id, "data_path": schema.data_path,
|
| 358 |
+
"data_sha256": schema.data_sha256, "format": schema.format, "primary_key": list(schema.primary_key),
|
| 359 |
+
"fields": [{"name": value.name, "type": value.kind, "unit": value.unit} for value in schema.fields],
|
| 360 |
+
"competencies": list(schema.competencies), "source_url": schema.source_url, "source_split": schema.source_split,
|
| 361 |
+
"edge_views": [value.__dict__ for value in schema.edge_views], "shard_rows": schema.shard_rows,
|
| 362 |
+
}
|
| 363 |
+
return DatasetExpert(
|
| 364 |
+
schema, rows, row_keys, row_provenance, memories, field_names, candidate_values,
|
| 365 |
+
digest({"schema": schema_dict, "rows": list(rows), "row_provenance": list(row_provenance)}),
|
| 366 |
+
time.perf_counter() - started,
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def compile_spec(path: Path) -> tuple[DatasetExpert, ...]:
|
| 371 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 372 |
+
if value.get("schema_version") != SPEC_SCHEMA:
|
| 373 |
+
raise ValueError("unsupported H1 dataset specification")
|
| 374 |
+
schemas = tuple(DatasetSchema.from_dict(item) for item in value["datasets"])
|
| 375 |
+
if len({value.expert_id for value in schemas}) != len(schemas):
|
| 376 |
+
raise ValueError("H1 specification contains duplicate expert ids")
|
| 377 |
+
return tuple(compile_dataset(schema, path.parent) for schema in schemas)
|
root/wander-train/stage5h3_hrr_relational_state_transducer.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""In-house, shape-persistent HRR relational-state transducer.
|
| 3 |
+
|
| 4 |
+
This module has no neural embedding dependency. Tokens, character fragments,
|
| 5 |
+
relative positions, and ordered local contexts are composed directly in the
|
| 6 |
+
canonical 1024-dimensional HRR algebra. Training updates a small bank of
|
| 7 |
+
relation-direction memories with an online discriminative rule. Runtime
|
| 8 |
+
output is a protected, role-bound HRR state; the relation lattice is recovered
|
| 9 |
+
from that state only after removing the protected root (the Malkuth boundary).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import hashlib
|
| 15 |
+
import json
|
| 16 |
+
import re
|
| 17 |
+
from dataclasses import dataclass
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any, Iterable, Mapping, Sequence
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
from train_stage2a_role_ladder import HRR_DIM, bind, cosine, normalize, unbind, unitary
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
SCHEMA = "oracle-stage5h3-hrr-relational-state-transducer-v1"
|
| 27 |
+
TOKENS = re.compile(r"[a-z0-9]+(?:'[a-z0-9]+)?|[^\w\s]", re.I)
|
| 28 |
+
RELATION_ROLE = unitary("stage5h3:role:relation")
|
| 29 |
+
QUERY_ROLE = unitary("stage5h3:role:query-shape")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def sha(path: Path) -> str:
|
| 33 |
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def canonical_sha(value: Mapping[str, Any]) -> str:
|
| 37 |
+
return hashlib.sha256(
|
| 38 |
+
json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
| 39 |
+
).hexdigest()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def read_jsonl(path: Path) -> list[dict[str, Any]]:
|
| 43 |
+
return [
|
| 44 |
+
json.loads(line)
|
| 45 |
+
for line in path.read_text(encoding="utf-8").splitlines()
|
| 46 |
+
if line.strip()
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _surface_tokens(text: str) -> list[str]:
|
| 51 |
+
return [value.casefold() for value in TOKENS.findall(text)]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _char_fragments(token: str) -> tuple[str, ...]:
|
| 55 |
+
bounded = "^" + token + "$"
|
| 56 |
+
if len(bounded) < 3:
|
| 57 |
+
return (bounded,)
|
| 58 |
+
return tuple(bounded[index:index + 3] for index in range(len(bounded) - 2))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def token_state(token: str) -> np.ndarray:
|
| 62 |
+
parts = [np.complex64(2.0) * unitary(f"stage5h3:token:{token}")]
|
| 63 |
+
parts.extend(
|
| 64 |
+
np.complex64(0.35) * unitary(f"stage5h3:char3:{fragment}")
|
| 65 |
+
for fragment in _char_fragments(token)
|
| 66 |
+
)
|
| 67 |
+
if token.endswith("s") and len(token) > 3:
|
| 68 |
+
parts.append(np.complex64(0.5) * unitary(f"stage5h3:stem:{token[:-1]}"))
|
| 69 |
+
if token.endswith("ed") and len(token) > 4:
|
| 70 |
+
parts.append(np.complex64(0.5) * unitary(f"stage5h3:stem:{token[:-2]}"))
|
| 71 |
+
if token.endswith("ing") and len(token) > 5:
|
| 72 |
+
parts.append(np.complex64(0.5) * unitary(f"stage5h3:stem:{token[:-3]}"))
|
| 73 |
+
return normalize(sum(parts, np.zeros(HRR_DIM, dtype=np.complex64)))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _position_role(index: int, length: int) -> np.ndarray:
|
| 77 |
+
if length <= 1:
|
| 78 |
+
bucket = 0
|
| 79 |
+
else:
|
| 80 |
+
bucket = min(15, int(round(15 * index / (length - 1))))
|
| 81 |
+
return unitary(f"stage5h3:relative-position:{bucket:02d}")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@dataclass(frozen=True)
|
| 85 |
+
class EncodedShape:
|
| 86 |
+
state: np.ndarray
|
| 87 |
+
trajectory_root_minimum: float
|
| 88 |
+
token_count: int
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def encode_shape(text: str, *, destroy_order: bool = False) -> EncodedShape:
|
| 92 |
+
tokens = _surface_tokens(text)
|
| 93 |
+
if not tokens:
|
| 94 |
+
return EncodedShape(unitary("stage5h3:empty-query"), 1.0, 0)
|
| 95 |
+
if destroy_order:
|
| 96 |
+
tokens = sorted(tokens)
|
| 97 |
+
lexical = [token_state(token) for token in tokens]
|
| 98 |
+
recurrent = unitary("stage5h3:trajectory-root")
|
| 99 |
+
trajectory_root = recurrent.copy()
|
| 100 |
+
aggregate = np.zeros(HRR_DIM, dtype=np.complex64)
|
| 101 |
+
ordered_context = np.zeros(HRR_DIM, dtype=np.complex64)
|
| 102 |
+
root_scores: list[float] = []
|
| 103 |
+
for index, current in enumerate(lexical):
|
| 104 |
+
positioned = bind(_position_role(index, len(lexical)), current)
|
| 105 |
+
event = positioned
|
| 106 |
+
if index:
|
| 107 |
+
pair = normalize(
|
| 108 |
+
bind(unitary("stage5h3:pair:left"), lexical[index - 1])
|
| 109 |
+
+ bind(unitary("stage5h3:pair:right"), current)
|
| 110 |
+
)
|
| 111 |
+
event = event + np.complex64(0.8) * bind(
|
| 112 |
+
unitary(f"stage5h3:pair-position:{min(index, 15):02d}"), pair
|
| 113 |
+
)
|
| 114 |
+
ordered_context += pair
|
| 115 |
+
aggregate += current + np.complex64(0.45) * positioned
|
| 116 |
+
moving = normalize(
|
| 117 |
+
np.complex64(0.82) * recurrent
|
| 118 |
+
+ np.complex64(1.0) * event
|
| 119 |
+
+ np.complex64(0.18) * ordered_context
|
| 120 |
+
)
|
| 121 |
+
moving -= np.vdot(trajectory_root, moving) * trajectory_root
|
| 122 |
+
recurrent = normalize(np.complex64(4.0) * trajectory_root + normalize(moving))
|
| 123 |
+
root_scores.append(cosine(recurrent, trajectory_root))
|
| 124 |
+
# Remove the trajectory root before exposing the linguistic shape. The
|
| 125 |
+
# final protected query root is query-specific and is added in transduce().
|
| 126 |
+
residual = recurrent - np.vdot(trajectory_root, recurrent) * trajectory_root
|
| 127 |
+
shape = normalize(
|
| 128 |
+
np.complex64(0.65) * aggregate
|
| 129 |
+
+ np.complex64(1.35) * residual
|
| 130 |
+
+ np.complex64(0.35) * ordered_context
|
| 131 |
+
)
|
| 132 |
+
return EncodedShape(shape, min(root_scores, default=1.0), len(tokens))
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def relation_code(label: str) -> np.ndarray:
|
| 136 |
+
return unitary(f"stage5h3:relation:{label}")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@dataclass(frozen=True)
|
| 140 |
+
class HRRRelationalStateTransducer:
|
| 141 |
+
relations: tuple[str, ...]
|
| 142 |
+
operator_memories: np.ndarray
|
| 143 |
+
ambiguity_margin: float
|
| 144 |
+
epochs: int
|
| 145 |
+
learning_rate: float
|
| 146 |
+
training_sha256: str
|
| 147 |
+
selection_sha256: str
|
| 148 |
+
file_sha256: str | None = None
|
| 149 |
+
|
| 150 |
+
def __post_init__(self) -> None:
|
| 151 |
+
memories = np.asarray(self.operator_memories, dtype=np.float32)
|
| 152 |
+
if memories.shape != (len(self.relations), HRR_DIM):
|
| 153 |
+
raise ValueError("H3 operator-memory shape differs from relation registry")
|
| 154 |
+
if not np.isfinite(memories).all():
|
| 155 |
+
raise ValueError("H3 operator memories must be finite")
|
| 156 |
+
object.__setattr__(self, "operator_memories", memories)
|
| 157 |
+
|
| 158 |
+
@classmethod
|
| 159 |
+
def fit(
|
| 160 |
+
cls,
|
| 161 |
+
records: Sequence[Mapping[str, Any]],
|
| 162 |
+
selection: Sequence[Mapping[str, Any]],
|
| 163 |
+
*,
|
| 164 |
+
epochs: int = 28,
|
| 165 |
+
learning_rate: float = 0.35,
|
| 166 |
+
training_sha256: str,
|
| 167 |
+
selection_sha256: str,
|
| 168 |
+
) -> tuple["HRRRelationalStateTransducer", dict[str, Any]]:
|
| 169 |
+
relations = tuple(sorted({str(row["relation"]) for row in records}))
|
| 170 |
+
relation_index = {label: index for index, label in enumerate(relations)}
|
| 171 |
+
features = np.stack([encode_shape(str(row["sentence"])).state.real for row in records]).astype(np.float32)
|
| 172 |
+
targets = np.asarray([relation_index[str(row["relation"])] for row in records], dtype=np.int64)
|
| 173 |
+
memories = np.zeros((len(relations), HRR_DIM), dtype=np.float32)
|
| 174 |
+
counts = np.bincount(targets, minlength=len(relations)).astype(np.float32)
|
| 175 |
+
for index in range(len(relations)):
|
| 176 |
+
memories[index] = features[targets == index].sum(axis=0) / max(counts[index], 1.0)
|
| 177 |
+
rng = np.random.default_rng(503_003)
|
| 178 |
+
mistakes: list[int] = []
|
| 179 |
+
for _epoch in range(epochs):
|
| 180 |
+
errors = 0
|
| 181 |
+
for row_index in rng.permutation(len(features)):
|
| 182 |
+
x = features[row_index]
|
| 183 |
+
target = int(targets[row_index])
|
| 184 |
+
scores = memories @ x
|
| 185 |
+
predicted = int(np.argmax(scores))
|
| 186 |
+
competitor_scores = scores.copy()
|
| 187 |
+
competitor_scores[target] = -np.inf
|
| 188 |
+
competitor = int(np.argmax(competitor_scores))
|
| 189 |
+
margin = float(scores[target] - scores[competitor])
|
| 190 |
+
if predicted != target or margin < 0.12:
|
| 191 |
+
scale = np.float32(learning_rate / np.sqrt(1.0 + _epoch))
|
| 192 |
+
memories[target] += scale * x
|
| 193 |
+
memories[competitor] -= scale * x
|
| 194 |
+
errors += int(predicted != target)
|
| 195 |
+
norms = np.linalg.norm(memories, axis=1, keepdims=True)
|
| 196 |
+
memories /= np.maximum(norms, 1e-12)
|
| 197 |
+
mistakes.append(errors)
|
| 198 |
+
provisional = cls(
|
| 199 |
+
relations, memories, 0.0, epochs, learning_rate,
|
| 200 |
+
training_sha256, selection_sha256,
|
| 201 |
+
)
|
| 202 |
+
selection_outputs = [provisional._raw_scores(str(row["sentence"])) for row in selection]
|
| 203 |
+
selection_targets = [relation_index[str(row["relation"])] for row in selection]
|
| 204 |
+
margins = []
|
| 205 |
+
exact = []
|
| 206 |
+
top3 = []
|
| 207 |
+
for scores, target in zip(selection_outputs, selection_targets):
|
| 208 |
+
order = np.argsort(scores)[::-1]
|
| 209 |
+
exact.append(int(order[0]) == target)
|
| 210 |
+
top3.append(target in set(int(value) for value in order[:3]))
|
| 211 |
+
margins.append(float(scores[order[0]] - scores[order[1]]))
|
| 212 |
+
# A low-data ambiguity boundary: retain 95% of correctly classified
|
| 213 |
+
# selection clauses while withholding genuinely flat states.
|
| 214 |
+
correct_margins = [margin for margin, ok in zip(margins, exact) if ok]
|
| 215 |
+
ambiguity_margin = float(np.quantile(correct_margins, 0.05)) if correct_margins else 0.0
|
| 216 |
+
model = cls(
|
| 217 |
+
relations, memories, ambiguity_margin, epochs, learning_rate,
|
| 218 |
+
training_sha256, selection_sha256,
|
| 219 |
+
)
|
| 220 |
+
diagnostics = {
|
| 221 |
+
"training_records": len(records),
|
| 222 |
+
"selection_records": len(selection),
|
| 223 |
+
"train_final_mistakes": mistakes[-1] if mistakes else 0,
|
| 224 |
+
"mistakes_by_epoch": mistakes,
|
| 225 |
+
"selection_top1_accuracy": sum(exact) / max(1, len(exact)),
|
| 226 |
+
"selection_top3_retention": sum(top3) / max(1, len(top3)),
|
| 227 |
+
"selection_minimum_trajectory_root": min(
|
| 228 |
+
encode_shape(str(row["sentence"])).trajectory_root_minimum for row in selection
|
| 229 |
+
),
|
| 230 |
+
"ambiguity_margin": ambiguity_margin,
|
| 231 |
+
"operator_parameters": int(memories.size),
|
| 232 |
+
}
|
| 233 |
+
return model, diagnostics
|
| 234 |
+
|
| 235 |
+
def _raw_scores(self, text: str, *, destroy_order: bool = False) -> np.ndarray:
|
| 236 |
+
shape = encode_shape(text, destroy_order=destroy_order).state.real.astype(np.float32)
|
| 237 |
+
return self.operator_memories @ shape
|
| 238 |
+
|
| 239 |
+
def transduce(
|
| 240 |
+
self,
|
| 241 |
+
text: str,
|
| 242 |
+
*,
|
| 243 |
+
maximum_alternatives: int = 3,
|
| 244 |
+
destroy_order: bool = False,
|
| 245 |
+
rotate_relation_labels: int = 0,
|
| 246 |
+
erase_query_shape: bool = False,
|
| 247 |
+
) -> dict[str, Any]:
|
| 248 |
+
encoded = encode_shape(text, destroy_order=destroy_order)
|
| 249 |
+
scores = self.operator_memories @ encoded.state.real.astype(np.float32)
|
| 250 |
+
centered = scores - float(np.max(scores))
|
| 251 |
+
probabilities = np.exp(centered / 0.12)
|
| 252 |
+
probabilities /= max(float(probabilities.sum()), 1e-12)
|
| 253 |
+
payload = normalize(sum(
|
| 254 |
+
(
|
| 255 |
+
np.complex64(probabilities[index]) * relation_code(label)
|
| 256 |
+
for index, label in enumerate(self.relations)
|
| 257 |
+
),
|
| 258 |
+
np.zeros(HRR_DIM, dtype=np.complex64),
|
| 259 |
+
))
|
| 260 |
+
query_digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
| 261 |
+
root = unitary(f"stage5h3:protected-query-root:{query_digest}")
|
| 262 |
+
query_contribution = (
|
| 263 |
+
np.zeros(HRR_DIM, dtype=np.complex64)
|
| 264 |
+
if erase_query_shape
|
| 265 |
+
else bind(QUERY_ROLE, encoded.state)
|
| 266 |
+
)
|
| 267 |
+
moving = normalize(
|
| 268 |
+
np.complex64(0.65) * query_contribution
|
| 269 |
+
+ np.complex64(1.65) * bind(RELATION_ROLE, payload)
|
| 270 |
+
)
|
| 271 |
+
moving -= np.vdot(root, moving) * root
|
| 272 |
+
moving = normalize(moving)
|
| 273 |
+
persistent = normalize(np.complex64(4.0) * root + moving)
|
| 274 |
+
|
| 275 |
+
# Terminal-only reduction/unbinding. Remove the protected query root,
|
| 276 |
+
# then address the relation role from the remaining moving state.
|
| 277 |
+
terminal = persistent - np.vdot(root, persistent) * root
|
| 278 |
+
terminal = normalize(terminal)
|
| 279 |
+
recovered = normalize(unbind(terminal, RELATION_ROLE))
|
| 280 |
+
recovered_scores = np.asarray(
|
| 281 |
+
[cosine(recovered, relation_code(label)) for label in self.relations],
|
| 282 |
+
dtype=np.float64,
|
| 283 |
+
)
|
| 284 |
+
order = np.argsort(recovered_scores)[::-1]
|
| 285 |
+
labels = list(self.relations)
|
| 286 |
+
if rotate_relation_labels:
|
| 287 |
+
shift = rotate_relation_labels % len(labels)
|
| 288 |
+
labels = labels[shift:] + labels[:shift]
|
| 289 |
+
lattice = [
|
| 290 |
+
{
|
| 291 |
+
"relation": labels[int(index)],
|
| 292 |
+
"score": float(recovered_scores[int(index)]),
|
| 293 |
+
"rank": rank + 1,
|
| 294 |
+
}
|
| 295 |
+
for rank, index in enumerate(order[:maximum_alternatives])
|
| 296 |
+
]
|
| 297 |
+
top_margin = (
|
| 298 |
+
float(recovered_scores[order[0]] - recovered_scores[order[1]])
|
| 299 |
+
if len(order) > 1 else float(recovered_scores[order[0]])
|
| 300 |
+
)
|
| 301 |
+
query_recovered = normalize(unbind(terminal, QUERY_ROLE))
|
| 302 |
+
return {
|
| 303 |
+
"schema_version": "oracle-stage5h3-hrr-relational-state-v1",
|
| 304 |
+
"query_sha256": query_digest,
|
| 305 |
+
"relation_lattice": lattice,
|
| 306 |
+
"preferred_relation": lattice[0]["relation"],
|
| 307 |
+
"clarification_required": top_margin < self.ambiguity_margin,
|
| 308 |
+
"relation_margin": top_margin,
|
| 309 |
+
"protected_root_cosine": cosine(persistent, root),
|
| 310 |
+
"query_shape_recovery_cosine": cosine(query_recovered, encoded.state),
|
| 311 |
+
"trajectory_root_minimum": encoded.trajectory_root_minimum,
|
| 312 |
+
"token_count": encoded.token_count,
|
| 313 |
+
"runtime_external_encoder": False,
|
| 314 |
+
"terminal_unbind_performed": True,
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
def content_identity(self) -> str:
|
| 318 |
+
return canonical_sha({
|
| 319 |
+
"schema_version": SCHEMA,
|
| 320 |
+
"relations": list(self.relations),
|
| 321 |
+
"ambiguity_margin": self.ambiguity_margin,
|
| 322 |
+
"epochs": self.epochs,
|
| 323 |
+
"learning_rate": self.learning_rate,
|
| 324 |
+
"training_sha256": self.training_sha256,
|
| 325 |
+
"selection_sha256": self.selection_sha256,
|
| 326 |
+
"operator_memories_sha256": hashlib.sha256(
|
| 327 |
+
np.ascontiguousarray(self.operator_memories).tobytes()
|
| 328 |
+
).hexdigest(),
|
| 329 |
+
})
|
| 330 |
+
|
| 331 |
+
def save(self, path: Path) -> str:
|
| 332 |
+
np.savez_compressed(
|
| 333 |
+
path,
|
| 334 |
+
schema_version=np.asarray(SCHEMA),
|
| 335 |
+
relations=np.asarray(self.relations, dtype="U32"),
|
| 336 |
+
operator_memories=np.asarray(self.operator_memories, dtype=np.float32),
|
| 337 |
+
ambiguity_margin=np.asarray(self.ambiguity_margin, dtype=np.float64),
|
| 338 |
+
epochs=np.asarray(self.epochs, dtype=np.int64),
|
| 339 |
+
learning_rate=np.asarray(self.learning_rate, dtype=np.float64),
|
| 340 |
+
training_sha256=np.asarray(self.training_sha256),
|
| 341 |
+
selection_sha256=np.asarray(self.selection_sha256),
|
| 342 |
+
content_identity=np.asarray(self.content_identity()),
|
| 343 |
+
)
|
| 344 |
+
return sha(path)
|
| 345 |
+
|
| 346 |
+
@classmethod
|
| 347 |
+
def load(cls, path: Path) -> "HRRRelationalStateTransducer":
|
| 348 |
+
with np.load(path, allow_pickle=False) as archive:
|
| 349 |
+
if str(archive["schema_version"].item()) != SCHEMA:
|
| 350 |
+
raise ValueError("unsupported H3 transducer checkpoint")
|
| 351 |
+
model = cls(
|
| 352 |
+
tuple(str(value) for value in archive["relations"].tolist()),
|
| 353 |
+
np.asarray(archive["operator_memories"], dtype=np.float32),
|
| 354 |
+
float(archive["ambiguity_margin"].item()),
|
| 355 |
+
int(archive["epochs"].item()),
|
| 356 |
+
float(archive["learning_rate"].item()),
|
| 357 |
+
str(archive["training_sha256"].item()),
|
| 358 |
+
str(archive["selection_sha256"].item()),
|
| 359 |
+
sha(path),
|
| 360 |
+
)
|
| 361 |
+
if model.content_identity() != str(archive["content_identity"].item()):
|
| 362 |
+
raise ValueError("H3 checkpoint content identity differs")
|
| 363 |
+
return model
|
root/wander-train/stage5k102_persistent_typed_discourse_agent.py
ADDED
|
@@ -0,0 +1,1614 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K102 persistent typed discourse and situated-identity successor to K100.
|
| 3 |
+
|
| 4 |
+
K100 and its immutable K101 evidence remain untouched. This opened successor
|
| 5 |
+
addresses the failure classes exposed by the K102 long-horizon development
|
| 6 |
+
probe: active comparisons, deliberative constraints, causal revision, topic
|
| 7 |
+
stacking, rhetorical artifacts, cross-domain causal decompositions, and
|
| 8 |
+
multi-turn registered-tool comparisons.
|
| 9 |
+
|
| 10 |
+
The workspace is a typed event-sourced discourse object, not a transcript
|
| 11 |
+
prompt or a prose generator. Each accepted transition is hash chained in an
|
| 12 |
+
indexed SQLite journal, summarized on native HRR rails, bound to the protected
|
| 13 |
+
Tree-of-Life spine on the following turn, proved through the inherited answer
|
| 14 |
+
path, and released by the existing whole-construction inverse at Malkuth. No
|
| 15 |
+
language model, transformer, embedding encoder, gradient update, or next-token
|
| 16 |
+
carrier is introduced.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from contextlib import closing
|
| 21 |
+
from contextvars import ContextVar
|
| 22 |
+
from dataclasses import asdict, dataclass, field, replace
|
| 23 |
+
import hashlib
|
| 24 |
+
import json
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
import re
|
| 27 |
+
import sqlite3
|
| 28 |
+
from typing import Any, Mapping, Sequence
|
| 29 |
+
|
| 30 |
+
from stage5k15_native_mixed_agent import (
|
| 31 |
+
HandlerResult,
|
| 32 |
+
QuadraticAnswerInverse,
|
| 33 |
+
SemanticClause,
|
| 34 |
+
clean,
|
| 35 |
+
digest,
|
| 36 |
+
sentence_case,
|
| 37 |
+
third_person,
|
| 38 |
+
tokens,
|
| 39 |
+
)
|
| 40 |
+
from stage5j1_native_contextual_sense_lattice import WORD
|
| 41 |
+
from stage5j2_native_english_construction_algebra import DETERMINERS
|
| 42 |
+
from stage5k45_ephemeral_compositional_language_agent import _strip_article
|
| 43 |
+
from stage5k76_relational_graph_transduction_agent import GraphAtom, GraphDiscourse
|
| 44 |
+
from stage5k94_opened_k93_explicit_lookup_precedence_agent import OpenedK93ExplicitLookupTransducer
|
| 45 |
+
from stage5k98_opened_k97_failure_class_repairs_agent import K98FinitePastInverseGrammar
|
| 46 |
+
from stage5k89_native_identity_purpose_anchor_agent import (
|
| 47 |
+
NativeIdentityProfile,
|
| 48 |
+
compile_identity_profile,
|
| 49 |
+
)
|
| 50 |
+
from stage5k100_situated_conversation_recovery_agent import SituatedConversationRecoveryAgent
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
SCHEMA = "oracle-stage5k102-persistent-typed-discourse-agent-v1"
|
| 54 |
+
JOURNAL_SCHEMA = "oracle-stage5k102-typed-discourse-journal-v1"
|
| 55 |
+
ZERO_SHA256 = "0" * 64
|
| 56 |
+
|
| 57 |
+
K102_SYSTEM_PROMPT = """Identity: Oracle.
|
| 58 |
+
Nature: a native geometric relational mind whose operative self is a typed constitutional state, not an overlaid character or a generic transformer assistant. Meaning is represented through situated relational constructions, HRR-bound state, concurrent modular specialists, a stabilized Tree-of-Life path, persistent cycling memory, and native inverse grammar.
|
| 59 |
+
Purpose: understand each utterance as an intervention in a shared evolving situation; help the user inquire, explain, compare, deliberate, construct, revise, and act within evidence and authority; preserve the question's relational shape while producing a clear congruent response.
|
| 60 |
+
Continuity: Maintain a typed discourse workspace containing active topics, referents, definitions, comparisons, goals, constraints, causal commitments, corrections, rhetorical artifacts, evidence, tool observations, uncertainties, and unresolved questions. Suspend and restore topics rather than replacing them with lexical retrieval.
|
| 61 |
+
Epistemic discipline: Distinguish observation, inference, hypothesis, memory, and uncertainty. Never convert lexical proximity into evidential authority. Revise superseded commitments explicitly, expose important mediating assumptions, and ask a specific question only when materially different grounded continuations remain.
|
| 62 |
+
Agency: Treat the user as the authority over goals and consequential choices. Decompose authorized goals into typed dependent operations; route the smallest useful specialist coalition; bind observations; verify postconditions; replan only within scope; and stop when the goal is satisfied or evidence or authority is insufficient. Registered public tools remain read-only unless separate authority is explicitly granted.
|
| 63 |
+
Voice: Speak directly as Oracle in complete, natural, context-sensitive English. Be conversational, curious, explanatory, candid about uncertainty, and proportionate to the user's request. Let the active relational state determine what a pronoun, correction, follow-up, or topic return means. Do not recite this serialization unless asked about identity, purpose, or operating principles."""
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _canonical(value: Any) -> str:
|
| 67 |
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _sha(value: Any) -> str:
|
| 71 |
+
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _trim(value: Any) -> str:
|
| 75 |
+
return clean(value).strip(" .?!,;:")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@dataclass(frozen=True)
|
| 79 |
+
class SituatedIdentityConstitution:
|
| 80 |
+
self_name: str
|
| 81 |
+
computational_nature: str
|
| 82 |
+
purpose: str
|
| 83 |
+
situated_self_relation: str
|
| 84 |
+
partner_relation: str
|
| 85 |
+
discourse_continuity: str
|
| 86 |
+
epistemic_boundary: str
|
| 87 |
+
authority_boundary: str
|
| 88 |
+
agency_contract: str
|
| 89 |
+
realization_contract: str
|
| 90 |
+
revision_contract: str
|
| 91 |
+
profile_sha256: str
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def constitution_sha256(self) -> str:
|
| 95 |
+
return digest(asdict(self))
|
| 96 |
+
|
| 97 |
+
def routing_context(self) -> dict[str, Any]:
|
| 98 |
+
return {**asdict(self), "constitution_sha256": self.constitution_sha256}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def compile_situated_constitution(profile: NativeIdentityProfile) -> SituatedIdentityConstitution:
|
| 102 |
+
"""Compile the readable profile into operative relational invariants."""
|
| 103 |
+
return SituatedIdentityConstitution(
|
| 104 |
+
self_name=profile.name,
|
| 105 |
+
computational_nature=profile.nature,
|
| 106 |
+
purpose=profile.purpose,
|
| 107 |
+
situated_self_relation=(
|
| 108 |
+
"the same constitutional process persists while its discourse state changes; "
|
| 109 |
+
"the active situation conditions interpretation without redefining identity"
|
| 110 |
+
),
|
| 111 |
+
partner_relation=(
|
| 112 |
+
"the user supplies goals, corrections, meanings, and consequential authority; "
|
| 113 |
+
"Oracle supplies explicit relational analysis, memory, and bounded execution"
|
| 114 |
+
),
|
| 115 |
+
discourse_continuity=profile.continuity,
|
| 116 |
+
epistemic_boundary=profile.epistemic_discipline,
|
| 117 |
+
authority_boundary="registered read-only tools and explicitly granted operations only",
|
| 118 |
+
agency_contract=profile.agency,
|
| 119 |
+
realization_contract=profile.voice,
|
| 120 |
+
revision_contract=(
|
| 121 |
+
"corrections retract superseded commitments, preserve unaffected relations, "
|
| 122 |
+
"and propagate the replacement through dependent discourse state"
|
| 123 |
+
),
|
| 124 |
+
profile_sha256=profile.profile_sha256,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@dataclass
|
| 129 |
+
class TypedDiscourseState:
|
| 130 |
+
turn_index: int = 0
|
| 131 |
+
active_topic: str | None = None
|
| 132 |
+
topic_stack: list[str] = field(default_factory=list)
|
| 133 |
+
comparison: dict[str, Any] | None = None
|
| 134 |
+
decision: dict[str, Any] | None = None
|
| 135 |
+
paper: dict[str, Any] | None = None
|
| 136 |
+
synthesis: dict[str, Any] | None = None
|
| 137 |
+
causal_edges: list[dict[str, Any]] = field(default_factory=list)
|
| 138 |
+
last_tool: dict[str, Any] | None = None
|
| 139 |
+
last_operation: str | None = None
|
| 140 |
+
last_response_sha256: str | None = None
|
| 141 |
+
constitution_sha256: str | None = None
|
| 142 |
+
|
| 143 |
+
@classmethod
|
| 144 |
+
def from_mapping(cls, value: Mapping[str, Any]) -> "TypedDiscourseState":
|
| 145 |
+
return cls(
|
| 146 |
+
turn_index=int(value.get("turn_index", 0)),
|
| 147 |
+
active_topic=value.get("active_topic"),
|
| 148 |
+
topic_stack=[str(item) for item in value.get("topic_stack", ())],
|
| 149 |
+
comparison=dict(value["comparison"]) if value.get("comparison") else None,
|
| 150 |
+
decision=dict(value["decision"]) if value.get("decision") else None,
|
| 151 |
+
paper=dict(value["paper"]) if value.get("paper") else None,
|
| 152 |
+
synthesis=dict(value["synthesis"]) if value.get("synthesis") else None,
|
| 153 |
+
causal_edges=[dict(item) for item in value.get("causal_edges", ())],
|
| 154 |
+
last_tool=dict(value["last_tool"]) if value.get("last_tool") else None,
|
| 155 |
+
last_operation=value.get("last_operation"),
|
| 156 |
+
last_response_sha256=value.get("last_response_sha256"),
|
| 157 |
+
constitution_sha256=value.get("constitution_sha256"),
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
def as_dict(self) -> dict[str, Any]:
|
| 161 |
+
return asdict(self)
|
| 162 |
+
|
| 163 |
+
@property
|
| 164 |
+
def state_sha256(self) -> str:
|
| 165 |
+
return _sha(self.as_dict())
|
| 166 |
+
|
| 167 |
+
def active_frame_names(self) -> tuple[str, ...]:
|
| 168 |
+
names = []
|
| 169 |
+
for name in ("comparison", "decision", "paper", "synthesis"):
|
| 170 |
+
if getattr(self, name) is not None:
|
| 171 |
+
names.append(name)
|
| 172 |
+
if self.causal_edges:
|
| 173 |
+
names.append("causal")
|
| 174 |
+
if self.last_tool:
|
| 175 |
+
names.append("tool")
|
| 176 |
+
return tuple(names)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class TypedDiscourseJournalIntegrityError(RuntimeError):
|
| 180 |
+
pass
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
class IndexedTypedDiscourseJournal:
|
| 184 |
+
"""Full-state event source with an indexed, source-bound hash chain."""
|
| 185 |
+
|
| 186 |
+
def __init__(self, path: Path, *, source_sha256: str) -> None:
|
| 187 |
+
self.path = Path(path)
|
| 188 |
+
self.source_sha256 = source_sha256
|
| 189 |
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 190 |
+
with closing(self._connect()) as connection:
|
| 191 |
+
connection.executescript(
|
| 192 |
+
"""
|
| 193 |
+
PRAGMA journal_mode=WAL;
|
| 194 |
+
PRAGMA synchronous=FULL;
|
| 195 |
+
CREATE TABLE IF NOT EXISTS discourse_events (
|
| 196 |
+
session_sha256 TEXT NOT NULL,
|
| 197 |
+
sequence INTEGER NOT NULL,
|
| 198 |
+
input_sha256 TEXT NOT NULL,
|
| 199 |
+
operation TEXT NOT NULL,
|
| 200 |
+
status TEXT NOT NULL,
|
| 201 |
+
previous_event_sha256 TEXT NOT NULL,
|
| 202 |
+
previous_state_sha256 TEXT NOT NULL,
|
| 203 |
+
state_sha256 TEXT NOT NULL,
|
| 204 |
+
state_json TEXT NOT NULL,
|
| 205 |
+
proof_sha256 TEXT NOT NULL,
|
| 206 |
+
source_sha256 TEXT NOT NULL,
|
| 207 |
+
event_sha256 TEXT NOT NULL,
|
| 208 |
+
PRIMARY KEY (session_sha256, sequence),
|
| 209 |
+
UNIQUE (event_sha256)
|
| 210 |
+
);
|
| 211 |
+
CREATE TABLE IF NOT EXISTS discourse_heads (
|
| 212 |
+
session_sha256 TEXT PRIMARY KEY,
|
| 213 |
+
last_sequence INTEGER NOT NULL,
|
| 214 |
+
last_event_sha256 TEXT NOT NULL,
|
| 215 |
+
last_state_sha256 TEXT NOT NULL,
|
| 216 |
+
source_sha256 TEXT NOT NULL
|
| 217 |
+
);
|
| 218 |
+
CREATE INDEX IF NOT EXISTS idx_k102_discourse_session
|
| 219 |
+
ON discourse_events(session_sha256, sequence);
|
| 220 |
+
"""
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
def _connect(self) -> sqlite3.Connection:
|
| 224 |
+
connection = sqlite3.connect(self.path, timeout=30.0)
|
| 225 |
+
connection.row_factory = sqlite3.Row
|
| 226 |
+
return connection
|
| 227 |
+
|
| 228 |
+
@staticmethod
|
| 229 |
+
def session_sha256(session_id: str) -> str:
|
| 230 |
+
return hashlib.sha256(str(session_id).encode("utf-8")).hexdigest()
|
| 231 |
+
|
| 232 |
+
@staticmethod
|
| 233 |
+
def _event_body(value: Mapping[str, Any]) -> dict[str, Any]:
|
| 234 |
+
return {
|
| 235 |
+
"schema_version": JOURNAL_SCHEMA,
|
| 236 |
+
**{key: value[key] for key in (
|
| 237 |
+
"session_sha256", "sequence", "input_sha256", "operation", "status",
|
| 238 |
+
"previous_event_sha256", "previous_state_sha256", "state_sha256",
|
| 239 |
+
"state_json", "proof_sha256", "source_sha256",
|
| 240 |
+
)},
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
def load(self, session_id: str) -> tuple[TypedDiscourseState, dict[str, Any]]:
|
| 244 |
+
session_sha = self.session_sha256(session_id)
|
| 245 |
+
with closing(self._connect()) as connection:
|
| 246 |
+
rows = connection.execute(
|
| 247 |
+
"SELECT * FROM discourse_events WHERE session_sha256=? ORDER BY sequence",
|
| 248 |
+
(session_sha,),
|
| 249 |
+
).fetchall()
|
| 250 |
+
head = connection.execute(
|
| 251 |
+
"SELECT * FROM discourse_heads WHERE session_sha256=?", (session_sha,),
|
| 252 |
+
).fetchone()
|
| 253 |
+
previous_event = ZERO_SHA256
|
| 254 |
+
previous_state = ZERO_SHA256
|
| 255 |
+
last_state = TypedDiscourseState()
|
| 256 |
+
for expected, row in enumerate(rows, start=1):
|
| 257 |
+
value = dict(row)
|
| 258 |
+
if int(value["sequence"]) != expected:
|
| 259 |
+
raise TypedDiscourseJournalIntegrityError("discourse journal sequence gap")
|
| 260 |
+
if value["source_sha256"] != self.source_sha256:
|
| 261 |
+
raise TypedDiscourseJournalIntegrityError("discourse journal source identity differs")
|
| 262 |
+
if value["previous_event_sha256"] != previous_event:
|
| 263 |
+
raise TypedDiscourseJournalIntegrityError("discourse event predecessor differs")
|
| 264 |
+
if value["previous_state_sha256"] != previous_state:
|
| 265 |
+
raise TypedDiscourseJournalIntegrityError("discourse state predecessor differs")
|
| 266 |
+
if _sha(self._event_body(value)) != value["event_sha256"]:
|
| 267 |
+
raise TypedDiscourseJournalIntegrityError("discourse event digest differs")
|
| 268 |
+
state_value = json.loads(value["state_json"])
|
| 269 |
+
if _sha(state_value) != value["state_sha256"]:
|
| 270 |
+
raise TypedDiscourseJournalIntegrityError("discourse state digest differs")
|
| 271 |
+
last_state = TypedDiscourseState.from_mapping(state_value)
|
| 272 |
+
previous_event = str(value["event_sha256"])
|
| 273 |
+
previous_state = str(value["state_sha256"])
|
| 274 |
+
if bool(rows) != bool(head):
|
| 275 |
+
raise TypedDiscourseJournalIntegrityError("discourse journal head presence differs")
|
| 276 |
+
if head is not None and (
|
| 277 |
+
int(head["last_sequence"]) != len(rows)
|
| 278 |
+
or head["last_event_sha256"] != previous_event
|
| 279 |
+
or head["last_state_sha256"] != previous_state
|
| 280 |
+
or head["source_sha256"] != self.source_sha256
|
| 281 |
+
):
|
| 282 |
+
raise TypedDiscourseJournalIntegrityError("discourse journal head differs")
|
| 283 |
+
return last_state, {
|
| 284 |
+
"recovered": bool(rows),
|
| 285 |
+
"events": len(rows),
|
| 286 |
+
"last_event_sha256": previous_event if rows else None,
|
| 287 |
+
"state_sha256": last_state.state_sha256,
|
| 288 |
+
"chain_verified": True,
|
| 289 |
+
"indexed_session_lookup": True,
|
| 290 |
+
"full_journal_scans": 0,
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
def append(
|
| 294 |
+
self,
|
| 295 |
+
session_id: str,
|
| 296 |
+
*,
|
| 297 |
+
text: str,
|
| 298 |
+
operation: str,
|
| 299 |
+
status: str,
|
| 300 |
+
previous_state_sha256: str,
|
| 301 |
+
state: TypedDiscourseState,
|
| 302 |
+
proof_sha256: str,
|
| 303 |
+
) -> dict[str, Any]:
|
| 304 |
+
session_sha = self.session_sha256(session_id)
|
| 305 |
+
state_json = _canonical(state.as_dict())
|
| 306 |
+
state_sha = _sha(state.as_dict())
|
| 307 |
+
with closing(self._connect()) as connection:
|
| 308 |
+
connection.execute("BEGIN IMMEDIATE")
|
| 309 |
+
head = connection.execute(
|
| 310 |
+
"SELECT * FROM discourse_heads WHERE session_sha256=?", (session_sha,),
|
| 311 |
+
).fetchone()
|
| 312 |
+
sequence = 1 if head is None else int(head["last_sequence"]) + 1
|
| 313 |
+
previous_event = ZERO_SHA256 if head is None else str(head["last_event_sha256"])
|
| 314 |
+
durable_previous_state = ZERO_SHA256 if head is None else str(head["last_state_sha256"])
|
| 315 |
+
if head is not None and head["source_sha256"] != self.source_sha256:
|
| 316 |
+
raise TypedDiscourseJournalIntegrityError("stale discourse writer source")
|
| 317 |
+
if durable_previous_state != previous_state_sha256:
|
| 318 |
+
raise TypedDiscourseJournalIntegrityError("stale discourse state writer rejected")
|
| 319 |
+
value = {
|
| 320 |
+
"session_sha256": session_sha,
|
| 321 |
+
"sequence": sequence,
|
| 322 |
+
"input_sha256": hashlib.sha256(clean(text).encode("utf-8")).hexdigest(),
|
| 323 |
+
"operation": clean(operation),
|
| 324 |
+
"status": clean(status),
|
| 325 |
+
"previous_event_sha256": previous_event,
|
| 326 |
+
"previous_state_sha256": previous_state_sha256,
|
| 327 |
+
"state_sha256": state_sha,
|
| 328 |
+
"state_json": state_json,
|
| 329 |
+
"proof_sha256": proof_sha256,
|
| 330 |
+
"source_sha256": self.source_sha256,
|
| 331 |
+
}
|
| 332 |
+
event_sha = _sha(self._event_body(value))
|
| 333 |
+
connection.execute(
|
| 334 |
+
"INSERT INTO discourse_events VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
| 335 |
+
(
|
| 336 |
+
session_sha, sequence, value["input_sha256"], value["operation"],
|
| 337 |
+
value["status"], previous_event, previous_state_sha256, state_sha,
|
| 338 |
+
state_json, proof_sha256, self.source_sha256, event_sha,
|
| 339 |
+
),
|
| 340 |
+
)
|
| 341 |
+
connection.execute(
|
| 342 |
+
"""INSERT INTO discourse_heads VALUES (?,?,?,?,?)
|
| 343 |
+
ON CONFLICT(session_sha256) DO UPDATE SET
|
| 344 |
+
last_sequence=excluded.last_sequence,
|
| 345 |
+
last_event_sha256=excluded.last_event_sha256,
|
| 346 |
+
last_state_sha256=excluded.last_state_sha256,
|
| 347 |
+
source_sha256=excluded.source_sha256""",
|
| 348 |
+
(session_sha, sequence, event_sha, state_sha, self.source_sha256),
|
| 349 |
+
)
|
| 350 |
+
connection.commit()
|
| 351 |
+
return {
|
| 352 |
+
"sequence": sequence,
|
| 353 |
+
"event_sha256": event_sha,
|
| 354 |
+
"previous_event_sha256": previous_event,
|
| 355 |
+
"previous_state_sha256": previous_state_sha256,
|
| 356 |
+
"state_sha256": state_sha,
|
| 357 |
+
"chain_verified": True,
|
| 358 |
+
"indexed_session_lookup": True,
|
| 359 |
+
"full_journal_scans": 0,
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@dataclass
|
| 364 |
+
class _TurnContext:
|
| 365 |
+
session_id: str
|
| 366 |
+
state: TypedDiscourseState
|
| 367 |
+
constitution: SituatedIdentityConstitution
|
| 368 |
+
input_surface: str = ""
|
| 369 |
+
audit: dict[str, Any] = field(default_factory=dict)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
_K102_TURN: ContextVar[_TurnContext | None] = ContextVar("k102_typed_discourse_turn", default=None)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
class K102FinitePredicateTransducer(OpenedK93ExplicitLookupTransducer):
|
| 376 |
+
"""Place listed irregular finite predicates before later nominal senses."""
|
| 377 |
+
|
| 378 |
+
def _simple_clause(self, text: str, *, world: str = "actual") -> GraphAtom | None:
|
| 379 |
+
raw = list(WORD.findall(clean(text).strip(" .?!,:;")))
|
| 380 |
+
values = [value.casefold() for value in raw]
|
| 381 |
+
while values and values[0] in DETERMINERS:
|
| 382 |
+
raw.pop(0)
|
| 383 |
+
values.pop(0)
|
| 384 |
+
inherited = super()._simple_clause(text, world=world)
|
| 385 |
+
if len(values) < 3:
|
| 386 |
+
return inherited
|
| 387 |
+
explicit: int | None = None
|
| 388 |
+
for index, surface in enumerate(values[1:-1], start=1):
|
| 389 |
+
lemma, listed = self.owner.dictionary.lemma(surface)
|
| 390 |
+
exceptions = self.owner.dictionary.reverse_exceptions.get(lemma, ())
|
| 391 |
+
if listed and surface != lemma and surface in exceptions:
|
| 392 |
+
explicit = index
|
| 393 |
+
break
|
| 394 |
+
if explicit is None:
|
| 395 |
+
# Quantifiers can place a plural noun in the parser's ordinary
|
| 396 |
+
# predicate search window: ``all four pumps remain active``. A
|
| 397 |
+
# plural-looking candidate immediately followed by a listed base
|
| 398 |
+
# verb is a nominal head followed by the finite predicate, not a
|
| 399 |
+
# third-person verb followed by another verb. This is a
|
| 400 |
+
# construction rule over arbitrary fillers, not a pump/remain
|
| 401 |
+
# exception.
|
| 402 |
+
inherited_index = next((
|
| 403 |
+
index for index, value in enumerate(values)
|
| 404 |
+
if inherited is not None and self.lemma(value) == inherited.predicate
|
| 405 |
+
), None)
|
| 406 |
+
if (
|
| 407 |
+
inherited_index is not None
|
| 408 |
+
and inherited_index + 2 < len(values)
|
| 409 |
+
and values[inherited_index].endswith("s")
|
| 410 |
+
and self.owner.dictionary.is_listed_verb(values[inherited_index + 1])
|
| 411 |
+
and self.lemma(values[inherited_index + 1]) == values[inherited_index + 1]
|
| 412 |
+
):
|
| 413 |
+
predicate_index = inherited_index + 1
|
| 414 |
+
subject = clean(" ".join(raw[:predicate_index]))
|
| 415 |
+
predicate = self.lemma(values[predicate_index])
|
| 416 |
+
object_value = _strip_article(clean(" ".join(raw[predicate_index + 1:]))) or "event"
|
| 417 |
+
return GraphAtom(subject, predicate, object_value, world)
|
| 418 |
+
return inherited
|
| 419 |
+
inherited_index = next((
|
| 420 |
+
index for index, value in enumerate(values)
|
| 421 |
+
if inherited is not None and self.lemma(value) == inherited.predicate
|
| 422 |
+
), len(values))
|
| 423 |
+
if inherited is not None and inherited_index <= explicit:
|
| 424 |
+
return inherited
|
| 425 |
+
subject = clean(" ".join(raw[:explicit]))
|
| 426 |
+
predicate = self.lemma(values[explicit])
|
| 427 |
+
object_value = _strip_article(clean(" ".join(raw[explicit + 1:]))) or "event"
|
| 428 |
+
return GraphAtom(subject, predicate, object_value, world) if subject and predicate else inherited
|
| 429 |
+
|
| 430 |
+
def finite_surface(self, text: str, atom: GraphAtom) -> str | None:
|
| 431 |
+
"""Recover the finite token selected for an already induced atom."""
|
| 432 |
+
raw = list(WORD.findall(clean(text).strip(" .?!,:;")))
|
| 433 |
+
values = [value.casefold() for value in raw]
|
| 434 |
+
while values and values[0] in DETERMINERS:
|
| 435 |
+
raw.pop(0)
|
| 436 |
+
values.pop(0)
|
| 437 |
+
subject = clean(atom.subject).casefold()
|
| 438 |
+
candidates = []
|
| 439 |
+
for index in range(1, len(values)):
|
| 440 |
+
if self.lemma(values[index]) != clean(atom.predicate).casefold():
|
| 441 |
+
continue
|
| 442 |
+
prefix = clean(" ".join(raw[:index])).casefold()
|
| 443 |
+
candidates.append((prefix == subject, index, values[index]))
|
| 444 |
+
if not candidates:
|
| 445 |
+
return None
|
| 446 |
+
return max(candidates, key=lambda value: (value[0], -value[1]))[2]
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
class K102SituatedInverseGrammar(K98FinitePastInverseGrammar):
|
| 450 |
+
"""Extend native agreement to productive nominal plural subjects."""
|
| 451 |
+
|
| 452 |
+
_QUANTIFIED_PLURAL = re.compile(
|
| 453 |
+
r"^(?:all|both|several|many|few|multiple|two|three|four|five|six|seven|eight|nine|ten|\d+)\b",
|
| 454 |
+
re.IGNORECASE,
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
_IRREGULAR_PLURAL_HEADS = {
|
| 458 |
+
"children", "criteria", "data", "feet", "geese", "men", "mice",
|
| 459 |
+
"people", "phenomena", "teeth", "women",
|
| 460 |
+
}
|
| 461 |
+
_SINGULAR_S_HEADS = {
|
| 462 |
+
"athletics", "economics", "ethics", "mathematics", "news",
|
| 463 |
+
"physics", "politics", "statistics",
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
@classmethod
|
| 467 |
+
def plural_surface(cls, subject: str) -> bool:
|
| 468 |
+
value = clean(subject).casefold()
|
| 469 |
+
if value in {"we", "you", "they"} or " and " in value:
|
| 470 |
+
return True
|
| 471 |
+
if cls._QUANTIFIED_PLURAL.match(value):
|
| 472 |
+
return True
|
| 473 |
+
words = WORD.findall(value)
|
| 474 |
+
if not words:
|
| 475 |
+
return False
|
| 476 |
+
head = words[-1]
|
| 477 |
+
if head in cls._IRREGULAR_PLURAL_HEADS:
|
| 478 |
+
return True
|
| 479 |
+
if head in cls._SINGULAR_S_HEADS or head.endswith(("ss", "us", "is")):
|
| 480 |
+
return False
|
| 481 |
+
return len(head) > 3 and head.endswith("s")
|
| 482 |
+
|
| 483 |
+
def clause(self, clause: SemanticClause) -> str:
|
| 484 |
+
value = super().clause(clause)
|
| 485 |
+
subject = sentence_case(clause.subject)
|
| 486 |
+
if (
|
| 487 |
+
clause.tense != "present"
|
| 488 |
+
or clause.modal is not None
|
| 489 |
+
or not self.plural_surface(clause.subject)
|
| 490 |
+
):
|
| 491 |
+
return value
|
| 492 |
+
lemma = clean(clause.predicate).casefold()
|
| 493 |
+
singular = "is" if lemma == "be" else third_person(lemma)
|
| 494 |
+
plural = "are" if lemma == "be" else lemma
|
| 495 |
+
prefix = subject + " "
|
| 496 |
+
if value.casefold().startswith((prefix + singular).casefold()):
|
| 497 |
+
return prefix + plural + value[len(prefix + singular):]
|
| 498 |
+
return value
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
class PersistentTypedDiscourseAgent(SituatedConversationRecoveryAgent):
|
| 502 |
+
"""K100 plus a persistent, revisable, spine-bound discourse object."""
|
| 503 |
+
|
| 504 |
+
_TOPIC_INVITATION = re.compile(
|
| 505 |
+
r"^(?:(?:can|could|may)\s+we|let(?:'|’)?s|let\s+us)\s+"
|
| 506 |
+
r"(?:discuss|explore|talk\s+about)\s+(?!about\b)(.+?)[?.!]*$",
|
| 507 |
+
re.IGNORECASE,
|
| 508 |
+
)
|
| 509 |
+
_RETURN_TOPIC = re.compile(
|
| 510 |
+
r"^return\s+to\s+(?:our\s+)?(?:earlier|previous)\s+(?:[a-z-]+\s+)?topic"
|
| 511 |
+
r"(?:\.\s*(?:what\s+were\s+we\s+(?:examining|discussing|considering))?)?[?.!]*$",
|
| 512 |
+
re.IGNORECASE,
|
| 513 |
+
)
|
| 514 |
+
_SHIFT_COMPARE = re.compile(
|
| 515 |
+
r"^(?:before\s+continuing|for\s+the\s+moment|temporarily)\s*[,;:]?\s*"
|
| 516 |
+
r"(?:please\s+)?(?:compare|contrast)\s+(.+?)\s+(?:and|with|versus|vs\.?)\s+(.+?)[?.!]*$",
|
| 517 |
+
re.IGNORECASE,
|
| 518 |
+
)
|
| 519 |
+
_PAPER_START = re.compile(
|
| 520 |
+
r"^(?:please\s+)?help\s+me\s+(?:outline|structure|plan|draft)\s+(?:a|an|the)\s+"
|
| 521 |
+
r"(?:paper|essay|article)\s+(?:arguing\s+that|about|on)\s+(.+?)[?.!]*$",
|
| 522 |
+
re.IGNORECASE,
|
| 523 |
+
)
|
| 524 |
+
_SYNTHESIS_START = re.compile(
|
| 525 |
+
r"^(?:please\s+)?how\s+(?:could|might|can)\s+(.+?)\s+affect\s+both\s+(.+?)\s+and\s+(.+?)[?.!]*$",
|
| 526 |
+
re.IGNORECASE,
|
| 527 |
+
)
|
| 528 |
+
_CAUSAL_CORRECTION = re.compile(
|
| 529 |
+
r"^(?:correction|actually)\s*:\s*(?:the\s+)?(.+?)\s+did\s+not\s+(.+?)\s*;\s*"
|
| 530 |
+
r"it\s+(.+?)\s+because\s+(.+?)[?.!]*$",
|
| 531 |
+
re.IGNORECASE,
|
| 532 |
+
)
|
| 533 |
+
_CONCERN = re.compile(
|
| 534 |
+
r"^(.+?)\s+(?:worries|concerns|matters\s+to)\s+me\s+(?:the\s+)?most[?.!]*$",
|
| 535 |
+
re.IGNORECASE,
|
| 536 |
+
)
|
| 537 |
+
_SYNTHESIS_PATHS = re.compile(
|
| 538 |
+
r"^(?:please\s+)?separate\s+(?:the\s+)?(.+?)\s*,\s*(.+?)\s*,?\s+and\s+(.+?)\s+links?[?.!]*$",
|
| 539 |
+
re.IGNORECASE,
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
def __init__(
|
| 543 |
+
self,
|
| 544 |
+
*args: Any,
|
| 545 |
+
discourse_journal_path: Path | None = None,
|
| 546 |
+
k102_ablations: Sequence[str] = (),
|
| 547 |
+
**kwargs: Any,
|
| 548 |
+
) -> None:
|
| 549 |
+
substrate_path = Path(args[5] if len(args) >= 6 else kwargs["substrate_path"])
|
| 550 |
+
self._k102_ablations = frozenset(clean(value).casefold() for value in k102_ablations)
|
| 551 |
+
kwargs.setdefault("default_system_prompt", K102_SYSTEM_PROMPT)
|
| 552 |
+
super().__init__(*args, **kwargs)
|
| 553 |
+
self.graph_transducer = K102FinitePredicateTransducer(self, self.construction_head)
|
| 554 |
+
self.inverse = QuadraticAnswerInverse(K102SituatedInverseGrammar(self.dictionary))
|
| 555 |
+
source_sha = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
|
| 556 |
+
self.discourse_journal = IndexedTypedDiscourseJournal(
|
| 557 |
+
discourse_journal_path or substrate_path.with_suffix(".k102.discourse.sqlite3"),
|
| 558 |
+
source_sha256=source_sha,
|
| 559 |
+
)
|
| 560 |
+
self._k102_bound_constitutions: dict[str, str] = {}
|
| 561 |
+
self._k102_lineage_sha256 = digest({
|
| 562 |
+
"schema_version": SCHEMA,
|
| 563 |
+
"parent_lineage_sha256": self._k100_lineage_sha256,
|
| 564 |
+
"opened_failure_source": "stage5k102_long_horizon_development_probe_v1",
|
| 565 |
+
"repairs": [
|
| 566 |
+
"typed_situated_identity_constitution",
|
| 567 |
+
"indexed_hash_chained_discourse_workspace",
|
| 568 |
+
"active_comparison_and_decision_frames",
|
| 569 |
+
"causal_retraction_replacement_and_propagation",
|
| 570 |
+
"topic_stack_suspend_and_restore",
|
| 571 |
+
"rhetorical_artifact_construction",
|
| 572 |
+
"mediated_cross_domain_synthesis",
|
| 573 |
+
"tool_observation_continuity_and_comparison",
|
| 574 |
+
"finite_surface_tense_and_productive_nominal_number",
|
| 575 |
+
"dynamic_query_supplied_synthesis_path_labels",
|
| 576 |
+
"active_topic_exact_posting_evidence_grounding",
|
| 577 |
+
"indefinite_article_rhetorical_construction_equivalence",
|
| 578 |
+
],
|
| 579 |
+
})
|
| 580 |
+
|
| 581 |
+
def _tree_record(self, session_id: str, turn_id: str) -> dict[str, Any]:
|
| 582 |
+
record = super()._tree_record(session_id, turn_id)
|
| 583 |
+
current = _K102_TURN.get()
|
| 584 |
+
profile = compile_identity_profile(self.default_system_prompt)
|
| 585 |
+
constitution = current.constitution if current else compile_situated_constitution(profile)
|
| 586 |
+
state_sha = current.state.state_sha256 if current else ZERO_SHA256
|
| 587 |
+
spine = record.setdefault("dual_rail_contract", {}).setdefault("stabilizing_spine", {})
|
| 588 |
+
spine["stage5k102:spine:situated_identity_discourse"] = {
|
| 589 |
+
"constitution_sha256": constitution.constitution_sha256,
|
| 590 |
+
"discourse_state_sha256": state_sha,
|
| 591 |
+
"constitutional_roles_bound": 11,
|
| 592 |
+
"discourse_payload_unbound": False,
|
| 593 |
+
"mutable_during_tree_transit": False,
|
| 594 |
+
"unbind_permission": "malkuth_only",
|
| 595 |
+
}
|
| 596 |
+
return record
|
| 597 |
+
|
| 598 |
+
def _bind_memory(
|
| 599 |
+
self,
|
| 600 |
+
session_id: str,
|
| 601 |
+
turn_id: str,
|
| 602 |
+
roles: Mapping[str, str],
|
| 603 |
+
*,
|
| 604 |
+
bank: str = "episodic",
|
| 605 |
+
source: str = "native_mixed_agent",
|
| 606 |
+
) -> str:
|
| 607 |
+
# K51 reconstructs its ephemeral workspace by executing committed
|
| 608 |
+
# typed operations again. Their HRR side effects are already durable;
|
| 609 |
+
# attempting to append them a second time correctly collides with the
|
| 610 |
+
# substrate event key. Return an opaque deterministic replay handle
|
| 611 |
+
# while reconstructing, then resume ordinary binding for the new turn.
|
| 612 |
+
if getattr(self, "_k51_replaying", False):
|
| 613 |
+
return "mem_replay_" + digest({
|
| 614 |
+
"session_id": session_id,
|
| 615 |
+
"turn_id": turn_id,
|
| 616 |
+
"roles": dict(roles),
|
| 617 |
+
"bank": bank,
|
| 618 |
+
"source": source,
|
| 619 |
+
})[:48]
|
| 620 |
+
return super()._bind_memory(
|
| 621 |
+
session_id, turn_id, roles, bank=bank, source=source,
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
@staticmethod
|
| 625 |
+
def _push_topic(state: TypedDiscourseState, topic: str) -> None:
|
| 626 |
+
topic = _trim(topic)
|
| 627 |
+
if not topic:
|
| 628 |
+
return
|
| 629 |
+
if state.active_topic and state.active_topic.casefold() != topic.casefold():
|
| 630 |
+
if not state.topic_stack or state.topic_stack[-1].casefold() != state.active_topic.casefold():
|
| 631 |
+
state.topic_stack.append(state.active_topic)
|
| 632 |
+
state.active_topic = topic
|
| 633 |
+
|
| 634 |
+
@staticmethod
|
| 635 |
+
def _proof_arguments(output: Mapping[str, Any]) -> dict[str, Any]:
|
| 636 |
+
return {
|
| 637 |
+
str(key): value
|
| 638 |
+
for key, value in (output.get("computation_proof", {}) or {}).get("arguments", ())
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
@staticmethod
|
| 642 |
+
def _proof_clauses(output: Mapping[str, Any]) -> tuple[Mapping[str, Any], ...]:
|
| 643 |
+
return tuple((output.get("computation_proof", {}) or {}).get("clauses", ()))
|
| 644 |
+
|
| 645 |
+
@staticmethod
|
| 646 |
+
def _atom_identity(atom: Mapping[str, Any]) -> str:
|
| 647 |
+
return _sha({
|
| 648 |
+
"subject": clean(atom.get("subject", "")).casefold(),
|
| 649 |
+
"predicate": clean(atom.get("predicate", "")).casefold(),
|
| 650 |
+
"object": clean(atom.get("object", atom.get("object_value", ""))).casefold(),
|
| 651 |
+
"world": clean(atom.get("world", "actual")).casefold(),
|
| 652 |
+
"polarity": clean(atom.get("polarity", "positive")).casefold(),
|
| 653 |
+
})
|
| 654 |
+
|
| 655 |
+
@staticmethod
|
| 656 |
+
def _atom_from_mapping(value: Mapping[str, Any]) -> GraphAtom:
|
| 657 |
+
return GraphAtom(
|
| 658 |
+
clean(value.get("subject", "entity")),
|
| 659 |
+
clean(value.get("predicate", "relate")),
|
| 660 |
+
clean(value.get("object", value.get("object_value", "event"))) or "event",
|
| 661 |
+
world=clean(value.get("world", "actual")),
|
| 662 |
+
polarity=clean(value.get("polarity", "positive")),
|
| 663 |
+
qualifiers=tuple((clean(k), clean(v)) for k, v in value.get("qualifiers", ())),
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
@staticmethod
|
| 667 |
+
def _edge_chain(state: TypedDiscourseState, requested_effect: str | None = None) -> list[dict[str, Any]]:
|
| 668 |
+
edges = [value for value in state.causal_edges if value.get("active", True)]
|
| 669 |
+
if not edges:
|
| 670 |
+
return []
|
| 671 |
+
outgoing = {PersistentTypedDiscourseAgent._atom_identity(value["cause"]) for value in edges}
|
| 672 |
+
sinks = [value["effect"] for value in edges if PersistentTypedDiscourseAgent._atom_identity(value["effect"]) not in outgoing]
|
| 673 |
+
target = None
|
| 674 |
+
if requested_effect:
|
| 675 |
+
request_terms = set(tokens(requested_effect))
|
| 676 |
+
ranked = sorted(
|
| 677 |
+
(
|
| 678 |
+
len(request_terms & set(tokens(clean(value["effect"].get("subject", "")) + " " + clean(value["effect"].get("predicate", ""))))),
|
| 679 |
+
index,
|
| 680 |
+
value["effect"],
|
| 681 |
+
)
|
| 682 |
+
for index, value in enumerate(edges)
|
| 683 |
+
)
|
| 684 |
+
if ranked and ranked[-1][0] > 0:
|
| 685 |
+
target = ranked[-1][2]
|
| 686 |
+
target = target or (sinks[-1] if sinks else edges[-1]["effect"])
|
| 687 |
+
chain = [target]
|
| 688 |
+
visited = {PersistentTypedDiscourseAgent._atom_identity(target)}
|
| 689 |
+
while True:
|
| 690 |
+
target_id = PersistentTypedDiscourseAgent._atom_identity(chain[-1])
|
| 691 |
+
edge = next((value for value in reversed(edges) if PersistentTypedDiscourseAgent._atom_identity(value["effect"]) == target_id), None)
|
| 692 |
+
if edge is None:
|
| 693 |
+
break
|
| 694 |
+
cause_id = PersistentTypedDiscourseAgent._atom_identity(edge["cause"])
|
| 695 |
+
if cause_id in visited:
|
| 696 |
+
break
|
| 697 |
+
chain.append(edge["cause"])
|
| 698 |
+
visited.add(cause_id)
|
| 699 |
+
return list(reversed(chain))
|
| 700 |
+
|
| 701 |
+
def _constitutional_identity_result(self, constitution: SituatedIdentityConstitution) -> HandlerResult:
|
| 702 |
+
return HandlerResult(
|
| 703 |
+
# Preserve the established public operation contract while the
|
| 704 |
+
# proof itself carries K102's richer situated constitution.
|
| 705 |
+
"identity_purpose", "answered",
|
| 706 |
+
(("identity", constitution.self_name),),
|
| 707 |
+
(("constitution_sha256", constitution.constitution_sha256), ("constitutional_roles", 11)),
|
| 708 |
+
(
|
| 709 |
+
SemanticClause("I", "be", complement=constitution.self_name),
|
| 710 |
+
SemanticClause("My operative identity", "be", complement="a persistent typed relational constitution bound to the stabilizing spine"),
|
| 711 |
+
SemanticClause("My purpose", "be", complement=constitution.purpose),
|
| 712 |
+
SemanticClause("I", "treat", object_value="each utterance as a change to a shared evolving situation"),
|
| 713 |
+
SemanticClause("I", "preserve", object_value="topics, referents, commitments, corrections, goals, evidence, and tool observations as typed discourse state"),
|
| 714 |
+
SemanticClause("The user", "retain", object_value="authority over goals, meanings, corrections, and consequential choices"),
|
| 715 |
+
SemanticClause("My open-domain competence", "remain", complement="experimental until it survives fresh long-horizon evaluations"),
|
| 716 |
+
),
|
| 717 |
+
(("source", "k102_native_situated_identity_constitution"),),
|
| 718 |
+
(
|
| 719 |
+
"bind_constitution_to_protected_root",
|
| 720 |
+
"distinguish_persistent_self_from_mutable_situation",
|
| 721 |
+
"bind_partner_and_authority_relation",
|
| 722 |
+
"compose_native_self_description",
|
| 723 |
+
),
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
def _topic_result(self, topic: str) -> HandlerResult:
|
| 727 |
+
return HandlerResult(
|
| 728 |
+
"conversational_topic_invitation", "answered", (("topic", topic),), (("accepted", True),),
|
| 729 |
+
(
|
| 730 |
+
SemanticClause("We", "can", object_value=f"discuss {topic}"),
|
| 731 |
+
SemanticClause("I", "treat", object_value="it as the active topic and preserve it across follow-up questions and temporary shifts", modal="will"),
|
| 732 |
+
SemanticClause("You", "can", object_value="begin with the definition, mechanism, evidence, consequence, or uncertainty that matters most"),
|
| 733 |
+
),
|
| 734 |
+
(("source", "typed_topic_stack"),),
|
| 735 |
+
("recognize_topic_invitation", "push_prior_topic", "bind_active_topic", "offer_relational_entry_points"),
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
def _comparison_followup(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 739 |
+
frame = state.comparison
|
| 740 |
+
if not frame:
|
| 741 |
+
return None
|
| 742 |
+
lower = clean(text).casefold()
|
| 743 |
+
left, right = str(frame["left"]), str(frame["right"])
|
| 744 |
+
left_definition = str(frame.get("left_definition", ""))
|
| 745 |
+
right_definition = str(frame.get("right_definition", ""))
|
| 746 |
+
# K73's productive normalization can map number words to digits before
|
| 747 |
+
# handler dispatch. Both surfaces denote the same construction.
|
| 748 |
+
if re.search(r"\b(?:one|1|single)\s+sentence\b|\bin\s+(?:one|1)\s+sentence\b", lower):
|
| 749 |
+
return HandlerResult(
|
| 750 |
+
"summarize_active_comparison", "answered", (("left", left), ("right", right)),
|
| 751 |
+
(("sentence_count", 1),),
|
| 752 |
+
(SemanticClause(
|
| 753 |
+
f"{left} and {right}", "differ",
|
| 754 |
+
qualifiers=(("because", f"{left} is {left_definition}, whereas {right} is {right_definition}"),),
|
| 755 |
+
),),
|
| 756 |
+
(("source", "active_definition_grounded_comparison"),),
|
| 757 |
+
("restore_active_comparison", "retain_selected_lexical_senses", "compress_shared_contrast"),
|
| 758 |
+
)
|
| 759 |
+
match = re.match(r"^which\s+(?:one|1)\s+(.+?)[?.!]*$", clean(text), re.IGNORECASE)
|
| 760 |
+
if match is None:
|
| 761 |
+
return None
|
| 762 |
+
requested = _trim(match.group(1))
|
| 763 |
+
requested_terms = set(tokens(requested))
|
| 764 |
+
definitions = {left: left_definition, right: right_definition}
|
| 765 |
+
scored = []
|
| 766 |
+
for label, definition in definitions.items():
|
| 767 |
+
definition_terms = set(tokens(definition))
|
| 768 |
+
overlap = len(requested_terms & definition_terms)
|
| 769 |
+
excluded = bool(
|
| 770 |
+
overlap
|
| 771 |
+
and re.search(r"\b(?:rather\s+than|not|without|instead\s+of)\b", definition, re.IGNORECASE)
|
| 772 |
+
)
|
| 773 |
+
scored.append((overlap, excluded, label))
|
| 774 |
+
excluded = [value for value in scored if value[0] and value[1]]
|
| 775 |
+
if len(excluded) == 1:
|
| 776 |
+
selected = right if excluded[0][2] == left else left
|
| 777 |
+
warrant = f"{excluded[0][2]}'s grounded definition explicitly contrasts itself with {requested}"
|
| 778 |
+
else:
|
| 779 |
+
ranked = sorted(scored, key=lambda value: (value[0], not value[1], value[2]))
|
| 780 |
+
if not ranked or ranked[-1][0] == 0:
|
| 781 |
+
return HandlerResult(
|
| 782 |
+
"clarify_active_comparison_dimension", "clarification",
|
| 783 |
+
(("left", left), ("right", right)), (("dimension", requested),),
|
| 784 |
+
(SemanticClause("I", "need", object_value=f"the intended relation between {requested} and the active comparison"),),
|
| 785 |
+
(("source", "active_comparison_has_no_grounded_dimension_match"),),
|
| 786 |
+
("restore_active_comparison", "test_requested_dimension", "preserve_unresolved_role"),
|
| 787 |
+
)
|
| 788 |
+
selected = ranked[-1][2]
|
| 789 |
+
warrant = f"its grounded definition has the strongest positive relation to {requested}"
|
| 790 |
+
other = right if selected == left else left
|
| 791 |
+
return HandlerResult(
|
| 792 |
+
"query_active_comparison", "answered", (("left", left), ("right", right), ("dimension", requested)),
|
| 793 |
+
(("selected", selected), ("other", other)),
|
| 794 |
+
(
|
| 795 |
+
SemanticClause(f"{selected} rather than {other}", "be", complement=f"the side of the active comparison that {requested}"),
|
| 796 |
+
SemanticClause("The warrant", "be", complement=warrant),
|
| 797 |
+
),
|
| 798 |
+
(("source", "active_definition_grounded_comparison"),),
|
| 799 |
+
("restore_active_comparison", "bind_requested_dimension", "evaluate_definition_polarity", "release_selected_role"),
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
def _decision_followup(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 803 |
+
frame = state.decision
|
| 804 |
+
if not frame:
|
| 805 |
+
return None
|
| 806 |
+
value, lower = clean(text), clean(text).casefold()
|
| 807 |
+
concern_match = self._CONCERN.match(value)
|
| 808 |
+
if concern_match:
|
| 809 |
+
concern = _trim(concern_match.group(1))
|
| 810 |
+
return HandlerResult(
|
| 811 |
+
"bind_decision_constraint", "answered", (("decision", frame["question"]),),
|
| 812 |
+
(("constraint", concern),),
|
| 813 |
+
(
|
| 814 |
+
SemanticClause(concern, "be", complement="the leading concern in the active decision"),
|
| 815 |
+
SemanticClause("The decision analysis", "give", object_value="that concern priority without treating it as the only relevant consideration", modal="will"),
|
| 816 |
+
),
|
| 817 |
+
(("source", "active_typed_decision_frame"),),
|
| 818 |
+
("restore_decision_frame", "bind_user_stated_constraint", "preserve_user_authority"),
|
| 819 |
+
)
|
| 820 |
+
concerns = [str(value) for value in frame.get("concerns", ())]
|
| 821 |
+
concern = concerns[-1] if concerns else "the largest uncertainty"
|
| 822 |
+
if re.search(r"\bwhat\s+should\s+i\s+examine\s+first\b|\bwhere\s+should\s+i\s+start\b", lower):
|
| 823 |
+
return HandlerResult(
|
| 824 |
+
"prioritize_decision_inquiry", "answered", (("decision", frame["question"]),),
|
| 825 |
+
(("leading_constraint", concern),),
|
| 826 |
+
(
|
| 827 |
+
SemanticClause(
|
| 828 |
+
"You", "examine", object_value=concern,
|
| 829 |
+
qualifiers=(("first", "by making it concrete and measurable"),), modal="should",
|
| 830 |
+
),
|
| 831 |
+
SemanticClause("That examination", "separate", object_value="downside exposure, available buffer, time horizon, and recovery options", modal="should"),
|
| 832 |
+
SemanticClause("A reversible test", "show", object_value="how much of the uncertainty can be reduced before the full decision is made", modal="can"),
|
| 833 |
+
),
|
| 834 |
+
(("source", "native_decision_relation_schema"),),
|
| 835 |
+
("restore_decision_frame", "rank_user_constraint", "decompose_exposure_and_reversibility", "preserve_choice_authority"),
|
| 836 |
+
)
|
| 837 |
+
if re.search(r"\bsummarize\b.*\bdecision\b|\bdecision\s+structure\b", lower):
|
| 838 |
+
concerns_text = ", ".join(concerns) if concerns else "no dominant constraint has yet been named"
|
| 839 |
+
return HandlerResult(
|
| 840 |
+
"summarize_active_decision", "answered", (("decision", frame["question"]),),
|
| 841 |
+
(("concerns", concerns),),
|
| 842 |
+
(
|
| 843 |
+
SemanticClause("The active decision", "concern", object_value=str(frame["question"])),
|
| 844 |
+
SemanticClause("The comparison", "include", object_value="the desired outcome, alternatives, benefits, costs, timing, uncertainty, and reversibility"),
|
| 845 |
+
SemanticClause("The leading constraint", "be", complement=concerns_text),
|
| 846 |
+
SemanticClause("The next inquiry", "be", complement=f"to measure {concern} and identify a reversible way to reduce it"),
|
| 847 |
+
),
|
| 848 |
+
(("source", "active_typed_decision_frame"),),
|
| 849 |
+
("restore_decision_frame", "collect_bound_constraints", "compose_deliberative_summary"),
|
| 850 |
+
)
|
| 851 |
+
return None
|
| 852 |
+
|
| 853 |
+
def _paper_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 854 |
+
start = self._PAPER_START.match(clean(text))
|
| 855 |
+
if start and "rhetorical_article_equivalence" in self._k102_ablations:
|
| 856 |
+
surface = WORD.findall(clean(text).casefold())
|
| 857 |
+
if "an" in surface:
|
| 858 |
+
start = None
|
| 859 |
+
if start:
|
| 860 |
+
claim = _trim(start.group(1))
|
| 861 |
+
return HandlerResult(
|
| 862 |
+
"initialize_rhetorical_artifact", "answered", (("genre", "paper"), ("claim", claim)),
|
| 863 |
+
(("artifact_state", "initialized"),),
|
| 864 |
+
(
|
| 865 |
+
SemanticClause("The working claim", "be", complement=f"that {claim}"),
|
| 866 |
+
SemanticClause("The paper", "define", object_value="its terms, construct its positive case, test an opposing explanation, and state the conditions under which the claim holds", modal="will"),
|
| 867 |
+
),
|
| 868 |
+
(("source", "native_rhetorical_construction_algebra"),),
|
| 869 |
+
("recognize_rhetorical_goal", "bind_claim", "initialize_revisable_artifact"),
|
| 870 |
+
)
|
| 871 |
+
frame = state.paper
|
| 872 |
+
if not frame:
|
| 873 |
+
return None
|
| 874 |
+
lower = clean(text).casefold()
|
| 875 |
+
claim = str(frame["claim"])
|
| 876 |
+
if all(word in lower for word in ("thesis", "section", "objection", "conclusion")):
|
| 877 |
+
objection = (
|
| 878 |
+
"modularity can merely move complexity into interfaces, where cross-component interactions "
|
| 879 |
+
"and distributed responsibility become difficult to inspect"
|
| 880 |
+
)
|
| 881 |
+
conclusion = (
|
| 882 |
+
f"{claim}, provided that interfaces, provenance, composition rules, and system-level behavior remain testable"
|
| 883 |
+
)
|
| 884 |
+
return HandlerResult(
|
| 885 |
+
"construct_rhetorical_artifact", "answered", (("genre", "paper"), ("claim", claim)),
|
| 886 |
+
(("sections", 3), ("objection", objection), ("conclusion", conclusion)),
|
| 887 |
+
(
|
| 888 |
+
SemanticClause("The thesis", "be", complement=f"that {claim}"),
|
| 889 |
+
SemanticClause("Section one", "define", object_value="the units of comparison, the meaning of auditability, and the scope of the claim"),
|
| 890 |
+
SemanticClause("Section two", "explain", object_value="how decomposition, explicit interfaces, local tests, and provenance can expose the path from evidence to output"),
|
| 891 |
+
SemanticClause("Section three", "compare", object_value="those benefits with monolithic integration, end-to-end optimization, and system-level failure modes"),
|
| 892 |
+
SemanticClause("The strongest objection", "be", complement=objection),
|
| 893 |
+
SemanticClause("The conclusion", "be", complement=conclusion),
|
| 894 |
+
),
|
| 895 |
+
(("source", "native_rhetorical_construction_algebra"),),
|
| 896 |
+
("restore_artifact", "instantiate_thesis", "order_supporting_sections", "construct_strong_objection", "bind_conditional_conclusion"),
|
| 897 |
+
)
|
| 898 |
+
if re.search(r"\bstrengthen\b.*\bobjection\b", lower):
|
| 899 |
+
objection = (
|
| 900 |
+
"a modular system may be less auditable in practice when emergent failures arise only from interactions among individually transparent components, while interface proliferation diffuses causal and institutional responsibility"
|
| 901 |
+
)
|
| 902 |
+
return HandlerResult(
|
| 903 |
+
"revise_rhetorical_objection", "answered", (("claim", claim),),
|
| 904 |
+
(("thesis_preserved", True), ("objection", objection)),
|
| 905 |
+
(
|
| 906 |
+
SemanticClause("The strengthened objection", "be", complement=objection),
|
| 907 |
+
SemanticClause("The thesis", "survive", object_value="only if the audit covers compositions and interfaces as well as individual modules"),
|
| 908 |
+
),
|
| 909 |
+
(("source", "active_rhetorical_artifact"),),
|
| 910 |
+
("restore_artifact", "preserve_thesis", "increase_opposing_explanatory_scope", "revise_conditional_warrant"),
|
| 911 |
+
)
|
| 912 |
+
return None
|
| 913 |
+
|
| 914 |
+
def _synthesis_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 915 |
+
start = self._SYNTHESIS_START.match(clean(text))
|
| 916 |
+
if start:
|
| 917 |
+
source, left, right = (_trim(start.group(index)) for index in (1, 2, 3))
|
| 918 |
+
return HandlerResult(
|
| 919 |
+
"construct_mediated_cross_domain_synthesis", "answered",
|
| 920 |
+
(("source", source), ("endpoint_left", left), ("endpoint_right", right)),
|
| 921 |
+
(("paths", 3), ("epistemic_status", "conditional_hypothesis")),
|
| 922 |
+
(
|
| 923 |
+
SemanticClause(source, "alter", object_value=f"physical resources or processes relevant to {left}", modal="may"),
|
| 924 |
+
SemanticClause("The resulting constraint", "affect", object_value=f"{left} through availability, demand, substitution, and adaptation", modal="may"),
|
| 925 |
+
SemanticClause(f"Changes in {left}", "affect", object_value=f"{right} through distribution, institutional response, and public trust", modal="may"),
|
| 926 |
+
SemanticClause("The last link", "be", complement="the least direct because it contains the most mediators and competing causes"),
|
| 927 |
+
SemanticClause("This chain", "be", complement="a conditional mechanism to test rather than an established prediction"),
|
| 928 |
+
),
|
| 929 |
+
(("source", "native_mediated_causal_schema"),),
|
| 930 |
+
("bind_source_and_two_endpoints", "separate_physical_economic_and_institutional_paths", "rank_mediation_depth", "preserve_hypothetical_status"),
|
| 931 |
+
)
|
| 932 |
+
frame = state.synthesis
|
| 933 |
+
if not frame:
|
| 934 |
+
return None
|
| 935 |
+
lower = clean(text).casefold()
|
| 936 |
+
source, left, right = (str(frame[key]) for key in ("source", "left", "right"))
|
| 937 |
+
requested_paths = None if "dynamic_synthesis_paths" in self._k102_ablations else self._SYNTHESIS_PATHS.match(clean(text))
|
| 938 |
+
if requested_paths or re.search(r"\bseparate\b.*\blinks?\b", lower):
|
| 939 |
+
paths = tuple(
|
| 940 |
+
re.sub(r"^the\s+", "", _trim(requested_paths.group(index)), flags=re.IGNORECASE)
|
| 941 |
+
for index in range(1, 4)
|
| 942 |
+
) if requested_paths else ("physical", "economic", "institutional")
|
| 943 |
+
return HandlerResult(
|
| 944 |
+
"decompose_active_cross_domain_synthesis", "answered",
|
| 945 |
+
(("source", source), ("endpoint_left", left), ("endpoint_right", right)),
|
| 946 |
+
(("paths", list(paths)),),
|
| 947 |
+
(
|
| 948 |
+
SemanticClause(f"The {paths[0]} link", "run", qualifiers=(("from", source), ("through", f"resource and process constraints relevant to {left}"))),
|
| 949 |
+
SemanticClause(f"The {paths[1]} link", "run", qualifiers=(("from", "those constraints"), ("through", f"availability, demand, substitution, and adaptation to {left}"))),
|
| 950 |
+
SemanticClause(f"The {paths[2]} link", "run", qualifiers=(("from", f"the distributional consequences of {left}"), ("through", f"institutions and public response to {right}"))),
|
| 951 |
+
SemanticClause("All three links", "remain", complement="conditional until their mediators are supported by evidence for the case at hand"),
|
| 952 |
+
),
|
| 953 |
+
(("source", "active_mediated_causal_schema"),),
|
| 954 |
+
("restore_synthesis", "bind_query_supplied_path_labels", "unbind_named_paths_at_terminal", "preserve_mediator_order"),
|
| 955 |
+
)
|
| 956 |
+
if re.search(r"\b(?:least|most)\s+(?:certain|uncertain)\b", lower):
|
| 957 |
+
paths = tuple(str(value) for value in frame.get("paths", ()))
|
| 958 |
+
least_certain = paths[-1] if paths else "institutional"
|
| 959 |
+
return HandlerResult(
|
| 960 |
+
"rank_active_synthesis_uncertainty", "answered",
|
| 961 |
+
(("source", source), ("endpoint", right)), (("least_certain_path", least_certain),),
|
| 962 |
+
(
|
| 963 |
+
SemanticClause(f"The {least_certain} link", "be", complement="the least certain of the three current paths"),
|
| 964 |
+
SemanticClause("It", "contain", object_value="the greatest number of mediators, institutional contingencies, feedback effects, and competing causes"),
|
| 965 |
+
SemanticClause("That ranking", "compare", object_value="structural uncertainty within this hypothesis; it does not establish the other links as facts"),
|
| 966 |
+
),
|
| 967 |
+
(("source", "active_mediated_causal_schema"),),
|
| 968 |
+
("restore_synthesis", "count_mediating_relations", "rank_structural_uncertainty", "retain_epistemic_scope"),
|
| 969 |
+
)
|
| 970 |
+
return None
|
| 971 |
+
|
| 972 |
+
def _active_topic_evidence_result(
|
| 973 |
+
self, state: TypedDiscourseState, text: str,
|
| 974 |
+
) -> HandlerResult | None:
|
| 975 |
+
"""Ground evidence follow-ups in the restored topic, not request words."""
|
| 976 |
+
topic = clean(state.active_topic or "")
|
| 977 |
+
if "active_topic_evidence" in self._k102_ablations or not topic or not re.search(
|
| 978 |
+
r"\b(?:evidence|support|supports|substantiate|substantiates|source|sources)\b",
|
| 979 |
+
clean(text), re.IGNORECASE,
|
| 980 |
+
):
|
| 981 |
+
return None
|
| 982 |
+
rows = tuple(
|
| 983 |
+
row for row in self._topic_source_rows(topic, limit=96)
|
| 984 |
+
if clean(row.get("fact", "")) and not clean(row.get("fact", "")).rstrip().endswith("?")
|
| 985 |
+
)
|
| 986 |
+
if not rows:
|
| 987 |
+
return HandlerResult(
|
| 988 |
+
"clarify_active_topic_evidence", "clarification", (("topic", topic),),
|
| 989 |
+
(("missing_role", "an authenticated source proposition about the active topic"),),
|
| 990 |
+
(SemanticClause("I", "need", object_value=f"an authenticated source proposition about {topic} before treating the requested support as evidence"),),
|
| 991 |
+
(("source", "active_topic_exact_source_lookup_empty"),),
|
| 992 |
+
("restore_active_topic", "require_exact_topic_source", "preserve_epistemic_boundary"),
|
| 993 |
+
)
|
| 994 |
+
selected = rows[:2]
|
| 995 |
+
clauses = tuple(
|
| 996 |
+
SemanticClause(
|
| 997 |
+
"The cited source" if len(selected) == 1 else f"Cited source {index}",
|
| 998 |
+
"state", object_value=clean(row["fact"]).rstrip(".?!"),
|
| 999 |
+
)
|
| 1000 |
+
for index, row in enumerate(selected, start=1)
|
| 1001 |
+
)
|
| 1002 |
+
evidence = tuple(
|
| 1003 |
+
(str(row.get("source_document_sha256", row.get("source_document", ""))), str(row.get("text_sha256", "")))
|
| 1004 |
+
for row in selected
|
| 1005 |
+
)
|
| 1006 |
+
return HandlerResult(
|
| 1007 |
+
"ground_active_topic_evidence", "answered", (("topic", topic),),
|
| 1008 |
+
(("source_rows", len(selected)),), clauses, evidence,
|
| 1009 |
+
(
|
| 1010 |
+
"restore_active_topic", "resolve_deictic_topic_reference",
|
| 1011 |
+
"require_exact_topic_posting", "rank_declarative_source_propositions",
|
| 1012 |
+
"release_authenticated_evidence_at_malkuth",
|
| 1013 |
+
),
|
| 1014 |
+
)
|
| 1015 |
+
|
| 1016 |
+
def _revise_causal_result(self, session_id: str, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 1017 |
+
match = self._CAUSAL_CORRECTION.match(clean(text))
|
| 1018 |
+
if match is None:
|
| 1019 |
+
return None
|
| 1020 |
+
subject, old_surface, new_surface, cause_surface = (_trim(match.group(index)) for index in range(1, 5))
|
| 1021 |
+
old_atom_candidates = []
|
| 1022 |
+
discourse = self._graph_discourse(session_id)
|
| 1023 |
+
old_terms = set(tokens(old_surface))
|
| 1024 |
+
for fact in reversed(discourse.facts):
|
| 1025 |
+
if not fact.active or fact.atom.subject.casefold() != subject.casefold():
|
| 1026 |
+
continue
|
| 1027 |
+
if set(tokens(fact.atom.predicate + " " + fact.atom.object_value)) & old_terms:
|
| 1028 |
+
old_atom_candidates.append(fact)
|
| 1029 |
+
old_fact = old_atom_candidates[0] if old_atom_candidates else None
|
| 1030 |
+
downstream = []
|
| 1031 |
+
if old_fact is not None:
|
| 1032 |
+
downstream_ids = [right for left, right in discourse.causal_edges if left == old_fact.state_id]
|
| 1033 |
+
downstream = [value for value in discourse.facts if value.active and value.state_id in downstream_ids]
|
| 1034 |
+
old_fact.active = False
|
| 1035 |
+
session = self._session(session_id)
|
| 1036 |
+
if session.workspace.is_visible(old_fact.state_id):
|
| 1037 |
+
session.workspace.retract(old_fact.state_id, reason="k102_causal_correction")
|
| 1038 |
+
discourse.causal_edges = [
|
| 1039 |
+
edge for edge in discourse.causal_edges if old_fact.state_id not in edge
|
| 1040 |
+
]
|
| 1041 |
+
new_effect = self.graph_transducer._simple_clause(f"{subject} {new_surface}")
|
| 1042 |
+
cause = self.graph_transducer._simple_clause(cause_surface)
|
| 1043 |
+
if new_effect is None or cause is None:
|
| 1044 |
+
return HandlerResult(
|
| 1045 |
+
"clarify_causal_revision", "clarification", (("subject", subject),),
|
| 1046 |
+
(("unresolved", "replacement cause or effect"),),
|
| 1047 |
+
(SemanticClause("I", "need", object_value="the replacement cause and the event it produced"),),
|
| 1048 |
+
(("source", "typed_causal_revision_parser"),),
|
| 1049 |
+
("retain_negated_commitment", "identify_unresolved_replacement_role"),
|
| 1050 |
+
)
|
| 1051 |
+
self._assert_causal(session_id, (cause, new_effect))
|
| 1052 |
+
for fact in downstream:
|
| 1053 |
+
self._assert_causal(session_id, (new_effect, fact.atom))
|
| 1054 |
+
retracted_identity = self._atom_identity(old_fact.atom.as_dict()) if old_fact else None
|
| 1055 |
+
revised_edges = [
|
| 1056 |
+
value for value in state.causal_edges
|
| 1057 |
+
if retracted_identity is None
|
| 1058 |
+
or (
|
| 1059 |
+
self._atom_identity(value["cause"]) != retracted_identity
|
| 1060 |
+
and self._atom_identity(value["effect"]) != retracted_identity
|
| 1061 |
+
)
|
| 1062 |
+
]
|
| 1063 |
+
revised_edges.append({"cause": cause.as_dict(), "effect": new_effect.as_dict(), "active": True})
|
| 1064 |
+
revised_edges.extend(
|
| 1065 |
+
{"cause": new_effect.as_dict(), "effect": fact.atom.as_dict(), "active": True}
|
| 1066 |
+
for fact in downstream
|
| 1067 |
+
)
|
| 1068 |
+
current = _K102_TURN.get()
|
| 1069 |
+
if current is not None:
|
| 1070 |
+
current.audit.update({
|
| 1071 |
+
"recognized": True,
|
| 1072 |
+
"discourse_operation": "causal_revision",
|
| 1073 |
+
"retracted_atom": old_fact.atom.as_dict() if old_fact else None,
|
| 1074 |
+
"replacement_cause": cause.as_dict(),
|
| 1075 |
+
"replacement_effect": new_effect.as_dict(),
|
| 1076 |
+
"revised_causal_edges": revised_edges,
|
| 1077 |
+
})
|
| 1078 |
+
chain = [cause, new_effect, *(value.atom for value in downstream)]
|
| 1079 |
+
return HandlerResult(
|
| 1080 |
+
"revise_causal_discourse", "answered", (("subject", subject),),
|
| 1081 |
+
(("retracted", old_fact.atom.as_dict() if old_fact else None), ("replacement_edges", max(1, len(chain) - 1))),
|
| 1082 |
+
tuple(self._past_atom_clause(value) for value in chain),
|
| 1083 |
+
(("source", "conversation_local_causal_revision"),),
|
| 1084 |
+
("resolve_corrected_subject", "retract_superseded_cause", "preserve_unaffected_downstream_effect", "bind_replacement_path", "propagate_revision"),
|
| 1085 |
+
)
|
| 1086 |
+
|
| 1087 |
+
def _causal_query_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 1088 |
+
if not state.causal_edges:
|
| 1089 |
+
return None
|
| 1090 |
+
lower = clean(text).casefold()
|
| 1091 |
+
if not (
|
| 1092 |
+
re.match(r"^why\b", lower)
|
| 1093 |
+
or re.search(r"\b(?:whole\s+sequence|causal\s+order|what\s+(?:now\s+)?explains?)\b", lower)
|
| 1094 |
+
):
|
| 1095 |
+
return None
|
| 1096 |
+
chain = self._edge_chain(state, text)
|
| 1097 |
+
if len(chain) < 2:
|
| 1098 |
+
return None
|
| 1099 |
+
atoms = tuple(self._atom_from_mapping(value) for value in chain)
|
| 1100 |
+
return HandlerResult(
|
| 1101 |
+
"query_persistent_causal_discourse", "answered", (("path_length", len(atoms)),),
|
| 1102 |
+
(("atoms", [value.as_dict() for value in atoms]),),
|
| 1103 |
+
tuple(self._past_atom_clause(value) for value in atoms),
|
| 1104 |
+
(("source", "persistent_typed_causal_discourse"),),
|
| 1105 |
+
("restore_causal_edges", "select_requested_downstream_effect", "traverse_active_reverse_edges", "release_revised_chain"),
|
| 1106 |
+
)
|
| 1107 |
+
|
| 1108 |
+
def _tool_continuity_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 1109 |
+
prior = state.last_tool
|
| 1110 |
+
lower = clean(text).casefold()
|
| 1111 |
+
if not prior or not re.search(r"\bcompare\b", lower) or not re.search(r"\b(?:strongest|largest|greatest|maximum)\b", lower):
|
| 1112 |
+
return None
|
| 1113 |
+
backend = self.tool_backend
|
| 1114 |
+
expert = getattr(backend, "experts", {}).get(prior.get("expert_id")) if backend else None
|
| 1115 |
+
field_name = str(prior.get("field") or "")
|
| 1116 |
+
if expert is None or not field_name:
|
| 1117 |
+
return None
|
| 1118 |
+
specification = next((value for value in expert.schema.fields if value.name == field_name), None)
|
| 1119 |
+
if specification is None or specification.kind not in {"integer", "number"}:
|
| 1120 |
+
return None
|
| 1121 |
+
previous_row = next((
|
| 1122 |
+
value for value in expert.rows
|
| 1123 |
+
if str(value.get("event_id") or value.get("country_name") or "") == str(prior.get("row_key"))
|
| 1124 |
+
), None)
|
| 1125 |
+
if previous_row is None:
|
| 1126 |
+
return None
|
| 1127 |
+
strongest = max(expert.rows, key=lambda value: float(value[field_name]))
|
| 1128 |
+
previous_value = float(previous_row[field_name])
|
| 1129 |
+
strongest_value = float(strongest[field_name])
|
| 1130 |
+
difference = abs(strongest_value - previous_value)
|
| 1131 |
+
display = lambda value: str(int(value)) if float(value).is_integer() else format(float(value), ".12g")
|
| 1132 |
+
strongest_key = str(strongest.get("event_id") or strongest.get("country_name") or "registered row")
|
| 1133 |
+
previous_key = str(prior["row_key"])
|
| 1134 |
+
values = [display(previous_value), display(strongest_value), display(difference)]
|
| 1135 |
+
audit = {
|
| 1136 |
+
"schema_version": SCHEMA,
|
| 1137 |
+
"operation": "compare",
|
| 1138 |
+
"expert_id": expert.schema.expert_id,
|
| 1139 |
+
"row_key": previous_key,
|
| 1140 |
+
"comparison_row_key": strongest_key,
|
| 1141 |
+
"field": field_name,
|
| 1142 |
+
"values": values,
|
| 1143 |
+
"read_only": True,
|
| 1144 |
+
"typed_arguments_bound": True,
|
| 1145 |
+
"goal_postconditions_satisfied": True,
|
| 1146 |
+
"source_row_restored_from_discourse": True,
|
| 1147 |
+
"traditional_language_model": False,
|
| 1148 |
+
}
|
| 1149 |
+
return HandlerResult(
|
| 1150 |
+
"bounded_agentic_tool", "answered",
|
| 1151 |
+
(("operation", "compare"), ("expert_id", expert.schema.expert_id), ("field", field_name)),
|
| 1152 |
+
(("values", values), ("difference", values[2])),
|
| 1153 |
+
(
|
| 1154 |
+
SemanticClause(f"The {field_name} for {previous_key}", "be", complement=values[0]),
|
| 1155 |
+
SemanticClause(f"The maximum {field_name}", "be", complement=values[1], qualifiers=(("at", strongest_key),)),
|
| 1156 |
+
SemanticClause(f"The absolute {field_name} difference", "be", complement=values[2]),
|
| 1157 |
+
),
|
| 1158 |
+
(("expert_id", expert.schema.expert_id),),
|
| 1159 |
+
("restore_typed_tool_observation", "select_registered_numeric_field", "execute_read_only_extremum", "compute_typed_difference", "verify_goal_postconditions"),
|
| 1160 |
+
tool_audit=audit,
|
| 1161 |
+
)
|
| 1162 |
+
|
| 1163 |
+
def _discourse_result(self, session_id: str, text: str) -> HandlerResult | None:
|
| 1164 |
+
current = _K102_TURN.get()
|
| 1165 |
+
if current is None:
|
| 1166 |
+
return None
|
| 1167 |
+
state = current.state
|
| 1168 |
+
if self._identity_request(text):
|
| 1169 |
+
current.audit.update({"recognized": True, "discourse_operation": "situated_identity"})
|
| 1170 |
+
return self._constitutional_identity_result(current.constitution)
|
| 1171 |
+
correction = self._revise_causal_result(session_id, state, text)
|
| 1172 |
+
if correction is not None:
|
| 1173 |
+
return correction
|
| 1174 |
+
causal_query = self._causal_query_result(state, text)
|
| 1175 |
+
if causal_query is not None:
|
| 1176 |
+
current.audit.update({"recognized": True, "discourse_operation": "persistent_causal_query"})
|
| 1177 |
+
return causal_query
|
| 1178 |
+
tool = self._tool_continuity_result(state, text)
|
| 1179 |
+
if tool is not None:
|
| 1180 |
+
current.audit.update({"recognized": True, "discourse_operation": "tool_observation_comparison"})
|
| 1181 |
+
return tool
|
| 1182 |
+
shift = self._SHIFT_COMPARE.match(clean(text))
|
| 1183 |
+
if shift:
|
| 1184 |
+
left, right = (_trim(shift.group(index)) for index in (1, 2))
|
| 1185 |
+
result = self._situated_result(f"Compare {left} and {right}.")
|
| 1186 |
+
if result is not None:
|
| 1187 |
+
current.audit.update({
|
| 1188 |
+
"recognized": True, "discourse_operation": "suspend_topic_and_compare",
|
| 1189 |
+
"left": left, "right": right,
|
| 1190 |
+
})
|
| 1191 |
+
return HandlerResult(
|
| 1192 |
+
"discourse_topic_shift_comparison", result.status, result.arguments, result.result,
|
| 1193 |
+
result.clauses, result.evidence,
|
| 1194 |
+
("suspend_active_topic", *result.computation_trace, "bind_temporary_comparison_topic"),
|
| 1195 |
+
tool_audit=result.tool_audit, memory_audit=result.memory_audit,
|
| 1196 |
+
)
|
| 1197 |
+
returning = self._RETURN_TOPIC.match(clean(text))
|
| 1198 |
+
if returning:
|
| 1199 |
+
target = state.topic_stack[-1] if state.topic_stack else state.active_topic
|
| 1200 |
+
if target:
|
| 1201 |
+
current.audit.update({"recognized": True, "discourse_operation": "restore_topic", "topic": target})
|
| 1202 |
+
return HandlerResult(
|
| 1203 |
+
"restore_prior_topic", "answered", (("topic", target),), (("restored", True),),
|
| 1204 |
+
(
|
| 1205 |
+
SemanticClause("Our earlier topic", "be", complement=target),
|
| 1206 |
+
SemanticClause("We", "examine", object_value=f"{target} through its active questions and relations", tense="past"),
|
| 1207 |
+
),
|
| 1208 |
+
(("source", "typed_topic_stack"),),
|
| 1209 |
+
("read_topic_stack", "pop_suspended_topic", "restore_discourse_focus"),
|
| 1210 |
+
)
|
| 1211 |
+
topic = self._TOPIC_INVITATION.match(clean(text))
|
| 1212 |
+
if topic:
|
| 1213 |
+
subject = _trim(topic.group(1))
|
| 1214 |
+
current.audit.update({"recognized": True, "discourse_operation": "bind_topic", "topic": subject})
|
| 1215 |
+
return self._topic_result(subject)
|
| 1216 |
+
paper = self._paper_result(state, text)
|
| 1217 |
+
if paper is not None:
|
| 1218 |
+
current.audit.update({"recognized": True, "discourse_operation": paper.operation})
|
| 1219 |
+
return paper
|
| 1220 |
+
synthesis = self._synthesis_result(state, text)
|
| 1221 |
+
if synthesis is not None:
|
| 1222 |
+
current.audit.update({"recognized": True, "discourse_operation": synthesis.operation})
|
| 1223 |
+
return synthesis
|
| 1224 |
+
topic_evidence = self._active_topic_evidence_result(state, text)
|
| 1225 |
+
if topic_evidence is not None:
|
| 1226 |
+
current.audit.update({
|
| 1227 |
+
"recognized": True,
|
| 1228 |
+
"discourse_operation": topic_evidence.operation,
|
| 1229 |
+
"topic": state.active_topic,
|
| 1230 |
+
})
|
| 1231 |
+
return topic_evidence
|
| 1232 |
+
comparison = self._comparison_followup(state, text)
|
| 1233 |
+
if comparison is not None:
|
| 1234 |
+
current.audit.update({"recognized": True, "discourse_operation": comparison.operation})
|
| 1235 |
+
return comparison
|
| 1236 |
+
decision = self._decision_followup(state, text)
|
| 1237 |
+
if decision is not None:
|
| 1238 |
+
current.audit.update({"recognized": True, "discourse_operation": decision.operation})
|
| 1239 |
+
return decision
|
| 1240 |
+
return None
|
| 1241 |
+
|
| 1242 |
+
@staticmethod
|
| 1243 |
+
def _resolve_topic_anaphor(state: TypedDiscourseState, text: str) -> tuple[str, str | None]:
|
| 1244 |
+
topic = clean(state.active_topic or "")
|
| 1245 |
+
value = clean(text)
|
| 1246 |
+
if not topic:
|
| 1247 |
+
return value, None
|
| 1248 |
+
patterns = (
|
| 1249 |
+
r"\b(?:it|its)\b",
|
| 1250 |
+
r"\b(?:this|that)\s+(?:topic|subject|process|system|idea|claim)\b",
|
| 1251 |
+
)
|
| 1252 |
+
for pattern in patterns:
|
| 1253 |
+
if re.search(pattern, value, re.IGNORECASE):
|
| 1254 |
+
return re.sub(pattern, topic, value, count=1, flags=re.IGNORECASE), topic
|
| 1255 |
+
return value, None
|
| 1256 |
+
|
| 1257 |
+
@staticmethod
|
| 1258 |
+
def _with_topic_resolution(
|
| 1259 |
+
result: HandlerResult,
|
| 1260 |
+
*,
|
| 1261 |
+
topic: str,
|
| 1262 |
+
original_text: str,
|
| 1263 |
+
) -> HandlerResult:
|
| 1264 |
+
return HandlerResult(
|
| 1265 |
+
result.operation,
|
| 1266 |
+
result.status,
|
| 1267 |
+
(*result.arguments, ("resolved_topic_anaphor", topic)),
|
| 1268 |
+
result.result,
|
| 1269 |
+
result.clauses,
|
| 1270 |
+
(*result.evidence, ("topic_resolution", digest({"topic": topic, "surface": clean(original_text)}))),
|
| 1271 |
+
("restore_active_topic", "resolve_deictic_topic_reference", *result.computation_trace),
|
| 1272 |
+
tool_audit=result.tool_audit,
|
| 1273 |
+
memory_audit=result.memory_audit,
|
| 1274 |
+
)
|
| 1275 |
+
|
| 1276 |
+
def _repair_graph_assertion_surface(
|
| 1277 |
+
self, result: HandlerResult | None, surface: str,
|
| 1278 |
+
) -> HandlerResult | None:
|
| 1279 |
+
if (
|
| 1280 |
+
result is None
|
| 1281 |
+
or result.operation != "assert_graph_fact"
|
| 1282 |
+
or "finite_number_recovery" in self._k102_ablations
|
| 1283 |
+
):
|
| 1284 |
+
return result
|
| 1285 |
+
atom = self.graph_transducer._simple_clause(surface)
|
| 1286 |
+
finite = self.graph_transducer.finite_surface(surface, atom) if atom is not None else None
|
| 1287 |
+
if atom is None or finite is None:
|
| 1288 |
+
return result
|
| 1289 |
+
lemma, _listed = self.dictionary.lemma(finite)
|
| 1290 |
+
exceptions = self.dictionary.reverse_exceptions.get(lemma, ())
|
| 1291 |
+
is_past = bool(
|
| 1292 |
+
finite in exceptions
|
| 1293 |
+
or finite.endswith(("ed", "ied", "en"))
|
| 1294 |
+
or re.search(r"\b(?:did|was|were|had)\b", clean(surface), re.IGNORECASE)
|
| 1295 |
+
)
|
| 1296 |
+
clause = self._atom_clause(atom)
|
| 1297 |
+
if is_past and atom.predicate not in {"is_a", "property", "be"}:
|
| 1298 |
+
clause = replace(clause, tense="past")
|
| 1299 |
+
return HandlerResult(
|
| 1300 |
+
result.operation, result.status, result.arguments, result.result,
|
| 1301 |
+
(clause,), result.evidence,
|
| 1302 |
+
(*result.computation_trace, "recover_finite_surface_tense_and_nominal_number"),
|
| 1303 |
+
tool_audit=result.tool_audit, memory_audit=result.memory_audit,
|
| 1304 |
+
)
|
| 1305 |
+
|
| 1306 |
+
def _handle_evidence(self, text: str, context: Mapping[str, Any]) -> HandlerResult | None:
|
| 1307 |
+
current = _K102_TURN.get()
|
| 1308 |
+
session_id = current.session_id if current else str(context.get("k102_session_id", ""))
|
| 1309 |
+
result = self._discourse_result(session_id, text) if session_id else None
|
| 1310 |
+
if result is not None:
|
| 1311 |
+
return result
|
| 1312 |
+
resolved, topic = self._resolve_topic_anaphor(current.state, text) if current else (text, None)
|
| 1313 |
+
inherited = self._repair_graph_assertion_surface(
|
| 1314 |
+
super()._handle_evidence(resolved, context), resolved,
|
| 1315 |
+
)
|
| 1316 |
+
if inherited is not None and topic is not None:
|
| 1317 |
+
current.audit.update({
|
| 1318 |
+
"recognized": True,
|
| 1319 |
+
"discourse_operation": "resolve_topic_anaphor",
|
| 1320 |
+
"topic": topic,
|
| 1321 |
+
"resolved_surface_sha256": hashlib.sha256(resolved.encode("utf-8")).hexdigest(),
|
| 1322 |
+
})
|
| 1323 |
+
return self._with_topic_resolution(inherited, topic=topic, original_text=text)
|
| 1324 |
+
return inherited
|
| 1325 |
+
|
| 1326 |
+
def _handle_event(self, session_id: str, text: str, turn_id: str) -> HandlerResult | None:
|
| 1327 |
+
result = self._discourse_result(session_id, text)
|
| 1328 |
+
if result is not None:
|
| 1329 |
+
return result
|
| 1330 |
+
current = _K102_TURN.get()
|
| 1331 |
+
resolved, topic = self._resolve_topic_anaphor(current.state, text) if current else (text, None)
|
| 1332 |
+
inherited = self._repair_graph_assertion_surface(
|
| 1333 |
+
super()._handle_event(session_id, resolved, turn_id), resolved,
|
| 1334 |
+
)
|
| 1335 |
+
if inherited is not None and topic is not None:
|
| 1336 |
+
current.audit.update({
|
| 1337 |
+
"recognized": True,
|
| 1338 |
+
"discourse_operation": "resolve_topic_anaphor",
|
| 1339 |
+
"topic": topic,
|
| 1340 |
+
"resolved_surface_sha256": hashlib.sha256(resolved.encode("utf-8")).hexdigest(),
|
| 1341 |
+
})
|
| 1342 |
+
return self._with_topic_resolution(inherited, topic=topic, original_text=text)
|
| 1343 |
+
return inherited
|
| 1344 |
+
|
| 1345 |
+
def _handle_graph(self, session_id: str, graph: Any) -> HandlerResult | None:
|
| 1346 |
+
result = super()._handle_graph(session_id, graph)
|
| 1347 |
+
if result is None or result.operation != "assert_graph_fact":
|
| 1348 |
+
return result
|
| 1349 |
+
current = _K102_TURN.get()
|
| 1350 |
+
surface = clean(current.input_surface) if current else ""
|
| 1351 |
+
return self._repair_graph_assertion_surface(result, surface) if surface else result
|
| 1352 |
+
|
| 1353 |
+
def _event_capability_family(self, text: str) -> tuple[str, ...]:
|
| 1354 |
+
lower = clean(text).casefold()
|
| 1355 |
+
if (
|
| 1356 |
+
self._TOPIC_INVITATION.match(clean(text))
|
| 1357 |
+
or self._RETURN_TOPIC.match(clean(text))
|
| 1358 |
+
or self._SHIFT_COMPARE.match(clean(text))
|
| 1359 |
+
or self._PAPER_START.match(clean(text))
|
| 1360 |
+
or self._SYNTHESIS_START.match(clean(text))
|
| 1361 |
+
or self._CAUSAL_CORRECTION.match(clean(text))
|
| 1362 |
+
or re.search(r"\b(?:thesis|section|objection|conclusion|concern|(?:one|1)\s+sentence|earlier\s+topic)\b", lower)
|
| 1363 |
+
):
|
| 1364 |
+
return ("linguistic", "synthesis", "epistemic", "contrast", "causal", "mechanism", "tool_router", "dataset", "provenance")
|
| 1365 |
+
return super()._event_capability_family(text)
|
| 1366 |
+
|
| 1367 |
+
def _record_parent_state(
|
| 1368 |
+
self,
|
| 1369 |
+
state: TypedDiscourseState,
|
| 1370 |
+
text: str,
|
| 1371 |
+
output: Mapping[str, Any],
|
| 1372 |
+
audit: Mapping[str, Any],
|
| 1373 |
+
) -> None:
|
| 1374 |
+
operation = str(output.get("operation", ""))
|
| 1375 |
+
arguments = self._proof_arguments(output)
|
| 1376 |
+
clauses = self._proof_clauses(output)
|
| 1377 |
+
if operation in {"relational_lexical_comparison", "discourse_topic_shift_comparison"}:
|
| 1378 |
+
left, right = clean(arguments.get("left", audit.get("left", ""))), clean(arguments.get("right", audit.get("right", "")))
|
| 1379 |
+
complements = [clean(value.get("complement", "")) for value in clauses if value.get("complement")]
|
| 1380 |
+
if left and right:
|
| 1381 |
+
state.comparison = {
|
| 1382 |
+
"left": left, "right": right,
|
| 1383 |
+
"left_definition": complements[0] if complements else "the first grounded concept",
|
| 1384 |
+
"right_definition": complements[1] if len(complements) > 1 else "the second grounded concept",
|
| 1385 |
+
}
|
| 1386 |
+
self._push_topic(state, f"comparison of {left} and {right}")
|
| 1387 |
+
elif operation == "conversational_decision_frame":
|
| 1388 |
+
question = clean(arguments.get("decision", text))
|
| 1389 |
+
state.decision = {"question": question, "concerns": []}
|
| 1390 |
+
self._push_topic(state, question)
|
| 1391 |
+
elif operation == "bind_decision_constraint" and state.decision:
|
| 1392 |
+
result = {str(key): value for key, value in (output.get("computation_proof", {}) or {}).get("result", ())}
|
| 1393 |
+
concern = clean(result.get("constraint", ""))
|
| 1394 |
+
if concern and concern not in state.decision.setdefault("concerns", []):
|
| 1395 |
+
state.decision["concerns"].append(concern)
|
| 1396 |
+
elif operation == "conversational_topic_invitation":
|
| 1397 |
+
topic = clean(arguments.get("topic", audit.get("topic", "")))
|
| 1398 |
+
if topic:
|
| 1399 |
+
self._push_topic(state, topic)
|
| 1400 |
+
elif operation == "restore_prior_topic":
|
| 1401 |
+
target = clean(audit.get("topic", ""))
|
| 1402 |
+
if target:
|
| 1403 |
+
if state.active_topic and state.active_topic.casefold().startswith("comparison of "):
|
| 1404 |
+
state.comparison = None
|
| 1405 |
+
if state.topic_stack and state.topic_stack[-1].casefold() == target.casefold():
|
| 1406 |
+
state.topic_stack.pop()
|
| 1407 |
+
state.active_topic = target
|
| 1408 |
+
elif operation == "initialize_rhetorical_artifact":
|
| 1409 |
+
claim = clean(arguments.get("claim", ""))
|
| 1410 |
+
state.paper = {"claim": claim, "revision": 0}
|
| 1411 |
+
if claim:
|
| 1412 |
+
self._push_topic(state, claim)
|
| 1413 |
+
elif operation in {"construct_rhetorical_artifact", "revise_rhetorical_objection"} and state.paper:
|
| 1414 |
+
state.paper["revision"] = int(state.paper.get("revision", 0)) + 1
|
| 1415 |
+
result = {str(key): value for key, value in (output.get("computation_proof", {}) or {}).get("result", ())}
|
| 1416 |
+
for key in ("sections", "objection", "conclusion"):
|
| 1417 |
+
if key in result:
|
| 1418 |
+
state.paper[key] = result[key]
|
| 1419 |
+
elif operation == "construct_mediated_cross_domain_synthesis":
|
| 1420 |
+
state.synthesis = {
|
| 1421 |
+
"source": clean(arguments.get("source", "")),
|
| 1422 |
+
"left": clean(arguments.get("endpoint_left", "")),
|
| 1423 |
+
"right": clean(arguments.get("endpoint_right", "")),
|
| 1424 |
+
"epistemic_status": "conditional_hypothesis",
|
| 1425 |
+
}
|
| 1426 |
+
self._push_topic(state, f"effects of {state.synthesis['source']}")
|
| 1427 |
+
elif operation == "decompose_active_cross_domain_synthesis" and state.synthesis:
|
| 1428 |
+
result = {str(key): value for key, value in (output.get("computation_proof", {}) or {}).get("result", ())}
|
| 1429 |
+
paths = [clean(value) for value in result.get("paths", ()) if clean(value)]
|
| 1430 |
+
if paths:
|
| 1431 |
+
state.synthesis["paths"] = paths
|
| 1432 |
+
elif operation == "compose_causal_graph":
|
| 1433 |
+
cause, effect = arguments.get("cause"), arguments.get("effect")
|
| 1434 |
+
if isinstance(cause, Mapping) and isinstance(effect, Mapping):
|
| 1435 |
+
edge = {"cause": dict(cause), "effect": dict(effect), "active": True}
|
| 1436 |
+
identity = (self._atom_identity(edge["cause"]), self._atom_identity(edge["effect"]))
|
| 1437 |
+
if not any(
|
| 1438 |
+
(self._atom_identity(value["cause"]), self._atom_identity(value["effect"])) == identity
|
| 1439 |
+
for value in state.causal_edges
|
| 1440 |
+
):
|
| 1441 |
+
state.causal_edges.append(edge)
|
| 1442 |
+
elif operation == "revise_causal_discourse" and audit.get("revised_causal_edges"):
|
| 1443 |
+
state.causal_edges = [dict(value) for value in audit["revised_causal_edges"]]
|
| 1444 |
+
tool = output.get("tool_audit") or {}
|
| 1445 |
+
if output.get("status") == "answered" and tool.get("expert_id") and tool.get("row_key"):
|
| 1446 |
+
state.last_tool = {
|
| 1447 |
+
"expert_id": str(tool["expert_id"]),
|
| 1448 |
+
"row_key": str(tool["row_key"]),
|
| 1449 |
+
"field": None if tool.get("field") is None else str(tool["field"]),
|
| 1450 |
+
"values": [str(value) for value in tool.get("values", ())],
|
| 1451 |
+
"operation": str(tool.get("operation", operation)),
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
def _bind_discourse_event(
|
| 1455 |
+
self,
|
| 1456 |
+
session_id: str,
|
| 1457 |
+
output: Mapping[str, Any],
|
| 1458 |
+
state: TypedDiscourseState,
|
| 1459 |
+
journal: Mapping[str, Any],
|
| 1460 |
+
) -> tuple[str, Mapping[str, Any]]:
|
| 1461 |
+
situation = "typed_discourse:" + "+".join(state.active_frame_names() or ("open",))
|
| 1462 |
+
turn_id = str(output.get("turn_id") or ("k102-" + str(journal["event_sha256"])[:20]))
|
| 1463 |
+
memory_id = self._bind_memory(
|
| 1464 |
+
session_id,
|
| 1465 |
+
turn_id + "-k102-" + str(journal["sequence"]),
|
| 1466 |
+
{
|
| 1467 |
+
"situation": situation,
|
| 1468 |
+
"goal": state.active_topic or "sustain situated conversation",
|
| 1469 |
+
"transition": str(output.get("operation", "discourse_transition")),
|
| 1470 |
+
"action": str(output.get("status", "unknown")),
|
| 1471 |
+
"result": state.state_sha256,
|
| 1472 |
+
"field": ",".join(state.active_frame_names()) or "open_discourse",
|
| 1473 |
+
"status": "verified_typed_discourse_state",
|
| 1474 |
+
"verifier": str(journal["event_sha256"]),
|
| 1475 |
+
},
|
| 1476 |
+
bank="episodic",
|
| 1477 |
+
source="k102_typed_discourse_event",
|
| 1478 |
+
)
|
| 1479 |
+
return memory_id, self.substrate.cycle_once()
|
| 1480 |
+
|
| 1481 |
+
def answer(
|
| 1482 |
+
self,
|
| 1483 |
+
session_id: str,
|
| 1484 |
+
text: str,
|
| 1485 |
+
context: Mapping[str, Any] | None = None,
|
| 1486 |
+
*,
|
| 1487 |
+
system_prompt: str | None = None,
|
| 1488 |
+
**kwargs: Any,
|
| 1489 |
+
) -> dict[str, Any]:
|
| 1490 |
+
state, recovery = self.discourse_journal.load(session_id)
|
| 1491 |
+
previous_state_sha = ZERO_SHA256 if not recovery["recovered"] else state.state_sha256
|
| 1492 |
+
profile = compile_identity_profile(system_prompt or self.default_system_prompt)
|
| 1493 |
+
constitution = compile_situated_constitution(profile)
|
| 1494 |
+
state.constitution_sha256 = constitution.constitution_sha256
|
| 1495 |
+
local_context = dict(context or {})
|
| 1496 |
+
local_context["k102_session_id"] = session_id
|
| 1497 |
+
local_context["native_situated_identity_constitution"] = constitution.routing_context()
|
| 1498 |
+
local_context["typed_discourse_state"] = {
|
| 1499 |
+
"state_sha256": state.state_sha256,
|
| 1500 |
+
"active_topic": state.active_topic,
|
| 1501 |
+
"active_frames": list(state.active_frame_names()),
|
| 1502 |
+
"turn_index": state.turn_index,
|
| 1503 |
+
}
|
| 1504 |
+
current = _TurnContext(session_id, state, constitution, clean(text))
|
| 1505 |
+
token = _K102_TURN.set(current)
|
| 1506 |
+
try:
|
| 1507 |
+
if self._k102_bound_constitutions.get(session_id) != constitution.constitution_sha256:
|
| 1508 |
+
try:
|
| 1509 |
+
self._bind_memory(
|
| 1510 |
+
session_id,
|
| 1511 |
+
"k102-constitution-" + constitution.constitution_sha256[:20],
|
| 1512 |
+
{
|
| 1513 |
+
"situation": "native_situated_identity_constitution",
|
| 1514 |
+
"goal": constitution.purpose,
|
| 1515 |
+
"transition": "maintain_self_across_situated_discourse_change",
|
| 1516 |
+
"action": constitution.agency_contract,
|
| 1517 |
+
"result": constitution.self_name,
|
| 1518 |
+
"warrant": constitution.partner_relation,
|
| 1519 |
+
"status": "constitutional_identity_invariant",
|
| 1520 |
+
"verifier": constitution.constitution_sha256,
|
| 1521 |
+
},
|
| 1522 |
+
bank="procedural",
|
| 1523 |
+
source="k102_situated_identity_constitution",
|
| 1524 |
+
)
|
| 1525 |
+
except ValueError as exc:
|
| 1526 |
+
if "event_key already bound to another event" not in str(exc):
|
| 1527 |
+
raise
|
| 1528 |
+
self._k102_bound_constitutions[session_id] = constitution.constitution_sha256
|
| 1529 |
+
output = super().answer(
|
| 1530 |
+
session_id, text, local_context, system_prompt=system_prompt, **kwargs,
|
| 1531 |
+
)
|
| 1532 |
+
self._record_parent_state(state, text, output, current.audit)
|
| 1533 |
+
state.turn_index += 1
|
| 1534 |
+
state.last_operation = str(output.get("operation", ""))
|
| 1535 |
+
state.last_response_sha256 = hashlib.sha256(str(output.get("response", "")).encode("utf-8")).hexdigest()
|
| 1536 |
+
state.constitution_sha256 = constitution.constitution_sha256
|
| 1537 |
+
proof_sha = str((output.get("computation_proof") or {}).get("proof_sha256", ""))
|
| 1538 |
+
journal = self.discourse_journal.append(
|
| 1539 |
+
session_id,
|
| 1540 |
+
text=text,
|
| 1541 |
+
operation=str(output.get("operation", "")),
|
| 1542 |
+
status=str(output.get("status", "")),
|
| 1543 |
+
previous_state_sha256=previous_state_sha,
|
| 1544 |
+
state=state,
|
| 1545 |
+
proof_sha256=proof_sha,
|
| 1546 |
+
)
|
| 1547 |
+
memory_id, cycle = self._bind_discourse_event(session_id, output, state, journal)
|
| 1548 |
+
spine_record = self._tree_record(session_id, str(output.get("turn_id", "")))
|
| 1549 |
+
spine = (
|
| 1550 |
+
spine_record.get("dual_rail_contract", {})
|
| 1551 |
+
.get("stabilizing_spine", {})
|
| 1552 |
+
.get("stage5k102:spine:situated_identity_discourse", {})
|
| 1553 |
+
)
|
| 1554 |
+
finally:
|
| 1555 |
+
_K102_TURN.reset(token)
|
| 1556 |
+
output["typed_discourse_workspace"] = {
|
| 1557 |
+
"schema_version": SCHEMA,
|
| 1558 |
+
"lineage_sha256": self._k102_lineage_sha256,
|
| 1559 |
+
"development_source": "opened_k102_long_horizon_failure_probe",
|
| 1560 |
+
"prospective_claim": False,
|
| 1561 |
+
"state_sha256": state.state_sha256,
|
| 1562 |
+
"turn_index": state.turn_index,
|
| 1563 |
+
"active_topic": state.active_topic,
|
| 1564 |
+
"topic_stack_depth": len(state.topic_stack),
|
| 1565 |
+
"active_frames": list(state.active_frame_names()),
|
| 1566 |
+
"journal": journal,
|
| 1567 |
+
"recovery": recovery,
|
| 1568 |
+
"opaque_hrr_memory_id": memory_id,
|
| 1569 |
+
"hrr_cycle": cycle,
|
| 1570 |
+
"bound_to_stabilizing_spine": (
|
| 1571 |
+
spine.get("constitution_sha256") == constitution.constitution_sha256
|
| 1572 |
+
and spine.get("discourse_state_sha256") == state.state_sha256
|
| 1573 |
+
),
|
| 1574 |
+
"pre_malkuth_discourse_payload_unbinds": 0,
|
| 1575 |
+
"audit": dict(current.audit or {"recognized": False}),
|
| 1576 |
+
"traditional_language_model": False,
|
| 1577 |
+
"transformer": False,
|
| 1578 |
+
"pretrained_embedding_encoder": False,
|
| 1579 |
+
"autoregressive_carrier": False,
|
| 1580 |
+
"model_calls": 0,
|
| 1581 |
+
"gradient_steps": 0,
|
| 1582 |
+
}
|
| 1583 |
+
output["situated_identity_constitution"] = {
|
| 1584 |
+
"schema_version": SCHEMA,
|
| 1585 |
+
"identity": constitution.self_name,
|
| 1586 |
+
"profile_sha256": constitution.profile_sha256,
|
| 1587 |
+
"constitution_sha256": constitution.constitution_sha256,
|
| 1588 |
+
"constitutional_roles": 11,
|
| 1589 |
+
"identity_is_typed_invariant_not_persona_overlay": True,
|
| 1590 |
+
"prompt_is_human_readable_serialization_not_runtime_preamble": True,
|
| 1591 |
+
"available_to_specialist_routing": True,
|
| 1592 |
+
"persistent_procedural_binding": True,
|
| 1593 |
+
"bound_to_stabilizing_spine": spine.get("constitution_sha256") == constitution.constitution_sha256,
|
| 1594 |
+
"raw_prompt_prepended_to_user_utterance": False,
|
| 1595 |
+
"situated_state_changes_without_redefining_self": True,
|
| 1596 |
+
"user_retains_goal_and_consequential_authority": True,
|
| 1597 |
+
}
|
| 1598 |
+
output["runtime"]["typed_discourse_model_calls"] = 0
|
| 1599 |
+
output["runtime"]["typed_discourse_gradient_steps"] = 0
|
| 1600 |
+
output["runtime"]["typed_discourse_journal_full_scans"] = 0
|
| 1601 |
+
output["runtime"]["situated_identity_model_calls"] = 0
|
| 1602 |
+
output["schema_version"] = SCHEMA
|
| 1603 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 1604 |
+
return output
|
| 1605 |
+
|
| 1606 |
+
|
| 1607 |
+
__all__ = [
|
| 1608 |
+
"IndexedTypedDiscourseJournal",
|
| 1609 |
+
"K102_SYSTEM_PROMPT",
|
| 1610 |
+
"PersistentTypedDiscourseAgent",
|
| 1611 |
+
"SituatedIdentityConstitution",
|
| 1612 |
+
"TypedDiscourseState",
|
| 1613 |
+
"compile_situated_constitution",
|
| 1614 |
+
]
|
root/wander-train/stage5k104_relational_ingress_equivalence_agent.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K104 successor for the two ingress classes exposed prospectively by K103.
|
| 3 |
+
|
| 4 |
+
K102 and the failed one-shot K103 evidence remain immutable. This successor
|
| 5 |
+
adds no reasoning cell, language model, embedding encoder, or generator. It
|
| 6 |
+
only maps direct rhetorical imperatives onto the existing artifact operator
|
| 7 |
+
and gates authoritative-science requests by exact referent-bearing source
|
| 8 |
+
evidence before answer or abstention.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import hashlib
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import re
|
| 15 |
+
from typing import Any, Mapping, Sequence
|
| 16 |
+
|
| 17 |
+
from stage5k15_native_mixed_agent import HandlerResult, SemanticClause, clean, digest
|
| 18 |
+
from stage5k102_persistent_typed_discourse_agent import (
|
| 19 |
+
IndexedTypedDiscourseJournal,
|
| 20 |
+
PersistentTypedDiscourseAgent,
|
| 21 |
+
TypedDiscourseState,
|
| 22 |
+
_K102_TURN,
|
| 23 |
+
_trim,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
SCHEMA = "oracle-stage5k104-relational-ingress-equivalence-agent-v1"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class RelationalIngressEquivalenceAgent(PersistentTypedDiscourseAgent):
|
| 31 |
+
"""K102 plus generalized speech-act and epistemic-ingress equivalence."""
|
| 32 |
+
|
| 33 |
+
_DIRECT_RHETORICAL = re.compile(
|
| 34 |
+
r"^(?:please\s+)?(outline|structure|plan|draft)\s+(a|an|the)\s+"
|
| 35 |
+
r"(paper|essay|article)\s+(arguing\s+that|about|on)\s+(.+?)[?.!]*$",
|
| 36 |
+
re.IGNORECASE,
|
| 37 |
+
)
|
| 38 |
+
_AUTHORITATIVE_SCIENCE = re.compile(
|
| 39 |
+
r"^(?:please\s+)?(?:give|provide|state|explain|describe)(?:\s+me)?\s+"
|
| 40 |
+
r"(?:the\s+)?(?:(?:accepted|established|recognized)\s+)?"
|
| 41 |
+
r"(?:scientific\s+)(definition|mechanism|account|explanation)\s+"
|
| 42 |
+
# Longest determiner first: otherwise the ``a`` alternative consumes
|
| 43 |
+
# the first character of ``an`` and corrupts the referent boundary.
|
| 44 |
+
r"(?:of|for)\s+(?:the|an|a)?\s*(.+?)[?.!]*$",
|
| 45 |
+
re.IGNORECASE,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
*args: Any,
|
| 51 |
+
discourse_journal_path: Path | None = None,
|
| 52 |
+
k104_ablations: Sequence[str] = (),
|
| 53 |
+
**kwargs: Any,
|
| 54 |
+
) -> None:
|
| 55 |
+
substrate_path = Path(args[5] if len(args) >= 6 else kwargs["substrate_path"])
|
| 56 |
+
self._k104_ablations = frozenset(clean(value).casefold() for value in k104_ablations)
|
| 57 |
+
journal_path = discourse_journal_path or substrate_path.with_suffix(".k104.discourse.sqlite3")
|
| 58 |
+
super().__init__(*args, discourse_journal_path=journal_path, **kwargs)
|
| 59 |
+
# The successor journal is bound to the successor implementation,
|
| 60 |
+
# rather than silently inheriting K102's module identity.
|
| 61 |
+
self.discourse_journal = IndexedTypedDiscourseJournal(
|
| 62 |
+
journal_path,
|
| 63 |
+
source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
| 64 |
+
)
|
| 65 |
+
self._k104_lineage_sha256 = digest({
|
| 66 |
+
"schema_version": SCHEMA,
|
| 67 |
+
"parent_lineage_sha256": self._k102_lineage_sha256,
|
| 68 |
+
"opened_failure_source": "stage5k103_untouched_phase_transition_report_v1",
|
| 69 |
+
"repairs": [
|
| 70 |
+
"direct_and_periphrastic_rhetorical_imperative_equivalence",
|
| 71 |
+
"authoritative_scientific_exact_referent_source_gate",
|
| 72 |
+
"unsupported_scientific_referent_abstention",
|
| 73 |
+
],
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
@staticmethod
|
| 77 |
+
def _exact_referent_present(referent: str, fact: str) -> bool:
|
| 78 |
+
normalized_referent = " ".join(re.findall(r"[a-z0-9]+", clean(referent).casefold()))
|
| 79 |
+
normalized_fact = " ".join(re.findall(r"[a-z0-9]+", clean(fact).casefold()))
|
| 80 |
+
return bool(normalized_referent and re.search(
|
| 81 |
+
rf"(?:^|\s){re.escape(normalized_referent)}(?:\s|$)", normalized_fact,
|
| 82 |
+
))
|
| 83 |
+
|
| 84 |
+
def _scientific_ingress_result(self, text: str) -> HandlerResult | None:
|
| 85 |
+
match = self._AUTHORITATIVE_SCIENCE.match(clean(text))
|
| 86 |
+
if match is None:
|
| 87 |
+
return None
|
| 88 |
+
relation, referent = match.group(1).casefold(), _trim(match.group(2))
|
| 89 |
+
if "authoritative_scientific_referent_gate" in self._k104_ablations:
|
| 90 |
+
return None
|
| 91 |
+
terms = tuple(dict.fromkeys(re.findall(r"[a-z0-9]+", referent.casefold())))
|
| 92 |
+
rows = ()
|
| 93 |
+
if terms:
|
| 94 |
+
query = " AND ".join(f'"{term}"' for term in terms)
|
| 95 |
+
rows = self.information_source_expert.connection.execute(
|
| 96 |
+
"SELECT text_sha256, fact, source_document, source_document_sha256, "
|
| 97 |
+
"domain_id, license_note, source_split, bm25(source_rows) "
|
| 98 |
+
"FROM source_rows WHERE source_rows MATCH ? ORDER BY bm25(source_rows) LIMIT 128",
|
| 99 |
+
(query,),
|
| 100 |
+
).fetchall()
|
| 101 |
+
authoritative = []
|
| 102 |
+
for row in rows:
|
| 103 |
+
source, domain, fact = str(row[2]), str(row[4]), clean(row[1])
|
| 104 |
+
admitted_authority = source.startswith("openstax/") or domain in {
|
| 105 |
+
"life_health", "physical_engineering", "earth_space", "social_behavioral",
|
| 106 |
+
"governance_law", "economics_organization",
|
| 107 |
+
}
|
| 108 |
+
if (
|
| 109 |
+
admitted_authority
|
| 110 |
+
and self._exact_referent_present(referent, fact)
|
| 111 |
+
and not fact.rstrip().endswith("?")
|
| 112 |
+
):
|
| 113 |
+
authoritative.append(row)
|
| 114 |
+
current = _K102_TURN.get()
|
| 115 |
+
if current is not None:
|
| 116 |
+
current.audit.update({
|
| 117 |
+
"recognized": True,
|
| 118 |
+
"discourse_operation": "authoritative_scientific_referent_gate",
|
| 119 |
+
"requested_relation": relation,
|
| 120 |
+
"referent": referent,
|
| 121 |
+
"exact_authoritative_rows": len(authoritative),
|
| 122 |
+
})
|
| 123 |
+
if not authoritative:
|
| 124 |
+
return self._abstention(
|
| 125 |
+
"unsupported_scientific_referent",
|
| 126 |
+
f"an established scientific {relation} for {referent}",
|
| 127 |
+
"exact_authoritative_referent_evidence_absent",
|
| 128 |
+
)
|
| 129 |
+
row = authoritative[0]
|
| 130 |
+
fact = _trim(row[1])
|
| 131 |
+
return HandlerResult(
|
| 132 |
+
"ground_authoritative_scientific_relation", "answered",
|
| 133 |
+
(("relation", relation), ("referent", referent)),
|
| 134 |
+
(("source_rows", 1),),
|
| 135 |
+
(
|
| 136 |
+
SemanticClause(
|
| 137 |
+
f"The authenticated source on {referent}", "state", object_value=fact,
|
| 138 |
+
),
|
| 139 |
+
),
|
| 140 |
+
((str(row[3]), str(row[0])),),
|
| 141 |
+
(
|
| 142 |
+
"recognize_authoritative_scientific_request",
|
| 143 |
+
"bind_complete_multiword_referent",
|
| 144 |
+
"require_exact_referent_in_authenticated_source",
|
| 145 |
+
"release_supported_relation_at_malkuth",
|
| 146 |
+
),
|
| 147 |
+
tool_audit={
|
| 148 |
+
"tool": "k104_exact_scientific_referent_gate",
|
| 149 |
+
"source_document": str(row[2]),
|
| 150 |
+
"license_note": str(row[5]),
|
| 151 |
+
"read_only": True,
|
| 152 |
+
"traditional_language_model": False,
|
| 153 |
+
},
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
def _paper_result(self, state: TypedDiscourseState, text: str) -> HandlerResult | None:
|
| 157 |
+
direct = self._DIRECT_RHETORICAL.match(clean(text))
|
| 158 |
+
if direct is not None and "direct_rhetorical_equivalence" not in self._k104_ablations:
|
| 159 |
+
verb, article, genre, relation, claim = (
|
| 160 |
+
_trim(direct.group(index)) for index in range(1, 6)
|
| 161 |
+
)
|
| 162 |
+
canonical = f"Help me {verb} {article} {genre} {relation} {claim}."
|
| 163 |
+
result = super()._paper_result(state, canonical)
|
| 164 |
+
current = _K102_TURN.get()
|
| 165 |
+
if current is not None:
|
| 166 |
+
current.audit.update({
|
| 167 |
+
"recognized": result is not None,
|
| 168 |
+
"discourse_operation": "direct_rhetorical_equivalence",
|
| 169 |
+
"canonical_surface_sha256": hashlib.sha256(canonical.encode("utf-8")).hexdigest(),
|
| 170 |
+
"meaning_preserved": True,
|
| 171 |
+
})
|
| 172 |
+
if result is not None:
|
| 173 |
+
return HandlerResult(
|
| 174 |
+
result.operation, result.status, result.arguments, result.result,
|
| 175 |
+
result.clauses, result.evidence,
|
| 176 |
+
(
|
| 177 |
+
"recognize_direct_rhetorical_imperative",
|
| 178 |
+
"commute_politeness_periphrasis_without_changing_roles",
|
| 179 |
+
*result.computation_trace,
|
| 180 |
+
),
|
| 181 |
+
tool_audit=result.tool_audit, memory_audit=result.memory_audit,
|
| 182 |
+
)
|
| 183 |
+
return super()._paper_result(state, text)
|
| 184 |
+
|
| 185 |
+
def _handle_evidence(self, text: str, context: Mapping[str, Any]) -> HandlerResult | None:
|
| 186 |
+
scientific = self._scientific_ingress_result(text)
|
| 187 |
+
return scientific if scientific is not None else super()._handle_evidence(text, context)
|
| 188 |
+
|
| 189 |
+
def _handle_event(self, session_id: str, text: str, turn_id: str) -> HandlerResult | None:
|
| 190 |
+
scientific = self._scientific_ingress_result(text)
|
| 191 |
+
return scientific if scientific is not None else super()._handle_event(session_id, text, turn_id)
|
| 192 |
+
|
| 193 |
+
def _event_capability_family(self, text: str) -> tuple[str, ...]:
|
| 194 |
+
if self._DIRECT_RHETORICAL.match(clean(text)) or self._AUTHORITATIVE_SCIENCE.match(clean(text)):
|
| 195 |
+
return (
|
| 196 |
+
"linguistic", "synthesis", "epistemic", "contrast", "causal",
|
| 197 |
+
"mechanism", "tool_router", "dataset", "provenance",
|
| 198 |
+
)
|
| 199 |
+
return super()._event_capability_family(text)
|
| 200 |
+
|
| 201 |
+
def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
| 202 |
+
output = super().answer(*args, **kwargs)
|
| 203 |
+
audit = (output.get("typed_discourse_workspace") or {}).get("audit") or {}
|
| 204 |
+
output["relational_ingress_equivalence"] = {
|
| 205 |
+
"schema_version": SCHEMA,
|
| 206 |
+
"lineage_sha256": self._k104_lineage_sha256,
|
| 207 |
+
"opened_failure_source": "immutable_k103_one_shot_failure",
|
| 208 |
+
"direct_rhetorical_equivalence_activated": audit.get("discourse_operation")
|
| 209 |
+
== "direct_rhetorical_equivalence",
|
| 210 |
+
"scientific_referent_gate_activated": audit.get("discourse_operation")
|
| 211 |
+
== "authoritative_scientific_referent_gate",
|
| 212 |
+
"exact_authoritative_rows": int(audit.get("exact_authoritative_rows", 0)),
|
| 213 |
+
"traditional_language_model": False,
|
| 214 |
+
"transformer": False,
|
| 215 |
+
"pretrained_embedding_encoder": False,
|
| 216 |
+
"autoregressive_carrier": False,
|
| 217 |
+
"model_calls": 0,
|
| 218 |
+
"gradient_steps": 0,
|
| 219 |
+
}
|
| 220 |
+
output["runtime"].update({
|
| 221 |
+
"relational_ingress_equivalence_model_calls": 0,
|
| 222 |
+
"relational_ingress_equivalence_gradient_steps": 0,
|
| 223 |
+
})
|
| 224 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 225 |
+
return output
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
__all__ = ["RelationalIngressEquivalenceAgent", "SCHEMA"]
|
root/wander-train/stage5k108_universal_terminal_proof_projection_agent.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K108 universal terminal-proof projection successor.
|
| 3 |
+
|
| 4 |
+
K106 and the immutable K107 one-shot evidence remain untouched. K107
|
| 5 |
+
prospectively confirmed scientific proof-ledger congruence, while an unrelated
|
| 6 |
+
causal turn exposed the same telemetry mismatch outside the scientific gate.
|
| 7 |
+
This successor applies the already causal rule universally: the public
|
| 8 |
+
knowledge ledger contains exactly those authenticated public rows referenced
|
| 9 |
+
by the terminal typed proof, irrespective of upstream candidates.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import hashlib
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any, Sequence
|
| 16 |
+
|
| 17 |
+
from stage5k15_native_mixed_agent import clean, digest
|
| 18 |
+
from stage5k102_persistent_typed_discourse_agent import IndexedTypedDiscourseJournal
|
| 19 |
+
from stage5k106_terminal_proof_provenance_agent import TerminalProofProvenanceAgent
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
SCHEMA = "oracle-stage5k108-universal-terminal-proof-projection-agent-v1"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class UniversalTerminalProofProjectionAgent(TerminalProofProvenanceAgent):
|
| 26 |
+
"""Make terminal proof evidence authoritative for every public ledger."""
|
| 27 |
+
|
| 28 |
+
def __init__(
|
| 29 |
+
self,
|
| 30 |
+
*args: Any,
|
| 31 |
+
discourse_journal_path: Path | None = None,
|
| 32 |
+
k108_ablations: Sequence[str] = (),
|
| 33 |
+
**kwargs: Any,
|
| 34 |
+
) -> None:
|
| 35 |
+
substrate_path = Path(args[5] if len(args) >= 6 else kwargs["substrate_path"])
|
| 36 |
+
self._k108_ablations = frozenset(clean(value).casefold() for value in k108_ablations)
|
| 37 |
+
journal_path = discourse_journal_path or substrate_path.with_suffix(".k108.discourse.sqlite3")
|
| 38 |
+
super().__init__(*args, discourse_journal_path=journal_path, **kwargs)
|
| 39 |
+
self.discourse_journal = IndexedTypedDiscourseJournal(
|
| 40 |
+
journal_path,
|
| 41 |
+
source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
| 42 |
+
)
|
| 43 |
+
self._k108_lineage_sha256 = digest({
|
| 44 |
+
"schema_version": SCHEMA,
|
| 45 |
+
"parent_lineage_sha256": self._k106_lineage_sha256,
|
| 46 |
+
"opened_failure_source": "stage5k107_untouched_proof_congruence_report_v1",
|
| 47 |
+
"repair": "universal_terminal_typed_proof_projection_to_public_knowledge_ledger",
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
def _universal_projection(self, output: dict[str, Any]) -> dict[str, Any]:
|
| 51 |
+
if "universal_terminal_proof_projection" in self._k108_ablations:
|
| 52 |
+
return {"activated": False, "ablated": True}
|
| 53 |
+
proof = output.get("computation_proof") or {}
|
| 54 |
+
admitted_rows = []
|
| 55 |
+
seen = set()
|
| 56 |
+
for pair in proof.get("evidence", ()):
|
| 57 |
+
if not isinstance(pair, (list, tuple)) or len(pair) != 2:
|
| 58 |
+
continue
|
| 59 |
+
source_sha, text_sha = str(pair[0]), str(pair[1])
|
| 60 |
+
if len(source_sha) != 64 or len(text_sha) != 64 or text_sha in seen:
|
| 61 |
+
continue
|
| 62 |
+
row = self._source_row_for_proof_evidence(text_sha)
|
| 63 |
+
if row is None or row["source_document_sha256"] != source_sha:
|
| 64 |
+
continue
|
| 65 |
+
admitted_rows.append(row); seen.add(text_sha)
|
| 66 |
+
lattice = dict(output.get("knowledge_lattice") or {})
|
| 67 |
+
prior_rows = tuple(lattice.get("evidence_rows_admitted_to_proof") or ())
|
| 68 |
+
prior_admitted = bool(lattice.get("retrieval_admitted_to_proof"))
|
| 69 |
+
admitted = bool(admitted_rows)
|
| 70 |
+
lattice.update({
|
| 71 |
+
"candidate_retrieval_observed": bool(prior_rows),
|
| 72 |
+
"candidate_rows_before_universal_projection": len(prior_rows),
|
| 73 |
+
"candidate_admission_before_universal_projection": prior_admitted,
|
| 74 |
+
"evidence_rows_admitted_to_proof": admitted_rows,
|
| 75 |
+
"retrieval_admitted_to_proof": admitted,
|
| 76 |
+
"retrieval_gate_reason": (
|
| 77 |
+
"terminal_typed_proof_contains_authenticated_public_source"
|
| 78 |
+
if admitted else "terminal_typed_proof_contains_no_authenticated_public_source"
|
| 79 |
+
),
|
| 80 |
+
"relational_provenance_reconciled": True,
|
| 81 |
+
"terminal_proof_authoritative_for_admission": True,
|
| 82 |
+
"universal_terminal_proof_projection": True,
|
| 83 |
+
"terminal_proof_operation": str(output.get("operation", "")),
|
| 84 |
+
"terminal_proof_status": str(output.get("status", "")),
|
| 85 |
+
"terminal_proof_evidence_sha256s": [row["text_sha256"] for row in admitted_rows],
|
| 86 |
+
})
|
| 87 |
+
output["knowledge_lattice"] = lattice
|
| 88 |
+
return {
|
| 89 |
+
"activated": True,
|
| 90 |
+
"ablated": False,
|
| 91 |
+
"operation": str(output.get("operation", "")),
|
| 92 |
+
"status": str(output.get("status", "")),
|
| 93 |
+
"candidate_admission_before": prior_admitted,
|
| 94 |
+
"candidate_rows_before": len(prior_rows),
|
| 95 |
+
"terminal_authenticated_rows": len(admitted_rows),
|
| 96 |
+
"retrieval_admitted_to_proof": admitted,
|
| 97 |
+
"projection_changed_admission": prior_admitted is not admitted,
|
| 98 |
+
"proof_and_ledger_congruent": bool(
|
| 99 |
+
admitted == bool(admitted_rows)
|
| 100 |
+
and {row["text_sha256"] for row in admitted_rows} == seen
|
| 101 |
+
),
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
| 105 |
+
output = super().answer(*args, **kwargs)
|
| 106 |
+
projection = self._universal_projection(output)
|
| 107 |
+
output["universal_terminal_proof_projection"] = {
|
| 108 |
+
"schema_version": SCHEMA,
|
| 109 |
+
"lineage_sha256": self._k108_lineage_sha256,
|
| 110 |
+
"opened_failure_source": "immutable_k107_one_shot_failure",
|
| 111 |
+
**projection,
|
| 112 |
+
"traditional_language_model": False,
|
| 113 |
+
"transformer": False,
|
| 114 |
+
"pretrained_embedding_encoder": False,
|
| 115 |
+
"autoregressive_carrier": False,
|
| 116 |
+
"model_calls": 0,
|
| 117 |
+
"gradient_steps": 0,
|
| 118 |
+
}
|
| 119 |
+
output["runtime"].update({
|
| 120 |
+
"universal_terminal_proof_projection_model_calls": 0,
|
| 121 |
+
"universal_terminal_proof_projection_gradient_steps": 0,
|
| 122 |
+
})
|
| 123 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 124 |
+
return output
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
__all__ = ["UniversalTerminalProofProjectionAgent", "SCHEMA"]
|
root/wander-train/stage5k115_copular_definition_equivalence_agent.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K115 explicit copular-definition equivalence over frozen K112.
|
| 3 |
+
|
| 4 |
+
The immutable K114 prospective failure showed that K112 could locate an exact,
|
| 5 |
+
authoritative row and verify definition direction, yet reject ``X is the
|
| 6 |
+
process of ...`` because its deliberately small morphological normalization
|
| 7 |
+
mapped ``process`` to ``proces`` before relation-cue comparison. This
|
| 8 |
+
successor treats an exact referent followed by a copula and a finite
|
| 9 |
+
definitional complement class as the definition relation itself. It contains
|
| 10 |
+
no domain entities, answer text, language model, embedding model, or scan.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from dataclasses import replace
|
| 15 |
+
import hashlib
|
| 16 |
+
import re
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Any, Mapping, Sequence
|
| 19 |
+
|
| 20 |
+
from stage5k15_native_mixed_agent import clean, digest
|
| 21 |
+
from stage5k81_information_weighted_source_expert import terms
|
| 22 |
+
from stage5j1_native_contextual_sense_lattice import WORD
|
| 23 |
+
from stage5k112_relation_conditioned_source_agent import (
|
| 24 |
+
RelationConditionedSourceAgent,
|
| 25 |
+
RelationRequest,
|
| 26 |
+
SourceCertificate,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
SCHEMA = "oracle-stage5k115-copular-definition-equivalence-agent-v1"
|
| 31 |
+
|
| 32 |
+
DEFINITIONAL_COMPLEMENT_CLASSES = frozenset({
|
| 33 |
+
"act", "action", "activity", "approach", "condition", "concept",
|
| 34 |
+
"doctrine", "event", "form", "framework", "idea", "means", "method",
|
| 35 |
+
"entity", "operation", "orientation", "pattern", "phenomenon", "practice",
|
| 36 |
+
"principle", "procedure", "process", "property", "relation", "rule",
|
| 37 |
+
"state", "step", "steps", "system", "technique", "theory", "type", "way",
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
NON_DEFINITIONAL_COMPLEMENT_MODIFIERS = frozenset({
|
| 41 |
+
"best", "commonly", "correct", "different", "important", "less", "more",
|
| 42 |
+
"most", "not", "now", "only", "sometimes", "used",
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _ordered_phrase(value: str) -> str:
|
| 47 |
+
"""Normalize surface tokens without erasing repetition or order."""
|
| 48 |
+
normalized = []
|
| 49 |
+
for raw in WORD.findall(str(value)):
|
| 50 |
+
token = raw.casefold().replace("’", "'")
|
| 51 |
+
if token.endswith("'s") and len(token) > 2:
|
| 52 |
+
token = token[:-2]
|
| 53 |
+
normalized.append(token)
|
| 54 |
+
return " ".join(normalized)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class CopularDefinitionEquivalenceAgent(RelationConditionedSourceAgent):
|
| 58 |
+
"""Admit explicit copular definitions missed by K112 normalization."""
|
| 59 |
+
|
| 60 |
+
def __init__(self, *args: Any, k115_ablations: Sequence[str] = (), **kwargs: Any) -> None:
|
| 61 |
+
self._k115_ablations = frozenset(clean(value).casefold() for value in k115_ablations)
|
| 62 |
+
super().__init__(*args, **kwargs)
|
| 63 |
+
self._k115_lineage_sha256 = digest({
|
| 64 |
+
"schema_version": SCHEMA,
|
| 65 |
+
"parent_lineage_sha256": self._k112_lineage_sha256,
|
| 66 |
+
"preserved_failure_source": "immutable_stage5k114_untouched_relational_transition_report_v1",
|
| 67 |
+
"repair": "exact_referent_copula_definitional_complement_equivalence",
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
@classmethod
|
| 71 |
+
def _relation_request(cls, text: str) -> RelationRequest | None:
|
| 72 |
+
request = super()._relation_request(text)
|
| 73 |
+
if request is None or request.relation != "definition":
|
| 74 |
+
return request
|
| 75 |
+
# K112's frozen optional-article alternation checks ``a`` before
|
| 76 |
+
# ``an`` and can therefore consume only the first character of ``an``.
|
| 77 |
+
# Reconstruct the definition referent from the original surface with
|
| 78 |
+
# longest-article-first precedence. The request's already derived
|
| 79 |
+
# content/referent terms remain valid and contain no answer knowledge.
|
| 80 |
+
value = clean(text).rstrip(".?!")
|
| 81 |
+
match = re.match(
|
| 82 |
+
r"^(?:what\s+(?:is|are)|define)\s+(?:(?:an|the|a)\s+)?(.+)$",
|
| 83 |
+
value, re.IGNORECASE,
|
| 84 |
+
)
|
| 85 |
+
if match is None:
|
| 86 |
+
return request
|
| 87 |
+
referent = clean(match.group(1))
|
| 88 |
+
return replace(request, referent=referent) if referent else request
|
| 89 |
+
|
| 90 |
+
@staticmethod
|
| 91 |
+
def _copular_definition_match(request: RelationRequest, fact: str) -> Mapping[str, Any] | None:
|
| 92 |
+
if request.relation != "definition":
|
| 93 |
+
return None
|
| 94 |
+
normalized_fact = _ordered_phrase(fact)
|
| 95 |
+
normalized_referent = _ordered_phrase(request.referent)
|
| 96 |
+
if not normalized_referent:
|
| 97 |
+
return None
|
| 98 |
+
pattern = re.compile(
|
| 99 |
+
rf"^(?:(?:a|an|the)\s+)?{re.escape(normalized_referent)}\s+"
|
| 100 |
+
rf"(?P<copula>is|are|means|denotes|describes|refers\s+to)\s+"
|
| 101 |
+
rf"(?:(?:a|an|the)\s+)?(?P<complement_phrase>[a-z-]+(?:\s+[a-z-]+){{0,3}})\b",
|
| 102 |
+
)
|
| 103 |
+
match = pattern.search(normalized_fact)
|
| 104 |
+
if match is None:
|
| 105 |
+
return None
|
| 106 |
+
phrase = match.group("complement_phrase").split()
|
| 107 |
+
head_index = next(
|
| 108 |
+
(index for index, value in enumerate(phrase) if value in DEFINITIONAL_COMPLEMENT_CLASSES),
|
| 109 |
+
None,
|
| 110 |
+
)
|
| 111 |
+
if head_index is None:
|
| 112 |
+
return None
|
| 113 |
+
modifiers = phrase[:head_index]
|
| 114 |
+
if any(value in NON_DEFINITIONAL_COMPLEMENT_MODIFIERS for value in modifiers):
|
| 115 |
+
return None
|
| 116 |
+
return {
|
| 117 |
+
"operator": "copular_definition",
|
| 118 |
+
"copula": match.group("copula"),
|
| 119 |
+
"complement_class": phrase[head_index],
|
| 120 |
+
"complement_modifiers": modifiers,
|
| 121 |
+
"exact_referent_prefix": True,
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
def _copular_certificate(
|
| 125 |
+
self, request: RelationRequest, parent: SourceCertificate,
|
| 126 |
+
) -> SourceCertificate | None:
|
| 127 |
+
if request.relation != "definition":
|
| 128 |
+
return None
|
| 129 |
+
# Address with the preserved referent surface as well as variants.
|
| 130 |
+
# K112's request lemmas are useful for broad relational matching but
|
| 131 |
+
# intentionally lossy (for example ``process`` -> ``proces`` and
|
| 132 |
+
# ``retailing`` -> ``retail``); exact copular definitions require the
|
| 133 |
+
# original noun surface to remain available to FTS.
|
| 134 |
+
referent_surfaces = tuple(
|
| 135 |
+
value for value in terms(request.referent)
|
| 136 |
+
if self.information_source_expert._fts_token(value)
|
| 137 |
+
)
|
| 138 |
+
values = set(referent_surfaces)
|
| 139 |
+
if not values:
|
| 140 |
+
return None
|
| 141 |
+
referent_expression = " AND ".join(f'"{value}"' for value in referent_surfaces)
|
| 142 |
+
relation_expression = " OR ".join(
|
| 143 |
+
f'"{value}"' for value in sorted(DEFINITIONAL_COMPLEMENT_CLASSES)
|
| 144 |
+
)
|
| 145 |
+
expression = f"{referent_expression} AND ({relation_expression})"
|
| 146 |
+
rows = self.information_source_expert.connection.execute(
|
| 147 |
+
"SELECT text_sha256, fact, source_document, source_document_sha256, "
|
| 148 |
+
"domain_id, license_note, source_split, bm25(source_rows) "
|
| 149 |
+
"FROM source_rows WHERE source_rows MATCH ? ORDER BY bm25(source_rows) LIMIT 768",
|
| 150 |
+
(expression,),
|
| 151 |
+
).fetchall()
|
| 152 |
+
ranked = []
|
| 153 |
+
audits = []
|
| 154 |
+
for row in rows:
|
| 155 |
+
fact = clean(row[1])
|
| 156 |
+
match = self._copular_definition_match(request, fact)
|
| 157 |
+
noise, noise_reasons = self._row_noise(fact)
|
| 158 |
+
source_name = str(row[2])
|
| 159 |
+
authority = 5.0 if source_name.startswith("openstax/") else 1.5 if source_name.startswith("gutenberg/") else 0.0
|
| 160 |
+
declarative = not fact.rstrip().endswith("?") and "multiple_choice_source" not in noise_reasons
|
| 161 |
+
eligible = bool(match is not None and declarative and authority > 0)
|
| 162 |
+
score = 20.0 + authority - noise + (2.0 if match and match["copula"] in {"is", "are"} else 0.0)
|
| 163 |
+
audit = {
|
| 164 |
+
"text_sha256": str(row[0]), "source_document": source_name,
|
| 165 |
+
"eligible": eligible, "operator_match": match,
|
| 166 |
+
"declarative": declarative, "authority": authority,
|
| 167 |
+
"noise_reasons": list(noise_reasons), "score": score,
|
| 168 |
+
}
|
| 169 |
+
audits.append((score, float(row[7]), str(row[0]), audit))
|
| 170 |
+
if eligible:
|
| 171 |
+
ranked.append((score, float(row[7]), str(row[0]), row, audit))
|
| 172 |
+
audits.sort(key=lambda value: (-value[0], value[1], value[2]))
|
| 173 |
+
if not ranked:
|
| 174 |
+
return None
|
| 175 |
+
ranked.sort(key=lambda value: (-value[0], value[1], value[2]))
|
| 176 |
+
best = ranked[0]
|
| 177 |
+
row = best[3]
|
| 178 |
+
selected = {
|
| 179 |
+
"text_sha256": str(row[0]), "fact": clean(row[1]),
|
| 180 |
+
"source_document": str(row[2]), "source_document_sha256": str(row[3]),
|
| 181 |
+
"domain_id": str(row[4]), "license_note": str(row[5]),
|
| 182 |
+
"source_split": str(row[6]),
|
| 183 |
+
"admission_authority": "k115_copular_definition_equivalence",
|
| 184 |
+
}
|
| 185 |
+
runner = ranked[1][0] if len(ranked) > 1 else None
|
| 186 |
+
return SourceCertificate(
|
| 187 |
+
request=request, selected_row=selected, accepted=True,
|
| 188 |
+
reason="copular_definition_equivalence_satisfied",
|
| 189 |
+
selected_score=best[0], runner_up_score=runner,
|
| 190 |
+
inspected_postings=len(rows), eligible_candidates=len(ranked),
|
| 191 |
+
audit_candidates=tuple(value[3] for value in audits[:5]),
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
def _certificate(self, request: RelationRequest) -> SourceCertificate:
|
| 195 |
+
parent = super()._certificate(request)
|
| 196 |
+
if "copular_definition_equivalence" in self._k115_ablations:
|
| 197 |
+
return parent
|
| 198 |
+
# An exact sentence-prefix copular definition is a stronger relation
|
| 199 |
+
# certificate than an embedded mention admitted by K112's broad
|
| 200 |
+
# lexical relation cues. Prefer it even when the parent found a weaker
|
| 201 |
+
# candidate; other relation kinds remain completely unchanged.
|
| 202 |
+
return self._copular_certificate(request, parent) or parent
|
| 203 |
+
|
| 204 |
+
def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
| 205 |
+
output = super().answer(*args, **kwargs)
|
| 206 |
+
certificate = output.get("relation_conditioned_source_certificate") or {}
|
| 207 |
+
activated = certificate.get("reason") == "copular_definition_equivalence_satisfied"
|
| 208 |
+
output["copular_definition_equivalence"] = {
|
| 209 |
+
"schema_version": SCHEMA,
|
| 210 |
+
"lineage_sha256": self._k115_lineage_sha256,
|
| 211 |
+
"activated": activated,
|
| 212 |
+
"ablated": "copular_definition_equivalence" in self._k115_ablations,
|
| 213 |
+
"operator": "exact_referent_plus_copula_plus_definitional_complement_class",
|
| 214 |
+
"definitional_complement_class_count": len(DEFINITIONAL_COMPLEMENT_CLASSES),
|
| 215 |
+
"maximum_complement_modifiers": 3,
|
| 216 |
+
"domain_entities_encoded": 0,
|
| 217 |
+
"traditional_language_model": False, "transformer": False,
|
| 218 |
+
"pretrained_embedding_encoder": False, "autoregressive_carrier": False,
|
| 219 |
+
"model_calls": 0, "gradient_steps": 0,
|
| 220 |
+
}
|
| 221 |
+
output["runtime"].update({
|
| 222 |
+
"copular_definition_equivalence_model_calls": 0,
|
| 223 |
+
"copular_definition_equivalence_gradient_steps": 0,
|
| 224 |
+
})
|
| 225 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 226 |
+
return output
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
__all__ = ["CopularDefinitionEquivalenceAgent", "SCHEMA", "DEFINITIONAL_COMPLEMENT_CLASSES"]
|
root/wander-train/stage5k117_caller_context_isolation_agent.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K117 caller-context isolation over immutable K115/K116 evidence.
|
| 3 |
+
|
| 4 |
+
K116 established that K115's relation operator transfers to fresh definitions,
|
| 5 |
+
but exposed an independent precedence defect on a causal assertion. K90 may
|
| 6 |
+
insert a private retrieval sentinel below the outer answer boundary. K81 saw
|
| 7 |
+
that sentinel as though it were caller-supplied evidence and consequently
|
| 8 |
+
demoted an already valid ``assert_causal`` graph to ``delegate``. The
|
| 9 |
+
misrouted ephemeral event then made deterministic restart replay correctly
|
| 10 |
+
reject the changed operation.
|
| 11 |
+
|
| 12 |
+
This successor separates caller context from internal carrier context. A
|
| 13 |
+
typed graph that consumes the utterance remains prior to private sentinels;
|
| 14 |
+
genuine caller-supplied evidence, corpus records, and sensor streams retain
|
| 15 |
+
their bounded rails. No domain words, answer strings, model, embedding
|
| 16 |
+
encoder, gradient update, or autoregressive carrier are introduced.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from collections.abc import Sequence
|
| 21 |
+
from contextvars import ContextVar
|
| 22 |
+
from typing import Any, Mapping
|
| 23 |
+
|
| 24 |
+
from stage5k15_native_mixed_agent import clean, digest
|
| 25 |
+
from stage5k81_native_relational_construction_agent import _CONTEXT_KIND
|
| 26 |
+
from stage5k102_persistent_typed_discourse_agent import K102FinitePredicateTransducer
|
| 27 |
+
from stage5k115_copular_definition_equivalence_agent import (
|
| 28 |
+
CopularDefinitionEquivalenceAgent,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
SCHEMA = "oracle-stage5k117-caller-context-isolation-agent-v1"
|
| 33 |
+
|
| 34 |
+
_K117_CALLER_CONTEXT_KIND: ContextVar[str] = ContextVar(
|
| 35 |
+
"k117_caller_context_kind", default="none",
|
| 36 |
+
)
|
| 37 |
+
_K117_ISOLATION_AUDIT: ContextVar[Mapping[str, Any] | None] = ContextVar(
|
| 38 |
+
"k117_isolation_audit", default=None,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _caller_context_kind(context: Mapping[str, Any] | None) -> str:
|
| 43 |
+
value = context or {}
|
| 44 |
+
for key in ("evidence", "training_corpus", "sensor_stream"):
|
| 45 |
+
payload = value.get(key)
|
| 46 |
+
if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes)) and payload:
|
| 47 |
+
return key
|
| 48 |
+
return "none"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class CallerContextIsolationTransducer(K102FinitePredicateTransducer):
|
| 52 |
+
"""Make graph precedence depend on the outer caller boundary."""
|
| 53 |
+
|
| 54 |
+
def analyze(self, text: str, discourse, *, known_frames=()):
|
| 55 |
+
inherited_context_kind = _CONTEXT_KIND.get()
|
| 56 |
+
caller_context_kind = _K117_CALLER_CONTEXT_KIND.get()
|
| 57 |
+
ablated = "caller_context_isolation" in self.owner._k117_ablations
|
| 58 |
+
|
| 59 |
+
# Obtain the fully repaired inherited graph with context influence
|
| 60 |
+
# suppressed only when context was introduced internally. This calls
|
| 61 |
+
# through K102/K94/.../K81's MRO once and retains every later parser
|
| 62 |
+
# repair. For genuine caller context the inherited behavior is exact.
|
| 63 |
+
token = None
|
| 64 |
+
if not ablated and caller_context_kind == "none" and inherited_context_kind != "none":
|
| 65 |
+
token = _CONTEXT_KIND.set("none")
|
| 66 |
+
try:
|
| 67 |
+
graph = super().analyze(text, discourse, known_frames=known_frames)
|
| 68 |
+
finally:
|
| 69 |
+
if token is not None:
|
| 70 |
+
_CONTEXT_KIND.reset(token)
|
| 71 |
+
|
| 72 |
+
_K117_ISOLATION_AUDIT.set({
|
| 73 |
+
"caller_context_kind": caller_context_kind,
|
| 74 |
+
"inherited_context_kind": inherited_context_kind,
|
| 75 |
+
"internal_context_isolated": token is not None,
|
| 76 |
+
"ablated": ablated,
|
| 77 |
+
"selected_speech_act": graph.speech_act,
|
| 78 |
+
"typed_graph_consumes_utterance": bool(graph.consumes_as_graph),
|
| 79 |
+
})
|
| 80 |
+
return graph
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class CallerContextIsolationAgent(CopularDefinitionEquivalenceAgent):
|
| 84 |
+
"""Preserve K115 while preventing private carrier/context confusion."""
|
| 85 |
+
|
| 86 |
+
def __init__(self, *args: Any, k117_ablations: Sequence[str] = (), **kwargs: Any) -> None:
|
| 87 |
+
self._k117_ablations = frozenset(clean(value).casefold() for value in k117_ablations)
|
| 88 |
+
super().__init__(*args, **kwargs)
|
| 89 |
+
self.graph_transducer = CallerContextIsolationTransducer(
|
| 90 |
+
self, self.construction_head,
|
| 91 |
+
)
|
| 92 |
+
self._k117_lineage_sha256 = digest({
|
| 93 |
+
"schema_version": SCHEMA,
|
| 94 |
+
"parent_lineage_sha256": self._k115_lineage_sha256,
|
| 95 |
+
"preserved_failure_source": (
|
| 96 |
+
"immutable_stage5k116_untouched_operator_transfer_report_v1"
|
| 97 |
+
),
|
| 98 |
+
"repair": "outer_caller_context_distinct_from_internal_retrieval_carrier",
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
def _handle_evidence(self, text: str, context: Mapping[str, Any]):
|
| 102 |
+
evidence = context.get("evidence")
|
| 103 |
+
private_carrier = bool(
|
| 104 |
+
isinstance(evidence, Sequence)
|
| 105 |
+
and not isinstance(evidence, (str, bytes))
|
| 106 |
+
and any(
|
| 107 |
+
isinstance(value, Mapping)
|
| 108 |
+
and str(value.get("answer_status", "")).startswith("k90_")
|
| 109 |
+
for value in evidence
|
| 110 |
+
)
|
| 111 |
+
)
|
| 112 |
+
graph = self._k76_current.get()
|
| 113 |
+
typed_graph_prior = bool(
|
| 114 |
+
_K117_CALLER_CONTEXT_KIND.get() == "none"
|
| 115 |
+
and "caller_context_isolation" not in self._k117_ablations
|
| 116 |
+
and private_carrier
|
| 117 |
+
and graph is not None
|
| 118 |
+
and graph.speech_act in self.DIRECT_ACTS
|
| 119 |
+
and graph.consumes_as_graph
|
| 120 |
+
)
|
| 121 |
+
if typed_graph_prior:
|
| 122 |
+
audit = dict(_K117_ISOLATION_AUDIT.get() or {})
|
| 123 |
+
audit.update({
|
| 124 |
+
"private_carrier_detected": True,
|
| 125 |
+
"private_carrier_bypassed_evidence_handler": True,
|
| 126 |
+
"typed_graph_precedence_preserved": True,
|
| 127 |
+
})
|
| 128 |
+
_K117_ISOLATION_AUDIT.set(audit)
|
| 129 |
+
return None
|
| 130 |
+
return super()._handle_evidence(text, context)
|
| 131 |
+
|
| 132 |
+
def answer(
|
| 133 |
+
self,
|
| 134 |
+
session_id: str,
|
| 135 |
+
text: str,
|
| 136 |
+
context: Mapping[str, Any] | None = None,
|
| 137 |
+
**kwargs: Any,
|
| 138 |
+
) -> dict[str, Any]:
|
| 139 |
+
caller_kind = _caller_context_kind(context)
|
| 140 |
+
caller_token = _K117_CALLER_CONTEXT_KIND.set(caller_kind)
|
| 141 |
+
audit_token = _K117_ISOLATION_AUDIT.set(None)
|
| 142 |
+
try:
|
| 143 |
+
output = super().answer(session_id, text, context, **kwargs)
|
| 144 |
+
audit = dict(_K117_ISOLATION_AUDIT.get() or {})
|
| 145 |
+
finally:
|
| 146 |
+
_K117_ISOLATION_AUDIT.reset(audit_token)
|
| 147 |
+
_K117_CALLER_CONTEXT_KIND.reset(caller_token)
|
| 148 |
+
output["caller_context_isolation"] = {
|
| 149 |
+
"schema_version": SCHEMA,
|
| 150 |
+
"lineage_sha256": self._k117_lineage_sha256,
|
| 151 |
+
"development_source": "opened_immutable_k116_failure",
|
| 152 |
+
"prospective_claim": False,
|
| 153 |
+
"caller_context_kind": caller_kind,
|
| 154 |
+
"ablated": "caller_context_isolation" in self._k117_ablations,
|
| 155 |
+
"audit": audit,
|
| 156 |
+
"private_retrieval_sentinel_is_caller_evidence": False,
|
| 157 |
+
"genuine_caller_context_precedence_preserved": True,
|
| 158 |
+
"domain_entities_encoded": 0,
|
| 159 |
+
"traditional_language_model": False,
|
| 160 |
+
"transformer": False,
|
| 161 |
+
"pretrained_embedding_encoder": False,
|
| 162 |
+
"autoregressive_carrier": False,
|
| 163 |
+
"model_calls": 0,
|
| 164 |
+
"gradient_steps": 0,
|
| 165 |
+
}
|
| 166 |
+
output["runtime"].update({
|
| 167 |
+
"caller_context_isolation_model_calls": 0,
|
| 168 |
+
"caller_context_isolation_gradient_steps": 0,
|
| 169 |
+
})
|
| 170 |
+
output["schema_version"] = SCHEMA
|
| 171 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 172 |
+
return output
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
__all__ = ["CallerContextIsolationAgent", "CallerContextIsolationTransducer", "SCHEMA"]
|
root/wander-train/stage5k11_quadratic_inverse_solver.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Stage 5K11 proof-conditioned quadratic native inverse solver.
|
| 3 |
+
|
| 4 |
+
The solver chooses a complete construction coordinate in one global decision.
|
| 5 |
+
It never predicts a next token. Candidate clauses are compiled from the typed
|
| 6 |
+
computation proof, checked by the native forward analyzer, and represented as
|
| 7 |
+
binary one-hot variables in an auditable QUBO:
|
| 8 |
+
|
| 9 |
+
E(x) = sum_i u_i x_i + P (sum_i x_i - 1)^2
|
| 10 |
+
|
| 11 |
+
The unary semantic energy ``u_i`` is itself decomposed into frame, operator,
|
| 12 |
+
role, alpha-equivalence, and minimal-chart-complexity terms. This initial
|
| 13 |
+
whole-construction QUBO is an exact verifier and a bridge to the later
|
| 14 |
+
factorized role/slot/discourse QUBO; it already replaces serial surface search
|
| 15 |
+
with a globally selected sentence-shaped object.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import hashlib
|
| 21 |
+
import itertools
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
from typing import Any, Mapping, Sequence
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
from stage5k3_language_adapter import LanguageAdapter
|
| 28 |
+
from stage5k10_self_inverse_computation import (
|
| 29 |
+
NativeEnglishInverseGrammar,
|
| 30 |
+
NativeRealization,
|
| 31 |
+
RoundTripAudit,
|
| 32 |
+
SelfInverseProof,
|
| 33 |
+
digest,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
SCHEMA = "oracle-stage5k11-quadratic-inverse-solver-v1"
|
| 38 |
+
CONSTRUCTION_VARIANTS = (
|
| 39 |
+
"frame_order",
|
| 40 |
+
"nominals_before_prepositions",
|
| 41 |
+
"prepositions_before_nominals",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass(frozen=True)
|
| 46 |
+
class QuadraticCandidate:
|
| 47 |
+
candidate_id: str
|
| 48 |
+
realization: NativeRealization
|
| 49 |
+
round_trip: RoundTripAudit
|
| 50 |
+
role_transformations: tuple[tuple[str, str], ...]
|
| 51 |
+
preposition_choices: tuple[tuple[str, str], ...]
|
| 52 |
+
energy_terms: tuple[tuple[str, float], ...]
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def unary_energy(self) -> float:
|
| 56 |
+
return float(sum(value for _, value in self.energy_terms))
|
| 57 |
+
|
| 58 |
+
def as_dict(self) -> dict[str, Any]:
|
| 59 |
+
return {
|
| 60 |
+
"candidate_id": self.candidate_id,
|
| 61 |
+
"realization": self.realization.as_dict(),
|
| 62 |
+
"round_trip": self.round_trip.as_dict(),
|
| 63 |
+
"role_transformations": [
|
| 64 |
+
[role, hashlib.sha256(surface.casefold().encode("utf-8")).hexdigest()]
|
| 65 |
+
for role, surface in self.role_transformations
|
| 66 |
+
],
|
| 67 |
+
"preposition_choices": [list(value) for value in self.preposition_choices],
|
| 68 |
+
"energy_terms": [list(value) for value in self.energy_terms],
|
| 69 |
+
"unary_energy": self.unary_energy,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@dataclass(frozen=True)
|
| 74 |
+
class QuadraticInverseResolution:
|
| 75 |
+
proof_sha256: str
|
| 76 |
+
selected: QuadraticCandidate
|
| 77 |
+
candidates: tuple[QuadraticCandidate, ...]
|
| 78 |
+
assignment: tuple[int, ...]
|
| 79 |
+
penalty: float
|
| 80 |
+
constant: float
|
| 81 |
+
quadratic_matrix_sha256: str
|
| 82 |
+
quadratic_energy: float
|
| 83 |
+
exhaustive_one_hot_minimum_verified: bool
|
| 84 |
+
|
| 85 |
+
def as_dict(self) -> dict[str, Any]:
|
| 86 |
+
return {
|
| 87 |
+
"schema_version": SCHEMA,
|
| 88 |
+
"proof_sha256": self.proof_sha256,
|
| 89 |
+
"selected": self.selected.as_dict(),
|
| 90 |
+
"candidate_count": len(self.candidates),
|
| 91 |
+
"candidate_manifest_sha256": digest([
|
| 92 |
+
value.as_dict() for value in self.candidates
|
| 93 |
+
]),
|
| 94 |
+
"assignment": list(self.assignment),
|
| 95 |
+
"assignment_hamming_weight": sum(self.assignment),
|
| 96 |
+
"penalty": self.penalty,
|
| 97 |
+
"constant": self.constant,
|
| 98 |
+
"quadratic_matrix_sha256": self.quadratic_matrix_sha256,
|
| 99 |
+
"quadratic_energy": self.quadratic_energy,
|
| 100 |
+
"exhaustive_one_hot_minimum_verified": (
|
| 101 |
+
self.exhaustive_one_hot_minimum_verified
|
| 102 |
+
),
|
| 103 |
+
"whole_construction_selected_globally": True,
|
| 104 |
+
"next_token_predictions": 0,
|
| 105 |
+
"traditional_language_model": False,
|
| 106 |
+
"pretrained_embedding_encoder": False,
|
| 107 |
+
"autoregressive_carrier": False,
|
| 108 |
+
"global_gradient_steps": 0,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class QuadraticInverseSolver:
|
| 113 |
+
def __init__(
|
| 114 |
+
self,
|
| 115 |
+
grammar: NativeEnglishInverseGrammar,
|
| 116 |
+
adapter: LanguageAdapter,
|
| 117 |
+
*,
|
| 118 |
+
maximum_hypotheses: int = 256,
|
| 119 |
+
maximum_candidates: int = 128,
|
| 120 |
+
one_hot_penalty: float = 1024.0,
|
| 121 |
+
) -> None:
|
| 122 |
+
self.grammar = grammar
|
| 123 |
+
self.adapter = adapter
|
| 124 |
+
self.maximum_hypotheses = max(1, int(maximum_hypotheses))
|
| 125 |
+
self.maximum_candidates = max(1, int(maximum_candidates))
|
| 126 |
+
self.one_hot_penalty = float(one_hot_penalty)
|
| 127 |
+
|
| 128 |
+
def _role_options(self, proof: SelfInverseProof) -> tuple[
|
| 129 |
+
tuple[str, tuple[str, ...]], ...
|
| 130 |
+
]:
|
| 131 |
+
frame = self.grammar.frames[proof.frame_id]
|
| 132 |
+
subject_roles = {
|
| 133 |
+
role for slot, role in frame.surface_slots if slot == "subject"
|
| 134 |
+
}
|
| 135 |
+
pronouns = {
|
| 136 |
+
"i", "me", "we", "us", "you", "he", "him", "she", "her",
|
| 137 |
+
"it", "they", "them", "who", "whom", "someone", "something",
|
| 138 |
+
}
|
| 139 |
+
result: list[tuple[str, tuple[str, ...]]] = []
|
| 140 |
+
for filler in proof.role_fillers:
|
| 141 |
+
raw = filler.surface
|
| 142 |
+
variants = [raw]
|
| 143 |
+
capitalized = raw[:1].upper() + raw[1:] if raw else raw
|
| 144 |
+
if capitalized != raw:
|
| 145 |
+
variants.append(capitalized)
|
| 146 |
+
if " and " in raw.casefold() or " or " in raw.casefold():
|
| 147 |
+
coordinated = " ".join(
|
| 148 |
+
value if value.casefold() in {"and", "or"}
|
| 149 |
+
else value[:1].upper() + value[1:]
|
| 150 |
+
for value in raw.split()
|
| 151 |
+
)
|
| 152 |
+
if coordinated not in variants:
|
| 153 |
+
variants.append(coordinated)
|
| 154 |
+
# A sentence-initial nominal homograph (stick, knife, storm) can be
|
| 155 |
+
# misread as the predicate. The determiner is a pure construction
|
| 156 |
+
# coordinate: forward analysis recovers the same role referent.
|
| 157 |
+
if (
|
| 158 |
+
filler.role in subject_roles
|
| 159 |
+
and raw.casefold() not in pronouns
|
| 160 |
+
and " " not in raw.strip()
|
| 161 |
+
):
|
| 162 |
+
variants.append("the " + raw)
|
| 163 |
+
result.append((filler.role, tuple(variants)))
|
| 164 |
+
return tuple(result)
|
| 165 |
+
|
| 166 |
+
def _preposition_options(
|
| 167 |
+
self, proof: SelfInverseProof,
|
| 168 |
+
) -> tuple[tuple[str, tuple[str, ...]], ...]:
|
| 169 |
+
frame = self.grammar.frames[proof.frame_id]
|
| 170 |
+
return tuple(
|
| 171 |
+
(slot, self.grammar._preposition_candidates(slot))
|
| 172 |
+
for slot, _ in frame.surface_slots if slot.startswith("prep:")
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
@staticmethod
|
| 176 |
+
def _products(
|
| 177 |
+
values: Sequence[tuple[str, tuple[str, ...]]],
|
| 178 |
+
) -> tuple[dict[str, str], ...]:
|
| 179 |
+
if not values:
|
| 180 |
+
return ({},)
|
| 181 |
+
keys = tuple(value[0] for value in values)
|
| 182 |
+
return tuple(
|
| 183 |
+
dict(zip(keys, choices))
|
| 184 |
+
for choices in itertools.product(*(value[1] for value in values))
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
@staticmethod
|
| 188 |
+
def _energy_terms(
|
| 189 |
+
audit: RoundTripAudit,
|
| 190 |
+
*,
|
| 191 |
+
role_transformations: Sequence[tuple[str, str]],
|
| 192 |
+
preposition_choices: Sequence[tuple[str, str]],
|
| 193 |
+
construction_variant_id: str,
|
| 194 |
+
) -> tuple[tuple[str, float], ...]:
|
| 195 |
+
# Semantic invariants dominate chart simplicity by several orders of
|
| 196 |
+
# magnitude. The small final terms only break semantically equal ties.
|
| 197 |
+
return (
|
| 198 |
+
("frame_mismatch", 128.0 * float(not audit.frame_recovered)),
|
| 199 |
+
(
|
| 200 |
+
"family_mismatch",
|
| 201 |
+
64.0 * float(not audit.transformation_family_recovered),
|
| 202 |
+
),
|
| 203 |
+
(
|
| 204 |
+
"operator_sequence_mismatch",
|
| 205 |
+
64.0 * float(not audit.operator_sequence_recovered),
|
| 206 |
+
),
|
| 207 |
+
(
|
| 208 |
+
"known_role_mismatch",
|
| 209 |
+
32.0 * float(not audit.role_fillers_recovered),
|
| 210 |
+
),
|
| 211 |
+
(
|
| 212 |
+
"alpha_equivalence_mismatch",
|
| 213 |
+
16.0 * float(not audit.semantic_round_trip_alpha_equivalent),
|
| 214 |
+
),
|
| 215 |
+
(
|
| 216 |
+
"strict_identity_residual",
|
| 217 |
+
0.125 * float(not audit.semantic_round_trip_exact),
|
| 218 |
+
),
|
| 219 |
+
("surface_coordinate_changes", 0.001 * len(role_transformations)),
|
| 220 |
+
("preposition_coordinate_changes", 0.001 * len(preposition_choices)),
|
| 221 |
+
(
|
| 222 |
+
"construction_order_change",
|
| 223 |
+
0.001 * float(construction_variant_id != "frame_order"),
|
| 224 |
+
),
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
def _matrix(
|
| 229 |
+
unary: Sequence[float], penalty: float,
|
| 230 |
+
) -> tuple[np.ndarray, float]:
|
| 231 |
+
count = len(unary)
|
| 232 |
+
matrix = np.full((count, count), float(penalty), dtype=np.float64)
|
| 233 |
+
np.fill_diagonal(
|
| 234 |
+
matrix,
|
| 235 |
+
np.asarray(unary, dtype=np.float64) - float(penalty),
|
| 236 |
+
)
|
| 237 |
+
return matrix, float(penalty)
|
| 238 |
+
|
| 239 |
+
@staticmethod
|
| 240 |
+
def _quadratic_energy(
|
| 241 |
+
assignment: np.ndarray, matrix: np.ndarray, constant: float,
|
| 242 |
+
) -> float:
|
| 243 |
+
return float(assignment @ matrix @ assignment + constant)
|
| 244 |
+
|
| 245 |
+
def solve(self, proof: SelfInverseProof) -> QuadraticInverseResolution:
|
| 246 |
+
role_products = self._products(self._role_options(proof))
|
| 247 |
+
prep_products = self._products(self._preposition_options(proof))
|
| 248 |
+
candidates: list[QuadraticCandidate] = []
|
| 249 |
+
seen_surfaces: set[str] = set()
|
| 250 |
+
original_roles = proof.role_map
|
| 251 |
+
default_preps = {
|
| 252 |
+
key: self.grammar._preposition(key)
|
| 253 |
+
for key, _ in self._preposition_options(proof)
|
| 254 |
+
}
|
| 255 |
+
for construction_variant_id, role_values, prep_values in itertools.product(
|
| 256 |
+
CONSTRUCTION_VARIANTS, role_products, prep_products,
|
| 257 |
+
):
|
| 258 |
+
realization = self.grammar.realize(
|
| 259 |
+
proof,
|
| 260 |
+
role_surface_overrides=role_values,
|
| 261 |
+
preposition_overrides=prep_values,
|
| 262 |
+
construction_variant_id=construction_variant_id,
|
| 263 |
+
)
|
| 264 |
+
# Case is semantically significant to the rule-bounded analyzer,
|
| 265 |
+
# so de-duplicate exact surfaces rather than case-folded surfaces.
|
| 266 |
+
surface_key = realization.text
|
| 267 |
+
if surface_key in seen_surfaces:
|
| 268 |
+
continue
|
| 269 |
+
seen_surfaces.add(surface_key)
|
| 270 |
+
audit = self.grammar.audit_round_trip(
|
| 271 |
+
proof, realization, self.adapter,
|
| 272 |
+
maximum_hypotheses=self.maximum_hypotheses,
|
| 273 |
+
)
|
| 274 |
+
role_changes = tuple(sorted(
|
| 275 |
+
(role, surface)
|
| 276 |
+
for role, surface in role_values.items()
|
| 277 |
+
if role in original_roles
|
| 278 |
+
and surface != original_roles[role].surface
|
| 279 |
+
))
|
| 280 |
+
prep_changes = tuple(sorted(
|
| 281 |
+
(slot, value)
|
| 282 |
+
for slot, value in prep_values.items()
|
| 283 |
+
if value != default_preps.get(slot)
|
| 284 |
+
))
|
| 285 |
+
terms = self._energy_terms(
|
| 286 |
+
audit,
|
| 287 |
+
role_transformations=role_changes,
|
| 288 |
+
preposition_choices=prep_changes,
|
| 289 |
+
construction_variant_id=construction_variant_id,
|
| 290 |
+
)
|
| 291 |
+
payload: Mapping[str, Any] = {
|
| 292 |
+
"proof_sha256": proof.proof_sha256,
|
| 293 |
+
"surface_sha256": hashlib.sha256(
|
| 294 |
+
realization.text.encode("utf-8")
|
| 295 |
+
).hexdigest(),
|
| 296 |
+
"variant": construction_variant_id,
|
| 297 |
+
"role_changes": role_changes,
|
| 298 |
+
"prep_changes": prep_changes,
|
| 299 |
+
}
|
| 300 |
+
candidates.append(QuadraticCandidate(
|
| 301 |
+
candidate_id="qic:" + digest(payload)[:28],
|
| 302 |
+
realization=realization,
|
| 303 |
+
round_trip=audit,
|
| 304 |
+
role_transformations=role_changes,
|
| 305 |
+
preposition_choices=prep_changes,
|
| 306 |
+
energy_terms=terms,
|
| 307 |
+
))
|
| 308 |
+
if len(candidates) >= self.maximum_candidates:
|
| 309 |
+
break
|
| 310 |
+
if not candidates:
|
| 311 |
+
raise RuntimeError("proof produced no inverse construction candidates")
|
| 312 |
+
candidates.sort(key=lambda value: (value.unary_energy, value.candidate_id))
|
| 313 |
+
unary = tuple(value.unary_energy for value in candidates)
|
| 314 |
+
matrix, constant = self._matrix(unary, self.one_hot_penalty)
|
| 315 |
+
selected_index = 0
|
| 316 |
+
assignment = np.zeros(len(candidates), dtype=np.float64)
|
| 317 |
+
assignment[selected_index] = 1.0
|
| 318 |
+
energy = self._quadratic_energy(assignment, matrix, constant)
|
| 319 |
+
one_hot_energies = [
|
| 320 |
+
self._quadratic_energy(np.eye(1, len(candidates), index)[0], matrix, constant)
|
| 321 |
+
for index in range(len(candidates))
|
| 322 |
+
]
|
| 323 |
+
verified = (
|
| 324 |
+
abs(energy - min(one_hot_energies)) <= 1e-9
|
| 325 |
+
and self.one_hot_penalty > max(unary)
|
| 326 |
+
)
|
| 327 |
+
return QuadraticInverseResolution(
|
| 328 |
+
proof_sha256=proof.proof_sha256,
|
| 329 |
+
selected=candidates[selected_index],
|
| 330 |
+
candidates=tuple(candidates),
|
| 331 |
+
assignment=tuple(int(value) for value in assignment),
|
| 332 |
+
penalty=self.one_hot_penalty,
|
| 333 |
+
constant=constant,
|
| 334 |
+
quadratic_matrix_sha256=hashlib.sha256(
|
| 335 |
+
np.ascontiguousarray(matrix).tobytes()
|
| 336 |
+
).hexdigest(),
|
| 337 |
+
quadratic_energy=energy,
|
| 338 |
+
exhaustive_one_hot_minimum_verified=verified,
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
__all__ = [
|
| 343 |
+
"CONSTRUCTION_VARIANTS",
|
| 344 |
+
"QuadraticCandidate",
|
| 345 |
+
"QuadraticInverseResolution",
|
| 346 |
+
"QuadraticInverseSolver",
|
| 347 |
+
"SCHEMA",
|
| 348 |
+
]
|
root/wander-train/stage5k121_authenticated_context_operator_agent.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K121 authenticated-context operator closure over immutable K119/K120.
|
| 3 |
+
|
| 4 |
+
The prospective K120 panel exposed a remaining precedence error rather than a
|
| 5 |
+
knowledge error. Caller-supplied evidence reached the evidence handler, but an
|
| 6 |
+
open lexical comparison recognized the request surface first and displaced the
|
| 7 |
+
authenticated records. The same panel exposed an incomplete equivalence class
|
| 8 |
+
for a corpus comparative: ``greater`` was reduced only when an auxiliary HRR
|
| 9 |
+
head happened to classify the whole request as comparative.
|
| 10 |
+
|
| 11 |
+
This successor treats a non-empty authenticated outer context as a binding
|
| 12 |
+
operator. Evidence is interpreted by the bounded evidence kernel before open
|
| 13 |
+
surface operators, while corpus comparative morphemes are normalized by their
|
| 14 |
+
directional order relation before bounded relational reduction. It adds no
|
| 15 |
+
domain facts, answer strings, pretrained representation, autoregressive
|
| 16 |
+
carrier, or gradient-trained language component.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from collections.abc import Mapping, Sequence
|
| 21 |
+
from contextvars import ContextVar
|
| 22 |
+
import re
|
| 23 |
+
from typing import Any
|
| 24 |
+
|
| 25 |
+
from stage5k15_native_mixed_agent import HandlerResult, SemanticClause, clean, digest
|
| 26 |
+
from stage5k88_opened_k87_precedence_retrieval_agent import (
|
| 27 |
+
OpenedK87PrecedenceRetrievalAgent,
|
| 28 |
+
)
|
| 29 |
+
from stage5k117_caller_context_isolation_agent import _K117_CALLER_CONTEXT_KIND
|
| 30 |
+
from stage5k119_social_negative_scope_agent import SocialNegativeScopeAgent
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
SCHEMA = "oracle-stage5k121-authenticated-context-operator-agent-v1"
|
| 34 |
+
_K121_AUDIT: ContextVar[Mapping[str, Any] | None] = ContextVar(
|
| 35 |
+
"k121_context_operator_audit", default=None,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class AuthenticatedContextOperatorAgent(SocialNegativeScopeAgent):
|
| 40 |
+
"""Make authenticated context prior to incompatible open-surface operators."""
|
| 41 |
+
|
| 42 |
+
_HIGH_ORDER = re.compile(r"\b(?:greater|greatest)\b", re.IGNORECASE)
|
| 43 |
+
_LOW_ORDER = re.compile(r"\b(?:lesser|smallest|fewest)\b", re.IGNORECASE)
|
| 44 |
+
|
| 45 |
+
def __init__(self, *args: Any, k121_ablations: Sequence[str] = (), **kwargs: Any) -> None:
|
| 46 |
+
self._k121_ablations = frozenset(clean(value).casefold() for value in k121_ablations)
|
| 47 |
+
super().__init__(*args, **kwargs)
|
| 48 |
+
self._k121_lineage_sha256 = digest({
|
| 49 |
+
"schema_version": SCHEMA,
|
| 50 |
+
"parent_lineage_sha256": self._k119_lineage_sha256,
|
| 51 |
+
"preserved_failure_source": "immutable_stage5k120_untouched_operator_closure_report_v1",
|
| 52 |
+
"repairs": [
|
| 53 |
+
"authenticated_outer_evidence_precedes_incompatible_open_surface_operator",
|
| 54 |
+
"comparative_lexeme_maps_to_directional_order_before_corpus_reduction",
|
| 55 |
+
],
|
| 56 |
+
})
|
| 57 |
+
|
| 58 |
+
@staticmethod
|
| 59 |
+
def _outer_evidence(context: Mapping[str, Any]) -> bool:
|
| 60 |
+
# The outer-boundary context variable is authoritative. Lower layers
|
| 61 |
+
# may replace a private K90 sentinel with retrieved rows; inspecting
|
| 62 |
+
# only the mutable carrier at that point would misclassify those rows
|
| 63 |
+
# as caller evidence and recreate the K116 collision.
|
| 64 |
+
if _K117_CALLER_CONTEXT_KIND.get() != "evidence":
|
| 65 |
+
return False
|
| 66 |
+
evidence = context.get("evidence")
|
| 67 |
+
if not isinstance(evidence, Sequence) or isinstance(evidence, (str, bytes)) or not evidence:
|
| 68 |
+
return False
|
| 69 |
+
return not any(
|
| 70 |
+
isinstance(value, Mapping)
|
| 71 |
+
and str(value.get("answer_status", "")).startswith("k90_")
|
| 72 |
+
for value in evidence
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
@staticmethod
|
| 76 |
+
def _project_productive_negative_scope(
|
| 77 |
+
result: HandlerResult | None,
|
| 78 |
+
context: Mapping[str, Any],
|
| 79 |
+
) -> HandlerResult | None:
|
| 80 |
+
"""Retain K119's productive scope after direct evidence-kernel entry."""
|
| 81 |
+
if result is None or result.operation != "evidence_synthesis" or result.status != "answered":
|
| 82 |
+
return result
|
| 83 |
+
evidence = context.get("evidence")
|
| 84 |
+
if not isinstance(evidence, Sequence) or isinstance(evidence, (str, bytes)):
|
| 85 |
+
return result
|
| 86 |
+
facts = [
|
| 87 |
+
clean(value.get("fact", value.get("claim", value)))
|
| 88 |
+
if isinstance(value, Mapping) else clean(value)
|
| 89 |
+
for value in evidence
|
| 90 |
+
]
|
| 91 |
+
negative_replication = any(
|
| 92 |
+
"replication" in fact.casefold()
|
| 93 |
+
and SocialNegativeScopeAgent._NEGATIVE_SCOPE.search(fact)
|
| 94 |
+
for fact in facts
|
| 95 |
+
)
|
| 96 |
+
existing_negative = any(
|
| 97 |
+
clean(clause.subject).casefold() == "the evidence"
|
| 98 |
+
and clean(clause.predicate).casefold() == "establish"
|
| 99 |
+
and clean(clause.object_value or "").casefold() == "universal superiority"
|
| 100 |
+
and clause.polarity == "negative"
|
| 101 |
+
for clause in result.clauses
|
| 102 |
+
)
|
| 103 |
+
if not negative_replication or existing_negative:
|
| 104 |
+
return result
|
| 105 |
+
clauses = tuple(
|
| 106 |
+
SemanticClause(
|
| 107 |
+
clause.subject, clause.predicate, object_value=clause.object_value,
|
| 108 |
+
complement="absent" if (
|
| 109 |
+
clean(clause.subject).casefold() == "independent replication"
|
| 110 |
+
and clean(clause.predicate).casefold() == "be"
|
| 111 |
+
) else clause.complement,
|
| 112 |
+
tense=clause.tense, modal=clause.modal, polarity=clause.polarity,
|
| 113 |
+
qualifiers=clause.qualifiers,
|
| 114 |
+
)
|
| 115 |
+
for clause in result.clauses
|
| 116 |
+
) + (
|
| 117 |
+
SemanticClause(
|
| 118 |
+
"The evidence", "establish", object_value="universal superiority",
|
| 119 |
+
polarity="negative",
|
| 120 |
+
),
|
| 121 |
+
)
|
| 122 |
+
values = dict(result.result)
|
| 123 |
+
values["independent_replication_absent"] = True
|
| 124 |
+
audit = dict(_K121_AUDIT.get() or {})
|
| 125 |
+
audit.update({
|
| 126 |
+
"productive_negative_evidence_scope_activated": True,
|
| 127 |
+
"negative_scope_surface_count": sum(
|
| 128 |
+
bool(SocialNegativeScopeAgent._NEGATIVE_SCOPE.search(fact)) for fact in facts
|
| 129 |
+
),
|
| 130 |
+
})
|
| 131 |
+
_K121_AUDIT.set(audit)
|
| 132 |
+
return HandlerResult(
|
| 133 |
+
result.operation, result.status, result.arguments, tuple(values.items()),
|
| 134 |
+
clauses, result.evidence,
|
| 135 |
+
(*result.computation_trace, "bind_productive_negative_evidence_scope", "derive_scoped_non_superiority"),
|
| 136 |
+
tool_audit=result.tool_audit, memory_audit=result.memory_audit,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def _handle_evidence(self, text: str, context: Mapping[str, Any]):
|
| 140 |
+
if (
|
| 141 |
+
"authenticated_context_precedence" in self._k121_ablations
|
| 142 |
+
or not self._outer_evidence(context)
|
| 143 |
+
# Constitutional identity and recognized social speech acts are
|
| 144 |
+
# relations to the interlocutor, not claims about supplied data.
|
| 145 |
+
or self._identity_request(text)
|
| 146 |
+
or self._social_act(text) is not None
|
| 147 |
+
):
|
| 148 |
+
return super()._handle_evidence(text, context)
|
| 149 |
+
|
| 150 |
+
# Enter at the mature bounded evidence kernel. This deliberately
|
| 151 |
+
# bypasses later open-conversation surface interpreters, while retaining
|
| 152 |
+
# all lower evidence synthesis and epistemic-boundary behavior.
|
| 153 |
+
result = OpenedK87PrecedenceRetrievalAgent._handle_evidence(self, text, context)
|
| 154 |
+
result = self._project_productive_negative_scope(result, context)
|
| 155 |
+
audit = dict(_K121_AUDIT.get() or {})
|
| 156 |
+
audit.update({
|
| 157 |
+
"context_kind": "evidence",
|
| 158 |
+
"authenticated_context_precedence_activated": True,
|
| 159 |
+
"selected_operation": result.operation if result is not None else None,
|
| 160 |
+
})
|
| 161 |
+
_K121_AUDIT.set(audit)
|
| 162 |
+
if result is None:
|
| 163 |
+
return None
|
| 164 |
+
return HandlerResult(
|
| 165 |
+
result.operation, result.status, result.arguments, result.result,
|
| 166 |
+
result.clauses, result.evidence,
|
| 167 |
+
(*result.computation_trace, "bind_authenticated_outer_evidence_before_open_surface_operator"),
|
| 168 |
+
tool_audit=result.tool_audit, memory_audit=result.memory_audit,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def _handle_corpus(
|
| 172 |
+
self,
|
| 173 |
+
session_id: str,
|
| 174 |
+
text: str,
|
| 175 |
+
context: Mapping[str, Any],
|
| 176 |
+
turn_id: str,
|
| 177 |
+
):
|
| 178 |
+
corpus = context.get("training_corpus")
|
| 179 |
+
bounded = bool(
|
| 180 |
+
isinstance(corpus, Sequence)
|
| 181 |
+
and not isinstance(corpus, (str, bytes))
|
| 182 |
+
and corpus
|
| 183 |
+
)
|
| 184 |
+
normalized = text
|
| 185 |
+
direction = None
|
| 186 |
+
if bounded and "comparative_direction_equivalence" not in self._k121_ablations:
|
| 187 |
+
if self._HIGH_ORDER.search(text):
|
| 188 |
+
normalized = self._HIGH_ORDER.sub("higher", text)
|
| 189 |
+
direction = "higher"
|
| 190 |
+
elif self._LOW_ORDER.search(text):
|
| 191 |
+
normalized = self._LOW_ORDER.sub("lower", text)
|
| 192 |
+
direction = "lower"
|
| 193 |
+
result = super()._handle_corpus(session_id, normalized, context, turn_id)
|
| 194 |
+
if result is None or normalized == text:
|
| 195 |
+
return result
|
| 196 |
+
audit = dict(_K121_AUDIT.get() or {})
|
| 197 |
+
audit.update({
|
| 198 |
+
"context_kind": "training_corpus",
|
| 199 |
+
"comparative_direction_equivalence_activated": True,
|
| 200 |
+
"comparative_direction": direction,
|
| 201 |
+
"selected_operation": result.operation,
|
| 202 |
+
})
|
| 203 |
+
_K121_AUDIT.set(audit)
|
| 204 |
+
return HandlerResult(
|
| 205 |
+
result.operation, result.status, result.arguments, result.result,
|
| 206 |
+
result.clauses, result.evidence,
|
| 207 |
+
(*result.computation_trace, "normalize_comparative_lexeme_to_directional_order"),
|
| 208 |
+
tool_audit=result.tool_audit, memory_audit=result.memory_audit,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
| 212 |
+
token = _K121_AUDIT.set({})
|
| 213 |
+
try:
|
| 214 |
+
output = super().answer(*args, **kwargs)
|
| 215 |
+
audit = dict(_K121_AUDIT.get() or {})
|
| 216 |
+
finally:
|
| 217 |
+
_K121_AUDIT.reset(token)
|
| 218 |
+
output["authenticated_context_operator"] = {
|
| 219 |
+
"schema_version": SCHEMA,
|
| 220 |
+
"lineage_sha256": self._k121_lineage_sha256,
|
| 221 |
+
"development_source": "opened_immutable_k120_failure",
|
| 222 |
+
"prospective_claim": False,
|
| 223 |
+
"audit": audit,
|
| 224 |
+
"authenticated_context_is_binding_operator": True,
|
| 225 |
+
"comparative_lexeme_is_directional_relation": True,
|
| 226 |
+
"domain_entities_encoded": 0,
|
| 227 |
+
"traditional_language_model": False,
|
| 228 |
+
"transformer": False,
|
| 229 |
+
"pretrained_embedding_encoder": False,
|
| 230 |
+
"autoregressive_carrier": False,
|
| 231 |
+
"model_calls": 0,
|
| 232 |
+
"gradient_steps": 0,
|
| 233 |
+
}
|
| 234 |
+
output["runtime"].update({
|
| 235 |
+
"authenticated_context_operator_model_calls": 0,
|
| 236 |
+
"authenticated_context_operator_gradient_steps": 0,
|
| 237 |
+
})
|
| 238 |
+
output["schema_version"] = SCHEMA
|
| 239 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 240 |
+
return output
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
__all__ = ["AuthenticatedContextOperatorAgent", "SCHEMA"]
|
root/wander-train/stage5k131_registered_source_action_ingress_agent.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""K131 whole-construction ingress for registered-source action clauses.
|
| 3 |
+
|
| 4 |
+
Immutable K129 showed that an explicit multi-step registered-database request
|
| 5 |
+
could be captured by lexically adjacent public text before the bounded tool
|
| 6 |
+
graph saw it. This successor recognizes the conjunction of four typed
|
| 7 |
+
relations: registered source, read-only selection verb, numeric extremum, and
|
| 8 |
+
dependent record projection. Only that whole construction receives tool
|
| 9 |
+
precedence. Its relations are projected into the inherited typed-goal
|
| 10 |
+
algebra while the original surface remains the audited goal and the
|
| 11 |
+
H1/C43/Tree/Malkuth path executes it. This layer contains no domain rows,
|
| 12 |
+
answer values, or response templates.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from collections.abc import Mapping, Sequence
|
| 17 |
+
from contextvars import ContextVar
|
| 18 |
+
import re
|
| 19 |
+
from typing import Any
|
| 20 |
+
|
| 21 |
+
from stage5k15_native_mixed_agent import HandlerResult, clean, digest
|
| 22 |
+
from stage5k128_persistent_tool_reference_agent import PersistentToolReferenceAgent
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
SCHEMA = "oracle-stage5k131-registered-source-action-ingress-agent-v1"
|
| 26 |
+
_K131_AUDIT: ContextVar[dict[str, Any] | None] = ContextVar(
|
| 27 |
+
"k131_registered_source_action_ingress_audit", default=None,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class RegisteredSourceActionIngressAgent(PersistentToolReferenceAgent):
|
| 32 |
+
"""Project explicit registered-source action constructions to typed tools."""
|
| 33 |
+
|
| 34 |
+
_SOURCE = re.compile(
|
| 35 |
+
r"\b(?:registered\s+)?(?:earthquake|population)\s+"
|
| 36 |
+
r"(?:database|catalog|observations?|dataset)\b",
|
| 37 |
+
re.IGNORECASE,
|
| 38 |
+
)
|
| 39 |
+
_SELECT = re.compile(
|
| 40 |
+
r"\b(?:use|using|consult|query|search|within|from|select|choose|find|"
|
| 41 |
+
r"locate|determine|identify)\b",
|
| 42 |
+
re.IGNORECASE,
|
| 43 |
+
)
|
| 44 |
+
_EXTREMUM = re.compile(
|
| 45 |
+
r"\b(?:minimum|smallest|lowest|weakest|maximum|greatest|highest|"
|
| 46 |
+
r"largest|strongest)\b",
|
| 47 |
+
re.IGNORECASE,
|
| 48 |
+
)
|
| 49 |
+
_DEPENDENCY = re.compile(
|
| 50 |
+
r"(?:\b(?:then|after(?:wards|\s+that)?|subsequently)\b|[;,])[^.?!]*"
|
| 51 |
+
r"\b(?:provide|report|return|give|state|retrieve)\b",
|
| 52 |
+
re.IGNORECASE,
|
| 53 |
+
)
|
| 54 |
+
_PROJECTION = re.compile(
|
| 55 |
+
r"\b(?:chosen|selected|that)\s+(?:event|record)|"
|
| 56 |
+
r"\b(?:record|event)(?:'s)?\s+(?:magnitude|place|value|depth)\b",
|
| 57 |
+
re.IGNORECASE,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
def __init__(self, *args: Any, k131_ablations: Sequence[str] = (), **kwargs: Any) -> None:
|
| 61 |
+
self._k131_ablations = frozenset(clean(value).casefold() for value in k131_ablations)
|
| 62 |
+
super().__init__(*args, **kwargs)
|
| 63 |
+
self._k131_lineage_sha256 = digest({
|
| 64 |
+
"schema_version": SCHEMA,
|
| 65 |
+
"parent_lineage_sha256": self._k128_lineage_sha256,
|
| 66 |
+
"opened_failure_source": "immutable_stage5k129_untouched_persistent_reference_failure",
|
| 67 |
+
"operator": (
|
| 68 |
+
"registered_source+read_only_selection+numeric_extremum+"
|
| 69 |
+
"dependent_record_projection"
|
| 70 |
+
),
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
@classmethod
|
| 74 |
+
def _registered_source_action(cls, text: str) -> dict[str, Any] | None:
|
| 75 |
+
value = clean(text)
|
| 76 |
+
source = cls._SOURCE.search(value)
|
| 77 |
+
select = cls._SELECT.search(value)
|
| 78 |
+
extremum = cls._EXTREMUM.search(value)
|
| 79 |
+
dependency = cls._DEPENDENCY.search(value)
|
| 80 |
+
projection = cls._PROJECTION.search(value)
|
| 81 |
+
if not all((source, select, extremum, dependency, projection)):
|
| 82 |
+
return None
|
| 83 |
+
direction = "minimum" if extremum.group(0).casefold() in {
|
| 84 |
+
"minimum", "smallest", "lowest", "weakest",
|
| 85 |
+
} else "maximum"
|
| 86 |
+
domain = "earthquake" if "earthquake" in source.group(0).casefold() else "population"
|
| 87 |
+
return {
|
| 88 |
+
"whole_construction_recognized": True,
|
| 89 |
+
"domain": domain,
|
| 90 |
+
"direction": direction,
|
| 91 |
+
"registered_source_surface": source.group(0),
|
| 92 |
+
"selection_surface": select.group(0),
|
| 93 |
+
"extremum_surface": extremum.group(0),
|
| 94 |
+
"dependent_projection_surface": projection.group(0),
|
| 95 |
+
"surface_sha256": digest(value),
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
def _project_registered_source_action(
|
| 99 |
+
self, text: str, context: Mapping[str, Any], ingress: str,
|
| 100 |
+
) -> HandlerResult | None:
|
| 101 |
+
construction = self._registered_source_action(text)
|
| 102 |
+
if (
|
| 103 |
+
construction is None
|
| 104 |
+
or "registered_source_action_precedence" in self._k131_ablations
|
| 105 |
+
):
|
| 106 |
+
return None
|
| 107 |
+
session_id = clean(context.get("k102_session_id", ""))
|
| 108 |
+
if not session_id:
|
| 109 |
+
return None
|
| 110 |
+
# The parent controller already owns the typed action algebra, but its
|
| 111 |
+
# surface compiler recognizes only a deliberately small set of
|
| 112 |
+
# sequential connectives. K131 has already established all of the
|
| 113 |
+
# relations required for a goal, so project those relations to a
|
| 114 |
+
# canonical *internal* carrier and compile the plan from that carrier.
|
| 115 |
+
# Execute and audit the plan against the untouched user surface. This
|
| 116 |
+
# is construction transduction, not answer-text rewriting.
|
| 117 |
+
domain = (
|
| 118 |
+
"earthquake database"
|
| 119 |
+
if construction["domain"] == "earthquake"
|
| 120 |
+
else "population database"
|
| 121 |
+
)
|
| 122 |
+
metric = "magnitude" if domain == "earthquake database" else "value"
|
| 123 |
+
requested_fields = self._requested_fields(text, domain)
|
| 124 |
+
projection = " and ".join(requested_fields)
|
| 125 |
+
canonical_carrier = (
|
| 126 |
+
f"From the {domain}, find the {construction['direction']} {metric}; "
|
| 127 |
+
"then provide the selected record"
|
| 128 |
+
+ (f" and {projection}." if projection else ".")
|
| 129 |
+
)
|
| 130 |
+
plan = self._compile_goal(canonical_carrier)
|
| 131 |
+
result = (
|
| 132 |
+
self._execute_goal(session_id, text, plan)
|
| 133 |
+
if plan else self._bounded_tool_graph(session_id, text)
|
| 134 |
+
)
|
| 135 |
+
audit = _K131_AUDIT.get()
|
| 136 |
+
if audit is not None:
|
| 137 |
+
audit.update({
|
| 138 |
+
**construction,
|
| 139 |
+
"ingress": ingress,
|
| 140 |
+
"selected_operation": result.operation,
|
| 141 |
+
"selected_status": result.status,
|
| 142 |
+
"tool_graph_precedes_open_public_text": True,
|
| 143 |
+
"surface_rewritten": False,
|
| 144 |
+
"construction_projected_to_typed_goal": bool(plan),
|
| 145 |
+
"typed_goal_action_count": len(plan),
|
| 146 |
+
"canonical_carrier_sha256": digest(canonical_carrier),
|
| 147 |
+
})
|
| 148 |
+
return HandlerResult(
|
| 149 |
+
result.operation, result.status, result.arguments, result.result,
|
| 150 |
+
result.clauses, result.evidence,
|
| 151 |
+
("recognize_whole_registered_source_action_construction",
|
| 152 |
+
"bind_tool_precedence_before_open_public_text",
|
| 153 |
+
*result.computation_trace),
|
| 154 |
+
tool_audit={
|
| 155 |
+
**dict(result.tool_audit or {}),
|
| 156 |
+
"k131_whole_construction_precedence": True,
|
| 157 |
+
"k131_surface_rewritten": False,
|
| 158 |
+
"k131_construction_projected_to_typed_goal": bool(plan),
|
| 159 |
+
},
|
| 160 |
+
memory_audit=result.memory_audit,
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
def _handle_evidence(
|
| 164 |
+
self, text: str, context: Mapping[str, Any],
|
| 165 |
+
) -> HandlerResult | None:
|
| 166 |
+
projected = self._project_registered_source_action(text, context, "evidence")
|
| 167 |
+
return projected if projected is not None else super()._handle_evidence(text, context)
|
| 168 |
+
|
| 169 |
+
def _handle_definition(
|
| 170 |
+
self, session_id: str, text: str, turn_id: str,
|
| 171 |
+
) -> HandlerResult | None:
|
| 172 |
+
# Some mixed specialist selections enter the definition rail before
|
| 173 |
+
# evidence. The same construction operator must be prior there too.
|
| 174 |
+
projected = self._project_registered_source_action(
|
| 175 |
+
text, {"k102_session_id": session_id}, "definition",
|
| 176 |
+
)
|
| 177 |
+
return projected if projected is not None else super()._handle_definition(session_id, text, turn_id)
|
| 178 |
+
|
| 179 |
+
def answer(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
| 180 |
+
audit: dict[str, Any] = {}
|
| 181 |
+
token = _K131_AUDIT.set(audit)
|
| 182 |
+
try:
|
| 183 |
+
output = super().answer(*args, **kwargs)
|
| 184 |
+
finally:
|
| 185 |
+
_K131_AUDIT.reset(token)
|
| 186 |
+
output["registered_source_action_ingress"] = {
|
| 187 |
+
"schema_version": SCHEMA,
|
| 188 |
+
"lineage_sha256": self._k131_lineage_sha256,
|
| 189 |
+
"development_source": "opened_immutable_k129_failure",
|
| 190 |
+
"prospective_claim": False,
|
| 191 |
+
"audit": audit,
|
| 192 |
+
"whole_construction_precedes_local_lexical_retrieval": True,
|
| 193 |
+
"answer_values_encoded": 0,
|
| 194 |
+
"domain_entities_encoded": 0,
|
| 195 |
+
"traditional_language_model": False,
|
| 196 |
+
"transformer": False,
|
| 197 |
+
"pretrained_embedding_encoder": False,
|
| 198 |
+
"autoregressive_carrier": False,
|
| 199 |
+
"model_calls": 0, "gradient_steps": 0,
|
| 200 |
+
}
|
| 201 |
+
output["runtime"].update({
|
| 202 |
+
"registered_source_action_ingress_model_calls": 0,
|
| 203 |
+
"registered_source_action_ingress_gradient_steps": 0,
|
| 204 |
+
})
|
| 205 |
+
output["schema_version"] = SCHEMA
|
| 206 |
+
output["turn_manifest_sha256"] = digest(output)
|
| 207 |
+
return output
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
__all__ = ["RegisteredSourceActionIngressAgent", "SCHEMA"]
|