dhti-cli 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  - 🚀 *Dhanvantari rose out of the water with his four hands, holding a pot full of elixirs!*
15
15
 
16
- #### TL; DR: 🏥 DHTI enables rapid prototyping, sharing, and testing of GenAI applications within an Electronic Health Record (EHR), facilitating the seamless transition of your experiments to clinical practice.
16
+ ### TL; DR: 🏥 DHTI enables rapid prototyping, sharing, and testing of GenAI healthcare applications within an EHR, facilitating the seamless transition of your experiments to practice!
17
17
  👉 [Try it out today!](#try-it-out) and give us a star ⭐️ if you like it!
18
18
 
19
19
  ### About
@@ -62,8 +62,6 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
62
62
  <img src="https://github.com/dermatologist/dhti/blob/develop/notes/arch-1.drawio.svg" />
63
63
  </p>
64
64
 
65
- 🔥 **Coming soon!:** We are currently working on expanding the DHTI architecture to support traditional machine learning models, such as *EEG sleep stage classification* and *trichogram analysis*, exposing inference pipelines as agentic tools!
66
-
67
65
  ## ✨ Features
68
66
  * **Modular**: Supports installable Gen AI routines and UI elements.
69
67
  * **Quick prototyping**: CLI helps in quick prototyping and testing of Gen AI routines and UI elements.
@@ -71,7 +69,7 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
71
69
  * **Developer friendly**: Copy working files to running containers for testing.
72
70
  * **Dry-run mode**: Preview changes before execution with the `--dry-run` flag.
73
71
  * **Dependency Injection**: Dependency injection for models and hyperparameters for configuring elixirs.
74
- * **Generate synthetic data**: DHTI supports generating synthetic data for testing.
72
+ * **Generate synthetic data**: [DHTI supports generating synthetic data for testing, using synthea.](/notes/SYNTHEA.md)
75
73
  * **CQL support**: [CQL for clinical decision support](https://nuchange.ca/2025/06/v-llm-in-the-loop-cql-execution-with-unstructured-data-and-fhir-terminology-support.html).
76
74
  * **FHIR**: Data exchange with FHIR schema.
77
75
  * **MCP**: Built in MCP server for pluggable tools.
@@ -81,6 +79,15 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
81
79
  * **Graph utilities**: Neo4j for graph utilities.
82
80
  * **LLM**: Ollama for self-hosting LLM models.
83
81
 
82
+ ## ✨ New
83
+ * **MCPX integration**: DHTI now includes an [MCP integrator](https://docs.lunar.dev/mcpx/) that allows other MCP servers to be "installed" and exposed seamlessly to DHTI through the MCPX gateway.
84
+ * **DOCKTOR module**: A new module, [DOCKTOR](/notes/DOCKTOR.md), support traditional machine learning model packaged as Docker containers, to be used as MCP tools, enabling the deployment of inference pipelines as agent-invokable tools. (in beta)
85
+ * **MCP aware agent**: [dhti-elixir-template](https://github.com/dermatologist/dhti-elixir-template) used in the examples now includes an [MCP aware agent](https://github.com/dermatologist/dhti-elixir-template/blob/feature/agent-2/src/dhti_elixir_template/chain.py) that can autodiscover and invoke tools from the MCPX gateway. Install it using `npx dhti-cli elixir install -g https://github.com/dermatologist/dhti-elixir-template.git -n dhti-elixir-template -b feature/agent2`.
86
+ * **Medplum integration**: [Medplum](https://www.medplum.com/) is now supported as an alternative FHIR server. Read more [here](/notes/medplum.md). This allows you to add FHIR subscriptions for real-time updates and much more.
87
+ * **Synthea integration**: You can now generate synthetic FHIR data using [Synthea](https://synthetichealth.github.io/synthea/). Read more [here](/notes/SYNTHEA.md).
88
+ * **MIMIC support**: You can now load [MIMIC Demo](https://physionet.org/content/mimic-iv-demo/2.2/) data using DHTI in [one command](https://nuchange.ca/2024/11/loading-mimic-dataset-onto-a-fhir-server-in-two-easy-steps.html).
89
+
90
+
84
91
  ## 🔧 For Gen AI Developers
85
92
 
86
93
  *Developers can build elixirs and conchs for DHTI.*
@@ -124,6 +131,7 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
124
131
  ## :sparkles: Resources (in Alpha)
125
132
  * [cookiecutter for scaffolding elixirs](https://github.com/dermatologist/cookiecutter-uv)
126
133
  * [cds-hooks-sandbox for testing](https://github.com/dermatologist/cds-hooks-sandbox/tree/dhti-1)
134
+ * [Medplum integration](/notes/medplum.md)
127
135
 
128
136
  ## :sunglasses: Coming soon
129
137
 
@@ -59,15 +59,17 @@ export default class Compose extends Command {
59
59
  const mcpFhir = ['mcp-fhir', 'fhir', 'postgres-db'];
60
60
  const mcpx = ['mcpx'];
61
61
  const docktor = ['mcpx'];
62
+ const medplum = ['medplum-server', 'medplum-app', 'postgres-db', 'redis', 'mpclient'];
62
63
  const _modules = {
63
64
  cqlFhir,
65
+ docktor,
64
66
  fhir,
65
67
  gateway,
66
68
  langfuse,
67
69
  langserve,
68
70
  mcpFhir,
69
71
  mcpx,
70
- docktor,
72
+ medplum,
71
73
  neo4j,
72
74
  ollama,
73
75
  openmrs,
@@ -13,6 +13,6 @@ export default class Docktor extends Command {
13
13
  'model-path': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
14
  workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
15
  };
16
- private restartMcpxContainer;
17
16
  run(): Promise<void>;
17
+ private restartMcpxContainer;
18
18
  }
@@ -22,8 +22,8 @@ export default class Docktor extends Command {
22
22
  }),
23
23
  environment: Flags.string({
24
24
  char: 'e',
25
- multiple: true,
26
25
  description: 'Environment variables to pass to docker (format: VAR=value)',
26
+ multiple: true,
27
27
  }),
28
28
  image: Flags.string({ char: 'i', description: 'Docker image for the inference pipeline (required for install)' }),
29
29
  'model-path': Flags.string({
@@ -37,18 +37,6 @@ export default class Docktor extends Command {
37
37
  description: 'Working directory for MCPX config',
38
38
  }),
39
39
  };
40
- async restartMcpxContainer(mcpxConfigPath, containerName) {
41
- try {
42
- const { execSync } = await import('node:child_process');
43
- execSync(`docker cp ${mcpxConfigPath} ${containerName}:/lunar/packages/mcpx-server/`);
44
- this.log(chalk.green('Copied mcp.json to container: /lunar/packages/mcpx-server/config/mcp.json'));
45
- execSync(`docker restart ${containerName}`);
46
- this.log(chalk.green(`Restarted ${containerName} container.`));
47
- }
48
- catch (err) {
49
- this.log(chalk.red(`Failed to copy config or restart container '${containerName}'. Please check Docker status and container name.`));
50
- }
51
- }
52
40
  async run() {
53
41
  const { args, flags } = await this.parse(Docktor);
54
42
  const mcpxConfigPath = path.join(flags.workdir, 'config');
@@ -61,83 +49,105 @@ export default class Docktor extends Command {
61
49
  if (!fs.existsSync(mcpJsonPath)) {
62
50
  fs.writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: {} }, null, 2));
63
51
  }
64
- let mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
52
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
65
53
  // Ensure mcpServers exists
66
54
  if (!mcpConfig.mcpServers) {
67
55
  mcpConfig.mcpServers = {};
68
56
  }
69
- if (args.op === 'install') {
70
- if (!args.name) {
71
- this.error('Name is required for install operation');
72
- }
73
- if (!flags.image) {
74
- this.error('Image is required for install operation');
75
- }
76
- const binds = [];
77
- const envVars = [];
78
- if (flags['model-path']) {
79
- const absModelPath = path.resolve(flags['model-path']);
80
- binds.push(`${absModelPath}:/model`);
57
+ switch (args.op) {
58
+ case 'install': {
59
+ if (!args.name) {
60
+ this.error('Name is required for install operation');
61
+ }
62
+ if (!flags.image) {
63
+ this.error('Image is required for install operation');
64
+ }
65
+ const binds = [];
66
+ const envVars = [];
67
+ if (flags['model-path']) {
68
+ const absModelPath = path.resolve(flags['model-path']);
69
+ binds.push(`${absModelPath}:/model`);
70
+ }
71
+ if (flags.environment && flags.environment.length > 0) {
72
+ const invalidEnvVars = flags.environment.filter((e) => {
73
+ const idx = e.indexOf('=');
74
+ return idx <= 0 || idx === e.length - 1;
75
+ });
76
+ if (invalidEnvVars.length > 0) {
77
+ this.error(`Invalid environment variable format. Expected 'NAME=value'. Invalid entries: ${invalidEnvVars.join(', ')}`);
78
+ }
79
+ envVars.push(...flags.environment);
80
+ }
81
+ // Add socket mounting for docker tools if needed, but primarily we want the container to run as a server
82
+ // MCPX handles the running of the docker container.
83
+ // We need to configure it in mcp.json so MCPX picks it up.
84
+ // Based on MCP std, docker servers are defined with `docker` command.
85
+ // Add (merge) new server into existing mcpServers
86
+ mcpConfig.mcpServers[args.name] = {
87
+ args: [
88
+ 'run',
89
+ '-i',
90
+ '--rm',
91
+ ...binds.flatMap((b) => ['-v', b]),
92
+ ...envVars.flatMap((e) => ['-e', e]),
93
+ flags.image,
94
+ ],
95
+ command: 'docker',
96
+ };
97
+ // Write back the updated config (preserving all other properties and existing servers)
98
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
99
+ this.log(chalk.green(`Inference pipeline '${args.name}' added`));
100
+ // Copy only mcp.json to container and restart (non-fatal if it fails)
101
+ try {
102
+ await this.restartMcpxContainer(mcpxConfigPath, flags.container);
103
+ }
104
+ catch {
105
+ this.log(chalk.yellow('Note: Could not restart container. Please restart manually if needed.'));
106
+ }
107
+ break;
81
108
  }
82
- if (flags.environment && flags.environment.length > 0) {
83
- const invalidEnvVars = flags.environment.filter((e) => {
84
- const idx = e.indexOf('=');
85
- return idx <= 0 || idx === e.length - 1;
86
- });
87
- if (invalidEnvVars.length > 0) {
88
- this.error(`Invalid environment variable format. Expected 'NAME=value'. Invalid entries: ${invalidEnvVars.join(', ')}`);
109
+ case 'remove': {
110
+ if (!args.name) {
111
+ this.error('Name is required for remove operation');
112
+ }
113
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers[args.name]) {
114
+ delete mcpConfig.mcpServers[args.name];
115
+ // Write back the updated config (preserving all other properties and remaining servers)
116
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
117
+ this.log(chalk.green(`Inference pipeline '${args.name}' removed`));
118
+ }
119
+ else {
120
+ this.log(chalk.yellow(`Inference pipeline '${args.name}' not found.`));
89
121
  }
90
- envVars.push(...flags.environment);
122
+ break;
91
123
  }
92
- // Add socket mounting for docker tools if needed, but primarily we want the container to run as a server
93
- // MCPX handles the running of the docker container.
94
- // We need to configure it in mcp.json so MCPX picks it up.
95
- // Based on MCP std, docker servers are defined with `docker` command.
96
- // Add (merge) new server into existing mcpServers
97
- mcpConfig.mcpServers[args.name] = {
98
- command: 'docker',
99
- args: [
100
- 'run',
101
- '-i',
102
- '--rm',
103
- ...binds.flatMap((b) => ['-v', b]),
104
- ...envVars.flatMap((e) => ['-e', e]),
105
- flags.image,
106
- ],
107
- };
108
- // Write back the updated config (preserving all other properties and existing servers)
109
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
110
- this.log(chalk.green(`Inference pipeline '${args.name}' added to MCPX config.`));
111
- // Copy only mcp.json to container and restart
112
- await this.restartMcpxContainer(mcpxConfigPath, flags.container);
113
- }
114
- else if (args.op === 'remove') {
115
- if (!args.name) {
116
- this.error('Name is required for remove operation');
124
+ case 'restart': {
125
+ await this.restartMcpxContainer(mcpxConfigPath, flags.container);
126
+ break;
117
127
  }
118
- if (mcpConfig.mcpServers && mcpConfig.mcpServers[args.name]) {
119
- delete mcpConfig.mcpServers[args.name];
120
- // Write back the updated config (preserving all other properties and remaining servers)
121
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
122
- this.log(chalk.green(`Inference pipeline '${args.name}' removed from MCPX config.`));
123
- this.log(chalk.yellow('Please restart the MCPX container to apply changes: dhti-cli docktor restart'));
128
+ case 'list': {
129
+ this.log(chalk.blue('Installed Inference Pipelines:'));
130
+ for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
131
+ const argsList = Array.isArray(config.args) ? config.args.join(' ') : '';
132
+ this.log(`- ${name}: ${argsList}`);
133
+ }
134
+ break;
124
135
  }
125
- else {
126
- this.log(chalk.yellow(`Inference pipeline '${args.name}' not found.`));
136
+ default: {
137
+ this.error(`Unknown operation: ${args.op}`);
127
138
  }
128
139
  }
129
- else if (args.op === 'restart') {
130
- await this.restartMcpxContainer(mcpxConfigPath, flags.container);
131
- }
132
- else if (args.op === 'list') {
133
- this.log(chalk.blue('Installed Inference Pipelines:'));
134
- for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
135
- const argsList = Array.isArray(config.args) ? config.args.join(' ') : '';
136
- this.log(`- ${name}: ${argsList}`);
137
- }
140
+ }
141
+ async restartMcpxContainer(mcpxConfigPath, containerName) {
142
+ try {
143
+ const { execSync } = await import('node:child_process');
144
+ execSync(`docker cp ${mcpxConfigPath} ${containerName}:/lunar/packages/mcpx-server/`);
145
+ this.log(chalk.green('Copied mcp.json to container: /lunar/packages/mcpx-server/config/mcp.json'));
146
+ execSync(`docker restart ${containerName}`);
147
+ this.log(chalk.green(`Restarted ${containerName} container.`));
138
148
  }
139
- else {
140
- this.error(`Unknown operation: ${args.op}`);
149
+ catch {
150
+ this.log(chalk.red(`Failed to copy config or restart container '${containerName}'. Please check Docker status and container name.`));
141
151
  }
142
152
  }
143
153
  }
@@ -7,6 +7,7 @@ export default class Mimic extends Command {
7
7
  static examples: string[];
8
8
  static flags: {
9
9
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  };
11
12
  run(): Promise<void>;
12
13
  }
@@ -11,9 +11,18 @@ export default class Mimic extends Command {
11
11
  default: false,
12
12
  description: 'Show what changes would be made without actually making them',
13
13
  }),
14
+ token: Flags.string({
15
+ char: 't',
16
+ description: 'Bearer token for authentication (optional)',
17
+ }),
14
18
  };
15
19
  async run() {
16
20
  const { args, flags } = await this.parse(Mimic);
21
+ // Ensure server URL ends with /$import
22
+ let serverUrl = args.server;
23
+ if (!serverUrl.endsWith('/$import')) {
24
+ serverUrl = serverUrl.replace(/\/$/, '') + '/$import';
25
+ }
17
26
  const mimic_request = `{
18
27
 
19
28
  "resourceType": "Parameters",
@@ -148,25 +157,34 @@ export default class Mimic extends Command {
148
157
 
149
158
  }`;
150
159
  if (flags['dry-run']) {
151
- console.log(chalk.yellow(`[DRY RUN] Would send POST request to: ${args.server}`));
160
+ console.log(chalk.yellow(`[DRY RUN] Would send POST request to: ${serverUrl}`));
152
161
  console.log(chalk.cyan('[DRY RUN] Request headers:'));
153
162
  console.log(chalk.green(' Content-Type: application/fhir+json'));
154
163
  console.log(chalk.green(' Prefer: respond-async'));
164
+ if (flags.token) {
165
+ console.log(chalk.green(' Authorization: Bearer <token>'));
166
+ }
155
167
  console.log(chalk.cyan('[DRY RUN] Request body:'));
156
168
  console.log(mimic_request);
157
169
  return;
158
170
  }
171
+ // Build request headers
172
+ const headers = {
173
+ 'Content-Type': 'application/fhir+json',
174
+ Prefer: 'respond-async',
175
+ };
176
+ if (flags.token) {
177
+ headers.Authorization = `Bearer ${flags.token}`;
178
+ }
159
179
  // send a POST request to the server with the mimic_request body
160
- const response = await fetch(args.server, {
180
+ const response = await fetch(serverUrl, {
161
181
  body: mimic_request,
162
- headers: {
163
- 'Content-Type': 'application/fhir+json',
164
- Prefer: 'respond-async',
165
- },
182
+ headers,
166
183
  method: 'POST',
167
184
  });
168
185
  if (!response.ok) {
169
186
  console.error(`Error: ${response.status} ${response.statusText}`);
187
+ this.exit(1);
170
188
  }
171
189
  }
172
190
  }
@@ -0,0 +1,73 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * Synthea command for managing synthetic FHIR data generation
4
+ *
5
+ * This command provides subcommands to:
6
+ * - install: Download and install Synthea JAR file
7
+ * - generate: Generate synthetic FHIR data using Synthea
8
+ * - upload: Upload generated FHIR resources to a FHIR server
9
+ * - delete: Clean up generated synthetic data
10
+ * - download: Download pre-generated Synthea datasets
11
+ */
12
+ export default class Synthea extends Command {
13
+ static args: {
14
+ subcommand: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
15
+ };
16
+ static description: string;
17
+ static examples: string[];
18
+ static flags: {
19
+ age: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ city: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ covid19: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ covid19_10k: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ covid19_csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ covid19_csv_10k: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ endpoint: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
+ gender: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ population: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
29
+ seed: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
+ state: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
+ synthea_sample_data_csv_latest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
32
+ synthea_sample_data_fhir_latest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
33
+ synthea_sample_data_fhir_stu3_latest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
34
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
35
+ workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
36
+ };
37
+ /**
38
+ * Main command execution
39
+ * Dispatches to appropriate subcommand handler
40
+ * @returns Promise that resolves when subcommand completes
41
+ */
42
+ run(): Promise<void>;
43
+ /**
44
+ * Delete synthetic data
45
+ * @param flags Command flags including workdir and dry-run
46
+ * @returns Promise that resolves when deletion completes
47
+ */
48
+ private delete;
49
+ /**
50
+ * Download pre-generated Synthea datasets
51
+ * @param flags Command flags including workdir, dataset selections, and dry-run
52
+ * @returns Promise that resolves when download completes
53
+ */
54
+ private download;
55
+ /**
56
+ * Generate synthetic FHIR data
57
+ * @param flags Command flags including population, state, city, gender, age, seed, workdir, and dry-run
58
+ * @returns Promise that resolves when generation completes
59
+ */
60
+ private generate;
61
+ /**
62
+ * Install Synthea JAR file
63
+ * @param flags Command flags including workdir and dry-run
64
+ * @returns Promise that resolves when installation completes
65
+ */
66
+ private install;
67
+ /**
68
+ * Upload FHIR resources to server
69
+ * @param flags Command flags including endpoint, token, workdir, and dry-run
70
+ * @returns Promise that resolves when upload completes
71
+ */
72
+ private upload;
73
+ }