phenoforge.engine

The retrieval and expansion engine. No I/O to MCP or LLMs (AGENTS.md’s layering rule) — mcp and agent both import this, never the reverse.

phenoforge.engine.models

Pydantic models shared across the engine, MCP server, and agent.

Every code set carries provenance: a concept set is never a bare list of codes, every concept carries where it came from, and terms that could not be resolved are reported rather than silently dropped. This is the project’s anti-hallucination commitment — retrofitting it after the fact would mean changing every caller’s return type.

class phenoforge.engine.models.ProvenanceTier(value)[source]

Bases: StrEnum

Trust tier for a concept’s inclusion in a set.

Variables:
  • CURATED – From the OHDSI Phenotype Library. Use as-is, citing the cohort ID and operating characteristics. Not yet produced by anything in this codebase.

  • PUBLISHED – From a validated published algorithm in the literature. Deferred to a later version — not yet produced by this codebase.

  • GENERATED – From semantic/BM25 retrieval or hierarchy expansion. Ungrounded — requires human confirmation before use.

CURATED = 'curated'
PUBLISHED = 'published'
GENERATED = 'generated'
class phenoforge.engine.models.Concept(**data)[source]

Bases: BaseModel

A single vocabulary concept, as loaded into vocab.duckdb.

Variables:
  • concept_id – OMOP concept id.

  • concept_code – Vocabulary-native code, e.g. "E11.21".

  • concept_name – Human-readable concept name.

  • domain_id – OMOP domain, e.g. "Condition".

  • vocabulary_id – Source vocabulary. SNOMED rows are loaded locally for future curated-set resolution but are never a lookup or search target — only "ICD10CM" is ever surfaced outside the engine.

Parameters:
  • concept_id (int)

  • concept_code (str)

  • concept_name (str)

  • domain_id (str)

  • vocabulary_id (str)

concept_id: int
concept_code: str
concept_name: str
domain_id: str
vocabulary_id: str
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class phenoforge.engine.models.ConceptWithProvenance(**data)[source]

Bases: Concept

A concept annotated with why it is in a result set.

Variables:
  • tier – The trust tier this concept was included under.

  • source – Free-text description of the retrieval step that produced this concept, e.g. "hierarchy_expansion:E11" or "bm25:diabetic nephropathy". Not machine-parsed; exists for human review and debugging.

Parameters:
tier: ProvenanceTier
source: str
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class phenoforge.engine.models.UnmappableTerm(**data)[source]

Bases: BaseModel

A query term that could not be resolved to any concept.

Variables:
  • term – The term or phrase that failed to resolve.

  • reason – Why it failed, e.g. "no BM25 match above threshold".

Parameters:
term: str
reason: str
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class phenoforge.engine.models.ConceptSet(**data)[source]

Bases: BaseModel

A retrieval or expansion result.

Deliberately not a bare list of codes (AGENTS.md): every concept carries provenance, and unresolved terms are surfaced rather than dropped.

Variables:
  • concepts – Concepts included in the set, each with its provenance.

  • unmappable – Terms that could not be resolved to any concept.

Parameters:
concepts: list[ConceptWithProvenance]
unmappable: list[UnmappableTerm]
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

phenoforge.engine.db

Read-only connection helper for vocab.duckdb.

No I/O to MCP or LLMs lives in this package (AGENTS.md layering rule) — this module only knows how to open the database built by scripts/load_vocab.py.

phenoforge.engine.db.connect(db_path=PosixPath('data/vocab.duckdb'))[source]

Open a read-only connection to the vocabulary database.

Parameters:

db_path (Path) – Path to a DuckDB database built by scripts/load_vocab.py.

Returns:

An open, read-only DuckDB connection.

Return type:

duckdb.DuckDBPyConnection

Raises:

FileNotFoundError – If db_path does not exist.

phenoforge.engine.lookup

Direct concept_code lookup against the ICD-10-CM vocabulary.

phenoforge.engine.lookup.lookup_by_code(con, concept_code)[source]

Look up a single ICD-10-CM concept by its code.

Only ICD-10-CM is queryable here: SNOMED is loaded locally for future curated-set resolution but is never a user-facing lookup target.

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • concept_code (str) – ICD-10-CM code, e.g. "E11.21".

