Model or dataset
Oqura-ai/deepdoc avatar
Oqura-ai/deepdoc

deepdoc runs one research agent per report section and lets a Jev model reject the evidence before writing starts

Deep research tool for local knowledge base.

312 stars47 forksPythonMIT

At a glance

What is it?
deepdoc is a Python deep-research tool that indexes your own PDFs, DOCX, PPTX, images, and text into Qdrant and writes a Markdown report. The unusual part is the TypeSafe Jev model sitting between retrieval and writing: it scores each candidate chunk for relevance, usable evidence, contradiction, and prompt-injection-like content, and its typed probabilities drive filtering and retry decisions in ordinary Python code.
Who is it for?
deepdoc fits a researcher who has a folder of source documents and wants a Markdown report that can be traced back to them, and its per subsection coverage reflection is the part worth borrowing even if you write your own retrieval. It does not fit a quick one-shot summary, since a run needs Qdrant running, two API keys, and a planner round trip you have to approve, and the Jev threshold values are starting points rather than calibrated numbers for your documents.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Jev gates the evidence before the writing model ever sees it

The design separates two jobs that usually share one. TypeSafe's Jev model does not replace the generative LLM. It checks and reranks the chunks returned by Qdrant, then checks whether the collected evidence actually covers every required subsection of the report. The configured LLM still handles planning, query generation, synthesis, and the writing itself.

What crosses the boundary is a number rather than a decision. Jev returns typed probabilities, and the thresholds, ranking, retry limits, and graph routing all stay in ordinary Python logic in the application. So the filtering is auditable: you can see that a chunk was dropped because its relevance probability sat below `jev_relevance_threshold`, rather than having to trust a prompt instruction.

The implemented path inside each parallel research agent is documented as a fixed sequence:

text
Query generation
  -> Qdrant retrieval
  -> Jev evidence gate and reranker
  -> Evidence accumulator
  -> Jev subsection coverage reflection
       -> Query generation again when coverage is missing and retries remain
       -> Section writer when coverage is sufficient or the retry limit is reached

Every research agent runs that loop for its own report section, and the final report writer combines the finished sections into one Markdown file.

One agent per section, with coverage checked per subsection

The report is planned before any research runs. A planner builds the initial structure from your topic and outline, and you approve it or ask for changes before research starts. That gate matters because the number of research agents equals the number of sections, so an approved outline is also an approval of your token budget.

Each section then gets its own agent with a fixed loop: generate focused research queries, pull a shortlist of candidate chunks from Qdrant, have Jev judge them, filter and rerank what survives, then write the section from the accepted local evidence only. The agent is not allowed to fill a gap from model memory, because the writing step is defined as a function of accepted local evidence.

The gap filling is bounded. Jev reflection scores identify subsections that still lack evidence, and the agent generates new queries for those subsections up to the configured reflection limit, which in `configuration.py` is `num_reflections: 2` alongside `max_queries: 3` and `search_depth: 2`. When the limit is reached the section is written anyway, which means an under-covered section is possible by design, and the reflection count is the knob that decides how much that matters to you.

Contradiction and prompt-injection-like content are scored, not filtered by keyword

The judgement Jev performs is four dimensional rather than one relevance score. Per candidate chunk it assesses relevance, whether the chunk is usable as evidence at all, whether it contradicts what has already been collected, and whether it looks like prompt-injection-like content. Only after that does the filter and rerank step run, and only what survives reaches the writing model.

This is the practical difference between deepdoc and a plain retrieve-then-prompt loop. In a local corpus you do not control the provenance of every file, and a document that contains instructions aimed at an assistant is a real input to an agent that reads it. Treating that as one of four scored dimensions means the suspicion is visible in the pipeline rather than left to the writing model to notice.

The contradiction dimension has a second use. Because a contradicting chunk is scored rather than silently preferred, a section that pulls in conflicting evidence has to resolve it in the text, and the threshold that decides what counts as contradiction, `jev_contradiction_threshold`, is one of the values you calibrate yourself.

