Skip to content

MedicalRAG — developer guide

app/services/rag_engine.py implements MedicalRAG: indexing and search for the AI Medical Assistant knowledge base.

Repo: https://github.com/valera7623/AI-medical-assistant.

Enabling

from app.services.rag_engine import is_rag_enabled, rag_mode, get_medical_rag

is_rag_enabled()  # RAG_ENABLED and MEDICAL_ASSISTANT_ENABLED
rag_mode()        # local | external | hybrid (invalid → local)

Global singleton: get_medical_rag() (thread-safe lazy init).

Modes

RAG_MODE Chroma External POST …/search Notes
local yes no memory fallback if Chroma unavailable
external skipped yes soft-fallback to local when empty
hybrid yes yes merge_hits with RAG_HYBRID_LOCAL_WEIGHT

Variables:

Env Purpose
RAG_COLLECTION_NAME Collection name
RAG_EXTERNAL_URL External API base (…/v1)
RAG_EXTERNAL_API_KEY Bearer (optional)
RAG_EXTERNAL_TIMEOUT_SECONDS HTTP timeout
RAG_HYBRID_LOCAL_WEIGHT Local weight in hybrid (0–1)
CHROMA_PERSIST_DIR Chroma persistence path

Language filter

Only two lanes: en and ru.

from app.services.rag_engine import normalize_rag_language, VALID_RAG_LANGUAGES

normalize_rag_language("ru-RU")  # → "ru"
normalize_rag_language("en_US")  # → "en"
normalize_rag_language(None)     # → DEFAULT_LANGUAGE normalized to en|ru

Search drops hits whose language does not match the request. UI and seed must set language explicitly.

Document shape

Normalized document:

{
  "id": "guidelines:acs-en",
  "title": "ACS — initial steps",
  "category": "guidelines",
  "language": "en",
  "text": "...",
  "metadata": {
    "title": "...",
    "category": "guidelines",
    "language": "en",
    "source_file": "guidelines.json"
  }
}

Methods:

  • upsert_documents(docs) → int — write local (admin ingest also POSTs to external /ingest when configured);
  • search(query, *, language, limit=5) → list[hit] — normalized hits with similarity, source.

Hit after _normalize_hit:

{
  "text": "...",
  "title": "...",
  "category": "...",
  "language": "en",
  "similarity": 0.82,
  "source": "local",  # local | external | hybrid
  "id": "..."
}

Hybrid merge

merge_hits(local, external, limit=5, local_weight=0.5):

  1. Scales similarity by local / external weights.
  2. Dedupes by id or text hash.
  3. Sorts by similarity descending.

Unit tests: tests/test_rag_engine.py.

Seed

Script scripts/seed_medical_knowledge.py:

# Inside aima-app container
docker compose exec app python scripts/seed_medical_knowledge.py --also-db

docker compose exec app python scripts/seed_medical_knowledge.py \
  --from-url http://mock_rag:8090/v1/documents.json \
  --api-key "$RAG_EXTERNAL_API_KEY" \
  --skip-local-files

Sources:

  • app/data/medical_knowledge/guidelines.json
  • app/data/medical_knowledge/diseases.json
  • app/data/medical_knowledge/medications.json
  • optional JSON dump via --from-url

Mock external API

scripts/mock_rag_server.py and Compose profile mock-rag (container aima-mock-rag):

docker compose --profile mock-rag up -d mock_rag

Endpoints: /health, POST /v1/search, POST /v1/ingest, GET /v1/documents.json.

For the app on the same network: RAG_EXTERNAL_URL=http://mock_rag:8090/v1.

Tutor integration

app/services/ai_tutor.py calls MedicalRAG when answering; routes:

Path Purpose
POST /api/tutor/ask Answer + sources
POST /api/tutor/knowledge/ingest Admin upsert
GET /api/tutor/knowledge/status Config without secrets

MEDICAL_ASSISTANT_ENABLED gates all learning APIs (/api/tutor, /api/cases, /api/exam, /api/progress).

Local stack

Service Port / name
App http://localhost:8001 (APP_PORT)
Redis host 6380
Containers aima-*
Mock RAG 8090 (profile mock-rag)

Extending

  1. Always set language on documents.
  2. New external KBs must implement /search and optionally /ingest.
  3. Do not commit secrets (RAG_EXTERNAL_API_KEY) — only .env.example.
  4. After changing RAG_MODE, restart the app container and re-seed if needed.