@equationalapplications/expo-llm-wiki
v4.1.0
Published
Expo/React Native adapter for @equationalapplications/core-llm-wiki.
Downloads
1,404
Readme
@equationalapplications/expo-llm-wiki
Expo/React Native adapter for @equationalapplications/core-llm-wiki, powered by expo-sqlite.
Inspired by Andrej Karpathy's LLM Wiki memory spec.
Features
- Expo-ready — Pre-configured for React Native + Expo
- Built on
expo-sqlite— Stable, well-supported SQLite driver - Semantic search — Vector embeddings via
embedfunction, with MiniSearch fallback - Retrieval tuning — Per-call overrides for search behavior (pre-filter, hybrid blend)
- React hooks —
WikiProvider,useMemoryRead, and all other hooks are re-exported directly from@equationalapplications/expo-llm-wiki - Full-featured memory — Facts, tasks, events, maintenance jobs (librarian, heal, reembed, prune)
Installation
npx expo install expo-sqlite
npm install @equationalapplications/expo-llm-wikiSemantic Search
Enable vector-based retrieval by providing an embed function:
import { createWiki } from '@equationalapplications/expo-llm-wiki';
import { openDatabaseSync } from 'expo-sqlite';
const db = openDatabaseSync('wiki.db');
const wiki = createWiki(db, {
config: {
// Optimize retrieval for large memory stores
preFilterLimit: 50, // Limit cosine scoring to top-50 keyword matches
hybridWeight: 0.7, // Blend semantic (0.7) + keyword (0.3)
},
llmProvider: {
generateText: async ({ systemPrompt, userPrompt }) => {
// Your LLM call — must return the model output as a string
return 'Model output';
},
embed: async (text: string) => {
// Your embedding service (e.g., OpenAI, Cohere)
// Use an absolute URL — React Native / Expo apps do not have a browser
// origin to resolve relative URLs against on device or simulator.
const response = await fetch('https://your-api.example.com/api/embed', {
method: 'POST',
body: JSON.stringify({ text })
});
const { embedding } = await response.json();
return embedding; // number[]
},
},
onRetrievalFallback: (error) => {
console.warn('Embedding unavailable, using keyword search:', error);
},
});
await wiki.setup();
// Semantic query
const memory = await wiki.read('user-123', 'what activities should I do this weekend?');
// Matches facts like "Saturday hiking trip" even with no lexical overlap
// Per-call overrides
const fasterSearch = await wiki.read('user-123', 'activities', {
maxResults: 5,
preFilterLimit: 20, // Tighter pre-filter for speed
hybridWeight: 0.5, // More keyword weight
});Configuration
All WikiConfig fields are optional:
const wiki = createWiki(db, {
llmProvider: { /* ... */ },
config: {
tablePrefix: 'llm_wiki_', // default: 'llm_wiki_'
maxResults: 10, // default: 10
autoLibrarianThreshold: 20, // default: 20 — events before librarian auto-runs
autoHealThreshold: 100, // default: 100 — events before heal auto-runs
maxChunkLength: 12000, // default: 12000 (char count per ingestDocument chunk)
chunkOverlap: 400, // default: 400 (overlap between chunks in characters)
chunkConcurrency: 1, // default: 1 (parallel LLM calls per ingestDocument)
pruneRetainSoftDeletedFor: 7, // default: 7 (days before hard-deleting soft-deleted facts)
pruneEventsAfter: 30, // default: 30 (days before hard-deleting old events)
orphanAfterDays: 30, // default: 30 (days before runHeal flags sourceless facts; null to disable)
staleInferredAfterDays: 60, // default: 60 (days before runHeal downgrades inferred facts; null to disable)
preFilterLimit: 50, // default: undefined — MiniSearch pre-filter before cosine scan; recommended for >500 facts
hybridWeight: 0.7, // default: undefined — blend semantic (1.0) ↔ keyword (0.0); pure semantic when unset
},
});Retrieval Tuning
Optimize read() performance and blend retrieval strategies:
const config = {
// Limit cosine similarity scoring to top-K MiniSearch keyword candidates
preFilterLimit: 50,
// Blend semantic and keyword scores (0.0 = pure keyword, 1.0 = pure semantic)
hybridWeight: 0.7,
// Max results returned per read
maxResults: 10,
};
const wiki = createWiki(db, {
config,
llmProvider: { /* ... */ },
});Hybrid scoring blends:
hybridWeight: 1.0→ pure semantic ranking among the candidates being scored; ifpreFilterLimitis set, semantic scoring is still limited to the top-K MiniSearch matcheshybridWeight: 0.5→ balanced semantic + keyword (50/50 blend)hybridWeight: 0.0→ pure keyword ranking, skipsembed()entirely (no LLM API cost)
Pre-filtering optimization:
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).
Usage
import { createWiki } from '@equationalapplications/expo-llm-wiki';
import { openDatabaseSync } from 'expo-sqlite';
const db = openDatabaseSync('wiki.db');
const wiki = createWiki(db, {
llmProvider: {
generateText: async ({ systemPrompt, userPrompt }) => {
// Your LLM call — must return the model output as a string
return 'Model output';
},
},
});
// Initialize tables (call once on app startup)
await wiki.setup();
// Use wiki instance
await wiki.write('user-123', { event_type: 'observation', summary: '...' });With React
@equationalapplications/expo-llm-wiki re-exports all hooks and WikiProvider from @equationalapplications/react-llm-wiki:
import { WikiProvider } from '@equationalapplications/expo-llm-wiki';
<WikiProvider wiki={wiki}>
<MyApp />
</WikiProvider>Then use hooks in components:
import { useMemoryRead } from '@equationalapplications/expo-llm-wiki';
export function UserProfile({ userId }: { userId: string }) {
const { data, isPending } = useMemoryRead(userId, 'preferences');
if (isPending) return <Text>Loading...</Text>;
return <Text>{data?.facts.map(f => f.title).join(', ')}</Text>;
}Component Lifecycle
flowchart TD
A["<WikiProvider wiki={wiki}>"] --> B["App Components"]
B --> C{"Use Hook?"}
C -->|"useMemoryRead(entityId, query, options?)"| D["[Read Memory]"]
C -->|"useWikiWrite()"| E["[Write Memory]"]
C -->|"useWikiIngest()"| F["[Ingest Document]"]
C -->|"useWikiForget()"| G["[Delete Memory]"]
C -->|"useWikiMaintenance()"| H["[Run Jobs]"]
D --> I{"entityId, query, wiki,<br/>or ReadOptions changed?"}
I -->|"Yes"| J["Auto-refetch"]
I -->|"No"| K["Return cached data"]
J --> L["Trigger read()"]
L --> M["Embed query<br/>if embed available"]
M --> N["Phase 1: Score facts<br/>Phase 2: Fetch winners"]
N --> O["Update component state"]
O --> P["Re-render with data"]
E --> Q["Execute write()"]
F --> Q
G --> Q
H --> Q
Q --> R["Write completes"]Data flow:
- Wrap app with
<WikiProvider wiki={wiki}>— provides wiki context - Use hooks in components — access memory reactively
- Read operations auto-refetch when
entityId,query,wiki, orReadOptionsvalues change; callrefetch()to refresh manually - Write operations (write, ingest, forget, maintenance) do not automatically re-trigger
useMemoryRead; callrefetch()after a write to refresh read results - Re-render with new data flowing back to UI
Retrieval Engine Internals
flowchart TD
A["read(entityId, query)"] --> B{hybridWeight = 0?}
B -->|Yes| C["MiniSearch only<br/>(skip embed)"]
B -->|No| D{embed available?}
D -->|No| C
D -->|Yes| F["Embed query"]
F -->|throws| E["onRetrievalFallback<br/>callback"]
E --> C
F -->|succeeds| G{preFilterLimit<br/>active?}
G -->|Yes| H["MiniSearch pre-filter<br/>top K candidates"]
H --> I["Phase 1: Cosine score<br/>top K candidates"]
G -->|No| J["Phase 1: Cosine score<br/>all facts"]
J --> K["Cache vectors<br/>in-memory<br/>(full scan only)"]
K --> L{hybridWeight = 1?}
I --> L
L -->|Yes| M["Pure semantic<br/>ranking"]
L -->|No| N["Hybrid blend:<br/>semantic + keyword<br/>via MiniSearch"]
M --> O["Phase 2: Fetch full rows<br/>top maxResults"]
N --> O
C --> P["MiniSearch ranking"]
P --> O
O --> R["Track access"]
R --> Q["Return MemoryBundle"]The flowchart shows:
- Fast-path when
hybridWeight = 0(pure keyword, no embed cost) - Fallback chain when embed unavailable (MiniSearch silently) or throws (
onRetrievalFallbackcallback, then MiniSearch) - Pre-filtering to limit cosine scoring to top-K keyword matches (O(N) → O(K))
- Two-phase SELECT: phase 1 scores all/filtered facts with minimal columns, phase 2 fetches full rows for winners
- Hybrid scoring to blend semantic and keyword rankings
- Vector caching on full scans only; reads with
preFilterLimitactive skip cache population
License
MIT
Made with ❤️ by Equational Applications LLC. https://equationalapplications.com/
