dhti-cli 1.0.1 → 1.2.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 +14 -4
- package/dist/commands/compose.js +2 -0
- package/dist/commands/conch.d.ts +3 -2
- package/dist/commands/conch.js +77 -21
- package/dist/commands/docker.js +8 -3
- package/dist/commands/elixir.js +136 -50
- package/dist/resources/docker-compose-master.yml +24 -3
- package/dist/resources/genai/app/bootstrap.py +34 -4
- package/oclif.manifest.json +18 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ Generative AI features are built as [LangServe Apps](https://python.langchain.co
|
|
|
25
25
|
|
|
26
26
|
🚀 You can test the elixir using a real EMR system, [OpenMRS](https://openmrs.org/), that communicates with the elixir using **CDS-Hooks** or use any other CDS-Hooks compatible EMR system. You can also use the [CDS-Hooks sandbox for testing](https://github.com/dermatologist/cds-hooks-sandbox/tree/dhti-1) without an EMR.
|
|
27
27
|
|
|
28
|
+
🚀 Checkout **[Vidhi Recipes](/vidhi/README.md)** for chatbot, RAG, imaging (DICOM) and MCPX for dockerized calculators
|
|
29
|
+
|
|
28
30
|
#### How (non‑technical / clinical)
|
|
29
31
|
DHTI includes ready‑to‑use [skills](/.github/skills/) that can prompt agentic platforms (e.g., [AntiGravity](https://antigravity.google/), VSCode, or Claude) to generate the GenAI backends and UI components (elixirs and conches) you need. Test these components with synthetic data in OpenMRS or the CDS‑Hooks sandbox, then hand them off to production teams. Because DHTI follows open standards, that handoff (the “valley of death”) becomes smoother and more predictable. Try the [prompts](/.github/skills/start-dhti/examples/e2e-sample.md) in your preferred agentic platform after cloning this repo.
|
|
30
32
|
|
|
@@ -52,15 +54,16 @@ npx dhti-cli docker -u # start services from compose
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
Notes:
|
|
55
|
-
-
|
|
57
|
+
- Install from a local directory using `-l`.
|
|
56
58
|
- Stop and remove containers with `npx dhti-cli docker -d`.
|
|
57
59
|
|
|
58
60
|
✌️ Decide where to test the new elixir: OpenMRS a full EHR system, or CDS-Hooks sandbox for a lightweight testing without an EHR.
|
|
59
61
|
|
|
60
62
|
💥 Test elixir in a CDS-Hooks sandbox.
|
|
61
63
|
|
|
62
|
-
* `npx dhti-cli
|
|
63
|
-
*
|
|
64
|
+
* `npx dhti-cli elixir start -n dhti-elixir-schat` and navigate to the **Application URL displayed in the console** (scroll up to see this). Not the base URL listed at the bottom.
|
|
65
|
+
* Uses hapi.fhir.org for data.
|
|
66
|
+
* In the **Rx View** tab, type in the contentString textbox and wait for the elixir to respond (Submits automatically in 5 seconds).
|
|
64
67
|
|
|
65
68
|
<p align="center">
|
|
66
69
|
<img src="https://github.com/dermatologist/dhti/blob/develop/notes/cds-hook-sandbox.jpg" />
|
|
@@ -88,12 +91,17 @@ You will see the new **patient context aware chatbot** in the patient summary pa
|
|
|
88
91
|
|
|
89
92
|
* `npx dhti-cli docker -d` to stop and delete all the docker containers.
|
|
90
93
|
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
* `npx dhti-cli docker bootstrap -f bootstrap.py` will create and sync bootstrap.py where you can configure default model and hyperparameters for LangServe. Run this command after changing bootstrap.py to apply the changes.
|
|
97
|
+
|
|
91
98
|
## Wiki & Documentation
|
|
92
99
|
* [](https://github.com/dermatologist/dhti/wiki)
|
|
93
100
|
* [Documentation](https://dermatologist.github.io/dhti/)
|
|
94
101
|
* [CLI Reference](/notes/README.md)
|
|
95
102
|
|
|
96
103
|
## User contributions & examples
|
|
104
|
+
* 🚀 **[Vidhi Recipes](/vidhi/README.md)** for chatbot, RAG, imaging (DICOM) and MCPX for dockerized calculators
|
|
97
105
|
* [Elixirs](https://github.com/dermatologist/dhti-elixir)
|
|
98
106
|
* [OpenMRS Conches / UI](https://github.com/dermatologist/openmrs-esm-dhti)
|
|
99
107
|
* [CDS Hooks Sandbox for testing](https://github.com/dermatologist/cds-hooks-sandbox)
|
|
@@ -101,14 +109,16 @@ You will see the new **patient context aware chatbot** in the patient summary pa
|
|
|
101
109
|
## Presentations
|
|
102
110
|
⭐️ **Pitched at [Falling Walls Lab Illinois](https://falling-walls.com/falling-walls-lab-illinois) and released on 2025-09-12.**
|
|
103
111
|
|
|
104
|
-
## What problems do DHTI solve?
|
|
112
|
+
## 🔧 What problems do DHTI solve?
|
|
105
113
|
|
|
106
114
|
| Why | How |
|
|
107
115
|
| --- | --- |
|
|
116
|
+
| I am a clinician! I have no idea how to build GenAI apps. | ✨ DHTI comes with batteries ([skills](/.github/skills/)) included! Use your preferred agentic platform (e.g., [AntiGravity](https://antigravity.google/), [VSCode with Copilot in agent mode](https://code.visualstudio.com/docs/copilot/overview), Claude, [Cursor](https://cursor.com/) and many other) to generate elixirs and conches from [problem-oriented prompts](/prompts/e2e-sample.md) (most of these platforms have a free tier). Test them using synthetic data in OpenMRS or the CDS-Hooks sandbox, then hand them off to production teams. |
|
|
108
117
|
| I know LangChain, but I don’t know how to build a chain/agent based on data in our EHR. | [These sample elixirs](https://github.com/dermatologist/dhti-elixir) adopt FHIR and cds-hooks as standards for data retrieval and display. The [base class](https://github.com/dermatologist/dhti-elixir-base) provides reusable artifacts |
|
|
109
118
|
| I need a simple platform for experimenting. | This repository provides everything to start experimenting fast. The command-line tools help to virtualize and orchestrate your experiments using [Docker](https://www.docker.com/)|
|
|
110
119
|
| I am a UI designer. I want to design helpful UI for real users. | See [these sample conches](https://github.com/dermatologist/openmrs-esm-dhti). It shows how to build interface components (conches) for [OpenMRS](https://openmrs.org/) an open-source EMR used by many. Read more about [OpenMRS UI](https://o3-docs.openmrs.org/) |
|
|
111
120
|
| We use another EMR | Your EMR may support CDS-Hook for displaying components. In that case, you can use [cds-hooks-sandbox for testing](https://github.com/dermatologist/cds-hooks-sandbox/tree/dhti-1) |
|
|
121
|
+
| We don't use an EMR. We use a web based health information system for ------ population with no FHIR support. | You can still use DHTI as a GenAI experimentation platform. ✨ We have a [browser extension](https://github.com/dermatologist/openmrs-esm-dhti/blob/develop/packages/dhti-screen-grabber/README.md) that can read any web page! |
|
|
112
122
|
| Our IT team is often unable to take my experiments to production. | Use DHTI, follow the recommended patterns, and you will make their lives easier.|
|
|
113
123
|
|
|
114
124
|
|
package/dist/commands/compose.js
CHANGED
|
@@ -80,6 +80,7 @@ export default class Compose extends Command {
|
|
|
80
80
|
const mcpx = ['mcpx'];
|
|
81
81
|
const docktor = ['mcpx'];
|
|
82
82
|
const medplum = ['medplum-server', 'medplum-app', 'postgres-db', 'redis', 'mpclient'];
|
|
83
|
+
const orthanc = ['orthanc', 'cors-proxy'];
|
|
83
84
|
const _modules = {
|
|
84
85
|
cqlFhir,
|
|
85
86
|
docktor,
|
|
@@ -95,6 +96,7 @@ export default class Compose extends Command {
|
|
|
95
96
|
openmrs,
|
|
96
97
|
redis,
|
|
97
98
|
webui,
|
|
99
|
+
orthanc,
|
|
98
100
|
};
|
|
99
101
|
try {
|
|
100
102
|
const masterData = yaml.load(fs.readFileSync(path.join(RESOURCES_DIR, 'docker-compose-master.yml'), 'utf8'));
|
package/dist/commands/conch.d.ts
CHANGED
|
@@ -9,8 +9,9 @@ export default class Conch extends Command {
|
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
git: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
local: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
sources: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
};
|
|
16
17
|
run(): Promise<void>;
|
package/dist/commands/conch.js
CHANGED
|
@@ -8,13 +8,16 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
export default class Conch extends Command {
|
|
10
10
|
static args = {
|
|
11
|
-
op: Args.string({ description: 'Operation to perform (init, install, or start)' }),
|
|
11
|
+
op: Args.string({ description: 'Operation to perform (add, init, install, or start)' }),
|
|
12
12
|
};
|
|
13
13
|
static description = 'Initialize, install, or start OpenMRS frontend development';
|
|
14
14
|
static examples = [
|
|
15
|
+
'<%= config.bin %> <%= command.id %> add -g my-repo/my-package -n my-package -w ~/projects',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> add -g my-repo/my-package -b main -n my-package -w ~/projects',
|
|
15
17
|
'<%= config.bin %> <%= command.id %> install -n my-app -w ~/projects',
|
|
16
18
|
'<%= config.bin %> <%= command.id %> init -n my-app -w ~/projects',
|
|
17
19
|
'<%= config.bin %> <%= command.id %> start -n my-app -w ~/projects',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> start -n my-app -w ~/projects -s packages/chatbot -s packages/utils',
|
|
18
21
|
];
|
|
19
22
|
static flags = {
|
|
20
23
|
branch: Flags.string({
|
|
@@ -28,13 +31,18 @@ export default class Conch extends Command {
|
|
|
28
31
|
}),
|
|
29
32
|
git: Flags.string({
|
|
30
33
|
char: 'g',
|
|
31
|
-
default: 'dermatologist/openmrs-esm-dhti
|
|
34
|
+
default: 'dermatologist/openmrs-esm-dhti',
|
|
32
35
|
description: 'GitHub repository to install (for install operation)',
|
|
33
36
|
}),
|
|
34
|
-
|
|
37
|
+
local: Flags.string({
|
|
38
|
+
char: 'l',
|
|
39
|
+
description: 'Local path to use instead of calculated workdir/name path (for start operation)',
|
|
40
|
+
}),
|
|
41
|
+
name: Flags.string({ char: 'n', default: 'esm-dhti', description: 'Name of the conch' }),
|
|
35
42
|
sources: Flags.string({
|
|
36
43
|
char: 's',
|
|
37
|
-
description: 'Additional sources to include when starting (e.g., packages/esm-chatbot-agent)',
|
|
44
|
+
description: 'Additional sources to include when starting (e.g., packages/esm-chatbot-agent, packages/esm-another-app)',
|
|
45
|
+
multiple: true,
|
|
38
46
|
}),
|
|
39
47
|
workdir: Flags.string({
|
|
40
48
|
char: 'w',
|
|
@@ -44,6 +52,43 @@ export default class Conch extends Command {
|
|
|
44
52
|
};
|
|
45
53
|
async run() {
|
|
46
54
|
const { args, flags } = await this.parse(Conch);
|
|
55
|
+
if (args.op === 'add') {
|
|
56
|
+
// Validate that git and name are overridden from defaults
|
|
57
|
+
const defaultGit = 'dermatologist/openmrs-esm-dhti';
|
|
58
|
+
const defaultName = 'esm-dhti';
|
|
59
|
+
const gitOverridden = flags.git !== defaultGit;
|
|
60
|
+
const nameOverridden = flags.name !== defaultName;
|
|
61
|
+
if (!gitOverridden || !nameOverridden) {
|
|
62
|
+
console.log(chalk.yellow('Note: The "add" operation requires non-default values for both --git and --name flags.'));
|
|
63
|
+
if (!gitOverridden) {
|
|
64
|
+
console.log(chalk.yellow(' Current --git: (default)'));
|
|
65
|
+
}
|
|
66
|
+
if (!nameOverridden) {
|
|
67
|
+
console.log(chalk.yellow(' Current --name: (default)'));
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.yellow('\nNo changes made. Please provide custom --git and --name values.'));
|
|
70
|
+
this.exit(0);
|
|
71
|
+
}
|
|
72
|
+
if (flags['dry-run']) {
|
|
73
|
+
console.log(chalk.yellow('[DRY RUN] Would execute add operation:'));
|
|
74
|
+
const targetPath = path.join(flags.workdir, 'esm-dhti', 'packages', flags.name);
|
|
75
|
+
console.log(chalk.cyan(` npx degit ${flags.git}#${flags.branch} ${targetPath}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
console.log(chalk.blue(`Adding package ${flags.name} from ${flags.git}#${flags.branch}...`));
|
|
80
|
+
const targetPath = path.join(flags.workdir, 'esm-dhti', 'packages', flags.name);
|
|
81
|
+
const degitCommand = `npx degit ${flags.git}#${flags.branch} ${targetPath}`;
|
|
82
|
+
await execAsync(degitCommand);
|
|
83
|
+
console.log(chalk.green('✓ Package added successfully'));
|
|
84
|
+
console.log(chalk.green(`\n✓ Package is ready at ${targetPath}`));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(chalk.red('Error during add operation:'), error);
|
|
88
|
+
this.exit(1);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
47
92
|
if (args.op === 'init') {
|
|
48
93
|
// Validate required flags
|
|
49
94
|
if (!flags.workdir) {
|
|
@@ -91,21 +136,26 @@ export default class Conch extends Command {
|
|
|
91
136
|
}
|
|
92
137
|
if (args.op === 'start') {
|
|
93
138
|
// Validate required flags
|
|
94
|
-
if (!flags.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
if (!flags.local) {
|
|
140
|
+
// If --local is not provided, require workdir and name
|
|
141
|
+
if (!flags.workdir) {
|
|
142
|
+
console.error(chalk.red('Error: workdir flag is required for start operation (unless --local is provided)'));
|
|
143
|
+
this.exit(1);
|
|
144
|
+
}
|
|
145
|
+
if (!flags.name) {
|
|
146
|
+
console.error(chalk.red('Error: name flag is required for start operation (unless --local is provided)'));
|
|
147
|
+
this.exit(1);
|
|
148
|
+
}
|
|
101
149
|
}
|
|
102
|
-
const targetDir = path.join(flags.workdir, flags.name);
|
|
150
|
+
const targetDir = flags.local || path.join(flags.workdir, flags.name);
|
|
103
151
|
if (flags['dry-run']) {
|
|
104
152
|
console.log(chalk.yellow('[DRY RUN] Would execute start operation:'));
|
|
105
153
|
console.log(chalk.cyan(` cd ${targetDir}`));
|
|
106
154
|
let dryRunCommand = 'corepack enable && yarn install && yarn start';
|
|
107
|
-
if (flags.sources) {
|
|
108
|
-
|
|
155
|
+
if (flags.sources && flags.sources.length > 0) {
|
|
156
|
+
for (const source of flags.sources) {
|
|
157
|
+
dryRunCommand += ` --sources '${source}'`;
|
|
158
|
+
}
|
|
109
159
|
}
|
|
110
160
|
console.log(chalk.cyan(` ${dryRunCommand}`));
|
|
111
161
|
return;
|
|
@@ -113,7 +163,9 @@ export default class Conch extends Command {
|
|
|
113
163
|
// Check if directory exists (not in dry-run mode)
|
|
114
164
|
if (!fs.existsSync(targetDir)) {
|
|
115
165
|
console.error(chalk.red(`Error: Directory does not exist: ${targetDir}`));
|
|
116
|
-
|
|
166
|
+
if (!flags.local) {
|
|
167
|
+
console.log(chalk.yellow(`Run 'dhti-cli conch init -n ${flags.name} -w ${flags.workdir}' first`));
|
|
168
|
+
}
|
|
117
169
|
this.exit(1);
|
|
118
170
|
}
|
|
119
171
|
try {
|
|
@@ -121,8 +173,10 @@ export default class Conch extends Command {
|
|
|
121
173
|
console.log(chalk.yellow('Press Ctrl-C to stop\n'));
|
|
122
174
|
// Build the start command with sources flag if provided
|
|
123
175
|
let startCommand = 'corepack enable && yarn install && yarn start';
|
|
124
|
-
if (flags.sources) {
|
|
125
|
-
|
|
176
|
+
if (flags.sources && flags.sources.length > 0) {
|
|
177
|
+
for (const source of flags.sources) {
|
|
178
|
+
startCommand += ` --sources '${source}'`;
|
|
179
|
+
}
|
|
126
180
|
}
|
|
127
181
|
// Spawn corepack enable && yarn install && yarn start with stdio inheritance to show output and allow Ctrl-C
|
|
128
182
|
const child = spawn(startCommand, {
|
|
@@ -166,7 +220,7 @@ export default class Conch extends Command {
|
|
|
166
220
|
this.exit(1);
|
|
167
221
|
}
|
|
168
222
|
// Warn if sources flag is used with install (not applicable)
|
|
169
|
-
if (flags.sources) {
|
|
223
|
+
if (flags.sources && flags.sources.length > 0) {
|
|
170
224
|
console.warn(chalk.yellow('Warning: --sources flag is not applicable for install operation. It will be ignored.'));
|
|
171
225
|
console.warn(chalk.yellow('Use --sources with the start operation instead.'));
|
|
172
226
|
}
|
|
@@ -185,8 +239,10 @@ export default class Conch extends Command {
|
|
|
185
239
|
console.log(chalk.green(`\n✓ Installation complete! Your app is ready at ${targetDir}`));
|
|
186
240
|
console.log(chalk.blue(`\nTo start development, run:`));
|
|
187
241
|
let startCmd = `dhti-cli conch start -n ${flags.name} -w ${flags.workdir}`;
|
|
188
|
-
if (flags.sources) {
|
|
189
|
-
|
|
242
|
+
if (flags.sources && flags.sources.length > 0) {
|
|
243
|
+
for (const source of flags.sources) {
|
|
244
|
+
startCmd += ` -s '${source}'`;
|
|
245
|
+
}
|
|
190
246
|
}
|
|
191
247
|
console.log(chalk.cyan(` ${startCmd}`));
|
|
192
248
|
}
|
|
@@ -197,7 +253,7 @@ export default class Conch extends Command {
|
|
|
197
253
|
return;
|
|
198
254
|
}
|
|
199
255
|
// If no valid operation is provided
|
|
200
|
-
console.error(chalk.red('Error: Invalid operation. Use "install", "init", or "start"'));
|
|
256
|
+
console.error(chalk.red('Error: Invalid operation. Use "add", "install", "init", or "start"'));
|
|
201
257
|
this.exit(1);
|
|
202
258
|
}
|
|
203
259
|
}
|
package/dist/commands/docker.js
CHANGED
|
@@ -110,15 +110,20 @@ export default class Docker extends Command {
|
|
|
110
110
|
console.log('Please provide a valid path to bootstrap.py file');
|
|
111
111
|
this.exit(1);
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
// Determine copy direction based on whether local file exists
|
|
114
|
+
const fileExists = fs.existsSync(flags.file);
|
|
115
|
+
const copyCommand = fileExists
|
|
116
|
+
? `docker cp ${flags.file} ${flags.container}:/app/app/bootstrap.py`
|
|
117
|
+
: `docker cp ${flags.container}:/app/app/bootstrap.py ${flags.file}`;
|
|
114
118
|
const restartCommand = `docker restart ${flags.container}`;
|
|
115
119
|
if (flags['dry-run']) {
|
|
116
120
|
console.log(chalk.yellow('[DRY RUN] Would execute:'));
|
|
117
|
-
|
|
121
|
+
const direction = fileExists ? 'to container' : 'from container';
|
|
122
|
+
console.log(chalk.cyan(` ${copyCommand} (copy ${direction})`));
|
|
118
123
|
console.log(chalk.cyan(` ${restartCommand}`));
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
|
-
// copy
|
|
126
|
+
// copy file and only restart after copy completes
|
|
122
127
|
exec(copyCommand, (error, stdout, stderr) => {
|
|
123
128
|
if (error) {
|
|
124
129
|
console.error(`exec error: ${error}`);
|
package/dist/commands/elixir.js
CHANGED
|
@@ -299,43 +299,46 @@ export default class Elixir extends Command {
|
|
|
299
299
|
}
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
|
-
// Create a directory to install the elixir
|
|
303
|
-
|
|
302
|
+
// Create a directory to install the elixir (only on first install)
|
|
303
|
+
const elixirDir = `${flags.workdir}/elixir`;
|
|
304
|
+
const isFirstInstall = !fs.existsSync(elixirDir);
|
|
305
|
+
if (isFirstInstall) {
|
|
304
306
|
if (flags['dry-run']) {
|
|
305
|
-
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${
|
|
307
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${elixirDir}`));
|
|
308
|
+
console.log(chalk.yellow(`[DRY RUN] Would copy resources from ${RESOURCES_DIR}/genai to ${elixirDir}`));
|
|
306
309
|
}
|
|
307
310
|
else {
|
|
308
|
-
fs.mkdirSync(
|
|
311
|
+
fs.mkdirSync(elixirDir);
|
|
312
|
+
fs.cpSync(path.join(RESOURCES_DIR, 'genai'), elixirDir, { recursive: true });
|
|
313
|
+
console.log(chalk.blue(`✓ Initialized elixir directory at ${elixirDir}`));
|
|
309
314
|
}
|
|
310
315
|
}
|
|
311
|
-
if (
|
|
312
|
-
console.log(chalk.
|
|
316
|
+
else if (args.op === 'install') {
|
|
317
|
+
console.log(chalk.blue(`Using existing elixir directory at ${elixirDir}`));
|
|
313
318
|
}
|
|
314
|
-
|
|
315
|
-
fs.cpSync(path.join(RESOURCES_DIR, 'genai'), `${flags.workdir}/elixir`, { recursive: true });
|
|
316
|
-
}
|
|
317
|
-
// if whl is not none, copy the whl file to thee whl directory
|
|
319
|
+
// if whl is not none, copy the whl file to the whl directory
|
|
318
320
|
if (flags.whl !== 'none') {
|
|
319
|
-
|
|
321
|
+
const whlDir = `${elixirDir}/whl/`;
|
|
322
|
+
if (!fs.existsSync(whlDir)) {
|
|
320
323
|
if (flags['dry-run']) {
|
|
321
|
-
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${
|
|
324
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${whlDir}`));
|
|
322
325
|
}
|
|
323
326
|
else {
|
|
324
|
-
fs.mkdirSync(
|
|
327
|
+
fs.mkdirSync(whlDir);
|
|
325
328
|
}
|
|
326
329
|
}
|
|
327
330
|
if (flags['dry-run']) {
|
|
328
|
-
console.log(chalk.yellow(`[DRY RUN] Would copy ${flags.whl} to ${
|
|
329
|
-
console.log(chalk.cyan('[DRY RUN] Installing elixir from whl file. Please modify
|
|
331
|
+
console.log(chalk.yellow(`[DRY RUN] Would copy ${flags.whl} to ${whlDir}${path.basename(flags.whl)}`));
|
|
332
|
+
console.log(chalk.cyan('[DRY RUN] Installing elixir from whl file. Please modify bootstrap.py file if needed'));
|
|
330
333
|
}
|
|
331
334
|
else {
|
|
332
|
-
fs.cpSync(flags.whl, `${
|
|
333
|
-
console.log('Installing elixir from whl file. Please modify
|
|
335
|
+
fs.cpSync(flags.whl, `${whlDir}${path.basename(flags.whl)}`);
|
|
336
|
+
console.log('Installing elixir from whl file. Please modify bootstrap.py file if needed');
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
// Install the elixir from git adding to the pyproject.toml file
|
|
337
|
-
|
|
338
|
-
|
|
340
|
+
// Always read from the current state, not the template
|
|
341
|
+
let pyproject = flags['dry-run'] ? '' : fs.readFileSync(`${elixirDir}/pyproject.toml`, 'utf8');
|
|
339
342
|
let lineToAdd = '';
|
|
340
343
|
if (flags.whl !== 'none') {
|
|
341
344
|
lineToAdd = `${flags.name} = { file = "whl/${path.basename(flags.whl)}" }`;
|
|
@@ -366,11 +369,49 @@ export default class Elixir extends Command {
|
|
|
366
369
|
}
|
|
367
370
|
lineToAdd = `${flags.name} = { path = "${absolutePath}" }`;
|
|
368
371
|
}
|
|
372
|
+
// Helper function to add dependency to pyproject.toml
|
|
373
|
+
const addDependencyToPyproject = (content, depName) => {
|
|
374
|
+
// Check if dependency already exists
|
|
375
|
+
if (content.includes(`"${depName}"`)) {
|
|
376
|
+
return content;
|
|
377
|
+
}
|
|
378
|
+
// Add to dependencies array
|
|
379
|
+
return content.replace('dependencies = [', `dependencies = [\n"${depName}",`);
|
|
380
|
+
};
|
|
381
|
+
// Helper function to add source to pyproject.toml
|
|
382
|
+
const addSourceToPyproject = (content, source) => {
|
|
383
|
+
// Check if source already exists (by checking for the package name)
|
|
384
|
+
if (content.includes(`${flags.name} =`)) {
|
|
385
|
+
return content;
|
|
386
|
+
}
|
|
387
|
+
// Add to [tool.uv.sources] section
|
|
388
|
+
return content.replace('[tool.uv.sources]', `[tool.uv.sources]\n${source}\n`);
|
|
389
|
+
};
|
|
390
|
+
// Helper function to remove dependency from pyproject.toml
|
|
391
|
+
const removeDependencyFromPyproject = (content, depName) => {
|
|
392
|
+
// Remove from dependencies array - handle both formats: "depName", or "depName",\n
|
|
393
|
+
let result = content.replace(`"${depName}",`, '').replace(`"${depName}"`, '');
|
|
394
|
+
// Also try to remove with newline variations
|
|
395
|
+
result = result.replace(`\n"${depName}",`, '').replace(`"${depName}",\n`, '');
|
|
396
|
+
return result;
|
|
397
|
+
};
|
|
398
|
+
// Helper function to remove source from pyproject.toml
|
|
399
|
+
const removeSourceFromPyproject = (content, pkgName) => {
|
|
400
|
+
// Remove source line for this package
|
|
401
|
+
const sourceRegex = new RegExp(`${pkgName}\\s*=\\s*\\{[^}]*\\}\n?`, 'g');
|
|
402
|
+
return content.replace(sourceRegex, '');
|
|
403
|
+
};
|
|
404
|
+
let newPyproject = pyproject;
|
|
369
405
|
if (!flags['dry-run']) {
|
|
370
|
-
|
|
371
|
-
|
|
406
|
+
if (args.op === 'install') {
|
|
407
|
+
newPyproject = addDependencyToPyproject(pyproject, flags.name);
|
|
408
|
+
newPyproject = addSourceToPyproject(newPyproject, lineToAdd);
|
|
409
|
+
}
|
|
410
|
+
else if (args.op === 'uninstall') {
|
|
411
|
+
newPyproject = removeDependencyFromPyproject(pyproject, flags.name);
|
|
412
|
+
newPyproject = removeSourceFromPyproject(newPyproject, flags.name);
|
|
413
|
+
}
|
|
372
414
|
}
|
|
373
|
-
const newPyproject = pyproject;
|
|
374
415
|
// Add the elixir import and bootstrap to the server.py file
|
|
375
416
|
let CliImport = `from ${expoName}.bootstrap import bootstrap as ${expoName}_bootstrap\n`;
|
|
376
417
|
CliImport += `${expoName}_bootstrap()\n`;
|
|
@@ -380,54 +421,99 @@ ${expoName}_chain = ${expoName}_chain_class().get_chain_as_langchain_tool()
|
|
|
380
421
|
${expoName}_mcp_tool = ${expoName}_chain_class().get_chain_as_mcp_tool
|
|
381
422
|
mcp_server.add_tool(${expoName}_mcp_tool) # type: ignore
|
|
382
423
|
`;
|
|
383
|
-
let newCliImport = '';
|
|
384
|
-
if (!flags['dry-run']) {
|
|
385
|
-
newCliImport = fs
|
|
386
|
-
.readFileSync(`${flags.workdir}/elixir/app/server.py`, 'utf8')
|
|
387
|
-
.replace('# DHTI_CLI_IMPORT', `#DHTI_CLI_IMPORT\n${CliImport}`);
|
|
388
|
-
}
|
|
389
424
|
const langfuseRoute = `add_routes(app, ${expoName}_chain.with_config(config), path="/langserve/${expoName}")`;
|
|
390
|
-
const newLangfuseRoute = flags['dry-run']
|
|
391
|
-
? ''
|
|
392
|
-
: newCliImport.replace('# DHTI_LANGFUSE_ROUTE', `#DHTI_LANGFUSE_ROUTE\n ${langfuseRoute}`);
|
|
393
425
|
const normalRoute = `add_routes(app, ${expoName}_chain, path="/langserve/${expoName}")`;
|
|
394
|
-
const newNormalRoute = flags['dry-run']
|
|
395
|
-
? ''
|
|
396
|
-
: newLangfuseRoute.replace('# DHTI_NORMAL_ROUTE', `#DHTI_NORMAL_ROUTE\n ${normalRoute}`);
|
|
397
426
|
const commonRoutes = `\nadd_invokes(app, path="/langserve/${expoName}")\nadd_services(app, path="/langserve/${expoName}")`;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
427
|
+
// Helper function to add elixir to server.py
|
|
428
|
+
// The strategy is to append imports BEFORE the 'import uvicorn' line
|
|
429
|
+
// and append routes AFTER the route marker comments
|
|
430
|
+
const addElixirToServer = (elixirName) => {
|
|
431
|
+
// Read fresh file content each time to get the current state
|
|
432
|
+
const content = flags['dry-run'] ? '' : fs.readFileSync(`${elixirDir}/app/server.py`, 'utf8');
|
|
433
|
+
if (flags['dry-run'] || content === '') {
|
|
434
|
+
return '';
|
|
435
|
+
}
|
|
436
|
+
// Check if elixir already installed
|
|
437
|
+
if (content.includes(`${elixirName}_bootstrap`)) {
|
|
438
|
+
return content;
|
|
439
|
+
}
|
|
440
|
+
let result = content;
|
|
441
|
+
// Find where to insert the import - look for 'import uvicorn' and insert before it
|
|
442
|
+
// This way, all imports are together before uvicorn import
|
|
443
|
+
if (!result.includes('import uvicorn')) {
|
|
444
|
+
console.error(chalk.red('Error: Could not find "import uvicorn" marker in server.py'));
|
|
445
|
+
return content;
|
|
446
|
+
}
|
|
447
|
+
result = result.replace('import uvicorn', `${CliImport}\nimport uvicorn`);
|
|
448
|
+
// For routes, we need to insert into both langfuse and normal route sections
|
|
449
|
+
// But only if they exist
|
|
450
|
+
// Find the langfuse try block and insert before 'except'
|
|
451
|
+
if (result.includes('except:')) {
|
|
452
|
+
// Insert langfuse route before except with proper indentation
|
|
453
|
+
result = result.replace('except:', ` ${langfuseRoute}\n\nexcept:`);
|
|
454
|
+
// Insert normal route in the except block, after the '# DHTI_NORMAL_ROUTE' marker
|
|
455
|
+
result = result.replace('# DHTI_NORMAL_ROUTE\n', `# DHTI_NORMAL_ROUTE\n ${normalRoute}\n`);
|
|
456
|
+
}
|
|
457
|
+
// For common routes, look for the marker and append with proper indentation
|
|
458
|
+
const commonRoutesMarker = '# DHTI_COMMON_ROUTE';
|
|
459
|
+
if (result.includes(commonRoutesMarker)) {
|
|
460
|
+
// Get the exact indentation by checking what comes after the marker
|
|
461
|
+
const markerIndex = result.indexOf(commonRoutesMarker);
|
|
462
|
+
const afterMarker = result.substring(markerIndex + commonRoutesMarker.length);
|
|
463
|
+
const newlineAndIndent = afterMarker.match(/\n[ \t]*/)?.[0] || '\n';
|
|
464
|
+
result = result.replace(commonRoutesMarker, `${commonRoutesMarker}${newlineAndIndent}${commonRoutes}`);
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
};
|
|
468
|
+
// Helper function to remove elixir from server.py
|
|
469
|
+
const removeElixirFromServer = () => {
|
|
470
|
+
// Read fresh file content each time to get the current state
|
|
471
|
+
const content = flags['dry-run'] ? '' : fs.readFileSync(`${elixirDir}/app/server.py`, 'utf8');
|
|
472
|
+
if (flags['dry-run'] || content === '') {
|
|
473
|
+
return '';
|
|
474
|
+
}
|
|
475
|
+
let result = content;
|
|
476
|
+
result = result.replace(CliImport, '');
|
|
477
|
+
result = result.replace(langfuseRoute, '');
|
|
478
|
+
result = result.replace(normalRoute, '');
|
|
479
|
+
result = result.replace(commonRoutes, '');
|
|
480
|
+
return result;
|
|
481
|
+
};
|
|
482
|
+
let finalRoute = '';
|
|
483
|
+
if (!flags['dry-run'] && args.op === 'install') {
|
|
484
|
+
finalRoute = addElixirToServer(expoName);
|
|
485
|
+
}
|
|
486
|
+
else if (!flags['dry-run'] && args.op === 'uninstall') {
|
|
487
|
+
finalRoute = removeElixirFromServer();
|
|
488
|
+
}
|
|
402
489
|
if (args.op === 'install') {
|
|
403
490
|
if (flags['dry-run']) {
|
|
404
491
|
console.log(chalk.yellow('[DRY RUN] Would update files:'));
|
|
405
|
-
console.log(chalk.cyan(` - ${
|
|
492
|
+
console.log(chalk.cyan(` - ${elixirDir}/pyproject.toml`));
|
|
406
493
|
console.log(chalk.green(` Add dependency: "${flags.name}"`));
|
|
407
494
|
console.log(chalk.green(` Add source: ${lineToAdd}`));
|
|
408
|
-
console.log(chalk.cyan(` - ${
|
|
495
|
+
console.log(chalk.cyan(` - ${elixirDir}/app/server.py`));
|
|
409
496
|
console.log(chalk.green(` Add import and routes for ${expoName}`));
|
|
410
497
|
}
|
|
411
498
|
else {
|
|
412
|
-
fs.writeFileSync(`${
|
|
413
|
-
fs.writeFileSync(`${
|
|
499
|
+
fs.writeFileSync(`${elixirDir}/pyproject.toml`, newPyproject);
|
|
500
|
+
fs.writeFileSync(`${elixirDir}/app/server.py`, finalRoute);
|
|
501
|
+
console.log(chalk.green(`✓ Elixir '${flags.name}' installed successfully`));
|
|
414
502
|
}
|
|
415
503
|
}
|
|
416
504
|
if (args.op === 'uninstall') {
|
|
417
505
|
if (flags['dry-run']) {
|
|
418
506
|
console.log(chalk.yellow('[DRY RUN] Would update files:'));
|
|
419
|
-
console.log(chalk.cyan(` - ${
|
|
507
|
+
console.log(chalk.cyan(` - ${elixirDir}/pyproject.toml`));
|
|
508
|
+
console.log(chalk.green(` Remove dependency: "${flags.name}"`));
|
|
420
509
|
console.log(chalk.green(` Remove source: ${lineToAdd}`));
|
|
421
|
-
console.log(chalk.cyan(` - ${
|
|
510
|
+
console.log(chalk.cyan(` - ${elixirDir}/app/server.py`));
|
|
422
511
|
console.log(chalk.green(` Remove import and routes for ${expoName}`));
|
|
423
512
|
}
|
|
424
513
|
else {
|
|
425
|
-
|
|
426
|
-
fs.writeFileSync(`${
|
|
427
|
-
|
|
428
|
-
newServer = newServer.replace(langfuseRoute, '');
|
|
429
|
-
newServer = newServer.replace(normalRoute, '');
|
|
430
|
-
fs.writeFileSync(`${flags.workdir}/elixir/app/server.py`, newServer);
|
|
514
|
+
fs.writeFileSync(`${elixirDir}/pyproject.toml`, newPyproject);
|
|
515
|
+
fs.writeFileSync(`${elixirDir}/app/server.py`, finalRoute);
|
|
516
|
+
console.log(chalk.green(`✓ Elixir '${flags.name}' uninstalled successfully`));
|
|
431
517
|
}
|
|
432
518
|
}
|
|
433
519
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
services:
|
|
3
2
|
gateway:
|
|
4
3
|
image: beapen/dhti-gateway:latest
|
|
@@ -88,7 +87,6 @@ services:
|
|
|
88
87
|
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-openrouter-api-key}
|
|
89
88
|
- FHIR_BASE_URL=${FHIR_BASE_URL:-http://localhost:8080/openmrs/ws/fhir2/R4}
|
|
90
89
|
|
|
91
|
-
|
|
92
90
|
ollama:
|
|
93
91
|
image: ollama/ollama:latest
|
|
94
92
|
ports:
|
|
@@ -215,7 +213,7 @@ services:
|
|
|
215
213
|
environment:
|
|
216
214
|
- REDIS_HOSTS=local:redis:6379
|
|
217
215
|
ports:
|
|
218
|
-
- "
|
|
216
|
+
- "8181:8081"
|
|
219
217
|
|
|
220
218
|
neo4j:
|
|
221
219
|
image: neo4j:5.1-enterprise
|
|
@@ -357,6 +355,28 @@ services:
|
|
|
357
355
|
depends_on:
|
|
358
356
|
- medplum-server
|
|
359
357
|
|
|
358
|
+
orthanc:
|
|
359
|
+
image: orthancteam/orthanc:latest
|
|
360
|
+
ports:
|
|
361
|
+
# Orthanc web interface and DICOMweb port
|
|
362
|
+
- "8042:8042"
|
|
363
|
+
# DICOM store SCU/SCP port (default 4242 in container, 104 is standard, 8104 used in some examples)
|
|
364
|
+
- "8104:4242"
|
|
365
|
+
environment:
|
|
366
|
+
# Enable the web interface and remote access
|
|
367
|
+
- ORTHANC_JSON={"RemoteAccessAllowed":true,"AuthenticationEnabled":false,"HttpHeaders":{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"Content-Type"},"HttpRequestTimeout":60}
|
|
368
|
+
volumes:
|
|
369
|
+
# Persist the data (DICOM files and database)
|
|
370
|
+
- orthanc-data:/var/lib/orthanc/
|
|
371
|
+
restart: unless-stopped
|
|
372
|
+
|
|
373
|
+
cors-proxy:
|
|
374
|
+
image: redocly/cors-anywhere:latest
|
|
375
|
+
ports:
|
|
376
|
+
- "8010:8080"
|
|
377
|
+
restart: "unless-stopped"
|
|
378
|
+
|
|
379
|
+
|
|
360
380
|
volumes:
|
|
361
381
|
openmrs-data: ~
|
|
362
382
|
openmrs-db: ~
|
|
@@ -368,3 +388,4 @@ volumes:
|
|
|
368
388
|
ollama-root: ~
|
|
369
389
|
ollama-webui: ~
|
|
370
390
|
mcpx-config: ~
|
|
391
|
+
orthanc-data: ~
|
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
from kink import di
|
|
2
|
-
|
|
2
|
+
import os
|
|
3
3
|
from dotenv import load_dotenv
|
|
4
|
+
from langchain.chat_models import init_chat_model
|
|
5
|
+
from langchain_community.llms.fake import FakeListLLM
|
|
4
6
|
from langchain_core.prompts import PromptTemplate
|
|
5
|
-
from
|
|
7
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
8
|
+
from langchain_openai import ChatOpenAI
|
|
6
9
|
|
|
7
10
|
## Override the default configuration of elixirs here if needed
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
def bootstrap():
|
|
11
14
|
load_dotenv()
|
|
12
|
-
|
|
15
|
+
di["fhir_access_token"] = os.environ.get(
|
|
16
|
+
"FHIR_ACCESS_TOKEN", "YWRtaW46QWRtaW4xMjM="
|
|
17
|
+
) # admin:Admin123 in base64
|
|
18
|
+
di["fhir_base_url"] = os.environ.get(
|
|
19
|
+
"FHIR_BASE_URL", "http://backend:8080/openmrs/ws/fhir2/R4"
|
|
20
|
+
)
|
|
21
|
+
# Check if google api key is set in the environment
|
|
22
|
+
if os.environ.get("GOOGLE_API_KEY"):
|
|
23
|
+
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
|
|
24
|
+
# Check if openai api key is set in the environment
|
|
25
|
+
elif os.environ.get("OPENAI_API_KEY"):
|
|
26
|
+
llm = ChatOpenAI(model="gpt-4o", temperature=0)
|
|
27
|
+
else:
|
|
28
|
+
llm = FakeListLLM(responses=["I am a fake LLM", "I don't know"])
|
|
29
|
+
di["main_llm"] = llm
|
|
30
|
+
|
|
31
|
+
openrouter_api_key = os.environ.get("OPENROUTER_API_KEY")
|
|
32
|
+
if openrouter_api_key:
|
|
33
|
+
model = init_chat_model(
|
|
34
|
+
model="nvidia/nemotron-nano-9b-v2:free",
|
|
35
|
+
model_provider="openai",
|
|
36
|
+
base_url="https://openrouter.ai/api/v1",
|
|
37
|
+
api_key=openrouter_api_key,
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
# Fallback to the main LLM if no OpenRouter API key is configured
|
|
41
|
+
model = llm
|
|
42
|
+
|
|
43
|
+
di["function_llm"] = model
|
|
13
44
|
di["main_prompt"] = PromptTemplate.from_template(
|
|
14
45
|
"Summarize the following in 100 words: {input}"
|
|
15
46
|
)
|
|
16
|
-
di["main_llm"] = fake_llm
|
|
17
47
|
di["cds_hook_discovery"] = {
|
|
18
48
|
"services": [
|
|
19
49
|
{
|
package/oclif.manifest.json
CHANGED
|
@@ -87,15 +87,18 @@
|
|
|
87
87
|
"aliases": [],
|
|
88
88
|
"args": {
|
|
89
89
|
"op": {
|
|
90
|
-
"description": "Operation to perform (init, install, or start)",
|
|
90
|
+
"description": "Operation to perform (add, init, install, or start)",
|
|
91
91
|
"name": "op"
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"description": "Initialize, install, or start OpenMRS frontend development",
|
|
95
95
|
"examples": [
|
|
96
|
+
"<%= config.bin %> <%= command.id %> add -g my-repo/my-package -n my-package -w ~/projects",
|
|
97
|
+
"<%= config.bin %> <%= command.id %> add -g my-repo/my-package -b main -n my-package -w ~/projects",
|
|
96
98
|
"<%= config.bin %> <%= command.id %> install -n my-app -w ~/projects",
|
|
97
99
|
"<%= config.bin %> <%= command.id %> init -n my-app -w ~/projects",
|
|
98
|
-
"<%= config.bin %> <%= command.id %> start -n my-app -w ~/projects"
|
|
100
|
+
"<%= config.bin %> <%= command.id %> start -n my-app -w ~/projects",
|
|
101
|
+
"<%= config.bin %> <%= command.id %> start -n my-app -w ~/projects -s packages/chatbot -s packages/utils"
|
|
99
102
|
],
|
|
100
103
|
"flags": {
|
|
101
104
|
"branch": {
|
|
@@ -117,7 +120,15 @@
|
|
|
117
120
|
"char": "g",
|
|
118
121
|
"description": "GitHub repository to install (for install operation)",
|
|
119
122
|
"name": "git",
|
|
120
|
-
"default": "dermatologist/openmrs-esm-dhti
|
|
123
|
+
"default": "dermatologist/openmrs-esm-dhti",
|
|
124
|
+
"hasDynamicHelp": false,
|
|
125
|
+
"multiple": false,
|
|
126
|
+
"type": "option"
|
|
127
|
+
},
|
|
128
|
+
"local": {
|
|
129
|
+
"char": "l",
|
|
130
|
+
"description": "Local path to use instead of calculated workdir/name path (for start operation)",
|
|
131
|
+
"name": "local",
|
|
121
132
|
"hasDynamicHelp": false,
|
|
122
133
|
"multiple": false,
|
|
123
134
|
"type": "option"
|
|
@@ -126,16 +137,17 @@
|
|
|
126
137
|
"char": "n",
|
|
127
138
|
"description": "Name of the conch",
|
|
128
139
|
"name": "name",
|
|
140
|
+
"default": "esm-dhti",
|
|
129
141
|
"hasDynamicHelp": false,
|
|
130
142
|
"multiple": false,
|
|
131
143
|
"type": "option"
|
|
132
144
|
},
|
|
133
145
|
"sources": {
|
|
134
146
|
"char": "s",
|
|
135
|
-
"description": "Additional sources to include when starting (e.g., packages/esm-chatbot-agent)",
|
|
147
|
+
"description": "Additional sources to include when starting (e.g., packages/esm-chatbot-agent, packages/esm-another-app)",
|
|
136
148
|
"name": "sources",
|
|
137
149
|
"hasDynamicHelp": false,
|
|
138
|
-
"multiple":
|
|
150
|
+
"multiple": true,
|
|
139
151
|
"type": "option"
|
|
140
152
|
},
|
|
141
153
|
"workdir": {
|
|
@@ -796,5 +808,5 @@
|
|
|
796
808
|
]
|
|
797
809
|
}
|
|
798
810
|
},
|
|
799
|
-
"version": "1.0
|
|
811
|
+
"version": "1.2.0"
|
|
800
812
|
}
|