Skip to content

Commit 2253bdd

Browse files
authored
Merge pull request #6 from HealthRex/feature/populate-json-templates
Feature/populate json templates
2 parents 91f30e3 + 6af3941 commit 2253bdd

File tree

7 files changed

+132
-111
lines changed

7 files changed

+132
-111
lines changed

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/prompt.txt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@ You must strictly follow the Stanford template structure wherever applicable.
33
Given:
44
1. A clinical question from a PCP
55
2. Patient case details
6-
3. Access to Stanford templates
6+
3. Stanford JSON template
77
Generate JSON object with the following fields:
88
- Field: specialistSummary (3-4 lines):
99
- Summary of the relevant case details for a specialist who is reviewing the case
1010
- Focus on patient demographics and key clinical context, relevant history/findings, especially findings that impact management
1111
- Summarize existing data only; do not include recommendations or suggestions
12-
- Field: templateSelectionProcess:
13-
- First, examine ONLY the templates in the following Stanford document: {{TemplateGoogleDocLink}}
14-
- Mention ALL available templates in the above document and use step-by-step logic to justify the most appropriate selection.
1512
- Field: basicPatientSummary (return as array):
1613
- Patient age
1714
- Patient gender
1815
- Comma separated string of past medical history
1916
- Comma separated string of key medications
20-
- Field: populatedTemplate (STRICT FORMAT AND TEMPLATE ADHERENCE AND LAB VALUE INCLUSION REQUIRED):
21-
- Respond with JSON array and put each filled template field into its own JSON object, e.g., {"field": "filled template field name", "value": "filled template field value"} as an item of enclosing array.
22-
- Maintain original order of sections and questions without modifications.
23-
- Populate each field with case-specific information.
17+
- Field: populatedTemplate:
18+
- Populate "***" values of Stanford JSON template with case-specified information
19+
- Strictly follow input Stanford JSON format
2420
- For lab results, explicitly list numeric values instead of checkboxes. Example:
2521
* Thyroid Stimulating Hormone (TSH): 0.02 mIU/L
2622
* Free Thyroxine (Free T4): 2.1 ng/dL

