llm-wiki
A self-healing local knowledge base where a local LLM compounds your documents across four memory tiers — working, episodic, semantic, and procedural — with bi-temporal facts, automatic contradiction resolution, and scheduled memory maintenance.
llm-wiki is a FastAPI service that turns a folder of raw documents into a
continuously self-organising Markdown wiki. Drop PDFs, DOCX, PPTX, XLSX,
HTML, or Markdown into the ingest endpoint and the system extracts entities,
claims, and relations; writes confidence-scored pages; and keeps them honest
over time through bi-temporal fact tracking, Ebbinghaus decay, and weekly
self-lint runs.
The wiki conforms to Google's Open Knowledge Format (OKF) v0.1 — every page carries typed YAML frontmatter and bundle-relative markdown links, so the whole knowledge base can be exported as a portable OKF bundle (and external OKF bundles import directly as curated, high-trust pages).
The schema is fully described in CLAUDE.md and the agent
tool catalogue in AGENTS.md.
Architecture at a glance
flowchart LR
subgraph IN["Input"]
DOCS["Raw documents<br/>PDF · DOCX · PPTX · XLSX · HTML · MD"]
OKFIN["External OKF bundles<br/>(curated, no LLM pass)"]
end
subgraph ING["Ingest pipeline"]
direction TB
REDACT["Privacy redaction"] --> PLAN["Agentic chunk plan"]
PLAN --> SUMM["Summarise + extract entities/claims"]
SUMM --> CONF["Merge + confidence gate"]
CONF --> D2Q["Doc2Query questions"]
D2Q --> AUTO["Review Autopilot<br/>staged pages verified vs source"]
end
subgraph STORE["Knowledge store"]
direction TB
WIKI["Markdown wiki = OKF bundle<br/>sources · entities · procedures · episodic"]
KG["Knowledge graph<br/>bi-temporal facts, SQLite"]
IDX["Indexes<br/>BM25 + dense sub-chunks + hq units"]
end
subgraph QRY["Query pipeline"]
direction TB
CACHE{"Semantic answer cache<br/>cosine ≥ 0.95? (opt-in)"}
CACHE -- "miss" --> ORCH["Agentic orchestrator"]
ORCH --> RET["Hybrid retrieval<br/>RRF · rerank · small-to-big · MMR"]
RET --> SYNTH["Synthesis + citations<br/>+ NLI claim verification"]
end
SCHED["Scheduler<br/>decay · promote · review autopilot<br/>lint · procedures · topics"]
DOCS --> ING
OKFIN --> WIKI
ING --> STORE
STORE --> QRY
CACHE -- "hit" --> ANS["Cited answer"]
SYNTH --> ANS
ANS -- "save-back if conf ≥ 0.80<br/>(after NLI verification)" --> WIKI
ANS -. "cache confident answers" .-> CACHE
SCHED --> STORE
WIKI -- "export" --> OKFOUT["Shareable OKF bundle"]
Every LLM call is role-based and provider-agnostic — local Ollama by default, or any hosted model with one env var (see Bring your own model):
flowchart LR
ROLE["LLM role<br/>summary / reason / fast / solver / embed / vision"]
ROLE --> Q{"PROVIDER_ROLE set<br/>+ key + model present?"}
Q -- "no (default)" --> OLL["Ollama - local models"]
Q -- "yes" --> P["OpenAI-compatible provider<br/>Groq / GitHub / Gemini / OpenAI /<br/>Claude / Grok / OpenRouter / custom"]
P -- "HTTP error" --> OLL
Why this exists
Most "chat-with-your-docs" stacks throw documents into a vector store and
walk away. After three months they are full of stale claims, duplicate
entities, and dangling references. llm-wiki treats the knowledge base as a
living artefact that has to be maintained — it promotes recurring ideas,
decays unreinforced ones, supersedes facts when newer sources contradict
older ones, and crystallises repeated query patterns into reusable
procedures.
The four memory tiers
| Tier | Where | Lifetime | Contents |
|---|---|---|---|
| Working | in-process state | one request | retrieved candidates, draft answer |
| Episodic | wiki/episodic/<date>.md |
14 days (config) | every ingest / query / lint event with correlation IDs |
| Semantic | wiki/sources/, wiki/entities/ |
indefinite, decays | consolidated pages, auto-generated entity pages |
| Procedural | wiki/procedures/ + procedures.db |
indefinite | recurring query patterns crystallised into procedures |
Promotion rules:
- A query becomes part of episodic on every successful answer.
- A topic that recurs ≥ 3 times across ≥ 14 days of episodic auto-promotes
to semantic via the daily
promote_episodic_to_semanticjob (04:00 UTC). - A query pattern recurring ≥ 5 times with a similar retrieval set becomes a
procedure via the weekly
detect_proceduresjob (Sun 06:00 UTC). - A high-confidence answer (≥ 0.80, ≥ 2 citations) is saved back as
wiki/sources/synthesis-<slug>.mdimmediately at query time.
The ingest pipeline
file (PDF/DOCX/PPTX/XLSX/HTML/MD/TXT)
│
▼
load_elements ← multi-format loaders, structure-aware
│
▼
privacy redaction ← strip API keys / JWTs / private keys / passwords
│
▼
agentic ingest plan ← adaptive chunk size/overlap by content density
│
▼
layout_aware_chunks ← atomic tables / images, semantic blocks
│
▼
summarise per chunk (summary) ← summary-role model extracts entities + relations
│
▼
merge (reason role, 3-tier) ← reasoning-role model consolidates + scores conf
│
▼
extraction-signal floor ← rich text → confidence bump
│
▼
contextual preamble (Anthropic) ← short doc context attached to chunks
│
▼
Doc2Query ← the questions this doc answers, indexed as <pid>#hq
│
▼
confidence gate
│
├─ ≥ 0.60 → wiki/sources/<slug>.md
└─ < 0.60 → wiki/review/<slug>.md (awaits human accept)
│
▼
upsert entities + relations into graph.db
│
▼
extract S-P-O claims (reason role) → add_fact(valid_from=today)
│
▼
contradiction detection vs related pages
│
├─ concrete contradiction → supersede_fact() on older page
└─ composite-score auto-resolver (margin ≥ 0.2) → keep winner, mark loser
│
▼
reconciler — edits pre-existing pages that overlap on ≥ 2 entities
│ refines, contradicts, or adds context; staged in wiki/review/edits/
│
▼
media nodes (multimodal graph) ← tables/images/code/formulas → graph + own embeddings
│
▼
rebuild_index + rebuild_entity_pages
│
▼
episodic_log_entry (correlation_id)
Every step is logged in JSON to logs/app.log; security-relevant events
(writes, accepts/rejects, contradictions, supersessions) also go to
logs/audit.log.
Review & the Autopilot
Pages scoring below the confidence gate (0.60) land in wiki/review/ instead of
going live — the ingest-time score is the model's self-assessment and errs
cautious. The Review Autopilot then closes the loop automatically with a
strictly stronger verification:
flowchart TD
STAGED["Staged page<br/>(below 0.60 gate)"] --> SRC{"Original source<br/>re-readable?"}
SRC -- no --> HUMAN["Left for human<br/>GET /review → accept / reject"]
SRC -- yes --> JUDGE["Evidence-grounded judge<br/>page vs source → faithfulness + coverage"]
JUDGE --> GROUND["+ deterministic entity-grounding<br/>composite = 0.7·judge + 0.3·grounding"]
GROUND --> BORDER{"near a<br/>threshold?"}
BORDER -- yes --> SECOND["Second judge vote<br/>(reason role) → average"]
BORDER -- no --> DECIDE{"composite"}
SECOND --> DECIDE
DECIDE -- "≥ 0.70" --> PROMOTE
DECIDE -- "≤ 0.30" --> ARCHIVE["Auto-archive<br/>→ wiki/archive/ (reversible)"]
DECIDE -- "0.30–0.70" --> ANNOTATE["Stay in review<br/>annotated with scores + reasons"]
HUMAN -- "accept" --> PROMOTE
HUMAN -- "reject" --> ARCHIVE
subgraph PROM["promote_review_page (shared)"]
direction TB
PROMOTE["Move review/ → sources/"] --> PURGE["Purge stale review-id units<br/>parent · #hq · #n · #media"]
PURGE --> REIDX["Re-index under new id<br/>small-to-big + preamble + Doc2Query"]
REIDX --> REASSIGN["reassign_page_id in graph<br/>facts · entities · relations · media"]
end
Both accept paths — the Autopilot and the human POST /review/{id}/accept — funnel
through the shared promote_review_page helper, so a promoted page is indexed
exactly like a freshly-ingested one (small-to-big sub-chunks, contextual preamble,
Doc2Query #hq, dense metadata) and its knowledge-graph rows follow it to the new id
instead of dangling at the old review/* path. Rejection archives (reversible),
never hard-deletes.
- an LLM judge re-reads the staged page against the original source document and scores faithfulness + coverage (evidence-grounded, not self-assessed);
- a deterministic cross-check measures how many of the page's extracted entities literally appear in the source (composite = 0.7 × judge + 0.3 × grounding);
- borderline composites get a second judge vote from the reason role (a different model when your roles are split) and the votes average;
- decision: ≥ 0.70 auto-accept (moved to
sources/, indexed, audit-logged), ≤ 0.30 auto-archive (moved towiki/archive/— reversible, never deleted), in between → stays in review, annotated with the judge's scores + reasons (visible viaGET /reviewand in the page frontmatter asauto_review).
It runs inline right after ingest for each staged page, daily at 04:30 UTC for
the backlog, and on demand via POST /admin/run/review_autopilot. Pages whose
source can't be re-read (deleted files, machine-generated pages) are always left
for a human. Knobs: REVIEW_AUTOPILOT_ENABLED, REVIEW_ACCEPT_THRESHOLD,
REVIEW_REJECT_THRESHOLD, REVIEW_SECOND_OPINION.
For the (now rare) pages left in review: GET /review lists them with the
judge's annotation, then POST /review/{id}/accept or /reject — or use the
dashboard at /dashboard.
The query pipeline
flowchart TD
Q["User question"] --> CACHE{"Semantic answer cache<br/>(opt-in) — cosine ≥ threshold?"}
CACHE -- "hit" --> DONE["Return stored answer<br/>cached=true"]
CACHE -- "miss" --> PROC{"Crystallized<br/>procedure match?"}
PROC -- "yes" --> ANCHOR["Recall anchor pages directly"]
PROC -- "no" --> RET["Decompose · multi-query · HyDE<br/>→ hybrid retrieval → CRAG filter"]
ANCHOR --> SYNTH
RET --> SYNTH["Adaptive routing → synthesis<br/>numbered citations + per-claim conf"]
SYNTH --> VERIFY["Grounding check · CRAG ceiling<br/>NLI-lite claim verification (recalibrates conf)"]
VERIFY --> SAVE{"conf ≥ 0.80 ∧ ≥ 2 cits?<br/>(evaluated AFTER verification)"}
SAVE -- "yes" --> BACK["Save-back → wiki/sources/"]
SAVE -- "no" --> OUT
BACK --> OUT["Cited answer + episodic log<br/>+ populate answer cache"]
The linear detail:
user question
│
▼
semantic answer cache (opt-in) ← embed Q; cosine ≥ threshold vs recent answers → return cached
│ (complements the EXACT-hash procedural recall)
▼
intent classifier ← factual / multi_hop / synthesis / exhaustive
│
▼
decompose (if compound)
│
▼
multi-query paraphrase ← RAG-Fusion: N rewrites
HyDE seed for dense retrieval ← LLM hallucinates a hypothetical doc
│
▼
hybrid retrieval
├─ BM25 over wiki/sources/ (sub-chunks + #hq question units)
├─ dense over Chroma (or numpy fallback)
├─ RRF fuse
├─ FlashRank cross-encoder rerank (graceful passthrough if not installed)
├─ small-to-big: rerank the MATCHED sub-chunks ± neighbours, not page[:4000]
├─ machine-page down-weight ×0.85 (anti-feedback-loop for save-backs)
├─ graph 2-hop expansion via entity links
└─ MMR diversification
│
▼
mark_accessed() on every retrieved page → reinforces lifecycle counter
│
▼
CRAG relevance filter ← drop off-topic candidates
│
▼
adaptive model routing ← quantitative Q → solver reasons, reasoner formats
│
▼
multimodal expansion ← surface tables/figures linked to retrieved entities
│
▼
lost-in-the-middle reorder ← ends-load context: best page first, runner-up last
│
▼
synthesis
├─ numbered citations
├─ [Page]^conf markers per claim
└─ structured answer blocks
│
▼
grounding check + CRAG ceiling ← detect ungrounded statements
│
▼
NLI-lite claim verification ← ONE batched judge call per answer; unsupported
│ claims drag per-claim + overall confidence down
▼
reflection critique → optional refinement
│
▼
record_query_pattern() in procedural store
│
▼
save-back if confidence ≥ 0.80 ∧ citations ≥ 2 ← evaluated AFTER NLI verification, so a
│ page is never persisted with an inflated score
▼
populate semantic answer cache (grounded ∧ conf ≥ min) [QUERY_ANSWER_CACHE, off by default]
│
▼
episodic_log_entry
2026 adaptive upgrades
Beyond the base pipeline, the system adapts to what kind of content and question it is handling. Each upgrade is flag-gated and degrades gracefully when its model isn't installed.
| Upgrade | What it does | Flag (default) |
|---|---|---|
| Adaptive model routing | Quantitative questions (maths, economics, science, engineering) are reasoned by VibeThinker — a maths/STEM specialist — then the reasoning-role model formats + cites the result. Plain-English questions skip it. | ROUTE_SOLVER_ENABLED (on; self-disables if MODEL_SOLVER not served) |
| Domain detection + tagging | Every page and query is classified general / math / science / economics / engineering, driving routing and retrieval. | always on |
| Agentic retrieval | The /query front door auto-routes simple questions to a fast single pass and complex ones to an iterative plan → retrieve → sufficiency-check → gap-rewrite loop. |
AGENTIC_ENABLED (on) |
| Agentic ingestion | Per-document adaptive chunk sizing — dense technical content gets smaller chunks, narrative prose larger. | INGEST_AGENTIC_PLANNING (on) |
| Privacy redaction | Strips API keys, JWTs, private keys, and passwords from raw sources before ingest; audit-logged as PRIVACY_REDACT. |
INGEST_REDACT_SECRETS (on) |
| STEM embeddings | A stronger, notation-aware embedder (bge-m3) in a separate dense collection for quantitative content; routed by domain. |
EMBED_STEM_ENABLED (off) |
| Multimodal graph | Tables/images/code/formulas become first-class graph nodes linked to entities and embedded as their own units; retrieval surfaces media linked to the entities in play. | GRAPH_MULTIMODAL_NODES (off) |
| Semantic answer cache | Embeds each question and short-circuits near-duplicates (cosine ≥ threshold) with a stored answer — a fuzzy layer above the procedural store's EXACT pattern hash. Only grounded, confident answers are cached; entries carry a TTL + count cap. A staleness guard re-answers instead of serving a hit whose cited pages have since been archived/rejected/superseded, so a loosely-tuned threshold can never surface an answer built on pages that are gone. | QUERY_ANSWER_CACHE (off) |
See CLAUDE.md for the schema details and
docs/design/multimodal-graph.md for the
multimodal-graph rollout.
Governance & learning
Two capabilities keep the corpus correct and let it learn — the wiki isn't just retrieved from, it's governed and improved over time.
Profile / schema contract (runtime-enforced)
The wiki's conventions — allowed kinds, required frontmatter, valid domain/type
values, confidence bounds, the entity/relation vocabularies — are a declarative
contract validated at the page write surface, not just prose in CLAUDE.md. A
malformed page is caught deterministically instead of drifting in.
- Modes (
PROFILE_ENFORCEMENT):off·warn(default — logs + audits, still writes) ·strict(raisesProfileViolationso ingest routes the page to review). - The built-in
DEFAULT_PROFILEmatches the current schema exactly (nothing the pipeline already produces is rejected); override any subset of keys with aPROFILE_PATHJSON file. GET /profileshows the active contract;POST /admin/profile/validateaudits the whole live corpus for drift without rewriting anything.
Feedback curator (corrections → memory)
The episodic tier logs what happened; the feedback curator captures what the user
corrected or preferred and turns it into durable memory. Submit feedback on an
answer via POST /feedback; it is classified (a cheap heuristic drops generic acks
like "thanks" with no LLM call, else one reason-role call):
| Kind | On promotion |
|---|---|
| correction | writes a curated high-confidence sources/feedback-*.md page, indexed like any source |
| preference | becomes an active preference injected into every future synthesis prompt ("honour these") |
| approval | reinforces the cited page's lifecycle access counter |
| rejection | recorded and flagged for review |
| noise | dropped |
Promotion is explicit by default (GET /feedback → POST /feedback/{id}/promote or
/dismiss); set FEEDBACK_AUTO_PROMOTE=true to auto-apply high-signal
corrections/preferences at capture time. Validated live against hosted models — all
five categories classified correctly, actionable content extracted cleanly.
Best-of-best RAG package (v5)
Six further techniques, each flag-gated and on by default:
| Technique | What it does | Flag (default) |
|---|---|---|
| Small-to-big retrieval | Reranks/synthesises the 1500-char sub-chunks that actually matched (± neighbours), re-derived exactly as indexed — instead of the first 4000 chars of the page. Fixes relevant content beyond the prefix never reaching the LLM. | QUERY_CHUNK_CONTEXT (on) |
| Doc2Query (Nogueira & Lin) | At ingest, generates the questions each document answers and indexes them as <pid>#hq, so question-phrased queries match declarative text. |
INGEST_DOC2QUERY (on) |
| Lost-in-the-middle reorder (Liu et al. 2023) | Ends-loads the synthesis context — best page first, runner-up last — to counter positional attention decay. | QUERY_LITM_REORDER (on) |
| NLI-lite claim verification | One batched judge call checks every cited claim sentence against its cited snippet; unsupported claims get ×0.35 confidence. Catches "right page, wrong claim". | QUERY_CLAIM_VERIFY (on) |
| Machine-page down-weight | Synthesis/promoted/crystallized pages score ×0.85 at rerank so save-backs never outrank the primary sources they came from. | RETRIEVAL_SYNTH_DOWNWEIGHT (0.85) |
| RAPTOR-lite topics (Sarthi et al. 2024) | Weekly clustering of live pages into topic-*.md overview pages, so corpus-level questions ("main themes across my documents?") have a retrievable answer. |
JOB_BUILD_TOPICS_ENABLED (on) |
Pages ingested before v5 need a one-off backfill for the #hq units and topics:
python scripts/backfill_v5.py # uses configured models
python scripts/backfill_v5.py --summary-model llama3.2:latest --reason-model llama3.2:latest
Evaluation harness
With ~16 stacked techniques, measure what actually pays for its latency on your corpus:
python scripts/gen_golden.py --n 15 # LLM-generate golden Q/page pairs → eval/golden.jsonl
python scripts/run_eval.py # retrieval baseline: recall@k, MRR, hit-rate, latency
python scripts/run_eval.py --ablate # + one-flag-off variants (chunk-context, MMR, down-weight, graph)
python scripts/run_eval.py --answers # + full answer eval: keyword coverage, groundedness, confidence
Retrieval eval needs only the embedding model (cheap; run per ablation).
Answer eval runs the full pipeline per question. Results land in
eval/results-<label>.json. Hand-edit eval/golden.jsonl freely — an
LLM-generated golden set inherits its generator's blind spots.
Validation — tested end-to-end on hosted models
The pipeline was exercised end-to-end against NVIDIA build.nvidia.com hosted
models (fully local-GPU-free), routed through the provider fleet — text via
meta/llama-3.1-8b-instruct, embeddings via nvidia/nv-embedqa-e5-v5 (1024-dim).
Every stage ran through a real model, not a mock.
What was tested and the numbers that came back:
| Stage tested | Result |
|---|---|
| Provider connectivity (chat + embed) | chat ≈ 0.8 s/call · embed ≈ 0.3 s/call · /models auth ✓ |
| Ingest — 3 docs (RAG, vector DBs, Transformers) | all went live at confidence 0.95; entities + relations + Doc2Query extracted |
| Query retrieval | correct source page retrieved for every question |
| Synthesis factual accuracy | facts correct (RAG → Patrick Lewis / Facebook AI / 2020; Transformer → Vaswani / Google / 2017; FAISS/HNSW for vector DBs) |
| Eval harness — recall@5 / MRR / hit-rate | 1.000 / 1.000 / 1.000 across baseline + 4 ablations (chunk-context, MMR, down-weight, graph); ≈ 3–5 s/variant |
| Answer-cache precision @ threshold 0.80 | paraphrase → cache hit, unrelated question → miss (correct both ways) |
| Unit + integration suite | 145 passed, ruff-clean |
Answer-cache threshold, calibrated on real nv-embedqa cosines (replacing the
conservative 0.95 guess — cosine scale is embedder-specific):
| Question pair | Cosine |
|---|---|
| "What is RAG and who introduced it?" ~ "Who created RAG and what is it?" | 0.928 |
| "What are the two components of RAG?" ~ "What are RAG's main parts?" | 0.857 |
| loose reformulation (hallucination wording) | 0.589 |
| RAG question vs "How do transformers use attention?" | 0.201 |
| RAG question vs "capital of France?" / "quicksort complexity?" | ≈ 0.19–0.20 |
Paraphrases cluster at 0.86–0.93, unrelated questions at ≈ 0.20 — a wide safe
gap, so ANSWER_CACHE_SIM_THRESHOLD ≈ 0.80 is a high-precision cut for this
embedder (validated live: it hit paraphrases and rejected unrelated questions).
Honest caveats (what these numbers do and don't prove):
- The eval corpus was 3 very distinct docs, so retrieval is trivial and every ablation scores a perfect 1.000 — this proves the harness is push-button and correct, not that any single technique lifts recall. Differentiating the techniques needs a larger, more confusable corpus.
meta/llama-3.1-8b-instructis fast but doesn't reliably emit the[Title]^0.NNcitation markers, sogroundedis penalized (the answer content is still correct). For production-grade citation/grounding, use a larger reasoner (meta/llama-3.3-70b-instructor aqwentier) at the cost of latency.- Live testing also surfaced a real robustness fix: hosted models emit JSON with
literal newlines inside strings (from
##markdown), which strictjson.loadsrejected — hardened tostrict=Falseacross all 20 LLM-output parse sites, which lifted a query answer from an unparsed blob (confidence 0.2) to clean markdown (confidence 0.75). This is exactly the class of bug that only appears under a real model.
Reproduce with the NVIDIA block in .env.example (PROVIDER_*=nvidia + NVIDIA_API_KEY),
then the ingest / query / eval commands above.
OKF bundles — import & export
The wiki is an OKF bundle. Two scripts make that portable:
# Export the stable tiers (sources/entities/procedures + index.md + log.md)
# as a standalone, validated OKF bundle — share as a git repo or archive:
python scripts/export_okf.py dist/my-wiki-bundle
# Import someone else's OKF bundle as curated pages — no LLM pipeline, pages
# copy 1:1 with provenance stamped, links become RELATES_TO graph edges:
python scripts/import_okf.py path/to/their-bundle
python scripts/import_okf.py path/to/their-bundle --validate-only # conformance check
# Re-stamp pages written before OKF conformance (idempotent):
python scripts/migrate_okf.py
Confidence and decay
- Stored confidence is what the LLM assigned at ingest time; reads do not mutate it.
- Effective confidence = stored ×
exp(-Δdays / half_life_days), floored at 0.05. Default half-life is 90 days. - Reinforcement triggers when a page is accessed ≥ 3 times within a 14-day window; the reinforcement timestamp resets the decay clock.
- The decay sweep (daily 03:00 UTC) rewrites stored confidence based on
last_reinforced.
Bi-temporal facts carry ingested_at, optional valid_from, optional
valid_to, superseded_by, last_reinforced, and access_count. A new
source can never delete an old fact — only mark it superseded by setting
valid_to = today and superseded_by = <new_fact_id>.
Three triggers can supersede:
- Reconciler auto-apply when
action ∈ {refine, contradict}lands and the old text matches. - Contradiction detector when
_detect_contradictionsreturns a concrete claim excerpt. - Auto-resolver (Phase E2) when the composite-score margin ≥ 0.2;
sub-margin cases stay surfaced in
GET /admin/contradictionsfor human review.
Scheduled jobs
In-process APScheduler runs the following by default (each toggleable via env
var). Manual one-off runs available via POST /admin/run/{job_name}.
| UTC time | Job | Toggle env var |
|---|---|---|
| daily 03:00 | decay_sweep |
JOB_DECAY_SWEEP_ENABLED |
| daily 03:30 | episodic_prune |
JOB_EPISODIC_PRUNE_ENABLED |
| daily 04:00 | promote_episodic |
JOB_PROMOTE_EPISODIC_ENABLED |
| daily 04:30 | review_autopilot |
JOB_REVIEW_AUTOPILOT_ENABLED |
| weekly Sun 05:00 | lint_autofix |
JOB_LINT_AUTOFIX_ENABLED |
| weekly Sun 06:00 | detect_procedures |
JOB_DETECT_PROCEDURES_ENABLED |
| weekly Sun 07:00 | page_compaction |
JOB_PAGE_COMPACTION_ENABLED |
| weekly Sun 07:30 | build_topics |
JOB_BUILD_TOPICS_ENABLED |
Models
| Role | Model | Notes |
|---|---|---|
| Summarise, extract | gemma4:e4b |
Fast, strong instruction-following |
| Reason, route, lint, claims | qwen3:14b |
Deep reasoning, thinking mode |
| Quantitative specialist | vibethinker:3b |
AIME-class maths/STEM; routed to adaptively |
| Embeddings | nomic-embed-text:latest |
274 MB, MTEB-strong |
| STEM embeddings (optional) | bge-m3 |
Notation-aware; EMBED_STEM_ENABLED |
| Vision (image captions) | llava:7b |
Optional, when ingest_caption_images |
Served via Ollama at OLLAMA_HOST (default http://localhost:11434) by default.
Bring your own model — the provider fleet
Every LLM role can be pointed at any hosted or local provider that speaks the
OpenAI wire format. Clone the repo, copy .env.example to .env, set
PROVIDER_<ROLE> + that provider's key, run — good to go. A role falls back to
Ollama automatically when its key/model is missing, and a provider HTTP error
degrades to the existing Ollama role fallback, so misconfiguration never breaks
the pipeline. Keys live only in your local .env (gitignored) — never commit them.
| Provider | PROVIDER_<ROLE>= |
Key env var | Default model | Embeddings? |
|---|---|---|---|---|
| Ollama (default) | ollama |
— (local) | qwen3:14b / gemma4:e4b |
✅ nomic-embed-text |
| Groq | groq |
GROQ_API_KEY |
llama-3.3-70b-versatile |
— |
| GitHub Models | github |
GITHUB_MODELS_TOKEN |
openai/gpt-4.1-mini |
— |
| Google Gemini | gemini |
GOOGLE_GENAI_API_KEY |
gemini-2.5-flash-lite |
✅ text-embedding-004 |
| OpenAI | openai |
OPENAI_API_KEY |
gpt-4.1-mini |
✅ text-embedding-3-small |
| Anthropic Claude | anthropic |
ANTHROPIC_API_KEY |
claude-sonnet-5 |
— |
| xAI Grok | xai |
XAI_API_KEY |
grok-4 |
— |
| OpenRouter | openrouter |
OPENROUTER_API_KEY |
meta-llama/llama-3.3-70b-instruct (100+ OSS models, one key) |
— |
| Custom / self-hosted | custom |
CUSTOM_API_KEY (optional) |
CUSTOM_MODEL @ CUSTOM_BASE_URL |
✅ CUSTOM_EMBED_MODEL |
custom covers any OpenAI-compatible gateway: vLLM (http://localhost:8001/v1),
LM Studio (http://localhost:1234/v1), llama.cpp server, Together, Fireworks,
DeepSeek, Mistral La Plateforme, … — no code changes, no API key needed for local
gateways.
# Example mixed fleets (set in .env):
# Claude reasons, Groq handles the fast path, everything else local:
PROVIDER_REASON=anthropic ANTHROPIC_API_KEY=sk-ant-...
PROVIDER_FAST=groq GROQ_API_KEY=gsk_...
# Fully hosted, zero local GPU:
PROVIDER_REASON=openai OPENAI_API_KEY=sk-...
PROVIDER_SUMMARY=gemini GOOGLE_GENAI_API_KEY=...
PROVIDER_EMBED=openai
PROVIDER_FAST=xai XAI_API_KEY=xai-...
# Your own vLLM box serving an open-source model:
PROVIDER_REASON=custom CUSTOM_BASE_URL=http://localhost:8001/v1 CUSTOM_MODEL=qwen2.5-72b-instruct
Roles: PROVIDER_SUMMARY (summarise/extract), PROVIDER_REASON (synthesis / deep
reasoning), PROVIDER_FAST (fast-agent), PROVIDER_SOLVER (quantitative
specialist), PROVIDER_EMBED (embeddings), PROVIDER_VISION (image captions via
OpenAI image_url). All default to ollama.
Embeddings are supported by
ollama,gemini,openai, andcustom— the other providers expose no embeddings endpoint and fall back to Ollama. Heads-up: switching embedders mid-corpus requires a re-index (two embedders = two incompatible vector spaces).
Quickstart
Install from PyPI
pip install llm-compounding-wiki # core pipeline + FastAPI app
pip install "llm-compounding-wiki[mcp]" # + agent-facing MCP server
pip install "llm-compounding-wiki[ocr]" # + scanned-PDF OCR (needs Tesseract/Poppler)
llm-wiki serve --port 8000 # run the API (uvicorn llm_wiki.api:app)
llm-wiki mcp # run the MCP server
The import package is llm_wiki; the distribution on PyPI is llm-compounding-wiki.
From source
git clone https://github.com/krishddd/llm-wiki.git
cd llm-wiki
pip install -e ".[dev]" # or: pip install -r requirements.txt
cp .env.example .env
# Option A — fully local (default): pull the Ollama models
ollama pull qwen3:14b
ollama pull gemma4:e4b
ollama pull nomic-embed-text
# Option B — bring your own model: no Ollama needed, just set a provider in .env
# PROVIDER_REASON=anthropic ANTHROPIC_API_KEY=sk-ant-... (or openai / gemini /
# groq / github / xai / openrouter / custom — see the provider table above)
# Run the API
uvicorn llm_wiki.api:app --reload --port 8000
Ingest a doc, then ask a question:
curl -F files=@paper.pdf http://localhost:8000/ingest
curl -X POST http://localhost:8000/query \
-H 'Content-Type: application/json' \
-d '{"question": "What did the paper conclude about transformer scaling?"}'
Run the agent over MCP:
python -m llm_wiki.mcp_server # exposes ingest / query / lint as MCP tools
Trigger a job manually:
curl -X POST http://localhost:8000/admin/run/promote_episodic
Project structure
llm_wiki/
├── api.py FastAPI endpoints
├── ingest.py Multi-format ingest pipeline (+ Doc2Query)
├── query.py Hybrid retrieval + reflective synthesis + save-back
├── eval_harness.py Golden-set evaluation: recall@k/MRR + answer quality + ablations
├── lint.py Health check + auto-fix
├── graph.py Bi-temporal knowledge graph (SQLite-backed)
├── llm.py Async Ollama client (cached embeddings)
├── config.py pydantic-settings — all knobs
├── logging_config.py JSON logs + audit channel
├── scheduler.py APScheduler + JOB_REGISTRY
├── mcp_server.py Agent-facing MCP wrapper
├── search/ BM25, dense, RRF, FlashRank, MMR, multi-query, intent,
│ chunks (small-to-big reconstruction)
├── synth/ Answer blocks, per-claim confidence, claim verify,
│ reflect, followups
├── loaders/ Multi-format (PDF, DOCX, PPTX, XLSX, HTML, MD, TXT)
│ + OKF bundle loader
└── wiki/ Page store (OKF stamping), episodic, promote, procedures,
reconciler, lifecycle, contradiction_resolver,
entity_pages, topics (RAPTOR-lite), index_md, log_md,
okf_export, review_autopilot, review_promote (shared
review→sources promotion), reindex (shared indexing
primitives), answer_cache (semantic answer cache)
scripts/
├── migrate_okf.py Re-stamp pre-OKF pages (idempotent)
├── backfill_v5.py Backfill #hq units + topic pages for older ingests
├── gen_golden.py Generate eval/golden.jsonl from the live wiki
├── run_eval.py Run retrieval/answer eval (+ --ablate variants)
├── import_okf.py Import an external OKF bundle (curated, no LLM)
└── export_okf.py Export the wiki as a standalone OKF bundle
wiki/
├── index.md Auto-regenerated table of contents
├── log.md Append-only operation log
├── sources/ Semantic tier — primary citable content
├── entities/ Semantic tier — auto-generated entity pages
├── procedures/ Procedural tier — recurring patterns
├── episodic/<date>.md Episodic tier — append-only daily logs
├── archive/ Pages auto-moved here by lint auto-fix
├── review/ Confidence-gated drafts awaiting human accept
│ └── edits/ Reconciler-staged edit proposals
└── raw/ Immutable source documents
data/
├── graph.db SQLite: entities, relations, facts, page_access
├── procedures.db SQLite: recurring query patterns
├── answer_cache.db SQLite: semantic answer cache (opt-in)
├── bm25.pkl BM25 index
└── chroma/ ChromaDB persistence (or numpy fallback)
logs/
├── app.log Rotating JSON, all events
└── audit.log Filtered audit channel
Page frontmatter convention
Conforms to OKF v0.1: type (OKF's one required field), description,
resource, and timestamp are stamped centrally by write_page() on every
write, so all writers conform automatically.
---
title: "Page Title"
kind: source | entity | synthesis | promoted | crystallized | procedure | topic
type: "Source Document" | "Person|Organization|Concept|Place|Event" | "Synthesis" | "Topic Overview"
description: "One-sentence summary (first prose sentence of body if not supplied)"
resource: "wiki/raw/file.pdf" # OKF URI of the underlying asset
timestamp: "2026-07-15T16:51:36+00:00" # ISO 8601 last modification, auto-stamped
source: "wiki/raw/file.pdf" | "query-save-back" | "episodic-promotion"
ingested: 2026-05-01
confidence: 0.87
confidence_reason: "..."
domain: general | math | science | economics | engineering
chunk_count: 14 # sub-chunks indexed — lets re-index / promotion purge exactly
tags: [concept, person, org]
entity_refs: ["Entity A", "Entity B"]
hypothetical_questions: ["What does …?"] # Doc2Query, indexed as <pid>#hq
auto_review: # stamped by Review Autopilot (accepted / annotated pages)
{composite: 0.82, verdict: auto-accepted, faithfulness: 0.9, coverage: 0.8}
context_preamble: "..." # Anthropic Contextual Retrieval
has_tables: true
has_images: false
element_counts: {text: 14, heading: 6, table: 3, image: 0, code: 0}
evolved_by: # populated when reconciler edits this page
- {source: "Foo Doc", action: "refine", date: 2026-05-15}
correlation_ids: [COR-...] # crystallized / promoted only
---
CI & local development
GitHub Actions runs ruff, mypy, pytest (Ollama mocked), and a Docker build on
every push to main. Strict ruff config lives in pyproject.toml. The
integration suite (workflows/integration.yml) is gated behind a manually
triggered workflow_dispatch plus a REMOTE_OLLAMA_HOST secret, so day-to-day
CI never depends on a live LLM.
Two more workflows handle release:
publish.yml— builds the sdist/wheel and publishes to PyPI via Trusted Publishing (OIDC, no stored token) when a GitHub Release is published.docs.yml— builds the MkDocs Material site and deploys it to GitHub Pages on every push tomainthat touches the docs.
Status
Personal research project. Explores how far a local-first LLM-driven wiki can self-organise without a human curator.
Contributing
Contributions are welcome — see CONTRIBUTING.md for dev setup, tests, and the release flow. Notable changes are tracked in CHANGELOG.md.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_compounding_wiki-0.1.1.tar.gz.
File metadata
- Download URL: llm_compounding_wiki-0.1.1.tar.gz
- Upload date:
- Size: 275.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a76d1dc24ebcc427ef8acd261bc4bc7803c3ff696838ea71073034a90f58347
|
|
| MD5 |
7d6cd3fba47c5a7518ac1b28fcaf1dc7
|
|
| BLAKE2b-256 |
ce6c5503fe0aa8e4713d0b6afd17d049cfd0a19a599ad8cffd7100879eeabedb
|
Provenance
The following attestation bundles were made for llm_compounding_wiki-0.1.1.tar.gz:
Publisher:
publish.yml on krishddd/llm-wiki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
llm_compounding_wiki-0.1.1.tar.gz -
Subject digest:
0a76d1dc24ebcc427ef8acd261bc4bc7803c3ff696838ea71073034a90f58347 - Sigstore transparency entry: 2387821042
- Sigstore integration time:
-
Permalink:
krishddd/llm-wiki@7f8efb9c0733c979c0ee792ad52ed581058d2fb6 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/krishddd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7f8efb9c0733c979c0ee792ad52ed581058d2fb6 -
Trigger Event:
release
-
Statement type:
File details
Details for the file llm_compounding_wiki-0.1.1-py3-none-any.whl.
File metadata
- Download URL: llm_compounding_wiki-0.1.1-py3-none-any.whl
- Upload date:
- Size: 239.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b416e01d6c745dcff21f21609605b22b7d7e14dfb7c21e414b6691c27f85d842
|
|
| MD5 |
5deae4b48182a7cd12c805fef62689cb
|
|
| BLAKE2b-256 |
e318c983d2a7ce98e095b9a45f5625067d38f01cbc0a28b60753bc144a7c1f1a
|
Provenance
The following attestation bundles were made for llm_compounding_wiki-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on krishddd/llm-wiki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
llm_compounding_wiki-0.1.1-py3-none-any.whl -
Subject digest:
b416e01d6c745dcff21f21609605b22b7d7e14dfb7c21e414b6691c27f85d842 - Sigstore transparency entry: 2387821046
- Sigstore integration time:
-
Permalink:
krishddd/llm-wiki@7f8efb9c0733c979c0ee792ad52ed581058d2fb6 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/krishddd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7f8efb9c0733c979c0ee792ad52ed581058d2fb6 -
Trigger Event:
release
-
Statement type: