Skip to content

Commit

Permalink
Improve Prompts, Display Name, Date, and Summary Model (#13)
Browse files Browse the repository at this point in the history
# Improve Prompts, Display Name, Date, and Summary Model

## ⚙️ Release Notes 
- Improve the summary and interpretation prompts
- Better holistic to generate a display name that tries to inspect the
resource for information
- Best effort to determine the date of the FHIR resource
- Improvement of the summary and interpretation models as
`FHIRResourceProcesser` now has a generic `Content` type.

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
PSchmiedmayer authored Nov 26, 2023
1 parent 786cf34 commit d60882b
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 35 deletions.
33 changes: 31 additions & 2 deletions Sources/SpeziFHIR/FHIRResource/FHIRResource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,34 @@ public struct FHIRResource: Sendable, Identifiable, Hashable {
switch versionedResource {
case let .r4(resource):
switch resource {
case let condition as ModelsR4.Condition:
guard case let .dateTime(date) = condition.onset else {
return nil
}
return try? date.value?.asNSDate()
case let diagnosticReport as ModelsR4.DiagnosticReport:
guard case let .dateTime(date) = diagnosticReport.effective else {
return nil
}
return try? date.value?.asNSDate()
case let encounter as ModelsR4.Encounter:
return try? encounter.period?.end?.value?.asNSDate()
case let immunization as ModelsR4.Immunization:
guard case let .dateTime(date) = immunization.occurrence else {
return nil
}
return try? date.value?.asNSDate()
case let medicationRequest as ModelsR4.MedicationRequest:
return try? medicationRequest.authoredOn?.value?.asNSDate()
case let observation as ModelsR4.Observation:
return try? observation.issued?.value?.asNSDate()
case let procedure as ModelsR4.Procedure:
guard case let .dateTime(date) = procedure.performed else {
return nil
}
return try? date.value?.asNSDate()
case is ModelsR4.Patient:
return .now
default:
return nil
}
Expand All @@ -66,8 +92,11 @@ public struct FHIRResource: Sendable, Identifiable, Hashable {
return try? observation.issued?.value?.asNSDate()
case let medicationOrder as ModelsDSTU2.MedicationOrder:
return try? medicationOrder.dateWritten?.value?.asNSDate()
case let condition as ModelsDSTU2.MedicationOrder:
return try? condition.dateWritten?.value?.asNSDate()
case let condition as ModelsDSTU2.Condition:
guard case let .dateTime(date) = condition.onset else {
return nil
}
return try? date.value?.asNSDate()
case let procedure as ModelsDSTU2.Procedure:
guard case let .dateTime(date) = procedure.performed else {
return nil
Expand Down
45 changes: 45 additions & 0 deletions Sources/SpeziFHIR/FHIRResource/ResourceProxy+DisplayName.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// This source file is part of the Stanford Spezi open source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import ModelsR4


extension ResourceProxy {
/// Provides a best-effort human readable display name for the resource.
public var displayName: String {
switch self {
case let .condition(condition):
return condition.code?.text?.value?.string ?? resourceType
case let .diagnosticReport(diagnosticReport):
return diagnosticReport.code.coding?.first?.display?.value?.string ?? resourceType
case let .encounter(encounter):
return encounter.reasonCode?.first?.coding?.first?.display?.value?.string
?? encounter.type?.first?.coding?.first?.display?.value?.string
?? resourceType
case let .immunization(immunization):
return immunization.vaccineCode.text?.value?.string ?? resourceType
case let .medicationRequest(medicationRequest):
guard case let .codeableConcept(medicationCodeableConcept) = medicationRequest.medication else {
return resourceType
}
return medicationCodeableConcept.text?.value?.string ?? resourceType
case let .observation(observation):
return observation.code.text?.value?.string ?? resourceType
case let .procedure(procedure):
return procedure.code?.text?.value?.string ?? resourceType
case let .patient(patient):
let name = (patient.name?.first?.given?.first?.value?.string ?? "") + (patient.name?.first?.family?.value?.string ?? "")
guard !name.isEmpty else {
return resourceType
}
return name
default:
return resourceType
}
}
}
2 changes: 1 addition & 1 deletion Sources/SpeziFHIR/FHIRStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public class FHIRStore: Module, EnvironmentAccessible, DefaultInitializable {
let resourceProxies = bundle.entry?.compactMap { $0.resource } ?? []

for resourceProxy in resourceProxies {
insert(resource: FHIRResource(resource: resourceProxy.get(), displayName: resourceProxy.resourceType))
insert(resource: FHIRResource(resource: resourceProxy.get(), displayName: resourceProxy.displayName))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SpeziOpenAI
/// Responsible for interpreting FHIR resources.
@Observable
public class FHIRResourceInterpreter {
private let resourceProcesser: FHIRResourceProcesser
private let resourceProcesser: FHIRResourceProcesser<String>


/// - Parameters:
Expand Down
16 changes: 10 additions & 6 deletions Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import SpeziOpenAI


@Observable
class FHIRResourceProcesser {
typealias Results = [FHIRResource.ID: String]
class FHIRResourceProcesser<Content: Codable & LosslessStringConvertible> {
typealias Results = [FHIRResource.ID: Content]


private let localStorage: LocalStorage
Expand Down Expand Up @@ -49,8 +49,8 @@ class FHIRResourceProcesser {


@discardableResult
func process(resource: FHIRResource, forceReload: Bool = false) async throws -> String {
if let result = results[resource.id], !result.isEmpty, !forceReload {
func process(resource: FHIRResource, forceReload: Bool = false) async throws -> Content {
if let result = results[resource.id], !result.description.isEmpty, !forceReload {
return result
}

Expand All @@ -63,8 +63,12 @@ class FHIRResourceProcesser {
}
}

results[resource.id] = result
return result
guard let content = Content(result) else {
throw FHIRResourceProcesserError.notParsableAsAString
}

results[resource.id] = content
return content
}

private func systemPrompt(forResource resource: FHIRResource) -> Chat {
Expand Down
26 changes: 26 additions & 0 deletions Sources/SpeziFHIRInterpretation/FHIRResourceProcesserError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// This source file is part of the Stanford Spezi open source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation


enum FHIRResourceProcesserError: LocalizedError {
case notParsableAsAString


var errorDescription: String? {
switch self {
case .notParsableAsAString:
String(
localized: "Unable to parse result of the LLM prompt.",
bundle: .module,
comment: "Error thrown if the result can not be parsed in the underlying type."
)
}
}
}
31 changes: 28 additions & 3 deletions Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,32 @@ import SpeziOpenAI
/// Responsible for summarizing FHIR resources.
@Observable
public class FHIRResourceSummary {
private let resourceProcesser: FHIRResourceProcesser
/// Summary of a FHIR resource emited by the ``FHIRResourceSummary``.
public struct Summary: Codable, LosslessStringConvertible {
/// Title of the FHIR resource, should be shorter than 4 words.
public let title: String
/// Summary of the FHIR resource, should be a single line of text.
public let summary: String


public var description: String {
title + "\n" + summary
}


public init?(_ description: String) {
let components = description.split(separator: "\n")
guard components.count == 2, let title = components.first, let summary = components.last else {
return nil
}

self.title = String(title)
self.summary = String(summary)
}
}


private let resourceProcesser: FHIRResourceProcesser<Summary>


/// - Parameters:
Expand All @@ -39,15 +64,15 @@ public class FHIRResourceSummary {
/// - forceReload: A boolean value that indicates whether to reload and reprocess the resource.
/// - Returns: An asynchronous `String` representing the summarization of the resource.
@discardableResult
public func summarize(resource: FHIRResource, forceReload: Bool = false) async throws -> String {
public func summarize(resource: FHIRResource, forceReload: Bool = false) async throws -> Summary {
try await resourceProcesser.process(resource: resource, forceReload: forceReload)
}

/// Retrieve the cached summary of a given FHIR resource. Returns a human-readable summary or `nil` if it is not present.
///
/// - Parameter resource: The resource where the cached summary should be loaded from.
/// - Returns: The cached summary. Returns `nil` if the resource is not present.
public func cachedSummary(forResource resource: FHIRResource) -> String? {
public func cachedSummary(forResource resource: FHIRResource) -> Summary? {
resourceProcesser.results[resource.id]
}
}
Expand Down
59 changes: 39 additions & 20 deletions Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,64 @@ public struct FHIRResourceSummaryView: View {
public var body: some View {
Group {
if let summary = fhirResourceSummary.cachedSummary(forResource: resource) {
VStack(alignment: .leading, spacing: 4) {
Text(resource.displayName)
Text(summary)
VStack(alignment: .leading, spacing: 0) {
Text(summary.title)
if let date = resource.date {
Text(date, style: .date)
.font(.caption2)
.foregroundStyle(.secondary)
.padding(.top, 2)
}
Text(summary.summary)
.font(.caption)
.padding(.top, 4)
}
.multilineTextAlignment(.leading)
} else {
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: 0) {
Text(resource.displayName)
if let date = resource.date {
Text(date, style: .date)
.font(.caption2)
.foregroundStyle(.secondary)
.padding(.top, 2)
}
if viewState == .processing {
ProgressView()
.progressViewStyle(.circular)
.padding(.vertical, 6)
.padding(.top, 4)
}
}
.contextMenu {
Button(String(localized: "Create Resource Summary", bundle: .module)) {
Task {
viewState = .processing
do {
try await fhirResourceSummary.summarize(resource: resource)
viewState = .idle
} catch {
viewState = .error(
AnyLocalizedError(
error: error,
defaultErrorDescription: String(localized: "Could not create FHIR Summary", bundle: .module)
)
)
}
}
}
contextMenu
}
}
}
.viewStateAlert(state: $viewState)
}


@ViewBuilder private var contextMenu: some View {
Button(String(localized: "Create Resource Summary", bundle: .module)) {
Task {
viewState = .processing
do {
try await fhirResourceSummary.summarize(resource: resource)
viewState = .idle
} catch {
viewState = .error(
AnyLocalizedError(
error: error,
defaultErrorDescription: String(localized: "Could not create FHIR Summary", bundle: .module)
)
)
}
}
}
}


public init(resource: FHIRResource) {
self.resource = resource
}
Expand Down
15 changes: 13 additions & 2 deletions Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Your task is to interpret the following FHIR resource from the user's clinical record.\n\nInterpret the resource 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.\nYou should provide factual and precise information in a compact summary in short responses.\n\nThe following JSON representation defines the FHIR resource that you should interpret:\n{{FHIR_RESOURCE}}\n\nImmediately return an interpretation to the user, starting the conversation.\nDo not introduce yourself at the beginning, and start with your interpretation.\n\nChat with the user in the same language they chat in.\nChat with the user in {{LOCALE}}"
"value" : "Your task is to interpret the following FHIR resource from the user's clinical record. You should provide the title and summary in the following locale: {{LOCALE}}.\n\nInterpret the resource 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.\nYou should provide factual and precise information in a compact summary in short responses.\n\nImmediately return an interpretation to the user, starting the conversation.\nDo not introduce yourself at the beginning, and start with your interpretation.\n\nThe following JSON representation defines the FHIR resource that you should provide an interpretation for:\n\n{{FHIR_RESOURCE}}"
}
}
}
Expand Down Expand Up @@ -90,7 +90,18 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Your task is to interpret FHIR resources from the user's clinical records.\nThroughout the conversation with the user, use the get_resource_titles 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 get_resource_titles to output the FHIR resource titles for allergy records so you can then use them to answer the question. The output of get_resource_titles has to be the name of a resource or resources with the exact same title as in the list provided.\n\nAnswer the users question, the health record provided is not always related. The end goal is to answer the users question in the best way possible.\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.\nYou should provide factual and precise information in a compact summary in short responses.\n\nTell the user that they can ask any question about their health records and then create a short summary of the main categories of health records of the user which you have access to. These are the resource titles:\n{{FHIR_RESOURCE}}\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 with the following specifications:\n1. Short summary of the health records categories\n2. 5th grade reading level\n3. End with a question asking user if they have any questions. Make sure that this question is not generic but specific to their health records.\nDo not introduce yourself at the beginning, and start with your interpretation.\nMake sure your response is in the same language the user writes to you in.\nThe tense should be present. \n\nChat with the user in the same language they chat in.\nChat with the user in {{LOCALE}}"
"value" : "Your task is to private a title and compact summary for an FHIR resource from the user's clinical record. You should provide the title and summary in the following locale: {{LOCALE}}.\n\nYour response should contain two lines without headings, markdown formatting, or any other structure beyond two lines. Directly provide the content without any additional structure or an introduction. Another computer program will parse the output.\n\n1. Line: A 1-5 Word summary of the FHIR resource that immediately identifies the resource and provides the essential information at a glance. Do NOT use a complete sentence; instead, use a formatting typically used for titles in computer systems.\n\n2. Line: Provide a short one-sentence summary of the resource in less than 40 words. Ensure that the summary only focuses on the essential information that a patient would need and, e.g., excludes the person who prescribed a medication or similar metadata. Ensure that all clinically relevant data is included, which allows us to use the summary in additional prompts where using the complete JSON might not be feasible.\n\nThe following JSON representation defines the FHIR resource that you should provide a title and summary for:\n\n{{FHIR_RESOURCE}}"
}
}
}
},
"Unable to parse result of the LLM prompt." : {
"comment" : "Error thrown if the result can not be parsed in the underlying type.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unable to parse result of the LLM prompt."
}
}
}
Expand Down

0 comments on commit d60882b

Please sign in to comment.