Returns:

The matching concept, or None if no ICD-10-CM concept has that code.

Return type:

Concept | None

phenoforge.engine.retrieval

BM25 lexical retrieval over ICD-10-CM concept names.

One of three planned retrieval components — dense embeddings (paraphrase matching) and graph traversal (hierarchy expansion, see expansion.py) are the other two. BM25 covers exact strings, code fragments, and rare literal terms; it does not match paraphrase or clinical synonymy.

class phenoforge.engine.retrieval.BM25Retriever(con)[source]

Bases: object

A BM25 index over ICD-10-CM concept_name text.

Construction loads and tokenizes every ICD-10-CM concept name, so build one instance per process (e.g. once at MCP server startup) rather than per query.

Variables:

concept_ids – OMOP concept ids, parallel to the internal corpus.

Parameters:

con (duckdb.DuckDBPyConnection)

search(query, k=10, min_score=0.0)[source]

Search for concepts whose names best match a free-text query.

Parameters:
  • query (str) – Free-text search string.

  • k (int) – Maximum number of results to return.

  • min_score (float) – Minimum BM25 score for a result to be included.

Returns:

A tuple of (matching concepts tagged GENERATED, ordered by descending score; an UnmappableTerm if nothing scored above min_score, else None).

Return type:

tuple[list[ConceptWithProvenance], UnmappableTerm | None]

phenoforge.engine.dense

Dense (semantic) retrieval over ICD-10-CM concept names via a LanceDB index.

Third of three retrieval components alongside BM25 (retrieval.py) and graph traversal (expansion.py); covers paraphrase/synonymy that BM25 misses since it matches on meaning rather than shared words.

The embedding function is injectable (EmbedFn) so tests never need to download a real model — see tests/engine/fake_embedder.py. Production code gets a real embedder via default_embedder(), which lazily imports sentence_transformers so importing this module never requires it.

phenoforge.engine.dense.default_embedder()[source]

Build an embedding function backed by real BioLORD-2023.

Imports sentence_transformers and loads the model on call, not at module import time, so import phenoforge.engine.dense never requires the dependency to be installed or the model to be downloadable.

Returns:

A function embedding a batch of texts into L2-normalized BioLORD-2023 vectors (see EmbedFn).

Return type:

EmbedFn

Raises:

ImportError – If sentence-transformers is not installed.

class phenoforge.engine.dense.DenseRetriever(con, embed_fn=None, index_path=None)[source]

Bases: object

A LanceDB-backed semantic index over ICD-10-CM concept_name text.

Two modes: if index_path is given and already exists, opens the persisted table built by scripts/build_index.py (the production path — LanceDB’s value is a persisted, memory-mapped index, unlike BM25Retriever, which is cheap enough to rebuild every process start). If not, builds an ephemeral in-memory table via _embed_concept_rows() (the test path, with an injected fake embedder — no model download or prebuilt artifact needed).

Parameters:
  • con (duckdb.DuckDBPyConnection)

  • embed_fn (EmbedFn | None)

  • index_path (Path | None)

search(query, k=10, min_score=0.0)[source]

Search for concepts whose names are semantically closest to a query.

Parameters:
  • query (str) – Free-text search string.

  • k (int) – Maximum number of results to return.

  • min_score (float) – Minimum cosine similarity (derived from L2 distance over the embedder’s vectors) for a result to be included.

Returns:

A tuple of (matching concepts tagged GENERATED, ordered by descending similarity; an UnmappableTerm if nothing scored above min_score, else None).

Return type:

tuple[list[ConceptWithProvenance], UnmappableTerm | None]

phenoforge.engine.hybrid

Fuse ranked concept lists from multiple retrieval components.

reciprocal_rank_fusion is a pure function, no DB or network dependency. hybrid_search is the orchestration layer on top of it — deciding whether to query a dense retriever at all and building the fallback unmappable entry — which belongs here rather than in the MCP transport per AGENTS.md’s thin-server rule (mcp/ is tool registration only, no business logic).

phenoforge.engine.hybrid.reciprocal_rank_fusion(result_lists, k=10, rrf_k=60, weights=None)[source]

Fuse ranked concept lists by reciprocal rank.

Score for a concept is the sum of weight * 1 / (rrf_k + rank) across every input list it appears in (rank is 0-indexed position within that list). rrf_k=60 is the standard default from the original RRF literature — it sidesteps needing to normalize scores across retrievers whose raw scores aren’t comparable (BM25 term-frequency scores vs. cosine similarity), since RRF only uses rank, not magnitude.

A concept appearing in multiple lists is deduplicated by concept_id, keeping the ConceptWithProvenance instance from whichever list ranked it highest — its source therefore names the specific retriever that surfaced it best (e.g. "dense:..." or "bm25:..."), never a synthesized “hybrid” label, so provenance stays traceable to a real mechanism.

Parameters:
  • result_lists (list[list[ConceptWithProvenance]]) – Ranked concept lists to fuse, each already ordered best-first.

  • k (int) – Maximum number of fused results to return.

  • rrf_k (int) – RRF rank-damping constant.

  • weights (list[float] | None) – Per-list multiplier on each list’s RRF contribution, same length and order as result_lists. Defaults to 1.0 for every list (the original unweighted behavior — uniform scaling never changes relative order, so this is a strict superset of the prior signature, not a behavior change for existing callers).

Returns:

Top-k concepts by fused score, descending.

Return type:

list[ConceptWithProvenance]

Search using BM25 and, if available, dense retrieval, fused by RRF.

dense is optional so a caller (the MCP server) can pass None when no dense index has been built yet, degrading to BM25-only rather than requiring one. Same return contract as search() and search().

Parameters:
  • bm25 (BM25Retriever) – A built BM25 retriever.

  • dense (DenseRetriever | None) – A built dense retriever, or None to search lexically only.

  • query (str) – Free-text search string.

  • k (int) – Maximum number of fused results to return.

  • dense_weight (float) – Dense’s share of the fused RRF score, in [0, 1]; BM25 gets 1 - dense_weight. 0.5 (equal weight) matches this function’s original, unweighted behavior — uniform scaling of both lists never changes relative order, so the default is a strict no-op for existing callers. Ignored (BM25-only) when dense is None. See scripts/sweep_hybrid_weights.py for measuring whether a different value scores better against curated ground truth before changing this default.

  • candidate_k (int | None) – How many results each sub-retriever searches before fusion, independent of k (how many fused results come back). Defaults to 5 * k: a concept that one sub-retriever ranks well outside k (e.g. dense rank 5 of 50 for a seed term with zero BM25 lexical overlap) still gets a chance to compete in RRF fusion rather than being invisible to it, which silently happened under the old default of k itself (found via cohort 775 — “inflammatory bowel disease” has no matching ICD-10-CM concept name at all, so BM25 contributes nothing, and the correct code’s decent-but-not-top-k dense rank was never pooled). Widening the pool only ever adds candidates for fusion to rank from; it never changes how many results come back (still k) or demotes a result that was already going to win, so this is a pure ranking-quality improvement, not a behavior change any caller needs to opt into. Pass an explicit, fixed candidate_k whenever comparing a concept’s rank across calls with different k (e.g. explain_inclusion()’s evidence) to keep that comparison meaningful — or when k is already large and 5 * k would be wastefully deep.

Returns:

A tuple of (fused, deduplicated results ordered by combined relevance; an UnmappableTerm if nothing matched, else None).

Return type:

tuple[list[ConceptWithProvenance], UnmappableTerm | None]

phenoforge.engine.expansion

Hierarchy expansion over CONCEPT_RELATIONSHIP Is a edges.

OMOP’s precomputed transitive-closure table, CONCEPT_ANCESTOR, has zero rows for ICD-10-CM, so expansion instead walks direct Is a/Subsumes edges via a recursive CTE. The real Athena download contains redundant direct edges (e.g. a code’s grandparent is also a direct parent) — not cycles, but multiple paths to the same concept — so every query here carries a visited-path guard and the result is deduplicated to each concept’s shortest depth.

Two directions: expand_descendants() walks down (seed -> more specific codes), expand_ancestors() walks up (seed -> less specific codes, to the root). The latter powers hierarchy-distance scoring in phenoforge.eval.

phenoforge.engine.expansion.expand_descendants(con, seed_code)[source]

Expand an ICD-10-CM seed code to all of its hierarchy descendants.

A descendant of concept_id_2 is any concept concept_id_1 with an Is a edge to it (direct or transitive) — i.e. traversal follows Is a edges backwards from the seed, equivalently Subsumes forward from it.

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • seed_code (str) – ICD-10-CM code to expand from, e.g. "E11".

Returns:

Descendant concepts tagged with GENERATED provenance, ordered by ascending tree depth. Empty if seed_code does not exist or has no descendants.

Return type:

list[ConceptWithProvenance]

phenoforge.engine.expansion.expand_ancestors(con, seed_code)[source]

Walk an ICD-10-CM concept’s Is a ancestor chain to the root.

Counterpart to expand_descendants(), walking the same edges in the opposite direction (concept_id_1 “Is a” concept_id_2 means concept_id_2 is the parent).

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • seed_code (str) – ICD-10-CM code to walk ancestors from, e.g. "E11.21".

Returns:

Ancestor concepts tagged with GENERATED provenance, ordered by ascending distance from the seed. Empty if seed_code does not exist or is already a root.

Return type:

list[ConceptWithProvenance]

phenoforge.engine.expansion.ancestor_chain_ids(con, seed_code)[source]

Return seed_code’s own concept id followed by its ancestor ids, nearest first.

Thin wrapper around expand_ancestors() returning bare ids (self-inclusive, at distance 0) — the shape hierarchy-distance scoring in phenoforge.eval needs, without forcing every caller through the full ConceptWithProvenance model.

Concepts with multiple direct parents (real in the Athena data — see expand_ancestors()) make this a flattened traversal of what’s actually a DAG, not a strict linear chain; ties in depth are broken by concept_id in the underlying query so the same seed always produces the same chain (needed for phenoforge.eval.metrics.tree_distance() to be reproducible run to run — an earlier version of this query had no tiebreaker and silently returned differently-ordered chains across identical calls).

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • seed_code (str) – ICD-10-CM code.

Returns:

[seed_concept_id, parent_id, grandparent_id, ..., root_id]. Empty if seed_code does not exist.

Return type:

list[int]

phenoforge.engine.curated

Curated OHDSI Phenotype Library resolution to ICD-10-CM.

Extracts concept-set (not temporal/inclusion-rule) content from Circe-shaped cohort JSON, resolves each SNOMED concept id to ICD-10-CM via the Mapped from edges loaded by scripts/load_vocab.py, and applies a fan-out guard so a broad SNOMED grouper in a cohort definition doesn’t silently expand into thousands of unrelated ICD-10-CM codes.

A free-text query is matched to a bundled cohort via find_curated_definition(), which searches the same BM25/dense retrieval used for generated results against the full ICD-10-CM vocabulary and checks whether the best hit is actually a member of a cohort’s resolved code list — not, as an earlier version of this did, by comparing the query against cohort display names (which let one incidental shared word confidently mismatch an unrelated cohort).

phenoforge.engine.curated.find_high_fanout_snomed_concepts(con, fanout_threshold)[source]

Flag SNOMED concepts whose Mapped from fan-out into ICD-10-CM is excessive.

Verified against the v2026 Athena download: fan-out ranges 1..3029, with a median of 2. The high tail is structural (a high-level SNOMED grouper mapping to thousands of unrelated ICD-10-CM codes), not clinical specificity, and should be surfaced for review rather than expanded blindly at resolution time. Used both by the vocabulary loader (to flag broad mappings at build time) and by curated-set resolution (to skip them at resolution time).

Parameters:
  • con (DuckDBPyConnection) – Open connection with concept and concept_relationship loaded.

  • fanout_threshold (int) – Concepts mapping to more than this many ICD-10-CM codes are flagged.

Returns:

SNOMED concept_id values exceeding the threshold.

Return type:

list[int]

class phenoforge.engine.curated.CuratedCohortConceptItem(**data)[source]

Bases: BaseModel

One item within a cohort’s concept set, extracted from Circe JSON.

Variables:
  • concept_id – Source-vocabulary OMOP concept id (as recorded in the cohort’s own vocabulary snapshot — not necessarily present in the locally loaded concept table).

  • concept_code – Vocabulary-native code, e.g. a SNOMED code.

  • vocabulary_id – Source vocabulary of the item, e.g. "SNOMED".

  • is_excluded – Circe’s isExcluded flag. Excluded items are dropped entirely during resolution (see resolve_curated_concepts()).

  • include_descendants – Circe’s includeDescendants flag. Not expanded during resolution (see resolve_curated_concepts()).

