dhti-cli 0.3.3 → 0.5.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 +18 -10
- package/dist/commands/compose.d.ts +1 -0
- package/dist/commands/compose.js +59 -13
- package/dist/commands/conch.d.ts +1 -0
- package/dist/commands/conch.js +75 -9
- package/dist/commands/docker.d.ts +2 -1
- package/dist/commands/docker.js +50 -10
- package/dist/commands/docktor.d.ts +18 -0
- package/dist/commands/docktor.js +143 -0
- package/dist/commands/elixir.d.ts +1 -0
- package/dist/commands/elixir.js +85 -26
- package/dist/commands/mimic.d.ts +3 -0
- package/dist/commands/mimic.js +17 -2
- package/dist/commands/synthetic.d.ts +1 -0
- package/dist/commands/synthetic.js +29 -2
- package/dist/resources/docker-compose-master.yml +21 -3
- package/oclif.manifest.json +129 -12
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ Generative AI features are built as [LangServe Apps](https://python.langchain.co
|
|
|
22
22
|
|
|
23
23
|
### Want to know more?
|
|
24
24
|
|
|
25
|
+
*Watch this demo video:*
|
|
26
|
+
[](https://www.youtube.com/watch?v=5jFFe3wqKM0)
|
|
27
|
+
|
|
25
28
|
Gen AI can transform medicine. But it needs a framework for collaborative research and practice. DHTI is a reference architecture and an implementation for such a framework that integrates an EMR ([OpenMRS](https://openmrs.org/)), :link: Gen AI application server ([LangServe](https://python.langchain.com/v0.2/docs/langserve/)), self-hosted LLMs for privacy ([Ollama](https://ollama.com/)), tools on [MCP server](https://github.com/dermatologist/fhir-mcp-server), vector store for RAG ([redis](https://redis.io/)), monitoring ([LangFuse](https://langfuse.com/)), 🔥 FHIR repository with [CQL](https://nuchange.ca/2025/06/v-llm-in-the-loop-cql-execution-with-unstructured-data-and-fhir-terminology-support.html) support ([HAPI](https://cloud.alphora.com/sandbox/r4/cqm/)) and graph utilities ([Neo4j](https://neo4j.com/)) in one docker-compose! DHTI is inspired by [Bahmni](https://www.bahmni.org/) and **aims to facilitate GenAI adoption and research in areas with low resources.** The MCP server hosts pluggable, agent-invokable tools (FHIR query, summarization, terminology lookup, custom analytics, etc.) that you can extend without modifying core services.
|
|
26
29
|
|
|
27
30
|
The essence of DHTI is *modularity* with an emphasis on *configuration!* It is non-opinionated on LLMs, hyperparameters and pretty much everything. DHTI supports installable Gen AI routines through [LangServe Apps](https://python.langchain.com/docs/langserve/) (which we call :curry: **elixir**) and installable UI elements through [OpenMRS O3](https://o3-docs.openmrs.org/) React container (which we call :shell: **conch**). 🔥 FHIR is used for backend and [CDS-Hooks](https://cds-hooks.org/) for frontend communication, decoupling conches from OpenMRS, making them potentially usable with any health information system. We have a [fork of the cds-hook sandbox](https://github.com/dermatologist/cds-hooks-sandbox/tree/dhti-1) for testing that uses the [order-select](https://cds-hooks.org/hooks/order-select/) hook, utilizing the contentString from the [FHIR CommunicationRequest](https://build.fhir.org/communicationrequest.html) within the [cds-hook context](https://cds-hooks.org/examples/) for user inputs (recommended).
|
|
@@ -31,7 +34,7 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
|
|
|
31
34
|
<img src="https://github.com/dermatologist/openmrs-esm-dhti-template/blob/develop/notes/conch.jpg" />
|
|
32
35
|
</p>
|
|
33
36
|
|
|
34
|
-
*[OpenMRS ESM DHTI template](https://github.com/dermatologist/openmrs-esm-dhti-template) + [DHTI elixir template](https://github.com/dermatologist/dhti-elixir-template) together forms a simple but functional EMR chatbot too!* 👉 [Try it out today!](#try-it-out)
|
|
37
|
+
*[OpenMRS ESM DHTI template](https://github.com/dermatologist/openmrs-esm-dhti-template) (frontend) + [DHTI elixir template](https://github.com/dermatologist/dhti-elixir-template) (backend) together forms a simple but functional EMR chatbot too!* 👉 [Try it out today!](#try-it-out)
|
|
35
38
|
|
|
36
39
|
<p align="center">
|
|
37
40
|
<img src="https://github.com/dermatologist/dhti/blob/develop/notes/cds-hook-sandbox.jpg" />
|
|
@@ -66,6 +69,7 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
|
|
|
66
69
|
* **Quick prototyping**: CLI helps in quick prototyping and testing of Gen AI routines and UI elements.
|
|
67
70
|
* **Easy to use**: Can be installed in a few minutes.
|
|
68
71
|
* **Developer friendly**: Copy working files to running containers for testing.
|
|
72
|
+
* **Dry-run mode**: Preview changes before execution with the `--dry-run` flag.
|
|
69
73
|
* **Dependency Injection**: Dependency injection for models and hyperparameters for configuring elixirs.
|
|
70
74
|
* **Generate synthetic data**: DHTI supports generating synthetic data for testing.
|
|
71
75
|
* **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).
|
|
@@ -105,7 +109,7 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
|
|
|
105
109
|
* **EMR**: Built-in EMR, OpenMRS, for patient records.
|
|
106
110
|
* 👉 [Try it out today!](#try-it-out)
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
*Join us to make the Gen AI equitable and help doctors save lives!*
|
|
109
113
|
|
|
110
114
|
## :sparkles: Resources
|
|
111
115
|
* [fhiry](https://github.com/dermatologist/fhiry): FHIR to pandas dataframe for data analytics, AI and ML!
|
|
@@ -113,8 +117,8 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
|
|
|
113
117
|
|
|
114
118
|
## :sparkles: Resources (in Beta)
|
|
115
119
|
* [dhti-elixir-base](https://github.com/dermatologist/dhti-elixir-base): Base classes for dhti-elixir
|
|
116
|
-
* [dhti-elixir-template](https://github.com/dermatologist/dhti-elixir-template): A template for creating new dhti-elixirs
|
|
117
|
-
* [openmrs-esm-dhti-template](https://github.com/dermatologist/openmrs-esm-dhti-template): A conch template for OpenMRS
|
|
120
|
+
* [dhti-elixir-template](https://github.com/dermatologist/dhti-elixir-template): A template for creating new dhti-elixirs & a **simple EMR chatbot backend**.
|
|
121
|
+
* [openmrs-esm-dhti-template](https://github.com/dermatologist/openmrs-esm-dhti-template): A conch template for OpenMRS & a **simple EMR chatbot frontend**.
|
|
118
122
|
* [fhir-mcp-server](https://github.com/dermatologist/fhir-mcp-server): A MCP server for hosting FHIR-compliant tools.
|
|
119
123
|
|
|
120
124
|
## :sparkles: Resources (in Alpha)
|
|
@@ -123,20 +127,20 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
|
|
|
123
127
|
|
|
124
128
|
## :sunglasses: Coming soon
|
|
125
129
|
|
|
126
|
-
* [dhti-elixir-fhire](https://github.com/dermatologist/dhti-elixir-fhire): An elixir for
|
|
127
|
-
* [dhti-elixir-fhirs](https://github.com/dermatologist/dhti-elixir-fhirs): An elixir for text to FHIR search query conversion.
|
|
130
|
+
* [dhti-elixir-fhire](https://github.com/dermatologist/dhti-elixir-fhire): An elixir for FHIR embeddings.
|
|
128
131
|
* [dhti-elixir-upload](https://github.com/dermatologist/dhti-elixir-upload-file): Upload documents to the vector store for clinical knowledgebase and clinical trial matching.
|
|
129
|
-
* [openmrs-esm-qa](https://github.com/dermatologist/openmrs-esm-genai): A sample conch for Q&A on patient records using the dhti-elixir-fhire elixir.
|
|
130
132
|
|
|
131
133
|
## Try it out
|
|
132
134
|
|
|
133
135
|
* You only need [Node.js](https://nodejs.org/) and [Docker](https://www.docker.com/) installed to run this project. Optionally, you can install [Python](https://www.python.org/) if you want to develop new elixirs. We use a fake LLM script for testing purposes, so you don't need an OpenAI key to run this project. It just says "Paris" or "I don't know" to any prompt. You can replace it with any internal or external LLM service later.
|
|
134
136
|
|
|
137
|
+
👉 **If you are in a hurry, just run `./demo.sh` from a terminal (Linux or MacOS) in the root folder to try out the demo.** Windows users can use WSL. You only need [Node.js](https://nodejs.org/) and [Docker](https://www.docker.com/). This script runs all the commands below. Once done, use `npx dhti-cli docker -d` to stop and delete all the docker containers.
|
|
138
|
+
|
|
135
139
|
* `npx dhti-cli help` to see all available commands.
|
|
136
140
|
|
|
137
141
|
* `npx dhti-cli compose add -m openmrs -m langserve` to add OpenMRS and Langserve elixirs to your docker-compose.yml at ~/dhti. Other available modules: `ollama, langfuse, cqlFhir, redis, neo4j and mcpFhir`. You can read the newly created docker-compose by: `npx dhti-cli compose read`
|
|
138
142
|
|
|
139
|
-
* `npx dhti-cli elixir install -g https://github.com/dermatologist/dhti-elixir-template.git -n dhti-elixir-template` to install a sample elixir from github. (
|
|
143
|
+
* `npx dhti-cli elixir install -g https://github.com/dermatologist/dhti-elixir-template.git -n dhti-elixir-template` to install a sample elixir from github. *(Optional)* You may configure the LLM and hyperparameters in `~/dhti/elixir/app/bootstrap.py`. You can install multiple elixirs.
|
|
140
144
|
|
|
141
145
|
* `npx dhti-cli docker -n yourdockerhandle/genai-test:1.0 -t elixir` to build a docker image for the elixir.
|
|
142
146
|
|
|
@@ -146,6 +150,10 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
|
|
|
146
150
|
|
|
147
151
|
* `npx dhti-cli docker -u` to start all the docker images in your docker-compose.yml.
|
|
148
152
|
|
|
153
|
+
* *(Optional)* **🔍 Dry-run mode**: Add the `--dry-run` flag to any command to preview what changes will be made without actually executing them. For example:
|
|
154
|
+
- `npx dhti-cli compose add -m langserve --dry-run` to preview modules that would be added
|
|
155
|
+
- `npx dhti-cli elixir install -n test-elixir --dry-run` to see what files would be created/modified
|
|
156
|
+
|
|
149
157
|
### :clap: Access the Conch in OpenMRS and test the integration
|
|
150
158
|
|
|
151
159
|
* Go to `http://localhost/openmrs/spa/home`
|
|
@@ -161,9 +169,9 @@ You will see the text above the textbox.
|
|
|
161
169
|
|
|
162
170
|
* `npx dhti-cli docker -d` to stop and delete all the docker containers.
|
|
163
171
|
|
|
164
|
-
Read
|
|
172
|
+
Read [](https://github.com/dermatologist/dhti/wiki) for more details.
|
|
165
173
|
|
|
166
|
-
|
|
174
|
+
## 👋 The demo uses mock LLM. 👉 [Check out how to add real LLMs and configure them.](https://github.com/dermatologist/dhti/wiki/Configuration)
|
|
167
175
|
|
|
168
176
|
:hugs: **Thank you for trying out DHTI!**
|
|
169
177
|
|
|
@@ -6,6 +6,7 @@ export default class Compose extends Command {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
module: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
};
|
package/dist/commands/compose.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import yaml from 'js-yaml';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import os from 'node:os';
|
|
@@ -11,6 +12,10 @@ export default class Compose extends Command {
|
|
|
11
12
|
static description = 'Generates a docker-compose.yml file from a list of modules';
|
|
12
13
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
13
14
|
static flags = {
|
|
15
|
+
'dry-run': Flags.boolean({
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'Show what changes would be made without actually making them',
|
|
18
|
+
}),
|
|
14
19
|
file: Flags.string({
|
|
15
20
|
char: 'f',
|
|
16
21
|
default: `${os.homedir()}/dhti/docker-compose.yml`,
|
|
@@ -19,7 +24,7 @@ export default class Compose extends Command {
|
|
|
19
24
|
// flag with a value (-n, --name=VALUE)
|
|
20
25
|
module: Flags.string({
|
|
21
26
|
char: 'm',
|
|
22
|
-
description: 'Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j and
|
|
27
|
+
description: 'Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j, mcpFhir, mcpx and docktor)',
|
|
23
28
|
multiple: true,
|
|
24
29
|
}),
|
|
25
30
|
};
|
|
@@ -52,6 +57,8 @@ export default class Compose extends Command {
|
|
|
52
57
|
const webui = ['ollama-webui'];
|
|
53
58
|
const fhir = ['fhir', 'postgres-db'];
|
|
54
59
|
const mcpFhir = ['mcp-fhir', 'fhir', 'postgres-db'];
|
|
60
|
+
const mcpx = ['mcpx'];
|
|
61
|
+
const docktor = ['mcpx'];
|
|
55
62
|
const _modules = {
|
|
56
63
|
cqlFhir,
|
|
57
64
|
fhir,
|
|
@@ -59,6 +66,8 @@ export default class Compose extends Command {
|
|
|
59
66
|
langfuse,
|
|
60
67
|
langserve,
|
|
61
68
|
mcpFhir,
|
|
69
|
+
mcpx,
|
|
70
|
+
docktor,
|
|
62
71
|
neo4j,
|
|
63
72
|
ollama,
|
|
64
73
|
openmrs,
|
|
@@ -71,6 +80,10 @@ export default class Compose extends Command {
|
|
|
71
80
|
if (fs.existsSync(flags.file)) {
|
|
72
81
|
existingData = yaml.load(fs.readFileSync(flags.file, 'utf8'));
|
|
73
82
|
}
|
|
83
|
+
else if (flags['dry-run']) {
|
|
84
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${os.homedir()}/dhti`));
|
|
85
|
+
console.log(chalk.yellow(`[DRY RUN] Would create file: ${flags.file}`));
|
|
86
|
+
}
|
|
74
87
|
else {
|
|
75
88
|
Compose.init(); // Create the file if it does not exist
|
|
76
89
|
}
|
|
@@ -81,8 +94,14 @@ export default class Compose extends Command {
|
|
|
81
94
|
}
|
|
82
95
|
// Delete flags.file if args.op is reset
|
|
83
96
|
if (args.op === 'reset') {
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
if (flags['dry-run']) {
|
|
98
|
+
console.log(chalk.yellow(`[DRY RUN] Would delete file: ${flags.file}`));
|
|
99
|
+
console.log(chalk.yellow(`[DRY RUN] Would recreate file: ${flags.file}`));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
fs.unlinkSync(flags.file);
|
|
103
|
+
Compose.init(); // Recreate the file
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
// if existing data is not null and arg is delete, remove the modules from the existing data
|
|
88
107
|
if (Object.keys(existingData.services).length > 0 && args.op === 'delete') {
|
|
@@ -91,9 +110,19 @@ export default class Compose extends Command {
|
|
|
91
110
|
for (const module of flags.module ?? []) {
|
|
92
111
|
modulesToDelete = modulesToDelete.concat(_modules[module]);
|
|
93
112
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
if (flags['dry-run']) {
|
|
114
|
+
console.log(chalk.yellow('[DRY RUN] Would delete the following modules:'));
|
|
115
|
+
for (const module of modulesToDelete ?? []) {
|
|
116
|
+
if (existingData.services[module]) {
|
|
117
|
+
console.log(chalk.cyan(` - ${module}`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
for (const module of modulesToDelete ?? []) {
|
|
123
|
+
if (existingData.services[module]) {
|
|
124
|
+
delete existingData.services[module];
|
|
125
|
+
}
|
|
97
126
|
}
|
|
98
127
|
}
|
|
99
128
|
}
|
|
@@ -104,18 +133,35 @@ export default class Compose extends Command {
|
|
|
104
133
|
for (const module of flags.module ?? []) {
|
|
105
134
|
modulesToAdd = modulesToAdd.concat(_modules[module]);
|
|
106
135
|
}
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
if (flags['dry-run']) {
|
|
137
|
+
console.log(chalk.yellow('[DRY RUN] Would add the following modules:'));
|
|
138
|
+
for (const module of modulesToAdd ?? []) {
|
|
139
|
+
console.log(chalk.cyan(` - ${module}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
for (const module of modulesToAdd ?? []) {
|
|
144
|
+
existingData.services[module] = masterData.services[module];
|
|
145
|
+
}
|
|
109
146
|
}
|
|
110
147
|
}
|
|
111
148
|
// Add all volumes from master data to existing data by default
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
if (!flags['dry-run']) {
|
|
150
|
+
existingData.volumes = {};
|
|
151
|
+
for (const key of Object.keys(masterData.volumes)) {
|
|
152
|
+
existingData.volumes[key] = masterData.volumes[key];
|
|
153
|
+
}
|
|
115
154
|
}
|
|
116
155
|
const toWrite = yaml.dump(existingData).replaceAll('null', '');
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
if (flags['dry-run']) {
|
|
157
|
+
console.log(chalk.yellow(`[DRY RUN] Would write to file: ${flags.file}`));
|
|
158
|
+
console.log(chalk.green('[DRY RUN] File content would be:'));
|
|
159
|
+
console.log(toWrite);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log('Writing file:', toWrite);
|
|
163
|
+
fs.writeFileSync(flags.file, toWrite, 'utf8');
|
|
164
|
+
}
|
|
119
165
|
}
|
|
120
166
|
catch (error) {
|
|
121
167
|
console.error(error);
|
package/dist/commands/conch.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export default class Conch extends Command {
|
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
dev: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
git: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
image: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
package/dist/commands/conch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { exec } from 'node:child_process';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import os from 'node:os';
|
|
@@ -18,6 +19,10 @@ export default class Conch extends Command {
|
|
|
18
19
|
description: 'Name of the container to copy the conch to while in dev mode',
|
|
19
20
|
}),
|
|
20
21
|
dev: Flags.string({ char: 'd', default: 'none', description: 'Dev folder to install' }),
|
|
22
|
+
'dry-run': Flags.boolean({
|
|
23
|
+
default: false,
|
|
24
|
+
description: 'Show what changes would be made without actually making them',
|
|
25
|
+
}),
|
|
21
26
|
git: Flags.string({ char: 'g', default: 'none', description: 'Github repository to install' }),
|
|
22
27
|
image: Flags.string({
|
|
23
28
|
char: 'i',
|
|
@@ -46,9 +51,17 @@ export default class Conch extends Command {
|
|
|
46
51
|
// docker cp ../../openmrs-esm-genai/dist/. dhti-frontend-1:/usr/share/nginx/html/openmrs-esm-genai-1.0.0
|
|
47
52
|
// docker restart dhti-frontend-1
|
|
48
53
|
if (args.op === 'dev') {
|
|
49
|
-
|
|
54
|
+
const buildCommand = `cd ${flags.dev} && yarn build && docker cp dist/. ${flags.container}:/usr/share/nginx/html/${flags.name}-${flags.repoVersion}`;
|
|
55
|
+
const restartCommand = `docker restart ${flags.container}`;
|
|
56
|
+
if (flags['dry-run']) {
|
|
57
|
+
console.log(chalk.yellow('[DRY RUN] Would execute commands:'));
|
|
58
|
+
console.log(chalk.cyan(` ${buildCommand}`));
|
|
59
|
+
console.log(chalk.cyan(` ${restartCommand}`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log(buildCommand);
|
|
50
63
|
try {
|
|
51
|
-
exec(
|
|
64
|
+
exec(buildCommand, (error, stdout, stderr) => {
|
|
52
65
|
if (error) {
|
|
53
66
|
console.error(`exec error: ${error}`);
|
|
54
67
|
return;
|
|
@@ -56,7 +69,7 @@ export default class Conch extends Command {
|
|
|
56
69
|
console.log(`stdout: ${stdout}`);
|
|
57
70
|
console.error(`stderr: ${stderr}`);
|
|
58
71
|
});
|
|
59
|
-
exec(
|
|
72
|
+
exec(restartCommand, (error, stdout, stderr) => {
|
|
60
73
|
if (error) {
|
|
61
74
|
console.error(`exec error: ${error}`);
|
|
62
75
|
return;
|
|
@@ -68,15 +81,53 @@ export default class Conch extends Command {
|
|
|
68
81
|
catch (error) {
|
|
69
82
|
console.log('Error copying conch to container', error);
|
|
70
83
|
}
|
|
84
|
+
return;
|
|
71
85
|
}
|
|
72
86
|
// Create a directory to install the elixir
|
|
73
87
|
if (!fs.existsSync(`${flags.workdir}/conch`)) {
|
|
74
|
-
|
|
88
|
+
if (flags['dry-run']) {
|
|
89
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${flags.workdir}/conch`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
fs.mkdirSync(`${flags.workdir}/conch`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (flags['dry-run']) {
|
|
96
|
+
console.log(chalk.yellow(`[DRY RUN] Would copy resources from ${RESOURCES_DIR}/spa to ${flags.workdir}/conch`));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
fs.cpSync(path.join(RESOURCES_DIR, 'spa'), `${flags.workdir}/conch`, { recursive: true });
|
|
75
100
|
}
|
|
76
|
-
fs.cpSync(path.join(RESOURCES_DIR, 'spa'), `${flags.workdir}/conch`, { recursive: true });
|
|
77
101
|
// Rewrite files
|
|
78
102
|
const rewrite = () => {
|
|
79
103
|
flags.name = flags.name ?? 'openmrs-esm-genai';
|
|
104
|
+
if (flags['dry-run']) {
|
|
105
|
+
console.log(chalk.yellow('[DRY RUN] Would update configuration files:'));
|
|
106
|
+
console.log(chalk.cyan(` - ${flags.workdir}/conch/def/importmap.json`));
|
|
107
|
+
if (args.op === 'install') {
|
|
108
|
+
console.log(chalk.green(` Add import: ${flags.name.replace('openmrs-', '@openmrs/')} -> ./${flags.name}-${flags.repoVersion}/${flags.name}.js`));
|
|
109
|
+
}
|
|
110
|
+
if (args.op === 'uninstall') {
|
|
111
|
+
console.log(chalk.green(` Remove import: ${flags.name.replace('openmrs-', '@openmrs/')}`));
|
|
112
|
+
}
|
|
113
|
+
console.log(chalk.cyan(` - ${flags.workdir}/conch/def/spa-assemble-config.json`));
|
|
114
|
+
if (args.op === 'install') {
|
|
115
|
+
console.log(chalk.green(` Add module: ${flags.name.replace('openmrs-', '@openmrs/')} = ${flags.repoVersion}`));
|
|
116
|
+
}
|
|
117
|
+
if (args.op === 'uninstall') {
|
|
118
|
+
console.log(chalk.green(` Remove module: ${flags.name.replace('openmrs-', '@openmrs/')}`));
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.cyan(` - ${flags.workdir}/conch/Dockerfile`));
|
|
121
|
+
console.log(chalk.green(` Update with conch=${flags.name}, version=${flags.repoVersion}, image=${flags.image}`));
|
|
122
|
+
console.log(chalk.cyan(` - ${flags.workdir}/conch/def/routes.registry.json`));
|
|
123
|
+
if (args.op === 'install') {
|
|
124
|
+
console.log(chalk.green(` Add routes for ${flags.name.replace('openmrs-', '@openmrs/')}`));
|
|
125
|
+
}
|
|
126
|
+
if (args.op === 'uninstall') {
|
|
127
|
+
console.log(chalk.green(` Remove routes for ${flags.name.replace('openmrs-', '@openmrs/')}`));
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
80
131
|
// Read and process importmap.json
|
|
81
132
|
const importmap = JSON.parse(fs.readFileSync(`${flags.workdir}/conch/def/importmap.json`, 'utf8'));
|
|
82
133
|
if (args.op === 'install')
|
|
@@ -109,14 +160,23 @@ export default class Conch extends Command {
|
|
|
109
160
|
fs.writeFileSync(`${flags.workdir}/conch/def/routes.registry.json`, JSON.stringify(registry, null, 2));
|
|
110
161
|
};
|
|
111
162
|
if (flags.git !== 'none') {
|
|
163
|
+
const cloneCommand = `git clone ${flags.git} ${flags.workdir}/conch/${flags.name}`;
|
|
164
|
+
const checkoutCommand = `cd ${flags.workdir}/conch/${flags.name} && git checkout ${flags.branch}`;
|
|
165
|
+
if (flags['dry-run']) {
|
|
166
|
+
console.log(chalk.yellow('[DRY RUN] Would execute git commands:'));
|
|
167
|
+
console.log(chalk.cyan(` ${cloneCommand}`));
|
|
168
|
+
console.log(chalk.cyan(` ${checkoutCommand}`));
|
|
169
|
+
rewrite();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
112
172
|
// git clone the repository
|
|
113
|
-
exec(
|
|
173
|
+
exec(cloneCommand, (error, stdout, stderr) => {
|
|
114
174
|
if (error) {
|
|
115
175
|
console.error(`exec error: ${error}`);
|
|
116
176
|
return;
|
|
117
177
|
}
|
|
118
178
|
// Checkout the branch
|
|
119
|
-
exec(
|
|
179
|
+
exec(checkoutCommand, (error, stdout, stderr) => {
|
|
120
180
|
if (error) {
|
|
121
181
|
console.error(`exec error: ${error}`);
|
|
122
182
|
return;
|
|
@@ -131,8 +191,14 @@ export default class Conch extends Command {
|
|
|
131
191
|
}
|
|
132
192
|
// If flags.dev is not none, copy the dev folder to the conch directory
|
|
133
193
|
if (flags.dev !== 'none' && args.op !== 'dev') {
|
|
134
|
-
|
|
135
|
-
|
|
194
|
+
if (flags['dry-run']) {
|
|
195
|
+
console.log(chalk.yellow(`[DRY RUN] Would copy ${flags.dev} to ${flags.workdir}/conch/${flags.name}`));
|
|
196
|
+
rewrite();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
fs.cpSync(flags.dev, `${flags.workdir}/conch/${flags.name}`, { recursive: true });
|
|
200
|
+
rewrite();
|
|
201
|
+
}
|
|
136
202
|
}
|
|
137
203
|
}
|
|
138
204
|
}
|
|
@@ -6,9 +6,10 @@ export default class Docker extends Command {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
down: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
12
|
file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
-
container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
up: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
package/dist/commands/docker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import yaml from 'js-yaml';
|
|
3
4
|
import { exec } from 'node:child_process';
|
|
4
5
|
import fs from 'node:fs';
|
|
@@ -11,17 +12,21 @@ export default class Docker extends Command {
|
|
|
11
12
|
static description = 'Build a docker project and update docker-compose file';
|
|
12
13
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
13
14
|
static flags = {
|
|
15
|
+
container: Flags.string({
|
|
16
|
+
char: 'c',
|
|
17
|
+
default: 'dhti-langserve-1',
|
|
18
|
+
description: 'Name of the container to copy the bootstrap file to while in dev mode',
|
|
19
|
+
}),
|
|
14
20
|
down: Flags.boolean({ char: 'd', default: false, description: 'Run docker-compose down after building' }),
|
|
21
|
+
'dry-run': Flags.boolean({
|
|
22
|
+
default: false,
|
|
23
|
+
description: 'Show what changes would be made without actually making them',
|
|
24
|
+
}),
|
|
15
25
|
file: Flags.string({
|
|
16
26
|
char: 'f',
|
|
17
27
|
default: `${os.homedir()}/dhti/docker-compose.yml`,
|
|
18
28
|
description: 'Full path to the docker compose file to edit or run.',
|
|
19
29
|
}),
|
|
20
|
-
container: Flags.string({
|
|
21
|
-
char: 'c',
|
|
22
|
-
default: 'dhti-langserve-1',
|
|
23
|
-
description: 'Name of the container to copy the bootstrap file to while in dev mode',
|
|
24
|
-
}),
|
|
25
30
|
name: Flags.string({ char: 'n', description: 'Name of the container to build' }),
|
|
26
31
|
type: Flags.string({ char: 't', default: 'elixir', description: 'Type of the service (elixir/conch)' }),
|
|
27
32
|
up: Flags.boolean({ char: 'u', default: false, description: 'Run docker-compose up after building' }),
|
|
@@ -29,7 +34,13 @@ export default class Docker extends Command {
|
|
|
29
34
|
async run() {
|
|
30
35
|
const { args, flags } = await this.parse(Docker);
|
|
31
36
|
if (flags.up) {
|
|
32
|
-
|
|
37
|
+
const upCommand = `docker compose -f ${flags.file} up -d`;
|
|
38
|
+
if (flags['dry-run']) {
|
|
39
|
+
console.log(chalk.yellow('[DRY RUN] Would execute:'));
|
|
40
|
+
console.log(chalk.cyan(` ${upCommand}`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
exec(upCommand, (error, stdout, stderr) => {
|
|
33
44
|
if (error) {
|
|
34
45
|
console.error(`exec error: ${error}`);
|
|
35
46
|
return;
|
|
@@ -40,7 +51,13 @@ export default class Docker extends Command {
|
|
|
40
51
|
return;
|
|
41
52
|
}
|
|
42
53
|
if (flags.down) {
|
|
43
|
-
|
|
54
|
+
const downCommand = `docker compose -f ${flags.file} down`;
|
|
55
|
+
if (flags['dry-run']) {
|
|
56
|
+
console.log(chalk.yellow('[DRY RUN] Would execute:'));
|
|
57
|
+
console.log(chalk.cyan(` ${downCommand}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
exec(downCommand, (error, stdout, stderr) => {
|
|
44
61
|
if (error) {
|
|
45
62
|
console.error(`exec error: ${error}`);
|
|
46
63
|
return;
|
|
@@ -60,8 +77,16 @@ export default class Docker extends Command {
|
|
|
60
77
|
console.log('Please provide a valid path to bootstrap.py file');
|
|
61
78
|
this.exit(1);
|
|
62
79
|
}
|
|
80
|
+
const copyCommand = `docker cp ${flags.file} ${flags.container}:/app/app/bootstrap.py`;
|
|
81
|
+
const restartCommand = `docker restart ${flags.container}`;
|
|
82
|
+
if (flags['dry-run']) {
|
|
83
|
+
console.log(chalk.yellow('[DRY RUN] Would execute:'));
|
|
84
|
+
console.log(chalk.cyan(` ${copyCommand}`));
|
|
85
|
+
console.log(chalk.cyan(` ${restartCommand}`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
63
88
|
// copy -f to container:/app/app/ and only restart after copy completes
|
|
64
|
-
exec(
|
|
89
|
+
exec(copyCommand, (error, stdout, stderr) => {
|
|
65
90
|
if (error) {
|
|
66
91
|
console.error(`exec error: ${error}`);
|
|
67
92
|
return;
|
|
@@ -69,7 +94,7 @@ export default class Docker extends Command {
|
|
|
69
94
|
console.log(`stdout: ${stdout}`);
|
|
70
95
|
console.error(`stderr: ${stderr}`);
|
|
71
96
|
// restart the container only after copy completes
|
|
72
|
-
exec(
|
|
97
|
+
exec(restartCommand, (restartError, restartStdout, restartStderr) => {
|
|
73
98
|
if (restartError) {
|
|
74
99
|
console.error(`exec error: ${restartError}`);
|
|
75
100
|
return;
|
|
@@ -81,8 +106,23 @@ export default class Docker extends Command {
|
|
|
81
106
|
return;
|
|
82
107
|
}
|
|
83
108
|
// cd to path, docker build tag with name
|
|
109
|
+
const buildCommand = `cd ${args.path}/${flags.type} && docker build -t ${flags.name} . > /dev/null 2>&1`;
|
|
110
|
+
if (flags['dry-run']) {
|
|
111
|
+
console.log(chalk.yellow('[DRY RUN] Would execute:'));
|
|
112
|
+
console.log(chalk.cyan(` ${buildCommand}`));
|
|
113
|
+
console.log(chalk.yellow(`[DRY RUN] Would update docker-compose file: ${flags.file}`));
|
|
114
|
+
if (flags.type === 'elixir') {
|
|
115
|
+
console.log(chalk.green(` Set langserve.image = ${flags.name}`));
|
|
116
|
+
console.log(chalk.green(` Set langserve.pull_policy = if_not_present`));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(chalk.green(` Set frontend.image = ${flags.name}`));
|
|
120
|
+
console.log(chalk.green(` Set frontend.pull_policy = if_not_present`));
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
84
124
|
const spinner = ora('Running docker build ..').start();
|
|
85
|
-
exec(
|
|
125
|
+
exec(buildCommand, (error, stdout, stderr) => {
|
|
86
126
|
if (error) {
|
|
87
127
|
spinner.fail('Docker build failed');
|
|
88
128
|
console.error(`exec error: ${error}`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Docktor extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
op: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
environment: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
image: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'model-path': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
private restartMcpxContainer;
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export default class Docktor extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
op: Args.string({ description: 'Operation to perform (install, remove, restart, list)', required: true }),
|
|
9
|
+
name: Args.string({ description: 'Name of the inference pipeline (e.g., skin-cancer-classifier)', required: false }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Manage inference pipelines for MCPX';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %> install my-pipeline --image my-image:latest --model-path ./models',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> remove my-pipeline',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> list',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
container: Flags.string({
|
|
19
|
+
char: 'c',
|
|
20
|
+
default: 'dhti-mcpx-1',
|
|
21
|
+
description: 'Docker container name for MCPX (use docker ps to find the correct name)',
|
|
22
|
+
}),
|
|
23
|
+
environment: Flags.string({
|
|
24
|
+
char: 'e',
|
|
25
|
+
multiple: true,
|
|
26
|
+
description: 'Environment variables to pass to docker (format: VAR=value)',
|
|
27
|
+
}),
|
|
28
|
+
image: Flags.string({ char: 'i', description: 'Docker image for the inference pipeline (required for install)' }),
|
|
29
|
+
'model-path': Flags.string({
|
|
30
|
+
char: 'm',
|
|
31
|
+
default: '/lunar/packages/mcpx-server/config',
|
|
32
|
+
description: 'Local path to the model directory (optional for install)',
|
|
33
|
+
}),
|
|
34
|
+
workdir: Flags.string({
|
|
35
|
+
char: 'w',
|
|
36
|
+
default: `${os.homedir()}/dhti`,
|
|
37
|
+
description: 'Working directory for MCPX config',
|
|
38
|
+
}),
|
|
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
|
+
async run() {
|
|
53
|
+
const { args, flags } = await this.parse(Docktor);
|
|
54
|
+
const mcpxConfigPath = path.join(flags.workdir, 'config');
|
|
55
|
+
const mcpJsonPath = path.join(mcpxConfigPath, 'mcp.json');
|
|
56
|
+
// Ensure config directory exists
|
|
57
|
+
if (!fs.existsSync(mcpxConfigPath)) {
|
|
58
|
+
fs.mkdirSync(mcpxConfigPath, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
// Ensure mcp.json exists
|
|
61
|
+
if (!fs.existsSync(mcpJsonPath)) {
|
|
62
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: {} }, null, 2));
|
|
63
|
+
}
|
|
64
|
+
let mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
65
|
+
// Ensure mcpServers exists
|
|
66
|
+
if (!mcpConfig.mcpServers) {
|
|
67
|
+
mcpConfig.mcpServers = {};
|
|
68
|
+
}
|
|
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`);
|
|
81
|
+
}
|
|
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(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
envVars.push(...flags.environment);
|
|
91
|
+
}
|
|
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');
|
|
117
|
+
}
|
|
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'));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.log(chalk.yellow(`Inference pipeline '${args.name}' not found.`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
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
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.error(`Unknown operation: ${args.op}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -9,6 +9,7 @@ export default class Elixir extends Command {
|
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
dev: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
git: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
pypi: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
package/dist/commands/elixir.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { exec } from 'node:child_process';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import os from 'node:os';
|
|
@@ -18,6 +19,10 @@ export default class Elixir extends Command {
|
|
|
18
19
|
description: 'Name of the container to copy the elixir to while in dev mode',
|
|
19
20
|
}),
|
|
20
21
|
dev: Flags.string({ char: 'd', default: 'none', description: 'Dev folder to install' }),
|
|
22
|
+
'dry-run': Flags.boolean({
|
|
23
|
+
default: false,
|
|
24
|
+
description: 'Show what changes would be made without actually making them',
|
|
25
|
+
}),
|
|
21
26
|
git: Flags.string({ char: 'g', default: 'none', description: 'Github repository to install' }),
|
|
22
27
|
name: Flags.string({ char: 'n', description: 'Name of the elixir' }),
|
|
23
28
|
pypi: Flags.string({
|
|
@@ -47,9 +52,17 @@ export default class Elixir extends Command {
|
|
|
47
52
|
// if arg is dev then copy to docker as below
|
|
48
53
|
// docker restart dhti-langserve-1
|
|
49
54
|
if (args.op === 'dev') {
|
|
50
|
-
|
|
55
|
+
const devCommand = `cd ${flags.dev} && docker cp src/${expoName}/. ${flags.container}:/app/.venv/lib/python3.12/site-packages/${expoName}`;
|
|
56
|
+
const restartCommand = `docker restart ${flags.container}`;
|
|
57
|
+
if (flags['dry-run']) {
|
|
58
|
+
console.log(chalk.yellow('[DRY RUN] Would execute commands:'));
|
|
59
|
+
console.log(chalk.cyan(` ${devCommand}`));
|
|
60
|
+
console.log(chalk.cyan(` ${restartCommand}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(devCommand);
|
|
51
64
|
try {
|
|
52
|
-
exec(
|
|
65
|
+
exec(devCommand, (error, stdout, stderr) => {
|
|
53
66
|
if (error) {
|
|
54
67
|
console.error(`exec error: ${error}`);
|
|
55
68
|
return;
|
|
@@ -57,7 +70,7 @@ export default class Elixir extends Command {
|
|
|
57
70
|
console.log(`stdout: ${stdout}`);
|
|
58
71
|
console.error(`stderr: ${stderr}`);
|
|
59
72
|
});
|
|
60
|
-
exec(
|
|
73
|
+
exec(restartCommand, (error, stdout, stderr) => {
|
|
61
74
|
if (error) {
|
|
62
75
|
console.error(`exec error: ${error}`);
|
|
63
76
|
return;
|
|
@@ -69,23 +82,45 @@ export default class Elixir extends Command {
|
|
|
69
82
|
catch (error) {
|
|
70
83
|
console.log('Error copying conch to container', error);
|
|
71
84
|
}
|
|
85
|
+
return;
|
|
72
86
|
}
|
|
73
87
|
// Create a directory to install the elixir
|
|
74
88
|
if (!fs.existsSync(`${flags.workdir}/elixir`)) {
|
|
75
|
-
|
|
89
|
+
if (flags['dry-run']) {
|
|
90
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${flags.workdir}/elixir`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
fs.mkdirSync(`${flags.workdir}/elixir`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (flags['dry-run']) {
|
|
97
|
+
console.log(chalk.yellow(`[DRY RUN] Would copy resources from ${RESOURCES_DIR}/genai to ${flags.workdir}/elixir`));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
fs.cpSync(path.join(RESOURCES_DIR, 'genai'), `${flags.workdir}/elixir`, { recursive: true });
|
|
76
101
|
}
|
|
77
|
-
fs.cpSync(path.join(RESOURCES_DIR, 'genai'), `${flags.workdir}/elixir`, { recursive: true });
|
|
78
102
|
// if whl is not none, copy the whl file to thee whl directory
|
|
79
103
|
if (flags.whl !== 'none') {
|
|
80
104
|
if (!fs.existsSync(`${flags.workdir}/elixir/whl/`)) {
|
|
81
|
-
|
|
105
|
+
if (flags['dry-run']) {
|
|
106
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${flags.workdir}/whl/`));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
fs.mkdirSync(`${flags.workdir}/whl/`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (flags['dry-run']) {
|
|
113
|
+
console.log(chalk.yellow(`[DRY RUN] Would copy ${flags.whl} to ${flags.workdir}/elixir/whl/${path.basename(flags.whl)}`));
|
|
114
|
+
console.log(chalk.cyan('[DRY RUN] Installing elixir from whl file. Please modify boostrap.py file if needed'));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
fs.cpSync(flags.whl, `${flags.workdir}/elixir/whl/${path.basename(flags.whl)}`);
|
|
118
|
+
console.log('Installing elixir from whl file. Please modify boostrap.py file if needed');
|
|
82
119
|
}
|
|
83
|
-
fs.cpSync(flags.whl, `${flags.workdir}/elixir/whl/${path.basename(flags.whl)}`);
|
|
84
|
-
console.log('Installing elixir from whl file. Please modify boostrap.py file if needed');
|
|
85
120
|
}
|
|
86
121
|
// Install the elixir from git adding to the pyproject.toml file
|
|
87
|
-
let pyproject = fs.readFileSync(`${flags.workdir}/elixir/pyproject.toml`, 'utf8');
|
|
88
|
-
const originalServer = fs.readFileSync(`${flags.workdir}/elixir/app/server.py`, 'utf8');
|
|
122
|
+
let pyproject = flags['dry-run'] ? '' : fs.readFileSync(`${flags.workdir}/elixir/pyproject.toml`, 'utf8');
|
|
123
|
+
const originalServer = flags['dry-run'] ? '' : fs.readFileSync(`${flags.workdir}/elixir/app/server.py`, 'utf8');
|
|
89
124
|
let lineToAdd = '';
|
|
90
125
|
if (flags.whl !== 'none') {
|
|
91
126
|
lineToAdd = `${flags.name} = { file = "whl/${path.basename(flags.whl)}" }`;
|
|
@@ -96,8 +131,10 @@ export default class Elixir extends Command {
|
|
|
96
131
|
if (flags.pypi !== 'none') {
|
|
97
132
|
lineToAdd = flags.pypi;
|
|
98
133
|
}
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
if (!flags['dry-run']) {
|
|
135
|
+
pyproject = pyproject.replace('dependencies = [', `dependencies = [\n"${flags.name}",`);
|
|
136
|
+
pyproject = pyproject.replace('[tool.uv.sources]', `[tool.uv.sources]\n${lineToAdd}\n`);
|
|
137
|
+
}
|
|
101
138
|
const newPyproject = pyproject;
|
|
102
139
|
// Add the elixir import and bootstrap to the server.py file
|
|
103
140
|
let CliImport = `from ${expoName}.bootstrap import bootstrap as ${expoName}_bootstrap\n`;
|
|
@@ -108,27 +145,49 @@ ${expoName}_chain = ${expoName}_chain_class().get_chain_as_langchain_tool()
|
|
|
108
145
|
${expoName}_mcp_tool = ${expoName}_chain_class().get_chain_as_mcp_tool
|
|
109
146
|
mcp_server.add_tool(${expoName}_mcp_tool) # type: ignore
|
|
110
147
|
`;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
let newCliImport = '';
|
|
149
|
+
if (!flags['dry-run']) {
|
|
150
|
+
newCliImport = fs
|
|
151
|
+
.readFileSync(`${flags.workdir}/elixir/app/server.py`, 'utf8')
|
|
152
|
+
.replace('# DHTI_CLI_IMPORT', `#DHTI_CLI_IMPORT\n${CliImport}`);
|
|
153
|
+
}
|
|
114
154
|
const langfuseRoute = `add_routes(app, ${expoName}_chain.with_config(config), path="/langserve/${expoName}")`;
|
|
115
|
-
const newLangfuseRoute = newCliImport.replace('# DHTI_LANGFUSE_ROUTE', `#DHTI_LANGFUSE_ROUTE\n ${langfuseRoute}`);
|
|
155
|
+
const newLangfuseRoute = flags['dry-run'] ? '' : newCliImport.replace('# DHTI_LANGFUSE_ROUTE', `#DHTI_LANGFUSE_ROUTE\n ${langfuseRoute}`);
|
|
116
156
|
const normalRoute = `add_routes(app, ${expoName}_chain, path="/langserve/${expoName}")`;
|
|
117
|
-
const newNormalRoute = newLangfuseRoute.replace('# DHTI_NORMAL_ROUTE', `#DHTI_NORMAL_ROUTE\n ${normalRoute}`);
|
|
157
|
+
const newNormalRoute = flags['dry-run'] ? '' : newLangfuseRoute.replace('# DHTI_NORMAL_ROUTE', `#DHTI_NORMAL_ROUTE\n ${normalRoute}`);
|
|
118
158
|
const commonRoutes = `\nadd_invokes(app, path="/langserve/${expoName}")\nadd_services(app, path="/langserve/${expoName}")`;
|
|
119
|
-
const finalRoute = newNormalRoute.replace('# DHTI_COMMON_ROUTE', `#DHTI_COMMON_ROUTES${commonRoutes}`);
|
|
159
|
+
const finalRoute = flags['dry-run'] ? '' : newNormalRoute.replace('# DHTI_COMMON_ROUTE', `#DHTI_COMMON_ROUTES${commonRoutes}`);
|
|
120
160
|
// if args.op === install, add the line to the pyproject.toml file
|
|
121
161
|
if (args.op === 'install') {
|
|
122
|
-
|
|
123
|
-
|
|
162
|
+
if (flags['dry-run']) {
|
|
163
|
+
console.log(chalk.yellow('[DRY RUN] Would update files:'));
|
|
164
|
+
console.log(chalk.cyan(` - ${flags.workdir}/elixir/pyproject.toml`));
|
|
165
|
+
console.log(chalk.green(` Add dependency: "${flags.name}"`));
|
|
166
|
+
console.log(chalk.green(` Add source: ${lineToAdd}`));
|
|
167
|
+
console.log(chalk.cyan(` - ${flags.workdir}/elixir/app/server.py`));
|
|
168
|
+
console.log(chalk.green(` Add import and routes for ${expoName}`));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
fs.writeFileSync(`${flags.workdir}/elixir/pyproject.toml`, newPyproject);
|
|
172
|
+
fs.writeFileSync(`${flags.workdir}/elixir/app/server.py`, finalRoute);
|
|
173
|
+
}
|
|
124
174
|
}
|
|
125
175
|
if (args.op === 'uninstall') {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
if (flags['dry-run']) {
|
|
177
|
+
console.log(chalk.yellow('[DRY RUN] Would update files:'));
|
|
178
|
+
console.log(chalk.cyan(` - ${flags.workdir}/elixir/pyproject.toml`));
|
|
179
|
+
console.log(chalk.green(` Remove source: ${lineToAdd}`));
|
|
180
|
+
console.log(chalk.cyan(` - ${flags.workdir}/elixir/app/server.py`));
|
|
181
|
+
console.log(chalk.green(` Remove import and routes for ${expoName}`));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// if args.op === uninstall, remove the line from the pyproject.toml file
|
|
185
|
+
fs.writeFileSync(`${flags.workdir}/elixir/pyproject.toml`, pyproject.replace(lineToAdd, ''));
|
|
186
|
+
let newServer = originalServer.replace(CliImport, '');
|
|
187
|
+
newServer = newServer.replace(langfuseRoute, '');
|
|
188
|
+
newServer = newServer.replace(normalRoute, '');
|
|
189
|
+
fs.writeFileSync(`${flags.workdir}/elixir/app/server.py`, newServer);
|
|
190
|
+
}
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
}
|
package/dist/commands/mimic.d.ts
CHANGED
package/dist/commands/mimic.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { Args, Command } from '@oclif/core';
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
export default class Mimic extends Command {
|
|
3
4
|
static args = {
|
|
4
5
|
server: Args.string({ default: 'http://localhost/fhir/$import', description: 'Server URL to submit' }), // object with input, instruction (rationale in distillation), output
|
|
5
6
|
};
|
|
6
7
|
static description = 'Submit a FHIR request to a server';
|
|
7
8
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
9
|
+
static flags = {
|
|
10
|
+
'dry-run': Flags.boolean({
|
|
11
|
+
default: false,
|
|
12
|
+
description: 'Show what changes would be made without actually making them',
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
8
15
|
async run() {
|
|
9
16
|
const { args, flags } = await this.parse(Mimic);
|
|
10
17
|
const mimic_request = `{
|
|
@@ -140,6 +147,15 @@ export default class Mimic extends Command {
|
|
|
140
147
|
} ]
|
|
141
148
|
|
|
142
149
|
}`;
|
|
150
|
+
if (flags['dry-run']) {
|
|
151
|
+
console.log(chalk.yellow(`[DRY RUN] Would send POST request to: ${args.server}`));
|
|
152
|
+
console.log(chalk.cyan('[DRY RUN] Request headers:'));
|
|
153
|
+
console.log(chalk.green(' Content-Type: application/fhir+json'));
|
|
154
|
+
console.log(chalk.green(' Prefer: respond-async'));
|
|
155
|
+
console.log(chalk.cyan('[DRY RUN] Request body:'));
|
|
156
|
+
console.log(mimic_request);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
143
159
|
// send a POST request to the server with the mimic_request body
|
|
144
160
|
const response = await fetch(args.server, {
|
|
145
161
|
body: mimic_request,
|
|
@@ -151,7 +167,6 @@ export default class Mimic extends Command {
|
|
|
151
167
|
});
|
|
152
168
|
if (!response.ok) {
|
|
153
169
|
console.error(`Error: ${response.status} ${response.statusText}`);
|
|
154
|
-
return;
|
|
155
170
|
}
|
|
156
171
|
}
|
|
157
172
|
}
|
|
@@ -8,6 +8,7 @@ export default class Synthetic extends Command {
|
|
|
8
8
|
static description: string;
|
|
9
9
|
static examples: string[];
|
|
10
10
|
static flags: {
|
|
11
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
inputField: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
maxCycles: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
maxRecords: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import bootstrap from '../utils/bootstrap.js';
|
|
4
5
|
import { ChainService } from '../utils/chain.js';
|
|
@@ -13,6 +14,10 @@ export default class Synthetic extends Command {
|
|
|
13
14
|
'<%= config.bin %> <%= command.id %>',
|
|
14
15
|
];
|
|
15
16
|
static flags = {
|
|
17
|
+
'dry-run': Flags.boolean({
|
|
18
|
+
default: false,
|
|
19
|
+
description: 'Show what changes would be made without actually making them',
|
|
20
|
+
}),
|
|
16
21
|
inputField: Flags.string({ char: 'i', default: 'input', description: 'Input field to use', options: ['input', 'instruction', 'output'] }),
|
|
17
22
|
maxCycles: Flags.integer({ char: 'm', default: 0, description: 'Maximum number of cycles to run' }),
|
|
18
23
|
maxRecords: Flags.integer({ char: 'r', default: 10, description: 'Maximum number of records to generate' }),
|
|
@@ -24,13 +29,35 @@ export default class Synthetic extends Command {
|
|
|
24
29
|
// read prompt file if provided
|
|
25
30
|
if (args.prompt)
|
|
26
31
|
prompt = fs.readFileSync(args.prompt ?? '', 'utf8');
|
|
27
|
-
const container = await bootstrap();
|
|
28
|
-
const chain = new ChainService(container);
|
|
29
32
|
// if no output file, exit with error
|
|
30
33
|
if (!args.output) {
|
|
31
34
|
console.log("Please provide an output file");
|
|
32
35
|
this.exit(1);
|
|
33
36
|
}
|
|
37
|
+
if (flags['dry-run']) {
|
|
38
|
+
console.log(chalk.yellow('[DRY RUN] Synthetic data generation simulation'));
|
|
39
|
+
console.log(chalk.cyan(` Output file: ${args.output}`));
|
|
40
|
+
console.log(chalk.cyan(` Max records: ${flags.maxRecords}`));
|
|
41
|
+
if (flags.maxCycles) {
|
|
42
|
+
console.log(chalk.cyan(` Max cycles: ${flags.maxCycles}`));
|
|
43
|
+
console.log(chalk.cyan(` Output field: ${flags.outputField}`));
|
|
44
|
+
console.log(chalk.green('[DRY RUN] Would generate synthetic data in batches using LLM'));
|
|
45
|
+
console.log(chalk.green(`[DRY RUN] Would write ${flags.maxRecords} records to ${args.output}`));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(chalk.cyan(` Input file: ${args.input}`));
|
|
49
|
+
console.log(chalk.cyan(` Input field: ${flags.inputField}`));
|
|
50
|
+
console.log(chalk.cyan(` Output field: ${flags.outputField}`));
|
|
51
|
+
if (args.prompt) {
|
|
52
|
+
console.log(chalk.cyan(` Prompt file: ${args.prompt}`));
|
|
53
|
+
}
|
|
54
|
+
console.log(chalk.green('[DRY RUN] Would process input file records using LLM'));
|
|
55
|
+
console.log(chalk.green(`[DRY RUN] Would write processed records to ${args.output}`));
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const container = await bootstrap();
|
|
60
|
+
const chain = new ChainService(container);
|
|
34
61
|
if (flags.maxCycles) { // No input file, can process in batches
|
|
35
62
|
const input = {
|
|
36
63
|
input: prompt,
|
|
@@ -10,7 +10,7 @@ services:
|
|
|
10
10
|
- backend
|
|
11
11
|
ports:
|
|
12
12
|
- "80:80"
|
|
13
|
-
- "
|
|
13
|
+
- "9001:80"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
frontend:
|
|
@@ -133,7 +133,7 @@ services:
|
|
|
133
133
|
- "hapi.fhir.enforce_referential_integrity_on_delete=false"
|
|
134
134
|
|
|
135
135
|
mcp-fhir:
|
|
136
|
-
image: beapen/fhir-mcp-server:
|
|
136
|
+
image: beapen/fhir-mcp-server:4.0
|
|
137
137
|
ports:
|
|
138
138
|
- 8006:8000
|
|
139
139
|
restart: "unless-stopped"
|
|
@@ -225,6 +225,23 @@ services:
|
|
|
225
225
|
- spring.neo4j.authentication.username=neo4j
|
|
226
226
|
- spring.neo4j.authentication.password=password
|
|
227
227
|
|
|
228
|
+
mcpx:
|
|
229
|
+
image: us-central1-docker.pkg.dev/prj-common-442813/mcpx/mcpx:latest
|
|
230
|
+
ports:
|
|
231
|
+
- "8000:8000"
|
|
232
|
+
- "9000:9000"
|
|
233
|
+
- "5173:5173"
|
|
234
|
+
- "3000:3000"
|
|
235
|
+
# environment:
|
|
236
|
+
# - MCPX_PORT=9000
|
|
237
|
+
# - MCPX_SERVER_URL="http://10.0.0.211:9000"
|
|
238
|
+
# - VITE_MCPX_SERVER_PORT=9000
|
|
239
|
+
# - VITE_MCPX_SERVER_URL="http://10.0.0.211:9000"
|
|
240
|
+
restart: unless-stopped
|
|
241
|
+
volumes:
|
|
242
|
+
- mcpx-config:/lunar/packages/mcpx-server/config
|
|
243
|
+
privileged: true
|
|
244
|
+
|
|
228
245
|
|
|
229
246
|
volumes:
|
|
230
247
|
openmrs-data: ~
|
|
@@ -235,4 +252,5 @@ volumes:
|
|
|
235
252
|
neo4j-db: ~
|
|
236
253
|
ollama-code: ~
|
|
237
254
|
ollama-root: ~
|
|
238
|
-
ollama-webui: ~
|
|
255
|
+
ollama-webui: ~
|
|
256
|
+
mcpx-config: ~
|
package/oclif.manifest.json
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
"<%= config.bin %> <%= command.id %>"
|
|
14
14
|
],
|
|
15
15
|
"flags": {
|
|
16
|
+
"dry-run": {
|
|
17
|
+
"description": "Show what changes would be made without actually making them",
|
|
18
|
+
"name": "dry-run",
|
|
19
|
+
"allowNo": false,
|
|
20
|
+
"type": "boolean"
|
|
21
|
+
},
|
|
16
22
|
"file": {
|
|
17
23
|
"char": "f",
|
|
18
24
|
"description": "Full path to the docker compose file to read from. Creates if it does not exist",
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
},
|
|
25
31
|
"module": {
|
|
26
32
|
"char": "m",
|
|
27
|
-
"description": "Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j and
|
|
33
|
+
"description": "Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j, mcpFhir, mcpx and docktor)",
|
|
28
34
|
"name": "module",
|
|
29
35
|
"hasDynamicHelp": false,
|
|
30
36
|
"multiple": true,
|
|
@@ -86,6 +92,12 @@
|
|
|
86
92
|
"multiple": false,
|
|
87
93
|
"type": "option"
|
|
88
94
|
},
|
|
95
|
+
"dry-run": {
|
|
96
|
+
"description": "Show what changes would be made without actually making them",
|
|
97
|
+
"name": "dry-run",
|
|
98
|
+
"allowNo": false,
|
|
99
|
+
"type": "boolean"
|
|
100
|
+
},
|
|
89
101
|
"git": {
|
|
90
102
|
"char": "g",
|
|
91
103
|
"description": "Github repository to install",
|
|
@@ -160,6 +172,15 @@
|
|
|
160
172
|
"<%= config.bin %> <%= command.id %>"
|
|
161
173
|
],
|
|
162
174
|
"flags": {
|
|
175
|
+
"container": {
|
|
176
|
+
"char": "c",
|
|
177
|
+
"description": "Name of the container to copy the bootstrap file to while in dev mode",
|
|
178
|
+
"name": "container",
|
|
179
|
+
"default": "dhti-langserve-1",
|
|
180
|
+
"hasDynamicHelp": false,
|
|
181
|
+
"multiple": false,
|
|
182
|
+
"type": "option"
|
|
183
|
+
},
|
|
163
184
|
"down": {
|
|
164
185
|
"char": "d",
|
|
165
186
|
"description": "Run docker-compose down after building",
|
|
@@ -167,6 +188,12 @@
|
|
|
167
188
|
"allowNo": false,
|
|
168
189
|
"type": "boolean"
|
|
169
190
|
},
|
|
191
|
+
"dry-run": {
|
|
192
|
+
"description": "Show what changes would be made without actually making them",
|
|
193
|
+
"name": "dry-run",
|
|
194
|
+
"allowNo": false,
|
|
195
|
+
"type": "boolean"
|
|
196
|
+
},
|
|
170
197
|
"file": {
|
|
171
198
|
"char": "f",
|
|
172
199
|
"description": "Full path to the docker compose file to edit or run.",
|
|
@@ -176,15 +203,6 @@
|
|
|
176
203
|
"multiple": false,
|
|
177
204
|
"type": "option"
|
|
178
205
|
},
|
|
179
|
-
"container": {
|
|
180
|
-
"char": "c",
|
|
181
|
-
"description": "Name of the container to copy the bootstrap file to while in dev mode",
|
|
182
|
-
"name": "container",
|
|
183
|
-
"default": "dhti-langserve-1",
|
|
184
|
-
"hasDynamicHelp": false,
|
|
185
|
-
"multiple": false,
|
|
186
|
-
"type": "option"
|
|
187
|
-
},
|
|
188
206
|
"name": {
|
|
189
207
|
"char": "n",
|
|
190
208
|
"description": "Name of the container to build",
|
|
@@ -225,6 +243,86 @@
|
|
|
225
243
|
"docker.js"
|
|
226
244
|
]
|
|
227
245
|
},
|
|
246
|
+
"docktor": {
|
|
247
|
+
"aliases": [],
|
|
248
|
+
"args": {
|
|
249
|
+
"op": {
|
|
250
|
+
"description": "Operation to perform (install, remove, restart, list)",
|
|
251
|
+
"name": "op",
|
|
252
|
+
"required": true
|
|
253
|
+
},
|
|
254
|
+
"name": {
|
|
255
|
+
"description": "Name of the inference pipeline (e.g., skin-cancer-classifier)",
|
|
256
|
+
"name": "name",
|
|
257
|
+
"required": false
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"description": "Manage inference pipelines for MCPX",
|
|
261
|
+
"examples": [
|
|
262
|
+
"<%= config.bin %> <%= command.id %> install my-pipeline --image my-image:latest --model-path ./models",
|
|
263
|
+
"<%= config.bin %> <%= command.id %> remove my-pipeline",
|
|
264
|
+
"<%= config.bin %> <%= command.id %> list"
|
|
265
|
+
],
|
|
266
|
+
"flags": {
|
|
267
|
+
"container": {
|
|
268
|
+
"char": "c",
|
|
269
|
+
"description": "Docker container name for MCPX (use docker ps to find the correct name)",
|
|
270
|
+
"name": "container",
|
|
271
|
+
"default": "dhti-mcpx-1",
|
|
272
|
+
"hasDynamicHelp": false,
|
|
273
|
+
"multiple": false,
|
|
274
|
+
"type": "option"
|
|
275
|
+
},
|
|
276
|
+
"environment": {
|
|
277
|
+
"char": "e",
|
|
278
|
+
"description": "Environment variables to pass to docker (format: VAR=value)",
|
|
279
|
+
"name": "environment",
|
|
280
|
+
"hasDynamicHelp": false,
|
|
281
|
+
"multiple": true,
|
|
282
|
+
"type": "option"
|
|
283
|
+
},
|
|
284
|
+
"image": {
|
|
285
|
+
"char": "i",
|
|
286
|
+
"description": "Docker image for the inference pipeline (required for install)",
|
|
287
|
+
"name": "image",
|
|
288
|
+
"hasDynamicHelp": false,
|
|
289
|
+
"multiple": false,
|
|
290
|
+
"type": "option"
|
|
291
|
+
},
|
|
292
|
+
"model-path": {
|
|
293
|
+
"char": "m",
|
|
294
|
+
"description": "Local path to the model directory (optional for install)",
|
|
295
|
+
"name": "model-path",
|
|
296
|
+
"default": "/lunar/packages/mcpx-server/config",
|
|
297
|
+
"hasDynamicHelp": false,
|
|
298
|
+
"multiple": false,
|
|
299
|
+
"type": "option"
|
|
300
|
+
},
|
|
301
|
+
"workdir": {
|
|
302
|
+
"char": "w",
|
|
303
|
+
"description": "Working directory for MCPX config",
|
|
304
|
+
"name": "workdir",
|
|
305
|
+
"default": "/home/runner/dhti",
|
|
306
|
+
"hasDynamicHelp": false,
|
|
307
|
+
"multiple": false,
|
|
308
|
+
"type": "option"
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"hasDynamicHelp": false,
|
|
312
|
+
"hiddenAliases": [],
|
|
313
|
+
"id": "docktor",
|
|
314
|
+
"pluginAlias": "dhti-cli",
|
|
315
|
+
"pluginName": "dhti-cli",
|
|
316
|
+
"pluginType": "core",
|
|
317
|
+
"strict": true,
|
|
318
|
+
"enableJsonFlag": false,
|
|
319
|
+
"isESM": true,
|
|
320
|
+
"relativePath": [
|
|
321
|
+
"dist",
|
|
322
|
+
"commands",
|
|
323
|
+
"docktor.js"
|
|
324
|
+
]
|
|
325
|
+
},
|
|
228
326
|
"elixir": {
|
|
229
327
|
"aliases": [],
|
|
230
328
|
"args": {
|
|
@@ -265,6 +363,12 @@
|
|
|
265
363
|
"multiple": false,
|
|
266
364
|
"type": "option"
|
|
267
365
|
},
|
|
366
|
+
"dry-run": {
|
|
367
|
+
"description": "Show what changes would be made without actually making them",
|
|
368
|
+
"name": "dry-run",
|
|
369
|
+
"allowNo": false,
|
|
370
|
+
"type": "boolean"
|
|
371
|
+
},
|
|
268
372
|
"git": {
|
|
269
373
|
"char": "g",
|
|
270
374
|
"description": "Github repository to install",
|
|
@@ -347,7 +451,14 @@
|
|
|
347
451
|
"examples": [
|
|
348
452
|
"<%= config.bin %> <%= command.id %>"
|
|
349
453
|
],
|
|
350
|
-
"flags": {
|
|
454
|
+
"flags": {
|
|
455
|
+
"dry-run": {
|
|
456
|
+
"description": "Show what changes would be made without actually making them",
|
|
457
|
+
"name": "dry-run",
|
|
458
|
+
"allowNo": false,
|
|
459
|
+
"type": "boolean"
|
|
460
|
+
}
|
|
461
|
+
},
|
|
351
462
|
"hasDynamicHelp": false,
|
|
352
463
|
"hiddenAliases": [],
|
|
353
464
|
"id": "mimic",
|
|
@@ -386,6 +497,12 @@
|
|
|
386
497
|
"<%= config.bin %> <%= command.id %>"
|
|
387
498
|
],
|
|
388
499
|
"flags": {
|
|
500
|
+
"dry-run": {
|
|
501
|
+
"description": "Show what changes would be made without actually making them",
|
|
502
|
+
"name": "dry-run",
|
|
503
|
+
"allowNo": false,
|
|
504
|
+
"type": "boolean"
|
|
505
|
+
},
|
|
389
506
|
"inputField": {
|
|
390
507
|
"char": "i",
|
|
391
508
|
"description": "Input field to use",
|
|
@@ -449,5 +566,5 @@
|
|
|
449
566
|
]
|
|
450
567
|
}
|
|
451
568
|
},
|
|
452
|
-
"version": "0.
|
|
569
|
+
"version": "0.5.0"
|
|
453
570
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dhti-cli",
|
|
3
3
|
"description": "DHTI CLI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"author": "Bell Eapen",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dhti-cli": "bin/run.js"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"@oclif/core": "^4",
|
|
14
14
|
"@oclif/plugin-help": "^6",
|
|
15
15
|
"@oclif/plugin-plugins": "^5",
|
|
16
|
+
"chalk": "^4.1.2",
|
|
16
17
|
"child_process": "^1.0.2",
|
|
17
18
|
"js-yaml": "^4.1.0",
|
|
18
19
|
"medpromptjs": ">=0.4.3",
|