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
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
.PHONY: setup test lint typecheck check describe
.PHONY: setup test lint typecheck check describe inspector

PYTHON := .venv/bin/python3
CONFIG ?= config/groundworkers.example.toml
# Empty = the server-specific lookup (OA_CONFIG_PATH, then ~/.config/omop/config.toml).
# Override per run, e.g. make describe CONFIG=config/groundworkers.example.toml
CONFIG ?=

# Mirrors the install CI performs, so a local run reproduces the pipeline exactly.
setup:
uv sync --all-extras --dev

describe:
$(PYTHON) -m groundworkers.server --config $(CONFIG) --describe
$(PYTHON) -m groundworkers.server $(if $(CONFIG),--config-path $(CONFIG)) --describe

# Launch the official MCP Inspector against this checkout's stdio server.
inspector:
MCP_INSPECTOR_SECRET_STORE=memory npm_config_cache="$${TMPDIR:-/tmp}/groundworkers-inspector-npm-cache" npx --yes @modelcontextprotocol/inspector@2.12.0 uv run --project . groundworkers

lint:
uv run ruff check .
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ groundworkers
groundworkers --transport streamable-http --host 127.0.0.1 --port 8000
```

Explore the configured MCP tools, prompts, and resources interactively with the official MCP Inspector:

```bash
make inspector
```

This launches Groundworkers over stdio using the active shared OMOP configuration and opens the Inspector UI. It requires Node.js 22.19 or newer and `uv`; the first run downloads the Inspector package through `npx`.

REST is a separate, curated interface:

```bash
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ groundworkers = "groundworkers.server:main"
[project.entry-points."omop.config"]
groundworkers = "groundworkers.config:GroundworkersConfig"

[project.entry-points."ohdsi_prompt_registry.packs"]
groundworkers = "groundworkers.prompt_packs"

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
Expand Down
10 changes: 10 additions & 0 deletions src/groundworkers/adapters/omop_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,18 @@ def embedding_resolver_active(self) -> bool:
failure this property exists to prevent.
"""

try:
self._get_kg()
except GroundworkersError:
return False
return self._embedding_configured and self._model_backend_factory is not None

@property
def embedding_configuration_error(self) -> str | None:
"""Safe detail when embedding setup fell back to a lexical-only graph."""

return self._embedding_configuration_error

def is_available(self) -> bool:
try:
self._get_kg()
Expand Down
64 changes: 64 additions & 0 deletions src/groundworkers/omop_domains.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Domain routing facts that groundworkers owns and other components import.

The full set of OMOP ``domain_id`` values comes from the vocabulary's ``domain``
table (see :class:`groundworkers.base.domain_names.DomainNameResolver`); this module
does not repeat it. It owns two smaller facts the vocabulary does not record:

``DOMAIN_TO_TABLE``
The CDM event table a record is written to, keyed by the ``domain_id`` of its
selected standard target. The ``domain-routing`` knowledge pack publishes the
same mapping to agents; tests keep the pack identical to this table.

``CLASSIFIABLE_DOMAINS`` and ``NON_CLINICAL``
The output vocabulary of the ``domain_classify`` tool. The clinical values are
a subset of the routed domains. The non-clinical values are *not* OMOP domains:
they label data-dictionary fields that describe how data was collected
(``Metadata``) or that only link records (``Identifier``). Callers decide what
to do with them; groundcrew, for example, skips such fields.

This is published API: import from here rather than copying the values.
"""

from __future__ import annotations

from types import MappingProxyType
from typing import Final

DOMAIN_TO_TABLE: Final = MappingProxyType(
{
"Condition": "CONDITION_OCCURRENCE",
"Drug": "DRUG_EXPOSURE",
"Measurement": "MEASUREMENT",
"Observation": "OBSERVATION",
"Procedure": "PROCEDURE_OCCURRENCE",
"Device": "DEVICE_EXPOSURE",
"Specimen": "SPECIMEN",
"Visit": "VISIT_OCCURRENCE",
"Provider": "PROVIDER",
}
)

CLASSIFIABLE_DOMAINS: Final[tuple[str, ...]] = (
"Measurement",
"Condition",
"Observation",
"Procedure",
"Drug",
"Device",
)

NON_CLINICAL: Final[tuple[str, ...]] = ("Metadata", "Identifier")

DOMAIN_CLASSIFY_OUTPUTS: Final[frozenset[str]] = frozenset(CLASSIFIABLE_DOMAINS + NON_CLINICAL)


def table_for_domain(domain_id: str | None) -> str | None:
"""The CDM table for a canonical ``domain_id``, or None when it is not routed.

Expects the canonical spelling (``"Condition"``); normalise loosely typed input
with ``DomainNameResolver.canonical`` first.
"""

if domain_id is None:
return None
return DOMAIN_TO_TABLE.get(domain_id)
8 changes: 8 additions & 0 deletions src/groundworkers/prompt_packs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Prompt packs for the OHDSI Prompt Registry that ship with Groundworkers.

