This guide is the technical reference for quivr-core — the open-source RAG library that powers Quivr. It is written for the engineers and architects who will embed Quivr in a product, operate it in production, extend it with custom processors or vector stores, or audit its security and internals. For task-oriented usage, see the companion User Guide.
1. Executive Summary
quivr-core is a Python library (package quivr-core, version 0.0.33 at the documented commit) that implements an opinionated Retrieval-Augmented Generation pipeline on top of LangChain and LangGraph. It is not a service, a database, or a UI — it is a dependency you add to your own application.
The repository was historically a full-stack "second brain" web application; it has since been refactored down to this single reusable core plus a docs/ documentation site and four runnable examples/. The 39K GitHub stars reflect that history. Today the product surface is the quivr_core Python API.
What it gives you:
- A single
Brainfacade that ingests files, indexes them, and answers questions. - Provider-agnostic LLM access (OpenAI, Azure, Anthropic, Mistral, Gemini, Groq, Ollama).
- A pluggable document-processing registry covering ~15 file formats.
- A swappable vector store (FAISS by default, any LangChain
VectorStoreaccepted). - A declarative, YAML-driven retrieval workflow compiled into a LangGraph state machine, enabling features like dynamic retrieval, web search, tool use, and citations without code changes.
- Built-in observability via Langfuse.
Architectural stance: Quivr is a trusted-execution library, in the same family as LlamaIndex and LangChain. It assumes the hosting application owns authentication, authorization, input validation, and rate limiting. It is a building block, not a security boundary.
2. Architecture
2.1 High-level component diagram
┌──────────────────────────────────────────────┐
your application ────► │ Brain (facade) │
(from_files / ask) │ brain/brain.py — orchestrates everything │
└───┬───────────┬───────────┬───────────┬───────┘
│ │ │ │
┌───────────────▼──┐ ┌─────▼─────┐ ┌───▼────────┐ ┌▼───────────────┐
│ Storage │ │ Processor │ │ Embedder │ │ Vector store │
│ StorageBase │ │ registry │ │ Embeddings │ │ VectorStore │
│ ├ Transparent │ │ (lazy) │ │ (OpenAI) │ │ (FAISS default)│
│ └ LocalStorage │ └─────┬─────┘ └────────────┘ └────────┬────────┘
└───────────────────┘ │ │
▼ ▼
INGESTION: file ─► parse ─► split (SplitterConfig) ─► embed ─► store
│
▼
┌──────────────────────────────────────────────────────┐
RETRIEVAL per question │ QuivrQARAGLangGraph (rag/quivr_rag_langgraph.py) │
│ LangGraph StateGraph compiled from WorkflowConfig │
│ nodes: filter_history → rewrite → retrieve → │
│ [tool_routing → run_tool] → generate_rag │
│ LLMEndpoint (multi-supplier) + RerankerConfig + │
│ prompts registry + LLMToolFactory (web search…) │
└───────────────────────────────┬──────────────────────┘
▼
ParsedRAGResponse (answer + citations + sources)
Observability: Langfuse callback handler (global)
2.2 Two execution phases
Quivr has a clean split between a one-time ingestion phase and a per-question retrieval phase.
Ingestion (Brain.afrom_files → process_files):
- Each path is wrapped in a
QuivrFile(load_qfile) — computes a SHA-1, detects extension via mimetypes. - The file is placed in
storage(memory or disk). get_processor_class(extension)resolves a processor (lazy import, priority-ordered).- The processor reads the file, splits it (
RecursiveCharacterTextSplitter, token-based), and emits LangChainDocumentchunks; each chunk is enriched with metadata (chunk index, language, file SHA-1, processor class). - Chunks are embedded and added to the vector store (FAISS built via
FAISS.afrom_documents).
Retrieval (Brain.ask_streaming → QuivrQARAGLangGraph.answer_astream):
- The configured
WorkflowConfigis compiled into a LangGraphStateGraph. - The graph runs node by node over an
AgentState(messages, chat history, tasks, files…). - The final (
generate_rag) node's tokens are streamed back; the last chunk carries metadata.
2.3 The workflow-as-graph design (the key idea)
The most important architectural decision is that retrieval logic is data, not code. A WorkflowConfig is a list of NodeConfigs, each naming a method on QuivrQARAGLangGraph and declaring its outgoing edges or a conditional_edge. At runtime _build_workflow calls getattr(self, node.name) to bind each node to its implementing method and wires the edges into a LangGraph StateGraph.
Consequences:
- Adding web search, dynamic retrieval, or prompt-editing is a YAML change, not a code change — as long as the node names map to existing methods.
- The set of available node behaviours is fixed by the methods on
QuivrQARAGLangGraph(see §6.3). You cannot invent arbitrary node names; an unknown name raisesAttributeErrorat graph build. - The first node must be
START; the validatorcheck_first_node_is_startenforces this.
2.4 Two RAG engines
| Engine | File | Status | Mechanism |
|---|---|---|---|
QuivrQARAGLangGraph |
rag/quivr_rag_langgraph.py |
Default / primary | LangGraph state machine compiled from WorkflowConfig. |
QuivrQARAG |
rag/quivr_rag.py |
Legacy | A fixed LangChain Expression Language (LCEL) chain. |
Note
Brain.ask_streamingaccepts arag_pipelineargument, but the implementation always instantiatesQuivrQARAGLangGraph. The argument is effectively vestigial — the LangGraph engine is what runs. TreatQuivrQARAGas deprecated for new work.
3. Requirements
3.1 Runtime
| Requirement | Value |
|---|---|
| Python | ≥ 3.11 (requires-python = ">= 3.11") |
| Default LLM | OpenAI gpt-4o (needs OPENAI_API_KEY) |
| Default embedder | OpenAIEmbeddings |
| Default vector store | faiss-cpu (in-memory) |
| License | Apache 2.0 |
3.2 Core dependencies (core/pyproject.toml)
| Dependency | Constraint | Role |
|---|---|---|
pydantic |
>=2.8.2 |
Config models, serialization. |
langchain-core |
>=0.3,<0.4 |
Documents, messages, prompts, runnables. |
langchain |
>=0.3.9,<0.4 |
Retrievers, compressors. |
langgraph |
>=0.2.38,<0.3 |
The workflow state machine. |
langchain-openai |
>=0.3.0 |
Default LLM + embeddings. |
langchain-anthropic / -mistralai / -google-genai / -groq / -cohere / -community |
various | Provider clients + Tavily/Jina tools. |
faiss-cpu |
>=1.8.0.post1 |
Default vector store. |
tiktoken |
>=0.7.0 |
Token counting. |
transformers[sentencepiece] |
>=4.44.2 |
Provider-specific tokenizers. |
megaparse-sdk |
>=0.1.11 |
MegaParse document parsing client. |
fasttext-langdetect |
>=1.0.5 |
Per-chunk language detection. |
langfuse |
>=2.57.0 |
Observability/tracing. |
rapidfuzz |
>=3.10.1 |
Fuzzy matching for tool-name validation. |
httpx, aiofiles, rich |
— | HTTP (Tika), async file IO, console output. |
3.3 Optional external services
| Service | Needed for | Configure with |
|---|---|---|
| Apache Tika server | Default PDF parsing | TIKA_SERVER_URL (default http://localhost:9998/tika) |
| MegaParse (NATS) | Alternative document parsing | MegaparseConfig |
| Cohere / Jina | Re-ranking | COHERE_API_KEY / JINA_API_KEY |
| Tavily | Web search tool | TAVILY_API_KEY |
| Langfuse | Tracing | Langfuse env vars (host/keys) |
4. Configuration Mastery
All configuration is expressed as Pydantic v2 models that subclass QuivrBaseConfig. Every config object can be loaded from YAML via Config.from_yaml(path).
Important
QuivrBaseConfigsetsmodel_config = ConfigDict(extra="forbid"). Unknown keys raise a validation error. This is why a YAML containingmax_input_tokens(a key that does not exist) fails — even though the project's own README shows it. Always validate keys against the model fields below; the in-repocore/example_workflows/*.yamlfiles are the correct reference.
4.1 Configuration object hierarchy
AssistantConfig
├── retrieval_config: RetrievalConfig
│ ├── llm_config: LLMEndpointConfig
│ ├── reranker_config: RerankerConfig
│ └── workflow_config: WorkflowConfig
│ └── nodes: list[NodeConfig]
│ └── conditional_edge: ConditionalEdgeConfig
└── ingestion_config: IngestionConfig
└── parser_config: ParserConfig
├── splitter_config: SplitterConfig
└── megaparse_config: MegaparseConfig
4.2 RetrievalConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
llm_config |
LLMEndpointConfig |
LLMEndpointConfig() |
The answering model. |
reranker_config |
RerankerConfig |
RerankerConfig() |
Re-ranking (off unless supplier set). |
workflow_config |
WorkflowConfig |
default RAG graph | The retrieval graph. |
max_history |
int |
10 |
Conversation pairs kept in context. |
max_files |
int |
20 |
Max files listed in the prompt. |
k |
int |
40 |
Chunks fetched by the retriever. |
prompt |
str | None |
None |
Custom system instructions. |
4.3 LLMEndpointConfig
| Field | Type | Default | Notes |
|---|---|---|---|
supplier |
DefaultModelSuppliers |
openai |
Auto-inferred from model if not set. |
model |
str |
"gpt-4o" |
Model name. |
temperature |
float |
0.3 |
Sampling temperature. |
max_context_tokens |
int |
20000 |
Prompt budget; auto-capped to model limit; min 4096. |
max_output_tokens |
int |
4096 |
Answer length; auto-capped; min 4096. |
streaming |
bool |
True |
Token streaming. |
llm_base_url |
str | None |
None |
Custom endpoint (Ollama/Azure/proxy). |
llm_api_key |
str | None |
None |
Falls back to env_variable_name. |
env_variable_name |
str | None |
<SUPPLIER>_API_KEY |
Auto-derived key name. |
tokenizer_hub |
str | None |
per-model | HF tokenizer for token counting. |
Behaviour worth knowing:
- Model-aware capping (
set_llm_model_config): Quivr looks up the model in a built-inLLMModelConfig._model_defaultstable (per supplier) and lowersmax_context_tokens/max_output_tokensto the model's real limits, logging a warning. Values belowMIN_CONTEXT_TOKENS/MIN_OUTPUT_TOKENS(both 4096) raiseValueError. - Supplier inference (
set_llm_model): the supplier is derived bystartswithmatching against the model table (e.g.claude-*→ Anthropic). - Instance caching:
LLMEndpoint.from_configcaches instances by the config's__hash__, so identical configs reuse the same client.
Supported suppliers (DefaultModelSuppliers): openai, azure, anthropic, meta, mistral, groq, gemini. Each maps to a LangChain chat client in LLMEndpoint.from_config.
4.4 RerankerConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
supplier |
DefaultRerankers | None |
None |
cohere or jina; None = no re-ranking. |
model |
str | None |
supplier default | Cohere→rerank-v3.5, Jina→jina-reranker-v2-base-multilingual. |
top_n |
int |
5 |
Chunks kept after re-ranking. |
relevance_score_threshold |
float | None |
None |
Drop chunks below this score (0–1). |
relevance_score_key |
str |
"relevance_score" |
Metadata key holding the score. |
Warning
When
supplieris set,RerankerConfigvalidates that<SUPPLIER>_API_KEYexists in the environment at construction time and raisesValueErrorif missing. A workflow that names a reranker supplier will fail fast without the key.
4.5 SplitterConfig and MegaparseConfig
SplitterConfig |
Default | MegaparseConfig |
Default | |
|---|---|---|---|---|
chunk_size |
400 (tokens) |
method |
unstructured |
|
chunk_overlap |
100 (tokens) |
strategy |
fast (auto/hi_res) |
|
check_table |
False |
|||
model_name |
gpt-4o |
4.6 WorkflowConfig, NodeConfig, ConditionalEdgeConfig
WorkflowConfig:name,nodes: list[NodeConfig],available_tools: list[str](validated against the tool registry — an unknown tool raises a "did you mean…" error via fuzzy matching), plus internalvalidated_tools/activated_tools.NodeConfig:name(must equal aQuivrQARAGLangGraphmethod, orSTART/END),edgesorconditional_edge, optionaltools(instantiated viaLLMToolFactory),description.ConditionalEdgeConfig:routing_function(a method name likerouting_splitortool_routing) andconditions(a list/dict of next-node names, withSTART/ENDresolved to LangGraph sentinels).
4.7 Environment variable reference
| Variable | Used by | Default |
|---|---|---|
OPENAI_API_KEY |
OpenAI LLM/embeddings | — |
ANTHROPIC_API_KEY |
Anthropic | — |
MISTRAL_API_KEY |
Mistral | — |
GEMINI_API_KEY |
Gemini | — |
GROQ_API_KEY |
Groq | — |
AZURE_API_KEY |
Azure OpenAI | — |
COHERE_API_KEY / JINA_API_KEY |
Re-rankers | — |
TAVILY_API_KEY |
Web search tool | — |
TIKA_SERVER_URL |
PDF parsing | http://localhost:9998/tika |
QUIVR_LOCAL_STORAGE |
LocalStorage directory |
~/.cache/quivr/files |
TOKENIZERS_PARALLELISM |
HF tokenizers | set to false to silence fork warnings |
Note
There is no central env-var scrape and no
.envloaded by the library itself. API keys are read on demand viaos.getenvinside the config models (set_api_key). Your application is responsible for populating the environment (the examples usepython-dotenv). Missing keys are logged as warnings (the key value is never logged).
5. Repository Structure
quivr/
├── core/ # the quivr-core package
│ ├── quivr_core/
│ │ ├── brain/ # Brain facade + info + (de)serialization + defaults
│ │ ├── llm/ # LLMEndpoint multi-supplier client + tokenizer cache
│ │ ├── llm_tools/ # tool registry (web search / Tavily, cited_answer)
│ │ ├── processor/ # parser registry + implementations + splitter
│ │ ├── rag/ # RAG engines, prompts, entities (config/models/chat)
│ │ ├── storage/ # StorageBase + LocalStorage/TransparentStorage
│ │ ├── files/ # QuivrFile abstraction
│ │ ├── language/ # language enum + fasttext detection
│ │ ├── base_config.py # QuivrBaseConfig (from_yaml, extra=forbid)
│ │ └── config.py # MegaparseConfig, ParserType, StrategyEnum
│ ├── tests/ # pytest suite (markers: base/tika/unstructured/slow)
│ ├── example_workflows/ # canonical workflow YAML (correct keys)
│ └── pyproject.toml # package metadata + deps
├── docs/ # mkdocs-material documentation site
└── examples/ # chatbot, chatbot_voice, quivr-whisper, simple_question
Tip
The codebase is organized by concern, and each concern is fronted by an abstract base class (
StorageBase,ProcessorBase) or a registry (processor/registry.py,llm_tools/llm_tools.py). To extend Quivr you implement an ABC or register into a registry — you rarely modify existing files.
6. Module Reference
6.1 brain/brain.py — the Brain facade
The orchestrator. Key surface:
| Member | Kind | Purpose |
|---|---|---|
from_files / afrom_files |
classmethod | Ingest files → build brain. |
afrom_langchain_documents |
classmethod | Build from pre-made Documents. |
load / save |
classmethod / async | Persist & restore a brain (FAISS + config.json). |
ask / aask |
method | One-shot answer (ParsedRAGResponse). |
ask_streaming |
async generator | Streamed answer (ParsedRAGChunkResponse). |
asearch |
async | Similarity search → list[SearchResult]. |
info / print_info |
method | Rich tree of brain metadata. |
chat_history / get_chat_history |
property/method | Access conversation state. |
process_files(storage, skip_file_error, **kwargs) is the module-level ingestion helper: it iterates storage, resolves a processor per file, and concatenates the resulting chunks.
brain_defaults.py supplies default_llm() (OpenAI gpt-4o), default_embedder() (OpenAIEmbeddings), and build_default_vectordb() (FAISS), each raising an actionable ImportError if the optional dependency is missing.
6.2 llm/llm_endpoint.py — provider abstraction
LLMEndpoint.from_config(config)builds the right LangChain chat client for the supplier (ChatOpenAI,AzureChatOpenAI,ChatAnthropic,ChatMistralAI,ChatGoogleGenerativeAI,ChatGroq) and caches instances by config hash.supports_func_calling()returnsFalseforllama2,ollama3, andtest— driving the non-tool answering path.count_tokens()uses a per-model HF tokenizer with an LRU-style cache (LLMTokenizer, capped at 50 MB / 5 tokenizers) and acl100k_basetiktoken fallback.info()returns anLLMInfodataclass (used byprint_info).
6.3 rag/quivr_rag_langgraph.py — the workflow engine
QuivrQARAGLangGraph implements every node behaviour as a method. The available nodes/routers:
| Method (node) | Role |
|---|---|
filter_history |
Trim chat history to the token budget. |
rewrite |
Condense each task into a standalone question (async, per-task). |
retrieve |
Fetch chunks via the (optionally re-ranked) compression retriever. |
dynamic_retrieve |
Iteratively widen top_n/k until enough relevant chunks (max 3 iterations). |
tool_routing (router) |
Decide per task whether a tool is needed. |
run_tool |
Execute the selected tool (e.g. Tavily web search). |
edit_system_prompt |
Update the system prompt + activate/deactivate tools from user instructions. |
generate_rag |
Build the RAG prompt, reduce context to fit, generate the answer. |
generate_chat_llm |
Answer without retrieval (plain chat). |
generate_zendesk_rag |
Specialized Zendesk-ticket answering node. |
retrieve_full_documents_context |
Enterprise-only: fetch full docs by knowledge id. |
routing / routing_split (routers) |
Split user input into instructions vs. tasks. |
Supporting structures: AgentState (the LangGraph state TypedDict), UserTasks/UserTaskEntity (multi-task tracking), reduce_rag_context (shrinks context to 85% of the limit by dropping history then least-relevant docs), and (a)invoke_structured_output (Pydantic-typed LLM calls with a JSON-schema-then-fallback strategy).
Note
retrieve_full_documents_contextasserts the vector store exposesget_vectors_by_knowledge_idand is documented in-code as "an enterprise only feature." With the default FAISS store this node will fail — only enable it with a compatible store.
6.4 rag/prompts.py — prompt registry
A frozen MappingProxyType of named ChatPromptTemplates built once at import: RAG_ANSWER_PROMPT, CONDENSE_TASK_PROMPT, SPLIT_PROMPT, TOOL_ROUTING_PROMPT, USER_INTENT_PROMPT, UPDATE_PROMPT, CHAT_LLM_PROMPT, DEFAULT_DOCUMENT_PROMPT, and the ZENDESK_* prompts. The default RAG prompt hard-codes the grounding policy ("use ONLY the provided context… do not apologize… don't cite the source id in the answer").
6.5 processor/ — document parsing
registry.py: maps extensions → processor class paths with lazy import and a min-heap priority so optional/heavy parsers (MegaParse highest priority) are tried first and fall back to lighter ones onImportError.get_processor_class,register_processor,available_processorsare the public API.implementations/default.py:_build_processorfactory dynamically generates processor classes wrapping LangChain document loaders (CSVLoader,Docx2txtLoader,UnstructuredPDFLoader,PythonLoader,NotebookLoader, etc.).implementations/simple_txt_processor.py: dependency-free.txtprocessor (works with the base install).implementations/tika_processor.py: PDF via a Tika HTTP server (3 retries, configurable URL/timeout).implementations/megaparse_processor.py: MegaParse over NATS for many formats.processor_base.py:ProcessorBaseABC;process_fileenriches chunk metadata (chunk index,quivr_coreversion, detected language, file metadata) and sanitizes null bytes/encoding.splitter.py:SplitterConfig(chunk size/overlap).
6.6 storage/, files/, language/
storage_base.py—StorageBaseABC (nb_files,get_files,upload_file,remove_file,info).local_storage.py—LocalStorage(copies or symlinks to a managed dir, de-dupes by SHA-1) andTransparentStorage(in-memory; the default).files/file.py—QuivrFile(slots-based) +QuivrFileSerialized(Pydantic) +FileExtensionenum (~24 extensions) +load_qfile.language/— a ~170-entryLanguageenum anddetect_languagevia fastText.
6.7 rag/entities/
config.py— all the config models (§4).models.py— response/IO models:cited_answer(the citation tool schema),ParsedRAGResponse,ParsedRAGChunkResponse,RAGResponseMetadata,QuivrKnowledge,SearchResult,ChatMessage,LangchainMetadata.chat.py—ChatHistory(append,iter_pairs,to_list, time-ordered retrieval).
7. Security Analysis
Caution
Pickle deserialization on
Brain.load. Restoring a saved brain callsFAISS.load_local(..., allow_dangerous_deserialization=True), which unpickles the index. A maliciously crafted saved brain can execute arbitrary code on load. Only load brains your application produced or fully trusts. Never expose an endpoint that loads a user-supplied brain directory.
7.1 Threat model
Quivr is a library running inside your trusted process. It does not provide authentication, authorization, multi-tenant isolation, or request rate limiting — those are the hosting application's responsibility (the LlamaIndex/LangChain trust model).
| Concern | Quivr's behaviour | Your responsibility |
|---|---|---|
| Secrets | Read from env via os.getenv; wrapped in SecretStr for clients; missing keys logged as warnings (values never logged). Brain.save excludes the OpenAI API key from embedder serialization. |
Inject keys securely; don't commit .env; rotate keys. |
| Untrusted documents | Parsed by third-party loaders (unstructured, MegaParse, Tika). |
Sandbox/limit parsing of untrusted uploads; the parsers are large attack surfaces. |
| Prompt injection | Default prompt restricts the model to provided context, but retrieved documents can still contain adversarial instructions. | Treat ingested content as untrusted; consider output filtering. |
| Data egress | Document chunks and metadata are sent to your configured LLM/embedding/reranker/web-search providers by default. | Choose providers per your data-residency needs; use local models for sensitive data. |
| Network calls | Tika (TIKA_SERVER_URL), Tavily, Langfuse, and provider APIs. |
Control egress; pin/validate the Tika endpoint. |
| Saved-brain pickle | allow_dangerous_deserialization=True (above). |
Only load trusted brains. |
7.2 Observability and PII
A global Langfuse CallbackHandler is attached to LLM calls (LangfuseService). If Langfuse env vars are configured, prompts, contexts, and answers are sent to Langfuse for tracing. Disable or self-host Langfuse if your data cannot leave your perimeter.
8. Operations
8.1 Embedding Quivr in a service
Quivr has no server of its own — you run it inside yours (FastAPI, Flask, a worker, etc.). Operational guidance:
- [ ] Run all Quivr operations on the async path inside async web frameworks (
afrom_files,aask,ask_streaming) to avoid the event-loop error (§9). - [ ] Build brains once and persist them with
save(); reload withload()to avoid repeated embedding cost and latency. - [ ] Cache
Braininstances per user/session (thequivr-whisperexample keeps adictof brains keyed by session id). - [ ] Pre-warm tokenizers with
LLMTokenizer.preload_tokenizers([...])at startup if you count tokens on a hot path. - [ ] Pin
quivr-coreand thelangchain*/langgraphversions — the LangChain ecosystem moves fast and the constraints are narrow (<0.4).
8.2 Persistence layout
brain.save(folder) writes folder/brain_<id>/ containing config.json (a BrainSerialized: id, name, chat history, LLM config, vector-store config, embedder config, storage config) and a vector_store/ FAISS index. Brain.load(folder/brain_<id>) reconstructs from those.
Note
save()/load()currently support only the FAISS vector store, the OpenAI embedder, andTransparentStorage/LocalStorage. Other combinations raise"can't serialize ... for now". For Postgres/pgvector and other stores you must manage persistence yourself.
8.3 Testing the library
# Inside core/, with dev deps installed (rye sync) and a base env:
pytest -m base # tests needing the `base` extra
pytest -m "not slow" # skip slow tests
Markers (core/pyproject.toml): base, tika (needs a Tika server), unstructured (needs the extra), slow. A Dockerized run is provided via core/scripts/run_tests.sh (builds Dockerfile.test, runs tox).
8.4 Building the docs site
cd docs
rye sync
rye run docs # mkdocs serve
rye run build_docs # mkdocs build --strict
9. Troubleshooting (Operational)
Symptom: RuntimeError: There is no current event loop
Cause: Synchronous wrapper (from_files/ask) invoked where an event loop already runs (ASGI handler, notebook). The wrappers use asyncio.get_event_loop().run_until_complete. (Issue #3650.)
Fix: Use afrom_files/aask/ask_streaming. Prevent: Standardize on async in service code.
Symptom: asyncpg / CancelledError / connection-pool warnings
Cause: Async lifecycle issues when using a Postgres-backed vector store outside a properly scoped event loop (issues #3654, #3557). Fix: Ensure DB connections are created and closed within the same running loop; scope the pool to the request/worker lifecycle. Prevent: Manage the event loop explicitly in long-running services.
Symptom: Pydantic ValidationError loading a workflow YAML
Cause: An unknown key (e.g. max_input_tokens) — models use extra="forbid".
Fix: Use the documented field names (§4); start from core/example_workflows/*.yaml.
Symptom: ValueError: The API key for supplier '<x>' is not set
Cause: Reranker/LLM supplier configured without its env key (rerankers validate at construction). Fix: Export the named variable before building the config.
Symptom: RuntimeError: can't send parse request to tika server
Cause: No reachable Tika server for PDFs (3 retries exhausted).
Fix: Start apache/tika or set TIKA_SERVER_URL; or install an alternative PDF parser.
Symptom: max_context_tokens is too low
Cause: Token limit set below 4096. Fix: Use ≥ 4096 for both context and output tokens.
10. Code Quality & Patterns
Strengths
- Clean separation of concerns behind ABCs (
StorageBase,ProcessorBase) and registries — extensible without editing core files. - Config-as-data: the entire retrieval pipeline is declarative YAML compiled to LangGraph, a genuinely flexible design.
- Async-first with thin sync wrappers; comprehensive Pydantic validation with helpful error messages.
- Provider abstraction isolates six LLM vendors behind one
LLMEndpoint. - A real pytest suite with environment markers (
base/tika/unstructured).
Weaknesses / tech debt (from in-code TODO/FIXME and observed behaviour)
- The sync wrappers'
get_event_loop().run_until_completepattern is fragile in modern async runtimes (the #1 reported bug). Brain.add_file()is declared butraise NotImplementedError— files can only be added at construction.LocalStorage.remove_file/TransparentStorage.remove_fileareNotImplementedError.save()/load()are hard-wired to FAISS + OpenAI embedder; other stores aren't serializable.parse_chunk_responseis annotated in-code as "CONVOLUTED LOGIC / redo this."- The legacy
QuivrQARAGengine duplicates retrieval logic now owned by the LangGraph engine. - The project's own README/docs YAML examples use the invalid
max_input_tokenskey (contradicting the code and the in-repo example workflows). quivr_rag.pynotes a lingering dependency on the umbrellalangchainpackage the maintainers want to drop.
11. Suggested Improvements
- Replace the sync event-loop wrappers with
anyio/asyncio.run-safe execution (or document async-only), closing the most common failure mode. - Implement
Brain.add_fileandremove_fileso brains are mutable without a full rebuild. - Generalize serialization so
save()/load()support any vector store and embedder via a registry, mirroring the processor design. - Fix the documentation YAML keys (
max_input_tokens→max_output_tokens) and add a config-schema doc generated from the Pydantic models. - Retire
QuivrQARAGor clearly mark it deprecated and remove the vestigialrag_pipelineargument fromask_streaming. - Harden untrusted-document ingestion with sandboxing guidance and optional parser timeouts/limits.
- Make the saved-brain format pickle-free (or signed) to remove the deserialization risk.
12. Contributing
- Repository:
github.com/QuivrHQ/quivr; the package lives incore/. - Tooling (dev deps in
core/pyproject.toml):ruff,flake8+flake8-black,mypy,pre-commit,pytest(+pytest-asyncio,pytest-xdist,pytest-benchmark). - Build backend:
hatchling; environments managed withrye(lockfiles checked in). - Run the suite via
pytestor the Dockerizedcore/scripts/run_tests.sh. - License: Apache 2.0.
Note
Public-issue activity has slowed and the community has asked whether the project is still maintained (issue #3681). For production use, pin a specific
quivr-coreversion and vendor/patch as needed.
13. Quick Reference
Default values at a glance
| Setting | Default |
|---|---|
| LLM model / supplier | gpt-4o / openai |
| Temperature | 0.3 |
max_context_tokens / max_output_tokens |
20000 / 4096 (min 4096) |
| Embedder | OpenAIEmbeddings |
| Vector store | in-memory FAISS |
| Storage | TransparentStorage (in-memory) |
| Chunk size / overlap | 400 / 100 tokens |
Retriever k |
40 |
Reranker top_n |
5 (off unless supplier set) |
max_history / max_files |
10 / 20 |
Essential APIs
from quivr_core import Brain
from quivr_core.llm import LLMEndpoint
from quivr_core.rag.entities.config import (
RetrievalConfig, LLMEndpointConfig, IngestionConfig,
)
brain = await Brain.afrom_files(name="b", file_paths=[...]) # build
cfg = RetrievalConfig.from_yaml("workflow.yaml") # configure
async for c in brain.ask_streaming("q", run_id=uuid4(), # answer
retrieval_config=cfg):
...
await brain.save("path"); Brain.load("path/brain_<id>") # persist