local-llm-context
README.md
# local-llm-context
**Structured SQLite entity/fact/alias memory, CSV/JSON data importer, token-budgeted per-turn context builder, chat-history normalizer, and `stdio` MCP server for small local LLMs (`4B`–`9B`).**
Zero runtime npm dependencies (powered by Node.js `22.5+` built-in `node:sqlite`). Built for **`llama.cpp` (`llama-server`)**, **Ollama**, **LM Studio**, **vLLM**, **Open WebUI**, **Continue**, **Claude Desktop**, and custom local agents.
---
## The Local LLM Data Problem
When you run a `4B`–`9B` local model (`Qwen3.5-4B`, `Llama-3.2-3B`, `Gemma-3-4B`, `Ministral-8B`) over your own structured data, two standard approaches break down:
1. **Prompt-Stuffing Overflows Context:** Pasting a 200-row JSON/CSV of accounts, servers, domains, or projects into the system prompt burns `15k`–`40k` tokens on every turn—slowing down local inference, triggering *"lost in the middle"* hallucinations, or exhausting VRAM.
2. **Vector RAG Breaks Relationships & Aliases:** Chunking tables into embeddings loses exact `parent -> child` hierarchies (*"Which domains belong to Northwind?"*), short aliases (*"NWH"* -> *"Northwind Health Group"*), and exact status counts.
3. **Background Notes Crash Strict Jinja Chat Templates:** Injecting system/event notes or replaying interrupted tool calls creates consecutive `user` roles or orphaned `tool_calls` that either overwrite assistant replies or throw `400` template errors in `llama.cpp` and Ollama.
`local-llm-context` solves all three deterministically in `< 2ms` per turn:
```mermaid
flowchart LR
Data["CSV / JSON / Live Tool Learning"] -->|"Import & strip secrets"| DB[("SQLite ContextStore\nEntities · Aliases · Parents · Facts")]
Prompt["User Message\ne.g. 'Any issues for NWH?'"] -->|"Word-boundary + alias\nmatcher (< 2ms)"| Builder["buildTurnContext()\nStrict char/token budget"]
DB -->|"Matched entity +\nparent + children + facts"| Builder
Builder -->|"~500–3,500 chars\n(only what matters)"| LLM["Local 4B–9B LLM\n(Ollama / llama.cpp)"]
```
---
## Real-World Use Cases
- **IT, Homelab & DevOps Inventory:** Import `hosts.csv`, `containers.json`, and `domains.csv` linked by parent host or account. Ask a local `4B` model *"What runs on vps-west-01?"* or *"Which services under Harborfront are degraded?"* offline.
- **Private Local CRM & Freelancer/Agency Rosters:** Store clients, contacts, aliases, projects, and dated operational facts 100% locally in SQLite—zero cloud API calls, automatic secret-key stripping (`password`, `token`, `api_key`), and instant lookup.
- **Product Catalogs, Vendors & Operations Ledgers:** Map `vendor -> SKU` or `department -> asset` tables from CSV/JSON and let the model query exact attributes or learn new facts during conversation (`entity_learn`, `note_remember`).
- **Air-Gapped Research, Legal & Clinical Notes:** Maintain structured people, organizations, matters, and chronological facts on local hardware with deterministic recall.
---
## Quick Start
### 1. Import Your CSV or JSON Data & Start the MCP Server
```bash
# Import parent accounts from CSV (with aliases and contact facts)
npx local-llm-context import \
--db ./my-data.sqlite \
--file ./accounts.csv \
--kind account \
--name account_name \
--aliases short_code \
--status status \
--facts "primary_contact,renewal_date"
# Import child services/domains from JSON and auto-link them to parent accounts
npx local-llm-context import \
--db ./my-data.sqlite \
--file ./services.json \
--kind service \
--name domain \
--parent account_name \
--parent-kind account \
--status status
# Inspect what exact context block a prompt receives (capped to 3,500 chars)
npx local-llm-context query \
--db ./my-data.sqlite \
--prompt "What services belong to NWH and who is the primary contact?"
```
Expose the SQLite store to any **MCP client** (`Claude Desktop`, `Open WebUI`, `Continue`, `Cursor`, `Zed`, or custom local agents) over `stdio`:
```json
{
"mcpServers": {
"local-context": {
"command": "npx",
"args": ["-y", "local-llm-context", "mcp", "--db", "/absolute/path/to/my-data.sqlite"]
}
}
}
```
Injected MCP tools:
- `store_overview` — Counts of all entities by kind, facts, and notes
- `entity_lookup` — Exact ID or filtered search returning entity, parent, children, attributes, and facts
- `entity_learn` — Upsert entities, aliases, parent links, status, attributes, or durable facts
- `note_remember` / `note_recall` — Key-value operational notes
- `context_for_prompt` — Build a budgeted context block for any prompt string
---
### 2. Use in TypeScript / Node.js with `llama.cpp` or Ollama
```ts
import {
ContextStore,
appendBackgroundNote,
budgetChatHistory,
buildTurnContext,
executeContextTool,
getContextToolSpecs,
importJson,
type ChatTurnMessage,
} from 'local-llm-context';
const store = new ContextStore('./my-data.sqlite');
// 1. Load structured JSON or CSV once (secrets like api_key/password are stripped automatically)
importJson(
store,
[
{ name: 'Northwind Health', code: 'NWH', status: 'active', contact: 'dana@example.org' },
],
{ kind: 'account', nameField: 'name', aliasFields: ['code'], factFields: ['contact'] },
);
// 2. Per turn: inject ONLY the matched entities (+ parents/children/facts) within a strict budget
const userPrompt = 'Who is our contact at NWH?';
const turnContext = buildTurnContext(userPrompt, store, { budgetChars: 3_500 });
let history: ChatTurnMessage[] = [
{ role: 'system', content: ['You are a concise local assistant.', turnContext].filter(Boolean).join('\n\n') },
{ role: 'user', content: userPrompt },
];
// 3. Safely append background/event notes without overwriting assistant replies or breaking Jinja templates
history = appendBackgroundNote(history, 'Backup job completed at 03:00 UTC.', 'Background Event');
// 4. Normalize orphaned tool calls, strip old <think> traces, and compact stale tool payloads before calling Ollama / llama-server
const safeMessages = budgetChatHistory(history, {
maxHistoryChars: 48_000,
staleToolMaxChars: 2_000,
freshToolMaxChars: 16_000,
});
const response = await fetch('http://127.0.0.1:8080/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen-local',
messages: safeMessages,
tools: getContextToolSpecs().map((t) => ({
type: 'function',
function: { name: t.name, description: t.description, parameters: t.parameters },
})),
}),
});
```
---
## Companion Project
Need your small local LLM (`4B`–`9B`) to process **large 100+ row tool outputs across multiple turns** without hitting `max_tokens` output cutoffs? Pair `local-llm-context` with [**`local-llm-relay`**](https://github.com/cynthiaschomp/local-llm-relay).
---
## Development & Tests
Requires **Node.js `>= 22.5.0`**.
```bash
npm install
npm test
```
## License
MIT © Cynthia Schomp
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues