diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index bcd848b..2ff4fed 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -14,21 +14,6 @@ on: workflow_call: jobs: - reuse_action: - name: REUSE Compliance Check - uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2 - swiftlint: - name: SwiftLint - uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2 - codeql: - name: CodeQL - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - codeql: true - fastlanelane: codeql - permissions: - security-events: write - actions: read buildandtest: name: Build and Test uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -43,4 +28,4 @@ jobs: with: coveragereports: LLMonFHIR.xcresult secrets: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..7d537f1 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,42 @@ +# +# This source file is part of the Stanford LLM on FHIR project +# +# SPDX-FileCopyrightText: 2025 Stanford University +# +# SPDX-License-Identifier: MIT +# + +name: Static Analysis + +on: + pull_request: + workflow_dispatch: + workflow_call: + +jobs: + reuse_action: + name: REUSE Compliance Check + uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2 + permissions: + contents: read + swiftlint: + name: SwiftLint + uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2 + permissions: + contents: read + periphery: + name: Periphery + uses: StanfordBDHG/.github/.github/workflows/periphery.yml@v2 + permissions: + contents: read + with: + runsonlabels: '["macOS", "self-hosted"]' + codeql: + name: CodeQL + uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + codeql: true + fastlanelane: codeql + permissions: + security-events: write + actions: read \ No newline at end of file diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..04b3979 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,11 @@ +# +# This source file is part of the Stanford LLM on FHIR project +# +# SPDX-FileCopyrightText: 2025 Stanford University +# +# SPDX-License-Identifier: MIT +# + +project: LLMonFHIR.xcodeproj +schemes: +- LLMonFHIR diff --git a/LLMonFHIR.xcodeproj/project.pbxproj b/LLMonFHIR.xcodeproj/project.pbxproj index 4d70779..9542fcc 100644 --- a/LLMonFHIR.xcodeproj/project.pbxproj +++ b/LLMonFHIR.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */; }; 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; }; 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */; }; - 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; 2FE5DC7229EDD8D3004B9AB4 /* SpeziHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7129EDD8D3004B9AB4 /* SpeziHealthKit */; }; 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */; }; @@ -39,7 +38,6 @@ 44E18E8E2CEBEC9500829A26 /* SpeziLLMLocalDownload in Frameworks */ = {isa = PBXBuildFile; productRef = 44E18E8D2CEBEC9500829A26 /* SpeziLLMLocalDownload */; }; 44E18E902CEBEC9500829A26 /* SpeziLLMOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 44E18E8F2CEBEC9500829A26 /* SpeziLLMOpenAI */; }; 44E18E932CEBF29200829A26 /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 44E18E922CEBF29200829A26 /* SpeziLocalStorage */; }; - 44E18E952CEBFD3C00829A26 /* InterpretationModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E18E942CEBFD3A00829A26 /* InterpretationModelType.swift */; }; 44E55E9E2CEBC191008A7BD3 /* FHIRGetResourceLLMFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E55E802CEBC191008A7BD3 /* FHIRGetResourceLLMFunction.swift */; }; 44E55E9F2CEBC191008A7BD3 /* FHIRResourceProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E55E872CEBC191008A7BD3 /* FHIRResourceProcessor.swift */; }; 44E55EA02CEBC191008A7BD3 /* InspectResourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E55E982CEBC191008A7BD3 /* InspectResourceView.swift */; }; @@ -115,9 +113,7 @@ 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageKeys.swift; sourceTree = ""; }; - 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Image.swift"; sourceTree = ""; }; 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodableArray+RawRepresentable.swift"; sourceTree = ""; }; - 44E18E942CEBFD3A00829A26 /* InterpretationModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationModelType.swift; sourceTree = ""; }; 44E55E802CEBC191008A7BD3 /* FHIRGetResourceLLMFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRGetResourceLLMFunction.swift; sourceTree = ""; }; 44E55E812CEBC191008A7BD3 /* FHIRMultipleResourceInterpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRMultipleResourceInterpreter.swift; sourceTree = ""; }; 44E55E822CEBC191008A7BD3 /* MultipleResourcesChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleResourcesChatView.swift; sourceTree = ""; }; @@ -267,7 +263,6 @@ 44E55E8D2CEBC191008A7BD3 /* Binding+Negate.swift */, 44E55E8E2CEBC191008A7BD3 /* FHIRResource+Identifier.swift */, 44E55E8F2CEBC191008A7BD3 /* FHIRStore+Interpretation.swift */, - 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */, 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */, ); path = Helper; @@ -294,7 +289,6 @@ 44E55E862CEBC191008A7BD3 /* FHIRInterpretation */ = { isa = PBXGroup; children = ( - 44E18E942CEBFD3A00829A26 /* InterpretationModelType.swift */, 44E55E832CEBC191008A7BD3 /* MultipleResources */, 44E55E852CEBC191008A7BD3 /* SingleResource */, ); @@ -610,7 +604,6 @@ 44E55EA52CEBC191008A7BD3 /* FHIRStore+Interpretation.swift in Sources */, 44E55EA62CEBC191008A7BD3 /* MultipleResourcesChatView.swift in Sources */, 44E55EA72CEBC191008A7BD3 /* FHIRResource+Identifier.swift in Sources */, - 44E18E952CEBFD3C00829A26 /* InterpretationModelType.swift in Sources */, 44E55EA82CEBC191008A7BD3 /* FHIRResourceInterpreter.swift in Sources */, 44E55EA92CEBC191008A7BD3 /* FHIRResourceSummaryView.swift in Sources */, 44E55EAA2CEBC191008A7BD3 /* FHIRResourcesView.swift in Sources */, @@ -623,7 +616,6 @@ 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, 97B6C1A52B7F382C0092B0F4 /* FHIRResourcesInstructionsView.swift in Sources */, - 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, 2F4E23832989D51F0013F3D9 /* LLMonFHIRTestingSetup.swift in Sources */, 2FD8E8272A1AAD9B00357F4E /* LLMonFHIRStandard.swift in Sources */, 2F5E32BD297E05EA003432F8 /* LLMonFHIRDelegate.swift in Sources */, diff --git a/LLMonFHIR/FHIR Views/FHIRResourcesView.swift b/LLMonFHIR/FHIR Views/FHIRResourcesView.swift index 39a89a2..e729c91 100644 --- a/LLMonFHIR/FHIR Views/FHIRResourcesView.swift +++ b/LLMonFHIR/FHIR Views/FHIRResourcesView.swift @@ -37,7 +37,7 @@ import SwiftUI /// } /// } /// ``` -public struct FHIRResourcesView: View { +struct FHIRResourcesView: View { @Environment(FHIRStore.self) private var fhirStore @State private var searchText = "" @@ -46,7 +46,7 @@ public struct FHIRResourcesView: View { private let actionView: ActionView - public var body: some View { + var body: some View { List { Section { contentView @@ -93,7 +93,7 @@ public struct FHIRResourcesView: View { /// - navigationTitle: The localized title displayed for purposes of navigation. /// - contentView: A custom content `View` that is displayed as the first `Section` of the `List`. /// - actionView: A custom action `View` that is displayed as the second `Section` of the `List`. Only shown if no search `String` is present. - public init( + init( navigationTitle: LocalizedStringResource, @ViewBuilder contentView: () -> ContentView = { EmptyView() }, @ViewBuilder _ actionView: () -> ActionView = { EmptyView() } @@ -110,7 +110,7 @@ public struct FHIRResourcesView: View { /// - contentView: A custom content `View` that is displayed as the first `Section` of the `List`. /// - actionView: A custom action `View` that is displayed as the second `Section` of the `List`. Only shown if no search `String` is present. @_disfavoredOverload - public init( + init( navigationTitle: Title, @ViewBuilder contentView: () -> ContentView = { EmptyView() }, @ViewBuilder _ actionView: () -> ActionView = { EmptyView() } diff --git a/LLMonFHIR/FHIRInterpretation/InterpretationModelType.swift b/LLMonFHIR/FHIRInterpretation/InterpretationModelType.swift deleted file mode 100644 index f03ce4c..0000000 --- a/LLMonFHIR/FHIRInterpretation/InterpretationModelType.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// This source file is part of the Stanford Spezi project -// -// SPDX-FileCopyrightText: 2024 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import struct OpenAI.Model -import enum SpeziLLMLocal.LLMLocalModel - -/// A type that represents different kinds of interpretation models that can be used within the application. -/// `InterpretationModelType` provides a way to specify whether the interpretation -/// should be handled by OpenAI's or local LLM models. -public enum InterpretationModelType { - /// Represents an OpenAI-provided model. - /// - Parameter model: The specific OpenAI model to use for interpretation - case openAI(OpenAI.Model) - - /// Represents a locally available LLM model. - /// - Parameter model: The specific local model to use for interpretation - case local(LLMLocalModel) -} diff --git a/LLMonFHIR/FHIRInterpretation/MultipleResources/FHIRMultipleResourceInterpreter.swift b/LLMonFHIR/FHIRInterpretation/MultipleResources/FHIRMultipleResourceInterpreter.swift index bb966ba..2d883ac 100644 --- a/LLMonFHIR/FHIRInterpretation/MultipleResources/FHIRMultipleResourceInterpreter.swift +++ b/LLMonFHIR/FHIRInterpretation/MultipleResources/FHIRMultipleResourceInterpreter.swift @@ -24,7 +24,7 @@ private enum FHIRMultipleResourceInterpreterConstants { /// Used to interpret multiple FHIR resources via a chat-based interface with an LLM. @Observable -public class FHIRMultipleResourceInterpreter { +class FHIRMultipleResourceInterpreter { static let logger = Logger(subsystem: "edu.stanford.spezi.fhir", category: "SpeziFHIRLLM") private let localStorage: LocalStorage @@ -102,7 +102,7 @@ public class FHIRMultipleResourceInterpreter { /// Change the `LLMSchema` used by the ``FHIRMultipleResourceInterpreter``. @MainActor - public func changeLLMSchema( + func changeLLMSchema( openAIModel model: LLMOpenAIModelType, resourceCountLimit: Int, resourceSummary: FHIRResourceSummary, @@ -135,7 +135,7 @@ extension FHIRPrompt { /// Prompt used to interpret multiple FHIR resources /// /// This prompt is used by the ``FHIRMultipleResourceInterpreter``. - public static let interpretMultipleResources: FHIRPrompt = { + static let interpretMultipleResources: FHIRPrompt = { FHIRPrompt( storageKey: "prompt.interpretMultipleResources", localizedDescription: String( diff --git a/LLMonFHIR/FHIRInterpretation/MultipleResources/MultipleResourcesChatView.swift b/LLMonFHIR/FHIRInterpretation/MultipleResources/MultipleResourcesChatView.swift index 33db7c6..26a4abe 100644 --- a/LLMonFHIR/FHIRInterpretation/MultipleResources/MultipleResourcesChatView.swift +++ b/LLMonFHIR/FHIRInterpretation/MultipleResources/MultipleResourcesChatView.swift @@ -15,7 +15,7 @@ import SpeziViews import SwiftUI -public struct MultipleResourcesChatView: View { +struct MultipleResourcesChatView: View { @Environment(FHIRMultipleResourceInterpreter.self) private var multipleResourceInterpreter @Environment(\.dismiss) private var dismiss @@ -23,7 +23,7 @@ public struct MultipleResourcesChatView: View { private let navigationTitle: Text - public var body: some View { + var body: some View { @Bindable var multipleResourceInterpreter = multipleResourceInterpreter NavigationStack { Group { @@ -88,25 +88,11 @@ public struct MultipleResourcesChatView: View { /// - Parameters: /// - navigationTitle: The localized title displayed for purposes of navigation. /// - textToSpeech: Indicates if the output of the LLM is converted to speech and outputted to the user. - public init( + init( navigationTitle: LocalizedStringResource, textToSpeech: Binding ) { self.navigationTitle = Text(navigationTitle) self._textToSpeech = textToSpeech } - - /// Creates a ``MultipleResourcesChatView`` displaying a Spezi `Chat` with all available FHIR resources via a Spezi LLM.. - /// - /// - Parameters: - /// - navigationTitle: The title displayed for purposes of navigation. - /// - textToSpeech: Indicates if the output of the LLM is converted to speech and outputted to the user. - @_disfavoredOverload - public init( - navigationTitle: Title, - textToSpeech: Binding - ) { - self.navigationTitle = Text(verbatim: String(navigationTitle)) - self._textToSpeech = textToSpeech - } } diff --git a/LLMonFHIR/FHIRInterpretation/SingleResource/FHIRResourceInterpreter.swift b/LLMonFHIR/FHIRInterpretation/SingleResource/FHIRResourceInterpreter.swift index 6909192..920b0b5 100644 --- a/LLMonFHIR/FHIRInterpretation/SingleResource/FHIRResourceInterpreter.swift +++ b/LLMonFHIR/FHIRInterpretation/SingleResource/FHIRResourceInterpreter.swift @@ -14,14 +14,14 @@ import SpeziLocalStorage /// Responsible for interpreting FHIR resources. @Observable -public final class FHIRResourceInterpreter: Sendable { +final class FHIRResourceInterpreter: Sendable { private let resourceProcessor: FHIRResourceProcessor /// - Parameters: /// - localStorage: Local storage module that needs to be passed to the ``FHIRResourceInterpreter`` to allow it to cache interpretations. /// - openAIModel: OpenAI module that needs to be passed to the ``FHIRResourceInterpreter`` to allow it to retrieve interpretations. - public init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) { + init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) { self.resourceProcessor = FHIRResourceProcessor( localStorage: localStorage, llmRunner: llmRunner, @@ -39,7 +39,7 @@ public final class FHIRResourceInterpreter: Sendable { /// - 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 interpret(resource: FHIRResource, forceReload: Bool = false) async throws -> String { + func interpret(resource: FHIRResource, forceReload: Bool = false) async throws -> String { try await resourceProcessor.process(resource: resource, forceReload: forceReload) } @@ -47,7 +47,7 @@ public final class FHIRResourceInterpreter: Sendable { /// /// - 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? { + func cachedInterpretation(forResource resource: FHIRResource) -> String? { resourceProcessor.results[resource.id] } @@ -55,7 +55,7 @@ public final class FHIRResourceInterpreter: Sendable { /// /// - Parameters: /// - schema: The to-be-used `LLMSchema`. - public func changeLLMSchema(to schema: Schema) { + func changeLLMSchema(to schema: Schema) { self.resourceProcessor.llmSchema = schema } } @@ -65,7 +65,7 @@ extension FHIRPrompt { /// Prompt used to interpret FHIR resources /// /// This prompt is used by the ``FHIRResourceInterpreter``. - public static let interpretation: FHIRPrompt = { + static let interpretation: FHIRPrompt = { FHIRPrompt( storageKey: "prompt.interpretation", localizedDescription: String( diff --git a/LLMonFHIR/FHIRInterpretationModule.swift b/LLMonFHIR/FHIRInterpretationModule.swift index 3ed60b3..4a4571c 100644 --- a/LLMonFHIR/FHIRInterpretationModule.swift +++ b/LLMonFHIR/FHIRInterpretationModule.swift @@ -14,9 +14,10 @@ import SpeziLocalStorage import SwiftUI -public class FHIRInterpretationModule: Module, DefaultInitializable { - public enum Defaults { - public static var llmSchema: LLMOpenAISchema { +// periphery:ignore - Properties are used through dependency injection and @Model configuration in `configure()` +class FHIRInterpretationModule: Module, DefaultInitializable { + enum Defaults { + static var llmSchema: LLMOpenAISchema { .init( parameters: .init( modelType: .gpt4_turbo, @@ -43,7 +44,7 @@ public class FHIRInterpretationModule: Module, DefaultInitializable { /// - Warning: Ensure that passed LLM schema's don't contain a system prompt! This will be configured by the ``FHIRInterpretationModule``. - public init( + init( summaryLLMSchema: SummaryLLM = Defaults.llmSchema, interpretationLLMSchema: InterpretationLLM = Defaults.llmSchema, // swiftlint:disable:this function_default_parameter_at_end multipleResourceInterpretationOpenAIModel: LLMOpenAIModelType, // swiftlint:disable:this identifier_name @@ -58,7 +59,7 @@ public class FHIRInterpretationModule: Module, DefaultInitializable { } - public required convenience init() { + required convenience init() { self.init( summaryLLMSchema: Defaults.llmSchema, interpretationLLMSchema: Defaults.llmSchema, @@ -67,7 +68,7 @@ public class FHIRInterpretationModule: Module, DefaultInitializable { } - public func configure() { + func configure() { resourceSummary = FHIRResourceSummary( localStorage: localStorage, llmRunner: llmRunner, diff --git a/LLMonFHIR/FHIRSummary/FHIRResourceSummary.swift b/LLMonFHIR/FHIRSummary/FHIRResourceSummary.swift index deb0373..a9cf934 100644 --- a/LLMonFHIR/FHIRSummary/FHIRResourceSummary.swift +++ b/LLMonFHIR/FHIRSummary/FHIRResourceSummary.swift @@ -14,21 +14,21 @@ import SpeziLocalStorage /// Responsible for summarizing FHIR resources. @Observable -public final class FHIRResourceSummary: Sendable { +final class FHIRResourceSummary: Sendable { /// Summary of a FHIR resource emitted by the ``FHIRResourceSummary``. - public struct Summary: Codable, LosslessStringConvertible, Sendable { + struct Summary: Codable, LosslessStringConvertible, Sendable { /// Title of the FHIR resource, should be shorter than 4 words. - public let title: String + let title: String /// Summary of the FHIR resource, should be a single line of text. - public let summary: String + let summary: String - public var description: String { + var description: String { title + "\n" + summary } - public init?(_ description: String) { + init?(_ description: String) { let components = description.split(separator: "\n") guard components.count == 2, let title = components.first, let summary = components.last else { return nil @@ -46,7 +46,7 @@ public final class FHIRResourceSummary: Sendable { /// - Parameters: /// - localStorage: Local storage module that needs to be passed to the ``FHIRResourceSummary`` to allow it to cache summaries. /// - openAIModel: OpenAI module that needs to be passed to the ``FHIRResourceSummary`` to allow it to retrieve summaries. - public init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) { + init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) { self.resourceProcessor = FHIRResourceProcessor( localStorage: localStorage, llmRunner: llmRunner, @@ -64,7 +64,7 @@ public final class FHIRResourceSummary: Sendable { /// - 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 -> Summary { + func summarize(resource: FHIRResource, forceReload: Bool = false) async throws -> Summary { try await resourceProcessor.process(resource: resource, forceReload: forceReload) } @@ -72,7 +72,7 @@ public final class FHIRResourceSummary: Sendable { /// /// - 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) -> Summary? { + func cachedSummary(forResource resource: FHIRResource) -> Summary? { resourceProcessor.results[resource.id] } @@ -80,7 +80,7 @@ public final class FHIRResourceSummary: Sendable { /// /// - Parameters: /// - schema: The to-be-used `LLMSchema`. - public func changeLLMSchema(to schema: Schema) { + func changeLLMSchema(to schema: Schema) { self.resourceProcessor.llmSchema = schema } } @@ -90,7 +90,7 @@ extension FHIRPrompt { /// Prompt used to summarize FHIR resources /// /// This prompt is used by the ``FHIRResourceSummary``. - public static let summary: FHIRPrompt = { + static let summary: FHIRPrompt = { FHIRPrompt( storageKey: "prompt.summary", localizedDescription: String( diff --git a/LLMonFHIR/FHIRSummary/FHIRResourceSummaryView.swift b/LLMonFHIR/FHIRSummary/FHIRResourceSummaryView.swift index 27d5af0..0fa4664 100644 --- a/LLMonFHIR/FHIRSummary/FHIRResourceSummaryView.swift +++ b/LLMonFHIR/FHIRSummary/FHIRResourceSummaryView.swift @@ -12,14 +12,14 @@ 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 { +struct FHIRResourceSummaryView: View { @Environment(FHIRResourceSummary.self) private var fhirResourceSummary @State private var viewState: ViewState = .idle private let resource: FHIRResource - public var body: some View { + var body: some View { Group { if let summary = fhirResourceSummary.cachedSummary(forResource: resource) { VStack(alignment: .leading, spacing: 0) { @@ -80,7 +80,7 @@ public struct FHIRResourceSummaryView: View { } - public init(resource: FHIRResource) { + init(resource: FHIRResource) { self.resource = resource } } diff --git a/LLMonFHIR/Helper/Bundle+Image.swift b/LLMonFHIR/Helper/Bundle+Image.swift deleted file mode 100644 index 1451401..0000000 --- a/LLMonFHIR/Helper/Bundle+Image.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// This source file is part of the Stanford LLM on FHIR project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -extension Foundation.Bundle { - /// Loads an image from the `Bundle` instance. - /// - Parameters: - /// - name: The name of the image. - /// - fileExtension: The file extension of the image. - /// - Returns: Returns the `UIImage` loaded from the `Bundle` instance. - func image(withName name: String, fileExtension: String) -> UIImage { - guard let resourceURL = self.url(forResource: name, withExtension: fileExtension) else { - fatalError("Could not find the file \"\(name).\(fileExtension)\" in the bundle.") - } - - guard let resourceData = try? Data(contentsOf: resourceURL), - let image = UIImage(data: resourceData) else { - fatalError("Decode the image named \"\(name).\(fileExtension)\"") - } - - return image - } -} diff --git a/LLMonFHIR/Helper/FHIRStore+Interpretation.swift b/LLMonFHIR/Helper/FHIRStore+Interpretation.swift index ea248a1..66e55fb 100644 --- a/LLMonFHIR/Helper/FHIRStore+Interpretation.swift +++ b/LLMonFHIR/Helper/FHIRStore+Interpretation.swift @@ -145,8 +145,4 @@ extension Array where Element == FHIRResource { var seen = Set() return filter { seen.insert($0).inserted } } - - fileprivate func dateSuffix(maxLength: Int) -> [FHIRResource] { - self.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(maxLength) - } } diff --git a/LLMonFHIR/Home.swift b/LLMonFHIR/Home.swift index b343ed8..cfbf240 100644 --- a/LLMonFHIR/Home.swift +++ b/LLMonFHIR/Home.swift @@ -12,7 +12,6 @@ import SwiftUI struct HomeView: View { @State private var showSettings = false @State private var showMultipleResourcesChat = false - @AppStorage(StorageKeys.onboardingInstructions) private var onboardingInstructions = true @AppStorage(StorageKeys.enableTextToSpeech) private var textToSpeech = StorageKeys.Defaults.enableTextToSpeech diff --git a/LLMonFHIR/LLMonFHIRDelegate.swift b/LLMonFHIR/LLMonFHIRDelegate.swift index bf7cc25..55d2414 100644 --- a/LLMonFHIR/LLMonFHIRDelegate.swift +++ b/LLMonFHIR/LLMonFHIRDelegate.swift @@ -26,10 +26,7 @@ class LLMonFHIRDelegate: SpeziAppDelegate { LLMLocalPlatform() LLMOpenAIPlatform(configuration: .init(concurrentStreams: 20)) } - - // All OpenAI -// FHIRInterpretationModule() - + // Local Summary, Remote (OpenAI) Interpretation FHIRInterpretationModule( multipleResourceInterpretationOpenAIModel: .gpt4_o diff --git a/LLMonFHIR/Settings/FHIRPrompt.swift b/LLMonFHIR/Settings/FHIRPrompt.swift index cc5ae1a..3c3a9df 100644 --- a/LLMonFHIR/Settings/FHIRPrompt.swift +++ b/LLMonFHIR/Settings/FHIRPrompt.swift @@ -10,21 +10,21 @@ import Foundation /// Handle dynamic, localized LLM prompts for FHIR resources. -public struct FHIRPrompt: Hashable, Sendable { +struct FHIRPrompt: Hashable, Sendable { /// Placeholder for FHIR resource in prompts. - public static let fhirResourcePlaceholder = "{{FHIR_RESOURCE}}" + static let fhirResourcePlaceholder = "{{FHIR_RESOURCE}}" /// Placeholder for the current locale in a prompt - public static let localePlaceholder = "{{LOCALE}}" + static let localePlaceholder = "{{LOCALE}}" /// The key used for storing and retrieving the prompt. - public let storageKey: String + let storageKey: String /// A human-readable description of the prompt, localized as needed. - public let localizedDescription: String + let localizedDescription: String /// The default prompt text to be used if no custom prompt is set. - public let defaultPrompt: String + let defaultPrompt: String /// The current prompt, either from UserDefaults or the default, appended with a localized message that adapts to the user's language settings. - public var prompt: String { + var prompt: String { UserDefaults.standard.string(forKey: storageKey) ?? defaultPrompt } @@ -33,7 +33,7 @@ public struct FHIRPrompt: Hashable, Sendable { /// - storageKey: The key used for storing and retrieving the prompt. /// - localizedDescription: A human-readable description of the prompt, localized as needed. /// - defaultPrompt: The default prompt text to be used if no custom prompt is set. - public init( + init( storageKey: String, localizedDescription: String, defaultPrompt: String @@ -46,11 +46,11 @@ public struct FHIRPrompt: Hashable, Sendable { /// Saves a new version of the prompt. /// - Parameter prompt: The new prompt. - public func save(prompt: String) { + func save(prompt: String) { UserDefaults.standard.set(prompt, forKey: storageKey) } - public func hash(into hasher: inout Hasher) { + func hash(into hasher: inout Hasher) { hasher.combine(storageKey) } @@ -61,7 +61,7 @@ public struct FHIRPrompt: Hashable, Sendable { /// - resource: The resource that should be inserted in the prompt. /// - locale: The current locale that should be inserted in the prompt. /// - Returns: The constructed prompt. - public func prompt(withFHIRResource resource: String, locale: String = Locale.preferredLanguages[0]) -> String { + func prompt(withFHIRResource resource: String, locale: String = Locale.preferredLanguages[0]) -> String { prompt .replacingOccurrences(of: FHIRPrompt.fhirResourcePlaceholder, with: resource) .replacingOccurrences(of: FHIRPrompt.localePlaceholder, with: locale) diff --git a/LLMonFHIR/Settings/FHIRPromptSettingsView.swift b/LLMonFHIR/Settings/FHIRPromptSettingsView.swift index 36c4953..440f943 100644 --- a/LLMonFHIR/Settings/FHIRPromptSettingsView.swift +++ b/LLMonFHIR/Settings/FHIRPromptSettingsView.swift @@ -12,13 +12,13 @@ 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 FHIRPromptSettingsView: View { +struct FHIRPromptSettingsView: View { private let promptType: FHIRPrompt private let onSave: () -> Void @State private var prompt: String = "" - public var body: some View { + var body: some View { VStack(spacing: 16) { Text("Customize the \(promptType.localizedDescription.lowercased()).") .multilineTextAlignment(.leading) @@ -48,7 +48,7 @@ public struct FHIRPromptSettingsView: View { /// - Parameters: /// - promptType: The ``FHIRPrompt`` instance whose settings are being modified. It holds the information about the specific prompt being edited. /// - onSave: A closure to be called when the user saves the prompt. This allows for custom actions, like dismissing the view. - public init(promptType: FHIRPrompt, onSave: @escaping () -> Void) { + init(promptType: FHIRPrompt, onSave: @escaping () -> Void) { self.promptType = promptType self.onSave = onSave self._prompt = State(initialValue: promptType.prompt) diff --git a/LLMonFHIRTests/LLMonFHIRTests.swift b/LLMonFHIRTests/LLMonFHIRTests.swift index d91b30f..6738756 100644 --- a/LLMonFHIRTests/LLMonFHIRTests.swift +++ b/LLMonFHIRTests/LLMonFHIRTests.swift @@ -6,7 +6,6 @@ // SPDX-License-Identifier: MIT // -@testable import LLMonFHIR import XCTest diff --git a/LLMonFHIRUITests/OnboardingTests.swift b/LLMonFHIRUITests/OnboardingTests.swift index cdf106c..b5c8ab7 100644 --- a/LLMonFHIRUITests/OnboardingTests.swift +++ b/LLMonFHIRUITests/OnboardingTests.swift @@ -26,23 +26,17 @@ class OnboardingTests: XCTestCase { func testOnboardingFlow() throws { let app = XCUIApplication() - try app.navigateOnboardingFlow(assertThatHealthKitConsentIsShown: true) + try app.navigateOnboardingFlow() } } extension XCUIApplication { - func conductOnboardingIfNeeded() throws { - if self.staticTexts["LLMonFHIR"].waitForExistence(timeout: 5) { - try navigateOnboardingFlow(assertThatHealthKitConsentIsShown: false) - } - } - - func navigateOnboardingFlow(assertThatHealthKitConsentIsShown: Bool = true) throws { + func navigateOnboardingFlow() throws { try navigateOnboardingFlowWelcome() try navigateOnboardingFlowInterestingModules() try navigateOnboardingFlowOpenAI() - try navigateOnboardingFlowHealthKitAccess(assertThatHealthKitConsentIsShown: assertThatHealthKitConsentIsShown) + try navigateOnboardingFlowHealthKitAccess() } private func navigateOnboardingFlowWelcome() throws { @@ -71,7 +65,7 @@ extension XCUIApplication { buttons["Next"].tap() } - private func navigateOnboardingFlowHealthKitAccess(assertThatHealthKitConsentIsShown: Bool = true) throws { + private func navigateOnboardingFlowHealthKitAccess() throws { XCTAssertTrue(staticTexts["HealthKit Access"].waitForExistence(timeout: 2)) XCTAssertTrue(buttons["Grant Access"].waitForExistence(timeout: 2))