EctoCompass is an educational medical question-answering app for ectodermal dysplasia, especially hypohidrotic ectodermal dysplasia. It answers from a curated corpus, preserves source metadata, shows citation support, and avoids becoming a medical-record or diagnosis system.
The app is built around a safety-first retrieval pipeline:
- classify the request and detect urgent-risk or PHI-like input
- retrieve evidence from approved local, Postgres, and freshness sources
- assemble evidence spans with source tier, source id, document id, dates, and URLs
- draft short answers with sentence-level citations
- verify citation support before returning the answer
- fall back instead of returning unsupported medical claims
EctoCompass is for education only. It does not diagnose, provide emergency care, or give individualized treatment instructions. Do not enter names, contact details, medical record numbers, uploads, or personal medical histories.
Answers preserve source-quality labels:
Tier A - Registry or official medical sourceTier B - Literature or permitted medical referenceTier C - Practical support guidance
Biomedical claims should be grounded in Tier A or Tier B sources. Tier C material is for practical support context and is labeled as practical support guidance.
app/ FastAPI app, retrieval, answering, safety, freshness, worker code
manifests/ Curated source manifest and source metadata schema inputs
migrations/ Postgres schema migrations
schemas/ JSON schemas
scripts/ Local ingestion, database, eval, embedding, and health commands
sources/ Local PDF source files, subject to license/reuse review
storage/ Local generated artifacts, ignored by git
tests/ Unit and integration-style regression tests
This path uses the deterministic seed/local retrieval fallback and does not require Postgres.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
$env:SOURCE_MANIFEST_PATH="manifests/source_manifest.json"
$env:RETRIEVAL_BACKEND="seed"
python -m uvicorn app.api:app --reload --host 127.0.0.1 --port 8000Open http://127.0.0.1:8000.
Check the API:
Invoke-RestMethod `
-Method Post `
-Uri "http://127.0.0.1:8000/chat/preview" `
-ContentType "application/json" `
-Body '{"question":"What is hypohidrotic ectodermal dysplasia?"}'Docker Compose starts the API, worker, Postgres with pgvector, and Redis.
Copy-Item .env.example .env
docker compose up --buildThe default .env.example sets RETRIEVAL_BACKEND=postgres. For a clean local database, initialize it before relying on Postgres retrieval:
docker compose up -d postgres redis
$env:DATABASE_URL="postgresql://ectocompass:ectocompass@localhost:5432/ectocompass"
python -m scripts.apply_migrations
python -m scripts.ingest_source_documents --include-reviewed-full-text
python -m scripts.load_postgres_corpusOnly use --include-reviewed-full-text after verifying license and reuse terms for every PDF under sources/.
After loading the corpus:
docker compose up --buildOpen http://127.0.0.1:8000.
EctoCompass defaults to the deterministic extractive composer:
$env:ANSWER_COMPOSER_MODE="extractive"In this mode, no answer-generation LLM is called. The app selects supported sentences from retrieved evidence, applies source-tier rules, and verifies citations locally.
You can enable a local LLM through Ollama for more natural synthesis while keeping the same evidence and verifier boundaries. The local LLM is optional and never replaces retrieval or citation verification.
When ANSWER_COMPOSER_MODE=llm, app.api builds an LLMAnswerComposer and passes it into QuestionService.
For each question:
QuestionServiceruns the PHI guard and request classifier.- Retrieval returns an
EvidencePackcontaining only approved spans. assemble_evidenceclusters and ranks candidate spans.bounded_composer_inputcreates the only payload the model may see:- the user question
- the request label
- allowed span IDs
- retrieved span text
- source tier labels
- content domains
- publication, update, and retrieval dates
- For Ollama, EctoCompass sends a local chat request to
OLLAMA_API_URLwith:stream: falsethink: falsetemperature: 0- a JSON schema in
format
- The model must return structured JSON with:
claimssupporting_span_idssource_tier_labeluncertainty_statement
BoundedComposerOutput.from_dictrejects claims that cite span IDs outside the evidence pack.verify_answerchecks that each returned claim is supported by the cited span text.- If the provider call, JSON parse, span validation, or citation verification fails, EctoCompass falls back to the deterministic extractive composer.
The LLM does not receive the whole corpus, cannot browse, and cannot cite sources that retrieval did not return. It is used only to phrase claims from retrieved evidence spans.
Install Ollama, then pull a local chat model. The project defaults to qwen3:8b for local synthesis.
ollama pull qwen3:8b
ollama serveLeave Ollama running while EctoCompass is running.
In a new PowerShell session from the repository root:
.\.venv\Scripts\Activate.ps1
$env:SOURCE_MANIFEST_PATH="manifests/source_manifest.json"
$env:RETRIEVAL_BACKEND="seed"
$env:ANSWER_COMPOSER_MODE="llm"
$env:ANSWER_LLM_PROVIDER="ollama"
$env:ANSWER_MODEL="qwen3:8b"
$env:ANSWER_API_TIMEOUT_SECONDS="60"
$env:OLLAMA_API_URL="http://localhost:11434/api/chat"
python -m uvicorn app.api:app --reload --host 127.0.0.1 --port 8000Confirm the app sees the local LLM configuration:
Invoke-RestMethod "http://127.0.0.1:8000/api/info"Expected fields:
answer_composer_mode: llm
answer_llm_provider: ollama
answer_model: qwen3:8b
Ask a debug question and inspect answer.metadata:
Invoke-RestMethod `
-Method Post `
-Uri "http://127.0.0.1:8000/chat/preview?debug=true" `
-ContentType "application/json" `
-Body '{"question":"How does reduced sweating relate to overheating in HED?"}'When the local model succeeds, metadata includes:
generation_mode: llm_structured_evidence_composer
provider: ollama
model: qwen3:8b
verification.passed: true
If Ollama is stopped or returns invalid output, the request should still complete with deterministic fallback metadata such as:
generation_mode: structured_evidence_composer
llm_fallback_to_extractive: true
When the API runs inside Docker and Ollama runs on the host machine, use Docker's host gateway URL.
Update .env:
ANSWER_COMPOSER_MODE=llm
ANSWER_LLM_PROVIDER=ollama
ANSWER_MODEL=qwen3:8b
ANSWER_API_TIMEOUT_SECONDS=60.0
OLLAMA_API_URL=http://host.docker.internal:11434/api/chatThen run:
docker compose up --buildThe API container will call the Ollama server running on the host.
The same bounded composer can call OpenAI instead of Ollama:
$env:ANSWER_COMPOSER_MODE="llm"
$env:ANSWER_LLM_PROVIDER="openai"
$env:ANSWER_MODEL="gpt-5.4-nano"
$env:OPENAI_API_KEY="..."Hosted LLM output goes through the same span validation, citation verification, and fallback behavior.
Research and trial questions can use bounded live metadata checks when FRESHNESS_ENABLED=true.
Supported freshness sources are:
- ClinicalTrials.gov
- NCBI/PubMed metadata
Freshness calls are metadata-oriented. The app does not browse arbitrary websites and should not ingest full text unless reuse rights are verified.
Run tests:
python -m pytestRun focused answer and configuration tests:
python -m pytest tests/test_answering.py tests/test_embeddings.pyRun local evals with seed retrieval:
python -m scripts.run_local_evals --backend seedRun local evals with Postgres retrieval:
$env:DATABASE_URL="postgresql://ectocompass:ectocompass@localhost:5432/ectocompass"
python -m scripts.run_local_evals --backend postgresCheck run health:
python -m scripts.run_healthCheck database-backed health:
$env:DATABASE_URL="postgresql://ectocompass:ectocompass@localhost:5432/ectocompass"
python -m scripts.run_health --with-dbThe MVP defaults are intentionally conservative:
STORE_USER_HEALTH_TEXT=falseLOG_RAW_USER_TEXT=falsePER_USER_TELEMETRY_ENABLED=false- document uploads are not supported
- symptom diaries and saved patient histories are not supported
- urgent symptoms get safety footer language
For severe or urgent symptoms, users should contact a clinician or local emergency services. EctoCompass is an educational tool, not clinical care.