Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lift to SpeziLLM #17

Merged
merged 13 commits into from
Feb 26, 2024
13 changes: 9 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/FHIRModels", .upToNextMinor(from: "0.5.0")),
.package(url: "https://github.com/StanfordBDHG/HealthKitOnFHIR", .upToNextMinor(from: "0.2.4")),
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziHealthKit.git", .upToNextMinor(from: "0.4.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziML.git", .upToNextMinor(from: "0.3.1"))
.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", branch: "feat/structural-improvments"),
.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"))
],
targets: [
.target(
Expand All @@ -54,7 +56,10 @@ let package = Package(
.target(name: "SpeziFHIR"),
.product(name: "Spezi", package: "Spezi"),
.product(name: "ModelsR4", package: "FHIRModels"),
.product(name: "SpeziOpenAI", package: "SpeziML")
.product(name: "SpeziLLM", package: "SpeziLLM"),
.product(name: "SpeziLLMOpenAI", package: "SpeziLLM"),
.product(name: "SpeziLocalStorage", package: "SpeziStorage"),
.product(name: "SpeziChat", package: "SpeziChat")
],
resources: [
.process("Resources")
Expand Down
16 changes: 8 additions & 8 deletions Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@
//

import Foundation
import Observation
import SpeziFHIR
import SpeziLLM
import SpeziLocalStorage
import SpeziOpenAI


/// Responsible for interpreting FHIR resources.
@Observable
public class FHIRResourceInterpreter {
private let resourceProcesser: FHIRResourceProcesser<String>
private let resourceProcessor: FHIRResourceProcessor<String>


/// - 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, openAIModel: OpenAIModel) {
self.resourceProcesser = FHIRResourceProcesser(
public init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) {
self.resourceProcessor = FHIRResourceProcessor(
localStorage: localStorage,
openAIModel: openAIModel,
llmRunner: llmRunner,
llmSchema: llmSchema,
storageKey: "FHIRResourceInterpreter.Interpretations",
prompt: FHIRPrompt.interpretation
)
Expand All @@ -40,15 +40,15 @@
/// - 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)

Check warning on line 43 in Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift#L43

Added line #L43 was not covered by tests
}

/// 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]
resourceProcessor.results[resource.id]

Check warning on line 51 in Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift#L51

Added line #L51 was not covered by tests
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@
// SPDX-License-Identifier: MIT
//

import Observation
import SpeziChat
import SpeziFHIR
import SpeziLLM
import SpeziLLMOpenAI
import SpeziLocalStorage
import SpeziOpenAI


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


private let localStorage: LocalStorage
private let openAIModel: OpenAIModel
private let llmRunner: LLMRunner
private let llmSchema: any LLMSchema
private let storageKey: String
private let prompt: FHIRPrompt

Expand All @@ -36,12 +37,14 @@

init(
localStorage: LocalStorage,
openAIModel: OpenAIModel,
llmRunner: LLMRunner,
llmSchema: any LLMSchema,
storageKey: String,
prompt: FHIRPrompt
) {
self.localStorage = localStorage
self.openAIModel = openAIModel
self.llmRunner = llmRunner
self.llmSchema = llmSchema
self.storageKey = storageKey
self.prompt = prompt
self.results = (try? localStorage.read(storageKey: storageKey)) ?? [:]
Expand All @@ -54,27 +57,21 @@
return result
}

let chatStreamResults = try await openAIModel.queryAPI(withChat: [systemPrompt(forResource: resource)])
let chatStreamResults = try await llmRunner.oneShot(
with: llmSchema,
chat: .init(systemMessages: prompt.prompt(withFHIRResource: resource.jsonDescription))
)

Check warning on line 63 in Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift#L60-L63

Added lines #L60 - L63 were not covered by tests
var result = ""

for try await chatStreamResult in chatStreamResults {
for choice in chatStreamResult.choices {
result.append(choice.delta.content ?? "")
}
result.append(chatStreamResult)

Check warning on line 67 in Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift#L67

Added line #L67 was not covered by tests
}

guard let content = Content(result) else {
throw FHIRResourceProcesserError.notParsableAsAString
throw FHIRResourceProcessorError.notParsableAsAString

Check warning on line 71 in Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceProcessor.swift#L71

Added line #L71 was not covered by tests
}

results[resource.id] = content
return content
}

private func systemPrompt(forResource resource: FHIRResource) -> Chat {
Chat(
role: .system,
content: prompt.prompt(withFHIRResource: resource.jsonDescription)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation


enum FHIRResourceProcesserError: LocalizedError {
enum FHIRResourceProcessorError: LocalizedError {
case notParsableAsAString


Expand Down
14 changes: 7 additions & 7 deletions Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
//

import Foundation
import Observation
import SpeziFHIR
import SpeziLLM
import SpeziLocalStorage
import SpeziOpenAI


/// 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
Expand All @@ -41,16 +40,17 @@ public class FHIRResourceSummary {
}


private let resourceProcesser: FHIRResourceProcesser<Summary>
private let resourceProcesser: FHIRResourceProcessor<Summary>


/// - 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, openAIModel: OpenAIModel) {
self.resourceProcesser = FHIRResourceProcesser(
public init(localStorage: LocalStorage, llmRunner: LLMRunner, llmSchema: any LLMSchema) {
self.resourceProcesser = FHIRResourceProcessor(
localStorage: localStorage,
openAIModel: openAIModel,
llmRunner: llmRunner,
llmSchema: llmSchema,
storageKey: "FHIRResourceSummary.Summaries",
prompt: FHIRPrompt.summary
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
@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 {

Check warning on line 26 in Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift#L26

Added line #L26 was not covered by tests
VStack(alignment: .leading, spacing: 0) {
Text(summary.title)
if let date = resource.date {
Expand Down Expand Up @@ -58,6 +59,9 @@
}
}
.viewStateAlert(state: $viewState)
.task {
cachedSummary = await fhirResourceSummary.cachedSummary(forResource: resource)
}

Check warning on line 64 in Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRInterpretation/FHIRResourceSummaryView.swift#L62-L64

Added lines #L62 - L64 were not covered by tests
}


Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions Tests/UITests/TestApp/ExampleModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,29 @@

import Spezi
import SpeziFHIRInterpretation
import SpeziLLM
import SpeziLocalStorage
import class SpeziOpenAI.OpenAIModule
import class SpeziOpenAI.OpenAIModel


class ExampleModule: Module {
class ExampleModule: Module, @unchecked Sendable {
@Dependency private var localStorage: LocalStorage
@Dependency private var openAI: OpenAIModule
@Dependency private var llmRunner: LLMRunner

@Model private var resourceSummary: FHIRResourceSummary
@Model private var resourceInterpreter: FHIRResourceInterpreter

let llmSchema = LLMMockSchema()

func configure() {
resourceSummary = FHIRResourceSummary(localStorage: localStorage, openAIModel: openAI.model)
resourceInterpreter = FHIRResourceInterpreter(localStorage: localStorage, openAIModel: openAI.model)
resourceSummary = FHIRResourceSummary(
localStorage: localStorage,
llmRunner: llmRunner,
llmSchema: llmSchema
)
resourceInterpreter = FHIRResourceInterpreter(
localStorage: localStorage,
llmRunner: llmRunner,
llmSchema: llmSchema
)
}
}
4 changes: 4 additions & 0 deletions Tests/UITests/TestApp/TestAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@

import Spezi
import SpeziFHIR
import SpeziLLM
import SwiftUI


class TestAppDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration(standard: FHIR()) {
LLMRunner {
LLMMockPlatform()
}
ExampleModule()
}
}
Expand Down
Loading
Loading