Parameters:
  • concept_id (int)

  • concept_code (str)

  • vocabulary_id (str)

  • is_excluded (bool)

  • include_descendants (bool)

concept_id: int
concept_code: str
vocabulary_id: str
is_excluded: bool
include_descendants: bool
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

phenoforge.engine.curated.load_cohort_concept_items(cohort_json_path)[source]

Parse a Circe cohort JSON file’s ConceptSets into flat items.

Extracts only the concept-set portion of the cohort definition — Circe cohort JSON also carries temporal windows and inclusion-rule logic (PrimaryCriteria, InclusionRules, etc.), which is out of scope and not read here.

Items missing concept or any of its required fields are skipped rather than raising — this file is fetched from an external source (scripts/fetch_phenotype_library.py) with no schema guarantee, and a single malformed item should degrade gracefully, not crash the whole lookup.

Parameters:

cohort_json_path (Path) – Path to one cohort’s JSON file.

Returns:

Flattened concept-set items across all of the cohort’s concept sets. Malformed items are silently omitted.

Return type:

list[CuratedCohortConceptItem]

phenoforge.engine.curated.resolve_curated_concepts(con, cohort_id, cohort_name, items, fanout_threshold=100)[source]

Resolve a cohort’s SNOMED concept-set items to ICD-10-CM.

Per non-excluded item:

  • vocabulary_id != 'SNOMED': reported as unmappable — RxNorm/LOINC items in a cohort definition are out of scope (only the SNOMED to ICD-10-CM mapping is loaded).

  • fan-out over fanout_threshold (see find_high_fanout_snomed_concepts()): reported as unmappable rather than resolved, so a broad SNOMED grouper doesn’t silently expand the curated set into thousands of unrelated codes.

  • otherwise: resolved via concept_relationship 'Mapped from' edges (concept_id_1 = SNOMED, concept_id_2 = ICD-10-CM), tagged CURATED.

is_excluded items are dropped entirely — ConceptSet has no exclusion concept, so silently omitting them is more honest than forcing them into unmappable. include_descendants is not expanded: doing so would require a SNOMED-side hierarchy traversal, which is out of scope; this is a conservative simplification (it under-counts rather than over-includes).

Parameters:
Returns:

Resolved ICD-10-CM concepts tagged curated, deduplicated by concept_id (two cohort items can map to the same code), and unmappable items with their reasons.

Return type:

tuple[list[ConceptWithProvenance], list[UnmappableTerm]]

phenoforge.engine.curated.load_curated_concept_set(con, cohort_id, library_dir, fanout_threshold=100)[source]

Load one cohort’s JSON + manifest name from library_dir and resolve it.

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • cohort_id (str) – The cohort id, e.g. "503". Expects {library_dir}/{cohort_id}.json and a manifest.json mapping ids to display names, both written by scripts/fetch_phenotype_library.py.

  • library_dir (Path) – Directory containing fetched cohort JSON files.

  • fanout_threshold (int) – Passed through to resolve_curated_concepts().

Returns:

The resolved concept set.

Return type:

ConceptSet

Raises:

FileNotFoundError – If the cohort JSON or manifest is missing.

phenoforge.engine.curated.find_curated_definition(con, query, library_dir, bm25, dense=None, fanout_threshold=100, k=10)[source]

Find and resolve the OHDSI Phenotype Library cohort whose own codes best match a query.

Runs the same BM25 (+ dense, if given) search used for generated retrieval against the full ICD-10-CM vocabulary, then walks the ranked results looking for the best-ranked one that is actually a member of a bundled cohort’s resolved code list — matching on real curated code content rather than a cohort’s display name, which (found via a real end-to-end run) let a single incidental shared word like “diabetic” curated-match “diabetic nephropathy” to an unrelated “Diabetic ketoacidosis” cohort. A hit belonging to more than one cohort is deliberately treated as no match rather than guessed between them, for the same reason: a curated result claims to be safe to use as-is, and an ambiguous one would misrepresent that.

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • query (str) – Free-text population description or seed clinical term.

  • library_dir (Path) – Directory of fetched OHDSI Phenotype Library cohorts (scripts/fetch_phenotype_library.py output).

  • bm25 (BM25Retriever) – A built BM25 retriever over ICD-10-CM concept names — shared with hybrid_search()’s other callers rather than built fresh here.

  • dense (DenseRetriever | None) – A built dense retriever, or None to search lexically only, matching hybrid_search()’s existing fallback.

  • fanout_threshold (int) – Passed through to cohort resolution.

  • k (int) – How many top hybrid-search results to check for cohort membership before giving up.

