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
tensorcode/chunking.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Automatization: a sequence done the same way often enough stops being deliberated.
|
|
2
|
+
|
|
3
|
+
The deliberate-to-automatic transition is the most reliable finding in skill learning, and a
|
|
4
|
+
procedure interpreter has none of it: a learned skill walks its steps, evaluates every guard and
|
|
5
|
+
records every decision, on the thousandth run exactly as on the first. That is not carefulness,
|
|
6
|
+
it is an inability to learn *how* it does something as opposed to *that* it works.
|
|
7
|
+
|
|
8
|
+
A chunk here is the compiled form of a sequence that has run identically :attr:`Chunks.repeats`
|
|
9
|
+
times: the steps actually taken, the guards whose outcome was the same every time, and nothing
|
|
10
|
+
else. Running a chunk skips the deliberation, not the acts — a click is still a click. What it
|
|
11
|
+
skips is evaluating guards whose answer has never varied and recording a decision per step.
|
|
12
|
+
|
|
13
|
+
Automatization trades adaptability for speed, so the trade is made explicit:
|
|
14
|
+
|
|
15
|
+
* the expanded form is never discarded; a chunk is an *index* into it;
|
|
16
|
+
* a chunk is retired the moment a step's outcome diverges from what the recorded runs saw
|
|
17
|
+
(:func:`divergent`), and the walk continues expanded from that point — the fallback is the
|
|
18
|
+
whole reason this is safe;
|
|
19
|
+
* every chunk carries how often it was used and how often it fell back, so a chunk that keeps
|
|
20
|
+
breaking is visible rather than quietly wrong.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Iterable, Mapping, Sequence
|
|
29
|
+
|
|
30
|
+
#: bindings that mean a body step did not do what the recorded runs saw it do.
|
|
31
|
+
#: Deliberately *not* ``error_summary``: the assistant's own ``output_facts`` always fills that
|
|
32
|
+
#: in, falling back to the first line of a perfectly good output, so reading it as trouble would
|
|
33
|
+
#: retire every chunk on its first use. ``ok`` and ``errors`` are the marks that mean trouble.
|
|
34
|
+
TROUBLE = ("error", "problem", "timed_out", "unavailable")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Trace:
|
|
39
|
+
"""One run of a procedure: which steps ran, which were skipped, and how it ended."""
|
|
40
|
+
|
|
41
|
+
procedure: str
|
|
42
|
+
taken: tuple[int, ...] # program counters whose step executed, in order
|
|
43
|
+
skipped: tuple[int, ...] # program counters whose guard did not hold
|
|
44
|
+
status: str = "done"
|
|
45
|
+
#: (program counter, binding that showed trouble) for the steps that showed any. Some steps
|
|
46
|
+
#: are *meant* to fail — "stat the folder before creating it" expects "no such file" — so a
|
|
47
|
+
#: chunk has to know what normal looked like rather than treating any error as a surprise.
|
|
48
|
+
marks: tuple[tuple[int, str], ...] = ()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def shape(self) -> str:
|
|
52
|
+
"""Identity of the *path* through the procedure, which is what can be compiled."""
|
|
53
|
+
return hashlib.sha256(json.dumps([self.procedure, list(self.taken), sorted(self.skipped),
|
|
54
|
+
sorted(self.marks)]).encode()).hexdigest()[:12]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Chunk:
|
|
59
|
+
"""A compiled path: run these steps in this order, without re-deciding."""
|
|
60
|
+
|
|
61
|
+
procedure: str
|
|
62
|
+
taken: tuple[int, ...]
|
|
63
|
+
skipped: frozenset[int]
|
|
64
|
+
shape: str
|
|
65
|
+
from_runs: int
|
|
66
|
+
expected: Mapping[int, str] = field(default_factory=dict) # pc -> the trouble every run saw there
|
|
67
|
+
uses: int = 0
|
|
68
|
+
fallbacks: int = 0
|
|
69
|
+
retired: bool = False
|
|
70
|
+
retired_because: str = ""
|
|
71
|
+
|
|
72
|
+
def assumes_skipped(self, pc: int) -> bool:
|
|
73
|
+
return pc in self.skipped
|
|
74
|
+
|
|
75
|
+
def expects(self, pc: int) -> str:
|
|
76
|
+
"""The trouble mark every recorded run saw at this step ("" if they all saw none)."""
|
|
77
|
+
return self.expected.get(pc, "")
|
|
78
|
+
|
|
79
|
+
def describe(self) -> str:
|
|
80
|
+
state = "retired" if self.retired else "live"
|
|
81
|
+
return (f"chunk {self.procedure}/{self.shape} ({state}): {len(self.taken)} steps, "
|
|
82
|
+
f"{len(self.skipped)} guards assumed, from {self.from_runs} runs, "
|
|
83
|
+
f"used {self.uses}, fell back {self.fallbacks}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _marked(bindings: Mapping[str, object], name: str):
|
|
87
|
+
"""Values a step bound under ``name``, including under a step's ``as:`` prefix.
|
|
88
|
+
|
|
89
|
+
A step that names its results (``"as": "sib"``) binds ``sib_ok`` and ``sib_errors``, so a
|
|
90
|
+
check that only looked for ``ok`` would never see trouble in exactly the procedures that
|
|
91
|
+
keep several commands' results apart.
|
|
92
|
+
"""
|
|
93
|
+
for key, value in bindings.items():
|
|
94
|
+
if key == name or key.endswith(f"_{name}"):
|
|
95
|
+
yield key, value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mark(bindings: Mapping[str, object]) -> str:
|
|
99
|
+
"""The name of the binding that says this step had trouble, or ``""`` if none did.
|
|
100
|
+
|
|
101
|
+
Deliberately shallow: it reads only the marks the procedures already set. A chunk that skips
|
|
102
|
+
guards cannot notice a subtle difference, which is exactly the known cost of automatization —
|
|
103
|
+
so what is watched is trouble, and the *name* is what is compared, because the text of an
|
|
104
|
+
error varies while its kind does not.
|
|
105
|
+
"""
|
|
106
|
+
for name in TROUBLE:
|
|
107
|
+
for key, value in _marked(bindings, name):
|
|
108
|
+
if isinstance(value, bool) and value:
|
|
109
|
+
return key
|
|
110
|
+
if isinstance(value, str) and value.strip():
|
|
111
|
+
return key
|
|
112
|
+
for key, value in _marked(bindings, "errors"):
|
|
113
|
+
if isinstance(value, (list, tuple)) and value:
|
|
114
|
+
return key
|
|
115
|
+
for key, value in _marked(bindings, "ok"):
|
|
116
|
+
if value is False:
|
|
117
|
+
return key
|
|
118
|
+
return ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def divergent(bindings: Mapping[str, object], expected: str = "") -> str:
|
|
122
|
+
"""Why this step differs from the runs a chunk was compiled from ("" when it does not).
|
|
123
|
+
|
|
124
|
+
Both directions count. Trouble where there was none is the obvious case; *no* trouble where
|
|
125
|
+
every recorded run had some is equally a divergence, because a guard that branched on it —
|
|
126
|
+
and is now being skipped — would have gone the other way.
|
|
127
|
+
"""
|
|
128
|
+
found = mark(bindings)
|
|
129
|
+
if found == expected:
|
|
130
|
+
return ""
|
|
131
|
+
if found and expected:
|
|
132
|
+
return f"{found} where the recorded runs had {expected}"
|
|
133
|
+
if found:
|
|
134
|
+
return f"{found}, which the recorded runs never had"
|
|
135
|
+
return f"no {expected}, which every recorded run had"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class Chunks:
|
|
140
|
+
"""What this mind has automatized, and how well each chunk is holding up."""
|
|
141
|
+
|
|
142
|
+
repeats: int = 3 # identical paths before a chunk is compiled
|
|
143
|
+
enabled: bool = True
|
|
144
|
+
runs: dict[str, list[str]] = field(default_factory=dict) # procedure -> recent path shapes
|
|
145
|
+
traces: dict[str, Trace] = field(default_factory=dict) # shape -> the path it stands for
|
|
146
|
+
compiled: dict[str, Chunk] = field(default_factory=dict) # procedure -> live chunk
|
|
147
|
+
history: list[str] = field(default_factory=list) # compile/retire events, in order
|
|
148
|
+
|
|
149
|
+
def record(self, trace: Trace) -> Chunk | None:
|
|
150
|
+
"""Note how a run went; compile a chunk once the same path has repeated enough."""
|
|
151
|
+
if trace.status != "done":
|
|
152
|
+
self.runs.setdefault(trace.procedure, []).clear()
|
|
153
|
+
return None
|
|
154
|
+
shapes = self.runs.setdefault(trace.procedure, [])
|
|
155
|
+
shapes.append(trace.shape)
|
|
156
|
+
self.traces[trace.shape] = trace
|
|
157
|
+
recent = shapes[-self.repeats:]
|
|
158
|
+
if len(recent) < self.repeats or len(set(recent)) != 1:
|
|
159
|
+
return None
|
|
160
|
+
live = self.compiled.get(trace.procedure)
|
|
161
|
+
if live is not None and live.shape == trace.shape and not live.retired:
|
|
162
|
+
return live
|
|
163
|
+
chunk = Chunk(trace.procedure, trace.taken, frozenset(trace.skipped), trace.shape, self.repeats,
|
|
164
|
+
dict(trace.marks))
|
|
165
|
+
self.compiled[trace.procedure] = chunk
|
|
166
|
+
self.history.append(f"compiled {trace.procedure}/{trace.shape} after {self.repeats} identical runs")
|
|
167
|
+
return chunk
|
|
168
|
+
|
|
169
|
+
def chunk_for(self, procedure: str | None) -> Chunk | None:
|
|
170
|
+
if not self.enabled or procedure is None:
|
|
171
|
+
return None
|
|
172
|
+
chunk = self.compiled.get(procedure)
|
|
173
|
+
return None if chunk is None or chunk.retired else chunk
|
|
174
|
+
|
|
175
|
+
def used(self, chunk: Chunk) -> None:
|
|
176
|
+
chunk.uses += 1
|
|
177
|
+
|
|
178
|
+
def retire(self, chunk: Chunk, why: str) -> None:
|
|
179
|
+
"""Abandon a chunk and fall back to deliberating. The expanded form was never lost."""
|
|
180
|
+
chunk.retired, chunk.retired_because = True, why
|
|
181
|
+
chunk.fallbacks += 1
|
|
182
|
+
self.runs.setdefault(chunk.procedure, []).clear()
|
|
183
|
+
self.history.append(f"retired {chunk.procedure}/{chunk.shape}: {why}")
|
|
184
|
+
|
|
185
|
+
def stats(self) -> dict:
|
|
186
|
+
return {
|
|
187
|
+
"compiled": len([c for c in self.compiled.values() if not c.retired]),
|
|
188
|
+
"retired": len([c for c in self.compiled.values() if c.retired]),
|
|
189
|
+
"uses": sum(c.uses for c in self.compiled.values()),
|
|
190
|
+
"fallbacks": sum(c.fallbacks for c in self.compiled.values()),
|
|
191
|
+
"procedures": {p: c.describe() for p, c in self.compiled.items()},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = ["Chunk", "Chunks", "Trace", "divergent", "mark", "TROUBLE"]
|
tensorcode/cognition.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""A small cognitive substrate over the claim store.
|
|
2
|
+
|
|
3
|
+
perceive / parse -> Fragment (claims extracted from one input, with locators)
|
|
4
|
+
integrate(mind, *fragments) -> Thought (merge into working memory; report what changed)
|
|
5
|
+
think(mind, rules, since=thought) -> Thought (bottom-up rules fire on what changed)
|
|
6
|
+
explain(mind, claim) (why a claim is believed: observations and rules)
|
|
7
|
+
|
|
8
|
+
A ``Fragment`` can be a *snapshot* of a scope (for example, everything currently on
|
|
9
|
+
screen). Claims in that scope that the new snapshot no longer contains are retracted,
|
|
10
|
+
and derivations that depended only on them are withdrawn with them.
|
|
11
|
+
|
|
12
|
+
Perception can be wrong for a frame. An optional ``Corroboration`` policy keeps chosen
|
|
13
|
+
perceived claims *tentative* until they are seen the same way in ``k`` frames (or confirmed
|
|
14
|
+
by an action's outcome); derivations inherit tentativeness from their premises, and a
|
|
15
|
+
tentative claim a later frame contradicts is retracted. Rules and intentions that must not
|
|
16
|
+
act on a single glance ask for the established view.
|
|
17
|
+
|
|
18
|
+
Rules are ordinary Python: patterns select premises, a function derives claims. Every
|
|
19
|
+
derived claim cites its premises, so every thought can be explained. Rules may call
|
|
20
|
+
other operations (``tc.classify``, ``tc.parse``); those calls appear in the trace.
|
|
21
|
+
Nothing here decides what to *do*; that is ``choose`` over intentions.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import time as _time
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from typing import Any, Callable, Iterable, Mapping, Sequence
|
|
30
|
+
|
|
31
|
+
from .outcomes import Score
|
|
32
|
+
from .records import Claim, ClaimRecord, Evidence, Patch, Put, Ref, Retract, Store, Tell
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now() -> datetime:
|
|
36
|
+
return datetime.now(timezone.utc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Fragment:
|
|
41
|
+
"""Claims extracted from one input."""
|
|
42
|
+
|
|
43
|
+
source: Ref # the input these claims came from (a frame, a message, a document)
|
|
44
|
+
claims: tuple[tuple[Claim, str | None], ...] # (claim, locator within the source)
|
|
45
|
+
entities: tuple[tuple[Ref, Any], ...] = () # typed payloads, e.g. geometry for a control
|
|
46
|
+
snapshot_of: Ref | None = None # if set, this fragment is everything currently true in that scope
|
|
47
|
+
method: str = "parse"
|
|
48
|
+
observed_at: datetime = field(default_factory=_now)
|
|
49
|
+
confidence: Mapping[str, Score] = field(default_factory=dict) # claim id -> extractor's score
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Thought:
|
|
54
|
+
"""What changed in the mind. Empty thoughts are normal."""
|
|
55
|
+
|
|
56
|
+
added: tuple[ClaimRecord, ...] = ()
|
|
57
|
+
retracted: tuple[ClaimRecord, ...] = ()
|
|
58
|
+
established: tuple[ClaimRecord, ...] = () # tentative claims that became established (see ``Corroboration``)
|
|
59
|
+
|
|
60
|
+
def __add__(self, other: Thought) -> Thought:
|
|
61
|
+
return Thought(self.added + other.added, self.retracted + other.retracted, self.established + other.established)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def empty(self) -> bool:
|
|
65
|
+
return not self.added and not self.retracted and not self.established
|
|
66
|
+
|
|
67
|
+
def about(self, predicate: str) -> list[Claim]:
|
|
68
|
+
return [r.claim for r in self.added if r.claim.predicate == predicate]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Corroboration:
|
|
73
|
+
"""When a perceived claim may be acted on: after ``k`` consistent frames, or confirmation.
|
|
74
|
+
|
|
75
|
+
* ``predicates``: which perceived predicates this governs (None = all). Others are
|
|
76
|
+
established on sight, which keeps, say, button geometry instant while text readings
|
|
77
|
+
wait for a second look.
|
|
78
|
+
* A frame counts toward ``k`` only if the extractor's confidence (when it gave one) is at
|
|
79
|
+
least ``min_confidence``; below that a claim stays tentative until ``confirm``-ed.
|
|
80
|
+
* Derived claims are established only when every premise of some line of support is.
|
|
81
|
+
* A tentative claim that a later frame contradicts is retracted: in a snapshot scope by
|
|
82
|
+
no longer being perceived; elsewhere by a different object for a functional predicate.
|
|
83
|
+
|
|
84
|
+
State is per mind and per run: create a fresh policy for each episode.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
k: int = 2
|
|
88
|
+
min_confidence: float = 0.0
|
|
89
|
+
predicates: frozenset[str] | None = None
|
|
90
|
+
frames: dict[str, int] = field(default_factory=dict) # claim id -> frames that corroborated it
|
|
91
|
+
confirmed: set[str] = field(default_factory=set)
|
|
92
|
+
|
|
93
|
+
def governs(self, predicate: str) -> bool:
|
|
94
|
+
return self.predicates is None or predicate in self.predicates
|
|
95
|
+
|
|
96
|
+
def established(self, mind: Store, claim_id: str, _seen: frozenset[str] = frozenset()) -> bool:
|
|
97
|
+
rec = mind._claims.get(claim_id)
|
|
98
|
+
if rec is None or rec.retracted or claim_id in _seen:
|
|
99
|
+
return False
|
|
100
|
+
if claim_id in self.confirmed:
|
|
101
|
+
return True
|
|
102
|
+
for e in rec.evidence:
|
|
103
|
+
if e.derived_from:
|
|
104
|
+
if all(self.established(mind, p, _seen | {claim_id}) for p in e.derived_from):
|
|
105
|
+
return True
|
|
106
|
+
elif not self.governs(rec.claim.predicate) or self.frames.get(claim_id, 0) >= self.k:
|
|
107
|
+
return True
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def tentative(self, mind: Store, claim_id: str) -> bool:
|
|
111
|
+
return claim_id in mind._claims and not self.established(mind, claim_id)
|
|
112
|
+
|
|
113
|
+
def confirm(self, mind: Store, claim_id: str) -> Thought:
|
|
114
|
+
"""An action's outcome bore the claim out: establish it (and what now rests on it)."""
|
|
115
|
+
before = self.established(mind, claim_id)
|
|
116
|
+
self.confirmed.add(claim_id)
|
|
117
|
+
return Thought(established=tuple(mind.claim(i) for i in self._closure(mind, [claim_id]))) if not before else Thought()
|
|
118
|
+
|
|
119
|
+
def view(self, mind: Store) -> EstablishedView:
|
|
120
|
+
return EstablishedView(mind, self)
|
|
121
|
+
|
|
122
|
+
def _count(self, claim_id: str, score: Score | None) -> None:
|
|
123
|
+
if score is None or score.value >= self.min_confidence:
|
|
124
|
+
self.frames[claim_id] = self.frames.get(claim_id, 0) + 1
|
|
125
|
+
|
|
126
|
+
def _closure(self, mind: Store, ids: Iterable[str]) -> list[str]:
|
|
127
|
+
"""Claims now established among ``ids`` and the derivations resting on them."""
|
|
128
|
+
out, stack, seen = [], [i for i in ids], set()
|
|
129
|
+
while stack:
|
|
130
|
+
cid = stack.pop()
|
|
131
|
+
if cid in seen:
|
|
132
|
+
continue
|
|
133
|
+
seen.add(cid)
|
|
134
|
+
if self.established(mind, cid):
|
|
135
|
+
out.append(cid)
|
|
136
|
+
stack.extend(sorted(mind._dependents.get(cid, ())))
|
|
137
|
+
return list(dict.fromkeys(out))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class EstablishedView:
|
|
141
|
+
"""A read-only look at a mind that hides tentative claims (everything else delegates)."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, mind: Store, policy: Corroboration) -> None:
|
|
144
|
+
self._mind, self._policy = mind, policy
|
|
145
|
+
|
|
146
|
+
def __getattr__(self, name: str) -> Any:
|
|
147
|
+
return getattr(self._mind, name)
|
|
148
|
+
|
|
149
|
+
def claims(self, *args: Any, **kwargs: Any) -> list[ClaimRecord]:
|
|
150
|
+
return [r for r in self._mind.claims(*args, **kwargs) if self._policy.established(self._mind, r.id)]
|
|
151
|
+
|
|
152
|
+
def match(self, *patterns: Any, with_support: bool = False) -> list:
|
|
153
|
+
found = [(b, s) for b, s in self._mind.match(*patterns, with_support=True) if all(self._policy.established(self._mind, c) for c in s)]
|
|
154
|
+
return found if with_support else [b for b, _ in found]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def integrate(mind: Store, *fragments: Fragment, remember_retracted: bool = False, corroboration: Corroboration | None = None) -> Thought:
|
|
158
|
+
"""Merge fragments into working memory.
|
|
159
|
+
|
|
160
|
+
Snapshot scopes are ephemeral: claims that fall out of a snapshot are retracted and,
|
|
161
|
+
unless ``remember_retracted``, forgotten along with derivations that depended on them.
|
|
162
|
+
With a ``Corroboration`` policy, each fragment is one frame of evidence for its claims
|
|
163
|
+
(see there); ``Thought.established`` lists claims that crossed the threshold.
|
|
164
|
+
"""
|
|
165
|
+
policy = corroboration
|
|
166
|
+
was_tentative = {cid for f in fragments for c, _ in f.claims if (cid := c.id) in mind._claims and policy.tentative(mind, cid)} if policy else set()
|
|
167
|
+
edits: list[Any] = []
|
|
168
|
+
for f in fragments:
|
|
169
|
+
edits += [Put(ref, value) for ref, value in f.entities]
|
|
170
|
+
present = set()
|
|
171
|
+
for claim, locator in f.claims:
|
|
172
|
+
if f.snapshot_of is not None and claim.scope != f.snapshot_of:
|
|
173
|
+
raise ValueError(f"snapshot of {f.snapshot_of} contains a claim in scope {claim.scope}")
|
|
174
|
+
present.add(claim.id)
|
|
175
|
+
live = claim.id in mind._claims and not mind._claims[claim.id].retracted
|
|
176
|
+
if policy is not None:
|
|
177
|
+
policy._count(claim.id, f.confidence.get(claim.id))
|
|
178
|
+
if f.snapshot_of is None and claim.predicate in mind.functional:
|
|
179
|
+
edits += [Retract(r.id, "contradicted by a later observation", (Evidence(f.source, f.observed_at, locator, f.method),))
|
|
180
|
+
for r in mind.claims(claim.subject, claim.predicate, scope=claim.scope) if r.claim.object != claim.object and policy.tentative(mind, r.id)]
|
|
181
|
+
if f.snapshot_of is not None and live:
|
|
182
|
+
continue # still perceived; no need to pile up evidence every frame
|
|
183
|
+
edits.append(Tell(claim, (Evidence(f.source, f.observed_at, locator, f.method, f.confidence.get(claim.id)),)))
|
|
184
|
+
if f.snapshot_of is not None:
|
|
185
|
+
edits += [Retract(rec.id, "no longer perceived", (Evidence(f.source, f.observed_at, method=f.method),)) for rec in mind.claims(scope=f.snapshot_of) if rec.id not in present]
|
|
186
|
+
if not edits:
|
|
187
|
+
return _newly_established(mind, policy, was_tentative, ())
|
|
188
|
+
commit = mind.apply(Patch(tuple(edits), mind.revision))
|
|
189
|
+
thought = Thought(tuple(mind.claim(i) for i in commit.added), tuple(mind.claim(i) for i in commit.retracted))
|
|
190
|
+
thought += _newly_established(mind, policy, was_tentative, commit.added)
|
|
191
|
+
if policy is not None:
|
|
192
|
+
for cid in commit.retracted: # contradicted or no longer perceived: the count starts over
|
|
193
|
+
policy.frames.pop(cid, None)
|
|
194
|
+
policy.confirmed.discard(cid)
|
|
195
|
+
if not remember_retracted:
|
|
196
|
+
mind.forget(commit.retracted)
|
|
197
|
+
return thought
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _newly_established(mind: Store, policy: Corroboration | None, was_tentative: set[str], added: Sequence[str]) -> Thought:
|
|
201
|
+
if policy is None or not was_tentative:
|
|
202
|
+
return Thought()
|
|
203
|
+
flipped = [cid for cid in was_tentative if cid not in added and policy.established(mind, cid)]
|
|
204
|
+
return Thought(established=tuple(mind.claim(i) for i in policy._closure(mind, flipped) if i not in added))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True)
|
|
208
|
+
class Rule:
|
|
209
|
+
"""When the patterns match (and at least one matched claim is new), derive claims."""
|
|
210
|
+
|
|
211
|
+
name: str
|
|
212
|
+
when: tuple[tuple[Any, str, Any], ...]
|
|
213
|
+
then: Callable[[Mapping[str, Any], Store], Iterable[Claim | tuple[Claim, Score] | Fragment]]
|
|
214
|
+
version: str = "1" # a Fragment output is an act of reading: integrated as knowledge, not as a derivation
|
|
215
|
+
reacts_to: str = "new_premises" # or "any_change": also fire when something was retracted (e.g. a spinner vanished)
|
|
216
|
+
established_only: bool = False # with a Corroboration policy: premises must be established, and ``then`` sees the established view
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class ThinkStats:
|
|
221
|
+
rounds: int = 0
|
|
222
|
+
firings: int = 0
|
|
223
|
+
derived: int = 0
|
|
224
|
+
ms: float = 0.0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def think(mind: Store, rules: Sequence[Rule], *, since: Thought, max_rounds: int = 8, stats: ThinkStats | None = None, forget_withdrawn: bool = True,
|
|
228
|
+
corroboration: Corroboration | None = None) -> Thought:
|
|
229
|
+
"""Bottom-up inference: fire rules on matches that involve something new, until quiet.
|
|
230
|
+
|
|
231
|
+
Semi-naive evaluation: a match whose premises are all old already fired earlier. A claim
|
|
232
|
+
that just became established counts as new for ``established_only`` rules.
|
|
233
|
+
"""
|
|
234
|
+
t0 = _time.perf_counter()
|
|
235
|
+
stats = stats if stats is not None else ThinkStats()
|
|
236
|
+
total = Thought()
|
|
237
|
+
frontier = {r.id for r in since.added} | {r.id for r in since.established}
|
|
238
|
+
changed = bool(since.added or since.retracted or since.established)
|
|
239
|
+
view = corroboration.view(mind) if corroboration is not None else None
|
|
240
|
+
for round_no in range(max_rounds):
|
|
241
|
+
if not frontier and not (round_no == 0 and changed):
|
|
242
|
+
break
|
|
243
|
+
stats.rounds += 1
|
|
244
|
+
edits: list[Tell] = []
|
|
245
|
+
readings: list[Fragment] = []
|
|
246
|
+
for rule in rules:
|
|
247
|
+
strict = rule.established_only and view is not None
|
|
248
|
+
for bindings, support in (view if strict else mind).match(*rule.when, with_support=True):
|
|
249
|
+
if frontier.isdisjoint(support) and not (rule.reacts_to == "any_change" and round_no == 0 and changed):
|
|
250
|
+
continue
|
|
251
|
+
stats.firings += 1
|
|
252
|
+
for out in rule.then(bindings, view if strict else mind):
|
|
253
|
+
if isinstance(out, Fragment):
|
|
254
|
+
readings.append(out)
|
|
255
|
+
continue
|
|
256
|
+
claim, score = out if isinstance(out, tuple) else (out, None)
|
|
257
|
+
edits.append(Tell(claim, (Evidence(Ref(f"rule:{rule.name}"), _now(), method=f"derive@{rule.version}", confidence=score, derived_from=tuple(sorted(set(support)))),)))
|
|
258
|
+
if not edits and not readings:
|
|
259
|
+
break
|
|
260
|
+
added_ids: list[str] = []
|
|
261
|
+
if edits:
|
|
262
|
+
added_ids += mind.apply(Patch(tuple(edits), mind.revision)).added
|
|
263
|
+
if readings:
|
|
264
|
+
added_ids += [r.id for r in integrate(mind, *readings).added]
|
|
265
|
+
added = tuple(mind.claim(i) for i in dict.fromkeys(added_ids))
|
|
266
|
+
stats.derived += len(added)
|
|
267
|
+
total += Thought(added)
|
|
268
|
+
frontier = {r.id for r in added}
|
|
269
|
+
stats.ms += (_time.perf_counter() - t0) * 1e3
|
|
270
|
+
return total
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _shown(value: Any) -> Any:
|
|
274
|
+
"""How a claim's object reads in an explanation.
|
|
275
|
+
|
|
276
|
+
``Score`` and friends are worth unwrapping to their number, but a value that carries a
|
|
277
|
+
unit alongside it (a quantity) loses its point when reduced to a bare float: "60.0"
|
|
278
|
+
instead of "60 coin" is exactly the confusion the unit exists to prevent.
|
|
279
|
+
"""
|
|
280
|
+
if hasattr(value, "value") and hasattr(value, "unit"):
|
|
281
|
+
return str(value)
|
|
282
|
+
return getattr(value, "value", value)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def explain(mind: Store, claim_id: str, *, depth: int = 6) -> list[str]:
|
|
286
|
+
"""Indented lines: the claim, then each line of support down to observations."""
|
|
287
|
+
lines: list[str] = []
|
|
288
|
+
|
|
289
|
+
def show(cid: str, level: int, seen: frozenset[str]) -> None:
|
|
290
|
+
rec = mind._claims.get(cid)
|
|
291
|
+
pad = " " * level
|
|
292
|
+
if rec is None:
|
|
293
|
+
lines.append(f"{pad}(forgotten {cid})")
|
|
294
|
+
return
|
|
295
|
+
c = rec.claim
|
|
296
|
+
obj = _shown(c.object)
|
|
297
|
+
lines.append(f"{pad}{c.subject} {c.predicate} {obj!r}" + (" [retracted]" if rec.retracted else ""))
|
|
298
|
+
if level >= depth or cid in seen:
|
|
299
|
+
return
|
|
300
|
+
for e in rec.evidence:
|
|
301
|
+
conf = f" ({e.confidence.kind} {e.confidence.value:.2f})" if e.confidence else ""
|
|
302
|
+
if e.derived_from:
|
|
303
|
+
how = f" via {e.method}" if e.method else ""
|
|
304
|
+
lines.append(f"{pad} ← {e.source}{conf}{how} from:")
|
|
305
|
+
for p in e.derived_from:
|
|
306
|
+
show(p, level + 2, seen | {cid})
|
|
307
|
+
else:
|
|
308
|
+
lines.append(f"{pad} ← observed in {e.source}" + (f" at {e.locator}" if e.locator else "") + f" via {e.method}{conf}")
|
|
309
|
+
|
|
310
|
+
show(claim_id, 0, frozenset())
|
|
311
|
+
return lines
|
tensorcode/context.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Deterministic context assembly: redundancy removal and budgeted packing.
|
|
2
|
+
|
|
3
|
+
Packing is a lossy conversion, so the result says exactly what was dropped and why.
|
|
4
|
+
Required evidence is never silently dropped: if it cannot fit, the answer is Unknown.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Callable, Generic, Hashable, Sequence, TypeVar
|
|
12
|
+
|
|
13
|
+
from .outcomes import Score, Unknown
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
_TOKEN = re.compile(r"\w+|[^\w\s]")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def approx_tokens(text: str) -> int:
|
|
21
|
+
"""Word-and-punctuation count; a stand-in for a real tokenizer.
|
|
22
|
+
|
|
23
|
+
Measured against the Qwen3 tokenizer on 12,187 HotpotQA sentences: 0.82x the true total
|
|
24
|
+
(per-sentence median 0.84, p5 0.64), so it *undercounts*. Pass a real tokenizer as ``cost``
|
|
25
|
+
when a budget is a hard model limit.
|
|
26
|
+
"""
|
|
27
|
+
return len(_TOKEN.findall(text))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def shingle_similarity(a: str, b: str, n: int = 3) -> Score:
|
|
31
|
+
"""Jaccard overlap of word n-grams. A similarity, not a probability of duplication."""
|
|
32
|
+
|
|
33
|
+
def grams(s: str) -> set[tuple[str, ...]]:
|
|
34
|
+
words = s.lower().split()
|
|
35
|
+
return {tuple(words[i : i + n]) for i in range(max(1, len(words) - n + 1))}
|
|
36
|
+
|
|
37
|
+
ga, gb = grams(a), grams(b)
|
|
38
|
+
return Score(len(ga & gb) / len(ga | gb) if ga | gb else 0.0, "similarity")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Packed(Generic[T]):
|
|
43
|
+
items: tuple[T, ...]
|
|
44
|
+
used: int
|
|
45
|
+
budget: int
|
|
46
|
+
dropped: tuple[tuple[T, str], ...] # (item, reason)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dedupe(
|
|
50
|
+
ranked: Sequence[tuple[T, Score]],
|
|
51
|
+
*,
|
|
52
|
+
similarity: Callable[[T, T], Score],
|
|
53
|
+
threshold: float,
|
|
54
|
+
keep: Callable[[T], bool] = lambda item: False,
|
|
55
|
+
key: Callable[[T], Hashable] = id,
|
|
56
|
+
) -> tuple[list[tuple[T, Score]], list[tuple[T, str]]]:
|
|
57
|
+
"""Walk in rank order; drop an item too similar to one already kept (unless ``keep`` says otherwise)."""
|
|
58
|
+
kept: list[tuple[T, Score]] = []
|
|
59
|
+
dropped: list[tuple[T, str]] = []
|
|
60
|
+
for item, score in ranked:
|
|
61
|
+
dup = next((k for k, _ in kept if similarity(item, k).value >= threshold), None)
|
|
62
|
+
if dup is not None and not keep(item):
|
|
63
|
+
dropped.append((item, f"near-duplicate of {key(dup)!r}"))
|
|
64
|
+
else:
|
|
65
|
+
kept.append((item, score))
|
|
66
|
+
return kept, dropped
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def pack(
|
|
70
|
+
ranked: Sequence[tuple[T, Score]],
|
|
71
|
+
*,
|
|
72
|
+
budget: int,
|
|
73
|
+
cost: Callable[[T], int],
|
|
74
|
+
required: Sequence[T] = (),
|
|
75
|
+
key: Callable[[T], Hashable] = id,
|
|
76
|
+
dropped: Sequence[tuple[T, str]] = (),
|
|
77
|
+
) -> Packed[T] | Unknown:
|
|
78
|
+
"""Required items first, then ranked items greedily while they fit.
|
|
79
|
+
|
|
80
|
+
``dropped`` carries removals made earlier (e.g. by ``dedupe``) into the same report.
|
|
81
|
+
"""
|
|
82
|
+
required_keys = {key(r) for r in required}
|
|
83
|
+
chosen: list[T] = list(required)
|
|
84
|
+
used = sum(cost(r) for r in required)
|
|
85
|
+
if used > budget:
|
|
86
|
+
return Unknown("required_evidence_exceeds_budget", f"required items cost {used} > budget {budget}")
|
|
87
|
+
dropped = list(dropped)
|
|
88
|
+
for item, _ in ranked:
|
|
89
|
+
if key(item) in required_keys:
|
|
90
|
+
continue
|
|
91
|
+
c = cost(item)
|
|
92
|
+
if used + c <= budget:
|
|
93
|
+
chosen.append(item)
|
|
94
|
+
used += c
|
|
95
|
+
else:
|
|
96
|
+
dropped.append((item, f"budget: needs {c}, {budget - used} left"))
|
|
97
|
+
return Packed(tuple(chosen), used, budget, tuple(dropped))
|