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

Swift 6 language mode readiness and bump dependencies #23

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 9 additions & 30 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

//
// This source file is part of the Stanford Spezi open-source project
Expand All @@ -12,12 +12,6 @@ import class Foundation.ProcessInfo
import PackageDescription


#if swift(<6)
let strictConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let strictConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif

let package = Package(
name: "SpeziFHIR",
defaultLocalization: "en",
Expand All @@ -31,14 +25,14 @@ let package = Package(
.library(name: "SpeziFHIRMockPatients", targets: ["SpeziFHIRMockPatients"])
],
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", from: "1.2.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziHealthKit.git", .upToNextMinor(from: "0.5.1")),
.package(url: "https://github.com/StanfordSpezi/SpeziLLM.git", .upToNextMinor(from: "0.8.1")),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage.git", from: "1.0.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziChat.git", .upToNextMinor(from: "0.2.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziSpeech.git", from: "1.0.0")
.package(url: "https://github.com/apple/FHIRModels", .upToNextMinor(from: "0.6.0")),
.package(url: "https://github.com/StanfordBDHG/HealthKitOnFHIR", .upToNextMinor(from: "0.2.11")),
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.8.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziHealthKit", .upToNextMinor(from: "0.6.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziLLM", .upToNextMinor(from: "0.8.4")),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage", from: "1.2.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziChat", .upToNextMinor(from: "0.2.1")),
.package(url: "https://github.com/StanfordSpezi/SpeziSpeech", from: "1.1.0")
] + swiftLintPackage(),
targets: [
.target(
Expand All @@ -49,9 +43,6 @@ let package = Package(
.product(name: "ModelsDSTU2", package: "FHIRModels"),
.product(name: "HealthKitOnFHIR", package: "HealthKitOnFHIR")
],
swiftSettings: [
strictConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
Expand All @@ -61,9 +52,6 @@ let package = Package(
.product(name: "HealthKitOnFHIR", package: "HealthKitOnFHIR"),
.product(name: "SpeziHealthKit", package: "SpeziHealthKit")
],
swiftSettings: [
strictConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
Expand All @@ -81,9 +69,6 @@ let package = Package(
resources: [
.process("Resources")
],
swiftSettings: [
strictConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
Expand All @@ -95,19 +80,13 @@ let package = Package(
resources: [
.process("Resources")
],
swiftSettings: [
strictConcurrency
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziFHIRTests",
dependencies: [
.target(name: "SpeziFHIR")
],
swiftSettings: [
strictConcurrency
],
plugins: [] + swiftLintPlugin()
)
]
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziFHIR/Extensions/FHIR+Identifiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import ModelsR4


extension Resource: Identifiable {
extension Resource: @retroactive Identifiable {
public typealias ID = FHIRPrimitive<FHIRString>?
}


extension FHIRPrimitive: Identifiable where PrimitiveType: Identifiable { }
extension FHIRPrimitive: @retroactive Identifiable where PrimitiveType: Identifiable { }
7 changes: 5 additions & 2 deletions Sources/SpeziFHIR/FHIRStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import enum ModelsDSTU2.ResourceProxy
import Spezi


/// Module to 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: Module, EnvironmentAccessible, DefaultInitializable {
public final class FHIRStore: Module,
EnvironmentAccessible,
DefaultInitializable,
@unchecked Sendable /* `unchecked` `Sendable` conformance fine as access to `_resources` protected by `NSLock` */ {
private let lock = NSLock()
@ObservationIgnored private var _resources: [FHIRResource]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,31 @@ struct FHIRGetResourceLLMFunction: LLMFunction {
}


private static func filterFittingResources(_ fittingResources: [FHIRResource]) -> [FHIRResource] {
Self.logger.debug("Overall fitting Resources: \(fittingResources.count)")

var fittingResources = fittingResources

if fittingResources.count > 64 {
fittingResources = fittingResources.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(64)
Self.logger.debug(
"""
Reduced to the following 64 resources: \(fittingResources.map { $0.functionCallIdentifier }.joined(separator: ","))
"""
)
}

return fittingResources
}


func execute() async throws -> String? {
var functionOutput: [String] = []

try await withThrowingTaskGroup(of: [String].self) { outerGroup in
// Iterate over all requested resources by the LLM
for requestedResource in resources {
outerGroup.addTask {
outerGroup.addTask { @Sendable [fhirStore, resourceSummary] in
// Fetch relevant FHIR resources matching the resources requested by the LLM
var fittingResources = fhirStore.llmRelevantResources.filter { $0.functionCallIdentifier.contains(requestedResource) }

Expand All @@ -71,13 +89,14 @@ struct FHIRGetResourceLLMFunction: LLMFunction {
}

// Filter out fitting resources (if greater than 64 entries)
fittingResources = filterFittingResources(fittingResources)

fittingResources = Self.filterFittingResources(fittingResources)
try await withThrowingTaskGroup(of: String.self) { innerGroup in
// Iterate over fitting resources and summarizing them
for resource in fittingResources {
innerGroup.addTask {
try await summarizeResource(fhirResource: resource, resourceType: requestedResource)
innerGroup.addTask { @Sendable [resourceSummary] in
let summary = try await resourceSummary.summarize(resource: resource)
Self.logger.debug("Summary of appended FHIR resource \(requestedResource): \(summary.description)")
return String(localized: "This is the summary of the requested \(requestedResource):\n\n\(summary.description)")
}
}

Expand All @@ -97,27 +116,4 @@ struct FHIRGetResourceLLMFunction: LLMFunction {

return functionOutput.joined(separator: "\n\n")
}

private func summarizeResource(fhirResource: FHIRResource, resourceType: String) async throws -> String {
let summary = try await resourceSummary.summarize(resource: fhirResource)
Self.logger.debug("Summary of appended FHIR resource \(resourceType): \(summary.description)")
return String(localized: "This is the summary of the requested \(resourceType):\n\n\(summary.description)")
}

private func filterFittingResources(_ fittingResources: [FHIRResource]) -> [FHIRResource] {
Self.logger.debug("Overall fitting Resources: \(fittingResources.count)")

var fittingResources = fittingResources

if fittingResources.count > 64 {
fittingResources = fittingResources.lazy.sorted(by: { $0.date ?? .distantPast < $1.date ?? .distantPast }).suffix(64)
Self.logger.debug(
"""
Reduced to the following 64 resources: \(fittingResources.map { $0.functionCallIdentifier }.joined(separator: ","))
"""
)
}

return fittingResources
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public class FHIRMultipleResourceInterpreter {
}

/// Change the `LLMSchema` used by the ``FHIRMultipleResourceInterpreter``.
@MainActor
public func changeLLMSchema(
openAIModel model: LLMOpenAIModelType,
resourceCountLimit: Int,
Expand All @@ -123,7 +124,7 @@ public class FHIRMultipleResourceInterpreter {
}
self.llm = nil

Task { @MainActor in
Task {
await prepareLLM()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SpeziLocalStorage

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


Expand Down
14 changes: 7 additions & 7 deletions Sources/SpeziFHIRLLM/FHIRInterpretationModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ public class FHIRInterpretationModule: Module, DefaultInitializable {
public static var llmSchema: LLMOpenAISchema {
.init(
parameters: .init(
modelType: .gpt4_turbo_preview,
modelType: .gpt4_turbo,
systemPrompts: [] // No system prompt as this will be determined later by the resource interpreter
)
)
}
}


@Dependency private var localStorage: LocalStorage
@Dependency private var llmRunner: LLMRunner
@Dependency private var fhirStore: FHIRStore
@Dependency(LocalStorage.self) private var localStorage
@Dependency(LLMRunner.self) private var llmRunner
@Dependency(FHIRStore.self) private var fhirStore

@Model private var resourceSummary: FHIRResourceSummary
@Model private var resourceInterpreter: FHIRResourceInterpreter
Expand All @@ -43,9 +43,9 @@ 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<SummaryLLM: LLMSchema, InterpretationLLM: LLMSchema>( // swiftlint:disable:this function_default_parameter_at_end
public init<SummaryLLM: LLMSchema, InterpretationLLM: LLMSchema>(
summaryLLMSchema: SummaryLLM = Defaults.llmSchema,
interpretationLLMSchema: InterpretationLLM = Defaults.llmSchema,
interpretationLLMSchema: InterpretationLLM = Defaults.llmSchema, // swiftlint:disable:this function_default_parameter_at_end
multipleResourceInterpretationOpenAIModel: LLMOpenAIModelType, // swiftlint:disable:this identifier_name
resourceCountLimit: Int = 250,
allowedResourcesFunctionCallIdentifiers: Set<String>? = nil // swiftlint:disable:this discouraged_optional_collection
Expand All @@ -62,7 +62,7 @@ public class FHIRInterpretationModule: Module, DefaultInitializable {
self.init(
summaryLLMSchema: Defaults.llmSchema,
interpretationLLMSchema: Defaults.llmSchema,
multipleResourceInterpretationOpenAIModel: .gpt4_turbo_preview
multipleResourceInterpretationOpenAIModel: .gpt4_turbo
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import SpeziLLMOpenAI
import SpeziLocalStorage


class FHIRResourceProcessor<Content: Codable & LosslessStringConvertible> {
// Unchecked `Sendable` conformance is fine as storage is guarded by `NSLock`.
final class FHIRResourceProcessor<Content: Codable & LosslessStringConvertible>: @unchecked Sendable {
typealias Results = [FHIRResource.ID: Content]


Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziFHIRLLM/FHIRSummary/FHIRResourceSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import SpeziLocalStorage

/// Responsible for summarizing FHIR resources.
@Observable
public class FHIRResourceSummary {
public final class FHIRResourceSummary: Sendable {
/// Summary of a FHIR resource emitted by the ``FHIRResourceSummary``.
public struct Summary: Codable, LosslessStringConvertible {
public struct Summary: Codable, LosslessStringConvertible, Sendable {
/// 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.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziFHIRLLM/Settings/FHIRPrompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation


/// Handle dynamic, localized LLM prompts for FHIR resources.
public struct FHIRPrompt: Hashable {
public struct FHIRPrompt: Hashable, Sendable {
/// Placeholder for FHIR resource in prompts.
public static let fhirResourcePlaceholder = "{{FHIR_RESOURCE}}"
/// Placeholder for the current locale in a prompt
Expand Down
Loading
Loading