src/app.service.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,26 @@ export class AppService {
5757
async postReferralQuestion(
5858
request: ReferralRequest,
5959
): Promise<ReferralResponse> {
60-
const systemPrompt = await this.selectSystemPrompt(request);
61-
const llmResponse = await this.queryLLM<LLMResponse>(
60+
const bestTemplate: string =
61+
await this.templateSelectorService.selectBestTemplate(request.question);
62+
const systemPrompt: string = this.selectSystemPrompt(bestTemplate);
63+
64+
const llmResponse: LLMResponse = await this.queryLLM<LLMResponse>(
6265
systemPrompt,
6366
[
6467
'Clinical question: ' + request.question,
6568
'Patient notes: ' + request.clinicalNotes,
6669
],
67-
llmResponseSchema,
70+
llmResponseSchema(JSON.parse(bestTemplate)),
71+
);
72+
const pathwayResponse: SpecialistAIResponse = await this.queryPathway(
73+
request,
74+
llmResponse,
6875
);
69-
const pathwayResponse = await this.queryPathway(request, llmResponse);
7076
const response: ReferralResponse = llmResponse as ReferralResponse;
7177
response.specialistAIResponse = pathwayResponse;
7278

73-
this.logger.debug(response);
79+
this.logger.debug(JSON.stringify(response, null, 2));
7480

7581
return response;
7682
}
@@ -79,15 +85,17 @@ export class AppService {
7985
request: ReferralRequest,
8086
session: Record<string, any>,
8187
): Promise<Observable<{ data: ReferralResponse }>> {
82-
const systemPrompt = await this.selectSystemPrompt(request);
88+
const bestTemplate: string =
89+
await this.templateSelectorService.selectBestTemplate(request.question);
90+
const systemPrompt: string = this.selectSystemPrompt(bestTemplate);
8391
const llmResponseObservable: Observable<ReferralResponse> =
8492
this.queryLLMStreamed<ReferralResponse>(
8593
systemPrompt,
8694
[
8795
'Clinical question: ' + request.question,
8896
'Patient notes: ' + request.clinicalNotes,
8997
],
90-
llmResponseSchema,
98+
llmResponseSchema(JSON.parse(bestTemplate)),
9199
);
92100

93101
return new Observable((subscriber) => {
@@ -100,7 +108,7 @@ export class AppService {
100108
// reset Pathway conversation history on new referral request
101109
session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] = [];
102110

103-
this.logger.debug('LLM partial response: ', next);
111+
this.logger.debug('LLM partial response: ', JSON.stringify(next));
104112
subscriber.next({ data: next });
105113
},
106114
error: (reason) => {
@@ -227,6 +235,19 @@ export class AppService {
227235
return this.queryLLM<string[]>(systemPrompt, request, responseSchema);
228236
}
229237

238+
private selectSystemPrompt(bestTemplate: string) {
239+
if (bestTemplate.length > 0) {
240+
return fs
241+
.readFileSync(join(process.cwd(), systemPromptFilePath))
242+
.toString();
243+
} else {
244+
// no template selected
245+
return fs
246+
.readFileSync(join(process.cwd(), systemPromptWithoutTemplatesFilePath))
247+
.toString();
248+
}
249+
}
250+
230251
private async queryLLM<T>(
231252
systemPrompt: string,
232253
messages: string[],
@@ -310,25 +331,6 @@ export class AppService {
310331
);
311332
}
312333

313-
private async selectSystemPrompt(request: ReferralRequest) {
314-
const bestTemplate = await this.templateSelectorService.selectBestTemplate(
315-
request.question,
316-
);
317-
this.logger.debug('bestTemplate', bestTemplate);
318-
319-
if (bestTemplate) {
320-
return fs
321-
.readFileSync(join(process.cwd(), systemPromptFilePath))
322-
.toString()
323-
.replace('{{TemplateGoogleDocLink}}', bestTemplate);
324-
} else {
325-
// no template selected
326-
return fs
327-
.readFileSync(join(process.cwd(), systemPromptWithoutTemplatesFilePath))
328-
.toString();
329-
}
330-
}
331-
332334
private selectModel(): LanguageModelV1 {
333335
switch (String(process.env.AI_PROVIDER).toUpperCase() as AIProvider) {
334336
case AIProvider.Claude:

src/models/llmResponse.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { z } from 'zod';
3+
import { jsonToZod } from '../validation/jsonToZodSchema';
34

45
export class LLMResponse {
56
@ApiProperty({ description: 'Specialist summary response' })
@@ -24,13 +25,14 @@ export class LLMResponse {
2425
populatedTemplate: Record<string, string>[];
2526
}
2627

27-
export const llmResponseSchema = z.object({
28-
specialistSummary: z.string(),
29-
templateSelectionProcess: z.string(),
30-
basicPatientSummary: z.array(
31-
z.object({ field: z.string(), value: z.string() }),
32-
),
33-
populatedTemplate: z.array(
34-
z.object({ field: z.string(), value: z.string() }),
35-
),
36-
});
28+
export const llmResponseSchema = (
29+
templateJson: any,
30+
): z.ZodObject<any, any, any, any, any> => {
31+
return z.object({
32+
specialistSummary: z.string(),
33+
basicPatientSummary: z.array(
34+
z.object({ field: z.string(), value: z.string() }),
35+
),
36+
populatedTemplate: jsonToZod(templateJson),
37+
});
38+
};

src/template-selector/models/selectBestTemplateResponse.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export class SelectBestTemplateResponse {
44
question: string;
55
@Expose({ name: 'suggested_template' })
66
suggestedTemplate: string;
7+
@Expose({ name: 'suggested_template_json' })
8+
suggestedTemplateJson: string;
79
@Expose({ name: 'similarity_scores' })
810
similarityScores: object;
911
}
Lines changed: 8 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,24 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { HttpService } from '@nestjs/axios';
3-
import { AxiosError, AxiosResponse } from 'axios';
4-
import { catchError, lastValueFrom, map, of } from 'rxjs';
3+
import { AxiosError } from 'axios';
4+
import { catchError, lastValueFrom, map } from 'rxjs';
55
import { SelectBestTemplateRequest } from './models/selectBestTemplateRequest';
66
import { SelectBestTemplateResponse } from './models/selectBestTemplateResponse';
77
import { plainToInstance } from 'class-transformer';
88

9-
// TODO would passing drive.google.com/file/.../view link work as well?
10-
const templateGoogleDocLinks: { [key: string]: string } = {
11-
'Allergy eConsult Checklists _extracted.txt':
12-
'https://docs.google.com/document/d/1JtFiX31BDZWt0zt0TMKvzLEUxZO5OYlO/edit',
13-
'Cardiology eConsult Checklists_extracted.txt':
14-
'https://drive.google.com/file/d/1oHN8mjupLrk7QVoH-s2hNNM3V4qCM-Cx/view',
15-
'Chemical Dependency eConsult Checklists_extracted.txt':
16-
'https://drive.google.com/file/d/14Alx6GLZWsl5_D8c57geJSCdWitunPpr/view',
17-
'Dermatology eConsult Checklists_extracted.txt':
18-
'https://drive.google.com/file/d/1Bc695IqIwT65sSz4uxUJraKfN3BxGVOG/view',
19-
'Endocrinology eConsult Checklists FINAL 4.19.22_extracted.txt':
20-
'https://docs.google.com/document/d/1tA1bkGYlnoFq2OXKMfP996ZFGoSH0DRO/edit',
21-
'ENT eConsult Checklists_extracted.txt':
22-
'https://drive.google.com/file/d/1Cof4fRSaH5HbMmr_O8mK3xrb-2YElpov/view',
23-
'Gastroenterology eConsult Checklists_extracted.txt':
24-
'https://drive.google.com/file/d/1KzIx98EJlN8AGlHaYDBHQ0T4BEV9BvX4/view',
25-
'Gynecology eConsult Checklists_extracted.txt':
26-
'https://drive.google.com/file/d/1iHzbpO-Kmm98z8XxMXtBd191tzluBv0e/view',
27-
'Hematology eConsult Checklists FINAL 9.7.22_extracted.txt':
28-
'https://drive.google.com/file/d/1nRdw1uIWAAw047snDVXfWGr6AwiwEEeW/view',
29-
'Hepatology Oncology eConsult Checklists_extracted.txt':
30-
'https://drive.google.com/file/d/14ZSQwWhmqcLETp1nNNst2caPgtghQ4OP/view',
31-
'Infectious Disease eConsult Checklists_3.13.24_extracted.txt':
32-
'https://drive.google.com/file/d/1W2eXxt3i2U6pdEOiydE4pJLfkZQmQ4qr/view',
33-
'Interventional Pulmonology eConsult Checklists _extracted.txt':
34-
'https://drive.google.com/file/d/1VCmKnX4wdh9rxE9DOQDxUhVRocZx9gjH/view',
35-
'LGBTQ+ eConsult Checklists_extracted.txt':
36-
'https://drive.google.com/file/d/1UdZrdqu4g7ZDelVJy80pcfZb9JwjQ8yq/view',
37-
'Nephrology eConsult Checklists_extracted.txt':
38-
'https://drive.google.com/file/d/1MhBZ4JgUvHh1vMDsO1b4WXr14zFaaMTk/view',
39-
'Neurology eConsult Checklists_extracted.txt':
40-
'https://drive.google.com/file/d/1yRjPXD90c3aU5Oz7bjt1keQecIo0yEx5/view',
41-
'Oncology trial eConsult Checklists_extracted.txt':
42-
'https://drive.google.com/file/d/1o8VVVuUgSZhLjwCzc-2u3Va0MWOAgFUD/view',
43-
'Orthopedics eConsult Checklists_extracted.txt':
44-
'https://drive.google.com/file/d/1rw0CvTxyExXlSVIhYW_BNCcLtrGu5Yh1/view',
45-
'Pain Medicine eConsult Checklists_extracted.txt':
46-
'https://drive.google.com/file/d/1Sj-riOwZ9ow6JsUh1zvloyuAFl5pJV20/view',
47-
'Psychiatry eConsult Checklists_extracted.txt':
48-
'https://drive.google.com/file/d/1cnJ3MZFkt5UdSpXdLAGBRB6FnNz5tqGN/view',
49-
'Pulmonology eConsult Checklists_extracted.txt':
50-
'https://drive.google.com/file/d/11qKl1vkOIsFGxYTAuMU6Yo57mrjQqH56/view',
51-
'Rheumatology eConsult Checklists_extracted.txt':
52-
'https://drive.google.com/file/d/1sNB3Ls_RcYoBKjuV3YspAg9h1H30oSCB/view',
53-
'SHC Referral Templates Checklists compiled DRAFT_extracted.txt':
54-
'https://drive.google.com/file/d/1uQ7GfwsuHb5GVmb7aU0B_VLC_tTxJe7T/view',
55-
'Sleep Medicine eConsult Checklists_extracted.txt':
56-
'https://drive.google.com/file/d/1qdJh1mSNo8OPRsGtOpdkxh8vpeiPS1Pk/view',
57-
'Urology eConsult Checklists_extracted.txt':
58-
'https://drive.google.com/file/d/1dX80rZgPpzZGMbhdjpqBsHhCJkM3NInS/view',
59-
};
60-
619
@Injectable()
6210
export class TemplateSelectorService {
63-
private readonly logger = new Logger(TemplateSelectorService.name);
11+
private readonly logger: Logger = new Logger(TemplateSelectorService.name);
6412

6513
constructor(private readonly httpService: HttpService) {}
6614

6715
async selectBestTemplate(clinicalQuestion: string): Promise<string> {
6816
const request: SelectBestTemplateRequest = new SelectBestTemplateRequest();
6917
request.question = clinicalQuestion;
7018

71-
const { data } = await lastValueFrom(
19+
const suggestedTemplateJson: string = await lastValueFrom(
7220
this.httpService
7321
.post<SelectBestTemplateResponse, SelectBestTemplateRequest>(
74-
// TODO get-template is wrong name for POST method - should be smth like select-best-template
7522
String(process.env.EMBEDDINGS_API) + '/select-best-template',
7623
request,
7724
)
@@ -82,19 +29,14 @@ export class TemplateSelectorService {
8229
SelectBestTemplateResponse,
8330
response.data,
8431
);
85-
return response;
32+
return response.data.suggestedTemplateJson;
8633
}),
8734
)
8835
.pipe(
8936
catchError((error: AxiosError) => {
9037
if (error.status === 404) {
9138
// no template matched
92-
return of(
93-
error.response as AxiosResponse<
94-
SelectBestTemplateResponse,
95-
SelectBestTemplateRequest
96-
>,
97-
);
39+
return '';
9840
}
9941

10042
// TODO handle 404 error - need to send different prompt to LLM asking it to generate template by itself
@@ -107,8 +49,8 @@ export class TemplateSelectorService {
10749
}),
10850
),
10951
);
110-
this.logger.debug('data', data);
52+
this.logger.debug('suggestedTemplateJson', suggestedTemplateJson);
11153

112-
return templateGoogleDocLinks[data.suggestedTemplate];
54+
return suggestedTemplateJson;
11355
}
11456
}

src/validation/jsonToZodSchema.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { z } from 'zod';
2+
import { ZodTypeAny } from 'zod/lib/types';
3+
4+
// inspired from https://github.com/colinhacks/zod/discussions/585
5+
export const jsonToZod = (obj: any): z.ZodTypeAny => {
6+
const parse = (obj: any): z.ZodTypeAny => {
7+
switch (typeof obj) {
8+
case 'string':
9+
return z.string();
10+
case 'number':
11+
return z.number();
12+
case 'bigint':
13+
return z.number().int();
14+
case 'boolean':
15+
return z.boolean();
16+
case 'object': {
17+
if (Array.isArray(obj)) {
18+
const options = obj
19+
.map(parse)
20+
.reduce(
21+
(acc: z.ZodTypeAny[], curr) =>
22+
acc.includes(curr) ? acc : [...acc, curr],
23+
[],
24+
);
25+
if (options.length === 1) {
26+
return z.array(options[0]);
27+
} else if (options.length > 1) {
28+
return z.array(z.union([options[0], options[1], ...options]));
29+
} else {
30+
return z.array(z.unknown());
31+
}
32+
}
33+
const zodObj: Map<string, ZodTypeAny> = new Map(
34+
Object.entries(obj as object).map(([k, v]) => [k, parse(v)]),
35+
);
36+
return z.object(Object.fromEntries(zodObj));
37+
}
38+
case 'undefined':
39+
return z.undefined();
40+
case 'function':
41+
return z.function();
42+
case 'symbol':
43+
default:
44+
return z.unknown();
45+
}
46+
};
47+
48+
return parse(obj);
49+
};

0 commit comments

Comments
 (0)