Returns:

The matched cohort’s resolved curated concepts, or an empty set with one explanatory UnmappableTerm if the library hasn’t been fetched, nothing in the top k results belongs to a cohort, or the best hit is claimed by more than one cohort. A cohort with a stale manifest entry (missing JSON file) simply can never be matched — see _build_cohort_membership().

Return type:

ConceptSet

phenoforge.engine.explain

Explain why a concept code would be included in a result, via which path.

MCP tool calls are stateless — nothing persists a previously-returned ConceptSet server-side, so “why is this code in my set” has to be answered by re-deriving it: re-running the same curated/generated/ hierarchy resolution a caller would have gotten, and reporting which of those paths actually produce concept_code, with evidence specific to each path (a cohort citation, a search rank, a hierarchy distance). A code can legitimately be reachable by more than one path at once — e.g. both curated and hierarchy expansion — so every path checked is reported, not just the first hit.

class phenoforge.engine.explain.InclusionPath(**data)[source]

Bases: BaseModel

One way concept_code was found for the query/seed checked.

Variables:
  • tier – ProvenanceTier this path produces.

  • source – The same provenance string a real result would carry, e.g. "bm25:diabetic nephropathy" or "ohdsi_pl:503:Type 2 diabetes mellitus".

  • evidence – Human-readable detail specific to this path — a cohort citation, a search rank, or a hierarchy distance.

Parameters:
tier: ProvenanceTier
source: str
evidence: str
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class phenoforge.engine.explain.InclusionExplanation(**data)[source]

Bases: BaseModel

Every way concept_code was found, across every path checked.

Variables:
  • concept_code – The code that was explained.

  • concept_name – Its vocabulary display name, or None if the code doesn’t exist in the loaded ICD-10-CM vocabulary at all.

  • paths – One entry per path that actually produced this code; empty if none did (including if the code doesn’t exist).

Parameters:
concept_code: str
concept_name: str | None
paths: list[InclusionPath]
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

phenoforge.engine.explain.explain_inclusion(con, concept_code, bm25, library_dir, dense=None, query=None, seed_code=None, fanout_threshold=100, k=25)[source]

Check every applicable path for whether it produces concept_code.

Checks are independent and additive, mirroring the two distinct ways a caller actually reaches a code through this project’s tool surface — pass whichever parameters apply to what’s being explained:

Parameters:
  • con (DuckDBPyConnection) – Open connection to vocab.duckdb.

  • concept_code (str) – ICD-10-CM code to explain.

  • bm25 (BM25Retriever) – Shared BM25 index.

  • library_dir (Path) – Directory of fetched OHDSI Phenotype Library cohorts.

  • dense (DenseRetriever | None) – Pre-built dense retriever, or None to degrade the generated-tier check to BM25-only, matching search_concepts’s existing fallback.

  • query (str | None) – Free-text query to check curated/generated paths against.

  • seed_code (str | None) – ICD-10-CM seed code to check the hierarchy-expansion path against.

  • fanout_threshold (int) – Passed through to curated resolution.

  • k (int) – How deep to search for concept_code on the curated and generated paths — passed to hybrid_search() as its k (final result count); candidate_k is left at hybrid_search’s own default (5 * k) rather than pinned to k here, so this function’s reported rank benefits from the same widened fusion pool as every other caller. The rank reported in evidence is the code’s position within this many candidates; pass a larger k to find a code that didn’t show up at a shallower search depth, and don’t compare ranks from calls with different k directly, since they describe different-sized pools.

Returns:

Every path that produced concept_code; empty paths if none did, including if the code isn’t in the vocabulary at all.

Return type:

InclusionExplanation