Each direct child directory is one registry pack, published through the
``ohdsi_prompt_registry.packs`` entry point. A pack lives here when Groundworkers
owns the contract it describes: ``groundcrew-source-column-classification``
reviews columns against Groundworkers' ``ColumnRole`` vocabulary. The name keeps
its ``groundcrew-`` prefix so existing prompt names and overrides stay stable.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://local.groundcrew/schemas/source-column-classification-request.schema.json",
"title": "Groundcrew source-column classification request",
"type": "object",
"additionalProperties": false,
"required": [
"table_name",
"headers",
"original_headers",
"sample_rows",
"deterministic_annotations",
"candidate_headers"
],
"properties": {
"table_name": {"type": "string", "minLength": 1},
"headers": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"type": "string", "minLength": 1}
},
"original_headers": {
"type": "array",
"uniqueItems": true,
"items": {"type": "string", "minLength": 1}
},
"sample_rows": {
"type": "array",
"maxItems": 5,
"items": {
"type": "object",
"additionalProperties": {"type": ["string", "null"]}
}
},
"deterministic_annotations": {
"type": "object",
"additionalProperties": {"$ref": "#/$defs/annotation"}
},
"candidate_headers": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"type": "string", "minLength": 1}
}
},
"$defs": {
"role": {
"enum": [
"label",
"codes",
"values",
"annotation",
"entity",
"attribute",
"section",
"subsection",
"source_vocab",
"description",
"mapping_context",
"data_type",
"field_type_ctrl",
"local_pk",
"pii_flag",
"required",
"frequency",
"pipeline_meta",
"irrelevant"
]
},
"annotation": {
"type": "object",
"additionalProperties": false,
"required": ["role", "detection_tier", "confidence", "inferred_vocab", "packed_value", "notes"],
"properties": {
"role": {"$ref": "#/$defs/role"},
"detection_tier": {"enum": ["A", "B", "C", "D", "LLM"]},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"inferred_vocab": {"type": ["string", "null"]},
"packed_value": {"type": "boolean"},
"notes": {"type": "string"}
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://local.groundcrew/schemas/source-column-classification-result.schema.json",
"title": "Groundcrew source-column classification result",
"type": "object",
"additionalProperties": false,
"required": ["decisions"],
"properties": {
"decisions": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/decision"}
}
},
"$defs": {
"role": {
"enum": [
"label",
"codes",
"values",
"annotation",
"entity",
"attribute",
"section",
"subsection",
"source_vocab",
"description",
"mapping_context",
"data_type",
"field_type_ctrl",
"local_pk",
"pii_flag",
"required",
"frequency",
"pipeline_meta",
"irrelevant"
]
},
"decision": {
"type": "object",
"additionalProperties": false,
"required": ["header", "role", "confidence", "inferred_vocab", "packed_value", "notes"],
"properties": {
"header": {"type": "string", "minLength": 1},
"role": {
"oneOf": [
{"$ref": "#/$defs/role"},
{"type": "null"}
]
},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"inferred_vocab": {"type": ["string", "null"]},
"packed_value": {"type": "boolean"},
"notes": {"type": "string", "minLength": 1}
},
"allOf": [
{
"if": {"properties": {"role": {"type": "null"}}, "required": ["role"]},
"then": {
"properties": {
"confidence": {"maximum": 0.79},
"inferred_vocab": {"type": "null"},
"packed_value": {"const": false}
}
}
},
{
"if": {"properties": {"role": {"const": "values"}}, "required": ["role"]},
"then": {},
"else": {"properties": {"packed_value": {"const": false}}}
}
]
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
classification_request:
table_name: local_data_dictionary
headers: [var_key, display_text, coding_system, code_value, allowed_responses, status]
original_headers: [Var Key, Display Text, Coding System, Code Value, Allowed Responses, Status]
sample_rows:
- var_key: diabetes_history
display_text: History of diabetes
coding_system: ICD10CM
code_value: E11.9
allowed_responses: "0, No | 1, Yes"
status: Active
- var_key: hba1c
display_text: HbA1c result
coding_system: LOINC
code_value: 4548-4
allowed_responses: ""
status: Active
deterministic_annotations:
display_text:
role: label
detection_tier: B
confidence: 0.9
inferred_vocab: null
packed_value: false
notes: Header contains a strong label token.
coding_system:
role: source_vocab
detection_tier: B
confidence: 0.9
inferred_vocab: null
packed_value: false
notes: Header identifies a coding-system column.
allowed_responses:
role: values
detection_tier: B
confidence: 0.75
inferred_vocab: null
packed_value: true
notes: Header and cell syntax suggest a packed value set.
candidate_headers: [var_key, code_value, allowed_responses, status]

classification_result:
decisions:
- header: var_key
role: attribute
confidence: 0.94
inferred_vocab: null
packed_value: false
notes: Values are stable source variable identifiers paired with display labels.
- header: code_value
role: codes
confidence: 0.98
inferred_vocab: null
packed_value: false
notes: Values are terminology codes paired row-wise with an explicit coding-system column.
- header: allowed_responses
role: values
confidence: 0.96
inferred_vocab: null
packed_value: true
notes: Cells hold multiple encoded choices that require downstream expansion.
- header: status
role: null
confidence: 0.45
inferred_vocab: null
packed_value: false
notes: The samples do not distinguish operational row status from clinical mapping context.

Loading
Loading