@equationalapplications/expo-llm-wiki 7.15.1 → 7.16.0
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.
- package/README.md +22 -431
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -10,21 +10,14 @@ Local-first LLM memory for Expo and React Native. Combines the core semantic sea
|
|
|
10
10
|
|
|
11
11
|
> Inspired by [Andrej Karpathy's LLM Wiki memory spec](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**📖 Documentation: [equationalapplications.github.io/expo-llm-wiki/packages/expo](https://equationalapplications.github.io/expo-llm-wiki/packages/expo/)**. Start with the [Expo quickstart](https://equationalapplications.github.io/expo-llm-wiki/start/quickstart-expo/).
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
- **Built on `expo-sqlite`** — Stable, well-supported SQLite driver
|
|
17
|
-
- **Hermes-ready** — Secure record ID generation via [`expo-crypto`](https://docs.expo.dev/versions/latest/sdk/crypto/); wired automatically at import (no manual `crypto` polyfill)
|
|
18
|
-
- **Semantic search** — Vector embeddings via `embed` function, with MiniSearch fallback
|
|
19
|
-
- **Retrieval tuning** — Per-call overrides for search behavior (pre-filter, hybrid blend, tier weights)
|
|
20
|
-
- **Multi-entity reads** — Search across multiple `entity_id` namespaces in one pass with `tierWeights`
|
|
21
|
-
- **Source provenance** — `WikiFact.source_type` distinguishes immutable document facts (`immutable_document`) from mutable derived/user facts. Immutable document content is protected from librarian/heal rewriting and only changed by `forget()` or re-ingest.
|
|
22
|
-
- **Seeded ontologies** — Enforce strict taxonomies or allow emergent graph relationship extraction (`useOntologyManifest`, `useSetOntologyManifest`; Strict, Emergent, or Off; defaults to Off).
|
|
23
|
-
- **React hooks** — `WikiProvider`, `useMemoryRead`, `useOntologyManifest`, `useSetOntologyManifest`, `useWikiTraversal`, and all other hooks re-exported from `@equationalapplications/expo-llm-wiki`
|
|
24
|
-
- **Full-featured memory** — Facts, tasks, events, maintenance jobs (librarian, heal, reembed, prune)
|
|
25
|
-
- **Interoperability:** Supports [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) v0.1 + v0.2 import and export via the [llm-wiki OKF profiles](https://github.com/equationalapplications/expo-llm-wiki/blob/main/docs/okf-profile.md) (default `llm-wiki/2`, back-compat `llm-wiki/1`).
|
|
15
|
+
## Features
|
|
26
16
|
|
|
27
|
-
- **
|
|
17
|
+
- **Expo-ready.** Core plus an `expo-sqlite` adapter, so there's nothing to wire up.
|
|
18
|
+
- **Hermes-ready.** Secure record IDs through [`expo-crypto`](https://docs.expo.dev/versions/latest/sdk/crypto/), registered automatically on import. No `crypto` polyfill needed.
|
|
19
|
+
- **React hooks.** `WikiProvider`, `useMemoryRead`, `useWikiTraversal` and the rest of [`react-llm-wiki`](https://equationalapplications.github.io/expo-llm-wiki/reference/hooks/), re-exported.
|
|
20
|
+
- **Everything in core.** Semantic and keyword search, multi-entity reads, seeded ontologies and [GraphRAG](https://equationalapplications.github.io/expo-llm-wiki/guides/graphrag/), background maintenance, and OKF import and export.
|
|
28
21
|
|
|
29
22
|
## Installation
|
|
30
23
|
|
|
@@ -33,435 +26,33 @@ npx expo install expo-sqlite expo-crypto
|
|
|
33
26
|
npm install @equationalapplications/expo-llm-wiki
|
|
34
27
|
```
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## Semantic Search
|
|
39
|
-
|
|
40
|
-
Enable vector-based retrieval by providing an `embed` function:
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
import { createWiki } from '@equationalapplications/expo-llm-wiki';
|
|
44
|
-
import { openDatabaseSync } from 'expo-sqlite';
|
|
45
|
-
|
|
46
|
-
const db = openDatabaseSync('wiki.db');
|
|
47
|
-
|
|
48
|
-
const wiki = createWiki(db, {
|
|
49
|
-
config: {
|
|
50
|
-
// Optimize retrieval for large memory stores
|
|
51
|
-
preFilterLimit: 50, // Limit cosine scoring to top-50 keyword matches
|
|
52
|
-
hybridWeight: 0.7, // Blend semantic (0.7) + keyword (0.3)
|
|
53
|
-
},
|
|
54
|
-
llmProvider: {
|
|
55
|
-
generateText: async ({ systemPrompt, userPrompt }) => {
|
|
56
|
-
// Your LLM call — must return the model output as a string
|
|
57
|
-
return 'Model output';
|
|
58
|
-
},
|
|
59
|
-
maxOutputTokens: 4096, // optional — your model's output ceiling; lets maintenance passes size their LLM calls
|
|
60
|
-
embed: async (text: string) => {
|
|
61
|
-
// Your embedding service (e.g., OpenAI, Cohere)
|
|
62
|
-
// Use an absolute URL — React Native / Expo apps do not have a browser
|
|
63
|
-
// origin to resolve relative URLs against on device or simulator.
|
|
64
|
-
const response = await fetch('https://your-api.example.com/api/embed', {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
body: JSON.stringify({ text })
|
|
67
|
-
});
|
|
68
|
-
const { embedding } = await response.json();
|
|
69
|
-
return embedding; // number[]
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
onRetrievalFallback: (error) => {
|
|
73
|
-
console.warn('Embedding unavailable, using keyword search:', error);
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
await wiki.setup();
|
|
78
|
-
|
|
79
|
-
// Semantic query
|
|
80
|
-
const memory = await wiki.read('user-123', 'what activities should I do this weekend?');
|
|
81
|
-
// Matches facts like "Saturday hiking trip" even with no lexical overlap
|
|
82
|
-
|
|
83
|
-
// Per-call overrides
|
|
84
|
-
const fasterSearch = await wiki.read('user-123', 'activities', {
|
|
85
|
-
maxResults: 5,
|
|
86
|
-
preFilterLimit: 20, // Tighter pre-filter for speed
|
|
87
|
-
hybridWeight: 0.5, // More keyword weight
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Multi-entity with tier weights
|
|
91
|
-
const multiMemory = await wiki.read(['tier_wisdom', 'tier_fact', 'tier_working'], 'activities', {
|
|
92
|
-
maxResults: 8,
|
|
93
|
-
tierWeights: {
|
|
94
|
-
tier_wisdom: 2, // boost curated notes 2×
|
|
95
|
-
tier_fact: 1, // neutral baseline
|
|
96
|
-
tier_working: 0.25, // downrank unvetted context
|
|
97
|
-
},
|
|
98
|
-
// includeZeroWeightEntities: true — include 0-weight entities as bottom-ranked filler
|
|
99
|
-
});
|
|
100
|
-
// multiMemory.factScores — Record<factId, weightedScore> | undefined (array entityId only, populated when query is non-empty and at least one fact scored)
|
|
101
|
-
// multiMemory.metadata — { query, entityIds, tierWeights }
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Configuration
|
|
105
|
-
|
|
106
|
-
All `WikiConfig` fields are optional:
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
const wiki = createWiki(db, {
|
|
110
|
-
llmProvider: { /* ... */ },
|
|
111
|
-
config: {
|
|
112
|
-
tablePrefix: 'llm_wiki_', // default: 'llm_wiki_'
|
|
113
|
-
maxResults: 10, // default: 10
|
|
114
|
-
autoLibrarianThreshold: 20, // default: 20 — events before librarian auto-runs
|
|
115
|
-
autoHealThreshold: 100, // default: 100 — events before heal auto-runs
|
|
116
|
-
maxChunkLength: 12000, // default: 12000 (char count per ingestDocument chunk)
|
|
117
|
-
chunkOverlap: 400, // default: 400 (overlap between chunks in characters)
|
|
118
|
-
chunkConcurrency: 1, // default: 1 (parallel LLM calls per ingestDocument)
|
|
119
|
-
pruneRetainSoftDeletedFor: 7, // default: 7 (days before hard-deleting soft-deleted facts)
|
|
120
|
-
pruneEventsAfter: 30, // default: 30 (days before hard-deleting old events)
|
|
121
|
-
orphanAfterDays: 30, // default: 30 (days before runHeal flags sourceless facts; null to disable)
|
|
122
|
-
staleInferredAfterDays: 60, // default: 60 (days before runHeal downgrades inferred facts; null to disable)
|
|
123
|
-
preFilterLimit: 50, // default: undefined — MiniSearch pre-filter before cosine scan; recommended for >500 facts
|
|
124
|
-
hybridWeight: 0.7, // default: undefined — blend semantic (1.0) ↔ keyword (0.0); pure semantic when unset
|
|
125
|
-
excludeDrafts: false, // default: false — hide unreviewed drafts from reads; traversal skips discovered drafts (a draft start fact is still returned)
|
|
126
|
-
grounding: { mode: 'off' }, // default: off — 'draft' stores facts that don't quote their source as drafts
|
|
127
|
-
|
|
128
|
-
// Global prompt overrides — librarianSystemPrompt and healSystemPrompt apply to write() auto-runs;
|
|
129
|
-
// ingestSystemPrompt applies only to explicit ingestDocument() calls.
|
|
130
|
-
// ⚠ Overrides replace the entire default prompt, including the JSON output contract.
|
|
131
|
-
// Your prompt must instruct the LLM to return the required JSON shape — see packages/core/README.md#prompt-management--overrides.
|
|
132
|
-
prompts: {
|
|
133
|
-
ingestSystemPrompt: `Extract core facts from this document: {{documentChunk}}\n\nReturn ONLY valid JSON: { "facts": [{ "title": "string", "body": "string", "tags": ["string"], "confidence": "certain|inferred|tentative" }] }. No markdown.`,
|
|
134
|
-
librarianSystemPrompt: `Synthesize these thoughts into insights:\n{{events}}\n\nReturn ONLY valid JSON: { "facts": [{ "title": "string", "body": "string", "tags": ["string"], "confidence": "certain|inferred|tentative" }], "tasks": [{ "description": "string", "priority": 0 }] }. No markdown.`,
|
|
135
|
-
healSystemPrompt: `Fix the memory graph based on these candidates: {{healCandidates}}\n\nReturn ONLY valid JSON: { "downgraded": ["factId"], "deleted": ["factId"], "newFacts": [{ "title": "string", "body": "string", "tags": ["string"], "confidence": "certain|inferred|tentative" }] }. No markdown.`,
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**Diagnostics and draft review.** Pass `onDiagnostic` beside `llmProvider` (not inside `config`) to receive typed, content-free reports of dropped chunks, facts, edges and failed background jobs. `useMemoryRead` and `useWikiTraversal` accept `excludeDrafts` per call, which overrides the config default. There are no dedicated hooks for listing or promoting drafts yet: call `listDrafts` / `promoteDraft` on the `WikiMemory` instance from `useWiki()`, as shown below. With `grounding` on, facts that don't quote their source land as drafts; see [core: Grounding](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#grounding). See [core: Diagnostics](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#diagnostics) and [core: Draft Review](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#draft-review). Other `WikiConfig` fields, such as `ontology`, are documented in the [core README](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#configuration).
|
|
142
|
-
|
|
143
|
-
```tsx
|
|
144
|
-
import { useWiki } from '@equationalapplications/expo-llm-wiki';
|
|
29
|
+
Use `npx expo install` so Expo picks native builds that match your SDK.
|
|
145
30
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```
|
|
31
|
+
| Peer dependency | Range |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `expo-sqlite` | `^14 \|\| ^15 \|\| ^55 \|\| ^56 \|\| ^57 \|\| ^58` |
|
|
34
|
+
| `expo-crypto` | `>=12` |
|
|
35
|
+
| `react` | `>=17` |
|
|
152
36
|
|
|
153
|
-
|
|
37
|
+
## Quick look
|
|
154
38
|
|
|
155
39
|
```tsx
|
|
156
|
-
|
|
157
|
-
|
|
40
|
+
import * as SQLite from 'expo-sqlite';
|
|
41
|
+
import { createWiki, WikiProvider, useMemoryRead } from '@equationalapplications/expo-llm-wiki';
|
|
158
42
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// → { danglingEdges, manifestViolations, untypedFacts, drafts, unverifiedInferred,
|
|
162
|
-
// sample: { danglingEdgeIds, manifestViolationEdgeIds } }
|
|
163
|
-
|
|
164
|
-
// pendingSources: per-input change status including the partial-ingest state.
|
|
165
|
-
const loadStatuses = () => wiki.pendingSources(entityId, batch);
|
|
166
|
-
// → Array<{ sourceRef: string; status: 'new' | 'changed' | 'partial' | 'current' }>
|
|
167
|
-
|
|
168
|
-
// getInstructions: effective system prompts for each writer, with overrides applied.
|
|
169
|
-
const loadInstructions = () => wiki.getInstructions(entityId);
|
|
170
|
-
// → { ingest, librarian, heal, ontologyBackfill }
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
See [core: Lint](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#lint), [core: Batch Change Detection](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#batch-change-detection), and [core: Effective instructions](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#effective-instructions-getinstructions).
|
|
175
|
-
|
|
176
|
-
## Retrieval Tuning
|
|
177
|
-
|
|
178
|
-
Optimize `read()` performance and blend retrieval strategies:
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
const config = {
|
|
182
|
-
// Limit cosine similarity scoring to top-K MiniSearch keyword candidates
|
|
183
|
-
preFilterLimit: 50,
|
|
184
|
-
|
|
185
|
-
// Blend semantic and keyword scores (0.0 = pure keyword, 1.0 = pure semantic)
|
|
186
|
-
hybridWeight: 0.7,
|
|
187
|
-
|
|
188
|
-
// Max results returned per read
|
|
189
|
-
maxResults: 10,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const wiki = createWiki(db, {
|
|
193
|
-
config,
|
|
194
|
-
llmProvider: { /* ... */ },
|
|
195
|
-
});
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**Hybrid scoring blends:**
|
|
199
|
-
- `hybridWeight: 1.0` → pure semantic ranking among the candidates being scored; if `preFilterLimit` is set, semantic scoring is still limited to the top-K MiniSearch matches
|
|
200
|
-
- `hybridWeight: 0.5` → balanced semantic + keyword (50/50 blend)
|
|
201
|
-
- `hybridWeight: 0.0` → pure keyword ranking, skips `embed()` entirely (no LLM API cost)
|
|
202
|
-
|
|
203
|
-
**Pre-filtering optimization:**
|
|
204
|
-
When `preFilterLimit: 50` is set with 1000 facts, cosine similarity is computed only for the top 50 MiniSearch keyword matches, reducing O(N) scoring to O(50).
|
|
205
|
-
|
|
206
|
-
## Usage
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { createWiki } from '@equationalapplications/expo-llm-wiki';
|
|
210
|
-
import { openDatabaseSync } from 'expo-sqlite';
|
|
211
|
-
|
|
212
|
-
const db = openDatabaseSync('wiki.db');
|
|
213
|
-
|
|
214
|
-
const wiki = createWiki(db, {
|
|
215
|
-
llmProvider: {
|
|
216
|
-
generateText: async ({ systemPrompt, userPrompt }) => {
|
|
217
|
-
// Your LLM call — must return the model output as a string
|
|
218
|
-
return 'Model output';
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// Initialize tables (call once on app startup)
|
|
43
|
+
const db = await SQLite.openDatabaseAsync('app.db');
|
|
44
|
+
const wiki = createWiki(db, { llmProvider }); // pass the expo-sqlite database directly
|
|
224
45
|
await wiki.setup();
|
|
225
46
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Manual executions: runtime promptOverride applies only to this single call.
|
|
230
|
-
// Must include the JSON output contract — overrides replace the entire default prompt.
|
|
231
|
-
await wiki.runLibrarian('user-123', {
|
|
232
|
-
promptOverride: `Strict domain extraction task:\n{{events}}\n\nReturn ONLY valid JSON: { "facts": [{ "title": "string", "body": "string", "tags": ["string"], "confidence": "certain|inferred|tentative" }], "tasks": [{ "description": "string", "priority": 0 }] }. No markdown.`,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
await wiki.ingestDocument('user-123', {
|
|
236
|
-
sourceRef: 'doc-1',
|
|
237
|
-
sourceHash: sha256(content),
|
|
238
|
-
documentChunk: content,
|
|
239
|
-
promptOverride: `Focus strictly on technical APIs: {{documentChunk}}\n\nReturn ONLY valid JSON: { "facts": [{ "title": "string", "body": "string", "tags": ["string"], "confidence": "certain|inferred|tentative" }] }. No markdown.`,
|
|
240
|
-
});
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
## With React
|
|
244
|
-
|
|
245
|
-
`@equationalapplications/expo-llm-wiki` re-exports all hooks and `WikiProvider` from `@equationalapplications/react-llm-wiki`:
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
import { WikiProvider } from '@equationalapplications/expo-llm-wiki';
|
|
249
|
-
|
|
250
|
-
<WikiProvider wiki={wiki}>
|
|
251
|
-
<MyApp />
|
|
252
|
-
</WikiProvider>
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
Then use hooks in components:
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { useMemoryRead } from '@equationalapplications/expo-llm-wiki';
|
|
259
|
-
|
|
260
|
-
export function UserProfile({ userId }: { userId: string }) {
|
|
261
|
-
const { data, isPending } = useMemoryRead(userId, 'preferences');
|
|
262
|
-
|
|
263
|
-
if (isPending) return <Text>Loading...</Text>;
|
|
264
|
-
return <Text>{data?.facts.map(f => f.title).join(', ')}</Text>;
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
For live background-job status (e.g. a loading spinner while a document ingests or the librarian runs):
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
import { useEntityStatus } from '@equationalapplications/expo-llm-wiki';
|
|
272
|
-
|
|
273
|
-
export function EntityLoadingSpinner({ entityId }: { entityId: string }) {
|
|
274
|
-
const { ingesting, librarian, heal } = useEntityStatus(entityId);
|
|
275
|
-
if (!ingesting && !librarian && !heal) return null;
|
|
276
|
-
return <Spinner label={ingesting ? 'Ingesting…' : librarian ? 'Organizing…' : 'Healing…'} />;
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### `useOntologyManifest(entityId)`
|
|
281
|
-
|
|
282
|
-
Reactive read — fetches on mount and when `entityId` or `wiki` changes:
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
import { useOntologyManifest } from '@equationalapplications/expo-llm-wiki';
|
|
286
|
-
|
|
287
|
-
const { manifest, mode, isPending, error, refetch } = useOntologyManifest('user-123');
|
|
288
|
-
// manifest: OntologyManifest | null
|
|
289
|
-
// mode: OntologyMode | null ('strict' | 'emergent' | 'off' when present)
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Note: `manifest` and `mode` are `null` when the entity has no persisted or seeded manifest (`getOntologyManifest` returned `null`). Call `refetch()` after mutations to refresh.
|
|
293
|
-
|
|
294
|
-
### `useSetOntologyManifest()`
|
|
295
|
-
|
|
296
|
-
Mutation — same `{ execute, isPending, error, lastResult }` contract as `useWikiWrite`:
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import { useOntologyManifest, useSetOntologyManifest } from '@equationalapplications/expo-llm-wiki';
|
|
300
|
-
|
|
301
|
-
export function OntologySettings({ entityId }: { entityId: string }) {
|
|
302
|
-
const { manifest, mode, refetch } = useOntologyManifest(entityId);
|
|
303
|
-
const { execute, isPending, error } = useSetOntologyManifest();
|
|
304
|
-
|
|
305
|
-
const handleSave = async () => {
|
|
306
|
-
await execute(entityId, {
|
|
307
|
-
node_types: [{ type: 'person', description: 'An individual.' }],
|
|
308
|
-
edge_types: [{
|
|
309
|
-
type: 'reports_to',
|
|
310
|
-
source_type: 'person',
|
|
311
|
-
target_type: 'person',
|
|
312
|
-
description: 'Reporting hierarchy.',
|
|
313
|
-
}],
|
|
314
|
-
}, { mode: 'strict' });
|
|
315
|
-
refetch();
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// render manifest/mode; wire handleSave to a save button
|
|
47
|
+
function App() {
|
|
48
|
+
const { data, isPending } = useMemoryRead('user-123', 'preferences');
|
|
49
|
+
// ...
|
|
319
50
|
}
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
Global defaults and `seedManifests` bootstrap are configured at construction time via `createWiki(..., { config: { ontology: ... } })`. See the [core package README § Per-Entity Seeded Ontology](https://github.com/equationalapplications/expo-llm-wiki/blob/main/packages/core/README.md#per-entity-seeded-ontology) for mode semantics and manifest schema.
|
|
323
|
-
|
|
324
|
-
`useSetOntologyManifest` does not automatically refresh `useOntologyManifest` — call `refetch()` after a successful `execute()`, same as `useWikiWrite` + `useMemoryRead`.
|
|
325
|
-
|
|
326
|
-
### `useWikiTraversal(entityId, options)`
|
|
327
|
-
|
|
328
|
-
Reactive read — fetches on mount and whenever `entityId` or `options` change. Walks the knowledge graph N hops outward from a fact (`options.sourceId`) using edges written by `runLibrarian()`/`ingestDocument()`'s Seeded Ontology extraction pass:
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
import { useWikiTraversal, formatGraphContext } from '@equationalapplications/expo-llm-wiki';
|
|
332
|
-
|
|
333
|
-
const { nodes, edges, isPending, error, refetch } = useWikiTraversal('user-123', {
|
|
334
|
-
sourceId: 'fact_42',
|
|
335
|
-
maxDepth: 2,
|
|
336
|
-
direction: 'both',
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const promptContext = formatGraphContext({ nodes, edges });
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
- `maxDepth` is clamped to `[1, 3]` regardless of input.
|
|
343
|
-
- `edgeTypes: []` (explicit empty array) matches nothing; omitting it matches all edge types.
|
|
344
|
-
- Defaults (`maxTraversalNodes`, `minTraversalConfidence`, `traversalDirection`, `excludeSourceTypes`) can be set globally via `createWiki(..., { config: { maxTraversalNodes: 20, ... } })` and overridden per-call.
|
|
345
|
-
- `formatGraphContext()` is a pure function — call it with the hook's `{ nodes, edges }` to get a dense text block suitable for prompt injection.
|
|
346
|
-
|
|
347
|
-
## Component Lifecycle
|
|
348
|
-
|
|
349
|
-
```mermaid
|
|
350
|
-
flowchart TD
|
|
351
|
-
A["<WikiProvider wiki={wiki}>"] --> B["App Components"]
|
|
352
|
-
B --> C{"Use Hook?"}
|
|
353
|
-
C -->|"useMemoryRead(entityId, query, options?)"| D["[Read Memory]"]
|
|
354
|
-
C -->|"useWikiWrite()"| E["[Write Memory]"]
|
|
355
|
-
C -->|"useWikiIngest()"| F["[Ingest Document]"]
|
|
356
|
-
C -->|"useWikiForget()"| G["[Delete Memory]"]
|
|
357
|
-
C -->|"useWikiMaintenance()"| H["[Run Jobs]"]
|
|
358
|
-
C -->|"useOntologyManifest(entityId)"| S["[Read Ontology]"]
|
|
359
|
-
C -->|"useSetOntologyManifest()"| T["[Update Ontology]"]
|
|
360
|
-
D --> I{"entityId, query, wiki,<br/>or ReadOptions changed?"}
|
|
361
|
-
I -->|"Yes"| J["Auto-refetch"]
|
|
362
|
-
I -->|"No"| K["Return cached data"]
|
|
363
|
-
J --> L["Trigger read()"]
|
|
364
|
-
L --> M["Embed query<br/>if embed available"]
|
|
365
|
-
M --> N["Phase 1: Score facts<br/>Phase 2: Fetch winners"]
|
|
366
|
-
N --> O["Update component state"]
|
|
367
|
-
O --> P["Re-render with data"]
|
|
368
|
-
S --> I2{"entityId or wiki changed?"}
|
|
369
|
-
I2 -->|"Yes"| J2["Auto-refetch"]
|
|
370
|
-
I2 -->|"No"| K2["Return cached manifest/mode"]
|
|
371
|
-
J2 --> L2["Trigger getOntologyManifest()"]
|
|
372
|
-
L2 --> O2["Update component state"]
|
|
373
|
-
O2 --> P2["Re-render with manifest/mode"]
|
|
374
|
-
E --> Q["Execute write()"]
|
|
375
|
-
F --> Q
|
|
376
|
-
G --> Q
|
|
377
|
-
H --> Q
|
|
378
|
-
T --> Q
|
|
379
|
-
Q --> R["Write completes"]
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
**Data flow:**
|
|
383
|
-
1. **Wrap app** with `<WikiProvider wiki={wiki}>` — provides wiki context
|
|
384
|
-
2. **Use hooks** in components — access memory reactively
|
|
385
|
-
3. **Read operations** auto-refetch when `entityId`, `query`, `wiki`, or `ReadOptions` values change; call `refetch()` to refresh manually
|
|
386
|
-
4. **Ontology reads** auto-refetch when `entityId` or `wiki` changes; call `refetch()` manually after ontology mutations
|
|
387
|
-
5. **Write operations** (write, ingest, forget, maintenance) do not automatically re-trigger `useMemoryRead`; call `refetch()` after a write to refresh read results
|
|
388
|
-
6. **Ontology writes** (`useSetOntologyManifest`) do not automatically re-trigger `useOntologyManifest` in the same component unless `refetch()` is called after `execute()` succeeds
|
|
389
|
-
7. **Re-render** with new data flowing back to UI
|
|
390
|
-
|
|
391
|
-
## Retrieval Engine Internals
|
|
392
|
-
|
|
393
|
-
```mermaid
|
|
394
|
-
flowchart TD
|
|
395
|
-
A["read(entityId | entityId[], query, options?)"] --> B{hybridWeight = 0?}
|
|
396
|
-
B -->|Yes| C["MiniSearch only<br/>(skip embed)"]
|
|
397
|
-
B -->|No| D{embed available?}
|
|
398
|
-
D -->|No| C
|
|
399
|
-
D -->|Yes| F["Embed query"]
|
|
400
|
-
F -->|throws| E["onRetrievalFallback<br/>callback"]
|
|
401
|
-
E --> C
|
|
402
|
-
F -->|succeeds| G{preFilterLimit<br/>active?}
|
|
403
|
-
G -->|Yes| H["MiniSearch pre-filter<br/>top K candidates"]
|
|
404
|
-
H --> I["Phase 1: Cosine score<br/>top K candidates"]
|
|
405
|
-
G -->|No| J["Phase 1: Cosine score<br/>all facts"]
|
|
406
|
-
J --> K["Cache vectors<br/>in-memory<br/>(full scan only)"]
|
|
407
|
-
K --> L{hybridWeight = 1?}
|
|
408
|
-
I --> L
|
|
409
|
-
L -->|Yes| M["Pure semantic<br/>ranking"]
|
|
410
|
-
L -->|No| N["Hybrid blend:<br/>semantic + keyword<br/>via MiniSearch"]
|
|
411
|
-
M --> O["Phase 2: Fetch full rows<br/>top maxResults"]
|
|
412
|
-
N --> O
|
|
413
|
-
C --> P["MiniSearch ranking"]
|
|
414
|
-
P --> O
|
|
415
|
-
O --> R["Track access"]
|
|
416
|
-
R --> Q["Return MemoryBundle"]
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
The flowchart shows:
|
|
420
|
-
1. **Fast-path** when `hybridWeight = 0` (pure keyword, no embed cost)
|
|
421
|
-
2. **Fallback chain** when embed unavailable (MiniSearch silently) or throws (`onRetrievalFallback` callback, then MiniSearch)
|
|
422
|
-
3. **Pre-filtering** to limit cosine scoring to top-K keyword matches (O(N) → O(K))
|
|
423
|
-
4. **Two-phase SELECT**: phase 1 scores all/filtered facts with minimal columns, phase 2 fetches full rows for winners
|
|
424
|
-
5. **Hybrid scoring** to blend semantic and keyword rankings
|
|
425
|
-
6. **Vector caching** on full scans only; reads with `preFilterLimit` active skip cache population
|
|
426
51
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
`read()` accepts a single entity ID or an array to search across namespaces in one retrieval pass. Pass `tierWeights` to control per-entity ranking before the final top-K results:
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
const memory = await wiki.read(
|
|
433
|
-
['tier_wisdom', 'tier_fact', 'tier_working'],
|
|
434
|
-
'What do I know about this topic?',
|
|
435
|
-
{
|
|
436
|
-
maxResults: 8,
|
|
437
|
-
tierWeights: {
|
|
438
|
-
tier_wisdom: 2, // boost curated notes 2×
|
|
439
|
-
tier_fact: 1, // neutral
|
|
440
|
-
tier_working: 0.25, // downrank unvetted context
|
|
441
|
-
},
|
|
442
|
-
}
|
|
443
|
-
);
|
|
444
|
-
// memory.factScores — Record<factId, weightedScore> | undefined
|
|
445
|
-
// attached for array-shaped reads when the query is non-empty and at least one fact is scored
|
|
446
|
-
// memory.metadata — { query, entityIds, tierWeights }
|
|
447
|
-
// tasks capped at min(20 × entityCount, 200); events at min(10 × entityCount, 100)
|
|
52
|
+
// <WikiProvider wiki={wiki}><App /></WikiProvider>
|
|
448
53
|
```
|
|
449
54
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
## Concurrency
|
|
453
|
-
|
|
454
|
-
All write APIs are safe to call from concurrent async contexts. Transactions are
|
|
455
|
-
serialized internally on the single database connection — you never need to know that
|
|
456
|
-
SQLite forbids nested `BEGIN`, and you never need to throttle callers yourself.
|
|
457
|
-
|
|
458
|
-
- One connection per database file per process is the supported topology.
|
|
459
|
-
- Non-transactional reads are **not** serialized, so read latency is unaffected.
|
|
460
|
-
- Inside a transaction callback, use only the provided `tx` handle — never the outer
|
|
461
|
-
database handle. Using the outer handle deadlocks; opening a nested transaction throws.
|
|
462
|
-
|
|
463
|
-
Driver errors that escape a transaction surface as `WikiTransactionError` (re-exported
|
|
464
|
-
from `@equationalapplications/core-llm-wiki`) with a top-level `sqliteErrorCode`.
|
|
55
|
+
See the [Expo quickstart](https://equationalapplications.github.io/expo-llm-wiki/start/quickstart-expo/) for a complete app, including the `llmProvider`. On React Native, give `embed` an absolute URL.
|
|
465
56
|
|
|
466
57
|
## Monorepo Ecosystem
|
|
467
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equationalapplications/expo-llm-wiki",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.16.0",
|
|
4
4
|
"description": "Local-first LLM memory for Expo and React Native. Combines the core semantic search and extraction engine with expo-sqlite and ready-to-use React hooks.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"bugs": {
|
|
50
50
|
"url": "https://github.com/equationalapplications/expo-llm-wiki/issues"
|
|
51
51
|
},
|
|
52
|
-
"homepage": "https://github.
|
|
52
|
+
"homepage": "https://equationalapplications.github.io/expo-llm-wiki/packages/expo/",
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public",
|
|
55
55
|
"registry": "https://registry.npmjs.org"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@equationalapplications/core-llm-wiki": "7.
|
|
59
|
-
"@equationalapplications/react-llm-wiki": "7.
|
|
58
|
+
"@equationalapplications/core-llm-wiki": "7.16.0",
|
|
59
|
+
"@equationalapplications/react-llm-wiki": "7.16.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"expo-crypto": ">=12",
|