dhti-cli 0.8.0 → 1.1.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 +83 -111
- package/dist/commands/compose.d.ts +4 -0
- package/dist/commands/compose.js +119 -0
- package/dist/commands/conch.d.ts +2 -6
- package/dist/commands/conch.js +167 -215
- package/dist/commands/elixir.d.ts +2 -0
- package/dist/commands/elixir.js +346 -45
- package/dist/resources/docker-compose-master.yml +4 -2
- package/dist/resources/spa/README.md +3 -0
- package/dist/resources/spa/config-schema.ts +53 -0
- package/dist/resources/spa/glycemic.component.tsx +86 -0
- package/dist/resources/spa/glycemic.scss +44 -0
- package/dist/resources/spa/hooks/useDhti.ts +69 -0
- package/dist/resources/spa/hooks/usePatient.ts +61 -0
- package/dist/resources/spa/index.ts +17 -0
- package/dist/resources/spa/models/card.ts +80 -0
- package/dist/resources/spa/models/request.ts +42 -0
- package/dist/resources/spa/routes.json +17 -0
- package/oclif.manifest.json +66 -53
- package/package.json +3 -1
- package/dist/resources/spa/Dockerfile +0 -16
- package/dist/resources/spa/def/importmap.json +0 -39
- package/dist/resources/spa/def/routes.registry.json +0 -1599
- package/dist/resources/spa/def/spa-assemble-config.json +0 -40
package/dist/commands/elixir.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
// eslint-disable-next-line import/default
|
|
4
|
+
import yaml from 'js-yaml';
|
|
3
5
|
import { exec } from 'node:child_process';
|
|
4
6
|
import fs from 'node:fs';
|
|
5
7
|
import os from 'node:os';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
const execAsync = promisify(exec);
|
|
8
12
|
export default class Elixir extends Command {
|
|
9
13
|
static args = {
|
|
10
|
-
op: Args.string({ description: 'Operation to perform (install, uninstall or
|
|
14
|
+
op: Args.string({ description: 'Operation to perform (init, install, uninstall, dev or start)' }),
|
|
11
15
|
};
|
|
12
16
|
static description = 'Install or uninstall elixirs to create a Docker image';
|
|
13
17
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
@@ -23,6 +27,15 @@ export default class Elixir extends Command {
|
|
|
23
27
|
default: false,
|
|
24
28
|
description: 'Show what changes would be made without actually making them',
|
|
25
29
|
}),
|
|
30
|
+
elixir: Flags.string({
|
|
31
|
+
char: 'e',
|
|
32
|
+
description: 'Elixir endpoint URL',
|
|
33
|
+
}),
|
|
34
|
+
fhir: Flags.string({
|
|
35
|
+
char: 'f',
|
|
36
|
+
default: 'http://hapi.fhir.org/baseR4',
|
|
37
|
+
description: 'FHIR endpoint URL',
|
|
38
|
+
}),
|
|
26
39
|
git: Flags.string({ char: 'g', default: 'none', description: 'Github repository to install' }),
|
|
27
40
|
local: Flags.string({ char: 'l', default: 'none', description: 'Local directory to install from' }),
|
|
28
41
|
name: Flags.string({ char: 'n', description: 'Name of the elixir' }),
|
|
@@ -50,6 +63,202 @@ export default class Elixir extends Command {
|
|
|
50
63
|
const __filename = fileURLToPath(import.meta.url);
|
|
51
64
|
const __dirname = path.dirname(__filename);
|
|
52
65
|
const RESOURCES_DIR = path.resolve(__dirname, '../resources');
|
|
66
|
+
// Handle init operation
|
|
67
|
+
if (args.op === 'init') {
|
|
68
|
+
// Validate required flags
|
|
69
|
+
if (!flags.workdir) {
|
|
70
|
+
console.error(chalk.red('Error: workdir flag is required for init operation'));
|
|
71
|
+
this.exit(1);
|
|
72
|
+
}
|
|
73
|
+
if (!flags.name) {
|
|
74
|
+
console.error(chalk.red('Error: name flag is required for init operation'));
|
|
75
|
+
this.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const targetDir = path.join(flags.workdir, 'dhti-elixir');
|
|
78
|
+
if (flags['dry-run']) {
|
|
79
|
+
console.log(chalk.yellow('[DRY RUN] Would execute init operation:'));
|
|
80
|
+
console.log(chalk.cyan(` npx degit dermatologist/dhti-elixir ${targetDir}`));
|
|
81
|
+
console.log(chalk.cyan(` Copy ${targetDir}/packages/starter to ${targetDir}/packages/${flags.name}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
// Run npx degit to clone the dhti-elixir template
|
|
86
|
+
console.log(chalk.blue(`Initializing DHTI elixir template in ${targetDir}...`));
|
|
87
|
+
const degitCommand = `npx degit dermatologist/dhti-elixir ${targetDir}`;
|
|
88
|
+
await execAsync(degitCommand);
|
|
89
|
+
console.log(chalk.green('✓ DHTI elixir template cloned successfully'));
|
|
90
|
+
// Copy packages/starter subdirectory to packages/<name>
|
|
91
|
+
const simpleChatSource = path.join(targetDir, 'packages', 'starter');
|
|
92
|
+
const targetPackageDir = path.join(targetDir, 'packages', flags.name);
|
|
93
|
+
if (fs.existsSync(simpleChatSource)) {
|
|
94
|
+
console.log(chalk.blue(`Copying starter to packages/${flags.name}...`));
|
|
95
|
+
fs.cpSync(simpleChatSource, targetPackageDir, { recursive: true });
|
|
96
|
+
console.log(chalk.green(`✓ starter copied to packages/${flags.name}`));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(chalk.yellow(`Warning: starter not found at ${simpleChatSource}`));
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk.green(`\n✓ Initialization complete! Your elixir workspace is ready at ${targetDir}`));
|
|
102
|
+
console.log(chalk.blue(`\nNext steps:`));
|
|
103
|
+
console.log(chalk.cyan(` 1. cd ${targetDir}`));
|
|
104
|
+
console.log(chalk.cyan(` 2. Follow the README.md for development instructions`));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error(chalk.red('Error during initialization:'), error);
|
|
108
|
+
this.exit(1);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Handle start operation
|
|
113
|
+
if (args.op === 'start') {
|
|
114
|
+
// Determine the elixir endpoint URL
|
|
115
|
+
let elixirUrl = flags.elixir;
|
|
116
|
+
if (!elixirUrl) {
|
|
117
|
+
// If --elixir is not provided, construct it from --name
|
|
118
|
+
if (!flags.name) {
|
|
119
|
+
console.error(chalk.red('Error: Either --elixir or --name flag must be provided for start operation'));
|
|
120
|
+
this.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const nameWithUnderscores = flags.name.replaceAll('-', '_');
|
|
123
|
+
elixirUrl = `http://localhost:8001/langserve/${nameWithUnderscores}/cds-services`;
|
|
124
|
+
}
|
|
125
|
+
const sandboxDir = path.join(flags.workdir, 'cds-hooks-sandbox');
|
|
126
|
+
if (flags['dry-run']) {
|
|
127
|
+
const directoryExists = fs.existsSync(sandboxDir);
|
|
128
|
+
console.log(chalk.yellow('[DRY RUN] Would execute start operation:'));
|
|
129
|
+
if (directoryExists) {
|
|
130
|
+
console.log(chalk.cyan(` [SKIP] Directory already exists: ${sandboxDir}`));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk.cyan(` npx degit dermatologist/cds-hooks-sandbox ${sandboxDir}`));
|
|
134
|
+
console.log(chalk.cyan(` cd ${sandboxDir}`));
|
|
135
|
+
console.log(chalk.cyan(` yarn install`));
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.cyan(` yarn dhti ${elixirUrl} ${flags.fhir}`));
|
|
138
|
+
console.log(chalk.cyan(` Update docker-compose.yml FHIR_BASE_URL=${flags.fhir}`));
|
|
139
|
+
console.log(chalk.cyan(` docker restart ${flags.container}`));
|
|
140
|
+
console.log(chalk.cyan(` yarn dev`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
// Check if the directory already exists
|
|
145
|
+
const directoryExists = fs.existsSync(sandboxDir);
|
|
146
|
+
if (directoryExists) {
|
|
147
|
+
console.log(chalk.blue(`Using existing CDS Hooks Sandbox at ${sandboxDir}...`));
|
|
148
|
+
console.log(chalk.green('✓ Skipped clone and install (directory already exists)'));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Clone the cds-hooks-sandbox repository
|
|
152
|
+
console.log(chalk.blue(`Cloning CDS Hooks Sandbox to ${sandboxDir}...`));
|
|
153
|
+
const degitCommand = `npx degit dermatologist/cds-hooks-sandbox ${sandboxDir}`;
|
|
154
|
+
await execAsync(degitCommand);
|
|
155
|
+
console.log(chalk.green('✓ CDS Hooks Sandbox cloned successfully'));
|
|
156
|
+
// Install dependencies
|
|
157
|
+
console.log(chalk.blue('Installing dependencies...'));
|
|
158
|
+
const installCommand = `cd ${sandboxDir} && yarn install`;
|
|
159
|
+
const { stderr: installError } = await execAsync(installCommand);
|
|
160
|
+
if (installError && !installError.includes('warning')) {
|
|
161
|
+
console.error(chalk.yellow(`Installation warnings: ${installError}`));
|
|
162
|
+
}
|
|
163
|
+
console.log(chalk.green('✓ Dependencies installed successfully'));
|
|
164
|
+
}
|
|
165
|
+
// Configure dhti endpoints
|
|
166
|
+
console.log(chalk.blue('Configuring DHTI endpoints...'));
|
|
167
|
+
const dhtiCommand = `cd ${sandboxDir} && yarn dhti ${elixirUrl} ${flags.fhir}`;
|
|
168
|
+
const { stderr: dhtiError } = await execAsync(dhtiCommand);
|
|
169
|
+
if (dhtiError && !dhtiError.includes('warning')) {
|
|
170
|
+
console.error(chalk.yellow(`Configuration warnings: ${dhtiError}`));
|
|
171
|
+
}
|
|
172
|
+
console.log(chalk.green('✓ DHTI endpoints configured successfully'));
|
|
173
|
+
// Configure Docker container with FHIR_BASE_URL environment variable
|
|
174
|
+
console.log(chalk.blue('Setting up Docker container environment...'));
|
|
175
|
+
try {
|
|
176
|
+
// Update the docker-compose.yml file with the new FHIR_BASE_URL
|
|
177
|
+
const dockerComposeFile = path.join(flags.workdir, 'docker-compose.yml');
|
|
178
|
+
if (fs.existsSync(dockerComposeFile)) {
|
|
179
|
+
const composeContent = fs.readFileSync(dockerComposeFile, 'utf8');
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
const compose = yaml.load(composeContent);
|
|
182
|
+
if (compose.services && compose.services.langserve) {
|
|
183
|
+
if (!compose.services.langserve.environment) {
|
|
184
|
+
compose.services.langserve.environment = [];
|
|
185
|
+
}
|
|
186
|
+
// Update or add the FHIR_BASE_URL environment variable
|
|
187
|
+
const envArray = compose.services.langserve.environment;
|
|
188
|
+
const fhirIndex = envArray.findIndex((env) => {
|
|
189
|
+
if (typeof env === 'string') {
|
|
190
|
+
return env.startsWith('FHIR_BASE_URL=');
|
|
191
|
+
}
|
|
192
|
+
return typeof env === 'object' && env !== null && 'FHIR_BASE_URL' in env;
|
|
193
|
+
});
|
|
194
|
+
if (fhirIndex >= 0) {
|
|
195
|
+
// Update existing environment variable
|
|
196
|
+
const existingEnv = envArray[fhirIndex];
|
|
197
|
+
if (typeof existingEnv === 'string') {
|
|
198
|
+
envArray[fhirIndex] = `FHIR_BASE_URL=${flags.fhir}`;
|
|
199
|
+
}
|
|
200
|
+
else if (typeof existingEnv === 'object' && existingEnv !== null) {
|
|
201
|
+
existingEnv.FHIR_BASE_URL = flags.fhir;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Add new environment variable
|
|
206
|
+
envArray.push(`FHIR_BASE_URL=${flags.fhir}`);
|
|
207
|
+
}
|
|
208
|
+
const updatedCompose = yaml.dump(compose);
|
|
209
|
+
fs.writeFileSync(dockerComposeFile, updatedCompose);
|
|
210
|
+
console.log(chalk.green(`✓ docker-compose.yml updated with FHIR_BASE_URL=${flags.fhir}`));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.warn(chalk.yellow('⚠ Warning: langserve service not found in docker-compose.yml'));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
console.warn(chalk.yellow(`⚠ Warning: docker-compose.yml not found at ${dockerComposeFile}`));
|
|
218
|
+
}
|
|
219
|
+
// Restart the container to apply the new environment variables
|
|
220
|
+
const upCommand = `docker compose up -d`; // Docker Compose is smart enough to re-create only the services where the configuration, including environment variables, has changed
|
|
221
|
+
await execAsync(upCommand, { cwd: flags.workdir });
|
|
222
|
+
console.log(chalk.green(`✓ Docker container ${flags.container} restarted (compose up -d) successfully`));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const err = error;
|
|
226
|
+
if (err.message?.includes('No such container')) {
|
|
227
|
+
console.warn(chalk.yellow(`⚠ Warning: Docker container ${flags.container} not found. Skipping container restart.`));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.error(chalk.red(`Error setting up Docker container: ${err.message}`));
|
|
231
|
+
this.exit(1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Start the development server
|
|
235
|
+
console.log(chalk.blue('Starting development server...'));
|
|
236
|
+
const { spawn } = await import('node:child_process');
|
|
237
|
+
const devCommand = `yarn dhti ${elixirUrl} ${flags.fhir} && yarn dev`;
|
|
238
|
+
try {
|
|
239
|
+
const child = spawn(devCommand, { cwd: sandboxDir, shell: true, stdio: 'inherit' });
|
|
240
|
+
await new Promise((resolve, reject) => {
|
|
241
|
+
child.on('exit', (code) => {
|
|
242
|
+
if (code === 0)
|
|
243
|
+
resolve(undefined);
|
|
244
|
+
else
|
|
245
|
+
reject(new Error(`Dev server exited with code ${code}`));
|
|
246
|
+
});
|
|
247
|
+
child.on('error', reject);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
const err = error;
|
|
252
|
+
console.error(chalk.red('Error starting development server:'), err.message);
|
|
253
|
+
this.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
console.error(chalk.red('Error during start operation:'), error);
|
|
258
|
+
this.exit(1);
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
53
262
|
if (!flags.name) {
|
|
54
263
|
console.log('Please provide a name for the elixir');
|
|
55
264
|
this.exit(1);
|
|
@@ -90,43 +299,46 @@ export default class Elixir extends Command {
|
|
|
90
299
|
}
|
|
91
300
|
return;
|
|
92
301
|
}
|
|
93
|
-
// Create a directory to install the elixir
|
|
94
|
-
|
|
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) {
|
|
95
306
|
if (flags['dry-run']) {
|
|
96
|
-
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}`));
|
|
97
309
|
}
|
|
98
310
|
else {
|
|
99
|
-
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}`));
|
|
100
314
|
}
|
|
101
315
|
}
|
|
102
|
-
if (
|
|
103
|
-
console.log(chalk.
|
|
316
|
+
else if (args.op === 'install') {
|
|
317
|
+
console.log(chalk.blue(`Using existing elixir directory at ${elixirDir}`));
|
|
104
318
|
}
|
|
105
|
-
|
|
106
|
-
fs.cpSync(path.join(RESOURCES_DIR, 'genai'), `${flags.workdir}/elixir`, { recursive: true });
|
|
107
|
-
}
|
|
108
|
-
// 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
|
|
109
320
|
if (flags.whl !== 'none') {
|
|
110
|
-
|
|
321
|
+
const whlDir = `${elixirDir}/whl/`;
|
|
322
|
+
if (!fs.existsSync(whlDir)) {
|
|
111
323
|
if (flags['dry-run']) {
|
|
112
|
-
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${
|
|
324
|
+
console.log(chalk.yellow(`[DRY RUN] Would create directory: ${whlDir}`));
|
|
113
325
|
}
|
|
114
326
|
else {
|
|
115
|
-
fs.mkdirSync(
|
|
327
|
+
fs.mkdirSync(whlDir);
|
|
116
328
|
}
|
|
117
329
|
}
|
|
118
330
|
if (flags['dry-run']) {
|
|
119
|
-
console.log(chalk.yellow(`[DRY RUN] Would copy ${flags.whl} to ${
|
|
120
|
-
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'));
|
|
121
333
|
}
|
|
122
334
|
else {
|
|
123
|
-
fs.cpSync(flags.whl, `${
|
|
124
|
-
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');
|
|
125
337
|
}
|
|
126
338
|
}
|
|
127
339
|
// Install the elixir from git adding to the pyproject.toml file
|
|
128
|
-
|
|
129
|
-
|
|
340
|
+
// Always read from the current state, not the template
|
|
341
|
+
let pyproject = flags['dry-run'] ? '' : fs.readFileSync(`${elixirDir}/pyproject.toml`, 'utf8');
|
|
130
342
|
let lineToAdd = '';
|
|
131
343
|
if (flags.whl !== 'none') {
|
|
132
344
|
lineToAdd = `${flags.name} = { file = "whl/${path.basename(flags.whl)}" }`;
|
|
@@ -157,11 +369,49 @@ export default class Elixir extends Command {
|
|
|
157
369
|
}
|
|
158
370
|
lineToAdd = `${flags.name} = { path = "${absolutePath}" }`;
|
|
159
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;
|
|
160
405
|
if (!flags['dry-run']) {
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
|
163
414
|
}
|
|
164
|
-
const newPyproject = pyproject;
|
|
165
415
|
// Add the elixir import and bootstrap to the server.py file
|
|
166
416
|
let CliImport = `from ${expoName}.bootstrap import bootstrap as ${expoName}_bootstrap\n`;
|
|
167
417
|
CliImport += `${expoName}_bootstrap()\n`;
|
|
@@ -171,48 +421,99 @@ ${expoName}_chain = ${expoName}_chain_class().get_chain_as_langchain_tool()
|
|
|
171
421
|
${expoName}_mcp_tool = ${expoName}_chain_class().get_chain_as_mcp_tool
|
|
172
422
|
mcp_server.add_tool(${expoName}_mcp_tool) # type: ignore
|
|
173
423
|
`;
|
|
174
|
-
let newCliImport = '';
|
|
175
|
-
if (!flags['dry-run']) {
|
|
176
|
-
newCliImport = fs
|
|
177
|
-
.readFileSync(`${flags.workdir}/elixir/app/server.py`, 'utf8')
|
|
178
|
-
.replace('# DHTI_CLI_IMPORT', `#DHTI_CLI_IMPORT\n${CliImport}`);
|
|
179
|
-
}
|
|
180
424
|
const langfuseRoute = `add_routes(app, ${expoName}_chain.with_config(config), path="/langserve/${expoName}")`;
|
|
181
|
-
const newLangfuseRoute = flags['dry-run'] ? '' : newCliImport.replace('# DHTI_LANGFUSE_ROUTE', `#DHTI_LANGFUSE_ROUTE\n ${langfuseRoute}`);
|
|
182
425
|
const normalRoute = `add_routes(app, ${expoName}_chain, path="/langserve/${expoName}")`;
|
|
183
|
-
const newNormalRoute = flags['dry-run'] ? '' : newLangfuseRoute.replace('# DHTI_NORMAL_ROUTE', `#DHTI_NORMAL_ROUTE\n ${normalRoute}`);
|
|
184
426
|
const commonRoutes = `\nadd_invokes(app, path="/langserve/${expoName}")\nadd_services(app, path="/langserve/${expoName}")`;
|
|
185
|
-
|
|
186
|
-
//
|
|
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
|
+
}
|
|
187
489
|
if (args.op === 'install') {
|
|
188
490
|
if (flags['dry-run']) {
|
|
189
491
|
console.log(chalk.yellow('[DRY RUN] Would update files:'));
|
|
190
|
-
console.log(chalk.cyan(` - ${
|
|
492
|
+
console.log(chalk.cyan(` - ${elixirDir}/pyproject.toml`));
|
|
191
493
|
console.log(chalk.green(` Add dependency: "${flags.name}"`));
|
|
192
494
|
console.log(chalk.green(` Add source: ${lineToAdd}`));
|
|
193
|
-
console.log(chalk.cyan(` - ${
|
|
495
|
+
console.log(chalk.cyan(` - ${elixirDir}/app/server.py`));
|
|
194
496
|
console.log(chalk.green(` Add import and routes for ${expoName}`));
|
|
195
497
|
}
|
|
196
498
|
else {
|
|
197
|
-
fs.writeFileSync(`${
|
|
198
|
-
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`));
|
|
199
502
|
}
|
|
200
503
|
}
|
|
201
504
|
if (args.op === 'uninstall') {
|
|
202
505
|
if (flags['dry-run']) {
|
|
203
506
|
console.log(chalk.yellow('[DRY RUN] Would update files:'));
|
|
204
|
-
console.log(chalk.cyan(` - ${
|
|
507
|
+
console.log(chalk.cyan(` - ${elixirDir}/pyproject.toml`));
|
|
508
|
+
console.log(chalk.green(` Remove dependency: "${flags.name}"`));
|
|
205
509
|
console.log(chalk.green(` Remove source: ${lineToAdd}`));
|
|
206
|
-
console.log(chalk.cyan(` - ${
|
|
510
|
+
console.log(chalk.cyan(` - ${elixirDir}/app/server.py`));
|
|
207
511
|
console.log(chalk.green(` Remove import and routes for ${expoName}`));
|
|
208
512
|
}
|
|
209
513
|
else {
|
|
210
|
-
|
|
211
|
-
fs.writeFileSync(`${
|
|
212
|
-
|
|
213
|
-
newServer = newServer.replace(langfuseRoute, '');
|
|
214
|
-
newServer = newServer.replace(normalRoute, '');
|
|
215
|
-
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`));
|
|
216
517
|
}
|
|
217
518
|
}
|
|
218
519
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
version: "3.7"
|
|
2
1
|
|
|
3
2
|
services:
|
|
4
3
|
gateway:
|
|
@@ -72,6 +71,7 @@ services:
|
|
|
72
71
|
|
|
73
72
|
langserve:
|
|
74
73
|
image: beapen/genai:latest
|
|
74
|
+
network_mode: host
|
|
75
75
|
pull_policy: always
|
|
76
76
|
ports:
|
|
77
77
|
- "8001:8001"
|
|
@@ -86,6 +86,8 @@ services:
|
|
|
86
86
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-openai-api-key}
|
|
87
87
|
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://openrouter.ai/api/v1}
|
|
88
88
|
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-openrouter-api-key}
|
|
89
|
+
- FHIR_BASE_URL=${FHIR_BASE_URL:-http://localhost:8080/openmrs/ws/fhir2/R4}
|
|
90
|
+
|
|
89
91
|
|
|
90
92
|
ollama:
|
|
91
93
|
image: ollama/ollama:latest
|
|
@@ -213,7 +215,7 @@ services:
|
|
|
213
215
|
environment:
|
|
214
216
|
- REDIS_HOSTS=local:redis:6379
|
|
215
217
|
ports:
|
|
216
|
-
- "
|
|
218
|
+
- "8181:8081"
|
|
217
219
|
|
|
218
220
|
neo4j:
|
|
219
221
|
image: neo4j:5.1-enterprise
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {Type, validator} from '@openmrs/esm-framework'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is the config schema. It expects a configuration object which
|
|
5
|
+
* looks like this:
|
|
6
|
+
*
|
|
7
|
+
* ```json
|
|
8
|
+
* { "casualGreeting": true, "whoToGreet": ["Mom"] }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* In OpenMRS Microfrontends, all config parameters are optional. Thus,
|
|
12
|
+
* all elements must have a reasonable default. A good default is one
|
|
13
|
+
* that works well with the reference application.
|
|
14
|
+
*
|
|
15
|
+
* To understand the schema below, please read the configuration system
|
|
16
|
+
* documentation:
|
|
17
|
+
* https://openmrs.github.io/openmrs-esm-core/#/main/config
|
|
18
|
+
* Note especially the section "How do I make my module configurable?"
|
|
19
|
+
* https://openmrs.github.io/openmrs-esm-core/#/main/config?id=im-developing-an-esm-module-how-do-i-make-it-configurable
|
|
20
|
+
* and the Schema Reference
|
|
21
|
+
* https://openmrs.github.io/openmrs-esm-core/#/main/config?id=schema-reference
|
|
22
|
+
*/
|
|
23
|
+
export const configSchema = {
|
|
24
|
+
casualGreeting: {
|
|
25
|
+
_type: Type.Boolean,
|
|
26
|
+
_default: false,
|
|
27
|
+
_description: 'Whether to use a casual greeting (or a formal one).',
|
|
28
|
+
},
|
|
29
|
+
dhtiTitle: {
|
|
30
|
+
_type: Type.String,
|
|
31
|
+
_default: 'DHTI Template Widget',
|
|
32
|
+
_description: 'Title to display for the DHTI Template Widget',
|
|
33
|
+
},
|
|
34
|
+
dhtiRoute: {
|
|
35
|
+
_type: Type.String,
|
|
36
|
+
_default: 'http://localhost:8001/langserve/dhti_elixir_template/cds-services/dhti-service',
|
|
37
|
+
_description: 'Route for the DHTI Template Service',
|
|
38
|
+
},
|
|
39
|
+
whoToGreet: {
|
|
40
|
+
_type: Type.Array,
|
|
41
|
+
_default: ['World'],
|
|
42
|
+
_description: 'Who should be greeted. Names will be separated by a comma and space.',
|
|
43
|
+
_elements: {
|
|
44
|
+
_type: Type.String,
|
|
45
|
+
},
|
|
46
|
+
_validators: [validator((v) => v.length > 0, 'At least one person must be greeted.')],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Config = {
|
|
51
|
+
casualGreeting: boolean
|
|
52
|
+
whoToGreet: Array<string>
|
|
53
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useConfig, openmrsFetch, fhirBaseUrl } from '@openmrs/esm-framework';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
import styles from './glycemic.scss';
|
|
5
|
+
import { useDhti } from './hooks/useDhti';
|
|
6
|
+
|
|
7
|
+
interface GlycemicWidgetProps {
|
|
8
|
+
patientUuid: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const GlycemicWidget: React.FC<GlycemicWidgetProps> = ({ patientUuid }) => {
|
|
12
|
+
const config = useConfig();
|
|
13
|
+
const serviceName = config?.dhtiServiceName || 'dhti_elixir_glycemic';
|
|
14
|
+
const { submitMessage, loading: aiLoading, error: aiError } = useDhti();
|
|
15
|
+
const [aiResponse, setAiResponse] = useState<any>(null);
|
|
16
|
+
|
|
17
|
+
// Check for Diabetes (SNOMED 44054006)
|
|
18
|
+
const { data: conditionsData, isLoading: conditionsLoading } = useSWR<any>(
|
|
19
|
+
patientUuid ? `${fhirBaseUrl}/Condition?patient=${patientUuid}&code=44054006` : null,
|
|
20
|
+
openmrsFetch
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const isDiabetic = conditionsData?.data?.entry?.length > 0;
|
|
24
|
+
|
|
25
|
+
// Fetch HbA1c (4548-4) and Glucose (2339-0) observations
|
|
26
|
+
const { data: obsData, isLoading: obsLoading } = useSWR<any>(
|
|
27
|
+
isDiabetic ? `${fhirBaseUrl}/Observation?patient=${patientUuid}&code=4548-4,2339-0&_sort=-date&_count=5` : null,
|
|
28
|
+
openmrsFetch
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!isDiabetic && !aiResponse && !aiLoading && !aiError) {
|
|
33
|
+
submitMessage('Analyze glycemic control', patientUuid).then(setAiResponse);
|
|
34
|
+
}
|
|
35
|
+
}, [isDiabetic, patientUuid, submitMessage, aiResponse, aiLoading, aiError]);
|
|
36
|
+
|
|
37
|
+
// if (conditionsLoading) return null;
|
|
38
|
+
// if (!isDiabetic) return null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={styles.container}>
|
|
42
|
+
<div className={styles.header}>
|
|
43
|
+
<h3>{config?.dhtiTitle || 'DHTI Glycemic Widget'}</h3>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className={styles.content}>
|
|
47
|
+
<div className={styles.observations}>
|
|
48
|
+
<h4>Latest Readings</h4>
|
|
49
|
+
{obsLoading ? (
|
|
50
|
+
<p>Loading observations...</p>
|
|
51
|
+
) : obsData?.data?.entry?.length > 0 ? (
|
|
52
|
+
<ul>
|
|
53
|
+
{obsData.data.entry.map((entry: any) => {
|
|
54
|
+
const obs = entry.resource;
|
|
55
|
+
const value = obs.valueQuantity ? `${obs.valueQuantity.value} ${obs.valueQuantity.unit}` : obs.valueString;
|
|
56
|
+
const date = new Date(obs.effectiveDateTime).toLocaleDateString();
|
|
57
|
+
const name = obs.code?.text || obs.code?.coding?.[0]?.display || 'Observation';
|
|
58
|
+
return (
|
|
59
|
+
<li key={obs.id}>
|
|
60
|
+
<strong>{name}:</strong> {value} <small>({date})</small>
|
|
61
|
+
</li>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</ul>
|
|
65
|
+
) : (
|
|
66
|
+
<p>No recent HbA1c or Glucose readings found.</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className={styles.aiInsights}>
|
|
71
|
+
<h4>GenAI Interpretation</h4>
|
|
72
|
+
{aiLoading && <p>Generating insights...</p>}
|
|
73
|
+
{aiError && <p className={styles.error}>Error: {aiError}</p>}
|
|
74
|
+
{aiResponse && (
|
|
75
|
+
<div className={styles.aiContent}>
|
|
76
|
+
<p>{aiResponse.summary}</p>
|
|
77
|
+
{aiResponse.detail && <p><small>{aiResponse.detail}</small></p>}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default GlycemicWidget;
|