Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
- name: Run hermetic suite with coverage and skip reasons
run: |
uv sync --frozen --all-extras --dev
uv run --frozen --no-sync python -c "import duckdb; c = duckdb.connect(); c.install_extension('vss'); c.close()"
export GROUNDWORKERS_REQUIRE_FORMAT_TESTS=1
echo "GROUNDWORKERS_REQUIRE_FORMAT_TESTS=1" >> "$GITHUB_ENV"
COVERAGE_FILE="$RUNNER_TEMP/groundworkers.coverage" uv run pytest -q -ra \
Expand Down Expand Up @@ -90,3 +91,6 @@ jobs:
uv venv "$RUNNER_TEMP/gw-wheel-extras" --python 3.12
uv pip install --python "$RUNNER_TEMP/gw-wheel-extras/bin/python" "$wheel[all_source,tui]" httpx
"$RUNNER_TEMP/gw-wheel-extras/bin/python" "$GITHUB_WORKSPACE/tests/support/wheel_smoke.py" extras
uv venv "$RUNNER_TEMP/gw-wheel-duckdb" --python 3.12
uv pip install --python "$RUNNER_TEMP/gw-wheel-duckdb/bin/python" "$wheel[duckdb]"
"$RUNNER_TEMP/gw-wheel-duckdb/bin/python" "$GITHUB_WORKSPACE/tests/support/wheel_smoke.py" duckdb
39 changes: 39 additions & 0 deletions config/groundworkers.duckdb.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Serve two existing, cleanly closed DuckDB files. Groundworkers opens them read-only.
# Set the model, dimensions and prefixes from the asset manifest.

[connections.datathon_cdm]
dialect = "duckdb"
database_name = "/abs/path/datathon_cdm.duckdb"

[connections.datathon_emb]
dialect = "duckdb"
database_name = "/abs/path/datathon_emb.duckdb"

[databases.cdm_db]
kind = "cdm"
connection = "datathon_cdm"

[databases.emb_db]
kind = "generic"
connection = "datathon_emb"

[providers.local_ollama]
provider = "ollama"
base_url = "http://localhost:11434"
api_key = "ollama"

[models.snowflake]
provider = "local_ollama"
model = "snowflake-arctic-embed2:568m"
embeddings = true
embedding_dim = 1024
query_prefix = "query: "

[vector_stores.datathon]
backend_type = "duckdbvss"
database = "emb_db"

[tools.groundworkers]
cdm_db = "cdm_db"
embedding_model_name = "snowflake"
vector_store_name = "datathon"
71 changes: 71 additions & 0 deletions docs/adapters/ancestry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Stored ancestry and shared targets

Groundworkers reads OMOP's `CONCEPT_ANCESTOR` closure table. Its `max_depth` is
the stored `min_levels_of_separation`, rather than a count of recursive
parent lookups. Distinct concepts can have separation zero, so zero depth can
include more than the input concept itself.

## Find a shared standard ancestor

Use `concept_shared_standard_ancestor` to find a shared representation for two
or more candidate concepts for the same source record. It does not combine
separate drug ingredients into a product or establish clinical equivalence.

```python
result = context.concept_shared_standard_ancestor(
[first_concept_id, second_concept_id],
max_depth=5,
concept_class_ids=["Clinical Drug"],
vocabulary_ids=["RxNorm", "RxNorm Extension"],
)
```

The Python/plugin method returns the service payload. The MCP operation wraps
that payload in its typed `data` field; errors have `error=true`, a code and a
message, with no selected target.

The operation requires 2–200 supplied positive integer IDs and at least two
distinct IDs. Boolean, string and fractional IDs are rejected. Class and
vocabulary filters use exact catalogue identifiers; null means unrestricted,
while empty lists, blank values and unknown identifiers are invalid. Asking
for RxNorm does not implicitly include RxNorm Extension.

Groundworkers intersects the complete stored ancestry of all inputs, including
each input at distance zero, then filters to active standard targets in the same
domain and any requested classes/vocabularies. It minimizes the maximum stored
distance from the inputs. A unique nearest target is selected; equal nearest
targets remain ambiguous. Unknown input IDs are errors, and mixed input domains
produce an incompatible-input result.

`limit` (1–50) and `offset` paginate displayed common candidates. They do not
affect selection: displaying one candidate cannot resolve a tie. The response
reports selection completeness, total candidates and display truncation
separately. A database failure returns an error rather than a partial selection.

## Nearest ancestors and path evidence

`concept_nearest_standard_ancestor` uses the same stored-distance semantics and
reports ambiguous nearest targets. Its text-query form discloses the rank-one
grounded seed and other seed candidates; it does not establish that the source
concept itself was unambiguous.

Relationship paths are separate evidence from stored ancestry. An ancestor can
be valid even when a bounded relationship-path search finds no path. The result
reports that limitation rather than fabricating a path. A `Maps to` fallback is
labelled as mapping evidence, not ancestry.

## Bounded named-relationship neighborhoods

The Python/plugin `concept_relationship_neighborhood` capability follows an
explicit set of active outbound relationship IDs. It accepts up to 50 distinct
seed concepts, traverses at most three levels and bounds each seed's nodes and
edges independently. `max_nodes_per_seed` and `max_edges_per_seed` describe
those allowances; `returned_edge_count` counts the deduplicated edges actually
returned across all seeds. One dense seed does not consume another seed's
allowance. `truncated_seeds` identifies walks that reached a resource limit.

The maximum supported union before overlap deduplication is 25,000 nodes and
125,000 edges (50 seeds, 500 nodes and 2,500 edges per seed). Prefer small seed
sets and limits for interactive use. Unknown seed concepts return `NOT_FOUND`
with the missing seed IDs. Missing metadata for reached non-seed concepts returns
`QUERY_ERROR` rather than a silently incomplete neighborhood.
12 changes: 11 additions & 1 deletion docs/adapters/omop_emb.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ The adapter does **not** resolve stack config itself. `build_application(...)` c
|---|---|
| `sqlitevec` | a `[vector_stores.*]` entry with `backend_type = "sqlitevec"` and a database reference |
| `pgvector` | a `[vector_stores.*]` entry with `backend_type = "pgvector"` and a `database` reference |
| `duckdbvss` | a `[vector_stores.*]` entry with `backend_type = "duckdbvss"` and a separate prebuilt DuckDB database reference |

FAISS remains a sidecar acceleration layer rather than a standalone primary backend.

See [database and vector-storage configuration](../usage/configuration.md#databases-and-vector-storage)
for connection dialects and file-serving requirements.

## Operation modes

### Index lookup mode
Expand All @@ -42,7 +46,13 @@ Requires `embedding_model_name` and `vector_store_name`, plus a configured embed

## Availability model

The underlying backend is built lazily on first use. If the backend cannot be opened, the adapter reports unavailability rather than failing application construction for callers that do not need embedding-backed operations.
Configuration is validated when constructing the application. A configured
DuckDB embedding file must already exist and be separate from the CDM file;
invalid file configuration stops startup.

The underlying backend is built lazily on first use. Failures while opening a
validly configured backend are reported as embedding unavailability. Registered
models and stored vectors can be inspected without a live query encoder.

Representative `index_status()` shape:

Expand Down
Loading
Loading