From 82c771f7f2a48f58ca2ddaa875c5249e66eb4a6b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 23 Nov 2023 02:54:55 +0100 Subject: [PATCH] Improve Public API and Improve FHIRResourceSummaryView (#12) # Improve Public API and Improve FHIRResourceSummaryView ## :gear: Release Notes - Improve Public API and Improve FHIRResourceSummaryView ## :pencil: 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). --- Sources/SpeziFHIR/FHIRStore.swift | 9 ++- ...lthKit.swift => FHIRStore+HealthKit.swift} | 12 ++-- .../FHIRResourceInterpreter.swift | 10 +++- .../FHIRResourceSummary.swift | 8 +++ .../FHIRResourceSummaryView.swift | 58 +++++++++++-------- .../Resources/Localizable.xcstrings | 30 ++++++---- .../Settings/FHIRPromptSettingsView.swift | 2 +- Tests/UITests/TestApp/PromptSettings.swift | 2 +- 8 files changed, 88 insertions(+), 43 deletions(-) rename Sources/SpeziFHIRHealthKit/{FHIR+HealthKit.swift => FHIRStore+HealthKit.swift} (85%) diff --git a/Sources/SpeziFHIR/FHIRStore.swift b/Sources/SpeziFHIR/FHIRStore.swift index 1eb135a..63d24f0 100644 --- a/Sources/SpeziFHIR/FHIRStore.swift +++ b/Sources/SpeziFHIR/FHIRStore.swift @@ -10,11 +10,14 @@ import Combine import Observation import class ModelsR4.Bundle import enum ModelsDSTU2.ResourceProxy +import Spezi -/// Manage FHIR resources grouped into automatically computed and updated categories. +/// Module to manage FHIR resources grouped into automatically computed and updated categories. +/// +/// The ``FHIRStore`` is automatically injected in the environment if you use the ``FHIR`` standard or can be used as a standalone module. @Observable -public class FHIRStore { +public class FHIRStore: Module, EnvironmentAccessible, DefaultInitializable { @ObservationIgnored private var _resources: [FHIRResource] @@ -73,7 +76,7 @@ public class FHIRStore { } - init() { + public required init() { self._resources = [] } diff --git a/Sources/SpeziFHIRHealthKit/FHIR+HealthKit.swift b/Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift similarity index 85% rename from Sources/SpeziFHIRHealthKit/FHIR+HealthKit.swift rename to Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift index db1c2d6..bbd5860 100644 --- a/Sources/SpeziFHIRHealthKit/FHIR+HealthKit.swift +++ b/Sources/SpeziFHIRHealthKit/FHIRStore+HealthKit.swift @@ -14,7 +14,7 @@ import SpeziFHIR import SpeziHealthKit -extension FHIR: HealthKitConstraint { +extension FHIRStore { private static let hkHealthStore: HKHealthStore? = { guard HKHealthStore.isHealthDataAvailable() else { return nil @@ -24,17 +24,21 @@ extension FHIR: HealthKitConstraint { }() + /// Add a HealthKit sample to the FHIR store. + /// - Parameter sample: The sample that should be added. public func add(sample: HKSample) async { do { let resource = try await transform(sample: sample) - store.insert(resource: resource) + insert(resource: resource) } catch { print("Could not transform HKSample: \(error)") } } + /// Remove a HealthKit sample delete object from the FHIR store. + /// - Parameter sample: The sample delete object that should be removed. public func remove(sample: HKDeletedObject) async { - store.remove(resource: sample.uuid.uuidString) + remove(resource: sample.uuid.uuidString) } @@ -52,7 +56,7 @@ extension FHIR: HealthKitConstraint { displayName: clinicalResource.displayName ) case let electrocardiogram as HKElectrocardiogram: - guard let hkHealthStore = FHIR.hkHealthStore else { + guard let hkHealthStore = Self.hkHealthStore else { fallthrough } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift index 2d5da8d..e9f01c6 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift @@ -39,9 +39,17 @@ public class FHIRResourceInterpreter { /// - forceReload: A boolean value that indicates whether to reload and reprocess the resource. /// - Returns: An asynchronous `String` representing the interpretation of the resource. @discardableResult - public func summarize(resource: FHIRResource, forceReload: Bool = false) async throws -> String { + public func interpret(resource: FHIRResource, forceReload: Bool = false) async throws -> String { try await resourceProcesser.process(resource: resource, forceReload: forceReload) } + + /// Retrieve the cached interpretation of a given FHIR resource. Returns a human-readable interpretation or `nil` if it is not present. + /// + /// - Parameter resource: The resource where the cached interpretation should be loaded from. + /// - Returns: The cached interpretation. Returns `nil` if the resource is not present. + public func cachedInterpretation(forResource resource: FHIRResource) -> String? { + resourceProcesser.results[resource.id] + } } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift index 4aca079..7443662 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift @@ -42,6 +42,14 @@ public class FHIRResourceSummary { public func summarize(resource: FHIRResource, forceReload: Bool = false) async throws -> String { 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? { + resourceProcesser.results[resource.id] + } } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift index 6576045..6704fa8 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift @@ -14,39 +14,51 @@ import SwiftUI /// Displays a FHIR resource, a summary if loaded, and provides a mechanism to load a summary using a context menu. public struct FHIRResourceSummaryView: View { @Environment(FHIRResourceSummary.self) private var fhirResourceSummary - @State private var summary: String? + @State private var viewState: ViewState = .idle private let resource: FHIRResource public var body: some View { - if let summary, !summary.isEmpty { - VStack(alignment: .leading, spacing: 4) { - Text(resource.displayName) - Text(summary) - .font(.caption) - } - .multilineTextAlignment(.leading) - } else { - VStack(alignment: .leading, spacing: 4) { - Text(resource.displayName) - if viewState == .processing { - ProgressView() - .progressViewStyle(.circular) - .padding(.vertical, 6) + Group { + if let summary = fhirResourceSummary.cachedSummary(forResource: resource) { + VStack(alignment: .leading, spacing: 4) { + Text(resource.displayName) + Text(summary) + .font(.caption) } - } - .contextMenu { - Button("Load Resource Summary") { - Task { - viewState = .processing - summary = try? await fhirResourceSummary.summarize(resource: resource) - viewState = .idle - } + .multilineTextAlignment(.leading) + } else { + VStack(alignment: .leading, spacing: 4) { + Text(resource.displayName) + if viewState == .processing { + ProgressView() + .progressViewStyle(.circular) + .padding(.vertical, 6) } } + .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) + ) + ) + } + } + } + } + } } + .viewStateAlert(state: $viewState) } diff --git a/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings b/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings index 5d22190..74cd538 100644 --- a/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings +++ b/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings @@ -1,44 +1,54 @@ { "sourceLanguage" : "en", "strings" : { - "Customize the %@." : { + "Could not create FHIR Summary" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Customize the %@." + "value" : "Could not create FHIR Summary" } } } }, - "Interpretation Prompt" : { - "comment" : "Title of the interpretation prompt.", + "Create Resource Summary" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Interpretation Prompt" + "value" : "Create Resource Summary" } } } }, - "Interpretation Prompt Content" : { - "comment" : "Content of the interpretation prompt.", + "Customize the %@." : { "localizations" : { "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" : "Customize the %@." + } + } + } + }, + "Interpretation Prompt" : { + "comment" : "Title of the interpretation prompt.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interpretation Prompt" } } } }, - "Load Resource Summary" : { + "Interpretation Prompt Content" : { + "comment" : "Content of the interpretation prompt.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Load Resource Summary" + "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}}" } } } diff --git a/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift b/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift index ef23542..bfd3597 100644 --- a/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift +++ b/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift @@ -12,7 +12,7 @@ import SwiftUI /// Customize LLM ``FHIRPrompt``s. /// /// Allows users to edit and save a prompt associated with a specific ``FHIRPrompt`` type, including where to insert FHIR resources dynamically in the prompt. -public struct PromptSettingsView: View { +public struct FHIRPromptSettingsView: View { private let promptType: FHIRPrompt private let onSave: () -> Void @State private var prompt: String = "" diff --git a/Tests/UITests/TestApp/PromptSettings.swift b/Tests/UITests/TestApp/PromptSettings.swift index 0cb616e..f6a77af 100644 --- a/Tests/UITests/TestApp/PromptSettings.swift +++ b/Tests/UITests/TestApp/PromptSettings.swift @@ -26,7 +26,7 @@ struct PromptSettings: View { } } .navigationDestination(for: FHIRPrompt.self) { prompt in - PromptSettingsView(promptType: prompt) { + FHIRPromptSettingsView(promptType: prompt) { print("Saved \(prompt.localizedDescription)") } }