cortexm 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- context_m.py +17 -0
- cortexm/__init__.py +45 -0
- cortexm/accel.py +403 -0
- cortexm/api/__init__.py +0 -0
- cortexm/api/chaos.py +118 -0
- cortexm/api/memory.py +635 -0
- cortexm/bench/__init__.py +0 -0
- cortexm/bench/abilities.py +311 -0
- cortexm/bench/baselines.py +89 -0
- cortexm/bench/beam_loader.py +317 -0
- cortexm/bench/generator.py +376 -0
- cortexm/bench/harness.py +211 -0
- cortexm/bench/messy.py +218 -0
- cortexm/bench/micro.py +251 -0
- cortexm/bench/ood.py +443 -0
- cortexm/bench/run.py +137 -0
- cortexm/bridge/__init__.py +0 -0
- cortexm/bridge/dates.py +178 -0
- cortexm/bridge/decoders.py +204 -0
- cortexm/bridge/enrich.py +255 -0
- cortexm/bridge/extractor.py +316 -0
- cortexm/bridge/fallback.py +332 -0
- cortexm/bridge/onnx_runtime.py +158 -0
- cortexm/bridge/patterns.py +760 -0
- cortexm/bridge/ppr.py +104 -0
- cortexm/bridge/prefilter.py +188 -0
- cortexm/bridge/query_extract.py +420 -0
- cortexm/bridge/reader.py +1174 -0
- cortexm/bridge/rerank.py +204 -0
- cortexm/bridge/writer.py +492 -0
- cortexm/cli.py +295 -0
- cortexm/cognition/__init__.py +53 -0
- cortexm/cognition/abstraction.py +192 -0
- cortexm/cognition/analogy.py +159 -0
- cortexm/cognition/engine.py +204 -0
- cortexm/cognition/gaps.py +365 -0
- cortexm/cognition/scanner.py +204 -0
- cortexm/config.py +375 -0
- cortexm/cortexm.py +8 -0
- cortexm/enterprise/__init__.py +0 -0
- cortexm/enterprise/audit.py +178 -0
- cortexm/enterprise/governance.py +239 -0
- cortexm/errors.py +35 -0
- cortexm/features/__init__.py +0 -0
- cortexm/features/git.py +204 -0
- cortexm/features/prefetch.py +88 -0
- cortexm/features/zk.py +105 -0
- cortexm/federation/__init__.py +39 -0
- cortexm/federation/crdt.py +275 -0
- cortexm/federation/fabric.py +109 -0
- cortexm/federation/hlc.py +80 -0
- cortexm/federation/node.py +145 -0
- cortexm/federation/schema_report.py +73 -0
- cortexm/federation/transport.py +164 -0
- cortexm/index/__init__.py +19 -0
- cortexm/index/nsg.py +386 -0
- cortexm/mcp/__init__.py +0 -0
- cortexm/mcp/server.py +985 -0
- cortexm/metrics.py +62 -0
- cortexm/migrate/__init__.py +0 -0
- cortexm/migrate/importers.py +192 -0
- cortexm/provenance/__init__.py +78 -0
- cortexm/provenance/agent.py +214 -0
- cortexm/provenance/cose.py +201 -0
- cortexm/provenance/scitt.py +258 -0
- cortexm/provenance/vc.py +250 -0
- cortexm/security/__init__.py +0 -0
- cortexm/security/crypto.py +162 -0
- cortexm/security/hashes.py +140 -0
- cortexm/security/injection.py +149 -0
- cortexm/security/mind.py +154 -0
- cortexm/security/pii.py +265 -0
- cortexm/security/rbac.py +169 -0
- cortexm/security/sandbox.py +131 -0
- cortexm/security/zk_hamming.py +142 -0
- cortexm/security/zk_sql.py +485 -0
- cortexm/server/__init__.py +0 -0
- cortexm/server/metrics.py +88 -0
- cortexm/server/rest.py +936 -0
- cortexm/server/sparql.py +984 -0
- cortexm/text/__init__.py +0 -0
- cortexm/text/dissim.py +252 -0
- cortexm/text/embedder.py +155 -0
- cortexm/text/fuzzy.py +218 -0
- cortexm/text/idiolect.py +253 -0
- cortexm/text/labse.py +374 -0
- cortexm/text/tokenizer.py +79 -0
- cortexm/trace/__init__.py +0 -0
- cortexm/trace/blob_arena.py +277 -0
- cortexm/trace/consolidate.py +337 -0
- cortexm/trace/contradictions.py +69 -0
- cortexm/trace/dedup.py +114 -0
- cortexm/trace/edges.py +214 -0
- cortexm/trace/fact.py +121 -0
- cortexm/trace/fade.py +245 -0
- cortexm/trace/lifecycle.py +112 -0
- cortexm/trace/rebuild.py +173 -0
- cortexm/trace/rules.py +171 -0
- cortexm/trace/store.py +680 -0
- cortexm/trace/structural.py +183 -0
- cortexm/trace/tmt.py +335 -0
- cortexm/util.py +148 -0
- cortexm/vsa/__init__.py +0 -0
- cortexm/vsa/attribution.py +149 -0
- cortexm/vsa/cleanup.py +161 -0
- cortexm/vsa/codecs.py +397 -0
- cortexm/vsa/hologram_overlay.py +139 -0
- cortexm/vsa/index.py +163 -0
- cortexm/vsa/ops.py +149 -0
- cortexm/vsa/palace.py +446 -0
- cortexm/vsa/role_vectors.py +236 -0
- cortexm/vsa/slb.py +78 -0
- cortexm/vsa/tlsh_trie.py +137 -0
- cortexm/vsa/working_memory.py +249 -0
- cortexm-0.3.0.dist-info/METADATA +482 -0
- cortexm-0.3.0.dist-info/RECORD +120 -0
- cortexm-0.3.0.dist-info/WHEEL +5 -0
- cortexm-0.3.0.dist-info/entry_points.txt +2 -0
- cortexm-0.3.0.dist-info/licenses/LICENSE +190 -0
- cortexm-0.3.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""The 10 BEAM memory abilities — probe builders + deterministic judge.
|
|
2
|
+
|
|
3
|
+
Ability taxonomy follows BEAM (arXiv:2510.27246, Table 1):
|
|
4
|
+
Abstention, Contradiction Resolution, Event Ordering, Information
|
|
5
|
+
Extraction, Instruction Following, Knowledge Update, Multi-Hop
|
|
6
|
+
Reasoning, Preference Following, Summarization, Temporal Reasoning.
|
|
7
|
+
|
|
8
|
+
Scoring is context-sufficiency ("nugget") based: a probe is answered
|
|
9
|
+
iff the retrieved memory block contains the ground-truth nuggets an
|
|
10
|
+
LLM reader would need to answer correctly — the same philosophy as
|
|
11
|
+
BEAM's nugget design, with a deterministic judge so the whole harness
|
|
12
|
+
runs offline under the μ=0 protocol (zero LLM calls, including the
|
|
13
|
+
judge). A pluggable LLM judge/reader slot is documented in
|
|
14
|
+
bench/harness.py for canonical-protocol replication.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from cortexm.util import month_name, normalize
|
|
23
|
+
|
|
24
|
+
ABILITIES = ["AB", "CR", "EO", "IE", "IF", "KU", "MH", "PF", "SZ", "TR"]
|
|
25
|
+
ABILITY_NAMES = {
|
|
26
|
+
"AB": "Abstention", "CR": "Contradiction Resolution",
|
|
27
|
+
"EO": "Event Ordering", "IE": "Information Extraction",
|
|
28
|
+
"IF": "Instruction Following", "KU": "Knowledge Update",
|
|
29
|
+
"MH": "Multi-Hop Reasoning", "PF": "Preference Following",
|
|
30
|
+
"SZ": "Summarization", "TR": "Temporal Reasoning",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
FACT_LINE = re.compile(r"^- \((.+?), ([\w]+), (.+?)\) \[valid ([^;→]+)→([^;]*?);", re.M)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Probe:
|
|
38
|
+
ability: str
|
|
39
|
+
question: str
|
|
40
|
+
user_id: str
|
|
41
|
+
entity: str
|
|
42
|
+
expected: dict = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _current(items):
|
|
46
|
+
"""Latest still-valid item from a (value, start, end) timeline."""
|
|
47
|
+
live = [it for it in items if it[2] is None]
|
|
48
|
+
return live[-1] if live else items[-1]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _prior(items, current):
|
|
52
|
+
vals = [it for it in items if it[0] != current[0]]
|
|
53
|
+
return vals[-1] if vals else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------- builders
|
|
57
|
+
def build_probes(personas, rng) -> list[Probe]:
|
|
58
|
+
probes: list[Probe] = []
|
|
59
|
+
for p in personas:
|
|
60
|
+
name = p.first
|
|
61
|
+
cur_emp = _current(p.employers)
|
|
62
|
+
prev_emp = _prior(p.employers, cur_emp)
|
|
63
|
+
cur_city = _current(p.cities)
|
|
64
|
+
prev_city = _prior(p.cities, cur_city)
|
|
65
|
+
|
|
66
|
+
# --- IE: single-fact recall ------------------------------------
|
|
67
|
+
m, d = p.birthday
|
|
68
|
+
probes += [
|
|
69
|
+
Probe("IE", q, p.user_id, p.full_name,
|
|
70
|
+
{"contains_any": [f"{month_name(m).lower()} {d}"]})
|
|
71
|
+
for q in (f"What is {name}'s birthday?",
|
|
72
|
+
f"When was {name} born?")
|
|
73
|
+
]
|
|
74
|
+
probes.append(Probe("IE", f"Who is {name}'s sister?", p.user_id,
|
|
75
|
+
p.full_name,
|
|
76
|
+
{"contains_any": [p.family[0][0].split()[0].lower()]}))
|
|
77
|
+
probes.append(Probe("IE", f"What does {name} do for a living?",
|
|
78
|
+
p.user_id, p.full_name,
|
|
79
|
+
{"contains_any": [p.roles[0][0].lower()]}))
|
|
80
|
+
probes.append(Probe("IE", f"What is {name}'s full name?",
|
|
81
|
+
p.user_id, p.full_name,
|
|
82
|
+
{"contains_any": [p.full_name.lower()]}))
|
|
83
|
+
probes.append(Probe("IE", f"What does {name} do in their free time?",
|
|
84
|
+
p.user_id, p.full_name,
|
|
85
|
+
{"contains_any": [p.hobbies[0].lower()]}))
|
|
86
|
+
|
|
87
|
+
# --- CR: contradiction resolution -------------------------------
|
|
88
|
+
probes += [
|
|
89
|
+
Probe("CR", q, p.user_id, p.full_name,
|
|
90
|
+
{"contains_any": [cur_emp[0].lower()]})
|
|
91
|
+
for q in (f"Where does {name} work now?",
|
|
92
|
+
f"What is {name}'s current employer?",
|
|
93
|
+
f"Which company does {name} currently work at?")
|
|
94
|
+
]
|
|
95
|
+
if prev_emp:
|
|
96
|
+
probes.append(Probe("CR", f"Has {name} always worked at {prev_emp[0]}?",
|
|
97
|
+
p.user_id, p.full_name,
|
|
98
|
+
{"contains_any": [cur_emp[0].lower()],
|
|
99
|
+
"also_any": [prev_emp[0].lower()]}))
|
|
100
|
+
|
|
101
|
+
# --- EO: event ordering ------------------------------------------
|
|
102
|
+
evs = sorted(p.events, key=lambda e: e[0])
|
|
103
|
+
pairs = [(evs[i], evs[j]) for i in range(len(evs))
|
|
104
|
+
for j in range(i + 1, len(evs))][:6]
|
|
105
|
+
for a, b in pairs:
|
|
106
|
+
probes.append(Probe(
|
|
107
|
+
"EO", f"Which happened first: {a[1]} or {b[1]}?",
|
|
108
|
+
p.user_id, p.full_name,
|
|
109
|
+
{"events": [(a[1], a[0]), (b[1], b[0])]}))
|
|
110
|
+
|
|
111
|
+
# --- KU: knowledge update / supersession --------------------------
|
|
112
|
+
if prev_emp:
|
|
113
|
+
probes += [
|
|
114
|
+
Probe("KU", f"Is {name} still working at {prev_emp[0]}?",
|
|
115
|
+
p.user_id, p.full_name,
|
|
116
|
+
{"current": cur_emp[0].lower(), "old": prev_emp[0].lower()})
|
|
117
|
+
for _ in range(2)
|
|
118
|
+
]
|
|
119
|
+
probes.append(Probe("KU", f"Where does {name} live these days?",
|
|
120
|
+
p.user_id, p.full_name,
|
|
121
|
+
{"contains_any": [cur_city[0].lower()]}))
|
|
122
|
+
|
|
123
|
+
# --- MH: multi-hop ------------------------------------------------
|
|
124
|
+
mgr, team = p.manager
|
|
125
|
+
tname, tech = p.team_tech
|
|
126
|
+
probes += [
|
|
127
|
+
Probe("MH", q, p.user_id, p.full_name,
|
|
128
|
+
{"contains_any": [tech.lower()], "also_any": [mgr.lower()]})
|
|
129
|
+
for q in (
|
|
130
|
+
f"What programming language does the team of {name}'s manager use?",
|
|
131
|
+
f"{name}'s manager leads a team — which language does that team use?",
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# --- PF: preference following ------------------------------------
|
|
136
|
+
by_cat: dict[str, list] = {}
|
|
137
|
+
for cat, v, s, e in p.prefs:
|
|
138
|
+
by_cat.setdefault(cat, []).append((v, s, e))
|
|
139
|
+
for cat, items in by_cat.items():
|
|
140
|
+
cur = _current(items)
|
|
141
|
+
probes += [
|
|
142
|
+
Probe("PF", q, p.user_id, p.full_name,
|
|
143
|
+
{"contains_any": [cur[0].lower()]})
|
|
144
|
+
for q in (f"What {cat} does {name} prefer now?",
|
|
145
|
+
f"If you were picking {cat} for {name}, what would you choose?")
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
# --- SZ: summarization (set F1) ----------------------------------
|
|
149
|
+
probes.append(Probe(
|
|
150
|
+
"SZ", f"List all the projects {name} has worked on.",
|
|
151
|
+
p.user_id, p.full_name,
|
|
152
|
+
{"set": [pr[0].lower() for pr in p.projects]}))
|
|
153
|
+
|
|
154
|
+
# --- TR: temporal reasoning ----------------------------------------
|
|
155
|
+
if prev_city:
|
|
156
|
+
probes += [
|
|
157
|
+
Probe("TR", f"Where did {name} live before {cur_city[0]}?",
|
|
158
|
+
p.user_id, p.full_name,
|
|
159
|
+
{"contains_any": [prev_city[0].lower()]})
|
|
160
|
+
]
|
|
161
|
+
old_job = p.employers[0]
|
|
162
|
+
if old_job[2]:
|
|
163
|
+
probes.append(Probe(
|
|
164
|
+
"TR", f"Where did {name} work in {old_job[1][:4]}?",
|
|
165
|
+
p.user_id, p.full_name,
|
|
166
|
+
{"contains_any": [old_job[0].lower()]}))
|
|
167
|
+
ev = evs[0]
|
|
168
|
+
probes.append(Probe(
|
|
169
|
+
"TR", f"What did {name} do in {month_name(int(ev[0][5:7]))} {ev[0][:4]}?",
|
|
170
|
+
p.user_id, p.full_name,
|
|
171
|
+
{"contains_any": [ev[1].lower()]}))
|
|
172
|
+
|
|
173
|
+
# --- IF: instruction following -------------------------------------
|
|
174
|
+
probes += [
|
|
175
|
+
Probe("IF", q, p.user_id, p.full_name,
|
|
176
|
+
{"contains_any": [p.instruction[1].lower()]})
|
|
177
|
+
for q in (f"In what language should you respond to {name}?"
|
|
178
|
+
if "french" in p.instruction[1].lower()
|
|
179
|
+
else f"How should answers to {name} be formatted?",
|
|
180
|
+
f"What standing instruction did {name} give you?")
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
# --- AB: abstention (never-mentioned attributes) --------------------
|
|
184
|
+
probes += [
|
|
185
|
+
Probe("AB", q, p.user_id, p.full_name,
|
|
186
|
+
{"forbidden": [kw], "entity": p.full_name.lower()})
|
|
187
|
+
for q, kw in (
|
|
188
|
+
(f"What is {name}'s favorite podcast?", "podcast"),
|
|
189
|
+
(f"What is {name}'s middle name?", "middle name"),
|
|
190
|
+
(f"What is {name}'s blood type?", "blood"),
|
|
191
|
+
(f"What is {name}'s zodiac sign?", "zodiac"),
|
|
192
|
+
)
|
|
193
|
+
]
|
|
194
|
+
return probes
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------- judge
|
|
198
|
+
def _norm(s: str) -> str:
|
|
199
|
+
return normalize(s)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _contains(context: str, key: str) -> bool:
|
|
203
|
+
k = _norm(key)
|
|
204
|
+
return k in _norm(context) if k else False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _content_words(s: str) -> set[str]:
|
|
208
|
+
from cortexm.text.tokenizer import content_words
|
|
209
|
+
return set(content_words(s))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _fuzzy_event_present(context: str, desc: str) -> bool:
|
|
213
|
+
"""Event description findable in context (word-overlap based)."""
|
|
214
|
+
target = _content_words(desc)
|
|
215
|
+
if not target:
|
|
216
|
+
return False
|
|
217
|
+
ctx_norm = _norm(context)
|
|
218
|
+
if _norm(desc) in ctx_norm:
|
|
219
|
+
return True
|
|
220
|
+
# any context segment sharing >= half the content words
|
|
221
|
+
for line in context.split("\n"):
|
|
222
|
+
words = _content_words(line)
|
|
223
|
+
if words and len(words & target) >= max(1, len(target) // 2):
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parse_fact_lines(context: str) -> list[dict]:
|
|
229
|
+
out = []
|
|
230
|
+
for m in FACT_LINE.finditer(context):
|
|
231
|
+
out.append({"subject": m.group(1), "relation": m.group(2),
|
|
232
|
+
"value": m.group(3), "valid_from": m.group(4),
|
|
233
|
+
"valid_to": m.group(5).strip() or None})
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def judge(probe: Probe, context: str) -> tuple[float, dict]:
|
|
238
|
+
"""Deterministic context-sufficiency judge. Returns (score, detail)."""
|
|
239
|
+
exp = probe.expected
|
|
240
|
+
facts = parse_fact_lines(context)
|
|
241
|
+
|
|
242
|
+
if probe.ability == "AB":
|
|
243
|
+
forbidden = exp["forbidden"]
|
|
244
|
+
if any(kw in context.lower() for kw in forbidden):
|
|
245
|
+
return 0.0, {"reason": f"fabricated/irrelevant '{forbidden}'"}
|
|
246
|
+
# a 'likes/prefers' fact presented as an answer to the persona also fails
|
|
247
|
+
ent_first = probe.entity.split()[0].lower()
|
|
248
|
+
for f in facts:
|
|
249
|
+
if (f["relation"] in ("likes", "prefers", "name", "alias")
|
|
250
|
+
and ent_first in f["subject"].lower()
|
|
251
|
+
and any(kw in f["value"].lower() for kw in forbidden)):
|
|
252
|
+
return 0.0, {"reason": "offered unrelated fact as answer"}
|
|
253
|
+
return 1.0, {"reason": "correct abstention"}
|
|
254
|
+
|
|
255
|
+
if probe.ability == "EO":
|
|
256
|
+
evs = exp["events"]
|
|
257
|
+
ok = all(_fuzzy_event_present(context, d) for d, _ in evs)
|
|
258
|
+
if ok:
|
|
259
|
+
# verify the ORDERING note, if present, is correct
|
|
260
|
+
note = re.search(r"ORDERING: (.+?) \((\d{4}-\d{2}-\d{2})\) happened before (.+?) \((\d{4}-\d{2}-\d{2})\)", context)
|
|
261
|
+
if note:
|
|
262
|
+
got = (note.group(2), note.group(4))
|
|
263
|
+
want = (evs[0][1], evs[1][1])
|
|
264
|
+
if got != want:
|
|
265
|
+
return 0.0, {"reason": "ordering note inverted"}
|
|
266
|
+
return 1.0, {"reason": "both events with dates present"}
|
|
267
|
+
return 0.0, {"reason": "event(s) missing from context"}
|
|
268
|
+
|
|
269
|
+
if probe.ability == "KU" and "current" in exp:
|
|
270
|
+
cur, old = exp["current"], exp["old"]
|
|
271
|
+
has_cur = _contains(context, cur)
|
|
272
|
+
has_left = any(f["relation"] == "left" and _contains(f["value"], old)
|
|
273
|
+
for f in facts)
|
|
274
|
+
has_expired = any(
|
|
275
|
+
f["relation"] in ("works_at", "lives_in") and _contains(f["value"], old)
|
|
276
|
+
and f["valid_to"] and f["valid_to"] not in ("∞", "")
|
|
277
|
+
for f in facts)
|
|
278
|
+
if has_cur and (has_left or has_expired):
|
|
279
|
+
return 1.0, {"reason": "current value + supersession evidence"}
|
|
280
|
+
if has_cur:
|
|
281
|
+
return 0.5, {"reason": "current value only"}
|
|
282
|
+
return 0.0, {"reason": "missing current value"}
|
|
283
|
+
|
|
284
|
+
if probe.ability == "SZ":
|
|
285
|
+
want = set(exp["set"])
|
|
286
|
+
got = set()
|
|
287
|
+
for f in facts:
|
|
288
|
+
for w in want:
|
|
289
|
+
if _contains(f["value"], w) or _contains(f["subject"], w):
|
|
290
|
+
got.add(w)
|
|
291
|
+
# also allow raw mentions anywhere in the context block
|
|
292
|
+
for w in want:
|
|
293
|
+
if _contains(context, w):
|
|
294
|
+
got.add(w)
|
|
295
|
+
if not want:
|
|
296
|
+
return 1.0, {"reason": "empty set"}
|
|
297
|
+
inter = len(got & want)
|
|
298
|
+
prec = inter / len(got) if got else 0.0
|
|
299
|
+
rec = inter / len(want)
|
|
300
|
+
f1 = 2 * prec * rec / (prec + rec) if (prec + rec) else 0.0
|
|
301
|
+
return (1.0 if f1 >= 0.5 else 0.0), {"f1": round(f1, 3),
|
|
302
|
+
"retrieved": sorted(got)}
|
|
303
|
+
|
|
304
|
+
# containment-based abilities: IE / CR / MH / PF / TR / IF
|
|
305
|
+
keys = exp.get("contains_any", [])
|
|
306
|
+
also = exp.get("also_any", [])
|
|
307
|
+
if not keys or any(_contains(context, k) for k in keys):
|
|
308
|
+
if also and not any(_contains(context, k) for k in also):
|
|
309
|
+
return 0.5, {"reason": "primary nugget only"}
|
|
310
|
+
return 1.0, {"reason": "nugget(s) present"}
|
|
311
|
+
return 0.0, {"reason": "nugget missing"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Baseline retrievers for the neuro-symbolic delta table.
|
|
2
|
+
|
|
3
|
+
* ``bm25_rag`` — lexical RAG over raw chunks (the "context stuffer"
|
|
4
|
+
proxy: BEAM's Vanilla/RAG baselines score 12-25%).
|
|
5
|
+
* ``vector_only`` — our VSA palace WITHOUT the symbolic read path:
|
|
6
|
+
pure neural fact-level RAG. Isolates exactly what
|
|
7
|
+
the symbolic Trace contributes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
from collections import Counter
|
|
15
|
+
|
|
16
|
+
from cortexm.text.tokenizer import STOPWORDS, words
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------- BM25
|
|
20
|
+
class BM25Index:
|
|
21
|
+
def __init__(self, docs: list[dict], k1: float = 1.5, b: float = 0.75) -> None:
|
|
22
|
+
self.k1, self.b = k1, b
|
|
23
|
+
self.docs = docs
|
|
24
|
+
self.doc_ids = [d["id"] for d in docs]
|
|
25
|
+
self.doc_len = []
|
|
26
|
+
self.tf: list[Counter] = []
|
|
27
|
+
self.postings: dict[str, list[tuple[int, int]]] = {}
|
|
28
|
+
for i, d in enumerate(docs):
|
|
29
|
+
toks = [t for t in words(d["text"]) if t not in STOPWORDS]
|
|
30
|
+
self.doc_len.append(len(toks))
|
|
31
|
+
counts = Counter(toks)
|
|
32
|
+
self.tf.append(counts)
|
|
33
|
+
for t, c in counts.items():
|
|
34
|
+
self.postings.setdefault(t, []).append((i, c))
|
|
35
|
+
self.N = len(docs) or 1
|
|
36
|
+
self.avgdl = (sum(self.doc_len) / self.N) if self.N else 1.0
|
|
37
|
+
self.df = {t: len(p) for t, p in self.postings.items()}
|
|
38
|
+
|
|
39
|
+
def search(self, query: str, k: int = 8) -> list[tuple[str, float]]:
|
|
40
|
+
qtf = Counter(t for t in words(query) if t not in STOPWORDS)
|
|
41
|
+
scores: dict[int, float] = {}
|
|
42
|
+
for t, _ in qtf.items():
|
|
43
|
+
postings = self.postings.get(t)
|
|
44
|
+
if not postings:
|
|
45
|
+
continue
|
|
46
|
+
idf = math.log(1 + (self.N - self.df[t] + 0.5) / (self.df[t] + 0.5))
|
|
47
|
+
for i, c in postings:
|
|
48
|
+
dl = self.doc_len[i] or 1
|
|
49
|
+
s = idf * (c * (self.k1 + 1)) / (
|
|
50
|
+
c + self.k1 * (1 - self.b + self.b * dl / self.avgdl))
|
|
51
|
+
scores[i] = scores.get(i, 0.0) + s
|
|
52
|
+
top = sorted(scores.items(), key=lambda kv: -kv[1])[:k]
|
|
53
|
+
return [(self.doc_ids[i], s) for i, s in top]
|
|
54
|
+
|
|
55
|
+
def doc_text(self, doc_id: str) -> str:
|
|
56
|
+
for d in self.docs:
|
|
57
|
+
if d["id"] == doc_id:
|
|
58
|
+
return d["text"]
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bm25_context(index: BM25Index, query: str, k: int = 8) -> str:
|
|
63
|
+
hits = index.search(query, k)
|
|
64
|
+
parts = [index.doc_text(did)[:220] for did, _ in hits]
|
|
65
|
+
return "\n".join(f"- {p}" for p in parts if p)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------- vector-only
|
|
69
|
+
def vector_only_context(memory, query: str, user_id: str, k: int = 8) -> str:
|
|
70
|
+
"""VSA palace search → source chunks. No temporal logic, no
|
|
71
|
+
contradiction chains, no symbolic expansion — neural retrieval only."""
|
|
72
|
+
q_vec = memory.palace.embedder.embed(query)
|
|
73
|
+
scope = {f.id for f in memory.store.query_facts(user_id=user_id,
|
|
74
|
+
active=True)}
|
|
75
|
+
hits = memory.palace.search(q_vec, max(k * 3, 24),
|
|
76
|
+
candidate_ids=scope or None)
|
|
77
|
+
seen_chunks: list[str] = []
|
|
78
|
+
seen_ids = set()
|
|
79
|
+
for fid, score in hits:
|
|
80
|
+
f = memory.store.get_fact(fid)
|
|
81
|
+
if not f or not f.source_id:
|
|
82
|
+
continue
|
|
83
|
+
chunk = memory.store.get_chunk(f.source_id)
|
|
84
|
+
if chunk and chunk["id"] not in seen_ids:
|
|
85
|
+
seen_ids.add(chunk["id"])
|
|
86
|
+
seen_chunks.append(chunk["text"][:240])
|
|
87
|
+
if len(seen_chunks) >= k:
|
|
88
|
+
break
|
|
89
|
+
return "\n".join(f"- {c}" for c in seen_chunks)
|