Retrieval breadth and evidence kept are separate dials, and breadth costs money

Two numbers in the thread configuration control how much survives retrieval, and they behave differently. `n_points: 6` is how many candidate chunks come back per query. `evidence_keep_per_query: 3` is how many of those candidates continue to the evidence accumulator.

Raising `n_points` improves candidate recall, and the documentation is explicit about the price: every query and chunk pair is judged independently, so more candidates means more Jev evaluations. That is a per-pair cost rather than a per-query cost, which is why the two dials behave so differently in practice. Widening recall and then keeping only a few still pays for every judgement on the discarded ones.

The thresholds `jev_relevance_threshold: 0.45` and `jev_evidence_threshold: 0.55` are described as starting points rather than universal constants, and the advice is to evaluate them on representative local documents before changing them. In other words the defaults are deliberately uncalibrated, so a report produced with the shipped values tells you about the defaults, not about your corpus.

Timing is configurable in the same block, with `max_retries: 6` and `timeout: 120.0` in `LLM_CONFIG` for the provider, alongside `temperature: 0.5` and the default `openai` provider with `gpt-4o-mini`.

The default Jev model tracks the newest release until you pin one

The environment template ships `TYPESAFE_DEFAULT_MODEL=jev-latest`, which follows the newest stable Jev release rather than a fixed version. That is convenient while you are calibrating and a liability afterwards, because a threshold you validated against last month's probabilities is being applied to this month's model.

The stated remedy is sequencing: calibrate your thresholds for production first, then pin a versioned Jev model if you need completely repeatable behaviour across future releases. In other words reproducibility is something you add after tuning, not something you get by default.

The rest of the Jev configuration is small. `TYPESAFE_LOG_LEVEL=warning` keeps the SDK quiet unless something is wrong, and the Jev client itself uses a bounded retry policy for temporary 429 and 5xx responses, connection failures, and timeouts, which is separate from the `max_retries` figure that governs the generation model. Two keys are required for the default setup, `OPENAI_API_KEY` and `TYPESAFE_API_KEY`, with `MISTRAL_API_KEY` and `TAVILY_API_KEY` also present in the template. The TypeSafe key comes from the TypeSafe console and the instructions are to keep `.env` local and never commit the real key.

QDRANT_DISABLE_THREADING is the one setting marked do not change

The vector store is a local Qdrant you start yourself, configured with `QDRANT_URL=http://localhost:6333`, `COLLECTION_NAME=knowledge_base`, and `EMBEDDING_MODEL=BAAI/bge-small-en-v1.5`. The compose file runs the `qdrant/qdrant:latest` image as a container named `qdrant_localresearch` with ports 6333, 6334, and 6335, mounts `./qdrant_data` at `/qdrant/storage`, and injects a production config with `log_level: INFO`.

One variable carries an explicit warning in the template: `QDRANT_DISABLE_THREADING=true # Don't change this`. Everything else in the file can be edited with a guess at the consequence. That one cannot, and it is the sort of line that saves a debugging session if you leave it alone and wastes one if you do not.

The embedding choice is fixed in the same way in practice. `BAAI/bge-small-en-v1.5` is a small English model, and it pairs with the fastembed dependency in `requirements.txt`. Changing it means re-embedding the whole collection, since existing vectors will no longer be comparable with new ones.

Concurrency is capped at two sections because providers rate limit

`THREAD_CONFIG` sets `max_concurrency: 2` with the stated reason of limiting simultaneous section branches to reduce provider token bursts. Each branch gets its own `thread_id`, generated as a `uuid.uuid4()`, so the branches do not share graph state.

The knobs interact with your provider's limits rather than with your machine. Lower `max_concurrency` to `1` for the most conservative token usage, and raise it only when your provider limits have enough headroom. Because a run has one agent per approved section, a twelve section outline at concurrency 2 is six waves of parallel generation, each with its own queries, its own retrieval, and its own set of Jev judgements.

