dhti-cli 0.1.0 → 0.1.1

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.
@@ -0,0 +1,156 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ export default class Mimic extends Command {
3
+ static args = {
4
+ server: Args.string({ default: 'http://localhost/fhir/$import', description: 'Server URL to submit' }), // object with input, instruction (rationale in distillation), output
5
+ };
6
+ static description = 'Submit a FHIR request to a server';
7
+ static examples = ['<%= config.bin %> <%= command.id %>'];
8
+ async run() {
9
+ const { args, flags } = await this.parse(Mimic);
10
+ const mimic_request = `{
11
+
12
+ "resourceType": "Parameters",
13
+
14
+ "parameter": [ {
15
+
16
+ "name": "inputFormat",
17
+
18
+ "valueCode": "application/fhir+ndjson"
19
+
20
+ }, {
21
+
22
+ "name": "inputSource",
23
+
24
+ "valueUri": "http://example.com/fhir/"
25
+
26
+ }, {
27
+
28
+ "name": "storageDetail",
29
+
30
+ "part": [ {
31
+
32
+ "name": "type",
33
+
34
+ "valueCode": "https"
35
+
36
+ }, {
37
+
38
+ "name": "credentialHttpBasic",
39
+
40
+ "valueString": "admin:password"
41
+
42
+ }, {
43
+
44
+ "name": "maxBatchResourceCount",
45
+
46
+ "valueString": "500"
47
+
48
+ } ]
49
+
50
+ }, {
51
+
52
+ "name": "input",
53
+
54
+ "part": [ {
55
+
56
+ "name": "type",
57
+
58
+ "valueCode": "Observation"
59
+
60
+ }, {
61
+
62
+ "name": "url",
63
+
64
+ "valueUri": "https://physionet.org/files/mimic-iv-fhir-demo/2.0/mimic-fhir/ObservationLabevents.ndjson"
65
+
66
+ } ]
67
+
68
+ }, {
69
+
70
+ "name": "input",
71
+
72
+ "part": [ {
73
+
74
+ "name": "type",
75
+
76
+ "valueCode": "Medication"
77
+
78
+ }, {
79
+
80
+ "name": "url",
81
+
82
+ "valueUri": "https://physionet.org/files/mimic-iv-fhir-demo/2.0/mimic-fhir/Medication.ndjson"
83
+
84
+ } ]
85
+
86
+ }, {
87
+
88
+ "name": "input",
89
+
90
+ "part": [ {
91
+
92
+ "name": "type",
93
+
94
+ "valueCode": "Procedure"
95
+
96
+ }, {
97
+
98
+ "name": "url",
99
+
100
+ "valueUri": "https://physionet.org/files/mimic-iv-fhir-demo/2.0/mimic-fhir/Procedure.ndjson"
101
+
102
+ } ]
103
+
104
+ }, {
105
+
106
+ "name": "input",
107
+
108
+ "part": [ {
109
+
110
+ "name": "type",
111
+
112
+ "valueCode": "Condition"
113
+
114
+ }, {
115
+
116
+ "name": "url",
117
+
118
+ "valueUri": "https://physionet.org/files/mimic-iv-fhir-demo/2.0/mimic-fhir/Condition.ndjson"
119
+
120
+ } ]
121
+
122
+ }, {
123
+
124
+ "name": "input",
125
+
126
+ "part": [ {
127
+
128
+ "name": "type",
129
+
130
+ "valueCode": "Patient"
131
+
132
+ }, {
133
+
134
+ "name": "url",
135
+
136
+ "valueUri": "https://physionet.org/files/mimic-iv-fhir-demo/2.0/mimic-fhir/Patient.ndjson"
137
+
138
+ } ]
139
+
140
+ } ]
141
+
142
+ }`;
143
+ // send a POST request to the server with the mimic_request body
144
+ const response = await fetch(args.server, {
145
+ body: mimic_request,
146
+ headers: {
147
+ 'Content-Type': 'application/fhir+json',
148
+ 'Prefer': 'respond-async'
149
+ },
150
+ method: 'POST',
151
+ });
152
+ if (!response.ok) {
153
+ console.error(`Error: ${response.status} ${response.statusText}`);
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Synthetic extends Command {
3
+ static args: {
4
+ input: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ output: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ prompt: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static description: string;
9
+ static examples: string[];
10
+ static flags: {
11
+ inputField: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ maxCycles: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
+ maxRecords: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
14
+ outputField: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,88 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import fs from 'node:fs';
3
+ import bootstrap from '../utils/bootstrap.js';
4
+ import { ChainService } from '../utils/chain.js';
5
+ export default class Synthetic extends Command {
6
+ static args = {
7
+ input: Args.string({ default: "", description: 'Input file to process' }), // object with input, instruction (rationale in distillation), output
8
+ output: Args.string({ description: 'Output file to write' }),
9
+ prompt: Args.string({ default: "", description: 'Prompt file to read' }),
10
+ };
11
+ static description = 'Generate synthetic data using LLM';
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %>',
14
+ ];
15
+ static flags = {
16
+ inputField: Flags.string({ char: 'i', default: 'input', description: 'Input field to use', options: ['input', 'instruction', 'output'] }),
17
+ maxCycles: Flags.integer({ char: 'm', default: 0, description: 'Maximum number of cycles to run' }),
18
+ maxRecords: Flags.integer({ char: 'r', default: 10, description: 'Maximum number of records to generate' }),
19
+ outputField: Flags.string({ char: 'o', default: 'output', description: 'Output field to use', options: ['input', 'instruction', 'output'] }),
20
+ };
21
+ async run() {
22
+ const { args, flags } = await this.parse(Synthetic);
23
+ let prompt = "";
24
+ // read prompt file if provided
25
+ if (args.prompt)
26
+ prompt = fs.readFileSync(args.prompt ?? '', 'utf8');
27
+ const container = await bootstrap();
28
+ const chain = new ChainService(container);
29
+ // if no output file, exit with error
30
+ if (!args.output) {
31
+ console.log("Please provide an output file");
32
+ this.exit(1);
33
+ }
34
+ if (flags.maxCycles) { // No input file, can process in batches
35
+ const input = {
36
+ input: prompt,
37
+ };
38
+ let responses = [];
39
+ for (let i = 0; i < flags.maxCycles; i++) {
40
+ const response = await chain.Chain(input);
41
+ let cycle = [];
42
+ const jsonArrayMatch = response.match(/\[[^\]]*]/);
43
+ if (jsonArrayMatch) {
44
+ try {
45
+ cycle = JSON.parse(jsonArrayMatch[0]);
46
+ }
47
+ catch (error) {
48
+ console.error('Failed to parse JSON array from response:', error);
49
+ }
50
+ }
51
+ responses = responses.concat(cycle);
52
+ console.log(`Iteration ${i + 1}: Collected ${responses.length} records so far, ${flags.maxRecords - responses.length} to go`);
53
+ if (responses.length >= flags.maxRecords)
54
+ break;
55
+ }
56
+ // convert to json
57
+ const jsonOutput = [];
58
+ for (const response of responses) {
59
+ jsonOutput.push({
60
+ [flags.outputField]: response,
61
+ });
62
+ }
63
+ fs.writeFileSync(args.output ?? '', JSON.stringify(jsonOutput, null, 4));
64
+ console.log(`${args.output} has been created with ${flags.maxRecords} records`);
65
+ }
66
+ else { // Input file, process one by one
67
+ // read input file
68
+ const input = JSON.parse(fs.readFileSync(args.input ?? '', 'utf8'));
69
+ const responses = [];
70
+ // for each record in input file
71
+ for (const [i, record] of input.entries()) {
72
+ const chainInput = {
73
+ input: record[flags.inputField],
74
+ prompt,
75
+ };
76
+ const response = await chain.Chain(chainInput);
77
+ record[flags.outputField] = response;
78
+ responses.push(record);
79
+ if (responses.length >= flags.maxRecords)
80
+ break;
81
+ console.log(`Processed ${i + 1} records so far, ${flags.maxRecords - responses.length} to go`);
82
+ }
83
+ // write to output file
84
+ fs.writeFileSync(args.output ?? '', JSON.stringify(responses, null, 4));
85
+ console.log(`${args.output} has been created with ${responses.length} records`);
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,2 @@
1
+ export * from './utils/index.js';
2
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './utils/index.js';
2
+ export { run } from '@oclif/core';
@@ -0,0 +1,3 @@
1
+ import "reflect-metadata";
2
+ declare const bootstrap: () => Promise<import("tsyringe").DependencyContainer>;
3
+ export default bootstrap;
@@ -0,0 +1,57 @@
1
+ import { ChatPromptTemplate } from "@langchain/core/prompts";
2
+ import { Ollama } from "@langchain/ollama";
3
+ import "reflect-metadata";
4
+ import { container } from "tsyringe";
5
+ // import { VertexAI } from "@langchain/google-vertexai";
6
+ // import { GoogleVertexAIEmbeddings } from "@langchain/community/embeddings/googlevertexai";
7
+ import { FakeListChatModel } from "@langchain/core/utils/testing";
8
+ const bootstrap = async () => {
9
+ let main_llm = null;
10
+ // try{
11
+ // const vertex = new VertexAI({
12
+ // temperature: 0.6,
13
+ // maxOutputTokens: 256,
14
+ // model: "gemini-pro",
15
+ // })
16
+ // main_llm = vertex;
17
+ // } catch (error) {
18
+ const ollama = new Ollama({
19
+ baseUrl: process.env.NEXT_PUBLIC_OLLAMA_URL || "http://localhost:11434",
20
+ model: process.env.NEXT_PUBLIC_OLLAMA_MODEL || "phi3:mini",
21
+ numPredict: 128,
22
+ temperature: 0.6,
23
+ });
24
+ const fake_llm = new FakeListChatModel({
25
+ responses: ["I'll callback later.", "You 'console' them!"],
26
+ });
27
+ main_llm = fake_llm;
28
+ // }
29
+ // const main_llm = new OllamaFunctions({
30
+ // temperature: 0.6,
31
+ // model: "phi3:mini",
32
+ // numPredict: 32,
33
+ // });
34
+ const gen_prompt = ChatPromptTemplate.fromMessages([
35
+ [
36
+ "system",
37
+ "You are a medical doctor.",
38
+ ],
39
+ ["human", "{input}"],
40
+ ]);
41
+ const instruct_prompt = ChatPromptTemplate.fromMessages([
42
+ [
43
+ "system",
44
+ "You are a HL7 FHIR expert.",
45
+ ],
46
+ ["human", "{prompt} {input}"],
47
+ ]);
48
+ const prompt = instruct_prompt;
49
+ container.register("main-llm", {
50
+ useValue: main_llm,
51
+ });
52
+ container.register("prompt", {
53
+ useValue: prompt,
54
+ });
55
+ return container;
56
+ };
57
+ export default bootstrap;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * TypeScript models for CDS Hook Card
3
+ *
4
+ * Example:
5
+ * {
6
+ * "summary": "Patient is at high risk for opioid overdose.",
7
+ * "detail": "According to CDC guidelines, the patient's opioid dosage should be tapered to less than 50 MME. [Link to CDC Guideline](https://www.cdc.gov/drugoverdose/prescribing/guidelines.html)",
8
+ * "indicator": "warning",
9
+ * "source": {
10
+ * "label": "CDC Opioid Prescribing Guidelines",
11
+ * "url": "https://www.cdc.gov/drugoverdose/prescribing/guidelines.html",
12
+ * "icon": "https://example.org/img/cdc-icon.png"
13
+ * },
14
+ * "links": [
15
+ * {
16
+ * "label": "View MME Conversion Table",
17
+ * "url": "https://www.cdc.gov/drugoverdose/prescribing/mme.html"
18
+ * }
19
+ * ]
20
+ * }
21
+ */
22
+ /**
23
+ * The allowed indicators for a CDS Hook Card.
24
+ * Mirrors: Literal["info", "warning", "hard-stop"]
25
+ */
26
+ export type CDSHookCardIndicator = 'hard-stop' | 'info' | 'warning';
27
+ /**
28
+ * Source of the CDS Hook Card
29
+ */
30
+ export declare class CDSHookCardSource {
31
+ icon?: string;
32
+ label: string;
33
+ url?: string;
34
+ constructor(init?: Partial<CDSHookCardSource>);
35
+ }
36
+ /**
37
+ * Link associated with the CDS Hook Card
38
+ */
39
+ export declare class CDSHookCardLink {
40
+ label: string;
41
+ url: string;
42
+ constructor(init?: Partial<CDSHookCardLink>);
43
+ }
44
+ /**
45
+ * CDS Hook Card Model
46
+ */
47
+ export declare class CDSHookCard {
48
+ detail?: string;
49
+ indicator?: CDSHookCardIndicator;
50
+ links?: CDSHookCardLink[];
51
+ source?: CDSHookCardSource;
52
+ summary: string;
53
+ constructor(init?: Partial<CDSHookCard>);
54
+ /**
55
+ * Factory to build a CDSHookCard from a plain object, ensuring nested types are instantiated.
56
+ */
57
+ static from(obj: Partial<CDSHookCard>): CDSHookCard;
58
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * TypeScript models for CDS Hook Card
3
+ *
4
+ * Example:
5
+ * {
6
+ * "summary": "Patient is at high risk for opioid overdose.",
7
+ * "detail": "According to CDC guidelines, the patient's opioid dosage should be tapered to less than 50 MME. [Link to CDC Guideline](https://www.cdc.gov/drugoverdose/prescribing/guidelines.html)",
8
+ * "indicator": "warning",
9
+ * "source": {
10
+ * "label": "CDC Opioid Prescribing Guidelines",
11
+ * "url": "https://www.cdc.gov/drugoverdose/prescribing/guidelines.html",
12
+ * "icon": "https://example.org/img/cdc-icon.png"
13
+ * },
14
+ * "links": [
15
+ * {
16
+ * "label": "View MME Conversion Table",
17
+ * "url": "https://www.cdc.gov/drugoverdose/prescribing/mme.html"
18
+ * }
19
+ * ]
20
+ * }
21
+ */
22
+ /**
23
+ * Source of the CDS Hook Card
24
+ */
25
+ export class CDSHookCardSource {
26
+ icon;
27
+ label;
28
+ url;
29
+ constructor(init) {
30
+ Object.assign(this, init);
31
+ }
32
+ }
33
+ /**
34
+ * Link associated with the CDS Hook Card
35
+ */
36
+ export class CDSHookCardLink {
37
+ label;
38
+ url;
39
+ constructor(init) {
40
+ Object.assign(this, init);
41
+ }
42
+ }
43
+ /**
44
+ * CDS Hook Card Model
45
+ */
46
+ export class CDSHookCard {
47
+ detail;
48
+ indicator;
49
+ links;
50
+ source;
51
+ summary;
52
+ constructor(init) {
53
+ if (init) {
54
+ // Shallow assign for primitives; nested objects handled below if present
55
+ const { links, source, ...rest } = init;
56
+ Object.assign(this, rest);
57
+ if (source)
58
+ this.source = new CDSHookCardSource(source);
59
+ if (links)
60
+ this.links = links.map((l) => new CDSHookCardLink(l));
61
+ }
62
+ }
63
+ /**
64
+ * Factory to build a CDSHookCard from a plain object, ensuring nested types are instantiated.
65
+ */
66
+ static from(obj) {
67
+ return new CDSHookCard(obj);
68
+ }
69
+ }
@@ -0,0 +1,5 @@
1
+ export declare class ChainService {
2
+ container: any;
3
+ constructor(container: any);
4
+ Chain(input: any): Promise<string>;
5
+ }
@@ -0,0 +1,17 @@
1
+ import { StringOutputParser } from "@langchain/core/output_parsers";
2
+ import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
3
+ export class ChainService {
4
+ container;
5
+ constructor(container) {
6
+ this.container = container;
7
+ }
8
+ async Chain(input) {
9
+ const output = RunnableSequence.from([
10
+ new RunnablePassthrough(),
11
+ this.container.resolve("prompt"),
12
+ this.container.resolve("main-llm"),
13
+ new StringOutputParser(),
14
+ ]);
15
+ return output.invoke(input);
16
+ }
17
+ }
@@ -0,0 +1,3 @@
1
+ import { CDSHookCard } from './card.js';
2
+ declare const cards: (response: any) => CDSHookCard[];
3
+ export default cards;
@@ -0,0 +1,11 @@
1
+ import { CDSHookCard } from './card.js';
2
+ const cards = (response) => {
3
+ const _cards = response?.data?.cards;
4
+ if (Array.isArray(_cards) && _cards.length > 0) {
5
+ const lastCard = _cards.at(-1);
6
+ const card = new CDSHookCard(lastCard);
7
+ return [card];
8
+ }
9
+ return [new CDSHookCard()];
10
+ };
11
+ export default cards;
@@ -0,0 +1,4 @@
1
+ export * from './card.js';
2
+ export * from './getCard.js';
3
+ export * from './request.js';
4
+ export * from './useDhti.js';
@@ -0,0 +1,5 @@
1
+ export * from './card.js';
2
+ export * from './getCard.js';
3
+ export * from './request.js';
4
+ export * from './useDhti.js';
5
+ // ...add others as needed
@@ -0,0 +1,30 @@
1
+ /**
2
+ * CDS Hook Request Model (TypeScript)
3
+ *
4
+ * Example:
5
+ * {
6
+ * "hookInstance": "d1577c69-dfbe-44ad-ba6d-3e05e953b2ea",
7
+ * "fhirServer": "https://example.com/fhir",
8
+ * "fhirAuthorization": { ... },
9
+ * "hook": "patient-view",
10
+ * "context": { ... },
11
+ * "prefetch": { ... }
12
+ * }
13
+ */
14
+ export declare class CDSHookRequest {
15
+ /** Context object passed by the EHR */
16
+ context?: Record<string, any> | null;
17
+ /** Authorization details (opaque to this model) */
18
+ fhirAuthorization?: Record<string, any> | null;
19
+ /** Base URL of the FHIR server associated with the hook */
20
+ fhirServer?: string;
21
+ /** Name of the hook (e.g., "patient-view", "order-select", etc.) */
22
+ hook?: string;
23
+ /** A unique identifier for this hook invocation */
24
+ hookInstance?: string;
25
+ /** Prefetched FHIR resources keyed by name */
26
+ prefetch?: Record<string, any> | null;
27
+ constructor(init?: Partial<CDSHookRequest>);
28
+ /** Factory to build a CDSHookRequest from a plain object. */
29
+ static from(obj: Partial<CDSHookRequest>): CDSHookRequest;
30
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CDS Hook Request Model (TypeScript)
3
+ *
4
+ * Example:
5
+ * {
6
+ * "hookInstance": "d1577c69-dfbe-44ad-ba6d-3e05e953b2ea",
7
+ * "fhirServer": "https://example.com/fhir",
8
+ * "fhirAuthorization": { ... },
9
+ * "hook": "patient-view",
10
+ * "context": { ... },
11
+ * "prefetch": { ... }
12
+ * }
13
+ */
14
+ export class CDSHookRequest {
15
+ /** Context object passed by the EHR */
16
+ context;
17
+ /** Authorization details (opaque to this model) */
18
+ fhirAuthorization;
19
+ /** Base URL of the FHIR server associated with the hook */
20
+ fhirServer;
21
+ /** Name of the hook (e.g., "patient-view", "order-select", etc.) */
22
+ hook;
23
+ /** A unique identifier for this hook invocation */
24
+ hookInstance;
25
+ /** Prefetched FHIR resources keyed by name */
26
+ prefetch;
27
+ constructor(init) {
28
+ Object.assign(this, init);
29
+ }
30
+ /** Factory to build a CDSHookRequest from a plain object. */
31
+ static from(obj) {
32
+ return new CDSHookRequest(obj);
33
+ }
34
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sends a CDS Hook Request where the request.context contains the user's input message.
3
+ */
4
+ declare const handleBundle: (newMessage: string) => Promise<import("axios").AxiosResponse<any, any>>;
5
+ export default handleBundle;
@@ -0,0 +1,21 @@
1
+ import axios from 'axios';
2
+ import { CDSHookRequest } from './request.js';
3
+ /**
4
+ * Sends a CDS Hook Request where the request.context contains the user's input message.
5
+ */
6
+ const handleBundle = (newMessage) => {
7
+ const request = new CDSHookRequest({
8
+ context: { input: newMessage },
9
+ });
10
+ // TODO: Investigate why nested input is required
11
+ const _request = {
12
+ input: request,
13
+ };
14
+ const endpoint = process.env.LANGSERVE_POST_ENDPOINT || '/langserve/dhti_elixir_template/invoke';
15
+ return axios.post(endpoint, {
16
+ config: {},
17
+ input: _request,
18
+ kwargs: {},
19
+ });
20
+ };
21
+ export default handleBundle;