Skip to content

Commit

Permalink
Medication sort (#41)
Browse files Browse the repository at this point in the history
# Medication sort

## ♻️ Current situation & Problem
Currently, the LLM can choose from all resources in the medications
category even though many are irrelevant to the user. Specifically, the
in-patient medications and `MedicationAdministration` resources are not
relevant.

## ⚙️ Release Notes 
A check for the presence of "inpatient" in the JSON description or
`MedicationAdministration` in the resource title was implemented in
[FHIRStore+Extensions.swift](main...medication_sort).
Resources which fit into one of these conditions were removed from the
sortedMedications.

**The relevantResources are still not being updated in the UI after the
sort**

### 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).

---------

Co-authored-by: Paul Schmiedmayer <[email protected]>
  • Loading branch information
AdritRao and PSchmiedmayer authored Dec 11, 2023
1 parent e74f9ef commit 98f1a87
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 54 deletions.
2 changes: 1 addition & 1 deletion LLMonFHIR.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@
repositoryURL = "https://github.com/StanfordSpezi/SpeziFHIR.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.5.2;
minimumVersion = 0.5.3;
};
};
2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/Spezi",
"state" : {
"revision" : "092eabc50a3600d8a03b43ad0d2dcd02914b223f",
"version" : "0.8.1"
"revision" : "d56ed07327cce8254b919665eb61e20bfd1467b0",
"version" : "0.8.2"
}
},
{
"identity" : "spezifhir",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziFHIR.git",
"state" : {
"revision" : "d60882bf6f91f2719f33d413e22d1fc3e6f32705",
"version" : "0.5.2"
"revision" : "07c2e56b8dd08bbf33ae4e3dfa2f2b9ca24807ea",
"version" : "0.5.3"
}
},
{
Expand Down Expand Up @@ -95,8 +95,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziViews.git",
"state" : {
"revision" : "eac443080926649d09a703483a6dd6f5a8bb7d51",
"version" : "0.6.2"
"revision" : "dcd0bc10ae8f22cba771ff9ab3ce16549f46f667",
"version" : "0.6.3"
}
},
{
Expand Down
3 changes: 3 additions & 0 deletions LLMonFHIR/FHIR Display/FHIRResourcesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ struct FHIRResourcesView: View {
} else {
resourcesSection
}
Section { } footer: {
Text("Total Number of Resources: \(fhirStore.allResources.count)")
}
}
.searchable(text: $searchText)
.navigationDestination(for: FHIRResource.self) { resource in
Expand Down
61 changes: 42 additions & 19 deletions LLMonFHIR/FHIR Interpretation/FHIRMultipleResourceInterpreter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,9 @@ class FHIRMultipleResourceInterpreter {
do {
viewState = .processing

if chat.isEmpty {
chat = [
Chat(
role: .system,
content: FHIRPrompt.interpretation.prompt
),
Chat(
role: .system,
content: String(
localized: "Content of the function context passed to the LLM",
comment: "The list of possible titles will be appended to the end of this prompt."
) + fhirStore.allResourcesFunctionCallIdentifier.rawValue
)
]
}
prepareSystemPrompt()

print("The Multiple Resource Interpreter has access to \(fhirStore.llmRelevantResources.count) resources.")

try await executeLLMQueries()

Expand All @@ -90,6 +78,29 @@ class FHIRMultipleResourceInterpreter {
}
}

private func prepareSystemPrompt() {
if chat.isEmpty {
chat = [
Chat(
role: .system,
content: FHIRPrompt.interpretMultipleResources.prompt
)
]
}

if let patient = fhirStore.patient {
chat.append(
Chat(
role: .system,
content: String(
localized: "Here is the JSON content of the patient resource as an initial context: \n\n \(patient.jsonDescription)",
comment: "System prompt used by the FHIRMultipleResourceInterpreter to pass in the patient JSON."
)
)
)
}
}

private func executeLLMQueries() async throws {
while true {
let chatStreamResults = try await openAIModel.queryAPI(withChat: chat, withFunction: functions)
Expand Down Expand Up @@ -165,11 +176,23 @@ class FHIRMultipleResourceInterpreter {
print("Parsed Resources: \(requestedResources)")

for requestedResource in requestedResources {
var fittingResources = fhirStore.allResources.filter { $0.functionCallIdentifier == requestedResource }
var fittingResources = fhirStore.llmRelevantResources.filter { $0.functionCallIdentifier.contains(requestedResource) }

guard !fittingResources.isEmpty else {
chat.append(
Chat(
role: .function,
content: String(localized: "The medical record does not include any FHIR resources for the search term \(requestedResource)."),
name: LLMFunction.getResourcesName
)
)
continue
}

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: ","))")
if fittingResources.count > 64 {
fittingResources = fittingResources.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(64)
print("Reduced to the following 64 resources: \(fittingResources.map { $0.functionCallIdentifier }.joined(separator: ","))")
}

for resource in fittingResources {
Expand Down
3 changes: 2 additions & 1 deletion LLMonFHIR/FHIR Interpretation/FHIRResource+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import SpeziFHIR
extension FHIRResource {
private static let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.dateFormat = "MM-dd-yyyy"
return dateFormatter
}()


var functionCallIdentifier: String {
resourceType.filter { !$0.isWhitespace }
+ displayName.filter { !$0.isWhitespace }
+ "-"
+ (date.map { FHIRResource.dateFormatter.string(from: $0) } ?? "")
}
}
131 changes: 117 additions & 14 deletions LLMonFHIR/FHIR Interpretation/FHIRStore+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,115 @@ import SwiftUI


extension FHIRStore {
var llmRelevantResources: [FHIRResource] {
allergyIntolerances
+ llmConditions
+ encounters.uniqueDisplayNames
+ immunizations
+ llmMedications
+ observations.uniqueDisplayNames
+ procedures.uniqueDisplayNames
}

var allResources: [FHIRResource] {
allergyIntolerances + conditions + diagnostics + encounters + immunizations + medications + observations + otherResources + procedures
allergyIntolerances
+ conditions
+ diagnostics
+ encounters
+ immunizations
+ medications
+ observations
+ otherResources
+ procedures
}

var patient: FHIRResource? {
otherResources
.first { resource in
guard case let .r4(resource) = resource.versionedResource,
resource is ModelsR4.Patient else {
return false
}

return true
}
}

private var llmConditions: [FHIRResource] {
conditions
.filter { resource in
guard case let .r4(resource) = resource.versionedResource,
let condition = resource as? ModelsR4.Condition else {
return false
}

return condition.clinicalStatus?.coding?.contains { coding in
guard coding.system?.value?.url == URL(string: "http://terminology.hl7.org/CodeSystem/condition-clinical"),
coding.code?.value?.string == "active" else {
return false
}

return true
} ?? false
}
}

private var llmMedications: [FHIRResource] {
func medicationRequest(resource: FHIRResource) -> MedicationRequest? {
guard case let .r4(resource) = resource.versionedResource,
let medicationRequest = resource as? ModelsR4.MedicationRequest else {
return nil
}

return medicationRequest
}

let outpatientMedications = medications
.filter { medication in
guard let medicationRequest = medicationRequest(resource: medication),
medicationRequest.category?
.contains(where: { codableconcept in
codableconcept.text?.value?.string.lowercased() == "outpatient"
})
?? false else {
return false
}

return true
}
.uniqueDisplayNames

let activeMedications = medications
.filter { medication in
guard let medicationRequest = medicationRequest(resource: medication),
medicationRequest.status == .active else {
return false
}

return true
}
.uniqueDisplayNames

return outpatientMedications + activeMedications
}

var allResourcesFunctionCallIdentifier: [String] {
@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

if llmRelevantResources.count > resourceLimit {
relevantResources = llmRelevantResources
.lazy
.filter {
$0.date != nil
}
.sorted {
$0.date ?? .distantPast < $1.date ?? .distantPast
}
.suffix(resourceLimit)
} else {
relevantResources = allResources
relevantResources = llmRelevantResources
}

return Array(Set(relevantResources.map { $0.functionCallIdentifier }))
Expand Down Expand Up @@ -61,6 +148,22 @@ extension FHIRStore {


extension Array where Element == FHIRResource {
fileprivate var uniqueDisplayNames: [FHIRResource] {
let reducedEncounters = Dictionary(
map { ($0.displayName, $0) },
uniquingKeysWith: { first, second in
if first.date ?? .distantFuture < second.date ?? .distantPast {
return second
} else {
return first
}
}
)

return Array(reducedEncounters.values)
}


fileprivate func dateSuffix(maxLength: Int) -> [FHIRResource] {
self.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(maxLength)
}
Expand Down
24 changes: 11 additions & 13 deletions LLMonFHIR/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@
}
}
},
"Content of the function context passed to the LLM" : {
"comment" : "The list of possible titles will be appended to the end of this prompt.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Use the function call 'get_resources' and provide a comma-separated list resource titles directly applicable to the question. The function will determine which FHIR health record titles are relevant to the user's question using this array: "
}
}
}
},
"Diagnostics" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -1179,6 +1168,9 @@
}
}
},
"Here is the JSON content of the patient resource as an initial context: \n\n %@" : {
"comment" : "System prompt used by the FHIRMultipleResourceInterpreter to pass in the patient JSON."
},
"Immunizations" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -1206,7 +1198,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 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."
"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\nE.g. if the user asks about their Medication it would be recommended to request all MedicationRequest FHIR resource types.\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 based on the FHIR patient resources if available to start the conversation. You may already request FHIR resources using the \"get_resources\" function.\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.\n"
}
}
}
Expand Down Expand Up @@ -1330,7 +1322,7 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Provide a comma-separated list of all the FHIR health record titles with the EXACT SAME NAME AS GIVEN IN THE LIST that are applicable to answer the user's questions. These titles have to be the SAME NAME AS GIVEN IN THE ARRAY provided. If multiple titles apply, separate each title by a comma and space (e.g for multiple medications). \n\nTry to provide all the required titles to allow yourself to fully answer the question in a comprehensive manner. \n\nFor example, if a user asks: 'Tell me more about my medications,' then output all titles associated with medications. A question can build upon a previous question and does not need to be explicit. e.g. if a user says prescribe, this is associated with medication. Do not exceed token limit with outputted titles."
"value" : "Provide a comma-separated list of all the FHIR health record titles with the exact same name as given in the list that are applicable to answer the user's questions. If multiple titles apply, separate each title by a comma and space (e.g for multiple medications). You can also request a larger set of FHIR resources by, e.g., just stating the resource type but this might not include all relevant resources to avoid exceeding the token limit.\n\nThe FHIR resource identifiers are composed of three elements:\n1. The FHIR resource type, e.g., MedicationRequest, Condition, and more.\n2. The title of the FHIR resource\n3. The date associated with the FHIR resource.\n\nUse these informations to determine the best possible FHIR resource for each question. Try to request all the required titles to allow yourself to fully answer the question in a comprehensive manner. \n\nFor example, if the user asks about their Medication it would be recommended to request all MedicationRequest FHIR resource types as well as other related FHIR resources.\n\nA question can build upon a previous question and does not need to be explicit. e.g. if a user says prescribe, this is associated with medication."
}
},
"es" : {
Expand Down Expand Up @@ -1804,6 +1796,9 @@
}
}
}
},
"The medical record does not include any FHIR resources for the search term %@." : {

},
"This is the summary of the requested %@:\n\n%@" : {
"localizations" : {
Expand All @@ -1814,6 +1809,9 @@
}
}
}
},
"Total Number of Resources: %lld" : {

},
"Use HealthKit Resources" : {
"localizations" : {
Expand Down

0 comments on commit 98f1a87

Please sign in to comment.