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


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

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
}
}
Loading
Loading