Skip to main content

kuhaku

CI PyPI Python License

An AI orchestration framework for Python. kuhaku.core is tool-agnostic runtime infrastructure — LLM abstraction, configuration, identity, security, observability, retry. Tools are built on top of it. Two ship today: retrieval-augmented generation and structured extraction.

Alpha — 0.2.0.


Why kuhaku

  • Access control that filters before it ranks. Tag a document and a caller without a matching tag never retrieves it — in every retrieval strategy, before ranking, so an entitled caller still gets a full result set.
  • Evaluation ships with the framework. A separate, tool-agnostic package with retrieval and answer-quality metrics that measures anything implementing the target contract.
  • Structured extraction that asks for what is missing. Name the fields and kuhaku pulls them out of free text. A value the text lacks is never invented: the result says which one is missing and gives you a question to ask for it.
  • Start light. pip install kuhaku pulls nothing heavy, and no default ever downloads a model. A feature's dependencies come when you use it.
  • No switch to leave in the wrong position. In RAG, PII sanitization has no flag at all, and access filtering has no global one — tagging a document is what protects it.

Quickstart

pip install kuhaku

Point it at a hosted model — no local server needed. Set a key and kuhaku uses it:

export OPENROUTER_API_KEY=sk-or-...    # free models available; or OPENAI_API_KEY, ...

Structured extraction

from kuhaku import Extractor

order = Extractor({"product": str, "quantity": float})

result = order.extract("tea please")
print(result.status)    # ExtractionStatus.NEEDS_CLARIFICATION
print(result.question)  # Could you also tell me: quantity?  -- send it to the user

result = order.extract("3 boxes", prior=result)  # their reply completes the same order
print(result.status)    # ExtractionStatus.COMPLETE
print(result.data)      # {'product': 'tea', 'quantity': 3}

That is the whole conversation loop: kuhaku keeps no state, so pass the previous result back as prior= with each new message, and send result.question while there is one. A message that starts a new order carries nothing over from the finished one. A field kuhaku should ask for must be required — one that accepts None (str | None) is optional and never asked for. To tell orders from complaints or small talk, give it a list of extractors, one per kind: Extractor([order, complaint]) picks the right one for each message.

See Structured extraction for routing between schemas, multi-turn slot filling, evidence checks and your own rules.

Retrieval-augmented generation

RAG needs a vector store and an embedding library, so it has its own extra:

pip install "kuhaku[rag]"
from kuhaku import RAG

rag = RAG(vector_store="./kuhaku-data")
rag.ingest(open("handbook.md").read(), filename="handbook.md")

answer = rag.ask("How do I reset my password?")
print(answer.text)

vector_store names the directory the index lives in. Pass it: without one, kuhaku creates a fresh temporary directory per RAG() instance, so anything you ingest is gone the next time your program starts.

The answer text carries its citations inline as [S1]-style tags. answer.citations maps each tag back to its document; answer.retrieved, answer.redactions (what PII sanitization masked), answer.abstained and answer.trace_id are on the same object.

When retrieval finds nothing relevant, kuhaku abstains rather than letting the model improvise.

Embeddings run locally whichever provider you choose. Dense retrieval needs a ~490 MB model: kuhaku asks before downloading it, and until it is on disk retrieval uses keyword (BM25) search alone — see Installation in detail.


What it does today

  • Structured extraction — text to validated fields from a plain field list (or a Pydantic model); choosing between several kinds of message, multi-turn slot filling, retries, fallback models, evidence checks, your own rules, optional PII masking and input guard
  • Ingestion — .txt, .md, .pdf; paragraph or structural chunking
  • Retrieval — dense (embeddings), sparse (BM25), or both fused with Reciprocal Rank Fusion; optional cross-encoder re-ranking
  • Document-level access filtering — flat tag intersection, enforced before ranking
  • Six LLM providers — Ollama, OpenAI, Anthropic, Google Vertex AI, Groq, OpenRouter
  • Security — PII sanitization, deterministic prompt-injection input guard, per-request audit record
  • Observability — structured logging with trace-id propagation, OpenTelemetry tracing and metrics
  • Resilience — retry with exponential backoff and circuit breakers on every external call
  • Evaluation — golden-dataset loader, retrieval metrics (hit rate@k, MRR, nDCG@k, precision, recall), answer-quality metrics and field-by-field structured-output metrics, in-memory or SQLite result stores
  • Caching — query-answer cache keyed by the entitled chunk set, so two entitlements never share an entry