Error handling follows the same conservative bias. `max_retries: 6` in the generation config exists for temporary provider errors such as HTTP 429 responses, and the LangChain dependency range is pinned below 0.4.0 across langchain, langchain-openai, and langgraph, with the OpenAI integration pinned separately. `typesafe-sdk` is pinned at 0.7.1, so the Jev client will not drift underneath your thresholds on its own.

The run is three CLI answers, and the report lands in output_folder

Starting the application is `python main.py`, and the CLI asks for three things: a topic, an outline or goal, and the local resource directory. Completed reports are saved in `output_folder`. There is no web interface in the setup path, which makes the tool easy to script around and easy to overlook if you were expecting a UI.

Setup is a fixed sequence built around `uv`, which manages the virtual environment and dependencies: clone the repository, create the environment, activate it, copy the environment template, install requirements, start Qdrant, then run.

bash
git clone https://github.com/Oqura-ai/deepdoc.git
cd deepdoc
uv venv
bash
uv pip install -r requirements.txt
bash
docker-compose up --build

Activation differs by platform, with `.venv\Scripts\activate` on Windows and `source .venv/bin/activate` on macOS and Linux, and the environment file starts as `cp .env.example .env`. The source layout is flat and readable: `main.py` at the root, `configuration.py` for behaviour, a `deepresearch/` package for the agents, `resource/` for input documents, `notebook/` for experiments, and `output_folder/` for results. Parsing support comes from pymupdf and pdfplumber for PDF, python-docx and python-pptx for Office formats, with qdrant-client and fastembed for storage and embeddings.

Editorial conclusion

deepdoc fits a researcher who has a folder of source documents and wants a Markdown report that can be traced back to them, and its per subsection coverage reflection is the part worth borrowing even if you write your own retrieval. It does not fit a quick one-shot summary, since a run needs Qdrant running, two API keys, and a planner round trip you have to approve, and the Jev threshold values are starting points rather than calibrated numbers for your documents. Before trusting a report, read the citations in it, decide whether you need a pinned Jev version instead of `jev-latest` for repeatability, and set `max_concurrency` to 1 if your provider has no rate limit headroom.

Frequently asked questions

What does deepdoc need before it will run?

A uv managed virtual environment, dependencies from requirements.txt, a local Qdrant started with docker-compose up --build, and API keys in .env. OPENAI_API_KEY and TYPESAFE_API_KEY are required for the default setup, and the .env file is created by copying .env.example.

Where does deepdoc store its vectors?

In a Qdrant instance at QDRANT_URL=http://localhost:6333, using COLLECTION_NAME=knowledge_base and the BAAI/bge-small-en-v1.5 embedding model. The compose file runs qdrant/qdrant:latest as a container named qdrant_localresearch and mounts ./qdrant_data for storage.

What does the Jev model do in deepdoc?

It checks and reranks the chunks Qdrant returns, judges relevance, usable evidence, contradiction, and prompt-injection-like content, and reports whether collected evidence covers each required subsection. The configured LLM still does planning, query generation, synthesis, and writing, while Python uses Jev probabilities for filtering and retry decisions.

Which file types can deepdoc index?

PDF, DOCX, PPTX, images, TXT, and Markdown. The requirements include pymupdf and pdfplumber for PDF handling, python-docx and python-pptx for Office documents, and fastembed for embeddings, with page and source metadata kept on every chunk.

How do I make deepdoc runs repeatable?

The default TYPESAFE_DEFAULT_MODEL=jev-latest follows the newest stable release, so pin a versioned Jev model after calibrating thresholds for production. In configuration.py, jev_relevance_threshold and jev_evidence_threshold are starting points to evaluate on your own documents, and lowering max_concurrency to 1 is the conservative option for token usage.

Official sources

  1. Issues
  2. License: MIT
  3. Oqura-ai/deepdoc on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/oqura-ai-deepdoc.svg)](https://hysenlabs.com/projects/oqura-ai-deepdoc)