From 69cfc14ed90157d1043e4f252f8cc1c19e93e304 Mon Sep 17 00:00:00 2001 From: Georgie Kennedy Date: Tue, 20 Oct 2026 11:53:57 -0400 Subject: [PATCH 1/4] cross-domain edge traversal --- docs/development/standard-resolution-packs.md | 133 ++++++++++++++++++ docs/tools/concept.md | 5 +- src/groundworkers/adapters/omop_graph.py | 3 + .../config/predicate_mapping.csv | 36 ++--- src/groundworkers/tools/concept_tools.py | 23 ++- tests/unit/test_omop_graph_adapter.py | 18 +++ .../test_predicate_mapping_classification.py | 44 ++++++ 7 files changed, 236 insertions(+), 26 deletions(-) create mode 100644 docs/development/standard-resolution-packs.md create mode 100644 tests/unit/test_predicate_mapping_classification.py diff --git a/docs/development/standard-resolution-packs.md b/docs/development/standard-resolution-packs.md new file mode 100644 index 0000000..3e48b83 --- /dev/null +++ b/docs/development/standard-resolution-packs.md @@ -0,0 +1,133 @@ +# Proposal: per-domain standard-resolution packs + +Status: draft for review (2026-10-19). Replaces the single +`core/standard-concept-preference` pack with a slim, always-loaded core +plus one pack per OMOP domain. + +## Why the broad pack falls short + +`standard-concept-preference` mixes two kinds of content: + +1. **Invariants** that hold in every domain: the target must be + `standard_concept = 'S'`, active, never a classification (`'C'`), and + `concept_id = 0` when nothing fits. +2. **Per-domain resolution rules**, compressed to one line each, e.g. + "Procedure: SNOMED preferred, CPT4 for US billing". + +The second kind is where agents actually go wrong, and one line per domain +cannot carry it. Checking the procedure domain against the live vocabulary +(v5.0 27-FEB-26) shows the one-liner is misleading: + +- Standardness is decided **per code, not per vocabulary**. CPT4 has + ~10.2k standard codes and ~3.8k non-standard ones that map to SNOMED, + CVX or RxNorm. OPCS4 and ICD9Proc are split roughly half and half. +- A standard source code (CPT4, ICD10PCS, standard HCPCS) **maps to + itself**. There is no "prefer SNOMED over CPT4" step: rewriting a + standard code to SNOMED discards information and is wrong. +- The vocabulary ships cross-vocabulary links that *look* like equivalences + but aren't (`CPT4 - SNOMED eq` often points to a broader concept, or to + several). An agent told only "prefer SNOMED" will follow them. +- Concept sets built from SNOMED descendants miss standard CPT4/ICD10PCS + records (e.g. CPT4 27447 total knee arthroplasty is not a descendant of + SNOMED 43531648 Total knee replacement). + +None of this applies to Condition, where one standard vocabulary (SNOMED) +absorbs everything through `Maps to`. Each domain resolves to +standardness differently, so each needs its own guidance. + +## Proposed structure + +``` +packs/core/ + standard-concept-preference/ # KEEP name (always: true) — invariants only + standard-resolution-condition/ # domains: [Condition] + standard-resolution-procedure/ # domains: [Procedure] ← drafted + standard-resolution-measurement/ # domains: [Measurement] + standard-resolution-observation/ # domains: [Observation] + standard-resolution-drug/ # domains: [Drug] + standard-resolution-device/ # domains: [Device] + drug-concept-granularity/ # unchanged; see_also from -drug +``` + +The catalogue only discovers `packs///manifest.yaml`, so the +domain packs are flat siblings rather than nested children. The link +between them is `see_also` plus `applicability.domains`, which +`PackApplicability.matches()` already filters on. + +### Slim core: `standard-concept-preference` + +Keep the name. It is always loaded and the bundled-baseline test asserts it +exists (`tests/unit/test_knowledge_catalogue.py:218`). Contents: + +- Active-target rule. Follow `Maps to` from `U`; never target `D`. +- `S` required for `*_concept_id`. `C` is never a target. Non-standard goes + in `*_source_concept_id`. +- **New:** standardness is per code. Check `standard_concept` on the + concept, never infer it from `vocabulary_id`. +- **New:** a standard source concept maps to itself. Do not re-map it to a + "preferred" vocabulary. +- **New:** `Maps to` is the only mapping edge. `eq`, `cat`, `poss_eq` and + `Is a` are evidence, not mappings. +- `concept_id = 0` fallback. +- Loading rule: "after resolving a target's `domain_id`, load + `standard-resolution-`". +- The `vocab_preference` block is removed and moves into the domain packs. + +### Domain pack template + +Every `standard-resolution-` pack uses the same sections, so an +agent knows where to look: + +| Section | Purpose | +|---|---| +| 1. Standard vocabularies in this domain | Which vocabularies hold `S` concepts here, and roughly what share. Snapshot counts carry a `verified_against` vocabulary version. | +| 2. Resolution path by input type | (a) code from a standard vocabulary, (b) non-standard code, (c) free text. Each has its own path. | +| 3. Relationship semantics | Which relationship_ids are mappings, which are evidence, and the known failure modes of each. | +| 4. Concept-set implications | Whether descendants of one vocabulary cover the others. If not, how to build a set that does. | +| 5. Traps | Concrete patterns the vocabulary gets wrong, with concept IDs. | +| 6. Verification checklist | What to check before accepting a target. | + +`rules.yaml` carries the machine-readable half under the same keys: +`standard_vocabularies`, `resolution_paths`, `relationship_semantics`, +`concept_set_roots`, `traps`, `checks`. Worked cases go in +`examples.yaml`, which `PackContent` already loads. + +### What each domain pack must cover (outline) + +- **Condition:** SNOMED is the only standard vocabulary. ICD10CM/ICD9CM/ICD10 + reach it via `Maps to`, sometimes 1:n (see `one-to-many-mapping`). Covers + `Maps to value` for pre-coordinated codes (see `pre-coordinated-split`), + Cancer Modifier / ICDO3 for oncology, and the domain-change trap (an + ICD10CM code mapping into Observation). +- **Procedure:** see the draft. Multiple standard vocabularies, self-maps, + `eq`/`cat` semantics, attribute-based alignment, concept sets with + several roots. +- **Measurement:** LOINC and SNOMED are both standard. LOINC wins for + lab tests. Covers CPT4 lab codes (1.3k standard CPT4 Measurement codes, + plus `CPT4 - LOINC eq`) and the test+result split. +- **Observation:** SNOMED / LOINC / CPT4 E&M / HCPCS. Covers history-of + (see `history-of-event`) and E&M visit codes landing in Observation. +- **Drug:** RxNorm before RxNorm Extension. Covers HCPCS J-codes and CPT4 + vaccine/drug codes that are non-standard and map to RxNorm/CVX. + Granularity is delegated to `drug-concept-granularity`. +- **Device:** HCPCS Level II is the main standard source (5.2k S). Covers + SNOMED physical objects and the Procedure/Device split for + implant-plus-procedure codes. + +## Mechanism note + +`post-filter` is declared in manifests, but nothing reads `rules.yaml` +programmatically today. `knowledge_pack` only returns it in the payload +(`tools/knowledge_tools.py:189`). The domain packs keep `rules.yaml` +declarative so a future post-filter can use it. The guidance is written to +stand on its own when it is context-injected. + +## Migration + +1. Land `standard-resolution-procedure`. It adds content and changes + nothing existing. +2. Draft the remaining domain packs from the template. +3. Slim `standard-concept-preference` to the invariants and add the + loading rule. Bump it to `version: "2.0"`. +4. Mirror into `gw-plugins/kb-prompts/packs/core/`, whose + `standard-concept-preference` is currently byte-identical. diff --git a/docs/tools/concept.md b/docs/tools/concept.md index 48261d2..7fc4b0f 100644 --- a/docs/tools/concept.md +++ b/docs/tools/concept.md @@ -83,7 +83,7 @@ Returns descendants of a concept up to `max_depth` hops (default 3, max 10). ## `concept_relationships` -Returns all direct relationships (edges) for one concept, grouped by direction. +Returns all direct relationships (edges) for one concept, grouped by direction. Edges are not filtered by domain, so cross-domain attribute edges (a Procedure's `Has method` → Observation, `Has dir proc site` → Spec Anatomic Site) are included. ```json {"concept_id": 4119419} @@ -227,6 +227,7 @@ Returns a bounded multi-hop neighborhood around one concept. All parameters except `concept_id` are optional. - `predicate_kinds` restricts traversal to specific relationship families. +- Edges are followed across domains (unlike `concept_path`'s `within_domain=true` default), so procedure attributes are reachable. - `max_depth` is clamped to `1..4`. - `max_nodes` is clamped to `10..500`. @@ -265,7 +266,7 @@ Use this when you want a local relationship subgraph without committing to a sin ## `concept_associations` -Enumerates classified `ASSOCIATION` edges such as therapeutic links between a regimen and its component drugs, or relationships between a drug and an indication. Use it when you need to discover associated concepts from one seed; `concept_neighbors` and `concept_relationships` do not provide this same classified association view. +Enumerates classified `ASSOCIATION` edges such as therapeutic links between a regimen and its component drugs, or relationships between a drug and an indication. Use it when you need to discover associated concepts from one seed, filtered by subkind and with standard flags; `concept_relationships` lists every edge kind unfiltered. SNOMED defining attributes (finding and procedure sites, method, device, substance) are `ATTRIBUTE`, not `ASSOCIATION`: use `concept_neighbors` with `predicate_kinds: ["ATTRIBUTE"]` for those. ```json { diff --git a/src/groundworkers/adapters/omop_graph.py b/src/groundworkers/adapters/omop_graph.py index 07ec549..3f9ebdd 100644 --- a/src/groundworkers/adapters/omop_graph.py +++ b/src/groundworkers/adapters/omop_graph.py @@ -200,6 +200,8 @@ def edges( """Return normalized edges for one concept (no concept-name enrichment). Each edge: ``{subject_id, object_id, predicate_id, predicate_kind, valid}``. + Edges are not filtered by domain: attribute and mapping edges usually + cross domains (Procedure -> Spec Anatomic Site, Condition -> Observation). """ try: raw = self._get_kg().edges( @@ -207,6 +209,7 @@ def edges( direction=direction, predicate_kinds=predicate_kinds, active_only=active_only, + within_domain=False, ) except Exception as exc: raise self._wrap_graph_error(exc, default_code="QUERY_ERROR") diff --git a/src/groundworkers/config/predicate_mapping.csv b/src/groundworkers/config/predicate_mapping.csv index 133e21d..65f3b7c 100644 --- a/src/groundworkers/config/predicate_mapping.csv +++ b/src/groundworkers/config/predicate_mapping.csv @@ -16,33 +16,33 @@ Association,Therapeutic,May be treated by,May be treated by (NDF-RT),"Links ther Association,Therapeutic,May treat,May treat (NDF-RT),"Links therapeutic interventions to clinical indications or contraindications. Example: [HYDROGEN PEROXIDE 3% SPRAY,TOP] -> [Surgical Wound Infection]",, Attribute,Temporal,Dev intended site of,Device intended site of (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Structure of lower thoracic esophagus] -> [Lower esophageal electrical stimulation system lead]",Represents the site where the device is intended to reside in or on the body.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/physical-object/physical-object-defining-attributes#has-device-intended-site Attribute,Anatomical,Dir morph of,Direct morphology of (SNOMED),"Defines a physical/structural abnormality at the cellular or tissue level. Example: [Benign neurofibroma] -> [Excision of neurofibroma, extensive]",Direct morphology (attribute) describes the morphologically abnormal structure that is the direct object of the Method action.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-morphology -Association,Diagnostic,Dir proc site of,Direct procedure site of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Structure of part of left hand] -> [Partial amputation of left hand]","is used when the action of the procedure is directly aimed at anatomical or acquired body structure or site, rather than something else located there",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-site-direct +Attribute,Anatomical,Dir proc site of,Direct procedure site of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Structure of part of left hand] -> [Partial amputation of left hand]","is used when the action of the procedure is directly aimed at anatomical or acquired body structure or site, rather than something else located there",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-site-direct Attribute,Anatomical,Direct site of,Direct site of (SNOMED),"Spatial qualifier describing location, morphology, or extent. Example: [Tissue specimen from penis obtained by excision] -> [Surgical margin involved by noninvasive premalignant neoplasm of penis in excised tissue specimen]",, -Association,Diagnostic,Finding site of,Finding site of (SNOMED),Links diagnostic evidence or tests to a clinical concept/state. Example: [Entire right fibula] -> [Longitudinal deficiency of bilateral fibulae],Specifies the body site affected by a condition.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#finding-site +Attribute,Anatomical,Finding site of,Finding site of (SNOMED),Links diagnostic evidence or tests to a clinical concept/state. Example: [Entire right fibula] -> [Longitudinal deficiency of bilateral fibulae],Specifies the body site affected by a condition.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#finding-site Attribute,Anatomical,Has asso morph,Has associated morphology (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Neoplasm of uncertain behavior of larynx] -> [Neoplasm of uncertain behavior],specifies the morphologic changes seen at the tissue or cellular level that are characteristic of a disease.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#associated-morphology Association,Administrative,Has asso visit,Has associated visit (OMOP),"Operational link for system billing, metadata, dictionaries, or encounter tracking. Example: [Remote physiologic monitoring treatment management services, clinical staff/physician/other qualified health care professional time in a calendar month requiring interactive communication with the patient/caregiver during the month; each additional 20...] -> [Telehealth Provided Other than in Patient’s Home]",,https://github.com/OHDSI/Vocabulary-v5.0/wiki/Vocab.-HCPCS#hcpcs-to-visit-relationships Attribute,Temporal,Has dev intend site,Has device intended site (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Neonatal indwelling urethral drainage catheter] -> [Structure of genitourinary system]",Represents the site where the device is intended to reside in or on the body.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/physical-object/physical-object-defining-attributes#has-device-intended-site Attribute,Anatomical,Has dir morph,Has direct morphology (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Primary open reduction of fracture of short bone and fixation using screw] -> [Fracture],Direct morphology (attribute) describes the morphologically abnormal structure that is the direct object of the Method action.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-morphology -Association,Diagnostic,Has dir proc site,Has direct procedure site (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Tc-99 MIBI (technetium 99m methoxyisobutylisonitrile) parathyroid subtraction study] -> [Parathyroid structure]","is used when the action of the procedure is directly aimed at anatomical or acquired body structure or site, rather than something else located there",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-site-direct +Attribute,Anatomical,Has dir proc site,Has direct procedure site (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Tc-99 MIBI (technetium 99m methoxyisobutylisonitrile) parathyroid subtraction study] -> [Parathyroid structure]","is used when the action of the procedure is directly aimed at anatomical or acquired body structure or site, rather than something else located there",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-site-direct Attribute,Anatomical,Has direct site,Has direct site (SNOMED),"Spatial qualifier describing location, morphology, or extent. Example: [Length of primary malignant neoplasm of prostate in resected prostate specimen] -> [Resected prostate specimen]",, -Association,Diagnostic,Has finding site,Has finding site (SNOMED),Links diagnostic evidence or tests to a clinical concept/state. Example: [Neoplasm of uncertain behavior of larynx] -> [Laryngeal structure],,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#finding-site +Attribute,Anatomical,Has finding site,Has finding site (SNOMED),Links diagnostic evidence or tests to a clinical concept/state. Example: [Neoplasm of uncertain behavior of larynx] -> [Laryngeal structure],,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#finding-site Attribute,Anatomical,Has indir morph,Has indirect morphology (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Drainage of abscess using computed tomography guidance] -> [Abscess morphology],, -Association,Diagnostic,Has indir proc site,Has indirect procedure site (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Total removal of prosthetic material from previous repair of vaginal prolapse] -> [Vaginal structure]",, +Attribute,Anatomical,Has indir proc site,Has indirect procedure site (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Total removal of prosthetic material from previous repair of vaginal prolapse] -> [Vaginal structure]",, Attribute,Temporal,Has intended site,Has dose form intended site (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Conventional release oral pellets in capsule] -> [Oral]",, Attribute,Anatomical,Has morphology,Has Morphology (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level.,, Attribute,Anatomical,Has proc morph,Has procedure morphology (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Endovenous mechanochemical ablation of varicose vein of lower limb] -> [Varix],"Procedure morphology (attribute) is used to specify the morphology, or abnormal structure, involved in a procedure. It is used when defining general concepts that subsume direct and indirect morphology. It subsumes the more specific attributes, Direct and Indirect Morphology. These should be used, if possible. ",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-morphology -Association,Diagnostic,Has proc site,Has procedure site (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Introduction of vaginal tape] -> [Vaginal structure]",, +Attribute,Anatomical,Has proc site,Has procedure site (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Introduction of vaginal tape] -> [Vaginal structure]",, Attribute,Anatomical,Has specimen morph,Has specimen source morphology (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Swab from bite wound] -> [Bite wound],Specifies the morphologic abnormality from which a specimen is obtained.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/specimen/specimen-defining-attributes#specimen-source-morphology Attribute,Anatomical,Indir morph of,Indirect morphology of (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Cyst] -> [Aspiration of cyst of thyroid using ultrasonographic guidance],, -Association,Diagnostic,Indir proc site of,Indirect procedure site of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Vertebral column region] -> [Insertion of neurostimulator electrodes into the spinal cord]","specifies the anatomical location but is not the direct focus of the procedure. The direct object of the action may be a device, a substance, or a morphologic abnormality that is not a part of the tissue structure of the anatomical site in which it is located, such as a calculus, thrombus, or foreign body.",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-site-indirect +Attribute,Anatomical,Indir proc site of,Indirect procedure site of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Vertebral column region] -> [Insertion of neurostimulator electrodes into the spinal cord]","specifies the anatomical location but is not the direct focus of the procedure. The direct object of the action may be a device, a substance, or a morphologic abnormality that is not a part of the tissue structure of the anatomical site in which it is located, such as a calculus, thrombus, or foreign body.",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-site-indirect Attribute,Temporal,Intended site of,Dose form intended site of (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Intravitreal] -> [Prolonged-release intravitreal implant]",, Attribute,Anatomical,Morphology of,Morphology of (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level.,, Attribute,Anatomical,Proc morph of,Procedure morphology of (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Cyst] -> [Operation on cyst of epididymis],"Procedure morphology (attribute) is used to specify the morphology, or abnormal structure, involved in a procedure. It is used when defining general concepts that subsume direct and indirect morphology. It subsumes the more specific attributes, Direct and Indirect Morphology. These should be used, if possible. ",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#procedure-morphology -Association,Diagnostic,Proc site of,Procedure site of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Vertebral column region] -> [Implantation of intrathecal drug delivery device adjacent to spinal cord]",, +Attribute,Anatomical,Proc site of,Procedure site of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Vertebral column region] -> [Implantation of intrathecal drug delivery device adjacent to spinal cord]",, Association,Administrative,Question source of,Question source of,"Operational link for system billing, metadata, dictionaries, or encounter tracking. Example: [RAND American Life Panel] -> [Do you have a child(ren) that are currently enrolled in childcare centers, schools, or colleges?]",, Attribute,Anatomical,Specimen morph of,Specimen source morphology of (SNOMED),Defines a physical/structural abnormality at the cellular or tissue level. Example: [Peritoneal dialysis catheter insertion site] -> [Body fluid specimen from peritoneal dialysis insertion site],Specifies the morphologic abnormality from which a specimen is obtained.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/specimen/specimen-defining-attributes#specimen-source-morphology Attribute,Anatomical,Has laterality,Has laterality (SNOMED),"Spatial qualifier describing location, morphology, or extent. Example: [Structure of flexor A4 pulley of index finger] -> [Side]",, @@ -55,10 +55,10 @@ Attribute,Context,Context of,Context of (HemOnc),Specific situational modifier o Attribute,Context,Finding context of,Finding context of (SNOMED),Contextual modifier for a clinical finding. Example: [Known] -> [Family grieving],, Attribute,Context,Has context,Has context (HemOnc),Specific situational modifier or contextual role. Example: [AC-D] -> [Neoadjuvant therapy],,https://hemonc.org/wiki/HemOncKB_canonical_triples Attribute,Context,Has finding context,Has finding context (SNOMED),Contextual modifier for a clinical finding. Example: [Elevated blood-pressure reading without diagnosis of hypertension] -> [Known present],, -Association,Diagnostic,Has proc context,Has procedure context (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Counselled by health visitor] -> [Done]",, +Attribute,Context,Has proc context,Has procedure context (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Counselled by health visitor] -> [Done]",, Attribute,Context,Has relat context,Has subject relationship context (SNOMED),Specific situational modifier or contextual role. Example: [Elevated blood-pressure reading without diagnosis of hypertension] -> [Subject of record],, Attribute,Temporal,Has temporal context,Has temporal context (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Elevated blood-pressure reading without diagnosis of hypertension] -> [Current]",, -Association,Diagnostic,Proc context of,Procedure context of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Recommended] -> [Hepatitis B immunization recommended]",, +Attribute,Context,Proc context of,Procedure context of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Recommended] -> [Hepatitis B immunization recommended]",, Attribute,Context,Relat context of,Subject relationship context of (SNOMED),Specific situational modifier or contextual role. Example: [Spouse of subject] -> [Spouse unwell],, Attribute,Temporal,Temporal context of,Temporal context of (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Current or past] -> [3.5 year examination not wanted]",, Attribute,Measurement,Has Scale,Has Scale,"Defines metrics, scales, units, and quantitative properties of an observation. Example: [Oral Health] -> [In the course of your current illness or its treatment, not only the past week:]",, @@ -148,7 +148,7 @@ Association,Mapping,Concept poss_eq from,Active possibly_equivalent_to inactive Association,Mapping,Concept poss_eq to,Inactive possibly_equivalent_to active (SNOMED),"SNOMED's loose, many-to-many fallback for retired concepts with no clean 1:1 successor. It is not a true equivalence, and not an administrative or billing link either; see linked known-issues page.",,https://github.com/OHDSI/Vocabulary-v5.0/wiki/Known-Issues-in-Vocabularies#snomed Identity,Version lineage,Concept replaced by,Concept replaced by,Handles lifecycle transitions and deprecated concept history.,, Identity,Version lineage,Concept replaces,Concept replaces,Handles lifecycle transitions and deprecated concept history.,, -Composition,Structural,Has dir subst,Has direct substance (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Administration of SARS-CoV-2 mRNA vaccine] -> [SARS-CoV-2 mRNA vaccine],Direct substance (attribute) describes the Substance or Pharmaceutical/biologic product on which the procedure's method directly acts.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-substance +Attribute,Procedural,Has dir subst,Has direct substance (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Administration of SARS-CoV-2 mRNA vaccine] -> [SARS-CoV-2 mRNA vaccine],Direct substance (attribute) describes the Substance or Pharmaceutical/biologic product on which the procedure's method directly acts.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-substance Attribute,Clinical,Has non-avail ind,Has VMP non-availability indicator (SNOMED),"Qualifies clinical states, statuses (e.g., severity, intent, prescribing status), or general clinical indicators.",, Association,Therapeutic,AB-drug cjgt of,Antibody-drug conjugate of (HemOnc),Links therapeutic interventions to clinical indications or contraindications. Example: [Loncastuximab tesirine] -> [Loncastuximab tesirine monotherapy],,https://hemonc.org/wiki/HemOncKB_canonical_triples Attribute,Anatomical,Absorbability of,Absorbability of (SNOMED),"Spatial qualifier describing location, morphology, or extent. Example: [Bioabsorbable] -> [Surgical plant polysaccharide hemostatic agent]",Represents the absorbability aspect of an object.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/physical-object/physical-object-defining-attributes#has-absorbability @@ -214,7 +214,7 @@ Composition,Structural,Denom value of,Presentation strength denominator value of Composition,Structural,Denominator unit of,Presentation strength denominator unit of (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Actuation] -> [Product containing precisely fluticasone propionate 500 microgram and salmeterol (as salmeterol xinafoate) 50 microgram/actuation conventional release powder for inhalation],, Attribute,Procedural,Device used by,Device used by (SNOMED),"Defines execution technicalities like route of administration, method, or device used. Example: [Oocyte aspiration needle] -> [Laparoscopic oocyte recovery]",, Attribute,Procedural,Dir device of,Direct device of (SNOMED),"Defines execution technicalities like route of administration, method, or device used. Example: [Accelerometer] -> [Provision of accelerometer]",Direct device (attribute) represents the device on which the method directly acts.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-device -Composition,Structural,Dir subst of,Direct substance of (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [SARS-CoV-2 antigen vaccine] -> [Administration of SARS-CoV-2 antigen vaccine],Direct substance (attribute) describes the Substance or Pharmaceutical/biologic product on which the procedure's method directly acts.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-substance +Attribute,Procedural,Dir subst of,Direct substance of (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [SARS-CoV-2 antigen vaccine] -> [Administration of SARS-CoV-2 antigen vaccine],Direct substance (attribute) describes the Substance or Pharmaceutical/biologic product on which the procedure's method directly acts.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#direct-substance Attribute,Anatomical,Direction of,Direction of,"Spatial qualifier describing location, morphology, or extent. Example: [Negative] -> [Have you had any unintentional release (leakage) of urine?]",, Attribute,Clinical,Disc indicator of,Discontinued indicator of (SNOMED),"Qualifies clinical states, statuses (e.g., severity, intent, prescribing status), or general clinical indicators.",, Attribute,Pharmaceutical,Disp dose form of,Dispensed dose form of (SNOMED),Qualifies physical drug delivery properties or dose forms. Example: [Tablet] -> [Levothyroxine sodium 25 microgram oral tablet],,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/pharmaceutical-and-biologic-product/index/index/pharmaceutical-dose-form-naming-and-modeling-conventions @@ -323,7 +323,7 @@ Attribute,Procedural,Has method,Has method,"Defines execution technicalities lik Attribute,Procedural,Has modality,Has modality (HemOnc),"Defines execution technicalities like route of administration, method, or device used. Example: [AC-D] -> [Chemotherapy]",, Composition,Structural,Has numerator unit,Has presentation strength numerator unit (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Elagolix sodium 300 mg oral capsule] -> [mg],, Composition,Structural,Has numerator value,Has presentation strength numerator value (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [AbobotulinumtoxinA 500unit lyophilized powder for conventional release solution for injection] -> [Five hundred],, -Association,Therapeutic,Has occurrence,Has occurrence (SNOMED),Links therapeutic interventions to clinical indications or contraindications. Example: [Sickle cell-hemoglobin SS disease] -> [Congenital],"Occurrence (attribute) is applied to procedures during a period of life. For example, procedures related to the gravid uterus should be modeled by the combination of the occurrence of the maternal pregnancy period and the uterus.",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#occurrence +Attribute,Temporal,Has occurrence,Has occurrence (SNOMED),Links therapeutic interventions to clinical indications or contraindications. Example: [Sickle cell-hemoglobin SS disease] -> [Congenital],"Occurrence (attribute) is applied to procedures during a period of life. For example, procedures related to the gravid uterus should be modeled by the combination of the occurrence of the maternal pregnancy period and the uterus.",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#occurrence Association,Therapeutic,Has off-label ind,Has off-label drug indication (FDB),Links therapeutic interventions to clinical indications or contraindications.,, Attribute,Pharmaceutical,Has ontological form,Has ontological dose form presentation (DM+D),Qualifies physical drug delivery properties or dose forms.,, Composition,Containment,Has pack,Has pack (SNOMED),Links items to their physical packaging or multi-item kit groupings.,, @@ -340,7 +340,7 @@ Hierarchy,Categorical – up,Has recipient cat,Has recipient category (SNOMED),A Association,Therapeutic,Has regimen type,Has regimen type (HemOnc),Links therapeutic interventions to clinical indications or contraindications.,, Attribute,Measurement,Has scale type,Has scale type,"Defines metrics, scales, units, and quantitative properties of an observation. Example: [SARS-CoV-2 (COVID-19) Ab [Presence] in Serum or Plasma by Immunoassay] -> [Ord]",, Association,Diagnostic,Has specimen proc,Has specimen procedure (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Swab from bite wound] -> [Taking of swab]",Identifies the procedure by which a specimen is obtained.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/specimen/specimen-defining-attributes#specimen-procedure -Association,Diagnostic,Has specimen topo,Has specimen source topography (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Cervicovaginal washout fluid sample] -> [Vaginal cervix]",, +Attribute,Anatomical,Has specimen topo,Has specimen source topography (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Cervicovaginal washout fluid sample] -> [Vaginal cervix]",, Attribute,Pharmaceutical,Has standard form,Has standard Dose Form (OMOP),Qualifies physical drug delivery properties or dose forms.,, Attribute,Temporal,Has start date,Has start date,"Qualifies clinical events by their timing, sequence, or duration. Example: [Regional Nodes Positive] -> [Date of Diagnosis]",, Association,Therapeutic,Has steroid tx,Has steroid therapy (HemOnc),Links therapeutic interventions to clinical indications or contraindications. Example: [Dara-KRd] -> [Dexamethasone],,https://hemonc.org/wiki/HemOncKB_canonical_triples @@ -428,7 +428,7 @@ Association,Administrative,NUCC - CMS Specialty,NUCC to CMS Specialty (CMS),"Ope Identity,Exact equivalence,Number mapped from,Value_as_number to non-standard map (OMOP),Direct semantic identity or mapping rule across different vocabularies.,, Composition,Structural,Numerator unit of,Presentation strength numerator unit of (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Million unit] -> [Product containing precisely lenograstim 13.4 million unit/1 vial powder for conventional release solution for injection],, Composition,Structural,Numerator value of,Presentation strength numerator value of (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Eight thousand] -> [Product containing precisely epoetin alfa 8000 unit/0.8 milliliter conventional release solution for injection syringe],, -Association,Therapeutic,Occurrence of,Occurrence of (SNOMED),Links therapeutic interventions to clinical indications or contraindications. Example: [Childhood - period] -> [Benign occipital epilepsy of childhood - late onset variant],"Occurrence (attribute) is applied to procedures during a period of life. For example, procedures related to the gravid uterus should be modeled by the combination of the occurrence of the maternal pregnancy period and the uterus.",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#occurrence +Attribute,Temporal,Occurrence of,Occurrence of (SNOMED),Links therapeutic interventions to clinical indications or contraindications. Example: [Childhood - period] -> [Benign occipital epilepsy of childhood - late onset variant],"Occurrence (attribute) is applied to procedures during a period of life. For example, procedures related to the gravid uterus should be modeled by the combination of the occurrence of the maternal pregnancy period and the uterus.",https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#occurrence Attribute,Temporal,Occurs after,Occurs after (SNOMED),"Qualifies clinical events by their timing, sequence, or duration. Example: [Vomiting after gastrointestinal tract surgery] -> [Operation on gastrointestinal tract]",, Association,Mapping,OPCS4 - SNOMED,OPCS4 to SNOMED equivalent or categorical (NHS),"Per its own r_name, this bundles true 1:1 equivalences together with loose categorical or broader mappings, used when no clean OPCS4-to-SNOMED equivalent exists. It is a no-clean-mapping fallback, not a trustworthy equivalence.",, Identity,Exact equivalence,Operator mapped from,Operator concept to non-standard map (OMOP),Direct semantic identity or mapping rule across different vocabularies.,, @@ -500,11 +500,11 @@ Association,Mapping,SNOMED - OPCS4,SNOMED to OPCS4 equivalent or categorical (NH Hierarchy,Categorical – up,SNOMED cat - CPT4,SNOMED category to CPT-4 (OMOP),"Assigns a concept to a high-level grouping or classification chapter. Example: [Hospital admission] -> [Induced abortion, by 1 or more intra-amniotic injections (amniocentesis-injections), including hospital admission and visits, delivery of fetus and secundines; with hysterotomy (failed intra-amniotic injection)]",,https://github.com/OHDSI/Vocabulary-v5.0/wiki/Vocab.-SNOMED#equivalent-and-categorical-relationships Association,Administrative,SNOMED proc - HCPCS,SNOMED procedure subsumes HCPCS (OMOP),"Operational link for system billing, metadata, dictionaries, or encounter tracking.",, Association,Diagnostic,Specimen of,Specimen of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Tissue specimen from left breast] -> [Mammography of left breast specimen]",Indicates the type of specimen on which a measurement or observation is performed.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/evaluation-procedure-attributes#has-specimen -Association,Diagnostic,Specimen topo of,Specimen source topography of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Structure of right cerebellar hemisphere] -> [Tissue specimen from right hemisphere of cerebellum]",, +Attribute,Anatomical,Specimen topo of,Specimen source topography of (SNOMED),"Links evidence, tests, variants, or clinical findings to support diagnostic reasoning. Example: [Structure of right cerebellar hemisphere] -> [Tissue specimen from right hemisphere of cerebellum]",, Attribute,Temporal,Start date of,Start date of,"Qualifies clinical events by their timing, sequence, or duration. Example: [RX Date BRM] -> [RX Summ--BRM]",, Association,Therapeutic,Steroid tx of,Steroid therapy of (HemOnc),Links therapeutic interventions to clinical indications or contraindications. Example: [Dexamethasone] -> [Selinexor and Dexamethasone (Sd)],,https://hemonc.org/wiki/HemOncKB_canonical_triples Hierarchy,Categorical – up,Subject matter of,Subject matter domain of (LOINC),Assigns a concept to a high-level grouping or classification chapter. Example: [Oncology] -> [Oncology Plan of care note],,https://github.com/OHDSI/Vocabulary-v5.0/wiki/Vocab.-LOINC#relationships -Composition,Structural,Subst used by,Substance used by (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Vitamin supplement] -> [Vitamin supplement therapy],,https://github.com/OHDSI/Vocabulary-v5.0/wiki/Vocab.-LOINC#relationships +Attribute,Procedural,Subst used by,Substance used by (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Vitamin supplement] -> [Vitamin supplement therapy],,https://github.com/OHDSI/Vocabulary-v5.0/wiki/Vocab.-LOINC#relationships Attribute,Pharmaceutical,Surf character of,Surface characteristic of (SNOMED),Qualifies physical drug delivery properties or dose forms. Example: [Saline-filled smooth breast implant] -> [Smooth],, Association,Therapeutic,Synth regimen of,Synthetic regimen of(HemOnc),Links therapeutic interventions to clinical indications or contraindications. Example: [PCR (rituximab-abbs)] -> [PCR],,https://hemonc.org/wiki/HemOncKB_canonical_triples Attribute,Clinical,Targ population of,Target population of (SNOMED),"Qualifies clinical states, statuses (e.g., severity, intent, prescribing status), or general clinical indicators. Example: [Adult population] -> [Hepatitis A virus antigen only adult vaccine product]",, @@ -525,7 +525,7 @@ Attribute,Procedural,Using acc device,Using access device (SNOMED),"Defines exec Attribute,Procedural,Using energy,Using energy (SNOMED),"Defines execution technicalities like route of administration, method, or device used. Example: [Photodynamic laser therapy to lesion of skin] -> [Light]",Using energy (attribute) refers to the energy used to execute an action., Association,Diagnostic,Using finding inform,Using finding informer (SNOMED),Links diagnostic evidence or tests to a clinical concept/state. Example: [Urinary symptoms] -> [Subject of record or other provider of history],Specifies the person or other entity from which the clinical finding information was obtained. It is not about the particular individual but about the category or type of informer. It is used to differentiate patient-reported symptoms from provider-determined signs.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#finding-informer Association,Diagnostic,Using finding method,Using finding method (SNOMED),Links diagnostic evidence or tests to a clinical concept/state. Example: [Abnormal breath sounds] -> [Auscultation],Specifies the means by which a clinical finding was determined. It includes findings that were determined by examination of the patient.,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/clinical-finding-and-disorder/clinical-finding-defining-attributes#finding-method -Composition,Structural,Using subst,Using substance (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Tc-99 MIBI (technetium 99m methoxyisobutylisonitrile) parathyroid subtraction study] -> [Radioactive isotope],Using substance (attribute) describes the Substance used to execute the action of a procedure. ,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#using-substance +Attribute,Procedural,Using subst,Using substance (SNOMED),Defines the inherent chemical constituents or dosage strength. Example: [Tc-99 MIBI (technetium 99m methoxyisobutylisonitrile) parathyroid subtraction study] -> [Radioactive isotope],Using substance (attribute) describes the Substance used to execute the action of a procedure. ,https://docs.snomed.org/education/snomed-ct-education-editorial-guide/readme/authoring/domain-specific-modeling/procedure/procedure-defining-attributes/index#using-substance Hierarchy,Categorical – up,Vaccine group of,Vaccine group of (CVX),"Assigns a concept to a high-level grouping or classification chapter. Example: [Anthrax vaccine, unspecified] -> [Anthrax vaccine, adsorbed, adjuvanted, post-exposure prophylaxis]",, Identity,Exact equivalence,Value mapped from,Value_as_concept to non-standard map (OMOP),Direct semantic identity or mapping rule across different vocabularies. Example: [Hospice] -> [Patients who use hospice services any time during the measurement period],, Attribute,Measurement,Value of,Value of,"Defines metrics, scales, units, and quantitative properties of an observation. Example: [refractometry result error: "" 2""] -> [Refractometry result unreliable (right)]",, diff --git a/src/groundworkers/tools/concept_tools.py b/src/groundworkers/tools/concept_tools.py index b108e4b..b3de95c 100644 --- a/src/groundworkers/tools/concept_tools.py +++ b/src/groundworkers/tools/concept_tools.py @@ -75,7 +75,12 @@ def concept_descendants(concept_id: int, max_depth: int = 3) -> dict[str, Any]: @server.tool("concept_relationships") def concept_relationships(concept_id: int) -> dict[str, Any]: - """Returns all relationships for a concept grouped by direction (inbound/outbound).""" + """Returns all relationships for a concept grouped by direction (inbound/outbound). + + Edges are not filtered by domain, so cross-domain attribute edges are + included: e.g. a Procedure's "Has method" (-> Observation), "Has dir + proc site" (-> Spec Anatomic Site) and "Has dir device" (-> Device). + """ if concept_id <= 0: return {"error": True, "code": "INVALID_INPUT", "message": "concept_id must be a positive integer"} try: @@ -168,12 +173,16 @@ def concept_neighbors( the edges connecting them. This reaches across all relationship types simultaneously — hierarchy, identity, attribute, composition, and association — unlike concept_ancestors / concept_descendants which only - follow the parent/child hierarchy. + follow the parent/child hierarchy. Edges are followed across domains + (unlike concept_path's within_domain=True default), so a Procedure's + attributes — method, procedure site, device, substance — are reached. Use this to discover what is conceptually related to a concept without knowing in advance which relationship types connect them. Typical uses: - finding associated anatomical sites, morphologies, or procedures for a clinical finding + - listing a procedure's method, site, device, and substance + attributes (predicate_kinds=["ATTRIBUTE"], max_depth=1) - mapping a drug concept to its ingredient, dose forms, and routes - exploring OMOP concept class membership and equivalence clusters @@ -222,10 +231,12 @@ def concept_associations( Each edge carries the related concept and its standard_concept flag, so a regimen resolves straight to its standard RxNorm ingredients in one call. - Why this is a separate tool: concept_neighbors and concept_relationships do - NOT surface ASSOCIATION edges (the graph's edge view omits them), and - concept_path only reaches them between two already-known concepts. Use this - to *enumerate* what a concept is clinically associated with. + Why this is a separate tool: concept_relationships lists every edge kind + unfiltered, and concept_path only reaches associations between two + already-known concepts. Use this to *enumerate* just what a concept is + clinically associated with, filtered by subkind, with standard flags. + Defining attributes such as finding and procedure sites are ATTRIBUTE, + not ASSOCIATION — use concept_neighbors(predicate_kinds=["ATTRIBUTE"]). direction: "out" (default; edges where this concept is the subject), "in", or "both". predicate_subkinds: optional filter on the classification subkind, e.g. ["Therapeutic"]. diff --git a/tests/unit/test_omop_graph_adapter.py b/tests/unit/test_omop_graph_adapter.py index 1c13f4d..6fd9963 100644 --- a/tests/unit/test_omop_graph_adapter.py +++ b/tests/unit/test_omop_graph_adapter.py @@ -333,6 +333,24 @@ def test_wrap_graph_error_classifies_primary_database_outage_before_setup_text() assert "database is unavailable" in wrapped.message +def test_edges_are_not_filtered_by_domain(monkeypatch): + # Procedure attributes cross domains (Has method -> Observation, Has dir + # proc site -> Spec Anatomic Site); a same-domain filter hides them all. + captured: dict[str, object] = {} + + class FakeKnowledgeGraph: + def edges(self, concept_ids, **kwargs): + captured.update(kwargs) + return () + + adapter = OmopGraphAdapter(_engine_with_graph_tables()) + monkeypatch.setattr(adapter, "_get_kg", lambda: FakeKnowledgeGraph()) + + adapter.edges(40482788, direction="out", active_only=True) + + assert captured["within_domain"] is False + + def _engine_with_graph_tables(): from omop_alchemy.cdm.model.vocabulary import Concept from omop_graph.extensions.omop_alchemy import RelationshipMapping diff --git a/tests/unit/test_predicate_mapping_classification.py b/tests/unit/test_predicate_mapping_classification.py new file mode 100644 index 0000000..543f9d0 --- /dev/null +++ b/tests/unit/test_predicate_mapping_classification.py @@ -0,0 +1,44 @@ +"""SNOMED defining attributes in the bundled predicate mapping are ATTRIBUTE. + +Procedure and finding sites were classed Association/Diagnostic while their +sibling attributes (Has method, Has dir device, Has asso morph) were +Attribute, so concept_neighbors(predicate_kinds=["ATTRIBUTE"]) missed them. +""" + +from __future__ import annotations + +import csv +from importlib.resources import files + +DEFINING_ATTRIBUTES = { + "Has dir proc site": ("Attribute", "Anatomical"), + "Dir proc site of": ("Attribute", "Anatomical"), + "Has indir proc site": ("Attribute", "Anatomical"), + "Indir proc site of": ("Attribute", "Anatomical"), + "Has proc site": ("Attribute", "Anatomical"), + "Proc site of": ("Attribute", "Anatomical"), + "Has finding site": ("Attribute", "Anatomical"), + "Finding site of": ("Attribute", "Anatomical"), + "Has specimen topo": ("Attribute", "Anatomical"), + "Specimen topo of": ("Attribute", "Anatomical"), + "Has proc context": ("Attribute", "Context"), + "Proc context of": ("Attribute", "Context"), + "Has occurrence": ("Attribute", "Temporal"), + "Occurrence of": ("Attribute", "Temporal"), + "Using subst": ("Attribute", "Procedural"), + "Subst used by": ("Attribute", "Procedural"), + "Has dir subst": ("Attribute", "Procedural"), + "Dir subst of": ("Attribute", "Procedural"), + "Has method": ("Attribute", "Procedural"), + "Has dir device": ("Attribute", "Procedural"), + "Has access": ("Attribute", "Procedural"), + "Using acc device": ("Attribute", "Procedural"), +} + + +def test_bundled_mapping_classes_snomed_defining_attributes_as_attributes() -> None: + mapping_file = files("groundworkers") / "config" / "predicate_mapping.csv" + with mapping_file.open(newline="") as handle: + classes = {row["r_id"]: (row["class"], row["subclass"]) for row in csv.DictReader(handle)} + + assert {r_id: classes.get(r_id) for r_id in DEFINING_ATTRIBUTES} == DEFINING_ATTRIBUTES From 48fb7a63bbb143dae98c1a15fb93c644e7d799ab Mon Sep 17 00:00:00 2001 From: Georgie Kennedy Date: Tue, 20 Oct 2026 14:24:43 -0400 Subject: [PATCH 2/4] docs updates for plugins and prompts re-config --- docs/concepts.md | 55 +++++++---- docs/development/plugins.md | 3 + docs/from-scratch.md | 104 +++++++++++--------- docs/index.md | 4 + docs/plugins.md | 95 ++++++++++++++++++ docs/usage/configuration.md | 176 ++++++++++++++++++++-------------- docs/usage/prompt-handling.md | 79 +++++++++++++++ mkdocs.yaml | 2 + 8 files changed, 383 insertions(+), 135 deletions(-) create mode 100644 docs/plugins.md create mode 100644 docs/usage/prompt-handling.md diff --git a/docs/concepts.md b/docs/concepts.md index 9173f5e..26a889d 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -1,25 +1,21 @@ # Concepts and capability choices -Groundworkers is a read-only capability layer for working with OMOP vocabularies and source metadata. It combines deterministic vocabulary and graph operations with optional retrieval and model-assisted steps, then makes those capabilities available through MCP, REST, or direct Python. +Groundworkers provides read-only tools for working with OMOP vocabularies and source metadata. It combines deterministic vocabulary and graph operations with optional retrieval and model-assisted steps, available through MCP, REST, or direct Python. -The important distinction is between the work a capability performs and the transport used to call it. MCP, REST, and Python use the same runtime and, where a service exists, the same service implementation. +MCP, REST, and Python are different ways to call the same runtime. Where a service exists, they share its implementation. ## The main workflow -Most mapping-oriented work follows this shape: +Mapping usually starts with source text or fields. Text preparation or domain classification can help narrow the search. Retrieval finds candidate concepts; grounding tries to resolve the input using ranked rules. Mapping brings the candidates, evidence, and graph context together for a caller or reviewer to choose a mapping. Optional semantic projection then uses the grounded concept and source context to produce CDM rows. ```mermaid -flowchart LR - SOURCE[Source text or fields] --> PREP[Optional text or domain preparation] - PREP --> RETRIEVE[Retrieve OMOP candidates] - RETRIEVE --> GROUND[Ground or assemble evidence] - GROUND --> REVIEW[Caller or reviewer selects a mapping] - REVIEW --> CONTEXT[Add graph and mapping context] - CONTEXT --> PROJECT[Optional semantic projection] +flowchart TD + SOURCE["Source text or fields"] -->|Retrieval and grounding| CANDIDATES["Candidate concepts and evidence"] + CANDIDATES -->|Caller or reviewer chooses| SELECTED["Selected concept or concepts"] + SELECTED -. Optional semantic projection .-> ROWS["CDM rows"] + SOURCE -. Source context .-> ROWS ``` -These stages are deliberately separate. Retrieval produces candidates; grounding applies a ranked resolution policy; mapping gathers evidence and context for a decision; semantic projection turns an already-grounded concept plus source context into CDM rows. - ## Grounding, mapping, and retrieval ### Retrieval @@ -37,15 +33,34 @@ Use the search tools when you want to choose the strategy yourself and inspect i Grounding is a ranked, policy-driven attempt to resolve free text to concepts. `concept_ground` runs its configured tiers in order and stops at the first tier that produces results. It reports the matched tier and whether embedding retrieval was used. Domain, vocabulary, ancestry, standardness, and activity constraints narrow the search. +```mermaid +flowchart TD + INPUT["Text and constraints"] --> SEARCH["Search the next configured tier"] + SEARCH --> MATCH{"Any matches?"} + MATCH -->|Yes: stop searching| RESULT["Return candidates
and the matched tier"] + MATCH -->|No| MORE{"More tiers?"} + MORE -->|Yes| SEARCH + MORE -->|No| EMPTY["Return no candidates"] +``` + Grounding is not proof that the first result is the correct mapping. A caller that needs reviewable evidence should use `concept_candidate_bundle`, inspect the candidate union, and add `concept_mapping_context` for the selected concept. ### Mapping Mapping is the broader decision workflow around a source term. Mapping tools combine retrieval channels, navigate non-standard concepts to standard targets, find broader standard ancestors, assemble graph context, follow `Maps to value` relationships, and evaluate predictions against references. They are useful when the caller must preserve evidence or ask a human or another model to make the final choice. +```mermaid +flowchart TD + CANDIDATES["Candidates from
multiple searches"] --> REVIEW["Caller or reviewer
assesses the evidence"] + SOURCE["Source meaning
and domain rules"] --> REVIEW + GRAPH["Standard targets
and graph context"] --> REVIEW + REVIEW --> DECISION["Mapping to one or more concepts
with supporting evidence"] + REVIEW -->|Insufficient evidence| OPEN["Leave unresolved"] +``` + ## Deterministic and model-assisted capabilities -Groundworkers does not treat every capability as an LLM task. +Most vocabulary and graph operations run without an LLM. Some text preparation and classification tasks use a configured model. | Capability | Deterministic? | Additional infrastructure | |---|---:|---| @@ -63,15 +78,21 @@ An embedding model and a chat model are separate configuration references. A dep ## Source planning and knowledge packs -Source planning is stateless analysis before ingestion. It decomposes an input artifact, normalizes its structure, annotates columns, and returns neutral planning artifacts such as column roles, domain hints, warnings, and an ingestion strategy. It does not own session state or decide how a particular orchestration job is persisted. `groundcrew` is one consumer of this worker-side capability. +Source planning analyses an input before ingestion. It decomposes the input, normalizes its structure, annotates columns, and returns results such as column roles, domain hints, warnings, and an ingestion strategy. The caller manages session state and job persistence. `groundcrew` uses this capability. -The deterministic `source_plan` path uses format and header evidence. `source_plan_assisted` is an explicit second step that can use the configured chat model when deterministic classification is not strong enough. The result records whether the assisted tier was used. +The deterministic `source_plan` path uses format and header evidence. Call `source_plan_assisted` as a separate step when that evidence is insufficient; it can use the configured chat model. The result records whether the assisted tier was used. Knowledge packs are a separate discovery mechanism. Bundled baseline packs and optional site or localisation packs contain reusable guidance and rules. `knowledge_catalogue` finds packs applicable to a source system, domain, or section; `knowledge_pack` loads the selected pack. Discovery can happen before or after source planning. +## Prompt guidance and user decisions + +An agent can use workflow prompts to organise a task and elicitation prompts to ask for missing information. Domain rules and tool results help it assess the answers. Answering a question may resolve one decision while leaving other work open. + +See [Working with prompts](usage/prompt-handling.md) for choosing guidance, handling unanswered questions, and requesting more explanation when needed. + ## Semantic projection -Grounding answers “which concept does this source item refer to?” Projection answers “given that concept and this source context, which CDM row or rows should be emitted?” It is useful for sibling-field modifiers, value-carried family history, deterministic suppression, and multi-row output definitions. Projection never chooses the concept and never calls an LLM. It is opt-in because a deployment must deliberately choose to expose the definition catalogue. +Grounding answers “which concept does this source item refer to?” Projection answers “given that concept and this source context, which CDM row or rows should be emitted?” It is useful for sibling-field modifiers, value-carried family history, deterministic suppression, and multi-row output definitions. Projection never chooses the concept and never calls an LLM. It must be enabled explicitly to expose the definition catalogue. ## Choose an interface @@ -85,7 +106,7 @@ Use `groundworkers --describe` to inspect the active MCP tools, prompts, resourc ## Relationship to companion packages -Groundworkers composes several focused packages rather than replacing them: +Groundworkers uses these packages: - `omop-alchemy` provides OMOP CDM models and database configuration; - `omop-graph` provides graph-backed concept traversal and resolver primitives; diff --git a/docs/development/plugins.md b/docs/development/plugins.md index 11db6c0..b61db1f 100644 --- a/docs/development/plugins.md +++ b/docs/development/plugins.md @@ -1,5 +1,8 @@ # Plugin architecture +For an overview and examples, start with [Plugins](../plugins.md). This guide +covers the implementation contract for plugin authors. + ## What a plugin is Groundworkers plugins are independently installed packages that add an adapter, service, and MCP surface without adding plugin-specific imports to Groundworkers. They register one ready-to-use plugin object under the `groundworkers.plugins` entry-point group. diff --git a/docs/from-scratch.md b/docs/from-scratch.md index 35ad2bf..5d82b7f 100644 --- a/docs/from-scratch.md +++ b/docs/from-scratch.md @@ -1,43 +1,37 @@ # Initial local setup -This guide sets up a local Groundworkers runtime against an existing OMOP CDM database. The vocabulary tables must already be populated; Groundworkers does not load vocabulary data. +Use this guide to connect Groundworkers to an existing OMOP CDM database and start a local service. The vocabulary tables must already be populated; Groundworkers does not load vocabulary data. -The minimum useful configuration is a CDM database. Graph indexes, embeddings, and a chat model add capabilities but are independent of the core vocabulary service. +Start with the CDM connection. You can add graph indexes, embeddings, a chat model, and plugins as you need them. A chat model is not required for vocabulary lookup or lexical grounding. -### Current Status +## Before you start -The TUI is offered for convenience and should be considered experimental - we don't persist state centrally so you will probably notice that you need to re-test connections if you move around the interface. +You need Python 3.12 or newer, the database connection details, and access to the CDM and vocabulary schemas. If the vocabulary is stored separately from the clinical tables, configure its connection and schema too. -The examples below show the most thoroughly validated workflow at this time. FAISS and sqlite options for storage and anything other than ollama configuration are all yours to experiment with. +The setup console helps you edit configuration, test connections, and run preparation tasks. It does not run your agent conversations or mapping jobs. Configuration is saved to a shared TOML file; maintenance runs have their own saved history. Connection and model checks may need to be rerun after reopening the console or changing a service. -### Known TODOs - -- **Plugin-backed optional capabilities:** Finalise the drafted plugin contract for adding backends without expanding the core composition root for every integration. A plugin should declare its configuration schema and adapter/service factories through an explicit entry point. Groundworkers should retain lifecycle, error, and transport policy. Backend-specific assumptions must remain inside the plugin rather than leaking into generic services. -- **Async concurrency hardening:** Model-facing MCP tools use omop-llm's native async completion, embedding, and availability APIs. Before increasing request concurrency, cover lazy initialization, provider connection limits, cancellation, and shared-backend safety with load tests. -- **Curated REST workflows:** Extend REST around stable, application-level workflows rather than mirroring every MCP tool. New endpoints need typed request/response models, the same validation and error semantics as the underlying services, explicit limits for expensive operations, and a deployment-level authentication decision before exposure outside a trusted network. - -### Key Dependency Details - -- **FastAPI** provides the curated REST transport, including request validation, exception mapping, OpenAPI generation, and application lifecycle hooks. REST is selected explicitly with `--transport rest`; it is not served alongside MCP by default. Business logic belongs in services, not route handlers. -- **FastMCP**, supplied by the official `mcp[cli]` package, provides MCP tool, prompt, and resource registration plus stdio, SSE, and streamable-HTTP transports. Groundworkers preserves each handler's signature and docstring for schema discovery and registers model-facing handlers as native async tools. -- **omop-llm** is the provider-neutral model API. MCP-facing provider calls use its async completion, embedding, and availability methods so a cached provider client remains attached to the transport's persistent event loop. The synchronous methods remain available to direct Python callers. -- **omop-emb and omop-graph** remain synchronous storage and graph dependencies. Their database and vector operations must stay outside the event-loop thread until those packages expose supported async contracts. +## 1. Install -### Maintenance tasks +In a new working directory, create and activate a virtual environment. These activation commands are for macOS and Linux: -Long running tasks (like embedding population) will be spawned and polled, and you can review progress on the `Runs` tab. These tasks should persist even if you close and re-open groundworkers because they are spawned as maintenance tasks. This is all best-effort at this time, with a primary goal of supporting a centralised configuration workflow only, not being an actual groundworkers interface for real agentive work. +```bash +uv venv --python 3.12 +source .venv/bin/activate +``` -Open **Runs** to follow progress, inspect a safe log tail, cancel active work, retry a safe failed step, or export commands. Postflight is shown only for plans that actually define postflight checks. +Install Groundworkers with the setup console: -## 1. Install +```bash +uv pip install "groundworkers[tui]" +``` -Install the setup console and the optional backends you plan to use: +If you plan to store embeddings in PostgreSQL, include its backend support: ```bash uv pip install "groundworkers[tui,embedding-pgvector]" ``` -The `tui` extra is needed for the setup console. Embedding backend selection is via extras either `embedding-faiss` / `embedding-pgvector`. Use the `all_source` extra for XLSX, PDF, and DOCX source-planning inputs. +Other extras include `embedding-faiss` for FAISS sidecar acceleration and `all_source` for XLSX, PDF, and DOCX source-planning inputs. See [Installation](usage/installation.md) for the available extras. ## 2. Open the setup console @@ -45,55 +39,69 @@ The `tui` extra is needed for the setup console. Embedding backend selection is groundworkers tui ``` -To work with a specific file: +The default configuration file is `~/.config/omop/config.toml`, unless `OA_CONFIG_PATH` selects another file. To choose a file explicitly: ```bash groundworkers tui --config-path /path/to/config.toml ``` -The console shows required CDM readiness separately from optional graph, embedding, chat, and integration status. Before applying a change, review the redacted configuration diff. +The console can help you choose a location and create or repair the configuration. Review the redacted diff before applying changes. Use the same configuration path when you later start the service. + +**Overview** shows CDM readiness separately from optional graph, embedding, chat, and integration status. An unconfigured optional feature does not mean the CDM service has failed. ## 3. Configure the CDM -Configure the physical connection and the logical CDM database. The setup flow writes the shared `[connections.*]`, `[databases.*]`, and `[tools.groundworkers]` entries; Groundworkers references those entries by name. +Choose **Overview → Configure CDM**, or open **Database**. Configure the physical connection and the logical CDM database, including the vocabulary location. The setup flow writes the shared `[connections.*]`, `[databases.*]`, and `[tools.groundworkers]` entries. Groundworkers refers to these entries by name. -Run **Test connections** or **Overview → Verify all**. A connected CDM with populated vocabulary tables is the minimum runtime needed for concept lookup, search, grounding, mapping, source planning, knowledge packs, and system status. +Run **Test connections** in Database or **Verify all** in Overview. A working CDM connection with populated vocabulary tables supports concept lookup, lexical search and grounding, and mapping tools. Some searches and graph operations also need the preparation described below. + +See [Configuration](usage/configuration.md) for the shared configuration layout and named references. ## 4. Prepare graph and search support -Open **Graph → Prepare graph** when readiness reports missing relationship, full-text, or functional indexes. The operation runs as a durable maintenance task. Use **Performance** to inspect index readiness and start supported index maintenance. +Open **Graph → Prepare graph** when readiness reports missing relationship, full-text, or functional indexes. Review the proposed commands before starting. **Performance** shows index readiness and offers further index maintenance. + +Preparation runs as a background maintenance task; follow it in **Runs**. The database account used for preparation needs permission to create the required indexes and supporting objects. Normal service queries remain read-only. -Graph preparation is not required for every CDM-only operation, but `concept_ground` and several concept and mapping tools use graph-backed operations when available. +You can use basic vocabulary operations before preparing every index. Prepare the search and graph support needed by the tools you intend to use, then rerun the checks. ## 5. Add embeddings (optional) -Embedding search needs all three of these pieces: +Embedding search needs a named embedding model, a named vector store, and vectors for the concepts you want to search. The query model must be compatible with the stored vectors. + +1. **Configure the store.** Use Database to select or create the vector-store configuration. +2. **Configure the model.** In Embeddings, choose **Configure model**. Supply the provider details and select a model already registered in the store or a new model from the provider. +3. **Initialize and check.** Use **Initialize embedding store** if needed, then **Check model** to check the store registration and test whether the provider can encode text with that model. +4. **Choose the scope and populate.** Refresh coverage, select the vocabularies you need, and choose **Populate**. Review the scope, run limit, and commands before starting. +5. **Check the result.** Follow the task in Runs, then refresh coverage to see what is available. -- a named embedding model; -- a named vector store; -- populated vectors for the intended vocabulary scope. +Store initialization and population write to the embedding store. Population starts only when requested; a search never triggers it. A run limit caps the amount of work in one run. Coverage and the population plan show which concepts still need vectors. -Configure the vector store under **Database**, configure the model under **Embeddings**, initialize the store, and run the population task. Population is explicit and does not start during a query. A run limit caps one maintenance run; it does not decide whether the run is a backfill. +An embedding model and a chat model are separate choices and can use different providers. Configuring one does not enable the other. ## 6. Add a chat model (optional) -Configure a provider and a structured-output chat model in **Chat Model**. Chat enables text preprocessing, structured-field domain classification, and assisted source planning. Credentials are redacted in reviews and diagnostics. +Open **Chat Model**, configure the provider, and run **Test provider**. Use **Select chat model** to choose a model that supports structured output. The separate **Chat** section checks the selected model's chat and structured-output capabilities. + +Chat enables text preparation and structured-field domain classification. Assisted source planning also needs `source_planning_llm_assisted_enabled` to be enabled; it is enabled by default. Provider support comes from `omop-llm`; use the console checks to verify your chosen endpoint and model. Credentials are redacted in configuration reviews and diagnostics. ## 7. Start or inspect the service -After setup, inspect the active runtime: +After setup, inspect what Groundworkers will expose for your configuration: ```bash groundworkers --config-path /path/to/config.toml --describe ``` -Start MCP over stdio for a client that launches Groundworkers: +This lists registered tools, prompts, resources, plugins, and redacted configuration details. Use the console's connection and model checks to verify that the configured services are reachable too. + +For an agent client that launches Groundworkers, use MCP over stdio: ```bash groundworkers --config-path /path/to/config.toml --transport stdio ``` -Start a shared Streamable HTTP service: +To run an MCP service that clients can connect to over HTTP: ```bash groundworkers \ @@ -103,7 +111,7 @@ groundworkers \ --port 8000 ``` -REST is a separate explicit transport: +REST runs as a separate transport, with selected mapping and source-planning endpoints: ```bash groundworkers \ @@ -113,7 +121,13 @@ groundworkers \ --port 8080 ``` -The `--describe` output shows the exact active tools, prompts, resources, plugins, and safe configuration diagnostics. Optional surfaces are omitted when their prerequisites are unavailable. +These HTTP examples listen on the local machine. See [Integrations](usage/integrations.md) for client setup and the available interfaces. Restart a running service after changing its configuration or installed plugins. + +## Optional plugins and guidance + +Installed Groundworkers plugins can add tools, prompts, resources, and setup pages. Install them into the same Python environment as Groundworkers, then reopen the console or restart the service. Use `--describe` to check what was registered; plugins with readiness support also report configuration or dependency problems in the console. + +For prompt packs, see [Working with prompts](usage/prompt-handling.md). Bundled knowledge packs are available without a separate plugin; local knowledge-pack paths and the optional `semantic_projection_enabled` setting are described in [Configuration](usage/configuration.md). ## Configuration managed elsewhere @@ -125,10 +139,12 @@ groundworkers tui \ --config-read-only ``` -Change the authoritative configuration source, then reopen the console to verify the result. +Change the authoritative configuration source, then reopen the console to verify the result. This flag disables configuration edits; it does not make database preparation tasks read-only. + +## Maintenance runs -## Durable maintenance runs +Graph preparation and embedding population run in separate background processes. You can close and reopen the console while they continue. In **Runs**, follow progress, inspect a redacted log tail, cancel active work, retry a supported failed step, or export commands. Postflight results appear for tasks that define those checks. -Graph and embedding preparation tasks appear in **Runs**. From there you can follow progress, inspect a safe log tail, cancel an active task, retry a supported failure, or export commands. +Run history is saved on disk. Groundworkers uses `GROUNDWORKERS_STATE_HOME`, then `XDG_STATE_HOME`, as the state base directory and appends `groundworkers`. If neither is set, it uses `~/.local/state/groundworkers`. -Set `GROUNDWORKERS_STATE_HOME` to a persistent directory when running in a container. This keeps maintenance state across process restarts. The fallback is `$XDG_STATE_HOME/groundworkers` or the platform state directory. +In a container, set the state base to a mounted persistent directory to retain history across container replacement. An interrupted worker does not resume merely because its history survives; inspect its status in Runs before retrying. diff --git a/docs/index.md b/docs/index.md index ff32109..196ab79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,10 @@ Start with [Concepts and capability choices](concepts.md) for the mental model, then choose an integration path. +For agent-assisted tasks, [Working with prompts](usage/prompt-handling.md) explains how workflow guidance, user questions, domain knowledge, and tools fit together, and what to review before relying on a result. + +[Plugins](plugins.md) explains how specialist packages extend Groundworkers, the interface they implement, and where to find the plugin catalogue and examples. + ## At a glance ```mermaid diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..6d1b0f8 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,95 @@ +# Plugins + +Plugins add specialist capabilities to a Groundworkers installation. A plugin can expose tools, prompts, and resources through the same MCP server as the core vocabulary and mapping tools. It can also provide configuration pages, readiness checks, and preparation tasks in the setup console. + +Start with the [Groundworkers Plugin Catalogue](https://github.com/AustralianCancerDataNetwork/groundworkers-plugin-catalogue) to find components, installation instructions, and compatibility information. Access to the catalogue or individual component repositories may be required. Each component's own guide explains its data requirements and intended use. + +## What gets installed + +The ecosystem includes both executable plugins and reusable prompt content: + +| Component | How it fits | +|---|---| +| Runtime plugin | A Python package that Groundworkers discovers and asks to register its MCP capabilities. | +| Installed prompt pack | A content distribution read by the OHDSI Prompt Registry plugin. It supplies instructions, schemas, or examples. | +| Filesystem prompt pack | Content read by the registry from a configured directory, often for local guidance or a working copy. | + +For example, installing the Prompt Registry adds tools for finding and reading packs. Adding a pack supplies content to those tools. A pack does not need to implement the Groundworkers plugin interface itself. + +The catalogue describes what you can install. Your Python environment determines what Groundworkers can discover. Configuration and available dependencies determine which capabilities can be built and used. + +## How a plugin joins the host + +A plugin package advertises a constructed plugin object through the `groundworkers.plugins` Python entry-point group. Groundworkers discovers that object at startup, supplies its configuration and shared resources, and asks it to build its state. It then registers the plugin's MCP tools, prompts, or resources against that state. + +```mermaid +flowchart TD + PACKAGE["Installed plugin package"] --> BUILD["Build plugin state"] + HOST["Groundworkers resources
and plugin settings"] --> BUILD + BUILD --> MCP["Register tools, prompts and resources
on the shared MCP server"] +``` + +The plugin's service contains its own behaviour. Groundworkers provides discovery, shared runtime access, MCP registration, and common setup features. Registering MCP capabilities does not automatically create REST endpoints; REST exposes a separate set of workflows. + +## The interface contract + +Every runtime plugin implements four parts: + +| Part | Responsibility | +|---|---| +| `name` | A stable identity, unique among installed plugins. | +| `config_cls` | The plugin's configuration schema, or `None` when no settings are needed. Its `tool_name` must match the plugin name. | +| `build(context, config)` | Use shared resources and validated plugin settings to create the state needed by the plugin. Return `None` if a required prerequisite is missing. | +| `register(server, state)` | Expose MCP tools, prompts, or resources using the state returned by `build`. | + +The supplied `PluginContext` gives a plugin access to resolved CDM resources, the routed CDM session factory, and optional embedding, chat, and mapping capabilities. A plugin can reuse the host's grounding and mapping through `mapping_context`. Routed CDM sessions support installations where vocabulary tables use a different connection from clinical tables. + +Plugins can also declare references to independent databases, models, or vector stores. Their settings belong in `[tools.]`; connection details and credentials stay in the shared resource entries they reference. See [Configuration](usage/configuration.md#plugin-configuration) for how these settings fit into the stack. + +A plugin can optionally add: + +- **Readiness checks** to distinguish valid settings from usable data and services. +- **Setup steps** for explicit preparation work, with progress and logs in Runs. +- **A custom configuration flow** when its settings need more than the standard generated form. + +An individual plugin can be skipped when its configuration, prerequisites, build, or registration fail, while core capabilities remain available. Plugin identities must still be unique: duplicate names are a startup error. MCP tool names also share a namespace, so plugins should prefix their tool names. + +For method signatures, entry-point declarations, and authoring details, see the [plugin development guide](development/plugins.md). + +## Two examples + +### OHDSI Prompt Registry: exposing reusable guidance + +The [OHDSI Prompt Registry](https://AustralianCancerDataNetwork.github.io/ohdsi-prompt-registry/) is a runtime plugin that publishes prompt packs through MCP. Its tools let an agent discover packs, read selected instructions or schemas, validate structured documents, and render supported results for review. + +The registry can combine bundled content, separately installed pack distributions, and local filesystem packs. This lets a team share a workflow while keeping site-specific content separately maintained. Your agent conducts the conversation and applies the guidance; the registry supplies content and contract checks. + +See [Working with prompts](usage/prompt-handling.md) for the user-facing model and the registry's [pack-source guide](https://AustralianCancerDataNetwork.github.io/ohdsi-prompt-registry/sources/) for configuration. + +### Plugin template: a small service with tools and setup + +The [Groundworkers Plugin Template](https://github.com/AustralianCancerDataNetwork/gw-plugin-template) provides a small catalogue service with list, search, and status tools. It also demonstrates a preparation step and a readiness report. It is a starting point for authors to adapt. + +Its layout shows where the responsibilities belong: + +| Module | Purpose | +|---|---| +| `config.py` | Declare settings and references to shared resources. | +| `adapters.py` | Read files, query databases, or call external services. | +| `service.py` | Implement behaviour that can also be called directly from Python. | +| `tools.py` | Expose that behaviour through MCP. | +| `plugin.py` | Build the components and register the plugin's capabilities. | + +Longer preparation work lives separately in `setup_cli.py` and can run through the host's maintenance system. A specialised mapper can follow the same structure, reusing Groundworkers' mapping context while keeping source-specific rules and data in its own package. + +## Add a plugin to your installation + +1. **Check compatibility.** Use the catalogue to find the component's requirements, installation command, and recorded test evidence. A declared version range and a successful compatibility test are different kinds of evidence. +2. **Install and configure it.** Use the same Python environment that runs Groundworkers. Follow the component's guide for settings and any data preparation, then restart the host. +3. **Inspect the result.** Check registration and use the plugin's readiness checks, where available, to verify its dependencies and prepared data. + +```bash +groundworkers --config-path /path/to/config.toml --describe +``` + +The output lists active plugins and registered capabilities, with safe issue summaries for plugins that could not be built or registered. A registered tool may still need a working connection or prepared data before a call succeeds. diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md index 8d6a0dc..fe7b28e 100644 --- a/docs/usage/configuration.md +++ b/docs/usage/configuration.md @@ -1,41 +1,46 @@ # Configuration -`groundworkers` uses the shared OMOP stack configuration managed by `oa-configurator`. `build_app_config(...)` resolves that shared stack into a runtime `AppConfig`; callers do not provide a separate `groundworkers`-specific runtime file. +Groundworkers uses the shared OMOP stack configuration managed by `oa-configurator`. The file holds database connections, models, and other shared resources. The `[tools.groundworkers]` section selects which entries Groundworkers uses and sets its own defaults. + +For a guided setup, start with [Initial local setup](../from-scratch.md). This page explains the file layout and individual settings. ## Runtime entrypoints -Use these helpers depending on how much control you need: +Open the setup console to create or edit a configuration: -```python -from groundworkers.bootstrap import build_app_config, build_app_config_from_stack +```bash +groundworkers tui ``` -- `build_app_config()` loads the active shared stack config from disk -- `build_app_config(config_path=...)` loads an explicit shared-stack file -- `build_app_config_from_stack(stack)` is useful in tests and programmatic tooling +Install the console with `pip install "groundworkers[tui]"`. The console edits configuration and runs checks; it does not start an MCP or REST service. -Then construct the application container: +### Choose the configuration file -```python -from groundworkers.app import build_application -from groundworkers.bootstrap import build_app_config +The CLI selects a file in this order: -config = build_app_config() -app = build_application(config) +1. `--config-path /path/to/config.toml`. +2. `OA_CONFIG_PATH` supplied by your shell or client environment. +3. `OA_CONFIG_PATH` saved in `~/.config/omop/groundworkers.env`. +4. The default, `~/.config/omop/config.toml`. + +The console can remember a selected location in `groundworkers.env`; that file records the location of the TOML file, not a second set of runtime settings. An unusable environment path is reported so you can correct it. Use an explicit path when you need to be sure which configuration is selected: + +```bash +groundworkers tui --config-path /path/to/config.toml +groundworkers --config-path /path/to/config.toml --describe ``` -The CLI entry point also accepts transport overrides and a flag for the interactive setup console: +For a configuration managed by a deployment or copied from elsewhere, use: ```bash -groundworkers --transport streamable-http --host 127.0.0.1 --port 8000 -groundworkers --tui +groundworkers tui --config-path /path/to/config.toml --config-read-only ``` -`--tui` launches the setup console and exits instead of starting an MCP or REST server. Install the optional TUI dependencies with `uv sync --extra tui` or `pip install groundworkers[tui]`. +This disables configuration edits in the console. Database preparation and embedding population are separate operations that can still write to their target stores. Edit the authoritative configuration source, then reopen the console to check it. Restart a running service after changing its configuration. ## Configuration ownership -`groundworkers` does not own every setting it consumes. The shared stack keeps package ownership explicit: +Shared resources have their own entries so several packages can use the same connection or model: | Concern | Where it lives | |---|---| @@ -46,11 +51,11 @@ groundworkers --tui | Vector-store backend and its database | `[vector_stores.*]` | | Which of those entries Groundworkers uses, plus MCP/REST defaults, grounding policy, source-planning, knowledge-pack, and semantic-projection settings | `[tools.groundworkers]` | -Groundworkers references stack entries **by name** and resolves them through `oa-configurator`. It does not read any other package's configuration section. +Groundworkers references these entries **by name** and resolves them through `oa-configurator`. Its core runtime reads `[tools.groundworkers]` and the shared entries it names. Maintenance commands and installed plugins use their own package sections, as described below. ## Typical TOML shape -This is the steady-state structure to expect in `config.toml`: +This example includes a CDM connection, PostgreSQL embeddings, and an Ollama chat model. Replace the connection details and choose models available from your provider. The model names and grounding threshold are examples, not required defaults. ```toml [connections.cdm_main] @@ -58,7 +63,7 @@ dialect = "postgresql+psycopg" host = "localhost" port = 5432 user = "omop" -password = "…" +password = "replace-me" database_name = "omop" [databases.cdm_db] @@ -125,11 +130,16 @@ cdm_db = "cdm_db" Settings are flat, grouped by name prefix. For example, `omop-config configure groundworkers --grounding-max-depth 6` updates that field without changing its neighbours. -A CDM-only stack is valid: omit `embedding_model_name` and `vector_store_name` and every lexical feature still works. See [Runtime combinations](#what-becomes-available-at-runtime). +For a CDM-only setup, keep the connection, CDM database, and `[tools.groundworkers]` entries. Omit the model and vector-store references and their unused definitions. Lexical operations remain available, although some searches need prepared indexes. See [Runtime combinations](#what-becomes-available-at-runtime). -When both embedding references are configured, the setup console also writes `[tools.omop_emb]` with the same `cdm_db`, model, and vector-store names. `oa-configurator` gives each package a typed tool section; the database, model, provider, and store definitions remain single shared entries. This binding is required because managed population runs execute the `omop-emb` CLI. The setup change is planned and saved atomically with `[tools.groundworkers]`. +The setup console also configures the companion commands it uses for maintenance: -CDM setup similarly writes `[tools.omop_alchemy]` and `[tools.omop_graph]` with the same `cdm_db` reference. Groundworkers launches those packages' schema, full-text, index, and relationship-classification maintenance CLIs, which resolve their own typed package sections. Groundworkers' runtime graph adapter continues to receive its engine and optional embedding dependencies directly; it does not read omop-graph's traversal or embedding configuration. +| Package section | Used for | +|---|---| +| `[tools.omop_emb]` | Embedding population, using the same CDM, model, and vector-store references. Written when both embedding references are configured. | +| `[tools.omop_alchemy]` and `[tools.omop_graph]` | CDM schema, search-index, and graph preparation, using the same `cdm_db` reference. | + +These sections point to the shared resources; they do not duplicate connection details or credentials. If you edit the file by hand, keep the references aligned before running maintenance. The core Groundworkers runtime does not take its graph or embedding settings from those companion sections. ### Reference fields @@ -140,9 +150,34 @@ CDM setup similarly writes `[tools.omop_alchemy]` and `[tools.omop_graph]` with | `llm_model_name` | a `[models.*]` entry | no | | `vector_store_name` | a `[vector_stores.*]` entry | no | -`embedding_model_name` is the model used for concept-document embeddings and compatible query encoding; `llm_model_name` is the chat model. Both name `[models.*]` entries. They may share a provider or model entry when that entry declares the required capabilities, may point at different providers, or one may be unset. There is no separate on/off flag: chat is available exactly when `llm_model_name` resolves. +`cdm_db` defaults to the name `cdm_db`, which must resolve to a CDM database entry. The other references are optional. + +`embedding_model_name` selects the model for concept embeddings and live query encoding; `llm_model_name` selects the chat model. They can use different providers, and either can be left unset. They can share a model entry if it supports both capabilities. Chat tools are registered when `llm_model_name` resolves; use the console checks to verify that the provider and model actually work. + +### A separate vocabulary connection + +If the vocabulary tables live in another database, define its connection and set `vocab_connection` on the CDM database entry: + +```toml +[connections.vocab_main] +dialect = "postgresql+psycopg" +host = "localhost" +port = 5432 +user = "omop" +password = "replace-me" +database_name = "vocabulary" + +[databases.cdm_db] +kind = "cdm" +connection = "cdm_main" +cdm_schema = "public" +vocab_connection = "vocab_main" +vocab_schema = "public" +``` + +Vocabulary reads use `vocab_connection` and `vocab_schema`. If you omit `vocab_connection`, they use the main CDM connection. Different schemas on the same connection need only separate `cdm_schema` and `vocab_schema` values. -## groundworkers-owned fields +## Groundworkers settings ### `app_name` @@ -178,7 +213,7 @@ REST is selected explicitly with `--transport rest`; it is not started alongside ### `llm_model_name` and chat -The chat model used by `TextService`, `DomainService`, and LLM-assisted source planning is a named `[models.*]` entry, exactly like the embedding model. Its endpoint and credentials live on the `[providers.*]` entry that model references, so they are stored, redacted, and reused the same way as every other provider in the stack: +Text preparation, domain classification, and assisted source planning use the model named by `llm_model_name`. Its provider entry holds the endpoint and credentials: ```toml [providers.local_ollama] @@ -195,11 +230,13 @@ structured_output = true llm_model_name = "chat_model" ``` -`structured_output = true` declares that the model can honour the JSON-mode request `complete_structured` makes; omop-llm treats every capability as opt-in. Chat is configured through the setup console's Chat section, using the same configuration workflow as the other setup sections. +Set `structured_output = true` only for a model that supports structured responses. Model capabilities are explicit declarations, not tests of the provider. Configure the provider and select the model in **Chat Model**, then use **Chat** to check chat completion and structured output. + +For an embedding model, declare `embeddings = true`. Its dimensions and any `document_prefix` or `query_prefix` must match the model and stored vectors. The embedding setup flow helps select these settings; **Check model** tests provider encoding and checks it against the store registration. ### Grounding (`grounding_*`) -Groundworkers-owned grounding policy. omop-graph's traversal limits are per-call arguments, not shared configuration, so they do not appear here. +These settings control Groundworkers' grounding policy. Other graph traversal limits are supplied with individual tool calls. | Field | Type | Default | |---|---|---| @@ -222,7 +259,7 @@ Grounding results carry strict OMOP flags: Grounding can legitimately land on a classification concept (an ATC or CPT4 hierarchy node). Those are valid hierarchy positions but **not** valid mapping targets for a CDM entity field, so check the flags rather than assuming every result is standard. -`concept_ground` also accepts `standard_only` and `active_only` (both default `false`). They narrow *candidate resolution*, not the returned results. +`concept_ground` also accepts `standard_only` and `active_only` for each call; both default to `false`. They filter the candidates considered during resolution. In this resolver, `standard_only` admits both `S` and `C` candidates, so it does not guarantee that every returned concept has `standard_concept = true`. ### Source planning (`source_planning_*`) @@ -230,7 +267,7 @@ Grounding can legitimately land on a classification concept (an ATC or CPT4 hier |---|---|---| | `source_planning_llm_assisted_enabled` | `bool` | `true` | -This controls whether `build_application(...)` wires the assisted classifier into `SourcePlanningService` when an LLM adapter is present. +When enabled and a chat model is configured, assisted source planning can use that model to classify ambiguous columns. Turning this off leaves deterministic source planning available. It does not disable other chat-backed tools. ### Knowledge packs (`knowledge_*`) @@ -240,18 +277,23 @@ This controls whether `build_application(...)` wires the assisted classifier int `groundworkers` includes bundled baseline knowledge packs as part of the package. Set `knowledge_packs_root` when you want to add site-specific or localisation packs on top of that baseline. -The configured directory should contain a `packs/` tree grouped by knowledge layer, for example: +Point `knowledge_packs_root` directly to the directory containing the layer directories: + +```toml +[tools.groundworkers] +knowledge_packs_root = "./my-knowledge/packs" +``` ```text my-knowledge/ - packs/ + packs/ # knowledge_packs_root points here localisation/ nacc-uds-v4/ manifest.yaml guidance.md ``` -If a configured pack has the same `layer` and `name` as a bundled baseline pack, the configured copy wins. +Relative paths resolve against the TOML file's directory; `~` is expanded. A configured pack with the same `layer` and `name` replaces the bundled copy. Other bundled packs remain available. These knowledge packs are separate from prompt workflows supplied by plugins; see [Working with prompts](prompt-handling.md). @@ -261,7 +303,7 @@ If a configured pack has the same `layer` and `name` as a bundled baseline pack, |---|---|---| | `semantic_projection_enabled` | `bool` | `false` | -Gates the `semantic_project` MCP tool and its backing `SemanticProjectionService`. +Enable this to expose the `semantic_project` MCP tool. Projection uses a grounded concept and source context to produce CDM rows; it needs no embedding or chat model. ```toml [tools.groundworkers] @@ -270,73 +312,59 @@ semantic_projection_enabled = true ## What becomes available at runtime -### With `cdm_db` alone +Configuration determines which tools are registered. Registration does not establish that a database, model endpoint, or store is ready: check connections, model compatibility, and coverage separately in the console. -Graph availability follows the resolved CDM database, so a CDM-only stack gets: +### With `cdm_db` alone -- `CDMAdapter` -- `OmopGraphAdapter` (lexical only) -- `VocabService`, `GraphService`, `ConceptGroundingService`, `MappingService` -- concept, resolver, search, mapping, source-planning, knowledge, and system MCP tools +Concept lookup, lexical search and grounding, graph and mapping tools, deterministic source planning, knowledge packs, and system tools are available. Some search and graph operations need prepared indexes or supporting objects. No separate `[tools.omop_graph]` section is needed by the core runtime. -Embedding status reports as unconfigured. No `[tools.omop_graph]` section is required or read. +### With `vector_store_name` alone -### With `embedding_model_name` **and** `vector_store_name` +Stored-vector operations remain available without a live embedding model. For example, `embedding_neighbours` can find neighbours of a concept already in the store. Supply `model_name` when needed to select the stored model. -You additionally get: +Live text encoding, text-based embedding search, and the graph's embedding grounding tier need `embedding_model_name` as well. -- `OmopEmbAdapter` -- embedding MCP tools -- embedding-backed channels in `MappingService` -- the embedding grounding tier +### With `embedding_model_name` **and** `vector_store_name` -Both references are required. With only one configured, status reports an actionable incomplete-configuration message and embedding features stay off rather than guessing a default. +Both references enable live embedding search and embedding-backed grounding and mapping. The provider must be reachable and compatible vectors must already exist in the store. With a model reference alone, embedding store operations remain unavailable. -The store must be populated before the embedding tier can return anything. Population is explicit (see the Embeddings setup section) and never starts at query time. The server holds the graph read-only (`write=False`), so Groundworkers encodes query text for the embedding tier but never writes vectors. +Groundworkers opens the store read-only for queries, status, and coverage checks. Store initialization and population are explicit write operations. Searches never populate missing vectors. See [Initial local setup](../from-scratch.md#5-add-embeddings-optional) for the preparation steps. -Groundworkers opens vector stores with omop-emb's read-only reader for MCP tools, status, coverage, and setup diagnostics. Explicit store initialization uses omop-emb's writer. A database supports one vector store per model: reuse the existing store or choose another database when registering a model that is already present. On PostgreSQL, the wizard stores vectors in the dedicated `groundworkers` schema. omop-emb also creates `omop_emb_registry`, and oa-configurator may create `oa_configurator_provenance`. SQLite uses its database file without a schema. +The setup wizard uses the `groundworkers` schema for PostgreSQL vector storage. The model registry and configuration provenance use their packages' reserved schemas. SQLite uses its database file without a schema. Each database supports one store per model: reuse an existing registration or choose a different database for another store of that model. -If the store predates omop-emb 3.0, Groundworkers reports `LEGACY_REGISTRY`. The migration script is tracked in oa-configurator issue #40; once it ships, Groundworkers will show omop-emb's migration command. Groundworkers does not migrate legacy stores. +A `LEGACY_REGISTRY` error means the store layout is incompatible. Groundworkers does not migrate legacy stores automatically; inspect the diagnostic and use the migration guidance for your installed storage package. Avoid treating a registry error as an empty store. ### With `groundworkers.llm_model_name` configured -You additionally get: - -- `LLMAdapter` -- `TextService` -- `DomainService` -- text and domain MCP tools -- LLM-assisted source planning when `source_planning_llm_assisted_enabled = true` +Text preparation and domain classification tools are registered. Assisted source planning is also available when `source_planning_llm_assisted_enabled = true`. The embedding model is independent of this choice. ### With `semantic_projection_enabled = true` -You additionally get: +The `semantic_project` MCP tool is registered. Projection is deterministic and uses no model. See [Semantic projection](../tools/semantic_projection.md) for its inputs and results. -- `SemanticProjectionService` (constructed directly, not part of `app.services` — it needs no adapter) -- the `semantic_project` MCP tool +## Plugin configuration -## CLI selection rules +Installed plugins can have their own `[tools.]` settings and references to shared resources. They may provide console configuration pages or additional setup steps. Install plugins in the Groundworkers environment, follow their configuration guide, and restart the host after installation or configuration changes. -By default: +Use `groundworkers --describe` to check which plugins and tools were registered. Plugins that provide readiness checks can report missing configuration or dependencies in the console. Plugin settings belong to the plugin; they are not additional fields in `[tools.groundworkers]`. -- `groundworkers` loads the stack config file -- MCP startup uses the `mcp` defaults unless overridden on the CLI +## CLI selection rules -Available runtime selectors: +MCP uses the configured `mcp_*` defaults unless you override them on the command line. REST is selected explicitly and uses the `rest_*` defaults. `--host` and `--port` override the bind address for the chosen HTTP transport. ```bash groundworkers --config-path /path/to/config.toml --describe -groundworkers --transport streamable-http --host 0.0.0.0 --port 8000 -groundworkers --transport rest --host 0.0.0.0 --port 8080 +groundworkers --transport streamable-http --host 127.0.0.1 --port 8000 +groundworkers --transport rest --host 127.0.0.1 --port 8080 ``` -Equivalent environment override: +Use separate TOML files for separate environments, selected with `--config-path` or `OA_CONFIG_PATH`. See [Integrations](integrations.md) for client examples. -- `OA_CONFIG_PATH` +## Direct Python example -There is no Groundworkers profile selector. Keep separate stack files when you need separate environments and select one with `--config-path` or `OA_CONFIG_PATH`. +`build_app_config()` loads the active shared stack. Supply `config_path` to select a file explicitly, or use `build_app_config_from_stack(stack)` when your application already has a `StackConfig` object. -## Direct Python example +The Python helpers do not load `groundworkers.env`; pass `config_path` explicitly if you rely on a location remembered by the console. ```python from groundworkers.app import build_application @@ -349,6 +377,6 @@ grounding = app.services.grounding mapping = app.services.mapping ``` -Service attributes are `None` only when their prerequisites are not available in the resolved runtime. +Build the application once and reuse it. Optional services such as `app.services.text` are `None` when they are not configured. A service object being present does not prove its external dependencies are reachable. `config.describe()` returns a redacted summary keyed by `database`, `model`, `llm_model`, `vector_store`, `stack`, `groundworkers`, and `app_name`. CDM schema keys are `cdm_schema` and `vocab_schema`; generic/vector-store databases continue to use `schema_name`. Passwords and API keys are masked; safe URLs never carry credentials. diff --git a/docs/usage/prompt-handling.md b/docs/usage/prompt-handling.md new file mode 100644 index 0000000..a9307d3 --- /dev/null +++ b/docs/usage/prompt-handling.md @@ -0,0 +1,79 @@ +# Working with prompts + +Tell the agent what you want to achieve, what you already know, and what is still undecided. Choose a prompt workflow that fits the task and ask for more guidance when needed. + +Prompt packs contain reusable instructions for an agent. They can help you develop a study question, review source fields, or make a mapping decision. Groundworkers supplies the tools and guidance; your agent or application runs the conversation and keeps track of decisions. If you need to resume later, ask it to save those decisions and the draft. + +## How the parts fit together + +| Part | Question it addresses | Example | +|---|---|---| +| Workflow guidance | What needs to happen next? | Establish the study question, gather missing parameters, and prepare a draft for review. | +| User questions (elicitation) | What does the user mean or want to decide? | Ask which comparison you intend and clarify your answer. | +| Domain knowledge | What rules and evidence apply here? | Apply the relevant OMOP mapping conventions and source-specific context. | +| Tools | What can be looked up or checked? | Retrieve concept candidates or check a structured document. | + +You can use these parts together or separately. An agent can clarify a comparison on its own, or ask the same question as part of a study-design workflow. + +Once the agent understands your answer, the workflow still needs to check that it meets the task's requirements before continuing. + +## Start with a goal and relevant context + +1. **Describe the outcome.** Say whether you want an explanation, a draft study question, mapping recommendations, or a document for review. +2. **Supply what you already know.** Include relevant source information, constraints, and decisions you have made. Identify anything still undecided. +3. **Choose a pack.** Use your client's prompt list, or ask the agent to find a pack that fits the task. +4. **Review the result.** Check that it reflects your intent, look at the supporting evidence, and note anything still uncertain. + +For example: + +> Help me turn this study goal into a research question. Use the relevant study +> workflow, reuse the decisions I have already supplied, and ask one focused +> question at a time when required information is missing. Keep undecided +> parameters unresolved and show me the draft for review. + +The packs you can use depend on your installation and client. Knowledge packs supply guidance such as mapping rules; workflow packs help organise the task. Finding a knowledge pack does not start a workflow. See the [knowledge tools](../tools/knowledge.md) for finding and reading knowledge packs. Workflow packs are available through the optional [OHDSI Prompt Registry](https://AustralianCancerDataNetwork.github.io/ohdsi-prompt-registry/). + +## Keep track of answers and open questions + +A useful question says what needs deciding and gives you enough context to answer. The differences between options should be clear. If none fits, you should be able to explain what you mean. + +Your client may show clickable options or a numbered question in chat. In either case, the agent should record your response and wait for an answer before doing work that requires it. A default selection, silence, or closing a dialog is not an answer. + +| Your response | What the agent should preserve | +|---|---| +| An answer | Your intended choice or explanation, with clarification where needed. | +| “I will decide later” | A deferred decision, with dependent work still pending where the answer is required. | +| “I do not want to answer” | A declined question; the workflow determines whether it can continue without that information. | +| An unclear response or no answer | An open question, with no answer assumed. | + +Work can often continue while a question is open. An optional preference can wait; a required study parameter may hold up the final result while still allowing a draft. + +You can change an earlier answer. Ask the agent to check which decisions and drafts need updating. For example, changing the comparison may affect the research question while leaving unrelated work intact. + +## Ask for more explanation when you need it + +The main instructions should contain the rules needed for routine work. Separate references can explain unfamiliar concepts, give more examples, or cover unusual cases. + +The agent can start with those instructions and read more about a particular issue when needed. Required rules belong in the main instructions, so optional background can be left unread without missing a requirement. + +You can ask: + +> Explain why this question is needed and show one relevant example. Use the +> supporting guidance for this issue rather than loading the whole reference +> collection. + +Separate files help only if the client reads them as needed. Everything the agent reads takes up space in its working context, so ask for the explanation relevant to your question. + +## Save results for review and reuse + +Use the conversation to agree what the result means. A structured document makes it easier to save, check, and pass to another tool or team. Ask for a readable summary so you can review it without working directly with JSON. + +Passing validation means the document meets the structural checks applied to it. You still need to review its clinical or scientific content and decide whether to approve it. Keep missing decisions visible in the draft; the agent should not fill them with assumptions to make the document look complete. + +## Tell the agent how you prefer to work + +You can ask for one question at a time, short or detailed explanations, a preferred language, or a summary before the evidence. For batch review, ask the agent to collect open questions in a review document so it need not interrupt for every row. + +These preferences do not change the domain rules. Local or source-specific guidance applies to its intended setting. If guidance conflicts, ask the agent to explain the conflict and what evidence would help resolve it. + +Your agent or application controls how questions appear, whether answers are saved, and whether required steps are enforced. Prompt instructions alone cannot guarantee that behaviour. Before relying on a result, ask the agent to show which guidance it used, what you decided, and what is still open. diff --git a/mkdocs.yaml b/mkdocs.yaml index 9cb25bd..4336479 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -31,6 +31,8 @@ nav: - Home: index.md - Understand Groundworkers: - Concepts and capability choices: concepts.md + - Working with prompts: usage/prompt-handling.md + - Plugins: plugins.md - Getting Started: - Installation: usage/installation.md - Initial local setup: from-scratch.md From 68b159d19b946309a104d9c3fe9cbeec1944eef3 Mon Sep 17 00:00:00 2001 From: Georgie Kennedy Date: Wed, 21 Oct 2026 11:40:55 -0400 Subject: [PATCH 3/4] adding plugin linting --- docs/development/plugins.md | 16 +++ src/groundworkers/plugins.py | 116 ++++++++++++++++++++- src/groundworkers/server.py | 92 ++++++++++++++++- tests/unit/test_plugin_lint_cli.py | 160 +++++++++++++++++++++++++++++ tests/unit/test_plugins.py | 81 +++++++++++++++ 5 files changed, 460 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_plugin_lint_cli.py diff --git a/docs/development/plugins.md b/docs/development/plugins.md index b61db1f..53d015d 100644 --- a/docs/development/plugins.md +++ b/docs/development/plugins.md @@ -208,6 +208,22 @@ Verification is read-only and returns a `PluginReadinessResult` containing a sta The contract is headless: it contains no Groundskeeping types. Groundworkers uses the same result for generic setup-console rendering, while a plugin may return `result.as_dict()` from its own MCP status tool. Plugins own dependency- specific checks; the host only builds current plugin state, handles unavailable configuration safely, and maps readiness states to existing Groundskeeping status, table, empty, action, and detail contracts. +## Configuration linting + +A plugin may implement the runtime-checkable `GroundworkersPluginLinter` protocol when its settings need checks beyond the configuration schema. The hook is `lint_config(config) -> PluginLintReport`; it receives the plugin's validated `PackageConfigBase` instance, or `None` when the plugin has no config class. It runs without calling `build()`, so it can check configuration-dependent files or cross-field rules without constructing runtime services. + +Return a report with the plugin name and zero or more `PluginLintDiagnostic` values. Each finding has a stable code, `error` / `warning` / `info` severity, concise message, and optional configuration path. Errors make `report.valid` false; warnings and informational findings do not. Keep diagnostics safe for display: do not include secret values, full connection strings, or raw exception text. + +Headless consumers call `lint_plugin_config(plugin, config)`. It returns `None` when the optional protocol is not implemented. If a linter raises or returns a malformed report, Groundworkers logs a safe error summary and returns a generic error finding. + +Run the installed plugin checks from the command line: + +```bash +groundworkers lint-plugins --config-path /path/to/config.toml +``` + +The command prints a JSON report and exits with status 1 if any linter reports an error. Plugins without the optional hook appear under `skipped`; only plugins with the hook have their settings passed through their package schema and linter. It does not build the Groundworkers runtime or connect to OMOP services. This command covers plugin configuration only; prompt packs and Groundworkers knowledge packs should be validated by their owning subsystems. + ## Lifecycle and diagnostics diff --git a/src/groundworkers/plugins.py b/src/groundworkers/plugins.py index 49d2fc3..907e834 100644 --- a/src/groundworkers/plugins.py +++ b/src/groundworkers/plugins.py @@ -15,7 +15,7 @@ import logging from collections.abc import Callable, Mapping -from dataclasses import dataclass +from dataclasses import asdict, dataclass from enum import StrEnum from importlib.metadata import entry_points from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable @@ -117,6 +117,51 @@ def as_dict(self) -> dict[str, object]: } +class PluginLintSeverity(StrEnum): + """Severity levels for plugin configuration lint findings.""" + + ERROR = "error" + WARNING = "warning" + INFO = "info" + + +@dataclass(frozen=True) +class PluginLintDiagnostic: + """One actionable, safe-to-display configuration finding.""" + + code: str + severity: PluginLintSeverity + message: str + path: str | None = None + + def as_dict(self) -> dict[str, str | None]: + result = asdict(self) + result["severity"] = self.severity.value + return result + + +@dataclass(frozen=True) +class PluginLintReport: + """Structured result shared by plugin authors and host front ends.""" + + plugin: str + diagnostics: tuple[PluginLintDiagnostic, ...] = () + + @property + def valid(self) -> bool: + return all( + item.severity is not PluginLintSeverity.ERROR + for item in self.diagnostics + ) + + def as_dict(self) -> dict[str, object]: + return { + "plugin": self.plugin, + "valid": self.valid, + "diagnostics": [item.as_dict() for item in self.diagnostics], + } + + @dataclass(frozen=True) class PluginSetupArgument: """One argument rendered by Groundworkers' generic setup wizard. @@ -342,6 +387,75 @@ def verify_readiness(self, state: object) -> PluginReadinessResult: ... +@runtime_checkable +class GroundworkersPluginLinter(Protocol): + """Optional, side-effect-free validation of plugin configuration.""" + + def lint_config( + self, config: PackageConfigBase | None + ) -> PluginLintReport: + """Return findings without building plugin runtime state.""" + + ... + + +def lint_plugin_config( + plugin: GroundworkersPlugin, + config: PackageConfigBase | None, +) -> PluginLintReport | None: + """Call a plugin's optional linter; return ``None`` when unsupported.""" + + if not isinstance(plugin, GroundworkersPluginLinter): + return None + try: + report = plugin.lint_config(config) + except Exception as exc: + logger.error( + "Groundworkers plugin %s config lint failed (%s).", + plugin.name, + type(exc).__name__, + ) + return PluginLintReport( + plugin=plugin.name, + diagnostics=( + PluginLintDiagnostic( + code="plugin_linter.failed", + severity=PluginLintSeverity.ERROR, + message="Plugin configuration linting failed; review host logs.", + ), + ), + ) + valid_report = ( + isinstance(report, PluginLintReport) + and report.plugin == plugin.name + and isinstance(report.diagnostics, tuple) + and all( + isinstance(item, PluginLintDiagnostic) + and bool(item.code) + and isinstance(item.severity, PluginLintSeverity) + and bool(item.message) + and (item.path is None or isinstance(item.path, str)) + for item in report.diagnostics + ) + ) + if not valid_report: + logger.error( + "Groundworkers plugin %s returned an invalid config lint report.", + plugin.name, + ) + return PluginLintReport( + plugin=plugin.name, + diagnostics=( + PluginLintDiagnostic( + code="plugin_linter.invalid_report", + severity=PluginLintSeverity.ERROR, + message="Plugin returned an invalid configuration lint report.", + ), + ), + ) + return report + + def discover_plugins() -> list[GroundworkersPlugin]: """Discover installed plugins via the `groundworkers.plugins` entry-point group. diff --git a/src/groundworkers/server.py b/src/groundworkers/server.py index e5e3851..539576a 100644 --- a/src/groundworkers/server.py +++ b/src/groundworkers/server.py @@ -7,6 +7,8 @@ import argparse import json import logging +import os +from pathlib import Path from typing import Literal, cast, get_args from groundworkers._env import ( @@ -20,7 +22,7 @@ # keeping ordinary ``import groundworkers`` side-effect free. load_environment() -from oa_configurator import ConfigurationError +from oa_configurator import ConfigurationError, load_stack_config from groundworkers.app import GroundworkersApp, build_application from groundworkers.base.errors import scrub_error_message @@ -116,7 +118,9 @@ def create_server( def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Run the groundworkers MCP server") + parser = argparse.ArgumentParser( + description="Run Groundworkers or check installed plugin configuration." + ) parser.add_argument( "--config-path", help="Path to the shared OMOP stack config TOML. Defaults to OA_CONFIG_PATH or ~/.config/omop/config.toml.", @@ -157,8 +161,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument( "command", nargs="?", - choices=["serve", "tui"], - help="Optional command. Use 'tui' for the setup TUI.", + choices=["serve", "tui", "lint-plugins"], + help="Optional command: 'tui' opens setup; 'lint-plugins' checks plugin settings.", ) return parser.parse_args(argv) @@ -182,8 +186,13 @@ def main(argv: list[str] | None = None) -> None: else: _launch_groundworkers_tui(config_path=args.config_path) return + if args.command == "lint-plugins": + _lint_plugins(args.config_path) + return if args.config_path is None: _require_usable_config_path() + else: + _set_explicit_config_path(args.config_path) try: config = build_app_config(config_path=args.config_path) except (ConfigurationError, FileNotFoundError, OSError, TypeError, ValueError) as exc: @@ -228,6 +237,81 @@ def main(argv: list[str] | None = None) -> None: server.run(transport=cast(MCPTransport, transport), host=host, port=port) +def _lint_plugins(config_path: str | None) -> None: + """Run installed plugin linters without building runtime services.""" + + from groundworkers.plugins import ( + GroundworkersPluginLinter, + PluginLintDiagnostic, + PluginLintReport, + PluginLintSeverity, + discover_plugins, + lint_plugin_config, + ) + + if config_path is None: + _require_usable_config_path() + else: + _set_explicit_config_path(config_path) + try: + stack = load_stack_config(config_path) + except (ConfigurationError, FileNotFoundError, OSError, TypeError, ValueError) as exc: + raise SystemExit( + _configuration_failure(exc, config_path=config_path) + ) from None + + reports: list[PluginLintReport] = [] + skipped: list[str] = [] + for plugin in discover_plugins(): + if not isinstance(plugin, GroundworkersPluginLinter): + skipped.append(plugin.name) + continue + + config = None + if plugin.config_cls is not None: + try: + config = plugin.config_cls.validate_candidate(stack) + except ConfigurationError: + reports.append( + PluginLintReport( + plugin=plugin.name, + diagnostics=( + PluginLintDiagnostic( + code="plugin.config_invalid", + severity=PluginLintSeverity.ERROR, + message=( + "Plugin configuration is invalid; review the " + "corresponding tools section in the stack config." + ), + path=f"tools.{plugin.name}", + ), + ), + ) + ) + continue + + report = lint_plugin_config(plugin, config) + if report is not None: + reports.append(report) + + valid = all(report.valid for report in reports) + result = { + "valid": valid, + "checked": len(reports), + "skipped": sorted(skipped), + "reports": [report.as_dict() for report in reports], + } + print(json.dumps(result, indent=2)) + if not valid: + raise SystemExit(1) + + +def _set_explicit_config_path(config_path: str) -> None: + """Make relative plugin-owned paths resolve beside the selected stack file.""" + + os.environ[ENV_CONFIG_PATH] = str(Path(config_path).expanduser().resolve()) + + def _require_usable_config_path() -> None: """Refuse to serve from a fallback the operator did not ask for. diff --git a/tests/unit/test_plugin_lint_cli.py b/tests/unit/test_plugin_lint_cli.py new file mode 100644 index 0000000..b583208 --- /dev/null +++ b/tests/unit/test_plugin_lint_cli.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest +from oa_configurator import ConfigurationError, PackageConfigBase + +from groundworkers.plugins import ( + PluginLintDiagnostic, + PluginLintReport, + PluginLintSeverity, +) +from groundworkers.server import main, parse_args + + +@pytest.fixture(autouse=True) +def _isolate_config_env(monkeypatch: pytest.MonkeyPatch) -> None: + """``--config-path`` exports OA_CONFIG_PATH; restore it after each test. + + ``delenv`` alone records nothing when the variable is unset, so ``setenv`` + first makes monkeypatch restore the original state on teardown. + """ + + monkeypatch.setenv("OA_CONFIG_PATH", "") + monkeypatch.delenv("OA_CONFIG_PATH") + + +class LinterPlugin: + name = "lintable" + config_cls = None + + def lint_config(self, config: PackageConfigBase | None) -> PluginLintReport: + assert config is None + return PluginLintReport( + plugin=self.name, + diagnostics=( + PluginLintDiagnostic( + "example.warning", + PluginLintSeverity.WARNING, + "An example warning.", + "tools.lintable.setting", + ), + ), + ) + + +class NonLinterPlugin: + name = "legacy" + + +class InvalidPluginConfig: + @classmethod + def validate_candidate(cls, stack: object) -> PackageConfigBase: + raise ConfigurationError("secret-value-must-not-be-printed") + + +class InvalidConfigLinter(LinterPlugin): + name = "invalid_config" + config_cls = InvalidPluginConfig + + +class RelativeCatalogueLinter(LinterPlugin): + name = "relative_catalogue" + + def lint_config(self, config: PackageConfigBase | None) -> PluginLintReport: + assert config is None + selected_config = Path(os.environ["OA_CONFIG_PATH"]) + assert (selected_config.parent / "catalogue.json").is_file() + return PluginLintReport(plugin=self.name) + + +def test_parse_args_accepts_plugin_lint_command() -> None: + args = parse_args(["lint-plugins", "--config-path", "/tmp/stack.toml"]) + + assert args.command == "lint-plugins" + assert args.config_path == "/tmp/stack.toml" + + +def test_lint_plugins_reports_results_without_building_runtime( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr("groundworkers.server.load_stack_config", lambda _path: object()) + monkeypatch.setattr( + "groundworkers.server.build_app_config", + lambda **_kwargs: pytest.fail("linting must not build runtime services"), + ) + monkeypatch.setattr( + "groundworkers.plugins.discover_plugins", + lambda: [LinterPlugin(), NonLinterPlugin()], + ) + + main(["lint-plugins", "--config-path", "/tmp/stack.toml"]) + + result = json.loads(capsys.readouterr().out) + assert result["valid"] is True + assert result["checked"] == 1 + assert result["skipped"] == ["legacy"] + assert result["reports"][0]["plugin"] == "lintable" + + +def test_lint_plugins_reports_invalid_config_without_leaking_error_text( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr("groundworkers.server.load_stack_config", lambda _path: object()) + monkeypatch.setattr( + "groundworkers.plugins.discover_plugins", lambda: [InvalidConfigLinter()] + ) + + with pytest.raises(SystemExit) as exit_info: + main(["lint-plugins", "--config-path", "/tmp/stack.toml"]) + + output = capsys.readouterr().out + result = json.loads(output) + assert exit_info.value.code == 1 + assert result["valid"] is False + assert result["reports"][0]["diagnostics"][0]["code"] == "plugin.config_invalid" + assert "secret-value" not in output + + +def test_lint_plugins_resolves_relative_files_from_selected_config( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + config_dir = tmp_path / "config" + work_dir = tmp_path / "elsewhere" + config_dir.mkdir() + work_dir.mkdir() + config_file = config_dir / "stack.toml" + config_file.touch() + (config_dir / "catalogue.json").write_text("[]", encoding="utf-8") + monkeypatch.chdir(work_dir) + monkeypatch.setattr("groundworkers.server.load_stack_config", lambda _path: object()) + monkeypatch.setattr( + "groundworkers.plugins.discover_plugins", lambda: [RelativeCatalogueLinter()] + ) + + main(["lint-plugins", "--config-path", str(config_file)]) + + result = json.loads(capsys.readouterr().out) + assert result["valid"] is True + + +def test_serve_sets_explicit_config_before_building_app_config( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + config_file = tmp_path / "stack.toml" + config_file.touch() + + def check_selected_config(*, config_path: str | None) -> object: + assert config_path == str(config_file) + assert os.environ["OA_CONFIG_PATH"] == str(config_file.resolve()) + raise ConfigurationError("stop after checking config selection") + + monkeypatch.setattr("groundworkers.server.build_app_config", check_selected_config) + + with pytest.raises(SystemExit): + main(["--config-path", str(config_file), "serve"]) diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index f506829..793c4da 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -26,11 +26,16 @@ from groundworkers.base.errors import GroundworkersError from groundworkers.bootstrap import build_app_config_from_stack from groundworkers.plugins import ( + GroundworkersPluginLinter, PluginContext, + PluginLintDiagnostic, + PluginLintReport, + PluginLintSeverity, PluginReadinessField, PluginReadinessResult, PluginReadinessState, discover_plugins, + lint_plugin_config, validate_plugin_identities, ) from groundworkers.server import create_server @@ -83,6 +88,37 @@ def verify_readiness(self, state: object) -> PluginReadinessResult: ) +class FakeLinterPlugin(FakePlugin): + def lint_config(self, config: PackageConfigBase | None) -> PluginLintReport: + assert isinstance(config, FakePluginConfig) + return PluginLintReport( + plugin=self.name, + diagnostics=( + PluginLintDiagnostic( + "fake.setting.warning", + PluginLintSeverity.WARNING, + "The setting is unusual.", + "tools.fake_plugin.greeting", + ), + ), + ) + + +class FailingLinterPlugin(FakeLinterPlugin): + def lint_config(self, config: PackageConfigBase | None) -> PluginLintReport: + raise RuntimeError("secret-token-must-not-be-logged") + + +class InvalidReportLinterPlugin(FakeLinterPlugin): + def lint_config(self, config: PackageConfigBase | None) -> PluginLintReport: + return PluginLintReport(plugin="another_plugin") + + +class MalformedReportLinterPlugin(FakeLinterPlugin): + def lint_config(self, config: PackageConfigBase | None) -> PluginLintReport: + return PluginLintReport(plugin=self.name, diagnostics=("invalid",)) + + def _embedding_stack(tmp_path: Path, extra: dict | None = None): stack = build_embedding_stack() # Redirect the sqlite-backed vector store off the real filesystem/cwd, @@ -273,6 +309,51 @@ def test_optional_readiness_contract_is_headless_and_transport_stable( assert PluginReadinessResult.__module__ == "groundworkers.plugins" +def test_optional_plugin_linter_is_discovered_and_returns_stable_report() -> None: + plugin = FakeLinterPlugin() + + report = lint_plugin_config(plugin, FakePluginConfig()) + + assert isinstance(plugin, GroundworkersPluginLinter) + assert report is not None + assert report.as_dict() == { + "plugin": "fake_plugin", + "valid": True, + "diagnostics": [ + { + "code": "fake.setting.warning", + "severity": "warning", + "message": "The setting is unusual.", + "path": "tools.fake_plugin.greeting", + } + ], + } + + +def test_linter_dispatch_skips_plugins_without_the_optional_protocol() -> None: + assert lint_plugin_config(FakePlugin(), FakePluginConfig()) is None + + +@pytest.mark.parametrize( + ("plugin", "code"), + [ + (FailingLinterPlugin(), "plugin_linter.failed"), + (InvalidReportLinterPlugin(), "plugin_linter.invalid_report"), + (MalformedReportLinterPlugin(), "plugin_linter.invalid_report"), + ], +) +def test_linter_dispatch_returns_safe_error_for_broken_plugin( + plugin: FakePlugin, code: str, caplog: pytest.LogCaptureFixture +) -> None: + report = lint_plugin_config(plugin, FakePluginConfig()) + + assert report is not None + assert not report.valid + assert report.diagnostics[0].code == code + assert "secret-token" not in report.diagnostics[0].message + assert "secret-token" not in caplog.text + + def test_readiness_reports_unconfigured_when_plugin_cannot_build() -> None: report = verify_plugin_readiness( build_app_config_from_stack(build_cdm_stack()), From fafdaf298849ee4ffecd4fa42d155eff785ba74e Mon Sep 17 00:00:00 2001 From: Georgie Kennedy Date: Wed, 21 Oct 2026 11:41:56 -0400 Subject: [PATCH 4/4] dependabot --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 98f6d0c..511f2ee 100644 --- a/uv.lock +++ b/uv.lock @@ -2148,7 +2148,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.29.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2166,9 +2166,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511, upload-time = "2026-09-07T14:34:15.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581, upload-time = "2026-09-07T14:34:14.266Z" }, ] [package.optional-dependencies]