The test suite runs entirely offline — in-memory fakes for the embedder, vector store and LLM. No network, no model download, no running LLM server needed to run it.


Access control

from kuhaku import RAG, AuthContext

rag = RAG()

rag.ingest(hr_text,  filename="salary_policy.md", access_tags=["people_ops"])
rag.ingest(eng_text, filename="deploy_runbook.md")          # untagged → visible to all

answer = rag.ask(
    "what are the salary bands",
    auth_context=AuthContext(identity="ada", roles=("engineering",)),
)
# salary_policy.md is never retrieved, never cited, never reaches the model

How it behaves:

  • An untagged chunk is visible to everyone. Tagging is opt-in, so an existing corpus keeps working unchanged.
  • A tagged chunk is visible only when one of its tags appears in the caller's roles. Flat set intersection — no hierarchy, no ordering, no tag implying another.
  • A tagged chunk with no auth_context is not retrievable. Tagging a document is what turns protection on for it, which is why there is no enable/disable switch to forget.
  • Filtering happens before ranking, in dense, sparse and hybrid alike.
  • A result withheld for lack of entitlement is indistinguishable from a genuine no-match. Saying "you may not see this" would confirm that matching restricted material exists.

Tags are your vocabulary — kuhaku assigns no meaning to a tag string. ["people_ops"], ["level-3"] and ["muhasebe"] behave identically. Three constants ship as a starting point and nothing more:

from kuhaku import ACCESS_TAG_PUBLIC, ACCESS_TAG_INTERNAL, ACCESS_TAG_RESTRICTED

kuhaku does not authenticate anyone. Your application proves who the user is and hands kuhaku the result. kuhaku's job is enforcement: making sure retrieval cannot return a chunk that AuthContext is not entitled to see.


Installation in detail

Requires Python 3.11+.

What pip install kuhaku pulls in

The base install is light: pydantic, requests, PDF and file-type detection, OpenTelemetry and PyJWT. Nothing heavy comes with it. Each feature that needs more has its own extra:

Extra Adds For
kuhaku[rag] chromadb, sentence-transformers (pulls PyTorch) RAG's vector store and local embeddings
kuhaku[dense] sentence-transformers local embeddings alone
kuhaku[guard] scikit-learn, joblib, numpy the trained prompt-injection guard model
kuhaku[otlp] the OTLP trace exporter (pulls grpcio) sending traces to a collector
kuhaku[vertex] google-genai Google Vertex AI

Without the extra, a feature that needs it either asks before installing it (RAG) or stops and names the command to run (the others). Nothing is installed silently.

PyTorch's default Linux wheel pulls about 19 NVIDIA CUDA runtime libraries (nvidia-cublas, nvidia-cudnn, nccl, triton, …) — several gigabytes that are dead weight without an NVIDIA GPU. Installing the CPU-only build first avoids them:

pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install "kuhaku[rag]"

Windows and macOS wheels do not pull separate CUDA packages, so this step is a Linux concern.

Models downloaded at runtime

Model When Size
intfloat/multilingual-e5-small (embeddings) dense retrieval, after you approve it ~490 MB
BAAI/bge-reranker-base (re-ranker) only if you enable it ~1.1 GB

An API key removes the LLM download, not the embedding one. The only fully hosted configuration is Vertex AI for both the LLM and the embeddings (KUHAKU_RAG__EMBEDDING_PROVIDER=vertex), which needs a Google Cloud project and the vertex extra:

pip install "kuhaku[vertex]"   # only if you are using Google Vertex AI
pip install "kuhaku[dev]"      # pytest, ruff, mypy, build, and every optional package

Running the LLM locally

With no provider set, kuhaku continues with the provider you used last; the first time, it uses a provider whose API key is set, and without one a running local Ollama server with a model you pulled:

ollama serve
ollama pull llama3.2   # any Ollama chat model

With several pulled, kuhaku asks which one once; KUHAKU_OLLAMA_MODEL names it instead.


Defaults

A bare RAG() or Extractor(...) configures nothing and downloads no model. The governing rule: a default may cost CPU and memory, never a download.

