English | 简体中文
A DSM-5 diagnostic criteria knowledge QA system built on hybrid retrieval (vector search + keyword search), supporting queries in both Chinese and English.
The system provides two service entry points:
- FastAPI microservice (
dsm5-rag-api) — full RESTful API with integrated DeepSeek LLM for professional answer generation - FastMCP service (
dsm5-rag-mcp) — pure retrieval service over the MCP protocol, with no external LLM dependency, ideal for IDE integration
- Hybrid Retrieval — vector semantic search (MiniLM multilingual + Chroma) combined with a real BM25 keyword channel (
llama-index-retrievers-bm25), supportingHYBRID/VECTOR_ONLY/KEYWORD_ONLY/FUSIONmodes - Scale-safe fusion —
HYBRIDfuses the two channels by rank (Reciprocal Rank Fusion fromQueryFusionRetriever),FUSIONmin-max normalizes each channel and appliesvector_weight; keyword scores are normalized to 0~1 so the similarity cutoff, MCP rendering andretrieval_statsall behave the same for both channels - Precise terminology matching — BM25 handles the DSM-5 cases where exact strings matter (
296.3x,Major Depressive Disorder,anhedonia), which pure embedding search tends to blur - Multilingual Support — uses the
paraphrase-multilingual-MiniLM-L12-v2multilingual embedding model, supporting both Chinese and English queries - DeepSeek LLM — integrates the DeepSeek API to generate professional answers from retrieved content (FastAPI service)
- Pure Retrieval MCP — the FastMCP service strips out the LLM and exposes only retrieval capabilities, ideal for IDE and toolchain integration
- Real token streaming —
stream=truestreams the answer token by token over NDJSON and reportsttft_ms(time to first token) separately from generation time - Request-scoped retrieval engine —
mode/similarity_top_k/vector_weightare assembled per request, so concurrent requests cannot pollute each other;/mode/changeonly changes the service-level defaults - Timing you can trust — responses carry
retrieval_ms/llm_ms/total_msmeasured around the actual work - Automatic index management with change detection — an
index_manifest.jsonrecords each document's sha256/mtime; unchanged corpora are loaded instead of re-embedded, added/edited files trigger a rebuild. UsePOST /index/rebuildandGET /index/diff - RESTful API — built on FastAPI with a complete REST API
- Document Format Support — parses PDF, DOCX, and TXT documents
- ChromaDB Persistence — the index (vector store + BM25 index) is persisted automatically and survives restarts
- Python >= 3.10
- uv package manager (recommended) or pip
# Clone the repository
git clone <your-repo-url> && cd myDSM5RAG
# Install with uv (recommended)
uv sync --group dev
# Or with pip
pip install -e .Copy the environment template and edit it:
cp .example.env .envConfigure your DeepSeek API key in .env (get one from platform.deepseek.com):
DEEPSEEK_API_KEY="sk-your-api-key-here"Note: Without an API key, the FastAPI service runs in MockLLM simulation mode. The MCP service is unaffected (pure retrieval, no LLM needed).
Place your DSM-5 diagnostic criteria documents (PDF/DOCX/TXT) into the dsm5_documents/ directory.
# With uv
uv run dsm5-rag-api
# Or directly with uvicorn
uv run uvicorn dsm5_rag.api:app --host 127.0.0.1 --port 8031Once the service is running:
- API docs (Swagger UI): http://127.0.0.1:8031/docs
- ReDoc: http://127.0.0.1:8031/redoc
# stdio mode (IDE integration, e.g. Claude Desktop, Cursor)
uv run dsm5-rag-mcp
# Streamable HTTP mode (remote/production)
uv run python -c "from dsm5_rag.mcp_server import main_http; main_http()"
# Or debug with fastmcp dev mode
uv run fastmcp dev src/dsm5_rag/mcp_server.py# Basic query (using the Python SDK)
uv run python examples/basic_query.py
# Batch queries & keyword search
uv run python examples/batch_query.py
# HTTP API client (requires the API service to be running)
uv run python examples/api_client.pyAll configuration is set via environment variables (see .env):
| Variable | Default | Description |
|---|---|---|
API_HOST |
localhost |
Server listen address |
API_PORT |
8000 |
Server port |
API_RELOAD |
false |
Enable hot reload |
CORS_ORIGINS |
* |
Allowed CORS origins |
LOG_LEVEL |
INFO |
Log level |
| Variable | Default | Description |
|---|---|---|
DEEPSEEK_API_KEY |
"" |
API key (falls back to MockLLM if unset) |
DEEPSEEK_BASE_URL |
https://api.deepseek.com |
API base URL |
DEEPSEEK_MODEL |
deepseek-v4-flash |
Model name |
| Variable | Default | Description |
|---|---|---|
EMBEDDING_MODEL |
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 |
Embedding model name |
EMBEDDING_DEVICE |
cpu |
Runtime device (cpu / cuda) |
HF_ENDPOINT |
https://hf-mirror.com |
HuggingFace mirror endpoint |
MODELS_DIR |
./models |
Local model cache directory |
| Variable | Default | Description |
|---|---|---|
PERSIST_DIR |
./chroma_dsm5_db |
ChromaDB persistence directory |
DOCUMENTS_PATH |
./dsm5_documents |
Document path (directory or single file) |
| Variable | Default | Description |
|---|---|---|
DEFAULT_MODE |
HYBRID |
Default retrieval mode |
DEFAULT_TOP_K |
5 |
Default number of results |
DEFAULT_VECTOR_WEIGHT |
0.7 |
Default vector weight (FUSION mode) |
SIMILARITY_CUTOFF |
0.2 |
Similarity filter threshold (applies to both channels; scores are 0~1) |
MAX_SOURCES |
3 |
Maximum number of sources returned by /query and /search/keyword |
ENABLE_CONTEXT_REORDER |
true |
Enable context reordering (Lost-in-the-Middle optimization) |
BATCH_CONCURRENCY |
4 |
asyncio.Semaphore limit for POST /query/batch |
STREAM_CONTEXT_CHAR_LIMIT |
24000 |
Character cap on the prompt context used for streaming answers |
| Variable | Default | Description |
|---|---|---|
BM25_LANGUAGE |
en |
Stopword language for the keyword channel (en/de/nl/fr/es/pt/it/ru/sv/no/zh/tr/ko/da) |
BM25_SKIP_STEMMING |
false |
Disable stemming (keeps disorder and disorders strictly distinct) |
| Variable | Default | Description |
|---|---|---|
CHUNK_SIZE |
1024 |
Document chunk size (tokens for the sentence splitter) |
CHUNK_OVERLAP |
100 |
Chunk overlap size |
MAX_FILE_SIZE_MB |
50 |
Maximum file size (MB) |
Note on
API_PORT: the code default is8000, while.example.envships8031as an example. Set it explicitly if you run both services side by side.Note on Chroma:
dsm5-rag-apianddsm5-rag-mcpopen the samePERSIST_DIR. Run one service at a time per directory, or give each service its ownPERSIST_DIR, to avoid SQLite lock contention.
myDSM5RAG/
├── src/dsm5_rag/ # Core package
│ ├── __init__.py # Package entry, public API exports
│ ├── api.py # FastAPI microservice (with LLM)
│ ├── background.py # Thread pool / run_in_executor (keeps the event loop free)
│ ├── config.py # Configuration model (RagConfig)
│ ├── index_manifest.py # Per-document sha256/mtime manifest, rebuild decisions
│ ├── keyword_index.py # BM25 keyword index (build/persist/load/migrate + score normalization)
│ ├── mcp_server.py # FastMCP pure retrieval service (no LLM)
│ ├── models.py # Pydantic data models
│ ├── parser.py # Document parser (PDF/DOCX/TXT)
│ ├── retriever.py # HybridRetriever (RRF / weighted fusion on top of core's QueryFusionRetriever)
│ └── system.py # RAG system core (DeepSeekRAGSystem)
├── scripts/
│ └── repro_keyword_channel.py # Keyword-channel diagnostic (runs before/after changes)
├── tests/ # Test suite
│ ├── conftest.py # Fixtures: temp-dir isolation + fake embedding (no model download)
│ ├── fake_system.py # Fake RAG system for API contract tests
│ ├── lexical_embedding.py # Deterministic hashing embedding used by tests
│ ├── fixtures/corpus/ # 5 real DSM-5 excerpt .txt files
│ ├── test_api.py # Endpoint contracts incl. NDJSON stream format
│ ├── test_config.py # Configuration tests
│ ├── test_index_manifest.py # Manifest / change-detection tests
│ ├── test_keyword_index.py # BM25 recall tests on the real corpus
│ ├── test_mcp_server.py # MCP registration + tool behaviour tests
│ ├── test_models.py # Data model tests
│ ├── test_parser.py # Document parser tests
│ ├── test_retriever.py # Fusion semantics tests (scale invariance, weights, top_k)
│ ├── test_streaming.py # Real streaming + event-loop responsiveness
│ └── test_system_integration.py # End-to-end retrieval with the sample corpus
├── examples/ # Usage examples
│ ├── basic_query.py # Basic query
│ ├── batch_query.py # Batch queries & keyword search
│ └── api_client.py # HTTP API client
├── dsm5_documents/ # Documents directory (gitignored)
├── chroma_dsm5_db/ # ChromaDB data + index_manifest.json (gitignored)
├── models/ # Local model cache (gitignored)
├── .github/workflows/ci.yml # CI: uv sync -> ruff check -> pytest --cov
├── .env # Environment variables (gitignored)
├── .example.env # Environment template
├── pyproject.toml # Project config, dependencies, ruff/pytest settings
└── uv.lock # uv lock file
| Method | Path | Description |
|---|---|---|
| GET | / |
Service info |
| GET | /health |
Health check |
| GET | /status |
System status |
| GET | /index/status |
Index status |
| GET | /index/diff |
Documents changed since the last index build (added/changed/removed/config) |
| POST | /index/rebuild |
Rebuild the index (force, background; default background=true returns 202) |
| GET | /documents |
Document list |
| POST | /query |
Execute a query (streaming supported) |
| POST | /search/keyword |
BM25 keyword search (cutoff + max_sources applied; verbose for full text) |
| POST | /query/batch |
Batch queries (max 20 questions, BATCH_CONCURRENCY semaphore) |
| POST | /mode/change |
Change service-level default retrieval parameters |
application/x-ndjson, one JSON object per line:
{"event":"query_started","question":"...","mode":"HYBRID","similarity_top_k":5,"timestamp":"..."}
{"event":"retrieval_completed","count":5,"retrieval_ms":12.3}
{"event":"answer_chunk","chunk":"部","is_complete":false} # 一次或多次,token 级
...
{"event":"answer_chunk","chunk":"","is_complete":true,"answer_chars":512,
"retrieval_ms":12.3,"llm_ms":820.5,"ttft_ms":210.4,"total_ms":833.1}
{"event":"sources","sources":[{"rank":1,"file_name":"dsm5.txt","retrieval_type":"both","score":1.0}],
"total_sources":1,"retrieval_stats":{"vector":1,"keyword":1,"both":1}}
{"event":"query_completed","timestamp":"...","total_ms":833.1}
On failure the stream ends with {"event":"error","error":"..."}. format=text returns the same events rendered as plain text for Swagger UI.
MCP (Model Context Protocol) is a standardized protocol that provides LLM applications with secure, unified access to context and tools.
| Tool | Description |
|---|---|
build_index |
Build/rebuild the retrieval index, supports force_rebuild (compares the document manifest first) |
get_index_status |
Query index build status (ready / building / not_built) |
search |
Hybrid/vector/BM25-keyword retrieval, supports four modes; results are cutoff- and max_sources-filtered |
keyword_search |
Pure BM25 keyword retrieval (same truncation rules as search) |
list_documents |
List indexed documents |
| URI | Description |
|---|---|
dsm5://status |
Overall system status |
dsm5://index/status |
Index build status |
dsm5://config |
Configuration overview (secrets excluded) |
dsm5://documents |
Document file list |
| Template | Description |
|---|---|
dsm5_search_assistant |
Retrieval assistant system prompt with tool descriptions and retrieval mode guidance |
dsm5_differential_diagnosis(symptoms) |
Generates a differential diagnosis retrieval prompt from symptom descriptions |
| Mode | Description |
|---|---|
HYBRID |
Retrieves from both channels and fuses them by rank (Reciprocal Rank Fusion), then rescales the fused score to 0~1 |
VECTOR_ONLY |
Vector semantic search only |
KEYWORD_ONLY |
BM25 keyword search only; nodes with zero term overlap are dropped |
FUSION |
Min-max normalizes each channel, then blends with vector_weight / 1 - vector_weight |
All four modes return non-None scores in 0~1, so SIMILARITY_CUTOFF, the MCP result renderer and retrieval_stats behave consistently.
Dropping new files into DOCUMENTS_PATH is not enough on its own — the running service has to rebuild:
# see whether the index is behind the documents
curl -s localhost:8000/index/diff
# rebuild in the background (returns 202 immediately; poll /index/status)
curl -s -X POST "localhost:8000/index/rebuild?force=false"
# small corpora only: wait for the result inline
curl -s -X POST "localhost:8000/index/rebuild?force=true&background=false"build_index() re-scans the documents and compares each file's sha256 (plus chunking-related config) against PERSIST_DIR/index_manifest.json. Nothing changed → the existing index is loaded (no re-embedding). Something changed → the index is rebuilt and the manifest is rewritten. Note the rebuild re-embeds the whole corpus; per-document incremental embedding is not implemented yet.
Legacy keyword indexes (keyword_index/docstore.json from the old SummaryIndex layout) are migrated to BM25 in place on first load, without re-computing embeddings.
Tests never download embedding weights and never touch chroma_dsm5_db/: tests/conftest.py
points documents/persist/models at temp directories and installs a deterministic hashing
stand-in for HuggingFaceEmbedding (plus MockLLM when no API key is set).
# Run all tests
uv run pytest
# Run a specific test file
uv run pytest tests/test_api.py -v
# Run MCP server tests (registration + real tool calls against the sample corpus)
uv run pytest tests/test_mcp_server.py -v
# Coverage (pytest-cov is part of the dev group)
uv run pytest --cov=dsm5_rag --cov-report=term-missing
# Lint / format
uv run ruff check src tests scripts
uv run ruff format --check src/dsm5_rag
# Keyword-channel diagnostic: prints scores, fusion contributions, timings
uv run python scripts/repro_keyword_channel.pyThe suite covers: BM25 recall on the real tests/fixtures/corpus excerpts, fusion semantics
(rank-based scale invariance, vector_weight, top_k), the API contracts including the NDJSON
stream shape, real token streaming with an event-loop responsiveness check, index change
detection, and persistence/migration of the keyword index.
CI (.github/workflows/ci.yml) runs uv sync --frozen → ruff check → pytest --cov.
- API framework: FastAPI + uvicorn
- MCP framework: FastMCP (v3)
- RAG: LlamaIndex (vector index,
QueryFusionRetriever, retrieval query engines) - Keyword retrieval: llama-index-retrievers-bm25 on top of
bm25s+ PyStemmer - Vector database: ChromaDB
- Embedding model: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
- LLM: DeepSeek API
- Data validation: Pydantic
- Document parsing: pypdf / python-docx
- Package management: uv
- Lint/tests: ruff + pytest (+ pytest-cov), GitHub Actions
- Build: Hatchling
For MCP-capable IDEs such as Claude Desktop, Cursor, and VS Code:
{
"mcpServers": {
"dsm5-rag-mcp": {
"command": "uv",
"args": ["run", "dsm5-rag-mcp"],
"cwd": "/path/to/myDSM5RAG"
}
}
}{
"mcpServers": {
"dsm5-rag-mcp": {
"url": "http://host:8000/mcp",
"transport": "streamable-http"
}
}
}