Skip to content

Latest commit

 

History

26 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kbase — Knowledge Base Service

Documents in, retrievable chunks out. One collection per body of knowledge, one bearer key per tenant, and a semantic search that refuses to answer with something irrelevant.

Run it

pip install -e ".[dev,postgres]"   # postgres: the asyncpg driver, and tests/test_pgvector.py

export KB_API_KEYS=pick-a-long-random-string:acme
export KB_EMBED_BASE_URL=https://api.openai.com/v1
export KB_EMBED_API_KEY=sk-...
export KB_EMBED_MODEL=text-embedding-3-small

kb doctor     # says what is missing, and nothing else
kb serve      # refuses to start on a configuration doctor already failed
AUTH="Authorization: Bearer pick-a-long-random-string"

curl -X POST localhost:8090/v1/collections -H "$AUTH" \
  -H 'Content-Type: application/json' -d '{"name":"faq"}'

curl -X POST localhost:8090/v1/documents -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"collection":"faq","title":"Sổ tay","text":"## Bảo hành\n\nMười hai tháng."}'

curl -X POST localhost:8090/v1/search -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"collection":"faq","query":"bảo hành bao lâu"}'

Or the whole thing in one file:

cp .env.example .env      # put your keys in it
docker compose up -d

How it works

An upload is stored and returns 202 pending immediately; extraction, chunking, and embedding happen in the background. Embedding a large document takes minutes, and a synchronous upload would time out before it finished. Poll GET /v1/documents/{id} for status.

Chunks are cut on markdown headings first and length second (800 characters, 100 of overlap), and each one keeps the heading path it came from, so an answer can cite Bảo hành > Đổi trả rather than a floating paragraph.

A document either indexes completely or is marked failed with a reason. There is no partial index: a document holding only its first third would answer questions from that third and never say the rest is missing. A document with no text in it fails too, rather than reporting indexed with nothing to search.

failed is never the end of the road. Uploading the same bytes again retries that document rather than reporting a duplicate, and POST /v1/documents/{id}/reindex runs it again from the copy already stored — which is also how to re-embed a corpus after changing KB_EMBED_MODEL, without holding any of it locally.

The error a document carries is written for the tenant reading it. Anything describing the file they sent is quoted; anything describing this deployment — the embedding host, a driver's connection string — is not, and stays in the service log.

Search has two backends. On SQLite — and on Postgres with no KB_EMBED_DIM — it scores every chunk in the collection, in a worker thread, a partition at a time: a search costs about 16 MB whether the collection holds a thousand chunks or a hundred thousand. It is a linear scan, so it slows as the corpus grows, around 1.2 s over 10,000 chunks.

Set KB_EMBED_DIM on Postgres and chunks are stored in a pgvector column with an HNSW index, and the ordering happens in the database. Vectors already indexed are copied across on the next start — no re-embedding, no provider spend — and any chunk whose width does not match marks its document failed with a reason, recoverable with POST /v1/documents/{id}/reindex. HNSW does not go above 2000 dimensions; wider than that stores fine and searches by scanning, and kb doctor says so. Results from the two backends are not identical: HNSW is approximate, which is the ordinary price of a vector index.

That conversion is DDL, and it runs at boot: run one instance per database while it happens. A rolling deploy has an old instance still writing to the JSON column while a second new one runs the same DROP COLUMN / RENAME, which fails and crash-loops that replica. Stop the old instance, start one new one, then scale back up. (The stale-pending sweep in server/app.py assumes the same thing, for the same reason.) Unsetting KB_EMBED_DIM afterwards is refused at boot rather than obeyed: the column is a vector the scanning backend cannot read, and starting anyway would answer every search with nothing over a corpus that is entirely intact.

Configuration

KB_API_KEYS key:tenant,key:tenant. Unset means every request is a 401
KB_DATABASE_URL SQLite by default; a Postgres URL switches the store
KB_EMBED_DIM Postgres only. Setting it stores vectors in a pgvector column with an HNSW index; unset means the linear scan. Must match the model's width, and cannot be unset again once a database has been migrated
KB_EMBED_BASE_URL OpenAI-compatible /embeddings endpoint
KB_EMBED_API_KEY credential for the above
KB_EMBED_MODEL embedding model id
KB_EMBED_BATCH Inputs per /embeddings request (default 32). Providers disagree: DashScope's text-embedding-v3 rejects more than 10
KB_MAX_UPLOAD_BYTES rejected above this (default 20 MB)
KB_DOCS false closes /docs and /redoc, which need no credential

API

POST   /v1/collections            {name}
GET    /v1/collections
DELETE /v1/collections/{name}

POST   /v1/documents              multipart (file, collection, title?)
                                  or JSON {collection, title, text}
                                  202 queued, 200 already indexed here
GET    /v1/documents?collection=&status=
GET    /v1/documents/{id}
POST   /v1/documents/{id}/reindex index again from the stored bytes
DELETE /v1/documents/{id}

POST   /v1/search                 {collection, query, limit, min_score}
GET    /healthz

Not here yet

PDF and DOCX ingestion, URL crawling, hybrid keyword search, and re-ranking. Gateway integration is specified in the parent repository's spec and deliberately not built yet.

Tests

.venv/bin/pytest -q

Part of LUGO

LUGO is a self-hosted AI companion platform — models supply the intelligence, LUGO supplies the experience: one assistant that talks, remembers and acts across the browser, ESP32 boards and a Raspberry Pi.

This repository is one piece of it. Every client and service talks to the gateway:

Repo Role
lugo-gateway The hub — STT/TTS/LLM engines, auth, device pairing, MCP tools, per-user chat memory. Everything below talks to this.
lugo-web-client React + TypeScript web client: talk, devices, history, tools.
esp32-assistant ESP-IDF firmware for ESP32-S3 / ESP32-C3 — a hands-free voice terminal.
rpi-assistant Raspberry Pi voice client (mic capture, Opus duplex, systemd unit).
knowledge-api  ← you are here kbase — RAG knowledge base: documents in, retrievable chunks out.
router-memory-services memgw — one API in front of any AI memory provider (Mem0, Zep, pgvector).
mcp-basic-tools Remote MCP tool server (timedate, fetch, ipinfo, web search).
livehost-api TikTok Live AI co-host, an out-of-process gateway plugin.
voiceprint-api Speaker recognition (3D-Speaker), forked from xinnan-tech/voiceprint-api.
lugo-landing Marketing landing page for the platform, bilingual (Tiếng Việt / English).

About

kbase — knowledge base service for RAG: documents in, retrievable chunks out. Multi-tenant bearer keys, pgvector or SQLite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages