Ossian is a RAG backend for your agents: every answer carries the passage it came from, and when the corpus cannot support one it says so instead of inventing it. With the ingestion pipeline, memory, credentials, limits and audit trail needed to actually run that.
Drop documents in, ask questions, get answers you can check against the text they came from.
PDFs, Word, Markdown, plain text — or a URL, or a change streamed from your database through Debezium and Kafka. Tika reads the format from the bytes, so nothing is converted first.
Cut into overlapping passages, into pgvector. Without overlap a fact straddling a boundary belongs to neither passage and is never retrieved.
The question is embedded and matched, narrowed to a namespace if you want one. Weak matches are discarded rather than passed on.
Only the retrieved passages reach the model, numbered per document so its [1] markers map back to sources you can open — three passages of one handbook are one source, not three agreeing.
Below a similarity threshold, Ossian returns nothing and never calls the model. An unsupported answer is worse than no answer, because it is indistinguishable from a good one — and a confident invention presented as company policy is the failure this whole system exists to prevent. Every refusal is recorded, and that list is the most useful ingestion backlog anyone has handed a documentation team.
"I could not find anything about that in your documents."A question answered with its citation opened, the same corpus refusing one it cannot support, the vector inspector, agent memory, and the operations console.
Recorded from the running console · MP4
The demo is a weekend. This is the part that decides whether it survives contact with a real company.
Every stored chunk, its dimensions and norm, and a playground that runs a query with no model in the loop. Retrieval and generation failures look identical from outside and have opposite fixes.
Ranked by relevance, weighted by importance, decayed by the time since it was last said — not last read, so a stale preference cannot outrank the newer one that contradicts it. Kept apart from the corpus, so a recollection never surfaces as a citation.
API keys with their own roles, confinable to one namespace — a leaked pipeline key reads only what that pipeline was for. Stored hashed, shown once.
One limiter counts requests; the other counts embedding tokens, because a single upload can be a million of them. Ingestion waits rather than failing.
Prometheus metrics, failures grouped by cause, and a retry button. It catches documents left processing for an hour — nothing marks those failed, because the process that would have is the one that died. Or skip uploads entirely and stream changes from Kafka.
Model, temperature, chunk size, overlap, how many passages and how good a match must be. Chunking is per namespace, because a runbook and a contract want different sizes.
Seven tools over stdio. The descriptions tell the model when not to use each one — the failure that matters is not a malformed call but a well-formed call to the wrong tool.
# the MCP server
npm install -g ossian-mcp
{
"mcpServers": {
"ossian": {
"command": "ossian-mcp",
"env": {
"OSSIAN_URL": "http://localhost:8081",
"OSSIAN_API_KEY": "osk_…"
}
}
}
}
A knowledge base that is already a table does not need an export job. Debezium streams its changes into Kafka, and the Ossian sink turns each one into an indexed, citable document — or removes it.
{
"connector.class":
"io.github.dockndevai.ossian.kafka.connect.OssianSinkConnector",
"topics": "kb.public.kb_articles",
"ossian.url": "http://ossian:8081",
"ossian.api.key": "${env:OSSIAN_API_KEY}",
"ossian.text.fields": "title,body",
"ossian.filename.field": "title",
"errors.deadletterqueue.topic.name": "ossian-sink-dlq"
}
The key comes from the worker's environment, never the connector config — those live in a Kafka topic and come back from the REST API. Give it a key confined to the namespace it feeds.
A developer asked what happens when retrieved documents and an agent's memory disagree, or when several pieces of context come from the same source. Answering honestly meant reading the code, and the code had two answers that were wrong.
Retrieval numbered context per chunk, so three passages of one handbook reached the model — and the reader — as [1] [2] [3]: three sources agreeing, when it was one source said three times. Passages are now grouped under one number per document, every passage kept, ranked by the best. #2
Decay ran from when a memory was first written, so restating a fact never refreshed it. It now restarts on restatement — and deliberately not on recall, because a stale preference and the newer one contradicting it are recalled together, and refreshing both would erase the recency that lets the newer one win. #1
A DELETE event recorded the id of the document it had just removed, which broke a foreign key and returned 500 for the whole batch. A retry found nothing left to delete and succeeded, so every retrying pipeline hid it. The Debezium sink's first end-to-end run is what caught it. #5
New repository, ossian-kafka-connect, tested end to end: a snapshot, updates, deletes, blanked rows and an offset reset replaying the whole topic without a single duplicate.
The notebook's width followed whatever it contained, so it narrowed and re-centred while an answer streamed. It is now fixed to the window, a cited passage opens beside, over or under the conversation depending on the room, and the navigation folds into a bar on a phone.
Memories do not link back to the document they were learned from, and a contradicting memory does not supersede the old one — both come back, and recency decides. Keeping documents and memory apart is staying; the rest is an open design question.
Free and open source, Apache-2.0. Postgres with pgvector, Redis, Keycloak and a model gateway — all in the compose file.
git clone https://github.com/dockndevai/ossian cd ossian && docker compose up -d docker compose exec ollama ollama pull nomic-embed-text docker compose exec ollama ollama pull qwen2.5:3b ./mvnw -pl backend spring-boot:run cd frontend && npm install && npm run dev
Then ./scripts/smoke.sh — it ingests a sample corpus and asks five questions. The fifth is deliberately unanswerable; an answer to it means grounding has broken.
kubectl create secret generic ossian-postgres \ --from-literal=password=… kubectl create secret generic ossian-gateway \ --from-literal=api-key=… helm install ossian deploy/ossian \ --set ingress.enabled=true \ --set ingress.host=ossian.example.com
The chart ships the application only. Postgres, Redis and Keycloak are referenced rather than deployed — a database packaged inside an app chart is deleted by helm uninstall, and nobody means that.
Ossian runs entirely on your own infrastructure, so documents never leave it. But it is aimed at a different job from a notebook: it is a RAG backend that other software queries, with an API, machine credentials, rate limits and an audit trail. For one person reading twenty papers, Gemini Notebook or Open Notebook are the better answers, and I would rather say so than sell you the wrong tool.
Refuse rather than guess. Ossian discards passages below a similarity threshold, and when nothing clears it the model is never called — you get "I could not find anything about that in your documents". Answers that are produced carry the exact passages they were written from, so they can be checked rather than trusted.
That is what it is for. API keys let a process authenticate without a browser login, and the MCP server (npm i -g ossian-mcp) gives an agent seven tools across documents, memory and ingestion. A key can be confined to a single namespace, so a leaked credential reads only what that agent was for.
Postgres with pgvector, Redis, Keycloak, and any OpenAI-compatible model endpoint — all in the compose file, with a Helm chart for Kubernetes. Ossian holds no model credentials itself; those live in the gateway it talks to, so swapping models is a routing change rather than a deployment.
Yes — through Debezium and Kafka. The Kafka Connect sink indexes inserts and updates, removes deleted rows along with their chunks, and removes a row whose text was blanked rather than leaving it to answer. Event ids come from the record's position in Kafka, so a connector that restarts and re-reads a topic creates no duplicates.
Apache-2.0, source on GitHub. No hosted tier, no licence key, no telemetry. It is also young — well tested, but run in anger by nobody yet, and that is worth knowing before you put it in front of anything that matters.