Spaces:
Running on Zero
Running on Zero
cleanup: remove spike/ directory
Browse files- spike/vault_mind/DESIGN.md +0 -170
- spike/vault_mind/README.md +0 -83
- spike/vault_mind/__init__.py +0 -9
- spike/vault_mind/embed.py +0 -68
- spike/vault_mind/eval.py +0 -107
- spike/vault_mind/requirements-spike.txt +0 -14
- spike/vault_mind/schema.py +0 -62
- spike/vault_mind/store.py +0 -241
spike/vault_mind/DESIGN.md
DELETED
|
@@ -1,170 +0,0 @@
|
|
| 1 |
-
# Vault-mind spike — design & handoff
|
| 2 |
-
|
| 3 |
-
This is the full write-up so the spike can be continued in local dev. It records
|
| 4 |
-
(1) where the Chief Engineer's knowledge lives today, (2) what the pi-vault-mind
|
| 5 |
-
method is and how this spike implements it, (3) how CE knowledge maps onto it,
|
| 6 |
-
(4) how to read the eval, (5) **open questions / references needed from you**, and
|
| 7 |
-
(6) the **continue-locally checklist**.
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## 1. Starting point — where CE knowledge lives today (grounded in the code)
|
| 12 |
-
|
| 13 |
-
The Chief Engineer already has knowledge scattered across **four** surfaces, all
|
| 14 |
-
keyed and retrieved by hand-rolled exact match. The spike's premise is that these
|
| 15 |
-
are really *one* knowledge graph wearing four coats.
|
| 16 |
-
|
| 17 |
-
| Surface | Code | Shape | How it's retrieved today |
|
| 18 |
-
|---|---|---|---|
|
| 19 |
-
| **Lessons** (seed/earned/ingested/sim) | `ledger.py`, `models.py:LessonEntry` | append-only `data/lessons.jsonl` rows: `material, geometry_type, env_temp, env_humidity, outcome, lesson, source, timestamp` | `LedgerManager.retrieve`: exact `material` AND `geometry_type`, then nearest by **normalized** `[temp,humidity]` Euclidean, top-k. **No vector DB** (deliberate — `docs/plan/00-MASTER.md`, `02-ARCHITECTURE.md`). |
|
| 20 |
-
| **References** (hard params) | `ingest/distill.py:ReferenceFact` | `data/references.jsonl`: `material, param, value, source` | Injected into the prompt as a "Material Reference" block (`chief_engineer.advise(references=...)`). Not env-keyed. |
|
| 21 |
-
| **Policy cells** (learned offsets) | `learn/policy.py` | `data/policy.json`: keyed `material/geometry/temp-bucket/hum-bucket` → offsets + trials/successes | Exact cell-key lookup; injected as `policy_note`. **Generalizes** by bucketing (the "not just a lookup" layer). |
|
| 22 |
-
| **Sim observations** | `learn/loop.py`, `sim/outcome.py` | written back as `source="sim"` LessonEntry rows | same as lessons |
|
| 23 |
-
|
| 24 |
-
**The locked retrieval thesis** (do not casually break it): exact `material` +
|
| 25 |
-
`geometry_type`, normalized `[temp,humidity]` Euclidean, top 2–3; an empty result
|
| 26 |
-
is a *valid, strong* outcome (model reasons from material properties). The spike
|
| 27 |
-
must **respect** this as the baseline, not silently replace it.
|
| 28 |
-
|
| 29 |
-
The base the ledger was modeled on (`pi-qmd-ledger`) is itself a configurable
|
| 30 |
-
multi-ledger store — see `../pi-qmd-ledger.config.json`: named ledgers, per-ledger
|
| 31 |
-
`schema`, `dedupField`, regex `injectors`, and a `qmd` binary with `defaultLimit`.
|
| 32 |
-
pi-vault-mind sits on top of that family, adding the vector/FTS/graph index.
|
| 33 |
-
|
| 34 |
-
---
|
| 35 |
-
|
| 36 |
-
## 2. The pi-vault-mind method (as implemented here)
|
| 37 |
-
|
| 38 |
-
pi-vault-mind = **LanceDB store with three retrieval channels over the same rows**:
|
| 39 |
-
|
| 40 |
-
1. **Vector** — embed each knowledge atom's text; ANN search for semantic nearest.
|
| 41 |
-
2. **FTS** — LanceDB native full-text index over the same text; keyword recall.
|
| 42 |
-
3. **Graph** — typed edges between atoms; walk them to pull connected context.
|
| 43 |
-
|
| 44 |
-
This spike fuses (1)+(2) with **Reciprocal Rank Fusion** (reranker-free, transparent)
|
| 45 |
-
and then does a one-hop **graph expansion** over the top hits. Every returned hit
|
| 46 |
-
carries **provenance** — which channel(s) surfaced it (`vector|fts|graph`).
|
| 47 |
-
|
| 48 |
-
> **The Obsidian half is intentionally absent.** pi-vault-mind (the product) also
|
| 49 |
-
> watches an Obsidian vault of markdown notes, parses `[[wikilinks]]`/frontmatter
|
| 50 |
-
> into the graph, and re-indexes on file changes. None of that is here: the CE has
|
| 51 |
-
> no human-authored markdown vault, and the ask was explicitly to skip it. The
|
| 52 |
-
> graph edges here are **synthesized from structured fields**, not parsed from notes.
|
| 53 |
-
|
| 54 |
-
### Why RRF + manual hybrid instead of LanceDB's native hybrid
|
| 55 |
-
LanceDB's built-in `query_type="hybrid"` wants a registered embedding function and
|
| 56 |
-
a reranker to materialize (verified in-sandbox: it *accepts* the call but needs
|
| 57 |
-
that plumbing). For a spike, manual vector+FTS+RRF is more transparent and has zero
|
| 58 |
-
hidden network/model dependencies. **Reconcile to whatever pi-vault-mind actually
|
| 59 |
-
does** once the repo is in hand (it may use native hybrid + a specific reranker).
|
| 60 |
-
|
| 61 |
-
---
|
| 62 |
-
|
| 63 |
-
## 3. The mapping — CE knowledge → vault nodes + edges
|
| 64 |
-
|
| 65 |
-
Two tables (`schema.py`):
|
| 66 |
-
|
| 67 |
-
**`nodes`** — one row per knowledge atom. `text` is BOTH embedded and FTS-indexed;
|
| 68 |
-
structured fields (`material, geometry_type, env_temp, env_humidity, outcome,
|
| 69 |
-
source`) stay queryable for hard prefilters. Kinds:
|
| 70 |
-
|
| 71 |
-
- `lesson` — one per `LessonEntry` (covers seed/earned/ingested/sim uniformly).
|
| 72 |
-
- `reference` — one per `ReferenceFact`.
|
| 73 |
-
- `cell` — **synthesized** policy-cell node per `material/geometry/temp-bucket/
|
| 74 |
-
hum-bucket` (mirrors `learn.policy.cell_key`); the aggregation hub the learned
|
| 75 |
-
policy keys on.
|
| 76 |
-
- `concept` — one per material and per geometry; a graph hub for "all PETG knowledge".
|
| 77 |
-
|
| 78 |
-
**`edges`** — typed, directed:
|
| 79 |
-
|
| 80 |
-
- `informed_by`: `cell → lesson` (which lessons shaped a policy bucket).
|
| 81 |
-
- `same_cell`: `lesson ↔ lesson` (siblings in one env bucket → **transfer neighbors**;
|
| 82 |
-
this is what lets a humid-PETG-bridge lesson reach the next humid-PETG-bridge job).
|
| 83 |
-
- `about`: `lesson|reference → concept` (material/geometry hubs).
|
| 84 |
-
|
| 85 |
-
This is the spike's claim: the policy bucketing that already exists in
|
| 86 |
-
`learn/policy.py` *is* a graph relation; pi-vault-mind just makes it first-class and
|
| 87 |
-
traversable alongside semantic recall, instead of locked behind an exact cell-key.
|
| 88 |
-
|
| 89 |
-
---
|
| 90 |
-
|
| 91 |
-
## 4. How to read the eval (`eval.py`)
|
| 92 |
-
|
| 93 |
-
It is **diagnostic, not a leaderboard**. We have no human relevance labels for
|
| 94 |
-
"which past lesson should this job have surfaced", so claiming a win would be
|
| 95 |
-
dishonest. Two lenses:
|
| 96 |
-
|
| 97 |
-
- **LENS 1 — agreement.** With `hard_filter=True` (same exact material+geometry
|
| 98 |
-
constraint as the ledger), does the vault recover the ledger's top-k? In-sandbox:
|
| 99 |
-
**7/7 (100%)**. Meaning: constrained, the method reproduces today's behavior — so
|
| 100 |
-
adopting it is not a regression risk; it's a superset.
|
| 101 |
-
- **LENS 2 — reach.** With the soft path, on probes where `ledger.retrieve` returns
|
| 102 |
-
**`[]`** (no exact precedent), what does the vault surface? In-sandbox: 5 probes
|
| 103 |
-
pulled same-material cross-geometry / same-geometry cross-material neighbors via
|
| 104 |
-
vector+FTS+graph. **These candidate rows are the entire reason to consider the
|
| 105 |
-
added complexity** — and with the offline lexical embedder they only prove the
|
| 106 |
-
wiring, not semantic quality.
|
| 107 |
-
|
| 108 |
-
**The honest bottom line of the spike:** the method cleanly subsumes the current
|
| 109 |
-
retrieval and adds a transfer-recall channel. Whether that channel is *good* is
|
| 110 |
-
unproven until a real embedder + a bigger, labelled-ish corpus is in play. That's a
|
| 111 |
-
local-dev call, and it's now a measurable one.
|
| 112 |
-
|
| 113 |
-
---
|
| 114 |
-
|
| 115 |
-
## 5. Open questions / references needed from you
|
| 116 |
-
|
| 117 |
-
These are the things that would turn this independent reading into a faithful mirror
|
| 118 |
-
of *your* method. Drop them in local dev:
|
| 119 |
-
|
| 120 |
-
1. **The `pi-vault-mind` repo itself** (top priority). Specifically:
|
| 121 |
-
- Table layout: one `nodes` table with a `kind` column (as here) vs. table-per-kind?
|
| 122 |
-
- Exact field names / schema, and how the vector + FTS indexes are declared.
|
| 123 |
-
- **Hybrid mechanism**: native LanceDB `hybrid` + which reranker, or a custom
|
| 124 |
-
fusion? (We used RRF — cheap to change.)
|
| 125 |
-
- **Graph model**: separate `edges` table (as here) vs. edges embedded on nodes;
|
| 126 |
-
what relation types it defines; how many hops it traverses on a query.
|
| 127 |
-
- The **embedder** it standardizes on (model + dim) so vectors are comparable.
|
| 128 |
-
- How it ingests from `pi-qmd-ledger` ledgers (the `qmd` binary? a direct read?).
|
| 129 |
-
2. **`pi-qmd-ledger` repo** (helpful): the real `append_ledger`/injector/dedup API
|
| 130 |
-
and schema config, so the ingest side matches rather than infers from the one
|
| 131 |
-
`pi-qmd-ledger.config.json` we can see.
|
| 132 |
-
3. **A bigger / real lesson corpus** (or the MCP-hackathon Modal ingestion output
|
| 133 |
-
referenced in `TODO.md`): 12 seeds is enough to prove plumbing, not to judge
|
| 134 |
-
recall quality.
|
| 135 |
-
4. **Any relevance signal** — even a handful of "for job X, lesson Y was the useful
|
| 136 |
-
precedent" pairs would let LENS 2 become a real precision/recall number.
|
| 137 |
-
5. **Decision input:** is the goal to *replace* `ledger.retrieve`, to *augment* it
|
| 138 |
-
(vault as an optional second retriever the LLM also sees), or purely research?
|
| 139 |
-
The current docs lock the exact-match path as the thesis — augment is the
|
| 140 |
-
lower-risk graduation path.
|
| 141 |
-
|
| 142 |
-
---
|
| 143 |
-
|
| 144 |
-
## 6. Continue locally — checklist
|
| 145 |
-
|
| 146 |
-
1. `uv pip install -r spike/vault_mind/requirements-spike.txt`
|
| 147 |
-
2. **Swap the embedder.** Implement the `Embedder` protocol in `embed.py` with a
|
| 148 |
-
real model (uncomment the `OllamaEmbedder` sketch + `ollama pull nomic-embed-text`,
|
| 149 |
-
or `sentence-transformers`). Re-run `uv run python -m spike.vault_mind.eval` and compare
|
| 150 |
-
LENS 2 candidates against the lexical baseline — semantic neighbors should differ.
|
| 151 |
-
3. **Reconcile to the real method.** With `pi-vault-mind` in hand, adjust `schema.py`
|
| 152 |
-
+ `store.py` to match its table layout, hybrid mechanism, and graph model. Keep
|
| 153 |
-
the `Hit`/provenance shape — the cockpit will want "why did this surface".
|
| 154 |
-
4. **Scale the corpus.** Point `eval.py` at the real/ingested lessons; watch whether
|
| 155 |
-
reach stays plausible as N grows (does it surface noise?).
|
| 156 |
-
5. **Decide & graduate (or delete).** If reach earns its keep: graduate as an
|
| 157 |
-
*augmenting* retriever behind a flag, feeding extra candidates into
|
| 158 |
-
`chief_engineer.advise(retrieved=...)` alongside — not instead of — the locked
|
| 159 |
-
exact-match precedent, so the thesis stays intact. If not: `rm -rf spike/vault_mind/`.
|
| 160 |
-
|
| 161 |
-
---
|
| 162 |
-
|
| 163 |
-
## Appendix — provenance & isolation invariants (don't regress)
|
| 164 |
-
|
| 165 |
-
- The demo path never imports `spike/`. Keep it that way (grep before graduating).
|
| 166 |
-
- `lancedb` stays out of the Space `requirements.txt` until/unless step 5 graduates it.
|
| 167 |
-
- The offline `HashingEmbedder` must remain the zero-dependency default so the spike
|
| 168 |
-
runs in CI / the web sandbox without network.
|
| 169 |
-
- `schema.env_bucket` is a deliberate copy of `learn.policy.env_bucket` to keep the
|
| 170 |
-
spike import-isolated. If the policy buckets change, mirror them here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/README.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
# Spike: the pi-vault-mind storage method for the Chief Engineer
|
| 2 |
-
|
| 3 |
-
**Status:** runnable skeleton + design, validated offline in the web sandbox.
|
| 4 |
-
**Scope:** spike only — isolated, removable, NOT wired into the judged demo.
|
| 5 |
-
**Owner decision pending:** whether any of this graduates into the real ledger.
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## What this is
|
| 10 |
-
|
| 11 |
-
A spike of the **pi-vault-mind storage method — LanceDB with vector + full-text
|
| 12 |
-
search + a graph layer** — applied to the Chief Engineer's knowledge, with the
|
| 13 |
-
**Obsidian vault-watching side deliberately left out** (per the ask: "spike test
|
| 14 |
-
the pi vault method without worrying about obsidian aspects").
|
| 15 |
-
|
| 16 |
-
It exists to answer one question with behavior instead of opinion: *what does the
|
| 17 |
-
Chief Engineer's retrieval gain (and cost) if we revisit the locked
|
| 18 |
-
"no embeddings / no vector DB" decision* (`core/ledger.py`, `docs/plan/00-MASTER.md`)
|
| 19 |
-
*and store lessons the pi-vault-mind way instead?*
|
| 20 |
-
|
| 21 |
-
It is a **contrast object** for the shipped `ledger.retrieve` (exact match on
|
| 22 |
-
`material` AND `geometry_type`, ranked by normalized `[temp, humidity]` Euclidean
|
| 23 |
-
distance, top-k). Nothing here replaces that path yet.
|
| 24 |
-
|
| 25 |
-
## Why it's safe to have in the repo
|
| 26 |
-
|
| 27 |
-
- Nothing in the demo path imports `spike/`. Grep-clean.
|
| 28 |
-
- `lancedb` is a **spike-only** dependency (`requirements-spike.txt`), kept out of
|
| 29 |
-
the Space `requirements.txt`.
|
| 30 |
-
- To remove the entire experiment: `rm -rf spike/vault_mind/`. Nothing else refers to it.
|
| 31 |
-
|
| 32 |
-
## Run it
|
| 33 |
-
|
| 34 |
-
```bash
|
| 35 |
-
cd chief-engineer
|
| 36 |
-
uv pip install -r spike/vault_mind/requirements-spike.txt
|
| 37 |
-
uv run python -m spike.vault_mind.eval
|
| 38 |
-
```
|
| 39 |
-
|
| 40 |
-
No network, no model download, no GPU. The vector channel uses an offline
|
| 41 |
-
deterministic **lexical** embedder (`embed.py`) so the *pipeline* runs anywhere;
|
| 42 |
-
swapping in a real embedder is one class (see `embed.py` + `requirements-spike.txt`).
|
| 43 |
-
|
| 44 |
-
## What the run showed (web sandbox, 12 seed lessons)
|
| 45 |
-
|
| 46 |
-
```
|
| 47 |
-
vault built: {'lesson': 12, 'cell': 11, 'concept': 6, 'edges': 38}
|
| 48 |
-
|
| 49 |
-
LENS 1 — agreement (hard_filter, apples-to-apples):
|
| 50 |
-
vault recovered a ledger top-3 precedent in 7/7 probes (100%).
|
| 51 |
-
LENS 2 — reach (soft path finds precedent where exact match is EMPTY):
|
| 52 |
-
5 probes with no exact-key precedent surfaced same-material / same-geometry
|
| 53 |
-
neighbors (e.g. PETG/adhesion -> PETG/stringing via graph; PETG/bridge ->
|
| 54 |
-
PETG/overhang sag). Candidates — judge relevance by eye.
|
| 55 |
-
```
|
| 56 |
-
|
| 57 |
-
**Read this honestly:**
|
| 58 |
-
- LENS 1 (100%) shows the method **subsumes** the current behavior when you
|
| 59 |
-
constrain it to the same exact filter. Good — it's not a regression risk.
|
| 60 |
-
- LENS 2 shows the *only* thing worth the added complexity: surfacing **transfer
|
| 61 |
-
precedent** that exact match structurally cannot reach (different material or
|
| 62 |
-
geometry, or the `retrieve() -> []` "no precedent" case). With the offline
|
| 63 |
-
**lexical** embedder these are token-overlap neighbors, not true semantic ones —
|
| 64 |
-
so this proves the **plumbing**, not the payoff. The payoff needs a real
|
| 65 |
-
embedder, which is the first local-dev step (below).
|
| 66 |
-
|
| 67 |
-
## Files
|
| 68 |
-
|
| 69 |
-
| File | Role |
|
| 70 |
-
|------|------|
|
| 71 |
-
| `embed.py` | `Embedder` protocol + offline `HashingEmbedder` default; the single seam to swap in a real model. |
|
| 72 |
-
| `schema.py` | How CE knowledge maps to vault `nodes` + `edges` (kinds, relations, cell/concept ids). |
|
| 73 |
-
| `store.py` | `VaultStore`: ingest JSONL → nodes+edges, hybrid (vector⊕FTS via RRF), graph expand, provenance. |
|
| 74 |
-
| `eval.py` | Head-to-head vs `ledger.retrieve` (agreement + reach lenses). Diagnostic, not a scored win. |
|
| 75 |
-
| `requirements-spike.txt` | `lancedb` + real-embedder options. Spike-only. |
|
| 76 |
-
| `DESIGN.md` | Full write-up: mapping rationale, the method, eval interpretation, **open questions / references needed**, and the **local-dev continuation checklist**. |
|
| 77 |
-
|
| 78 |
-
## Next (local dev)
|
| 79 |
-
|
| 80 |
-
See **`DESIGN.md` → "Continue locally"**. Short version: (1) drop in a real
|
| 81 |
-
embedder, (2) provide the actual **pi-vault-mind** repo so the schema/query are
|
| 82 |
-
reconciled to the real method rather than this independent reading, (3) re-run the
|
| 83 |
-
eval on a bigger corpus and decide if reach justifies the dependency.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/__init__.py
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
"""vault_mind spike — an ISOLATED, REMOVABLE experiment.
|
| 2 |
-
|
| 3 |
-
Spikes the pi-vault-mind storage method (LanceDB: vector + FTS + graph) over the
|
| 4 |
-
Chief Engineer's knowledge, minus the Obsidian vault-watching side. Nothing in
|
| 5 |
-
the judged demo path imports this package; `lancedb` is a spike-only optional
|
| 6 |
-
dependency (see requirements-spike.txt), NOT in the Space requirements.txt.
|
| 7 |
-
|
| 8 |
-
To remove the spike entirely: delete this directory. Nothing else references it.
|
| 9 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/embed.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
"""Embedding layer for the vault spike — pluggable, offline by default.
|
| 2 |
-
|
| 3 |
-
The pi-vault-mind method is "vector + FTS + graph". The vector half needs an
|
| 4 |
-
embedder. For a self-contained spike that runs with NO network and NO model
|
| 5 |
-
download, the default here is a deterministic *lexical* embedder: bag-of-hashed
|
| 6 |
-
-tokens, L2-normalized. It is reproducible and dependency-free, which is exactly
|
| 7 |
-
what a plumbing spike wants.
|
| 8 |
-
|
| 9 |
-
HONESTY: this default carries NO real semantics — its "vector" similarity is
|
| 10 |
-
essentially lexical overlap, so on its own it does not beat the existing
|
| 11 |
-
keyword/FTS path. It exists to prove the *pipeline* (ingest → vector + FTS →
|
| 12 |
-
fusion → graph expand → provenance) runs end to end. Swap in a real embedder
|
| 13 |
-
(sentence-transformers, or an Ollama `embeddings` call against the same gemma
|
| 14 |
-
stack) when moving to local dev to get genuine semantic recall. The Embedder
|
| 15 |
-
protocol below is the single seam to do that — nothing else changes.
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import hashlib
|
| 21 |
-
import math
|
| 22 |
-
import re
|
| 23 |
-
from typing import Protocol, Sequence
|
| 24 |
-
|
| 25 |
-
_TOKEN = re.compile(r"[a-z0-9]+")
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class Embedder(Protocol):
|
| 29 |
-
"""The one seam. Implement this to swap the offline default for a real model."""
|
| 30 |
-
|
| 31 |
-
dim: int
|
| 32 |
-
|
| 33 |
-
def encode(self, texts: Sequence[str]) -> list[list[float]]: ...
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
class HashingEmbedder:
|
| 37 |
-
"""Deterministic, offline, lexical. Placeholder vector channel for the spike."""
|
| 38 |
-
|
| 39 |
-
def __init__(self, dim: int = 256) -> None:
|
| 40 |
-
self.dim = dim
|
| 41 |
-
|
| 42 |
-
def _one(self, text: str) -> list[float]:
|
| 43 |
-
vec = [0.0] * self.dim
|
| 44 |
-
for tok in _TOKEN.findall(text.lower()):
|
| 45 |
-
h = int(hashlib.blake2b(tok.encode(), digest_size=8).hexdigest(), 16)
|
| 46 |
-
idx = h % self.dim
|
| 47 |
-
sign = 1.0 if (h >> 1) & 1 else -1.0
|
| 48 |
-
vec[idx] += sign
|
| 49 |
-
norm = math.sqrt(sum(v * v for v in vec)) or 1.0
|
| 50 |
-
return [v / norm for v in vec]
|
| 51 |
-
|
| 52 |
-
def encode(self, texts: Sequence[str]) -> list[list[float]]:
|
| 53 |
-
return [self._one(t) for t in texts]
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
# --- a sketch of the local-dev swap (left commented; needs network/model) ----
|
| 57 |
-
# class OllamaEmbedder:
|
| 58 |
-
# """Real semantic vectors via the same Ollama stack the cockpit already uses.
|
| 59 |
-
# `ollama pull nomic-embed-text` (or mxbai-embed-large), then point at it."""
|
| 60 |
-
# def __init__(self, model: str = "nomic-embed-text", dim: int = 768) -> None:
|
| 61 |
-
# import ollama # noqa
|
| 62 |
-
# self.model, self.dim, self._c = model, dim, ollama.Client()
|
| 63 |
-
# def encode(self, texts):
|
| 64 |
-
# return [self._c.embeddings(model=self.model, prompt=t)["embedding"] for t in texts]
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def default_embedder() -> Embedder:
|
| 68 |
-
return HashingEmbedder()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/eval.py
DELETED
|
@@ -1,107 +0,0 @@
|
|
| 1 |
-
"""Head-to-head: vault hybrid+graph retrieval vs. the locked `ledger.retrieve`.
|
| 2 |
-
|
| 3 |
-
This is DIAGNOSTIC, not a scored win. We have no human relevance labels, so we
|
| 4 |
-
do not claim one method is "better" — we surface WHAT CHANGES when you move from
|
| 5 |
-
exact match + env-Euclidean to vector+FTS+graph, so the local-dev decision is
|
| 6 |
-
informed by behavior, not vibes.
|
| 7 |
-
|
| 8 |
-
Two lenses:
|
| 9 |
-
1. AGREEMENT — with hard_filter on (apples-to-apples), does the vault recover
|
| 10 |
-
the same exact-match precedent the ledger returns? (Sanity: the method
|
| 11 |
-
subsumes the current behavior when constrained.)
|
| 12 |
-
2. REACH — with the soft path, what cross-key precedent does the vault
|
| 13 |
-
surface that exact match structurally CANNOT (different material/geometry,
|
| 14 |
-
or the cases where ledger.retrieve returns [])? These are the candidates
|
| 15 |
-
the thesis is betting on — inspect them by eye for plausibility.
|
| 16 |
-
|
| 17 |
-
Run: uv run python -m spike.vault_mind.eval (from chief-engineer/)
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import sys
|
| 23 |
-
import tempfile
|
| 24 |
-
from pathlib import Path
|
| 25 |
-
|
| 26 |
-
CE_ROOT = Path(__file__).resolve().parents[2] # chief-engineer/
|
| 27 |
-
if str(CE_ROOT) not in sys.path:
|
| 28 |
-
sys.path.insert(0, str(CE_ROOT))
|
| 29 |
-
sys.path.insert(0, str(Path(__file__).resolve().parent)) # spike pkg local imports
|
| 30 |
-
|
| 31 |
-
from core.ledger import LedgerManager # noqa: E402 (the locked baseline)
|
| 32 |
-
from store import VaultStore # noqa: E402
|
| 33 |
-
|
| 34 |
-
DATA = CE_ROOT / "data"
|
| 35 |
-
LESSONS = DATA / "lessons.jsonl"
|
| 36 |
-
SEEDS = DATA / "seed_lessons.jsonl"
|
| 37 |
-
REFS = DATA / "references.jsonl"
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _ledger_ids(lm: LedgerManager, material, geometry, temp, hum, k):
|
| 41 |
-
return [f"lesson::{e.job_id}" for e, _ in lm.retrieve(material, geometry, temp, hum, k=k)]
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def main() -> None:
|
| 45 |
-
sources = [p for p in (LESSONS, SEEDS) if p.exists() and p.stat().st_size > 0] or [SEEDS]
|
| 46 |
-
vault = VaultStore(tempfile.mkdtemp(prefix="vaultspike_"))
|
| 47 |
-
counts = vault.build(sources, REFS if REFS.exists() else None)
|
| 48 |
-
print("vault built:", counts)
|
| 49 |
-
print(f"baseline ledger source(s): {[s.name for s in sources]}\n")
|
| 50 |
-
|
| 51 |
-
# Point the baseline LedgerManager at the SAME data the vault saw, so LENS 1
|
| 52 |
-
# is apples-to-apples (default data/lessons.jsonl is empty until a run seeds it).
|
| 53 |
-
combined = Path(tempfile.mkdtemp(prefix="ledger_")) / "lessons.jsonl"
|
| 54 |
-
combined.write_text("\n".join(p.read_text().strip() for p in sources) + "\n")
|
| 55 |
-
lm = LedgerManager(path=combined)
|
| 56 |
-
|
| 57 |
-
# query set = every seed lesson's (material, geometry, env) as a probe
|
| 58 |
-
import json
|
| 59 |
-
probes = []
|
| 60 |
-
for s in sources:
|
| 61 |
-
for line in s.read_text().splitlines():
|
| 62 |
-
if line.strip():
|
| 63 |
-
d = json.loads(line)
|
| 64 |
-
probes.append((d["job_id"], d["material"], d["geometry_type"],
|
| 65 |
-
d["env_temp"], d["env_humidity"]))
|
| 66 |
-
|
| 67 |
-
agree_hits = agree_tot = 0
|
| 68 |
-
reach_examples = []
|
| 69 |
-
for job_id, mat, geo, t, h in probes:
|
| 70 |
-
led = set(_ledger_ids(lm, mat, geo, t, h, k=3))
|
| 71 |
-
led.discard(f"lesson::{job_id}") # leave-one-out
|
| 72 |
-
|
| 73 |
-
hard = {hit.id for hit in vault.query(material=mat, geometry_type=geo, env_temp=t,
|
| 74 |
-
env_humidity=h, k=3, hard_filter=True, expand=False)}
|
| 75 |
-
hard.discard(f"lesson::{job_id}")
|
| 76 |
-
if led:
|
| 77 |
-
agree_tot += 1
|
| 78 |
-
if led & hard:
|
| 79 |
-
agree_hits += 1
|
| 80 |
-
|
| 81 |
-
soft = vault.query(material=mat, geometry_type=geo, env_temp=t, env_humidity=h,
|
| 82 |
-
k=4, hard_filter=False, expand=True)
|
| 83 |
-
cross = [hit for hit in soft
|
| 84 |
-
if hit.id != f"lesson::{job_id}"
|
| 85 |
-
and (hit.meta.get("material") != mat or hit.meta.get("geometry_type") != geo)]
|
| 86 |
-
if cross and not led: # exactly the gap the thesis targets: no exact precedent
|
| 87 |
-
reach_examples.append((job_id, mat, geo, t, h, cross[:2]))
|
| 88 |
-
|
| 89 |
-
print("== LENS 1: agreement (hard_filter, apples-to-apples) ==")
|
| 90 |
-
rate = (agree_hits / agree_tot * 100) if agree_tot else 0.0
|
| 91 |
-
print(f" vault recovered a ledger top-3 precedent in {agree_hits}/{agree_tot} probes "
|
| 92 |
-
f"that had any exact precedent ({rate:.0f}%).\n")
|
| 93 |
-
|
| 94 |
-
print("== LENS 2: reach (soft path finds precedent where exact match is EMPTY) ==")
|
| 95 |
-
if not reach_examples:
|
| 96 |
-
print(" (none in this dataset — expected when every probe has an exact-key sibling)")
|
| 97 |
-
for job_id, mat, geo, t, h, cross in reach_examples[:6]:
|
| 98 |
-
print(f" {job_id}: {mat}/{geo} @ {t:.0f}C/{h:.0f}% had NO exact precedent; vault surfaced:")
|
| 99 |
-
for c in cross:
|
| 100 |
-
print(f" - [{','.join(c.channels)}] {c.meta.get('material')}/"
|
| 101 |
-
f"{c.meta.get('geometry_type')}: {c.text[:90]}...")
|
| 102 |
-
print("\nReminder: LENS 2 rows are CANDIDATES — judge relevance by eye. With the offline")
|
| 103 |
-
print("lexical embedder they reflect token overlap; a real embedder is needed for semantics.")
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
if __name__ == "__main__":
|
| 107 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/requirements-spike.txt
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
# Spike-only dependencies. NOT part of the Space/demo requirements.txt.
|
| 2 |
-
# Install into your local dev venv to run the vault_mind spike:
|
| 3 |
-
# pip install -r spike/vault_mind/requirements-spike.txt
|
| 4 |
-
#
|
| 5 |
-
# lancedb 0.33.0 is verified working in the web sandbox (vector + native FTS +
|
| 6 |
-
# hybrid all import and run). Pin loosely; bump when you move to local dev.
|
| 7 |
-
lancedb==0.33.0
|
| 8 |
-
|
| 9 |
-
# The vector channel uses an offline deterministic LEXICAL embedder by default
|
| 10 |
-
# (embed.py:HashingEmbedder) so the spike runs with no network/model download.
|
| 11 |
-
# For REAL semantic recall, add one of these locally and wire it via the
|
| 12 |
-
# Embedder protocol (embed.py) — see the commented OllamaEmbedder sketch:
|
| 13 |
-
# ollama # + `ollama pull nomic-embed-text` (reuses the cockpit stack)
|
| 14 |
-
# sentence-transformers # e.g. all-MiniLM-L6-v2, fully offline after first pull
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/schema.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
"""Vault schema — how the Chief Engineer's scattered knowledge maps onto the
|
| 2 |
-
pi-vault-mind node+edge model.
|
| 3 |
-
|
| 4 |
-
Two tables (this is the spike's reading of "vector + FTS + graph"):
|
| 5 |
-
|
| 6 |
-
nodes — every knowledge atom as one row: a `text` field that is BOTH the
|
| 7 |
-
vector-embedded content and the FTS-indexed content, plus structured
|
| 8 |
-
metadata for hard filters, plus the embedding `vector`.
|
| 9 |
-
edges — typed, directed relationships between node ids. The graph layer.
|
| 10 |
-
|
| 11 |
-
Node kinds folded in from the existing CE surfaces:
|
| 12 |
-
lesson — a LessonEntry row (seed | earned | ingested | sim) from lessons.jsonl
|
| 13 |
-
reference — a ReferenceFact (hard material parameter) from references.jsonl
|
| 14 |
-
cell — a synthesized policy-cell node (material/geometry/temp-bucket/hum-bucket),
|
| 15 |
-
the aggregation point the learned policy keys on
|
| 16 |
-
concept — a material or geometry concept node (graph hub for "all PETG knowledge")
|
| 17 |
-
|
| 18 |
-
Edge relations:
|
| 19 |
-
informed_by — cell -> lesson (this policy cell was shaped by this lesson)
|
| 20 |
-
same_cell — lesson<-> lesson (siblings in one policy bucket; transfer neighbors)
|
| 21 |
-
about — lesson -> concept / reference -> concept
|
| 22 |
-
|
| 23 |
-
NOTE (reconcile locally): these names/kinds are the spike's independent reading.
|
| 24 |
-
The real pi-vault-mind table layout, field names, and whether it uses one table
|
| 25 |
-
with a `kind` column vs. a table-per-kind should be confirmed against the repo
|
| 26 |
-
and this file adjusted to match. See OPEN-QUESTIONS in DESIGN.md.
|
| 27 |
-
"""
|
| 28 |
-
|
| 29 |
-
from __future__ import annotations
|
| 30 |
-
|
| 31 |
-
NODES_TABLE = "nodes"
|
| 32 |
-
EDGES_TABLE = "edges"
|
| 33 |
-
|
| 34 |
-
# structured metadata carried on every node (None where not applicable)
|
| 35 |
-
NODE_FIELDS = (
|
| 36 |
-
"id", "kind", "text", "vector",
|
| 37 |
-
"material", "geometry_type", "env_temp", "env_humidity",
|
| 38 |
-
"outcome", "source", "timestamp",
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
EDGE_FIELDS = ("src", "dst", "rel", "weight")
|
| 42 |
-
|
| 43 |
-
REL_INFORMED_BY = "informed_by"
|
| 44 |
-
REL_SAME_CELL = "same_cell"
|
| 45 |
-
REL_ABOUT = "about"
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def env_bucket(temp: float, humidity: float) -> tuple[str, str]:
|
| 49 |
-
"""Mirror of learn.policy.env_bucket — duplicated so the spike stays
|
| 50 |
-
import-isolated from the demo package (removable without touching it)."""
|
| 51 |
-
tb = "cool" if temp < 20 else ("warm" if temp > 26 else "mid")
|
| 52 |
-
hb = "dry" if humidity < 35 else ("humid" if humidity > 55 else "mid")
|
| 53 |
-
return tb, hb
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def cell_id(material: str, geometry: str, temp: float, humidity: float) -> str:
|
| 57 |
-
tb, hb = env_bucket(temp, humidity)
|
| 58 |
-
return f"cell::{material}/{geometry}/{tb}/{hb}"
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def concept_id(kind: str, value: str) -> str:
|
| 62 |
-
return f"concept::{kind}::{value}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spike/vault_mind/store.py
DELETED
|
@@ -1,241 +0,0 @@
|
|
| 1 |
-
"""VaultStore — the spike's implementation of the pi-vault-mind storage method
|
| 2 |
-
(LanceDB: vector + FTS + graph), applied to the Chief Engineer's knowledge,
|
| 3 |
-
with the Obsidian vault-watching side deliberately left out.
|
| 4 |
-
|
| 5 |
-
What it demonstrates end to end, offline:
|
| 6 |
-
1. INGEST lessons.jsonl (+ optional references.jsonl) into a `nodes` table,
|
| 7 |
-
synthesizing policy-cell and concept nodes and the `edges` between
|
| 8 |
-
them (the graph).
|
| 9 |
-
2. HYBRID retrieval = vector search ⊕ full-text search, fused with Reciprocal
|
| 10 |
-
Rank Fusion (transparent, reranker-free), with optional hard
|
| 11 |
-
metadata prefilters.
|
| 12 |
-
3. GRAPH expansion = walk edges from the hybrid hits to pull in the
|
| 13 |
-
connected policy cell and its sibling lessons (cross-env transfer),
|
| 14 |
-
and concept hubs.
|
| 15 |
-
4. PROVENANCE every returned hit says WHY it surfaced (vector | fts | graph).
|
| 16 |
-
|
| 17 |
-
This is the contrast object for the locked exact-match `ledger.retrieve`. See
|
| 18 |
-
eval.py for the head-to-head, and DESIGN.md for what it would buy / cost.
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
from __future__ import annotations
|
| 22 |
-
|
| 23 |
-
import json
|
| 24 |
-
from collections import defaultdict
|
| 25 |
-
from dataclasses import dataclass, field
|
| 26 |
-
from pathlib import Path
|
| 27 |
-
from typing import Iterable
|
| 28 |
-
|
| 29 |
-
import lancedb
|
| 30 |
-
|
| 31 |
-
from embed import Embedder, default_embedder
|
| 32 |
-
from schema import (
|
| 33 |
-
EDGES_TABLE, NODES_TABLE, REL_ABOUT, REL_INFORMED_BY, REL_SAME_CELL,
|
| 34 |
-
cell_id, concept_id, env_bucket,
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
_RRF_K = 60 # standard Reciprocal Rank Fusion constant
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
@dataclass
|
| 41 |
-
class Hit:
|
| 42 |
-
id: str
|
| 43 |
-
kind: str
|
| 44 |
-
text: str
|
| 45 |
-
score: float
|
| 46 |
-
channels: list[str] = field(default_factory=list) # provenance: vector|fts|graph
|
| 47 |
-
meta: dict = field(default_factory=dict)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def _node_row(emb_text: str, vector: list[float], *, id: str, kind: str,
|
| 51 |
-
material=None, geometry_type=None, env_temp=None, env_humidity=None,
|
| 52 |
-
outcome=None, source=None, timestamp=None) -> dict:
|
| 53 |
-
return {
|
| 54 |
-
"id": id, "kind": kind, "text": emb_text, "vector": vector,
|
| 55 |
-
"material": material or "", "geometry_type": geometry_type or "",
|
| 56 |
-
"env_temp": float(env_temp) if env_temp is not None else 0.0,
|
| 57 |
-
"env_humidity": float(env_humidity) if env_humidity is not None else 0.0,
|
| 58 |
-
"outcome": outcome or "", "source": source or "", "timestamp": timestamp or "",
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
class VaultStore:
|
| 63 |
-
def __init__(self, db_path: str | Path, embedder: Embedder | None = None) -> None:
|
| 64 |
-
self.db = lancedb.connect(str(db_path))
|
| 65 |
-
self.embedder = embedder or default_embedder()
|
| 66 |
-
self.dim = self.embedder.dim
|
| 67 |
-
|
| 68 |
-
# --- ingest ------------------------------------------------------------
|
| 69 |
-
@staticmethod
|
| 70 |
-
def _read_jsonl(path: Path) -> list[dict]:
|
| 71 |
-
if not path or not Path(path).exists():
|
| 72 |
-
return []
|
| 73 |
-
out = []
|
| 74 |
-
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
| 75 |
-
line = line.strip()
|
| 76 |
-
if line:
|
| 77 |
-
try:
|
| 78 |
-
out.append(json.loads(line))
|
| 79 |
-
except Exception:
|
| 80 |
-
continue
|
| 81 |
-
return out
|
| 82 |
-
|
| 83 |
-
def build(self, lessons_paths: Iterable[str | Path],
|
| 84 |
-
references_path: str | Path | None = None) -> dict[str, int]:
|
| 85 |
-
"""(Re)build the vault from JSONL. Returns counts per node kind."""
|
| 86 |
-
lessons: list[dict] = []
|
| 87 |
-
for p in lessons_paths:
|
| 88 |
-
lessons.extend(self._read_jsonl(Path(p)))
|
| 89 |
-
refs = self._read_jsonl(Path(references_path)) if references_path else []
|
| 90 |
-
|
| 91 |
-
nodes: list[dict] = []
|
| 92 |
-
edges: list[dict] = []
|
| 93 |
-
concepts: set[tuple[str, str]] = set()
|
| 94 |
-
cell_members: dict[str, list[str]] = defaultdict(list)
|
| 95 |
-
|
| 96 |
-
# lessons -> lesson nodes (+ cell membership, + concept edges)
|
| 97 |
-
for e in lessons:
|
| 98 |
-
lid = f"lesson::{e.get('job_id','?')}"
|
| 99 |
-
mat, geo = e.get("material", ""), e.get("geometry_type", "")
|
| 100 |
-
text = (f"{mat} {geo} at {e.get('env_temp','?')}C/"
|
| 101 |
-
f"{e.get('env_humidity','?')}% RH -> {e.get('outcome','')}. "
|
| 102 |
-
f"{e.get('lesson','')}")
|
| 103 |
-
nodes.append(_node_row(text, [], id=lid, kind="lesson", material=mat,
|
| 104 |
-
geometry_type=geo, env_temp=e.get("env_temp"),
|
| 105 |
-
env_humidity=e.get("env_humidity"),
|
| 106 |
-
outcome=e.get("outcome"), source=e.get("source", "earned"),
|
| 107 |
-
timestamp=e.get("timestamp", "")))
|
| 108 |
-
cid = cell_id(mat, geo, float(e.get("env_temp", 0)), float(e.get("env_humidity", 0)))
|
| 109 |
-
cell_members[cid].append(lid)
|
| 110 |
-
if mat:
|
| 111 |
-
concepts.add(("material", mat)); edges.append({"src": lid, "dst": concept_id("material", mat), "rel": REL_ABOUT, "weight": 1.0})
|
| 112 |
-
if geo:
|
| 113 |
-
concepts.add(("geometry", geo)); edges.append({"src": lid, "dst": concept_id("geometry", geo), "rel": REL_ABOUT, "weight": 1.0})
|
| 114 |
-
|
| 115 |
-
# references -> reference nodes (+ concept edge to material)
|
| 116 |
-
for i, r in enumerate(refs):
|
| 117 |
-
rid = f"reference::{i}"
|
| 118 |
-
mat = r.get("material", "")
|
| 119 |
-
text = (f"{mat} reference: {r.get('param','')} = {r.get('value','')} "
|
| 120 |
-
f"(source: {r.get('source','')})")
|
| 121 |
-
nodes.append(_node_row(text, [], id=rid, kind="reference", material=mat,
|
| 122 |
-
source=r.get("source", "reference")))
|
| 123 |
-
if mat:
|
| 124 |
-
concepts.add(("material", mat)); edges.append({"src": rid, "dst": concept_id("material", mat), "rel": REL_ABOUT, "weight": 1.0})
|
| 125 |
-
|
| 126 |
-
# synthesized policy-cell nodes + informed_by / same_cell edges (the graph)
|
| 127 |
-
for cid, members in cell_members.items():
|
| 128 |
-
_, body = cid.split("::", 1)
|
| 129 |
-
nodes.append(_node_row(f"policy cell {body}", [], id=cid, kind="cell"))
|
| 130 |
-
for m in members:
|
| 131 |
-
edges.append({"src": cid, "dst": m, "rel": REL_INFORMED_BY, "weight": 1.0})
|
| 132 |
-
for a in members: # sibling transfer edges within a bucket
|
| 133 |
-
for b in members:
|
| 134 |
-
if a != b:
|
| 135 |
-
edges.append({"src": a, "dst": b, "rel": REL_SAME_CELL, "weight": 0.5})
|
| 136 |
-
|
| 137 |
-
# concept hub nodes
|
| 138 |
-
for kind, val in concepts:
|
| 139 |
-
nodes.append(_node_row(f"{kind}: {val}", [], id=concept_id(kind, val), kind="concept",
|
| 140 |
-
material=val if kind == "material" else None,
|
| 141 |
-
geometry_type=val if kind == "geometry" else None))
|
| 142 |
-
|
| 143 |
-
# embed all node texts in one batch, then write
|
| 144 |
-
vectors = self.embedder.encode([n["text"] for n in nodes])
|
| 145 |
-
for n, v in zip(nodes, vectors):
|
| 146 |
-
n["vector"] = v
|
| 147 |
-
|
| 148 |
-
self.db.drop_table(NODES_TABLE, ignore_missing=True)
|
| 149 |
-
self.db.drop_table(EDGES_TABLE, ignore_missing=True)
|
| 150 |
-
ntbl = self.db.create_table(NODES_TABLE, data=nodes)
|
| 151 |
-
ntbl.create_fts_index("text", use_tantivy=False, replace=True)
|
| 152 |
-
self.db.create_table(EDGES_TABLE, data=edges or [{"src": "", "dst": "", "rel": "", "weight": 0.0}])
|
| 153 |
-
|
| 154 |
-
counts: dict[str, int] = defaultdict(int)
|
| 155 |
-
for n in nodes:
|
| 156 |
-
counts[n["kind"]] += 1
|
| 157 |
-
counts["edges"] = len(edges)
|
| 158 |
-
return dict(counts)
|
| 159 |
-
|
| 160 |
-
# --- retrieval ---------------------------------------------------------
|
| 161 |
-
def _nodes(self):
|
| 162 |
-
return self.db.open_table(NODES_TABLE)
|
| 163 |
-
|
| 164 |
-
def _edges(self):
|
| 165 |
-
return self.db.open_table(EDGES_TABLE)
|
| 166 |
-
|
| 167 |
-
@staticmethod
|
| 168 |
-
def _all_rows(tbl) -> list[dict]:
|
| 169 |
-
return tbl.to_arrow().to_pylist()
|
| 170 |
-
|
| 171 |
-
def _hybrid(self, query_text: str, limit: int, where: str | None) -> dict[str, Hit]:
|
| 172 |
-
"""Vector ⊕ FTS, fused with RRF. Returns {id: Hit} with channel provenance."""
|
| 173 |
-
tbl = self._nodes()
|
| 174 |
-
qv = self.embedder.encode([query_text])[0]
|
| 175 |
-
|
| 176 |
-
vsearch = tbl.search(qv, vector_column_name="vector")
|
| 177 |
-
if where:
|
| 178 |
-
vsearch = vsearch.where(where, prefilter=True)
|
| 179 |
-
vec_ids = [r["id"] for r in vsearch.limit(limit * 4).to_list()]
|
| 180 |
-
|
| 181 |
-
try:
|
| 182 |
-
fsearch = tbl.search(query_text, query_type="fts")
|
| 183 |
-
if where:
|
| 184 |
-
fsearch = fsearch.where(where, prefilter=True)
|
| 185 |
-
fts_ids = [r["id"] for r in fsearch.limit(limit * 4).to_list()]
|
| 186 |
-
except Exception:
|
| 187 |
-
fts_ids = []
|
| 188 |
-
|
| 189 |
-
rows = {r["id"]: r for r in self._all_rows(tbl)}
|
| 190 |
-
fused: dict[str, Hit] = {}
|
| 191 |
-
for channel, ids in (("vector", vec_ids), ("fts", fts_ids)):
|
| 192 |
-
for rank, nid in enumerate(ids):
|
| 193 |
-
r = rows.get(nid)
|
| 194 |
-
if not r:
|
| 195 |
-
continue
|
| 196 |
-
h = fused.get(nid) or Hit(id=nid, kind=r["kind"], text=r["text"], score=0.0,
|
| 197 |
-
meta={k: r[k] for k in ("material", "geometry_type",
|
| 198 |
-
"env_temp", "env_humidity", "outcome", "source")})
|
| 199 |
-
h.score += 1.0 / (_RRF_K + rank + 1)
|
| 200 |
-
if channel not in h.channels:
|
| 201 |
-
h.channels.append(channel)
|
| 202 |
-
fused[nid] = h
|
| 203 |
-
return fused
|
| 204 |
-
|
| 205 |
-
def query(self, *, material: str = "", geometry_type: str = "", env_temp: float = 0.0,
|
| 206 |
-
env_humidity: float = 0.0, text: str | None = None, k: int = 3,
|
| 207 |
-
expand: bool = True, hard_filter: bool = False) -> list[Hit]:
|
| 208 |
-
"""Hybrid + graph retrieval. `hard_filter=True` restricts to exact
|
| 209 |
-
material+geometry (apples-to-apples with ledger.retrieve); default is the
|
| 210 |
-
soft path that lets cross-key precedent surface — the spike's whole point."""
|
| 211 |
-
q = text or (f"{material} {geometry_type} at {env_temp:.0f}C/{env_humidity:.0f}% RH")
|
| 212 |
-
where = None
|
| 213 |
-
if hard_filter and material and geometry_type:
|
| 214 |
-
where = f"material = '{material}' AND geometry_type = '{geometry_type}'"
|
| 215 |
-
|
| 216 |
-
fused = self._hybrid(q, k, where)
|
| 217 |
-
# keep lessons/references as primary hits; cells/concepts arrive via graph
|
| 218 |
-
primary = {i: h for i, h in fused.items() if h.kind in ("lesson", "reference")}
|
| 219 |
-
|
| 220 |
-
if expand and primary:
|
| 221 |
-
etbl = self._edges()
|
| 222 |
-
top_ids = sorted(primary, key=lambda i: primary[i].score, reverse=True)[:k]
|
| 223 |
-
edge_rows = self._all_rows(etbl)
|
| 224 |
-
ntbl_rows = {r["id"]: r for r in self._all_rows(self._nodes())}
|
| 225 |
-
for nid in top_ids:
|
| 226 |
-
tb, hb = env_bucket(primary[nid].meta.get("env_temp", 0),
|
| 227 |
-
primary[nid].meta.get("env_humidity", 0))
|
| 228 |
-
for er in edge_rows:
|
| 229 |
-
if er["src"] == nid and er["rel"] == REL_SAME_CELL:
|
| 230 |
-
sib = ntbl_rows.get(er["dst"])
|
| 231 |
-
if sib and er["dst"] not in primary:
|
| 232 |
-
h = Hit(id=er["dst"], kind=sib["kind"], text=sib["text"],
|
| 233 |
-
score=primary[nid].score * 0.4, channels=["graph"],
|
| 234 |
-
meta={kk: sib[kk] for kk in ("material", "geometry_type",
|
| 235 |
-
"env_temp", "env_humidity", "outcome", "source")})
|
| 236 |
-
primary[er["dst"]] = h
|
| 237 |
-
elif sib and "graph" not in primary[er["dst"]].channels:
|
| 238 |
-
primary[er["dst"]].channels.append("graph")
|
| 239 |
-
primary[er["dst"]].score += primary[nid].score * 0.2
|
| 240 |
-
|
| 241 |
-
return sorted(primary.values(), key=lambda h: h.score, reverse=True)[:k]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|