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
Merged
22 changes: 15 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ let package = Package(
products: [
.library(name: "SpeziFHIR", targets: ["SpeziFHIR"]),
.library(name: "SpeziFHIRHealthKit", targets: ["SpeziFHIRHealthKit"]),
.library(name: "SpeziFHIRInterpretation", targets: ["SpeziFHIRInterpretation"]),
.library(name: "SpeziFHIRLLM", targets: ["SpeziFHIRLLM"]),
.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", .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.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.7.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage.git", from: "1.0.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziChat.git", .upToNextMinor(from: "0.1.8")),
.package(url: "https://github.com/StanfordSpezi/SpeziSpeech.git", from: "1.0.0")
],
targets: [
.target(
Expand All @@ -49,12 +52,16 @@ let package = Package(
]
),
.target(
name: "SpeziFHIRInterpretation",
name: "SpeziFHIRLLM",
dependencies: [
.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"),
.product(name: "SpeziSpeechSynthesizer", package: "SpeziSpeech")
],
resources: [
.process("Resources")
Expand All @@ -63,7 +70,8 @@ let package = Package(
.target(
name: "SpeziFHIRMockPatients",
dependencies: [
.target(name: "SpeziFHIR")
.target(name: "SpeziFHIR"),
.product(name: "ModelsR4", package: "FHIRModels")
],
resources: [
.process("Resources")
Expand Down
111 changes: 0 additions & 111 deletions Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// This source file is part of the Stanford Spezi project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import os
import SpeziFHIR
import SpeziLLMOpenAI


struct FHIRGetResourceLLMFunction: LLMFunction {
static let logger = Logger(subsystem: "edu.stanford.spezi.fhir", category: "SpeziFHIRLLM")

static let name = "get_resources"
static let description = String(localized: "FUNCTION_DESCRIPTION")

private let fhirStore: FHIRStore
private let resourceSummary: FHIRResourceSummary


@Parameter var resources: [String]


init(
fhirStore: FHIRStore,
resourceSummary: FHIRResourceSummary,
resourceCountLimit: Int,
allowedResourcesFunctionCallIdentifiers: Set<String>? = nil // swiftlint:disable:this discouraged_optional_collection
) {
self.fhirStore = fhirStore
self.resourceSummary = resourceSummary

// Only take newest values of the health records
var allResourcesFunctionCallIdentifiers = Set(fhirStore.allResourcesFunctionCallIdentifier.suffix(resourceCountLimit))

// If identifiers are restricted, filter for only allowed function call identifiers of health records.
if let allowedResourcesFunctionCallIdentifiers {
allResourcesFunctionCallIdentifiers.formIntersection(allowedResourcesFunctionCallIdentifiers)
}
philippzagar marked this conversation as resolved.
Show resolved Hide resolved

_resources = Parameter(
description: String(localized: "PARAMETER_DESCRIPTION"),
enumValues: Array(allResourcesFunctionCallIdentifiers)
)
}

Check warning on line 48 in Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift#L32-L48

Added lines #L32 - L48 were not covered by tests


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 {
// Fetch relevant FHIR resources matching the resources requested by the LLM
var fittingResources = fhirStore.llmRelevantResources.filter { $0.functionCallIdentifier.contains(requestedResource) }

// Stores output of nested task group summarizing fitting resources
var nestedFunctionOutputResults = [String]()

guard !fittingResources.isEmpty else {
nestedFunctionOutputResults.append(
String(
localized: "The medical record does not include any FHIR resources for the search term \(requestedResource)."
)
)
return []
}

// Filter out fitting resources (if greater than 64 entries)
fittingResources = 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)
}
}

for try await nestedResult in innerGroup {
nestedFunctionOutputResults.append(nestedResult)
}
}

return nestedFunctionOutputResults
}
}

for try await result in outerGroup {
functionOutput.append(contentsOf: result)
}
}

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

Check warning on line 99 in Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift#L51-L99

Added lines #L51 - L99 were not covered by tests

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)")
}

Check warning on line 105 in Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift#L101-L105

Added lines #L101 - L105 were not covered by tests

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
}

Check warning on line 122 in Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFHIRLLM/FHIRInterpretation/MultipleResources/FHIRGetResourceLLMFunction.swift#L107-L122

Added lines #L107 - L122 were not covered by tests
}
Loading
Loading