Default
Retrieval auto — hybrid (dense + BM25/RRF) when an embedding backend builds, else sparse-only
LLM auto — the provider used last; else a provider whose API key is set (OpenRouter's free model first); else a running Ollama, with a model you pulled
Embeddings intfloat/multilingual-e5-small; device auto — CUDA / MPS / CPU
Vector store Chroma, collection default_kb, temporary directory
Chunking Paragraph, 500 characters, 80 overlap
top_k 4
PII sanitization (RAG) Always on — no setting disables it
Prompt-injection input guard (RAG) Always on — the RAG facade exposes no way to disable it
PII masking, input guard, audit (extraction) Off — each gets in the way of ordinary use; one setting turns it on for every extraction
Audit log On — one record per request, whatever the outcome (RAG(audit_enabled=False) disables it)
Query cache On — SQLite, 1 hour TTL
Cross-encoder re-ranker Off — BAAI/bge-reranker-base is ~1.1 GB

retrieval, llm_provider and embedding_device ship as auto: resolved once at construction from what is installed or reachable, downgrading toward fewer external dependencies and announcing any downgrade on the terminal. An explicit value always wins, and KUHAKU_AUTO=false disables the probing entirely — see docs/configuration.md.

Every setting reads from the environment under a KUHAKU_ prefix. Each tool's settings carry its own name — KUHAKU_RAG__ for RAG, KUHAKU_EXTRACTION__ for extraction:

KUHAKU_RAG__TOP_K=8
KUHAKU_RAG__RETRIEVAL=dense
KUHAKU_EXTRACTION__MAX_RETRIES=2

Six ecosystem-standard names are also accepted unprefixed, because another library reading them means the same thing: OPENAI_API_KEY, ANTHROPIC_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION.

kuhaku never configures logging or reads a .env file on its own — both belong to your application.

Anything the RAG facade does not expose is reachable through rag.engine, a RAGEngine built by constructor injection: swap the retriever, inject an authorization policy, supply your own messages.


Architecture

kuhaku/
├── core/          tool-agnostic runtime infrastructure
│   ├── config          typed, environment-driven Settings
│   ├── llm             LLMProvider + Ollama / OpenAI / Anthropic / Vertex AI /
│   │                   Groq / OpenRouter
│   ├── resolve         capability resolution: what is usable now, asked before installing
│   ├── auth            AuthContext and identity primitives
│   ├── security        input policy, prompt-injection guard, PII sanitization, audit
│   ├── observability   structured logging, OpenTelemetry tracing and metrics
│   └── retry           retry with backoff, circuit breakers
├── evaluation/    tool-agnostic evaluation harness
└── tools/
    ├── rag/         ingestion, chunking, embeddings, vector store, retrieval, fusion,
    │                re-ranking, access filtering, caching
    └── extraction/  schemas, extraction pipeline, slot filling, routing, evidence

The rule: a tool may depend on kuhaku.core. Core may never depend on a tool. A new tool is a new package under tools/ with its own settings, never a new field on core.


Limitations

  • load_documents applies one tag set to the whole call, not per file.
  • The BM25 index rebuilds on the first query after an ingest rather than updating incrementally, so that query pays an O(corpus size) cost. RAG(retrieval="dense") avoids it.
  • Chroma is the only vector store implementation, and there are no async APIs.

Contributing

Issues and pull requests are welcome. The one hard requirement is architectural: core stays tool-agnostic. See CONTRIBUTING.md.

git clone https://github.com/ism00efe/kuhaku
cd kuhaku
pip install -e ".[dev]"
pytest

License

Apache License 2.0 — see LICENSE and NOTICE.

Metadata

Release files for kuhaku 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kuhaku 0.2.0
File Size Uploaded
kuhaku-0.2.0.tar.gz 245.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kuhaku 0.2.0
File Interpreter ABI Platform
kuhaku-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 515.2 kB

Release files / kuhaku-0.2.0.tar.gz

Download URL kuhaku-0.2.0.tar.gz
Size 245.1 kB
Tags Source
SHA-256 checksum
How to use checksums
78c27c250f5e3f63edce1c5093a768876b755b2efa7c17e72f8d4a5606b1c038
BLAKE2b-256 checksum
How to use checksums
6a7718e1966a64ccd6783b1e8e5dc7e1d105e3d0efe59c671b41c88e330bb9ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / kuhaku-0.2.0-py3-none-any.whl

Download URL kuhaku-0.2.0-py3-none-any.whl
Size 270.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ef64074f944e393a656cba4724d4c69e26f4e6903982a362ca4ab0656e3a1e40
BLAKE2b-256 checksum
How to use checksums
d7590bc629c7485bf2fdb84cd000109dc4748005ff663ca4778ff26c32d6e7c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page