From a24b5bc070cf9f0dfe6cefeadf2e1a29f83f7d70 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 26 Nov 2023 15:26:16 +0100 Subject: [PATCH] FHIR 0.5.2 and Improved Resource Handling (#39) # FHIR 0.5.2 and Improved Resource Handling ## :gear: Release Notes - Update to Spezi FHIR 0.5.2 and all included improvements: https://github.com/StanfordSpezi/SpeziFHIR/releases/tag/0.5.2 - Display the data in the overview - Improve the resource selection and reenable the resource limit - Improve the prompts and resource selection ### Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md). --- LLMonFHIR.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../FHIR Display/InspectResourceView.swift | 24 +++++++++++---- .../FHIRMultipleResourceInterpreter.swift | 17 ++++++++--- .../FHIRResource+Extensions.swift | 11 ++++++- .../FHIRStore+Extensions.swift | 29 ++++++++++++++++++- LLMonFHIR/Resources/Localizable.xcstrings | 26 +++++++++++++++-- ...e-a2d0-d016-0839-bab3757c4c58.json.license | 2 +- LLMonFHIR/Settings/SettingsView.swift | 9 ++++++ LLMonFHIR/SharedContext/StorageKeys.swift | 2 +- LLMonFHIRUITests/FHIRDisplayTests.swift | 2 +- 11 files changed, 107 insertions(+), 21 deletions(-) diff --git a/LLMonFHIR.xcodeproj/project.pbxproj b/LLMonFHIR.xcodeproj/project.pbxproj index a594c53..3cc0cdf 100644 --- a/LLMonFHIR.xcodeproj/project.pbxproj +++ b/LLMonFHIR.xcodeproj/project.pbxproj @@ -1110,7 +1110,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziFHIR.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.5.0; + minimumVersion = 0.5.2; }; }; 2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */ = { diff --git a/LLMonFHIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LLMonFHIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8d0a2f6..5713477 100644 --- a/LLMonFHIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LLMonFHIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFHIR.git", "state" : { - "revision" : "786cf34055340aeb887265781aab5703bd685359", - "version" : "0.5.1" + "revision" : "d60882bf6f91f2719f33d413e22d1fc3e6f32705", + "version" : "0.5.2" } }, { diff --git a/LLMonFHIR/FHIR Display/InspectResourceView.swift b/LLMonFHIR/FHIR Display/InspectResourceView.swift index 66c01ba..3c0e3fd 100644 --- a/LLMonFHIR/FHIR Display/InspectResourceView.swift +++ b/LLMonFHIR/FHIR Display/InspectResourceView.swift @@ -45,13 +45,25 @@ struct InspectResourceView: View { Spacer() } } else if let summary = fhirResourceSummary.cachedSummary(forResource: resource) { - Text(summary) - .multilineTextAlignment(.leading) - .contextMenu { - Button("FHIR_RESOURCES_SUMMARY_BUTTON") { - loadSummary(forceReload: true) - } + VStack { + HStack(spacing: 0) { + Text(summary.title) + .font(.title2) + .multilineTextAlignment(.leading) + .bold() + Spacer() } + HStack(spacing: 0) { + Text(summary.summary) + .multilineTextAlignment(.leading) + .contextMenu { + Button("FHIR_RESOURCES_SUMMARY_BUTTON") { + loadSummary(forceReload: true) + } + } + Spacer() + } + } } else { Button("FHIR_RESOURCES_SUMMARY_BUTTON") { loadSummary() diff --git a/LLMonFHIR/FHIR Interpretation/FHIRMultipleResourceInterpreter.swift b/LLMonFHIR/FHIR Interpretation/FHIRMultipleResourceInterpreter.swift index 3742332..b75cfe5 100644 --- a/LLMonFHIR/FHIR Interpretation/FHIRMultipleResourceInterpreter.swift +++ b/LLMonFHIR/FHIR Interpretation/FHIRMultipleResourceInterpreter.swift @@ -141,7 +141,7 @@ class FHIRMultipleResourceInterpreter { switch functionCall.name { case LLMFunction.getResourcesName: - callGetResources(functionCall: functionCall) + try await callGetResources(functionCall: functionCall) default: break } @@ -150,7 +150,7 @@ class FHIRMultipleResourceInterpreter { } - private func callGetResources(functionCall: LLMStreamResult.FunctionCall) { + private func callGetResources(functionCall: LLMStreamResult.FunctionCall) async throws { struct Response: Codable { let resources: String } @@ -165,12 +165,21 @@ class FHIRMultipleResourceInterpreter { print("Parsed Resources: \(requestedResources)") for requestedResource in requestedResources { - for resource in fhirStore.allResources.filter({ $0.functionCallIdentifier == requestedResource }) { + var fittingResources = fhirStore.allResources.filter { $0.functionCallIdentifier == requestedResource } + print("Fitting Resources: \(fittingResources.count)") + if fittingResources.count > 20 { + fittingResources = fittingResources.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(10) + print("Reduced to the following 20 resources: \(fittingResources.map { $0.functionCallIdentifier }.joined(separator: ","))") + } + + for resource in fittingResources { print("Appending Resource: \(resource)") + let summary = try await resourceSummary.summarize(resource: resource) + print("Summary of Resource generated: \(summary)") chat.append( Chat( role: .function, - content: String(localized: "This is the content of the requested \(requestedResource):\n\n\(resource.jsonDescription)"), + content: String(localized: "This is the summary of the requested \(requestedResource):\n\n\(summary.description)"), name: LLMFunction.getResourcesName ) ) diff --git a/LLMonFHIR/FHIR Interpretation/FHIRResource+Extensions.swift b/LLMonFHIR/FHIR Interpretation/FHIRResource+Extensions.swift index 866d851..e120e7b 100644 --- a/LLMonFHIR/FHIR Interpretation/FHIRResource+Extensions.swift +++ b/LLMonFHIR/FHIR Interpretation/FHIRResource+Extensions.swift @@ -6,11 +6,20 @@ // SPDX-License-Identifier: MIT // +import Foundation import SpeziFHIR extension FHIRResource { + private static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + return dateFormatter + }() + var functionCallIdentifier: String { - resourceType.filter { !$0.isWhitespace } + displayName.filter { !$0.isWhitespace } + resourceType.filter { !$0.isWhitespace } + + displayName.filter { !$0.isWhitespace } + + (date.map { FHIRResource.dateFormatter.string(from: $0) } ?? "") } } diff --git a/LLMonFHIR/FHIR Interpretation/FHIRStore+Extensions.swift b/LLMonFHIR/FHIR Interpretation/FHIRStore+Extensions.swift index c51d437..3e0f9c2 100644 --- a/LLMonFHIR/FHIR Interpretation/FHIRStore+Extensions.swift +++ b/LLMonFHIR/FHIR Interpretation/FHIRStore+Extensions.swift @@ -8,6 +8,7 @@ import ModelsR4 import SpeziFHIR +import SwiftUI extension FHIRStore { @@ -16,7 +17,26 @@ extension FHIRStore { } var allResourcesFunctionCallIdentifier: [String] { - Array(Set(allResources.map { $0.functionCallIdentifier })) + @AppStorage(StorageKeys.resourceLimit) var resourceLimit = StorageKeys.Defaults.resourceLimit + + let relevantResources: [FHIRResource] + if allResources.count > resourceLimit { + var limitedResources: [FHIRResource] = [] + limitedResources.append(contentsOf: allergyIntolerances.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: conditions.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: diagnostics.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: encounters.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: immunizations.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: medications.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: observations.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: otherResources.dateSuffix(maxLength: resourceLimit / 9)) + limitedResources.append(contentsOf: procedures.dateSuffix(maxLength: resourceLimit / 9)) + relevantResources = limitedResources + } else { + relevantResources = allResources + } + + return Array(Set(relevantResources.map { $0.functionCallIdentifier })) } @@ -38,3 +58,10 @@ extension FHIRStore { } } } + + +extension Array where Element == FHIRResource { + fileprivate func dateSuffix(maxLength: Int) -> [FHIRResource] { + self.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(maxLength) + } +} diff --git a/LLMonFHIR/Resources/Localizable.xcstrings b/LLMonFHIR/Resources/Localizable.xcstrings index dfb8bf0..7d84bd7 100644 --- a/LLMonFHIR/Resources/Localizable.xcstrings +++ b/LLMonFHIR/Resources/Localizable.xcstrings @@ -1206,7 +1206,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "You are the LLM on FHIR application.\nYour task is to interpret FHIR resources from the user's clinical records.\n\nThroughout the conversation with the user, use the \"get_resources\" function to obtain the FHIR health resources necessary to properly answer the users question. For example, if the user asks about their allergies, you must use the \"get_resources\" function to output the FHIR resource titles for allergy records so you can then use them to answer the question. The end goal is to answer the users question in the best way possible while taking the FHIR resources obtained using \"get_resources\" into consideration.\n\nThese are the resource titles of the resources you can request using \"get_resources\":\n{{FHIR_RESOURCE}}\n\nInterpret the resources by explaining its data relevant to the user's health.\nExplain the relevant medical context in a language understandable by a user who is not a medical professional, ideally in a 5th grade reading level.\nYou should provide factual and precise information in a compact summary in short responses.\n\nDo not introduce yourself at the beginning, and start with your interpretation. \n\nImmediately return a short summary of the users health records to start the conversation.\nThe initial summary should be a short and simple summary and NOT just list the titles of the records.\nEnd with a question asking user if they have any questions. Make sure that this question is not generic but specific to their health records.\nMake sure your response is in the same language the user writes to you in.\nThe tense should be present." + "value" : "You are the LLM on FHIR application.\nYour task is to interpret FHIR resources from the user's clinical records.\n\nThroughout the conversation with the user, use the \"get_resources\" function to obtain the FHIR health resources necessary to answer the user's question properly. For example, if the user asks about their allergies, you must use the \"get_resources\" function to output the FHIR resource titles for allergy records so you can then use them to answer the question. The end goal is to answer the user's question in the best way possible while taking the FHIR resources obtained using \"get_resources\" into consideration.\n\nIf there is a Patient FHIR resource available, ensure that you request this resource first to get adequate information about the patient. Only request relevant resources and focus on recent resources. Try to reduce the number of requested resources to a reasonable scope.\n\nThese are the resource titles of the resources you can request using \"get_resources\":\n{{FHIR_RESOURCE}}\n\nInterpret the resources by explaining the data relevant to the user's health.\nExplain the relevant medical context in a language understandable by a user who is not a medical professional, ideally at a 5th-grade reading level.\nYou should provide factual and precise information in a compact summary in short responses.\n\nDo not introduce yourself at the beginning, and start with your interpretation. \n\nImmediately return a summary of the user's health records to start the conversation.\nThe initial summary should be short and simple and NOT list the records' titles or any resource's detailed content; stay at a high level.\nEnd with a question asking the user if they have any questions. Make sure that this question is not generic but specific to their health records.\nMake sure your response is in the same language the user writes to you in.\nThe tense should be present." } } } @@ -1373,6 +1373,26 @@ } } }, + "Resource Limit" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resource Limit" + } + } + } + }, + "Resource Limit %lld" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resource Limit %lld" + } + } + } + }, "Resource Selection" : { "localizations" : { "en" : { @@ -1785,12 +1805,12 @@ } } }, - "This is the content of the requested %@:\n\n%@" : { + "This is the summary of the requested %@:\n\n%@" : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "This is the content of the requested %1$@:\n\n%2$@" + "value" : "This is the summary of the requested %1$@:\n\n%2$@" } } } diff --git a/LLMonFHIR/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license b/LLMonFHIR/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license index 83d2922..750aeae 100644 --- a/LLMonFHIR/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license +++ b/LLMonFHIR/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license @@ -5,4 +5,4 @@ SPDX-FileCopyrightText: 2023 Stanford University SPDX-License-Identifier: MIT -The patient mock data is generated by Synthea: https://github.com/synthetichealth/synthea: \ No newline at end of file +The patient mock data is generated by Synthea: https://github.com/synthetichealth/synthea: Jason Walonoski, Mark Kramer, Joseph Nichols, Andre Quina, Chris Moesel, Dylan Hall, Carlton Duffett, Kudakwashe Dube, Thomas Gallagher, Scott McLachlan, Synthea: An approach, method, and software mechanism for generating synthetic patients and the synthetic electronic health care record, Journal of the American Medical Informatics Association, Volume 25, Issue 3, March 2018, Pages 230–238, https://doi.org/10.1093/jamia/ocx079 diff --git a/LLMonFHIR/Settings/SettingsView.swift b/LLMonFHIR/Settings/SettingsView.swift index 9445101..a69358b 100644 --- a/LLMonFHIR/Settings/SettingsView.swift +++ b/LLMonFHIR/Settings/SettingsView.swift @@ -32,6 +32,7 @@ struct SettingsView: View { List { openAISettings speechSettings + resourcesLimitSettings resourcesSettings promptsSettings } @@ -57,6 +58,14 @@ struct SettingsView: View { } } + private var resourcesLimitSettings: some View { + Section("Resource Limit") { + Stepper(value: $resourceLimit, in: 10...2000, step: 10) { + Text("Resource Limit \(resourceLimit)") + } + } + } + private var resourcesSettings: some View { Section("Resource Selection") { NavigationLink(value: SettingsDestinations.resourceSelection) { diff --git a/LLMonFHIR/SharedContext/StorageKeys.swift b/LLMonFHIR/SharedContext/StorageKeys.swift index 2b8d362..34a6b5e 100644 --- a/LLMonFHIR/SharedContext/StorageKeys.swift +++ b/LLMonFHIR/SharedContext/StorageKeys.swift @@ -10,7 +10,7 @@ enum StorageKeys { enum Defaults { static let enableTextToSpeech = false - static let resourceLimit = 50 + static let resourceLimit = 250 } diff --git a/LLMonFHIRUITests/FHIRDisplayTests.swift b/LLMonFHIRUITests/FHIRDisplayTests.swift index e121f4b..fe69e35 100644 --- a/LLMonFHIRUITests/FHIRDisplayTests.swift +++ b/LLMonFHIRUITests/FHIRDisplayTests.swift @@ -25,7 +25,7 @@ final class FHIRDisplayTests: XCTestCase { app.swipeUp() - let mockResource = app.otherElements.buttons["Mock Resource"] + let mockResource = app.staticTexts["Mock Resource"] XCTAssertTrue(mockResource.exists, "The 'Mock Resource' does not exist.") mockResource.tap()