From e4a150b1c802ab10078e98ade8709b99842cd0fb Mon Sep 17 00:00:00 2001 From: Philipp Zagar Date: Thu, 8 Feb 2024 23:53:35 -0800 Subject: [PATCH] Lift SpeziFHIR to SpeziLLM abstractions --- Package.swift | 2 +- .../FHIRResourceInterpreter.swift | 16 ++++---- ...sser.swift => FHIRResourceProcessor.swift} | 18 ++++---- ...swift => FHIRResourceProcessorError.swift} | 2 +- .../FHIRResourceSummary.swift | 16 ++++---- .../FHIRResourceSummaryView.swift | 6 ++- .../Settings/FHIRPrompt.swift | 2 +- Tests/UITests/TestApp/ExampleModule.swift | 18 +++++--- Tests/UITests/TestApp/TestAppDelegate.swift | 4 +- .../xcshareddata/swiftpm/Package.resolved | 41 +++++++++---------- 10 files changed, 66 insertions(+), 59 deletions(-) rename Sources/SpeziFHIRInterpretation/{FHIRResourceProcesser.swift => FHIRResourceProcessor.swift} (79%) rename Sources/SpeziFHIRInterpretation/{FHIRResourceProcesserError.swift => FHIRResourceProcessorError.swift} (92%) diff --git a/Package.swift b/Package.swift index 2066f19..76d782b 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ let package = Package( .package(url: "https://github.com/StanfordBDHG/HealthKitOnFHIR", .upToNextMinor(from: "0.2.4")), .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.1.0"), .package(url: "https://github.com/StanfordSpezi/SpeziHealthKit.git", .upToNextMinor(from: "0.5.0")), - .package(url: "https://github.com/StanfordSpezi/SpeziLLM.git", .upToNextMinor(from: "0.6.0")), + .package(url: "https://github.com/StanfordSpezi/SpeziLLM.git", branch: "feat/structural-improvements"), .package(url: "https://github.com/StanfordSpezi/SpeziStorage.git", from: "1.0.0"), .package(url: "https://github.com/StanfordSpezi/SpeziChat.git", .upToNextMinor(from: "0.1.4")) ], diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift index 81e6269..a1aa561 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift @@ -7,27 +7,25 @@ // import Foundation -import Observation import SpeziFHIR import SpeziLLM -import SpeziLLMOpenAI import SpeziLocalStorage /// Responsible for interpreting FHIR resources. @Observable public class FHIRResourceInterpreter { - private let resourceProcesser: FHIRResourceProcesser + 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, llm: any LLM) { - self.resourceProcesser = FHIRResourceProcesser( + public init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) { + self.resourceProcessor = FHIRResourceProcessor( localStorage: localStorage, llmRunner: llmRunner, - llm: llm, + llmSchema: llmSchema, storageKey: "FHIRResourceInterpreter.Interpretations", prompt: FHIRPrompt.interpretation ) @@ -42,15 +40,15 @@ public class FHIRResourceInterpreter { /// - Returns: An asynchronous `String` representing the interpretation of the resource. @discardableResult public func interpret(resource: FHIRResource, forceReload: Bool = false) async throws -> String { - try await resourceProcesser.process(resource: resource, forceReload: forceReload) + try await resourceProcessor.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] + public func cachedInterpretation(forResource resource: FHIRResource) async -> String? { + await resourceProcessor.results[resource.id] } } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift similarity index 79% rename from Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift rename to Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift index e7ba9ce..c2ed66d 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift @@ -6,7 +6,6 @@ // SPDX-License-Identifier: MIT // -import Observation import SpeziChat import SpeziFHIR import SpeziLLM @@ -14,14 +13,13 @@ import SpeziLLMOpenAI import SpeziLocalStorage -@Observable -class FHIRResourceProcesser { +actor FHIRResourceProcessor { typealias Results = [FHIRResource.ID: Content] private let localStorage: LocalStorage private let llmRunner: LLMRunner - private let llm: any LLM + private let llmSchema: any LLMSchema private let storageKey: String private let prompt: FHIRPrompt @@ -40,13 +38,13 @@ class FHIRResourceProcesser { init( localStorage: LocalStorage, llmRunner: LLMRunner, - llm: any LLM, + llmSchema: any LLMSchema, storageKey: String, prompt: FHIRPrompt ) { self.localStorage = localStorage self.llmRunner = llmRunner - self.llm = llm + self.llmSchema = llmSchema self.storageKey = storageKey self.prompt = prompt self.results = (try? localStorage.read(storageKey: storageKey)) ?? [:] @@ -59,11 +57,13 @@ class FHIRResourceProcesser { return result } + let llm = await llmRunner(with: llmSchema) + await MainActor.run { - llm.context.append(.init(role: .system, content: prompt.prompt(withFHIRResource: resource.jsonDescription))) + llm.context.append(systemMessage: prompt.prompt(withFHIRResource: resource.jsonDescription)) } - let chatStreamResults = try await llmRunner(with: llm).generate() + let chatStreamResults = try await llm.generate() var result = "" for try await chatStreamResult in chatStreamResults { @@ -71,7 +71,7 @@ class FHIRResourceProcesser { } guard let content = Content(result) else { - throw FHIRResourceProcesserError.notParsableAsAString + throw FHIRResourceProcessorError.notParsableAsAString } results[resource.id] = content diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceProcesserError.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceProcessorError.swift similarity index 92% rename from Sources/SpeziFHIRInterpretation/FHIRResourceProcesserError.swift rename to Sources/SpeziFHIRInterpretation/FHIRResourceProcessorError.swift index 84c1bc4..d8df61e 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceProcesserError.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceProcessorError.swift @@ -9,7 +9,7 @@ import Foundation -enum FHIRResourceProcesserError: LocalizedError { +enum FHIRResourceProcessorError: LocalizedError { case notParsableAsAString diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift index aaeec6a..65e0f01 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift @@ -7,17 +7,15 @@ // import Foundation -import Observation import SpeziFHIR import SpeziLLM -import SpeziLLMOpenAI import SpeziLocalStorage /// Responsible for summarizing FHIR resources. @Observable public class FHIRResourceSummary { - /// Summary of a FHIR resource emited by the ``FHIRResourceSummary``. + /// Summary of a FHIR resource emitted by the ``FHIRResourceSummary``. public struct Summary: Codable, LosslessStringConvertible { /// Title of the FHIR resource, should be shorter than 4 words. public let title: String @@ -42,17 +40,17 @@ public class FHIRResourceSummary { } - private let resourceProcesser: FHIRResourceProcesser + private let resourceProcesser: FHIRResourceProcessor /// - 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, llm: any LLM) { - self.resourceProcesser = FHIRResourceProcesser( + public init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) { + self.resourceProcesser = FHIRResourceProcessor( localStorage: localStorage, llmRunner: llmRunner, - llm: llm, + llmSchema: llmSchema, storageKey: "FHIRResourceSummary.Summaries", prompt: FHIRPrompt.summary ) @@ -74,8 +72,8 @@ public class FHIRResourceSummary { /// /// - 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? { - resourceProcesser.results[resource.id] + public func cachedSummary(forResource resource: FHIRResource) async -> Summary? { + await resourceProcesser.results[resource.id] } } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift index b560182..9b980fb 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift @@ -16,13 +16,14 @@ public struct FHIRResourceSummaryView: View { @Environment(FHIRResourceSummary.self) private var fhirResourceSummary @State private var viewState: ViewState = .idle + @State private var cachedSummary: FHIRResourceSummary.Summary? private let resource: FHIRResource public var body: some View { Group { - if let summary = fhirResourceSummary.cachedSummary(forResource: resource) { + if let summary = cachedSummary { VStack(alignment: .leading, spacing: 0) { Text(summary.title) if let date = resource.date { @@ -58,6 +59,9 @@ public struct FHIRResourceSummaryView: View { } } .viewStateAlert(state: $viewState) + .task { + cachedSummary = await fhirResourceSummary.cachedSummary(forResource: resource) + } } diff --git a/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift b/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift index 64ffb1d..0a13fc5 100644 --- a/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift +++ b/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift @@ -44,7 +44,7 @@ public struct FHIRPrompt: Hashable { } - /// Saves a new version of the propmpt. + /// Saves a new version of the prompt. /// - Parameter prompt: The new prompt. public func save(prompt: String) { UserDefaults.standard.set(prompt, forKey: storageKey) diff --git a/Tests/UITests/TestApp/ExampleModule.swift b/Tests/UITests/TestApp/ExampleModule.swift index df5e65e..5531290 100644 --- a/Tests/UITests/TestApp/ExampleModule.swift +++ b/Tests/UITests/TestApp/ExampleModule.swift @@ -12,19 +12,25 @@ import SpeziLLM import SpeziLocalStorage -class ExampleModule: Module { - private let llm = LLMMock() - - +class ExampleModule: Module, @unchecked Sendable { @Dependency private var localStorage: LocalStorage @Dependency private var llmRunner: LLMRunner @Model private var resourceSummary: FHIRResourceSummary @Model private var resourceInterpreter: FHIRResourceInterpreter + let llmSchema = LLMMockSchema() func configure() { - resourceSummary = FHIRResourceSummary(localStorage: localStorage, llmRunner: llmRunner, llm: llm) - resourceInterpreter = FHIRResourceInterpreter(localStorage: localStorage, llmRunner: llmRunner, llm: llm) + resourceSummary = FHIRResourceSummary( + localStorage: localStorage, + llmRunner: llmRunner, + llmSchema: llmSchema + ) + resourceInterpreter = FHIRResourceInterpreter( + localStorage: localStorage, + llmRunner: llmRunner, + llmSchema: llmSchema + ) } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 83093c8..96eca3e 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -15,7 +15,9 @@ import SwiftUI class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration(standard: FHIR()) { - LLMRunner() + LLMRunner { + LLMMockPlatform() + } ExampleModule() } } diff --git a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 605876f..8568563 100644 --- a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/llama.cpp", "state" : { - "revision" : "bcbf5bf9f677b92262aa28a2849defeddc00505d", - "version" : "0.1.6" + "revision" : "b0611c7d3cb049822f9911878514e4706b80e2ac", + "version" : "0.1.8" } }, { @@ -32,8 +32,16 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MacPaw/OpenAI", "state" : { - "revision" : "ac5892fd0de8d283362ddc30f8e9f1a0eaba8cc0", - "version" : "0.2.5" + "revision" : "35afc9a6ee127b8f22a85a31aec2036a987478af" + } + }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore.git", + "state" : { + "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", + "version" : "0.0.8" } }, { @@ -50,8 +58,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziChat.git", "state" : { - "revision" : "9d45c10bcf859c98f2998ecd4f6a80f31894fe2c", - "version" : "0.1.4" + "revision" : "ea5e21b4f42d99a5549dd7a7033e2a3efeb5fd36", + "version" : "0.1.5" } }, { @@ -72,22 +80,13 @@ "version" : "0.5.0" } }, - { - "identity" : "spezillm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziLLM.git", - "state" : { - "revision" : "24d6c197f1821925e3fc1ee9589859b6853aee01", - "version" : "0.6.0" - } - }, { "identity" : "spezionboarding", "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding", "state" : { - "revision" : "3ee713576eaeaa03200ba26bbc1269ceeb6abb25", - "version" : "1.0.1" + "revision" : "8fb6d9f1a080661c0cc564a93b82ead3c8d44d4f", + "version" : "1.0.2" } }, { @@ -113,8 +112,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews", "state" : { - "revision" : "0137e69d156bf4001a8d6bf5661c9a37b2bbd0aa", - "version" : "1.0.0" + "revision" : "7210f72d6821d2eeb93438b29cb854a8ce334164", + "version" : "1.2.0" } }, { @@ -122,8 +121,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", - "version" : "1.0.5" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, {