aether-context 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.
- aether_context/__init__.py +29 -0
- aether_context/_log.py +33 -0
- aether_context/cli.py +1191 -0
- aether_context/config.py +206 -0
- aether_context/context_pool.py +819 -0
- aether_context/encoder.py +213 -0
- aether_context/errors.py +93 -0
- aether_context/local_llm.py +846 -0
- aether_context/mpo.py +151 -0
- aether_context/py.typed +0 -0
- aether_context/quantize.py +86 -0
- aether_context/session.py +829 -0
- aether_context/slice_loader.py +501 -0
- aether_context/tokenizer.py +64 -0
- aether_context/ui.py +253 -0
- aether_context/witness.py +356 -0
- aether_context-0.3.0.dist-info/METADATA +429 -0
- aether_context-0.3.0.dist-info/RECORD +23 -0
- aether_context-0.3.0.dist-info/WHEEL +5 -0
- aether_context-0.3.0.dist-info/entry_points.txt +2 -0
- aether_context-0.3.0.dist-info/licenses/LICENSE +201 -0
- aether_context-0.3.0.dist-info/licenses/NOTICE.md +19 -0
- aether_context-0.3.0.dist-info/top_level.txt +1 -0
aether_context/mpo.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# aether-context (Unlimited Context)
|
|
2
|
+
# Copyright (c) 2026 Aether AI
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""MPO context chain — chains the session's context space and improves selection accuracy.
|
|
5
|
+
|
|
6
|
+
Cosine/semantic search remains the retrieval mechanism; this does not replace it. The MPO
|
|
7
|
+
(Matrix Product Operator) chain links the session's slices into one connected space: when
|
|
8
|
+
cosine pulls an entry slice, the chain pulls in the slices most coupled to it, widening the
|
|
9
|
+
working set with the connected thread instead of isolated nearest-neighbors.
|
|
10
|
+
|
|
11
|
+
Deterministic, numpy-only, and purely additive — it only ever *adds* connected slices to a
|
|
12
|
+
result, never blocks or replaces a hit, so any failure degrades cleanly to plain cosine.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Sequence
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger("aether_context.mpo")
|
|
23
|
+
|
|
24
|
+
DEFAULT_CHAIN_WIDTH: int = 8
|
|
25
|
+
DEFAULT_CHAIN_HOPS: int = 1
|
|
26
|
+
_FEAT: int = 8
|
|
27
|
+
_BOND: int = 4
|
|
28
|
+
_OUT: int = 4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ChainItem:
|
|
33
|
+
"""A retrieval candidate the chain operates on: id, unit vector, and ``c_t``."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
vector: np.ndarray
|
|
37
|
+
c_t: tuple[float, float]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lift(x: float, p: int) -> np.ndarray:
|
|
41
|
+
half = max(1, p // 2)
|
|
42
|
+
ks = np.arange(1, half + 1, dtype=np.float64)
|
|
43
|
+
f = np.concatenate([np.sin(np.pi * ks * x), np.cos(np.pi * ks * x)])
|
|
44
|
+
if f.size < p:
|
|
45
|
+
f = np.concatenate([f, np.zeros(p - f.size)])
|
|
46
|
+
return f[:p]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _unit(v: np.ndarray) -> np.ndarray:
|
|
50
|
+
n = float(np.linalg.norm(v))
|
|
51
|
+
return v if n < 1e-12 else v / n
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cos(a: np.ndarray, b: np.ndarray) -> float:
|
|
55
|
+
na, nb = float(np.linalg.norm(a)), float(np.linalg.norm(b))
|
|
56
|
+
if na < 1e-12 or nb < 1e-12:
|
|
57
|
+
return 0.0
|
|
58
|
+
return float(np.dot(a, b) / (na * nb))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _minmax(x: np.ndarray) -> np.ndarray:
|
|
62
|
+
lo, hi = float(x.min()), float(x.max())
|
|
63
|
+
if hi - lo < 1e-12:
|
|
64
|
+
return np.full_like(x, 0.5)
|
|
65
|
+
return (x - lo) / (hi - lo)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MpoChain:
|
|
69
|
+
"""Chains slices and expands a cosine hit into the slices most coupled to it."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
seed: int = 0,
|
|
75
|
+
width: int = DEFAULT_CHAIN_WIDTH,
|
|
76
|
+
hops: int = DEFAULT_CHAIN_HOPS,
|
|
77
|
+
) -> None:
|
|
78
|
+
rng = np.random.default_rng(seed)
|
|
79
|
+
p = _FEAT
|
|
80
|
+
self.width = max(1, int(width))
|
|
81
|
+
self.hops = max(1, int(hops))
|
|
82
|
+
self._w0 = (rng.standard_normal((1, p, _BOND)) * 0.5).astype(np.float64)
|
|
83
|
+
self._w1 = (rng.standard_normal((_BOND, p, _OUT)) * 0.5).astype(np.float64)
|
|
84
|
+
|
|
85
|
+
def _embed(self, c_t: tuple[float, float]) -> np.ndarray:
|
|
86
|
+
u = _lift(float(c_t[0]), _FEAT)
|
|
87
|
+
v = _lift(float(c_t[1]), _FEAT)
|
|
88
|
+
a = np.einsum("aib,i->ab", self._w0, u)[0]
|
|
89
|
+
e = np.einsum("b,bjm,j->m", a, self._w1, v)
|
|
90
|
+
return _unit(e)
|
|
91
|
+
|
|
92
|
+
def coupling(self, vec_i, e_i, vec_h, e_h) -> float:
|
|
93
|
+
"""Similarity of two slices for chaining (higher = more coupled)."""
|
|
94
|
+
return _cos(np.asarray(vec_i), np.asarray(vec_h)) * (0.5 + 0.5 * _cos(e_i, e_h))
|
|
95
|
+
|
|
96
|
+
def expand(
|
|
97
|
+
self,
|
|
98
|
+
hit_ids: Sequence[str],
|
|
99
|
+
candidates: Sequence[ChainItem],
|
|
100
|
+
*,
|
|
101
|
+
width: int | None = None,
|
|
102
|
+
hops: int | None = None,
|
|
103
|
+
) -> list[str]:
|
|
104
|
+
"""Widen ``hit_ids`` with the candidates most coupled to them.
|
|
105
|
+
|
|
106
|
+
Returns ordered ids: the hits first (in their given order), then the coupled slices in
|
|
107
|
+
pull order, de-duplicated.
|
|
108
|
+
"""
|
|
109
|
+
items = list(candidates)
|
|
110
|
+
if not items:
|
|
111
|
+
return list(hit_ids)
|
|
112
|
+
width = self.width if width is None else max(1, int(width))
|
|
113
|
+
hops = self.hops if hops is None else max(1, int(hops))
|
|
114
|
+
|
|
115
|
+
a0 = _minmax(np.array([it.c_t[0] for it in items], dtype=np.float64))
|
|
116
|
+
a1 = _minmax(np.array([it.c_t[1] for it in items], dtype=np.float64))
|
|
117
|
+
embeds = {it.id: self._embed((a0[i], a1[i])) for i, it in enumerate(items)}
|
|
118
|
+
by_id = {it.id: it for it in items}
|
|
119
|
+
|
|
120
|
+
present = [h for h in hit_ids if h in by_id] or [items[0].id]
|
|
121
|
+
selected: list[str] = []
|
|
122
|
+
seen: set[str] = set()
|
|
123
|
+
for h in present:
|
|
124
|
+
if h not in seen:
|
|
125
|
+
selected.append(h)
|
|
126
|
+
seen.add(h)
|
|
127
|
+
frontier = list(selected)
|
|
128
|
+
|
|
129
|
+
for _ in range(hops):
|
|
130
|
+
scored: list[tuple[float, str]] = []
|
|
131
|
+
for it in items:
|
|
132
|
+
if it.id in seen:
|
|
133
|
+
continue
|
|
134
|
+
best = max(
|
|
135
|
+
self.coupling(it.vector, embeds[it.id], by_id[h].vector, embeds[h])
|
|
136
|
+
for h in frontier
|
|
137
|
+
)
|
|
138
|
+
scored.append((best, it.id))
|
|
139
|
+
if not scored:
|
|
140
|
+
break
|
|
141
|
+
scored.sort(key=lambda p: p[0], reverse=True)
|
|
142
|
+
picked = [sid for _, sid in scored[:width]]
|
|
143
|
+
for sid in picked:
|
|
144
|
+
if sid not in seen:
|
|
145
|
+
selected.append(sid)
|
|
146
|
+
seen.add(sid)
|
|
147
|
+
frontier = picked
|
|
148
|
+
return selected
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = ["MpoChain", "ChainItem", "DEFAULT_CHAIN_WIDTH", "DEFAULT_CHAIN_HOPS"]
|
aether_context/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# aether-context (Unlimited Context)
|
|
2
|
+
# Copyright (c) 2026 Aether AI
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""TurboVec — scalar quantization of unit retrieval vectors (the context pool's compression codec).
|
|
5
|
+
|
|
6
|
+
The context pool stores L2-normalized 256-dim float32 vectors (1024 B/row). TurboVec quantizes each
|
|
7
|
+
row to ``bits``-bit codes + a per-row float32 scale, so the footprint drops to ``dim*bits/8 + 4`` bytes
|
|
8
|
+
— ~8x at 4-bit, ~4x at 8-bit. The same byte ceiling then holds that many more slices in the hot set,
|
|
9
|
+
which is the coherence-at-scale win (more of the run stays resident, fewer cold reads).
|
|
10
|
+
|
|
11
|
+
Per-row SYMMETRIC quantization: ``code = round(v/scale) + half`` over ``scale = max|v| / half``, so the
|
|
12
|
+
row's dynamic range maps onto the integer grid. Dequantization recovers ``(code-half)*scale`` and
|
|
13
|
+
RE-NORMALIZES to unit length (cosine only cares about direction). Recall@k parity vs float32 is the gate
|
|
14
|
+
the tests enforce (>=0.98). ``bits=0`` is the identity (the float32 path) — TurboVec is fully reversible.
|
|
15
|
+
|
|
16
|
+
Pure numpy, no dependencies. Only 4- and 8-bit are supported (the codes pack to whole bytes cleanly).
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
SUPPORTED_BITS = (0, 4, 8)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def packed_bytes_per_row(dim: int, bits: int) -> int:
|
|
26
|
+
"""On-disk/in-RAM code bytes for one row at ``bits`` (excludes the 4-byte per-row scale)."""
|
|
27
|
+
if bits == 0:
|
|
28
|
+
return dim * 4
|
|
29
|
+
if bits == 8:
|
|
30
|
+
return dim
|
|
31
|
+
if bits == 4:
|
|
32
|
+
return (dim + 1) // 2
|
|
33
|
+
raise ValueError(f"unsupported bits={bits}; use one of {SUPPORTED_BITS}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def quantize(matrix: np.ndarray, bits: int = 4) -> tuple[np.ndarray, np.ndarray]:
|
|
37
|
+
"""``(n, dim)`` float32 rows -> ``(codes uint8 (n, packed), scales float32 (n,))``.
|
|
38
|
+
|
|
39
|
+
Rows are assumed ~unit (the pool stores unit vectors). ``bits=0`` returns the raw float32 bytes as
|
|
40
|
+
uint8 with unit scales (identity passthrough), so callers can treat both paths uniformly.
|
|
41
|
+
"""
|
|
42
|
+
m = np.ascontiguousarray(matrix, dtype=np.float32)
|
|
43
|
+
n, dim = m.shape
|
|
44
|
+
if bits == 0:
|
|
45
|
+
return m.view(np.uint8).reshape(n, dim * 4), np.ones(n, dtype=np.float32)
|
|
46
|
+
levels = (1 << bits) - 1
|
|
47
|
+
half = levels >> 1
|
|
48
|
+
maxabs = np.max(np.abs(m), axis=1)
|
|
49
|
+
scales = np.where(maxabs > 1e-12, maxabs / half, 1.0).astype(np.float32)
|
|
50
|
+
q = np.rint(m / scales[:, None]) + half
|
|
51
|
+
q = np.clip(q, 0, levels).astype(np.uint8) # (n, dim) integer codes
|
|
52
|
+
if bits == 8:
|
|
53
|
+
return np.ascontiguousarray(q), scales
|
|
54
|
+
# 4-bit: pack two nibbles per byte (pad odd dim with a zero nibble).
|
|
55
|
+
if dim & 1:
|
|
56
|
+
q = np.pad(q, ((0, 0), (0, 1)))
|
|
57
|
+
codes = ((q[:, 0::2] << 4) | q[:, 1::2]).astype(np.uint8)
|
|
58
|
+
return np.ascontiguousarray(codes), scales
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def dequantize(codes: np.ndarray, scales: np.ndarray, dim: int, bits: int = 4) -> np.ndarray:
|
|
62
|
+
"""Inverse of :func:`quantize` -> ``(n, dim)`` float32 UNIT rows (re-normalized for cosine)."""
|
|
63
|
+
if bits == 0:
|
|
64
|
+
return codes.reshape(codes.shape[0], dim * 4).view(np.float32).reshape(-1, dim).copy()
|
|
65
|
+
levels = (1 << bits) - 1
|
|
66
|
+
half = levels >> 1
|
|
67
|
+
if bits == 8:
|
|
68
|
+
q = codes.astype(np.float32)
|
|
69
|
+
else: # 4-bit: unpack the nibbles
|
|
70
|
+
hi = (codes >> 4) & 0xF
|
|
71
|
+
lo = codes & 0xF
|
|
72
|
+
q = np.empty((codes.shape[0], hi.shape[1] * 2), dtype=np.float32)
|
|
73
|
+
q[:, 0::2] = hi
|
|
74
|
+
q[:, 1::2] = lo
|
|
75
|
+
q = q[:, :dim]
|
|
76
|
+
v = (q - half) * scales[:, None]
|
|
77
|
+
norms = np.linalg.norm(v, axis=1, keepdims=True)
|
|
78
|
+
norms[norms < 1e-12] = 1.0
|
|
79
|
+
return (v / norms).astype(np.float32)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compression_ratio(dim: int, bits: int) -> float:
|
|
83
|
+
"""float32 row bytes / quantized row bytes (vector only) — the headline TurboVec multiple."""
|
|
84
|
+
if bits == 0:
|
|
85
|
+
return 1.0
|
|
86
|
+
return (dim * 4) / (packed_bytes_per_row(dim, bits) + 4)
|