tensorcode 0.1.0a1__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.
- tensorcode/__init__.py +84 -0
- tensorcode/actions.py +137 -0
- tensorcode/answer_type.py +222 -0
- tensorcode/awareness.py +344 -0
- tensorcode/backends/__init__.py +0 -0
- tensorcode/backends/builtin.py +167 -0
- tensorcode/backends/hf_local.py +89 -0
- tensorcode/backends/linear.py +133 -0
- tensorcode/backends/neural.py +361 -0
- tensorcode/causal.py +262 -0
- tensorcode/change.py +566 -0
- tensorcode/chunking.py +195 -0
- tensorcode/cognition.py +311 -0
- tensorcode/context.py +97 -0
- tensorcode/control.py +291 -0
- tensorcode/cues.py +192 -0
- tensorcode/expectation.py +270 -0
- tensorcode/frames.py +232 -0
- tensorcode/language/__init__.py +36 -0
- tensorcode/language/chart.py +558 -0
- tensorcode/language/discourse.py +132 -0
- tensorcode/language/domains/__init__.py +0 -0
- tensorcode/language/domains/desktop.py +552 -0
- tensorcode/language/english.py +459 -0
- tensorcode/language/features.py +112 -0
- tensorcode/language/generate.py +574 -0
- tensorcode/language/grammar.py +893 -0
- tensorcode/language/semantics.py +349 -0
- tensorcode/learning/__init__.py +30 -0
- tensorcode/learning/certificate.py +148 -0
- tensorcode/learning/induce.py +304 -0
- tensorcode/learning/library.py +217 -0
- tensorcode/learning/literals.py +126 -0
- tensorcode/learning/verify.py +253 -0
- tensorcode/memory.py +303 -0
- tensorcode/metacognition.py +351 -0
- tensorcode/ops.py +207 -0
- tensorcode/outcomes.py +99 -0
- tensorcode/permanence.py +376 -0
- tensorcode/priming.py +191 -0
- tensorcode/py.typed +0 -0
- tensorcode/quantity.py +311 -0
- tensorcode/records.py +728 -0
- tensorcode/relation.py +771 -0
- tensorcode/runtime.py +471 -0
- tensorcode/semantics_bridge.py +308 -0
- tensorcode/social.py +380 -0
- tensorcode/temporal.py +189 -0
- tensorcode/wants.py +185 -0
- tensorcode-0.1.0a1.dist-info/METADATA +196 -0
- tensorcode-0.1.0a1.dist-info/RECORD +53 -0
- tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
- tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Whether an induced artifact may be adopted, and the controls that say what it means.
|
|
2
|
+
|
|
3
|
+
A fit is not a finding. Both source repos record the same lesson from opposite
|
|
4
|
+
directions: ``typed-crystallization-networks`` found a mined abstraction helping
|
|
5
|
+
exactly as much as a *wrong* abstraction of the same size (research §44), and
|
|
6
|
+
``symbolic-ai-models`` states it as a rule — "same-cardinality-but-meaningless is
|
|
7
|
+
this repo's most productive instrument … it corrected four separate published
|
|
8
|
+
results" (``symbolic_ai_lean/gate.py``). So an artifact is measured against four
|
|
9
|
+
controls, in the shape that file pre-registered:
|
|
10
|
+
|
|
11
|
+
``held_out``
|
|
12
|
+
Accuracy on cases the inducer never saw. A rule admitted on its training fit
|
|
13
|
+
alone is the failure mode TCN's §65 records.
|
|
14
|
+
``random``
|
|
15
|
+
The same artifact with its decisions shuffled among the same labels: a
|
|
16
|
+
same-cardinality, meaningless competitor. Beating it is the minimum.
|
|
17
|
+
``shifted``
|
|
18
|
+
The artifact applied to the *wrong question* — cases relabelled from a
|
|
19
|
+
different pool. Its accuracy must fall to the random level; if it does not,
|
|
20
|
+
the artifact is reading something other than the question.
|
|
21
|
+
``renamed``
|
|
22
|
+
Every symbol consistently renamed. The verdicts must be **identical**. An
|
|
23
|
+
artifact that moves under renaming is reading vocabulary, which is how
|
|
24
|
+
``ensemble-001``'s headline result died (0.467 → 0.038).
|
|
25
|
+
|
|
26
|
+
Adoption also requires a **simpler competitor** to lose: a one-condition rule, or
|
|
27
|
+
the majority label. Nothing is adopted silently, and nothing is rejected silently
|
|
28
|
+
either — :class:`Verification` records the numbers and the reason.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import random
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
36
|
+
|
|
37
|
+
from ..outcomes import Verdict
|
|
38
|
+
from .induce import DecisionList, Rule, decision_list
|
|
39
|
+
from .literals import Case, Literal, rename_facts, rename_map
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Verification:
|
|
44
|
+
"""What the controls said, and whether that is enough to adopt."""
|
|
45
|
+
|
|
46
|
+
held_out: float
|
|
47
|
+
train: float
|
|
48
|
+
random: float
|
|
49
|
+
shifted: float
|
|
50
|
+
renamed_identical: bool
|
|
51
|
+
floor: float # the best simple competitor (majority label, or a one-condition rule)
|
|
52
|
+
reasons: tuple[str, ...] = ()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def verdict(self) -> Verdict:
|
|
56
|
+
return Verdict("holds" if not self.reasons else "fails", self.reasons)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def adopted(self) -> bool:
|
|
60
|
+
return not self.reasons
|
|
61
|
+
|
|
62
|
+
def report(self) -> str:
|
|
63
|
+
rows = [
|
|
64
|
+
f"held-out {self.held_out:.3f}",
|
|
65
|
+
f"train {self.train:.3f}",
|
|
66
|
+
f"floor {self.floor:.3f} (majority or one condition)",
|
|
67
|
+
f"random {self.random:.3f} (same labels, shuffled)",
|
|
68
|
+
f"shifted {self.shifted:.3f} (wrong question; should fall to random)",
|
|
69
|
+
f"renamed {'identical' if self.renamed_identical else 'CHANGED — reading vocabulary'}",
|
|
70
|
+
f"verdict {'adopt' if self.adopted else 'reject: ' + '; '.join(self.reasons)}",
|
|
71
|
+
]
|
|
72
|
+
return "\n".join(rows)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def verify_decision_list(
|
|
76
|
+
artifact: DecisionList,
|
|
77
|
+
*,
|
|
78
|
+
train: Sequence[Case],
|
|
79
|
+
held_out: Sequence[Case],
|
|
80
|
+
literals: Sequence[Literal],
|
|
81
|
+
shifted: Sequence[Case] = (),
|
|
82
|
+
margin: float = 0.02,
|
|
83
|
+
seed: int = 0,
|
|
84
|
+
) -> Verification:
|
|
85
|
+
"""Run the four controls plus the simpler-competitor floor over an induced list."""
|
|
86
|
+
reasons: list[str] = []
|
|
87
|
+
train_acc = artifact.accuracy(train)
|
|
88
|
+
held = artifact.accuracy(held_out)
|
|
89
|
+
|
|
90
|
+
# floor: the majority label, and the best single-condition rule
|
|
91
|
+
labels = [label for _, label in train]
|
|
92
|
+
majority = max(set(labels), key=labels.count) if labels else None
|
|
93
|
+
floor = sum(label == majority for _, label in held_out) / max(1, len(held_out))
|
|
94
|
+
# the "one-line hand rule" competitor: a single condition plus a default. TCN's
|
|
95
|
+
# findings record learned priors being matched by exactly this.
|
|
96
|
+
one = decision_list(train, literals, max_conditions=1, mdl=False, max_rules=1)
|
|
97
|
+
floor = max(floor, one.accuracy(held_out))
|
|
98
|
+
|
|
99
|
+
# random: the same rules with their labels shuffled among the same multiset. One
|
|
100
|
+
# shuffle can come back as the identity, so this is the mean over several — the
|
|
101
|
+
# control is an *expected* accuracy, not a single draw.
|
|
102
|
+
rng = random.Random(seed)
|
|
103
|
+
original = [r.label for r in artifact.rules]
|
|
104
|
+
draws: list[float] = []
|
|
105
|
+
for _ in range(20 if len(original) > 1 else 0):
|
|
106
|
+
shuffled = list(original)
|
|
107
|
+
rng.shuffle(shuffled)
|
|
108
|
+
if shuffled == original:
|
|
109
|
+
continue
|
|
110
|
+
scrambled = DecisionList([Rule(r.conditions, label, r.support, r.correct)
|
|
111
|
+
for r, label in zip(artifact.rules, shuffled)], artifact.default)
|
|
112
|
+
draws.append(scrambled.accuracy(held_out))
|
|
113
|
+
random_acc = sum(draws) / len(draws) if draws else floor
|
|
114
|
+
|
|
115
|
+
# shifted: the right artifact, the wrong question
|
|
116
|
+
shifted_acc = artifact.accuracy(shifted) if shifted else random_acc
|
|
117
|
+
|
|
118
|
+
# renamed: the *same* artifact, run on consistently renamed cases. Renaming the
|
|
119
|
+
# artifact too would hide exactly the failure this control exists to catch — an
|
|
120
|
+
# artifact that is reading vocabulary rather than structure.
|
|
121
|
+
mapping = rename_map(list(train) + list(held_out))
|
|
122
|
+
identical = all(artifact.predict(rename_facts(facts, mapping)) == artifact.predict(facts)
|
|
123
|
+
for facts, _ in held_out)
|
|
124
|
+
|
|
125
|
+
if held < floor + margin:
|
|
126
|
+
reasons.append(f"held-out {held:.3f} does not beat the simple floor {floor:.3f}")
|
|
127
|
+
if held < random_acc + margin:
|
|
128
|
+
reasons.append(f"held-out {held:.3f} does not beat the same-size random control {random_acc:.3f}")
|
|
129
|
+
if shifted and shifted_acc > random_acc + 0.1:
|
|
130
|
+
reasons.append(f"scores {shifted_acc:.3f} on the wrong question: it is not reading the question")
|
|
131
|
+
if not identical:
|
|
132
|
+
reasons.append("verdicts change under renaming: it is reading vocabulary, not structure")
|
|
133
|
+
return Verification(held, train_acc, random_acc, shifted_acc, identical, floor, tuple(reasons))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------ concept adoption
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class Concept:
|
|
141
|
+
"""A proposed predicate: a name, a definition over existing claims, and its evidence."""
|
|
142
|
+
|
|
143
|
+
name: str
|
|
144
|
+
definition: tuple[Literal, ...]
|
|
145
|
+
support: int
|
|
146
|
+
functional: bool = False
|
|
147
|
+
|
|
148
|
+
def holds(self, facts: Any) -> bool:
|
|
149
|
+
return all(c.holds(facts) for c in self.definition)
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
return f"{self.name} ⇔ " + " ∧ ".join(repr(c) for c in self.definition)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class ConceptCheck:
|
|
157
|
+
"""Whether a proposed concept may join the vocabulary."""
|
|
158
|
+
|
|
159
|
+
concept: Concept
|
|
160
|
+
covers: int
|
|
161
|
+
round_trips: bool
|
|
162
|
+
renamed_identical: bool
|
|
163
|
+
conflicts: tuple[str, ...]
|
|
164
|
+
reasons: tuple[str, ...]
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def adopted(self) -> bool:
|
|
168
|
+
return not self.reasons
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_concept(concept: Concept, *, positives: Sequence[frozenset], negatives: Sequence[frozenset],
|
|
172
|
+
functional_values: Callable[[frozenset], Any] | None = None,
|
|
173
|
+
min_support: int = 3) -> ConceptCheck:
|
|
174
|
+
"""A concept is adopted only if it separates, round-trips, and survives renaming.
|
|
175
|
+
|
|
176
|
+
* it must hold of the positives and not of the negatives (it has content);
|
|
177
|
+
* **round trip**: re-describing a case with the concept must not lose what the
|
|
178
|
+
concept was defined from, so the original claims can still be derived;
|
|
179
|
+
* renaming symbols must not change which cases it covers;
|
|
180
|
+
* a functional predicate must not give one subject two values.
|
|
181
|
+
"""
|
|
182
|
+
reasons: list[str] = []
|
|
183
|
+
covers = sum(concept.holds(facts) for facts in positives)
|
|
184
|
+
leaks = sum(concept.holds(facts) for facts in negatives)
|
|
185
|
+
if covers < min_support:
|
|
186
|
+
reasons.append(f"covers only {covers} cases (needs {min_support})")
|
|
187
|
+
if leaks:
|
|
188
|
+
reasons.append(f"also holds of {leaks} negative cases: it does not separate them")
|
|
189
|
+
|
|
190
|
+
# round trip: the definition's own predicates must still be present in the case
|
|
191
|
+
round_trips = all(all(c.holds(facts) for c in concept.definition) for facts in positives if concept.holds(facts))
|
|
192
|
+
if not round_trips:
|
|
193
|
+
reasons.append("re-describing loses the claims it was defined from")
|
|
194
|
+
|
|
195
|
+
mapping = rename_map([(facts, None) for facts in list(positives) + list(negatives)])
|
|
196
|
+
renamed_positives = [rename_facts(facts, mapping) for facts in positives]
|
|
197
|
+
identical = [concept.holds(f) for f in positives] == [concept.holds(f) for f in renamed_positives]
|
|
198
|
+
if not identical:
|
|
199
|
+
reasons.append("coverage changes under renaming")
|
|
200
|
+
|
|
201
|
+
conflicts: list[str] = []
|
|
202
|
+
if concept.functional and functional_values is not None:
|
|
203
|
+
seen: dict[Any, Any] = {}
|
|
204
|
+
for facts in positives:
|
|
205
|
+
if not concept.holds(facts):
|
|
206
|
+
continue
|
|
207
|
+
key = tuple(sorted(facts, key=repr))
|
|
208
|
+
value = functional_values(facts)
|
|
209
|
+
if key in seen and seen[key] != value:
|
|
210
|
+
conflicts.append(f"two values for one subject: {seen[key]!r} and {value!r}")
|
|
211
|
+
seen[key] = value
|
|
212
|
+
if conflicts:
|
|
213
|
+
reasons.append("contradiction on a functional predicate")
|
|
214
|
+
return ConceptCheck(concept, covers, round_trips, identical, tuple(conflicts), tuple(reasons))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def propose_concepts(cases: Sequence[Case], literals: Sequence[Literal], *, label: Any,
|
|
218
|
+
max_conditions: int = 2, top: int = 3) -> list[Concept]:
|
|
219
|
+
"""Candidate definitions for "what these cases have in common", purest first.
|
|
220
|
+
|
|
221
|
+
Grouping is by *behaviour* — which cases a definition covers — not by syntax, so
|
|
222
|
+
two spellings of one concept collapse into a single candidate. TCN's research
|
|
223
|
+
§46 found one real concept arriving as 8–12 syntactically different copies.
|
|
224
|
+
"""
|
|
225
|
+
positives = [facts for facts, y in cases if y == label]
|
|
226
|
+
negatives = [facts for facts, y in cases if y != label]
|
|
227
|
+
if not positives:
|
|
228
|
+
return []
|
|
229
|
+
scored: dict[tuple, tuple[float, Concept]] = {}
|
|
230
|
+
for size in range(1, max_conditions + 1):
|
|
231
|
+
for combo in _combinations(literals, size):
|
|
232
|
+
covered = tuple(i for i, facts in enumerate(positives) if all(c.holds(facts) for c in combo))
|
|
233
|
+
if not covered:
|
|
234
|
+
continue
|
|
235
|
+
leaks = sum(all(c.holds(facts) for c in combo) for facts in negatives)
|
|
236
|
+
purity = len(covered) / (len(covered) + leaks)
|
|
237
|
+
concept = Concept(f"{label}_like", tuple(combo), len(covered))
|
|
238
|
+
best = scored.get(covered)
|
|
239
|
+
cost = sum(c.cost() for c in combo)
|
|
240
|
+
if best is None or (purity, -cost) > (best[0], -sum(c.cost() for c in best[1].definition)):
|
|
241
|
+
scored[covered] = (purity, concept)
|
|
242
|
+
ranked = sorted(scored.values(), key=lambda row: (-row[0], -row[1].support))
|
|
243
|
+
return [concept for _, concept in ranked[:top]]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _combinations(items: Sequence[Literal], size: int) -> Any:
|
|
247
|
+
if size == 1:
|
|
248
|
+
for item in items:
|
|
249
|
+
yield (item,)
|
|
250
|
+
return
|
|
251
|
+
for i, first in enumerate(items):
|
|
252
|
+
for rest in _combinations(items[i + 1:], size - 1):
|
|
253
|
+
yield (first, *rest)
|
tensorcode/memory.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Memory as several stores with different dynamics over one claim substrate.
|
|
2
|
+
|
|
3
|
+
working the awareness buffer: small, fast to decay, what thinking runs over
|
|
4
|
+
episodic time-indexed traces of what happened, encoded by salience, decaying
|
|
5
|
+
semantic generalizations, including what someone simply told us
|
|
6
|
+
spatial the same claims reached by where they are (``frames.Frames``)
|
|
7
|
+
procedural skills, recalled by how well a cue fits them
|
|
8
|
+
associative recall by cue similarity, not by exact pattern match
|
|
9
|
+
|
|
10
|
+
They are not separate databases. An episode is an entity with claims pointing at the claims
|
|
11
|
+
it contains, a generalization is a claim in the semantic scope, and forgetting is
|
|
12
|
+
``Store.forget``. What differs is the dynamics: episodic memory decays and consolidates,
|
|
13
|
+
semantic memory does not, and working memory holds only what is aware.
|
|
14
|
+
|
|
15
|
+
Forgetting is real and measured. Nothing is evicted while something still rests on it, and
|
|
16
|
+
told facts and generalizations are protected, so what goes is stale perception.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import time as _time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime, timedelta, timezone
|
|
24
|
+
from typing import Any, Iterable, Sequence
|
|
25
|
+
|
|
26
|
+
from .awareness import Awareness, AwarenessPolicy
|
|
27
|
+
from .cognition import Thought
|
|
28
|
+
from .context import shingle_similarity
|
|
29
|
+
from .outcomes import Score
|
|
30
|
+
from .records import Claim, ClaimRecord, Evidence, Ref, Store
|
|
31
|
+
|
|
32
|
+
SEMANTIC = Ref("scope:semantic")
|
|
33
|
+
EPISODIC = Ref("scope:episodic")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _now() -> datetime:
|
|
37
|
+
return datetime.now(timezone.utc)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class MemoryPolicy:
|
|
42
|
+
"""The dynamics: how much is held, how fast it fades, what is never dropped."""
|
|
43
|
+
|
|
44
|
+
working_budget: int = 64
|
|
45
|
+
episodic_capacity: int = 400 # episodes kept before the least salient are dropped
|
|
46
|
+
half_life: timedelta = timedelta(minutes=30) # perceptual claims older than this may go
|
|
47
|
+
min_salience: float = 0.15 # below this a stale claim is eligible to be forgotten
|
|
48
|
+
consolidate_after: int = 3 # how many episodes must agree before it becomes semantic
|
|
49
|
+
protect_scopes: tuple[Ref | None, ...] = (SEMANTIC,)
|
|
50
|
+
protect_predicates: frozenset[str] = frozenset({"said", "told", "name"})
|
|
51
|
+
# Testimony is not perception, and the half-life above is a perceptual one. A fact someone told
|
|
52
|
+
# you does not become doubtful because you have not looked at it lately, and it cannot be
|
|
53
|
+
# protected by predicate name: the predicate of a told fact is whatever word the teller used
|
|
54
|
+
# ("my cat is Mackerel" files `cat`), so a list of protected spellings can never contain it.
|
|
55
|
+
# Provenance can: claims whose evidence comes from an utterance are held on those grounds.
|
|
56
|
+
#
|
|
57
|
+
# Either party's utterance. Protecting only what the *user* said looks right and is the same
|
|
58
|
+
# mistake one level down: the record of having said something oneself is an utterance too, and
|
|
59
|
+
# dropping it leaves an assistant that still knows your name and has forgotten that it already
|
|
60
|
+
# told you it does. This list is a convention about source spellings, not the principle — a
|
|
61
|
+
# caller whose utterances are named differently has to extend it.
|
|
62
|
+
protect_sources: tuple[str, ...] = ("utterance:", "person:", "user:", "reply:", "said:")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class TurnReport:
|
|
67
|
+
"""What one turn of memory dynamics did, so the dynamics can be watched rather than trusted."""
|
|
68
|
+
|
|
69
|
+
episode: "Episode | None" = None
|
|
70
|
+
consolidated: int = 0
|
|
71
|
+
forgotten: "ForgetReport | None" = None
|
|
72
|
+
ms: float = 0.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class Episode:
|
|
77
|
+
"""One remembered happening: when, how much it mattered, and what it was made of."""
|
|
78
|
+
|
|
79
|
+
ref: Ref
|
|
80
|
+
at: datetime
|
|
81
|
+
salience: float
|
|
82
|
+
summary: str
|
|
83
|
+
claims: tuple[str, ...]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class Recollection:
|
|
88
|
+
"""What recall returned, and why each item came back."""
|
|
89
|
+
|
|
90
|
+
episodes: tuple[tuple[Episode, Score], ...] = ()
|
|
91
|
+
claims: tuple[tuple[ClaimRecord, Score], ...] = ()
|
|
92
|
+
cue: str = ""
|
|
93
|
+
|
|
94
|
+
def why(self) -> list[str]:
|
|
95
|
+
lines = [f"cue {self.cue!r}"]
|
|
96
|
+
lines += [f" episode {e.ref} ({e.summary!r}) similarity {s.value:.2f}" for e, s in self.episodes]
|
|
97
|
+
lines += [f" claim {r.claim.subject} {r.claim.predicate} {getattr(r.claim.object, 'value', r.claim.object)!r} similarity {s.value:.2f}"
|
|
98
|
+
for r, s in self.claims]
|
|
99
|
+
return lines
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ForgetReport:
|
|
104
|
+
"""What forgetting actually did, so it can be measured rather than assumed."""
|
|
105
|
+
|
|
106
|
+
claims_forgotten: int = 0
|
|
107
|
+
episodes_dropped: int = 0
|
|
108
|
+
kept_because_depended_on: int = 0
|
|
109
|
+
kept_because_protected: int = 0
|
|
110
|
+
kept_because_testimony: int = 0 # someone said so: not perception, so not on a perceptual clock
|
|
111
|
+
ms: float = 0.0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Memory:
|
|
115
|
+
"""The several memories of one mind."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, mind: Store, policy: MemoryPolicy | None = None, *,
|
|
118
|
+
awareness: Awareness | None = None, frames: Any = None) -> None:
|
|
119
|
+
self.mind = mind
|
|
120
|
+
self.policy = policy or MemoryPolicy()
|
|
121
|
+
self.frames = frames
|
|
122
|
+
self.working = awareness or Awareness(mind, AwarenessPolicy(budget=self.policy.working_budget),
|
|
123
|
+
extra_links=frames.links if frames is not None else None)
|
|
124
|
+
self._episodes: dict[Ref, Episode] = {}
|
|
125
|
+
self._counter = 0
|
|
126
|
+
self._turns = 0
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------- episodic
|
|
129
|
+
|
|
130
|
+
def encode(self, what: Thought | Sequence[ClaimRecord], *, summary: str = "", salience: float | None = None,
|
|
131
|
+
at: datetime | None = None) -> Episode:
|
|
132
|
+
"""Lay down an episode. Salience defaults to how aware its claims were."""
|
|
133
|
+
records = list(what.added) if isinstance(what, Thought) else list(what)
|
|
134
|
+
when = at or _now()
|
|
135
|
+
self._counter += 1
|
|
136
|
+
ref = Ref(f"episode:{self._counter}")
|
|
137
|
+
if salience is None:
|
|
138
|
+
saliences = [self.working.salience(r.id) for r in records]
|
|
139
|
+
salience = max(saliences) if saliences else 0.2
|
|
140
|
+
text = summary or "; ".join(_describe(r) for r in records[:4])
|
|
141
|
+
episode = Episode(ref, when, round(float(salience), 4), text, tuple(sorted(r.id for r in records)))
|
|
142
|
+
self._episodes[ref] = episode
|
|
143
|
+
source = Ref("memory:encode")
|
|
144
|
+
edits = [Claim(ref, "at", when, scope=EPISODIC), Claim(ref, "salience", episode.salience, scope=EPISODIC),
|
|
145
|
+
Claim(ref, "summary", episode.summary, scope=EPISODIC)]
|
|
146
|
+
edits += [Claim(ref, "contains", cid, scope=EPISODIC) for cid in episode.claims]
|
|
147
|
+
for claim in edits:
|
|
148
|
+
self.mind.tell(claim, Evidence(source, when, method="encode-episode"))
|
|
149
|
+
return episode
|
|
150
|
+
|
|
151
|
+
def episodes(self, *, since: datetime | None = None, min_salience: float = 0.0) -> list[Episode]:
|
|
152
|
+
rows = [e for e in self._episodes.values() if e.salience >= min_salience and (since is None or e.at >= since)]
|
|
153
|
+
return sorted(rows, key=lambda e: (e.at, e.ref.id))
|
|
154
|
+
|
|
155
|
+
def recall(self, cue: str, k: int = 3, *, min_similarity: float = 0.05) -> Recollection:
|
|
156
|
+
"""Associative recall: episodes and claims whose text is nearest the cue."""
|
|
157
|
+
scored_e = []
|
|
158
|
+
for episode in self._episodes.values():
|
|
159
|
+
score = shingle_similarity(cue, f"{episode.summary}", n=2)
|
|
160
|
+
if score.value >= min_similarity:
|
|
161
|
+
scored_e.append((episode, score))
|
|
162
|
+
scored_e.sort(key=lambda pair: (-pair[1].value, -pair[0].salience, pair[0].ref.id))
|
|
163
|
+
scored_c = []
|
|
164
|
+
for rec in self.mind.claims():
|
|
165
|
+
if rec.claim.scope == EPISODIC:
|
|
166
|
+
continue
|
|
167
|
+
score = shingle_similarity(cue, _describe(rec), n=2)
|
|
168
|
+
if score.value >= min_similarity:
|
|
169
|
+
scored_c.append((rec, score))
|
|
170
|
+
scored_c.sort(key=lambda pair: (-pair[1].value, pair[0].id))
|
|
171
|
+
return Recollection(tuple(scored_e[:k]), tuple(scored_c[:k]), cue)
|
|
172
|
+
|
|
173
|
+
def consolidate(self) -> list[Claim]:
|
|
174
|
+
"""What several episodes agree on becomes semantic: a generalization citing its episodes."""
|
|
175
|
+
counts: dict[tuple[Ref, str, str], list[Ref]] = {}
|
|
176
|
+
for episode in self.episodes():
|
|
177
|
+
for cid in episode.claims:
|
|
178
|
+
rec = self.mind._claims.get(cid)
|
|
179
|
+
if rec is None or rec.retracted or rec.claim.scope == EPISODIC:
|
|
180
|
+
continue
|
|
181
|
+
key = (rec.claim.subject, rec.claim.predicate, repr(rec.claim.object))
|
|
182
|
+
counts.setdefault(key, []).append(episode.ref)
|
|
183
|
+
made = []
|
|
184
|
+
for (subject, predicate, _), refs in sorted(counts.items(), key=lambda kv: (kv[0][0].id, kv[0][1], kv[0][2])):
|
|
185
|
+
if len(refs) < self.policy.consolidate_after:
|
|
186
|
+
continue
|
|
187
|
+
source_claim = next(r for r in self.mind.claims(subject=subject, predicate=predicate))
|
|
188
|
+
claim = Claim(subject, predicate, source_claim.claim.object, scope=SEMANTIC)
|
|
189
|
+
if self.mind.claims(subject=subject, predicate=predicate, scope=SEMANTIC):
|
|
190
|
+
continue
|
|
191
|
+
self.mind.tell(claim, Evidence(Ref("memory:consolidate"), _now(), method="consolidate",
|
|
192
|
+
confidence=Score(min(1.0, len(refs) / (self.policy.consolidate_after * 2)), "uncalibrated"),
|
|
193
|
+
derived_from=tuple(sorted(dict.fromkeys(cid for r in refs for cid in self._episodes[r].claims
|
|
194
|
+
if self.mind._claims.get(cid) and
|
|
195
|
+
self.mind._claims[cid].claim.subject == subject and
|
|
196
|
+
self.mind._claims[cid].claim.predicate == predicate)))))
|
|
197
|
+
made.append(claim)
|
|
198
|
+
return made
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------- semantic
|
|
201
|
+
|
|
202
|
+
def told(self, subject: Ref, predicate: str, object: Any, *, by: str = "user", at: datetime | None = None) -> ClaimRecord:
|
|
203
|
+
"""Someone simply said so. Semantic, protected from forgetting, and provably hearsay."""
|
|
204
|
+
claim = Claim(subject, predicate, object, scope=SEMANTIC)
|
|
205
|
+
return self.mind.tell(claim, Evidence(Ref(f"said:{by}"), at or _now(), method="told"))
|
|
206
|
+
|
|
207
|
+
def semantic(self, subject: Ref | None = None, predicate: str | None = None) -> list[ClaimRecord]:
|
|
208
|
+
return self.mind.claims(subject=subject, predicate=predicate, scope=SEMANTIC)
|
|
209
|
+
|
|
210
|
+
# ----------------------------------------------------------------- spatial
|
|
211
|
+
|
|
212
|
+
def here(self, **where: Any) -> list[ClaimRecord]:
|
|
213
|
+
"""What is at a place (see ``frames.Frames.spatial``)."""
|
|
214
|
+
if self.frames is None:
|
|
215
|
+
return []
|
|
216
|
+
return self.frames.spatial(**where)
|
|
217
|
+
|
|
218
|
+
# -------------------------------------------------------------- procedural
|
|
219
|
+
|
|
220
|
+
def skills(self, cue: str, procedures: Iterable[Any], k: int = 3) -> list[tuple[Any, Score]]:
|
|
221
|
+
"""Rank procedures by how well a cue fits their name and what they are for."""
|
|
222
|
+
scored = []
|
|
223
|
+
for proc in procedures:
|
|
224
|
+
text = " ".join(str(getattr(proc, attr, "") or "") for attr in ("id", "act", "why", "summary")).replace("_", " ")
|
|
225
|
+
scored.append((proc, shingle_similarity(cue, text, n=2)))
|
|
226
|
+
scored.sort(key=lambda pair: (-pair[1].value, str(getattr(pair[0], "id", ""))))
|
|
227
|
+
return scored[:k]
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------- forgetting
|
|
230
|
+
|
|
231
|
+
def turn(self, records: Sequence[ClaimRecord], *, summary: str = "", consolidate_every: int = 5,
|
|
232
|
+
forget_every: int = 5, keep_scopes: tuple[Ref | None, ...] = (), now: datetime | None = None) -> TurnReport:
|
|
233
|
+
"""One conversation turn's worth of dynamics: encode, and now and then consolidate and forget.
|
|
234
|
+
|
|
235
|
+
Encoding every turn and consolidating every turn are different costs: laying down an episode
|
|
236
|
+
is O(what just happened), while consolidation and forgetting sweep everything held. Doing the
|
|
237
|
+
sweeps on a stride is what keeps per-turn latency flat as a conversation grows long.
|
|
238
|
+
|
|
239
|
+
The caller decides what belongs in an episode. Raw perception does not: a retina's worth of
|
|
240
|
+
claims per turn would bury the few that record what actually happened, and the point of an
|
|
241
|
+
episode is to be re-findable later.
|
|
242
|
+
"""
|
|
243
|
+
t0 = _time.perf_counter()
|
|
244
|
+
self._turns += 1
|
|
245
|
+
episode = self.encode(records, summary=summary, at=now) if records else None
|
|
246
|
+
consolidated = len(self.consolidate()) if self._turns % max(1, consolidate_every) == 0 else 0
|
|
247
|
+
forgotten = (self.forget_stale(now=now, keep_scopes=keep_scopes)
|
|
248
|
+
if self._turns % max(1, forget_every) == 0 else None)
|
|
249
|
+
return TurnReport(episode, consolidated, forgotten, (_time.perf_counter() - t0) * 1e3)
|
|
250
|
+
|
|
251
|
+
def forget_stale(self, *, now: datetime | None = None, keep_scopes: tuple[Ref | None, ...] = ()) -> ForgetReport:
|
|
252
|
+
"""Drop stale, unsalient perception. Nothing that supports something else, nothing protected."""
|
|
253
|
+
t0 = _time.perf_counter()
|
|
254
|
+
when = now or _now()
|
|
255
|
+
report = ForgetReport()
|
|
256
|
+
protected_scopes = set(self.policy.protect_scopes) | set(keep_scopes) | {EPISODIC}
|
|
257
|
+
aware = self.working.aware_ids()
|
|
258
|
+
doomed = []
|
|
259
|
+
for cid, rec in sorted(self.mind._claims.items()):
|
|
260
|
+
if rec.retracted:
|
|
261
|
+
continue
|
|
262
|
+
if rec.claim.scope in protected_scopes or rec.claim.predicate in self.policy.protect_predicates:
|
|
263
|
+
report.kept_because_protected += 1
|
|
264
|
+
continue
|
|
265
|
+
if any(e.source.id.startswith(prefix) for e in rec.evidence for prefix in self.policy.protect_sources):
|
|
266
|
+
report.kept_because_testimony += 1
|
|
267
|
+
continue
|
|
268
|
+
newest = max((e.observed_at for e in rec.evidence), default=when)
|
|
269
|
+
age = when - newest
|
|
270
|
+
salience = self.working.salience(cid) if cid in aware else 0.0
|
|
271
|
+
if age < self.policy.half_life or salience >= self.policy.min_salience:
|
|
272
|
+
continue
|
|
273
|
+
if self.mind._dependents.get(cid):
|
|
274
|
+
report.kept_because_depended_on += 1
|
|
275
|
+
continue
|
|
276
|
+
doomed.append(cid)
|
|
277
|
+
self.mind.forget(doomed)
|
|
278
|
+
report.claims_forgotten = len(doomed)
|
|
279
|
+
for episode in self.episodes():
|
|
280
|
+
if len(self._episodes) <= self.policy.episodic_capacity:
|
|
281
|
+
break
|
|
282
|
+
if episode.salience < self.policy.min_salience:
|
|
283
|
+
self._episodes.pop(episode.ref, None)
|
|
284
|
+
self.mind.forget([r.id for r in self.mind.claims(subject=episode.ref)])
|
|
285
|
+
report.episodes_dropped += 1
|
|
286
|
+
report.ms = (_time.perf_counter() - t0) * 1e3
|
|
287
|
+
return report
|
|
288
|
+
|
|
289
|
+
# --------------------------------------------------------------- cycle
|
|
290
|
+
|
|
291
|
+
def attend(self, seeds: Sequence[Any], *, fade: bool = True) -> Awareness:
|
|
292
|
+
"""One cycle of working memory: fade what was aware, seed from what just arrived, spread."""
|
|
293
|
+
if fade:
|
|
294
|
+
self.working.fade()
|
|
295
|
+
self.working.seed(list(seeds))
|
|
296
|
+
self.working.spread()
|
|
297
|
+
return self.working
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _describe(rec: ClaimRecord) -> str:
|
|
301
|
+
c = rec.claim
|
|
302
|
+
obj = getattr(c.object, "value", c.object)
|
|
303
|
+
return f"{c.subject.id.split(':', 1)[-1]} {c.predicate} {obj}"
|