diff --git a/components/Chat.vue b/components/Chat.vue index 7d48eac..2655ac7 100644 --- a/components/Chat.vue +++ b/components/Chat.vue @@ -199,6 +199,9 @@ export default class Chat extends Vue { } else if (event.target?.className === "language-fhir") { this.$emit("apply-suggested-fhir", valueString); + } else if (event.target?.className === "language-jsonpatch") { + this.$emit("apply-suggested-jsonpatch", valueString); + } else { this.$emit("apply-suggested-json", valueString); } @@ -267,6 +270,7 @@ export default class Chat extends Vue { .message pre:has(.language-item), .message pre:has(.language-fhir), .message pre:has(.language-log), +.message pre:has(.language-jsonpatch), .message pre:has(.language-json) { position: relative; padding: 8px 8px 8px 16px; @@ -282,6 +286,7 @@ export default class Chat extends Vue { .language-questionnaire::after, .language-item::after, .language-fhir::after, +.language-jsonpatch::after, .language-json::after { content: 'assignment_return'; font-family: 'Material Icons'; @@ -301,6 +306,7 @@ export default class Chat extends Vue { .language-questionnaire:hover::after, .language-item:hover::after, .language-fhir:hover::after, +.language-jsonpatch:hover::after, .language-json:hover::after { color: #1976d2; } @@ -318,6 +324,7 @@ export default class Chat extends Vue { .language-item::before, .language-fhir::before, .language-log::before, +.language-jsonpatch::before, .language-json::before { font-size: small; font-style: italic; @@ -357,6 +364,10 @@ export default class Chat extends Vue { content: '(json)'; } +.language-jsonpatch::before { + content: '(patch)'; +} + .message pre:has(code) { background-color: ghostwhite; } diff --git a/helpers/openai_form_tester.ts b/helpers/openai_form_tester.ts new file mode 100644 index 0000000..1a2b983 --- /dev/null +++ b/helpers/openai_form_tester.ts @@ -0,0 +1,147 @@ +import { + EvaluateChatPrompt, + GetSystemPrompt, + IOpenAISettings, +} from "~/helpers/openai_utils"; +import Chat from "~/components/Chat.vue"; +import { + ChatMessage, + OpenAIClient, + AzureKeyCredential, + OpenAIKeyCredential, + OpenAIClientOptions, +} from "@azure/openai"; +import { types } from "fhirpath"; + +// 1. Determine data required for Query +// (display a message) +// 2. Retrieve Data +// (code: evaluate expression to get types/known/hidden issues) +// 3. Evaluate Query +// 4. Verify results +// (code) +// 5. Display Results + +export interface IDataRequired { + Mode: "edit-questionnaire" | "create-questionnaire" | "create-item" | "edit-item" | "unknown"; + QuestionnaireDefinition: number; // the complete definition is required + QuestionnaireResponse: number; // a sample response is required + FocusedQuestionnaireItem: number; // the question relates to a specific item in the questionnaire + SampleSourceData: number; // the question relates to pre-populating from a resource + SampleSourceResourceType?: string; // if the resource type of sample source resource is known + ErrorMessage?: string; +} + +// This is just the interface above copied into a string for use inside the OpenAI query prompt +const interfaceIDataRequired = ` +interface IDataRequired { + Mode: "edit-questionnaire" | "create-questionnaire" | "create-item" | "edit-item" | "unknown"; + QuestionnaireDefinition : number; // the complete definition is required as the request is editing the current form 0-10 + QuestionnaireResponse : number; // a sample response is required 0-10 + FocusedQuestionnaireItem : number; // the question relates to a specific item in the questionnaire 0-10 + SampleSourceData: Number; // the question relates to pre-populating from a resource 0-10 + ErrorMessage?: string; +} +`; + +export async function DetectDataRequiredForQuery( + settings: IOpenAISettings, + query: string +): Promise { + let prompt: Array = []; + + let systemPrompt = ` +You are a background system assisting with verifying what data would be required to answer a provided question. +Your response is a json object that conforms to this interface: +\`\`\` typescript +${interfaceIDataRequired} +\`\`\` + +What data would be required to answer the following question: +` + query; + + prompt.push({ role: "system", content: systemPrompt }); + + let result = await EvaluateChatPrompt(prompt, settings, 1, 3096); + try { + if (result) { + console.log(result); + const parsedResponse : IDataRequired = JSON.parse(result); + console.log(parsedResponse); + return parsedResponse; + } + } catch (err) { + console.log(err); + } + + return { + QuestionnaireDefinition: 1, + QuestionnaireResponse: 1, + FocusedQuestionnaireItem: 1, + SampleSourceData: 1, + SampleSourceResourceType: "Patient", + }; +} + +// async function handleSendMessage(message: string) { +// console.log("Message sent:", message); +// const chat = this.$refs.chatComponent as Chat; + +// this.openAIexpressionExplanationLoading = true; +// // this.openAIexpressionExplanationMessage = "Asking question..."; +// chat.setThinking(true); + +// // before asking the question, check to see if the question would require +// // the complete questionnaire to be included +// let promptPreparationCheck: string = ` +// You are a background system assisting with verifying what data would be required to answer a provided question. +// `; + +// // Perform any additional actions with the message here +// const systemPrompt = GetSystemPrompt(); + +// let userQuestionContext: string = ""; +// if (this.resourceJsonEditor) { +// var jsonValue = this.resourceJsonEditor.getValue(); +// if (jsonValue.length > 0) { +// try { +// var obj = JSON.parse(jsonValue) as fhir4b.Questionnaire; +// if (obj.text) delete obj.text; +// jsonValue = JSON.stringify(obj); +// } catch (err) { +// console.log(err); +// } +// userQuestionContext += `Based on the FHIR Questionnaire\r\n\`\`\` json\r\n ${jsonValue}\n\n\`\`\`\r\n`; +// } +// } + +// if (userQuestionContext != this.openAILastContext) { +// if (userQuestionContext.length > 0) +// chat.addMessage("Author", userQuestionContext, false); +// this.openAILastContext = userQuestionContext; +// } +// chat.addMessage("Author", message, true); + +// // userQuestion += message; +// // chat.addMessage("Author", userQuestion, true); + +// let prompt: Array = []; +// prompt.push({ role: "system", content: systemPrompt }); +// prompt = prompt.concat(chat.getConversationChat()); + +// const resultOfQuestion = await EvaluateChatPrompt( +// prompt, +// this.GetAISettings(), +// 1, +// 4000 +// ); +// // this.openAIexpressionExplanationMessage = "(Generated by OpenAI " + settings.getOpenAIModel() + ")"; +// this.openAIexpressionExplanationLoading = false; +// chat.addMessage( +// "FhirPath AI", +// resultOfQuestion ?? "", +// true, +// settings.getOpenAIModel() +// ); +// chat.setThinking(false); +// } diff --git a/package-lock.json b/package-lock.json index 6be6df0..f098494 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "consola": "^2.15.3", "core-js": "^3.15.1", "express": "^4.17.2", + "fast-json-patch": "^3.1.1", "fhir-extension-helpers": "^0.3.0", "fhir-sdc-helpers": "^0.1.0", "fhirclient": "^2.5.2", @@ -13555,6 +13556,11 @@ "node": ">=8" } }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -39367,6 +39373,11 @@ "micromatch": "^4.0.4" } }, + "fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/package.json b/package.json index 08ab062..19ec4c9 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "consola": "^2.15.3", "core-js": "^3.15.1", "express": "^4.17.2", + "fast-json-patch": "^3.1.1", "fhir-extension-helpers": "^0.3.0", "fhir-sdc-helpers": "^0.1.0", "fhirclient": "^2.5.2", diff --git a/pages/Questionnaire/tester.vue b/pages/Questionnaire/tester.vue index b4dab7a..427f7c6 100644 --- a/pages/Questionnaire/tester.vue +++ b/pages/Questionnaire/tester.vue @@ -286,8 +286,12 @@ :suggestions="chatPromptOptions" @remove-suggestion="removeSuggestion" @reset-conversation="resetConversation" - @apply-suggested-expression="applySuggestedExpression" - @apply-suggested-questionnaire="applySuggestedExpression" + @apply-suggested-expression="copySuggestionToClipboard" + @apply-suggested-questionnaire="applySuggestedQuestionnaire" + @apply-suggested-item="applySuggestedItem" + @apply-suggested-fhir="copySuggestionToClipboard" + @apply-suggested-json="copySuggestionToClipboard" + @apply-suggested-jsonpatch="applySuggestedJsonPatch" /> @@ -395,7 +399,9 @@ import { GetSystemPrompt, IOpenAISettings, } from "~/helpers/openai_utils"; +import { DetectDataRequiredForQuery } from "~/helpers/openai_form_tester"; import TwinPaneTab, { TabData } from "~/components/TwinPaneTab.vue"; +import * as jsonpatch from 'fast-json-patch'; // import "fhirclient"; // import { FHIR } from "fhirclient"; @@ -1196,7 +1202,12 @@ export default Vue.extend({ } }, - applySuggestedExpression(updatedExpression: string): void { + copySuggestionToClipboard(suggestion: string) { + console.log('Copied suggestion to clipboard: ', suggestion) + navigator.clipboard.writeText(suggestion); + }, + + applySuggestedQuestionnaire(updatedExpression: string): void { // before blindly applying the updated text, do some cleaning of the context if (this.resourceJsonEditor) { const jsonValue = this.resourceJsonEditor.getValue(); @@ -1206,7 +1217,43 @@ export default Vue.extend({ ); this.resourceJsonEditor.clearSelection(); this.resourceJsonEditor.renderer.updateFull(true); - } catch {} + } catch(err) { + console.log("Error applying json patch: ", err); + } + } + }, + + applySuggestedItem(updatedExpression: string): void { + // before blindly applying the updated text, do some cleaning of the context + if (this.resourceJsonEditor) { + const jsonValue = this.resourceJsonEditor.getValue(); + try { + this.resourceJsonEditor.setValue( + JSON.stringify(JSON.parse(updatedExpression), null, 4) + ); + this.resourceJsonEditor.clearSelection(); + this.resourceJsonEditor.renderer.updateFull(true); + } catch(err) { + console.log("Error applying json patch: ", err); + } + } + }, + + applySuggestedJsonPatch(jsonPatchString: string){ + if (this.resourceJsonEditor) { + const jsonValue = this.resourceJsonEditor.getValue(); + try { + var jsonPatch = JSON.parse(jsonPatchString); + var jsonValueObj = JSON.parse(jsonValue); + jsonPatch.forEach((patch: any) => { + jsonValueObj = jsonpatch.applyPatch(jsonValueObj, [patch]).newDocument; + }); + this.resourceJsonEditor.setValue(JSON.stringify(jsonValueObj, null, 4)); + this.resourceJsonEditor.clearSelection(); + this.resourceJsonEditor.renderer.updateFull(true); + } catch(err) { + console.log("Error applying json patch: ", err); + } } },