- Flag
- Description
- -s, --screenshot-dir <path>
- Directory to save page screenshots. Defaults to screenshots/ .
- -w, --overwrite
- Overwrite existing screenshots. Defaults to false .
- -c, --chunk-limit <number>
- Number of chunks to return. Defaults to 1 .
Specs
- FlagDescription
- -s, --screenshot-dir <path>Directory to save page screenshots. Defaults to screenshots/ .
- -w, --overwriteOverwrite existing screenshots. Defaults to false .
- FlagDescription
- -c, --chunk-limit <number>Number of chunks to return. Defaults to 1 .
- FlagDescription
- -r, --resume <session-id>Resume a previous agent session.
- FlagDescription
- --category <category>Filter questions by category.
- --output <path>Save results to a JSON file.
- --quietSuppress per-question output.
- CategoryScore
- cross_category_reasoning100.0%
- direct_lookup100.0%
- disambiguation100.0%
- negation_absence100.0%
- synonym_paraphrase97.0%
- brand_generic_resolution66.7%
- aggregation_counting33.3%
README
med-qa
An open source, local agent QA workflow for ingesting, searching, and querying a medical factsheet using vector embeddings.
Documents are parsed and converted to page-level screenshots by LiteParse, chunked with Chonkie, embedded with Google's Gemini embedding model, and stored in a local LanceDB vector database. A Claude-backed agent can then answer questions against this knowledge base using a two-step retrieval strategy: text search first, with optional image lookup for visual context grounding if the text-base information is insufficient or ambiguous.
Requirements
- Bun v1.0 or later
- A Google AI API key (for embeddings via
gemini-embedding-2-preview) - An Anthropic API key (for the agent)
Setup
Install the necessary dependencies:
bun install
Set the required environment variables:
export GOOGLE_API_KEY="..."
export ANTHROPIC_API_KEY="..."
Usage
Process a document
Parse, chunk, embed, and store a document into the local knowledge base:
bun run process <file>
Options:
| Flag | Description |
|---|---|
-s, --screenshot-dir <path> |
Directory to save page screenshots. Defaults to screenshots/. |
-w, --overwrite |
Overwrite existing screenshots. Defaults to false. |
Example (using this sample file):
bun run process ./data/Medication_Side_Effect_Flyer.pdf
Search
Run a vector similarity search against the knowledge base:
bun run search "<query>"
Options:
| Flag | Description |
|---|---|
-c, --chunk-limit <number> |
Number of chunks to return. Defaults to 1. |
Example:
bun run search "contraindications for ibuprofen" --chunk-limit 5
Agent
Use a Claude-backed agent with extended thinking to answer questions against the knowledge base. The agent follows a two-step retrieval process: it runs a text search first and, if needed, retrieves full-page screenshots for visual content such as charts or tables.
bun run agent "<query>"
Options:
| Flag | Description |
|---|---|
-r, --resume <session-id> |
Resume a previous agent session. |
Example:
bun run agent "What would i take Esomeprazole for and what could its side effects be?"
To resume a session:
bun run agent "Follow-up question" --resume <session-id>
Evaluation
Run the evaluation suite. Each question is sent to the agent, and the response is scored against expected answers. Tool usage (search and image calls) is tracked per question.
bun run eval
Options:
| Flag | Description |
|---|---|
--category <category> |
Filter questions by category. |
--output <path> |
Save results to a JSON file. |
--quiet |
Suppress per-question output. |
Example:
bun run eval --output results.json
bun run eval --category aggregation_counting
The eval suite uses 20 questions from src/eval/gold.json across 7 categories (direct_lookup, synonym_paraphrase, brand_generic_resolution, cross_category_reasoning, negation_absence, aggregation_counting, disambiguation). Answers are scored with type-specific strategies: set F1 (with alias resolution), exact match, boolean, numeric, and free-text via Claude Sonnet as LLM judge. Per-question output includes tool call counts and flags questions where the agent used the image fallback tool ([img]).
How it works
-
Processing —
@llamaindex/liteparseparses the document and produces per-page text and screenshots. Text is chunked with@chonkiejs/coreusing a recursive strategy (max 4096 characters per chunk). Each chunk is embedded via the Gemini embedding API (3072 dimensions) and stored in a local LanceDB table with an HNSW-SQ index. -
Search — A query is embedded with the same model and a vector similarity search is run against the LanceDB table, returning the closest matching text chunks and their associated screenshot paths.
-
Agent — A Claude agent is initialized with a retrieval MCP server that exposes two tools:
search(vector search) andget_image(reads a screenshot by path and returns it as a base64 image). The agent uses extended thinking and is instructed to rely only on retrieved content rather than prior knowledge.
Representative results
The agent QA results below are from the evaluation suite, built using the Claude Agent SDK
and the Sonnet 4.6 model. Depending on the model and harness you use, results may vary.
We designed a 20-question eval suite spanning seven categories. Each question targets a specific failure mode — synonym resolution, column disambiguation, cross-category reasoning, and so on. Answer types include set matching (scored by F1), boolean (exact match), numeric (exact match), and free text (LLM-as-judge with a rubric).
Results by Category
| Category | Score | Questions |
|---|---|---|
| cross_category_reasoning | 100.0% | 3 |
| direct_lookup | 100.0% | 2 |
| disambiguation | 100.0% | 2 |
| negation_absence | 100.0% | 3 |
| synonym_paraphrase | 97.0% | 4 |
| brand_generic_resolution | 66.7% | 3 |
| aggregation_counting | 33.3% | 3 |
| Overall | 84.4% | 20 |
Per-Question Results
| ID | Category | Score | Search Calls | Image Calls |
|---|---|---|---|---|
| DL-02 | direct_lookup | 100.0% | 4 | 1 |
| DL-03 | direct_lookup | 100.0% | 5 | 1 |
| SYN-01 | synonym_paraphrase | 100.0% | 1 | 0 |
| SYN-02 | synonym_paraphrase | 88.0% | 1 | 0 |
| SYN-03 | synonym_paraphrase | 100.0% | 1 | 0 |
| SYN-04 | synonym_paraphrase | 100.0% | 1 | 0 |
| BG-01 | brand_generic_resolution | 0.0% | 2 | 1 |
| BG-02 | brand_generic_resolution | 100.0% | 3 | 1 |
| BG-03 | brand_generic_resolution | 100.0% | 1 | 0 |
| XC-01 | cross_category_reasoning | 100.0% | 1 | 1 |
| XC-02 | cross_category_reasoning | 100.0% | 7 | 2 |
| XC-03 | cross_category_reasoning | 100.0% | 2 | 0 |
| NA-01 | negation_absence | 100.0% | 2 | 1 |
| NA-02 | negation_absence | 100.0% | 3 | 1 |
| NA-03 | negation_absence | 100.0% | 1 | 0 |
| AG-01 | aggregation_counting | 0.0% | 7 | 1 |
| AG-02 | aggregation_counting | 100.0% | 8 | 1 |
| AG-03 | aggregation_counting | 0.0% | 2 | 1 |
| DIS-01 | disambiguation | 100.0% | 1 | 0 |
| DIS-02 | disambiguation | 100.0% | 1 | 0 |
Development
bun run lint # run eslint, use lint:fix to fix errors
bun run format # run prettier, use format:fix to fix errors