From 2677043ddb997e9686ec10d9e4d5e6e002a53110 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 14 May 2024 21:45:20 -0700 Subject: [PATCH 01/13] Update Consent Document --- OwnYourData/Resources/ConsentDocument.md | 44 ++++++++++++++++++------ 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/OwnYourData/Resources/ConsentDocument.md b/OwnYourData/Resources/ConsentDocument.md index 81ab5f9..14a5905 100644 --- a/OwnYourData/Resources/ConsentDocument.md +++ b/OwnYourData/Resources/ConsentDocument.md @@ -1,43 +1,67 @@ **Consent for OwnYourData Application** **Introduction** + Welcome to OwnYourData. To provide you with the best experience and services, we need your consent to access your health records and match them with clinical trials listed in the NCI Clinical Trial Search database. **Purpose** + The purpose of accessing your health records is to identify suitable clinical trials that may benefit you based on your health data. **Data Security** -* **Encryption**: All data pulled from the EHR via the FHIR API will reside on your mobile device and be encrypted both in flight and at rest. Any data generated and aggregated in the application is stored locally using an Apple Keychain-based encryption. Data sent to the cloud-based LLM is encrypted in flight. -* **No Sharing Without Permission**: Only minimal data will be shared with the API, and no data sharing or processes will be performed without your explicit permission. If you agree to participate in a clinical trial, only the necessary information will be shared with the trial organizers. + +**Encryption**: All data pulled from the EHR via the FHIR API will reside on your mobile device and be encrypted both in flight and at rest. Any data generated and aggregated in the application is stored locally using an Apple Keychain-based encryption. Data sent to the cloud-based LLM is encrypted in flight. + +**No Sharing Without Permission**: Only minimal data will be shared with the API, and no data sharing or processes will be performed without your explicit permission. If you agree to participate in a clinical trial, only the necessary information will be shared with the trial organizers. **Data Storage** + * Your patient data is stored in the Apple Health app. + * Users are currently responsible for providing an OpenAI account for cloud-based services. +**Modeled Consent for Recontact** + +By using OwnYourData, you agree that we may contact you in the future regarding your eligibility for additional clinical trials or research studies. We will only contact you if we believe you may be a good fit for a study based on the health data you have provided. You have the right to opt out of these communications at any time by adjusting your settings in the application or by contacting our support team. + **Withdrawal of Consent** + You can withdraw your consent at any time by adjusting your settings in the application or by contacting our support team. **Privacy Policy** + 1. **Information We Collect** + We collect personal health information that you provide or authorize us to access. This includes medical history, current medications, and other relevant health data. 2. **How We Use Your Information** - * **Clinical Trial Matching**: To match your health records with clinical trials in the NCI Clinical Trial Search database. - * **Service Improvement**: To improve our services and provide you with better matches. + + **Clinical Trial Matching**: To match your health records with clinical trials in the NCI Clinical Trial Search database. + + **Service Improvement**: To improve our services and provide you with better matches. + + **Recontact for Additional Studies*: To contact you regarding your eligibility for additional clinical trials or research studies based on your health data. 3. **Data Security** + We implement industry-standard security measures to protect your information. This includes encryption of data both in transit and at rest. 4. **Data Sharing** + Your data will not be shared with any third party without your explicit consent. If you agree to participate in a clinical trial, only the necessary information will be shared with the trial organizers. 5. **Your Rights** - * **Access**: You have the right to access your data at any time. - * **Correction**: You can correct any inaccuracies in your data. - * **Deletion**: You can request the deletion of your data. - * **Withdrawal**: You can withdraw your consent and stop using the app at any time. -6. **Contact Us** - If you have any questions or concerns about our privacy practices or your data, please contact our support team at alexa.aalmai@gmail.com + **Access**: You have the right to access your data at any time. + **Correction**: You can correct any inaccuracies in your data. + **Deletion**: You can request the deletion of your data. + + **Withdrawal**: You can withdraw your consent and stop using the app at any time. + + **Opt-Out**: You can opt out of communications regarding additional studies at any time. + +6. **Contact Us** + + If you have any questions or concerns about our privacy practices or your data, please contact our support team at alexa.aalmai@gmail.com From f8d5924046220a5b038fb978b5599e7743a63c65 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 14 May 2024 23:56:56 -0700 Subject: [PATCH 02/13] Trial Matching --- OwnYourData.xcodeproj/project.pbxproj | 42 ++++- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../ClinicalTrials/ClinicalTrialsView.swift | 108 +++---------- .../ClinicalTrials/NCITrialsModel.swift | 98 ++++++++++++ OwnYourData/Resources/Localizable.xcstrings | 46 ++++++ .../FHIRPrompt+OwnYourData.swift | 40 +++++ .../FHIRResource+Identifier.swift | 27 ++++ .../GetFHIRResourceLLMFunction.swift | 124 +++++++++++++++ .../LLMFunctions/GetTrialsLLMFunction.swift | 46 ++++++ .../TrialsMatching/MatchingModule.swift | 149 ++++++++++++++++++ 10 files changed, 593 insertions(+), 95 deletions(-) create mode 100644 OwnYourData/ClinicalTrials/NCITrialsModel.swift create mode 100644 OwnYourData/TrialsMatching/FHIRPrompt+OwnYourData.swift create mode 100644 OwnYourData/TrialsMatching/FHIRResource+Identifier.swift create mode 100644 OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift create mode 100644 OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift create mode 100644 OwnYourData/TrialsMatching/MatchingModule.swift diff --git a/OwnYourData.xcodeproj/project.pbxproj b/OwnYourData.xcodeproj/project.pbxproj index 72ae22c..a582453 100644 --- a/OwnYourData.xcodeproj/project.pbxproj +++ b/OwnYourData.xcodeproj/project.pbxproj @@ -50,6 +50,12 @@ 2FB099B12A875DF100B20952 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B02A875DF100B20952 /* FirebaseFirestore */; }; 2FB099B32A875DF100B20952 /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B22A875DF100B20952 /* FirebaseFirestoreSwift */; }; 2FB099B62A875E2B00B20952 /* HealthKitOnFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B52A875E2B00B20952 /* HealthKitOnFHIR */; }; + 2FB4DBB82BF4781D00E68AD9 /* MatchingModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBB72BF4781D00E68AD9 /* MatchingModule.swift */; }; + 2FB4DBBB2BF479E600E68AD9 /* GetFHIRResourceLLMFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBBA2BF479E600E68AD9 /* GetFHIRResourceLLMFunction.swift */; }; + 2FB4DBC12BF47ABA00E68AD9 /* FHIRResource+Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBC02BF47ABA00E68AD9 /* FHIRResource+Identifier.swift */; }; + 2FB4DBC62BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBC52BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift */; }; + 2FB4DBC92BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */; }; + 2FB4DBCD2BF4915900E68AD9 /* NCITrialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */; }; 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; 2FCDFF7C2BF33E5400158BDE /* SpeziLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 2FCDFF7B2BF33E5400158BDE /* SpeziLocation */; }; @@ -147,6 +153,12 @@ 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 2FAEC07F297F583900C11C42 /* OwnYourData.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OwnYourData.entitlements; sourceTree = ""; }; + 2FB4DBB72BF4781D00E68AD9 /* MatchingModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingModule.swift; sourceTree = ""; }; + 2FB4DBBA2BF479E600E68AD9 /* GetFHIRResourceLLMFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetFHIRResourceLLMFunction.swift; sourceTree = ""; }; + 2FB4DBC02BF47ABA00E68AD9 /* FHIRResource+Identifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FHIRResource+Identifier.swift"; sourceTree = ""; }; + 2FB4DBC52BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTrialsLLMFunction.swift; sourceTree = ""; }; + 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FHIRPrompt+OwnYourData.swift"; sourceTree = ""; }; + 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCITrialsModel.swift; sourceTree = ""; }; 2FC94CD4298B0A1D009C8209 /* OwnYourData.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = OwnYourData.xctestplan; sourceTree = ""; }; 2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; 2FCDFF7E2BF33ED800158BDE /* LocationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissions.swift; sourceTree = ""; }; @@ -261,6 +273,7 @@ 2F42E9E32B91BB7000D88DB7 /* ClinicalTrials */ = { isa = PBXGroup; children = ( + 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */, 2F42E9E62B91BB8300D88DB7 /* ClinicalTrialsView.swift */, 2FCDFF812BF3417300158BDE /* TrialView.swift */, 2F42E9E52B91BB8300D88DB7 /* WebView.swift */, @@ -295,6 +308,26 @@ path = FHIR; sourceTree = ""; }; + 2FB4DBB92BF479CF00E68AD9 /* TrialsMatching */ = { + isa = PBXGroup; + children = ( + 2FB4DBCF2BF492F600E68AD9 /* LLMFunctions */, + 2FB4DBC02BF47ABA00E68AD9 /* FHIRResource+Identifier.swift */, + 2FB4DBB72BF4781D00E68AD9 /* MatchingModule.swift */, + 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */, + ); + path = TrialsMatching; + sourceTree = ""; + }; + 2FB4DBCF2BF492F600E68AD9 /* LLMFunctions */ = { + isa = PBXGroup; + children = ( + 2FB4DBBA2BF479E600E68AD9 /* GetFHIRResourceLLMFunction.swift */, + 2FB4DBC52BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift */, + ); + path = LLMFunctions; + sourceTree = ""; + }; 2FC9759D2978E30800BA99FE /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -395,6 +428,7 @@ 2FE5DC2829EDD398004B9AB4 /* Onboarding */, 56F6F29E2AB441640022FE5A /* Contributions */, 2F42E9D32B91BB1800D88DB7 /* Documents */, + 2FB4DBB92BF479CF00E68AD9 /* TrialsMatching */, 2F42E9E32B91BB7000D88DB7 /* ClinicalTrials */, 2F42E9E92B91BBDD00D88DB7 /* Instructions */, 2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */, @@ -655,6 +689,7 @@ 2F42E9F22B91BBF300D88DB7 /* HowItWorks.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, 2F42E9DE2B91BB2500D88DB7 /* PDFView.swift in Sources */, + 2FB4DBBB2BF479E600E68AD9 /* GetFHIRResourceLLMFunction.swift in Sources */, 2F38F3A02BE805090002E7D5 /* NICTrialsAPIDateFormatter.swift in Sources */, 2F42E9F42B91BBF300D88DB7 /* Instructions.swift in Sources */, 2FCDFF7F2BF33ED800158BDE /* LocationPermissions.swift in Sources */, @@ -671,6 +706,7 @@ A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F42E9D12B91BAB900D88DB7 /* LogoView.swift in Sources */, + 2FB4DBC12BF47ABA00E68AD9 /* FHIRResource+Identifier.swift in Sources */, 2F42EA072B91C24700D88DB7 /* URL+Zip.swift in Sources */, 2FF53D8D2A8729D600042B76 /* OwnYourDataStandard.swift in Sources */, 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, @@ -680,7 +716,10 @@ 2F42E9DD2B91BB2500D88DB7 /* DocumentGallery.swift in Sources */, 2F42E9DF2B91BB2500D88DB7 /* PDFDocument+Transferable.swift in Sources */, 2F42E9F52B91BBF300D88DB7 /* InstructionsStep.swift in Sources */, + 2FB4DBC92BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift in Sources */, + 2FB4DBC62BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift in Sources */, 2F4E23832989D51F0013F3D9 /* OwnYourDataTestingSetup.swift in Sources */, + 2FB4DBCD2BF4915900E68AD9 /* NCITrialsModel.swift in Sources */, 2F42E9DC2B91BB2500D88DB7 /* DocumentManager.swift in Sources */, 2F42E9E02B91BB2500D88DB7 /* PDFListDetailView.swift in Sources */, 2FCDFF822BF3417300158BDE /* TrialView.swift in Sources */, @@ -697,6 +736,7 @@ 653A2551283387FE005D4D48 /* OwnYourData.swift in Sources */, 2F42EA0E2B91CD7100D88DB7 /* OpenAIAPIKey.swift in Sources */, 2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */, + 2FB4DBB82BF4781D00E68AD9 /* MatchingModule.swift in Sources */, 5661552E2AB854C000209B80 /* PackageHelper.swift in Sources */, 2F42E9FF2B91BE3100D88DB7 /* FHIRStore+Extensions.swift in Sources */, ); @@ -1231,7 +1271,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziLLM.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.8.1; + minimumVersion = 0.8.2; }; }; 2F42E9BE2B91B70F00D88DB7 /* XCRemoteSwiftPackageReference "SpeziFHIR" */ = { diff --git a/OwnYourData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OwnYourData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4ac37a5..5291c2d 100644 --- a/OwnYourData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OwnYourData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -132,8 +132,8 @@ "repositoryURL": "https://github.com/StanfordBDHG/llama.cpp", "state": { "branch": null, - "revision": "7bfd6d4b5bbc9fd47bd023bdbb35f96c827977f3", - "version": "0.2.1" + "revision": "6839853a321778906e210a33ee2c6aec52f34c97", + "version": "0.3.3" } }, { @@ -249,8 +249,8 @@ "repositoryURL": "https://github.com/StanfordSpezi/SpeziLLM.git", "state": { "branch": null, - "revision": "cbaf20496e600c985dea2358f35a497fe4964116", - "version": "0.8.1" + "revision": "94f14f6a1d0fb4c7bb54efa6b6241f18dfc5004d", + "version": "0.8.2" } }, { diff --git a/OwnYourData/ClinicalTrials/ClinicalTrialsView.swift b/OwnYourData/ClinicalTrials/ClinicalTrialsView.swift index a053d44..8a22511 100644 --- a/OwnYourData/ClinicalTrials/ClinicalTrialsView.swift +++ b/OwnYourData/ClinicalTrials/ClinicalTrialsView.swift @@ -14,12 +14,9 @@ import SwiftUI struct ClinicalTrialsView: View { - @Environment(SpeziLocation.self) private var speziLocation + @Environment(NCITrialsModel.self) private var nciTrialsModel @State private var viewState: ViewState = .idle - @State private var trials: [TrialDetail] = [] - @State private var zipCode: String = "10025" - @State private var searchDistance: String = "100" var body: some View { @@ -34,38 +31,39 @@ struct ClinicalTrialsView: View { } @ViewBuilder @MainActor private var content: some View { -// switch viewState { -// case .processing, .error: -// VStack { -// ProgressView() -// Text("Loading NCI Trials") -// } -// case .idle: -// if trials.isEmpty { -// Text("No trials found.") -// } else { + switch viewState { + case .processing, .error: + VStack { + ProgressView() + Text("Loading NCI Trials") + } + case .idle: + if nciTrialsModel.trials.isEmpty { + Text("No trials found.") + } else { List { searchSection Section { - ForEach(trials, id: \.self) { trial in + ForEach(nciTrialsModel.trials, id: \.self) { trial in TrialView(trial: trial) } } } -// } -// } + } + } } @ViewBuilder @MainActor private var searchSection: some View { + @Bindable var nciTrialsModel = nciTrialsModel Section { LabeledContent { - TextField("Enter Zip Code", text: $zipCode) + TextField("Enter Zip Code", text: $nciTrialsModel.zipCode) } label: { Text("Zip Code:") .bold() } LabeledContent { - TextField("Enter Distance (mi)", text: $searchDistance) + TextField("Enter Distance (mi)", text: $nciTrialsModel.searchDistance) } label: { Text("Distance:") .bold() @@ -80,83 +78,13 @@ struct ClinicalTrialsView: View { } .disabled(viewState == .processing) } - - // Function to convert zip code to coordinates - private func getLocationFromZipCode(zipCode: String) async -> CLLocation? { - do { - let placemarks = try await CLGeocoder().geocodeAddressString(zipCode) - if let location = placemarks.first?.location { - return location - } - } catch { - print("Error converting zip code to coordinates: \(error)") - } - return nil - } - - // Function to convert coordinates to zip code - private func getZipCodeFromLocation(location: CLLocation) async { - do { - let placemarks = try await CLGeocoder().reverseGeocodeLocation(location) - if let postalCode = placemarks.first?.postalCode { - zipCode = postalCode - } else { - print("Postal code not found in placemark") - } - } catch { - print("Reverse geocoding failed with error: \(error.localizedDescription)") - } - } - - // Function to load the trials from NCI API - private func loadTrials(coordinate: CLLocationCoordinate2D?) async throws -> TrialResponse { - OpenAPIClientAPI.customHeaders = ["X-API-KEY": "tkMGxBkgOC4TDCUfjcPdw7eeZsuuZual632WpUnH"] - CodableHelper.dateFormatter = NICTrialsAPIDateFormatter() - - return try await withCheckedThrowingContinuation { continuation in - TrialsAPI.searchTrialsByGet( - size: 50, - keyword: "breast", - trialStatus: "OPEN", - phase: "III", - primaryPurpose: "TREATMENT", - sitesOrgCoordinatesLat: coordinate?.latitude, - sitesOrgCoordinatesLon: coordinate?.longitude, - sitesOrgCoordinatesDist: searchDistance + "mi" - ) { data, error in - guard let data else { - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume(throwing: DownloadException.responseFailed) - } - return - } - - continuation.resume(returning: data) - } - } - } private func fetchTrials() async { viewState = .processing // Set loading state - // Reload trials with updated search parameters - trials.removeAll() // Clear existing trials - - // Fetch trials with updated parameters - var coordinate: CLLocationCoordinate2D? - - if let userLocation = try? await speziLocation.getLatestLocations().first { - coordinate = userLocation.coordinate - await getZipCodeFromLocation(location: userLocation) - } else if let location = await getLocationFromZipCode(zipCode: zipCode) { - coordinate = location.coordinate - } - do { - trials = try await loadTrials(coordinate: coordinate).data ?? [] + try await nciTrialsModel.fetchTrials() viewState = .idle } catch { viewState = .error(AnyLocalizedError(error: error)) diff --git a/OwnYourData/ClinicalTrials/NCITrialsModel.swift b/OwnYourData/ClinicalTrials/NCITrialsModel.swift new file mode 100644 index 0000000..a2bfc4a --- /dev/null +++ b/OwnYourData/ClinicalTrials/NCITrialsModel.swift @@ -0,0 +1,98 @@ +// +// This source file is part of the OwnYourData based on the SpeziFHIR project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import CoreLocation +import OpenAPIClient +import SpeziLocation + + +@Observable +class NCITrialsModel { + private static let apiKey: String = "tkMGxBkgOC4TDCUfjcPdw7eeZsuuZual632WpUnH" + + + private let locationModule: SpeziLocation + + private(set) var trials: [TrialDetail] = [] + var zipCode: String = "10025" + var searchDistance: String = "100" + + + init(locationModule: SpeziLocation) { + self.locationModule = locationModule + } + + + func fetchTrials(keywords: [String] = []) async throws { + var coordinate: CLLocationCoordinate2D? + + if let userLocation = try? await locationModule.getLatestLocations().first { + coordinate = userLocation.coordinate + await getZipCodeFromLocation(location: userLocation) + } else if let location = await getLocationFromZipCode(zipCode: zipCode) { + coordinate = location.coordinate + } + + trials = try await loadTrials(keywords: keywords, coordinate: coordinate).data ?? [] + } + + + private func getLocationFromZipCode(zipCode: String) async -> CLLocation? { + do { + let placemarks = try await CLGeocoder().geocodeAddressString(zipCode) + if let location = placemarks.first?.location { + return location + } + } catch { + print("Error converting zip code to coordinates: \(error)") + } + return nil + } + + private func getZipCodeFromLocation(location: CLLocation) async { + do { + let placemarks = try await CLGeocoder().reverseGeocodeLocation(location) + if let postalCode = placemarks.first?.postalCode { + zipCode = postalCode + } else { + print("Postal code not found in placemark") + } + } catch { + print("Reverse geocoding failed with error: \(error.localizedDescription)") + } + } + + private func loadTrials(keywords: [String], coordinate: CLLocationCoordinate2D?) async throws -> TrialResponse { + OpenAPIClientAPI.customHeaders = ["X-API-KEY": Self.apiKey] + CodableHelper.dateFormatter = NICTrialsAPIDateFormatter() + + return try await withCheckedThrowingContinuation { continuation in + TrialsAPI.searchTrialsByGet( + size: 50, + keyword: keywords.joined(separator: ", "), + trialStatus: "OPEN", + phase: "III", + primaryPurpose: "TREATMENT", + sitesOrgCoordinatesLat: coordinate?.latitude, + sitesOrgCoordinatesLon: coordinate?.longitude, + sitesOrgCoordinatesDist: searchDistance + "mi" + ) { data, error in + guard let data else { + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(throwing: DownloadException.responseFailed) + } + return + } + + continuation.resume(returning: data) + } + } + } +} diff --git a/OwnYourData/Resources/Localizable.xcstrings b/OwnYourData/Resources/Localizable.xcstrings index 6774339..6975b31 100644 --- a/OwnYourData/Resources/Localizable.xcstrings +++ b/OwnYourData/Resources/Localizable.xcstrings @@ -68,6 +68,18 @@ } } } + }, + "GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION" : { + + }, + "GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION" : { + + }, + "GET_TRIALS_FUNCTION_DESCRIPTION" : { + + }, + "GET_TRIALS_PARAMETER_DESCRIPTION" : { + }, "Grant Access" : { @@ -93,6 +105,15 @@ }, "How It Works" : { + }, + "Identified Keywords: %@" : { + + }, + "Keyword Identification Prompt" : { + "comment" : "Title of the keyword identification prompt." + }, + "Keyword Identification Prompt Content" : { + "comment" : "Content of the keyword identification prompt." }, "Learn More" : { @@ -102,6 +123,9 @@ }, "License Information" : { + }, + "Loading NCI Trials" : { + }, "Location Access" : { @@ -117,6 +141,9 @@ }, "No Documents" : { + }, + "No trials found." : { + }, "Open the Apple Health app." : { @@ -159,15 +186,34 @@ }, "The following list contains all Swift Package dependencies of the SpeziOwnYourData." : { + }, + "The medical record does not include any FHIR resources for the search term %@." : { + }, "The OwnYourData App Icon" : { + }, + "This is the summary of the requested %@:\n\n%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This is the summary of the requested %1$@:\n\n%2$@" + } + } + } }, "This project is licensed under the MIT License." : { }, "Trial Matching" : { + }, + "Trial Matching Prompt" : { + "comment" : "Title of the trial matching prompt." + }, + "Trial Matching Prompt Content" : { + "comment" : "Content of the trial matching prompt." }, "Update Search" : { diff --git a/OwnYourData/TrialsMatching/FHIRPrompt+OwnYourData.swift b/OwnYourData/TrialsMatching/FHIRPrompt+OwnYourData.swift new file mode 100644 index 0000000..23c2754 --- /dev/null +++ b/OwnYourData/TrialsMatching/FHIRPrompt+OwnYourData.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the OwnYourData based on the SpeziFHIR project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziFHIRLLM + + +extension FHIRPrompt { + static let keywordIdentification: FHIRPrompt = { + FHIRPrompt( + storageKey: "prompt.keywordIdentification", + localizedDescription: String( + localized: "Keyword Identification Prompt", + comment: "Title of the keyword identification prompt." + ), + defaultPrompt: String( + localized: "Keyword Identification Prompt Content", + comment: "Content of the keyword identification prompt." + ) + ) + }() + + static let trialMatching: FHIRPrompt = { + FHIRPrompt( + storageKey: "prompt.trialMatching", + localizedDescription: String( + localized: "Trial Matching Prompt", + comment: "Title of the trial matching prompt." + ), + defaultPrompt: String( + localized: "Trial Matching Prompt Content", + comment: "Content of the trial matching prompt." + ) + ) + }() +} diff --git a/OwnYourData/TrialsMatching/FHIRResource+Identifier.swift b/OwnYourData/TrialsMatching/FHIRResource+Identifier.swift new file mode 100644 index 0000000..6f1e5f6 --- /dev/null +++ b/OwnYourData/TrialsMatching/FHIRResource+Identifier.swift @@ -0,0 +1,27 @@ +// +// This source file is part of the OwnYourData based on the SpeziFHIR project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziFHIR + + +extension FHIRResource { + private static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MM-dd-yyyy" + return dateFormatter + }() + + + var functionCallIdentifier: String { + resourceType.filter { !$0.isWhitespace } + + displayName.filter { !$0.isWhitespace } + + "-" + + (date.map { FHIRResource.dateFormatter.string(from: $0) } ?? "") + } +} diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift new file mode 100644 index 0000000..831452e --- /dev/null +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift @@ -0,0 +1,124 @@ +// +// This source file is part of the OwnYourData based on the SpeziFHIR project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import os +import SpeziFHIR +import SpeziFHIRLLM +import SpeziLLMOpenAI + + +struct GetFHIRResourceLLMFunction: LLMFunction { + static let logger = Logger(subsystem: "edu.stanford.cs342.ownyourdata", category: "FHIRGetResourceLLMFunction") + + static let name = "get_resources" + static let description = String(localized: "GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION") + + private let fhirStore: FHIRStore + private let resourceSummary: FHIRResourceSummary + + + @Parameter var resources: [String] + + + init( + fhirStore: FHIRStore, + resourceSummary: FHIRResourceSummary, + resourceCountLimit: Int, + allowedResourcesFunctionCallIdentifiers: Set? = 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) + } + + _resources = Parameter( + description: String(localized: "GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION"), + enum: 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 + } +} diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift new file mode 100644 index 0000000..5d7ff77 --- /dev/null +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift @@ -0,0 +1,46 @@ +// +// This source file is part of the OwnYourData based on the SpeziFHIR project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import os +import SpeziFHIR +import SpeziFHIRLLM +import SpeziLLMOpenAI + + +struct GetTrialsLLMFunction: LLMFunction { + static let logger = Logger(subsystem: "edu.stanford.cs342.ownyourdata", category: "GetTrialLLMFunction") + + static let name = "get_trials" + static let description = String(localized: "GET_TRIALS_FUNCTION_DESCRIPTION") + + private let nciTrialsModel: NCITrialsModel + + + @Parameter var trailIDs: [String] + + + init( + nciTrialsModel: NCITrialsModel + ) { + self.nciTrialsModel = nciTrialsModel + + _trailIDs = Parameter( + description: String(localized: "GET_TRIALS_PARAMETER_DESCRIPTION"), + enum: nciTrialsModel.trials.compactMap({ $0.nciId }) + ) + } + + + func execute() async throws -> String? { + trailIDs + .map { trailID in + return "" + } + .joined(separator: "\n\n") + } +} diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift new file mode 100644 index 0000000..11585ab --- /dev/null +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -0,0 +1,149 @@ +// +// This source file is part of the OwnYourData based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SpeziFHIR +import SpeziFHIRLLM +import SpeziLLM +import SpeziLLMOpenAI +import SpeziLocalStorage +import SpeziLocation +import SwiftUI + + +enum MatchingState { + case fhirInspection + case nciLoading + case matching +} + + +@Observable +class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { + public enum Defaults { + public static var llmSchema: LLMOpenAISchema { + .init( + parameters: .init( + modelType: .gpt4_turbo_preview, + systemPrompts: [] // No system prompt as this will be determined later by the resource interpreter + ) + ) + } + } + + + @ObservationIgnored @Dependency private var localStorage: LocalStorage + @ObservationIgnored @Dependency private var llmRunner: LLMRunner + @ObservationIgnored @Dependency private var fhirStore: FHIRStore + @ObservationIgnored @Dependency private var locationModule: SpeziLocation + + + @ObservationIgnored @Model private var resourceSummary: FHIRResourceSummary + @ObservationIgnored @Model private var nciTrialsModel: NCITrialsModel + + private var keywords: [String] = [] + + + required init() {} + + + func configure() { + resourceSummary = FHIRResourceSummary( + localStorage: localStorage, + llmRunner: llmRunner, + llmSchema: Defaults.llmSchema + ) + nciTrialsModel = NCITrialsModel( + locationModule: locationModule + ) + } + + + @MainActor + func keywordIdentification() async throws -> [String] { + let llm = llmRunner( + with: LLMOpenAISchema(parameters: .init(modelType: .gpt4_turbo_preview)) { + GetFHIRResourceLLMFunction( + fhirStore: self.fhirStore, + resourceSummary: self.resourceSummary, + resourceCountLimit: 100 + ) + } + ) + + llm.context.append(systemMessage: FHIRPrompt.keywordIdentification.prompt) + + if let patient = fhirStore.patient { + llm.context.append(systemMessage: patient.jsonDescription) + } + + guard let stream = try? await llm.generate() else { + return [] + } + + var output = "" + for try await token in stream { + output.append(token) + } + + keywords = output.components(separatedBy: ",") + return keywords + } + + @MainActor + func trialsIdentificaiton() async throws -> [String] { + if nciTrialsModel.trials.isEmpty { + try await nciTrialsModel.fetchTrials(keywords: keywords) + } + + let llm = llmRunner( + with: LLMOpenAISchema(parameters: .init(modelType: .gpt4_turbo_preview)) { + GetFHIRResourceLLMFunction( + fhirStore: self.fhirStore, + resourceSummary: self.resourceSummary, + resourceCountLimit: 100 + ) + GetTrialsLLMFunction(nciTrialsModel: self.nciTrialsModel) + } + ) + + llm.context.append(systemMessage: FHIRPrompt.trialMatching.prompt) + + if let patient = fhirStore.patient { + llm.context.append(systemMessage: patient.jsonDescription) + } + + if !keywords.isEmpty { + llm.context.append(systemMessage: String(localized: "Identified Keywords: \(keywords.joined(separator: ", "))")) + } + + guard let stream = try? await llm.generate() else { + return [] + } + + var output = "" + for try await token in stream { + output.append(token) + } + + return output.components(separatedBy: ",") + } +} + + +struct MatchingStateView: View { + var body: some View { + Group { + } + } +} + + +#Preview { + MatchingStateView() +} From 798ac02622a76a45bca456979c93f7b533ac9e43 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 14 May 2024 23:57:21 -0700 Subject: [PATCH 03/13] SwiftLint --- .../TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift index 5d7ff77..4bc665c 100644 --- a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift @@ -38,8 +38,8 @@ struct GetTrialsLLMFunction: LLMFunction { func execute() async throws -> String? { trailIDs - .map { trailID in - return "" + .map { _ in + "" } .joined(separator: "\n\n") } From 8ffc4cd0cda59145062a102e893d7ca0bc4f2054 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 00:14:03 -0700 Subject: [PATCH 04/13] Update LLM Functions --- OwnYourData.xcodeproj/project.pbxproj | 20 +++++++++++++-- .../FHIRPrompt+OwnYourData.swift | 0 .../FHIRResource+Identifier.swift | 0 .../LLMFunctions/GetTrialsLLMFunction.swift | 15 ++++++++--- .../TrialsMatching/MatchingModule.swift | 20 --------------- .../TrialsMatching/MatchingState.swift | 16 ++++++++++++ .../TrialsMatching/MatchingStateView.swift | 25 +++++++++++++++++++ 7 files changed, 71 insertions(+), 25 deletions(-) rename OwnYourData/TrialsMatching/{ => Helpers}/FHIRPrompt+OwnYourData.swift (100%) rename OwnYourData/TrialsMatching/{ => Helpers}/FHIRResource+Identifier.swift (100%) create mode 100644 OwnYourData/TrialsMatching/MatchingState.swift create mode 100644 OwnYourData/TrialsMatching/MatchingStateView.swift diff --git a/OwnYourData.xcodeproj/project.pbxproj b/OwnYourData.xcodeproj/project.pbxproj index a582453..7940298 100644 --- a/OwnYourData.xcodeproj/project.pbxproj +++ b/OwnYourData.xcodeproj/project.pbxproj @@ -56,6 +56,8 @@ 2FB4DBC62BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBC52BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift */; }; 2FB4DBC92BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */; }; 2FB4DBCD2BF4915900E68AD9 /* NCITrialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */; }; + 2FB4DBD52BF4946E00E68AD9 /* MatchingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */; }; + 2FB4DBD72BF4948700E68AD9 /* MatchingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */; }; 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; 2FCDFF7C2BF33E5400158BDE /* SpeziLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 2FCDFF7B2BF33E5400158BDE /* SpeziLocation */; }; @@ -159,6 +161,8 @@ 2FB4DBC52BF48E4F00E68AD9 /* GetTrialsLLMFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTrialsLLMFunction.swift; sourceTree = ""; }; 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FHIRPrompt+OwnYourData.swift"; sourceTree = ""; }; 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCITrialsModel.swift; sourceTree = ""; }; + 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingState.swift; sourceTree = ""; }; + 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingStateView.swift; sourceTree = ""; }; 2FC94CD4298B0A1D009C8209 /* OwnYourData.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = OwnYourData.xctestplan; sourceTree = ""; }; 2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; 2FCDFF7E2BF33ED800158BDE /* LocationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissions.swift; sourceTree = ""; }; @@ -311,10 +315,11 @@ 2FB4DBB92BF479CF00E68AD9 /* TrialsMatching */ = { isa = PBXGroup; children = ( + 2FB4DBD22BF4946100E68AD9 /* Helpers */, 2FB4DBCF2BF492F600E68AD9 /* LLMFunctions */, - 2FB4DBC02BF47ABA00E68AD9 /* FHIRResource+Identifier.swift */, + 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */, 2FB4DBB72BF4781D00E68AD9 /* MatchingModule.swift */, - 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */, + 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */, ); path = TrialsMatching; sourceTree = ""; @@ -328,6 +333,15 @@ path = LLMFunctions; sourceTree = ""; }; + 2FB4DBD22BF4946100E68AD9 /* Helpers */ = { + isa = PBXGroup; + children = ( + 2FB4DBC02BF47ABA00E68AD9 /* FHIRResource+Identifier.swift */, + 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 2FC9759D2978E30800BA99FE /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -685,6 +699,7 @@ 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */, 2F42E9F72B91BBF300D88DB7 /* ViewRecordsView.swift in Sources */, 2F42E9E22B91BB2500D88DB7 /* DocumentScanner.swift in Sources */, + 2FB4DBD72BF4948700E68AD9 /* MatchingStateView.swift in Sources */, 2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */, 2F42E9F22B91BBF300D88DB7 /* HowItWorks.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, @@ -724,6 +739,7 @@ 2F42E9E02B91BB2500D88DB7 /* PDFListDetailView.swift in Sources */, 2FCDFF822BF3417300158BDE /* TrialView.swift in Sources */, 2F38F3A52BE8BC890002E7D5 /* TrialDetail+Identifiable.swift in Sources */, + 2FB4DBD52BF4946E00E68AD9 /* MatchingState.swift in Sources */, 2F42E9FA2B91BDD300D88DB7 /* InstructionsView.swift in Sources */, 56F6F2A02AB441930022FE5A /* ContributionsList.swift in Sources */, 2F42E9D22B91BAB900D88DB7 /* OwnYourDataButton.swift in Sources */, diff --git a/OwnYourData/TrialsMatching/FHIRPrompt+OwnYourData.swift b/OwnYourData/TrialsMatching/Helpers/FHIRPrompt+OwnYourData.swift similarity index 100% rename from OwnYourData/TrialsMatching/FHIRPrompt+OwnYourData.swift rename to OwnYourData/TrialsMatching/Helpers/FHIRPrompt+OwnYourData.swift diff --git a/OwnYourData/TrialsMatching/FHIRResource+Identifier.swift b/OwnYourData/TrialsMatching/Helpers/FHIRResource+Identifier.swift similarity index 100% rename from OwnYourData/TrialsMatching/FHIRResource+Identifier.swift rename to OwnYourData/TrialsMatching/Helpers/FHIRResource+Identifier.swift diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift index 4bc665c..2e8b027 100644 --- a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift @@ -38,9 +38,18 @@ struct GetTrialsLLMFunction: LLMFunction { func execute() async throws -> String? { trailIDs - .map { _ in - "" + .compactMap { trailId in + nciTrialsModel.trials.first(where: { $0.nciId == trailId }) + .map { trial in + """ + **Trial \(trailId)** + + Title: \(trial.briefTitle ?? "") (\(trial.officialTitle ?? "")) + Description: \(trial.detailDescription ?? "") + Incluision Criteria: \(trial.eligibility?.unstructured?.compactMap({ $0.description }).joined() ?? "") + """ + } } - .joined(separator: "\n\n") + .joined(separator: "\n\n\n") } } diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift index 11585ab..337a150 100644 --- a/OwnYourData/TrialsMatching/MatchingModule.swift +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -16,13 +16,6 @@ import SpeziLocation import SwiftUI -enum MatchingState { - case fhirInspection - case nciLoading - case matching -} - - @Observable class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { public enum Defaults { @@ -134,16 +127,3 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { return output.components(separatedBy: ",") } } - - -struct MatchingStateView: View { - var body: some View { - Group { - } - } -} - - -#Preview { - MatchingStateView() -} diff --git a/OwnYourData/TrialsMatching/MatchingState.swift b/OwnYourData/TrialsMatching/MatchingState.swift new file mode 100644 index 0000000..a613f33 --- /dev/null +++ b/OwnYourData/TrialsMatching/MatchingState.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the OwnYourData based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +enum MatchingState { + case fhirInspection + case nciLoading + case matching +} diff --git a/OwnYourData/TrialsMatching/MatchingStateView.swift b/OwnYourData/TrialsMatching/MatchingStateView.swift new file mode 100644 index 0000000..aa4a95c --- /dev/null +++ b/OwnYourData/TrialsMatching/MatchingStateView.swift @@ -0,0 +1,25 @@ +// +// MatchingStateView.swift +// OwnYourData +// +// Created by Paul Shmiedmayer on 5/14/24. +// + +import SwiftUI + + +struct MatchingStateView: View { + @Environment(MatchingModule.self) private var matchingModule + + + var body: some View { + Group { + // ... + } + } +} + + +#Preview { + MatchingStateView() +} From 9ffa99bcae0a6a5341fa1cbbc5cd6d3ebe519348 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 00:29:11 -0700 Subject: [PATCH 05/13] Add Complete Function --- .../TrialsMatching/MatchingModule.swift | 32 +++++++++++++------ .../TrialsMatching/MatchingState.swift | 17 +++++++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift index 337a150..0d2921c 100644 --- a/OwnYourData/TrialsMatching/MatchingModule.swift +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import OpenAPIClient import Spezi import SpeziFHIR import SpeziFHIRLLM @@ -13,6 +14,7 @@ import SpeziLLM import SpeziLLMOpenAI import SpeziLocalStorage import SpeziLocation +import SpeziViews import SwiftUI @@ -20,12 +22,7 @@ import SwiftUI class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { public enum Defaults { public static var llmSchema: LLMOpenAISchema { - .init( - parameters: .init( - modelType: .gpt4_turbo_preview, - systemPrompts: [] // No system prompt as this will be determined later by the resource interpreter - ) - ) + LLMOpenAISchema(parameters: LLMOpenAIParameters(modelType: .gpt4_turbo_preview)) } } @@ -34,11 +31,12 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { @ObservationIgnored @Dependency private var llmRunner: LLMRunner @ObservationIgnored @Dependency private var fhirStore: FHIRStore @ObservationIgnored @Dependency private var locationModule: SpeziLocation - @ObservationIgnored @Model private var resourceSummary: FHIRResourceSummary @ObservationIgnored @Model private var nciTrialsModel: NCITrialsModel + var state: MatchingState = .idle + private(set) var matchingTrials: [TrialDetail] = [] private var keywords: [String] = [] @@ -58,7 +56,23 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { @MainActor - func keywordIdentification() async throws -> [String] { + private func matchTrials() async throws { + do { + self.state = .fhirInspection + let keywords = try await keywordIdentification() + self.state = .nciLoading + try await nciTrialsModel.fetchTrials(keywords: keywords) + self.state = .matching + let matchingTrialIds = try await trialsIdentificaiton() + matchingTrials = nciTrialsModel.trials.filter({ trial in matchingTrialIds.contains(where: { $0 == trial.nciId }) }) + self.state = .idle + } catch { + self.state = .error(AnyLocalizedError(error: error)) + } + } + + @MainActor + private func keywordIdentification() async throws -> [String] { let llm = llmRunner( with: LLMOpenAISchema(parameters: .init(modelType: .gpt4_turbo_preview)) { GetFHIRResourceLLMFunction( @@ -89,7 +103,7 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { } @MainActor - func trialsIdentificaiton() async throws -> [String] { + private func trialsIdentificaiton() async throws -> [String] { if nciTrialsModel.trials.isEmpty { try await nciTrialsModel.fetchTrials(keywords: keywords) } diff --git a/OwnYourData/TrialsMatching/MatchingState.swift b/OwnYourData/TrialsMatching/MatchingState.swift index a613f33..ef3a81c 100644 --- a/OwnYourData/TrialsMatching/MatchingState.swift +++ b/OwnYourData/TrialsMatching/MatchingState.swift @@ -7,10 +7,25 @@ // import Foundation +import SpeziViews -enum MatchingState { +enum MatchingState: OperationState { + case idle case fhirInspection case nciLoading case matching + case error(LocalizedError) + + + var representation: ViewState { + switch self { + case .idle: + .idle + case .fhirInspection, .nciLoading, .matching: + .processing + case .error(let error): + .error(error) + } + } } From 93c7c172f0f715166330895dfd507cb17e09b727 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 00:57:15 -0700 Subject: [PATCH 06/13] Add UI for Matching Process --- OwnYourData.xcodeproj/project.pbxproj | 12 ++-- ...iew.swift => ViewClinicalTrialsView.swift} | 4 +- OwnYourData/Home.swift | 10 ++- OwnYourData/Resources/Localizable.xcstrings | 21 ++++++ .../TrialsMatching/MatchingModule.swift | 2 +- .../TrialsMatching/MatchingStateView.swift | 67 +++++++++++++++++-- OwnYourData/TrialsMatching/MatchingView.swift | 44 ++++++++++++ 7 files changed, 146 insertions(+), 14 deletions(-) rename OwnYourData/ClinicalTrials/{ClinicalTrialsView.swift => ViewClinicalTrialsView.swift} (97%) create mode 100644 OwnYourData/TrialsMatching/MatchingView.swift diff --git a/OwnYourData.xcodeproj/project.pbxproj b/OwnYourData.xcodeproj/project.pbxproj index 7940298..42d3107 100644 --- a/OwnYourData.xcodeproj/project.pbxproj +++ b/OwnYourData.xcodeproj/project.pbxproj @@ -27,7 +27,7 @@ 2F42E9E12B91BB2500D88DB7 /* PDFListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9DA2B91BB2500D88DB7 /* PDFListRow.swift */; }; 2F42E9E22B91BB2500D88DB7 /* DocumentScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9DB2B91BB2500D88DB7 /* DocumentScanner.swift */; }; 2F42E9E72B91BB8300D88DB7 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9E52B91BB8300D88DB7 /* WebView.swift */; }; - 2F42E9E82B91BB8300D88DB7 /* ClinicalTrialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9E62B91BB8300D88DB7 /* ClinicalTrialsView.swift */; }; + 2F42E9E82B91BB8300D88DB7 /* ViewClinicalTrialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9E62B91BB8300D88DB7 /* ViewClinicalTrialsView.swift */; }; 2F42E9F22B91BBF300D88DB7 /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9EB2B91BBF300D88DB7 /* HowItWorks.swift */; }; 2F42E9F32B91BBF300D88DB7 /* AddRecordInstructView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9EC2B91BBF300D88DB7 /* AddRecordInstructView.swift */; }; 2F42E9F42B91BBF300D88DB7 /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F42E9ED2B91BBF300D88DB7 /* Instructions.swift */; }; @@ -58,6 +58,7 @@ 2FB4DBCD2BF4915900E68AD9 /* NCITrialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */; }; 2FB4DBD52BF4946E00E68AD9 /* MatchingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */; }; 2FB4DBD72BF4948700E68AD9 /* MatchingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */; }; + 2FB4DBDB2BF4A08000E68AD9 /* MatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */; }; 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; 2FCDFF7C2BF33E5400158BDE /* SpeziLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 2FCDFF7B2BF33E5400158BDE /* SpeziLocation */; }; @@ -136,7 +137,7 @@ 2F42E9DA2B91BB2500D88DB7 /* PDFListRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFListRow.swift; sourceTree = ""; }; 2F42E9DB2B91BB2500D88DB7 /* DocumentScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentScanner.swift; sourceTree = ""; }; 2F42E9E52B91BB8300D88DB7 /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; - 2F42E9E62B91BB8300D88DB7 /* ClinicalTrialsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClinicalTrialsView.swift; sourceTree = ""; }; + 2F42E9E62B91BB8300D88DB7 /* ViewClinicalTrialsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewClinicalTrialsView.swift; sourceTree = ""; }; 2F42E9EB2B91BBF300D88DB7 /* HowItWorks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = ""; }; 2F42E9EC2B91BBF300D88DB7 /* AddRecordInstructView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddRecordInstructView.swift; sourceTree = ""; }; 2F42E9ED2B91BBF300D88DB7 /* Instructions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; @@ -163,6 +164,7 @@ 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCITrialsModel.swift; sourceTree = ""; }; 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingState.swift; sourceTree = ""; }; 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingStateView.swift; sourceTree = ""; }; + 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingView.swift; sourceTree = ""; }; 2FC94CD4298B0A1D009C8209 /* OwnYourData.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = OwnYourData.xctestplan; sourceTree = ""; }; 2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; 2FCDFF7E2BF33ED800158BDE /* LocationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissions.swift; sourceTree = ""; }; @@ -278,7 +280,7 @@ isa = PBXGroup; children = ( 2FB4DBCC2BF4915900E68AD9 /* NCITrialsModel.swift */, - 2F42E9E62B91BB8300D88DB7 /* ClinicalTrialsView.swift */, + 2F42E9E62B91BB8300D88DB7 /* ViewClinicalTrialsView.swift */, 2FCDFF812BF3417300158BDE /* TrialView.swift */, 2F42E9E52B91BB8300D88DB7 /* WebView.swift */, 2F38F39F2BE805090002E7D5 /* NICTrialsAPIDateFormatter.swift */, @@ -320,6 +322,7 @@ 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */, 2FB4DBB72BF4781D00E68AD9 /* MatchingModule.swift */, 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */, + 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */, ); path = TrialsMatching; sourceTree = ""; @@ -739,11 +742,12 @@ 2F42E9E02B91BB2500D88DB7 /* PDFListDetailView.swift in Sources */, 2FCDFF822BF3417300158BDE /* TrialView.swift in Sources */, 2F38F3A52BE8BC890002E7D5 /* TrialDetail+Identifiable.swift in Sources */, + 2FB4DBDB2BF4A08000E68AD9 /* MatchingView.swift in Sources */, 2FB4DBD52BF4946E00E68AD9 /* MatchingState.swift in Sources */, 2F42E9FA2B91BDD300D88DB7 /* InstructionsView.swift in Sources */, 56F6F2A02AB441930022FE5A /* ContributionsList.swift in Sources */, 2F42E9D22B91BAB900D88DB7 /* OwnYourDataButton.swift in Sources */, - 2F42E9E82B91BB8300D88DB7 /* ClinicalTrialsView.swift in Sources */, + 2F42E9E82B91BB8300D88DB7 /* ViewClinicalTrialsView.swift in Sources */, 566155292AB8447C00209B80 /* Package+LicenseType.swift in Sources */, 5680DD392AB8983D004E6D4A /* PackageCell.swift in Sources */, 2F5E32BD297E05EA003432F8 /* OwnYourDataDelegate.swift in Sources */, diff --git a/OwnYourData/ClinicalTrials/ClinicalTrialsView.swift b/OwnYourData/ClinicalTrials/ViewClinicalTrialsView.swift similarity index 97% rename from OwnYourData/ClinicalTrials/ClinicalTrialsView.swift rename to OwnYourData/ClinicalTrials/ViewClinicalTrialsView.swift index 8a22511..8355d4a 100644 --- a/OwnYourData/ClinicalTrials/ClinicalTrialsView.swift +++ b/OwnYourData/ClinicalTrials/ViewClinicalTrialsView.swift @@ -13,7 +13,7 @@ import SpeziViews import SwiftUI -struct ClinicalTrialsView: View { +struct ViewClinicalTrialsView: View { @Environment(NCITrialsModel.self) private var nciTrialsModel @State private var viewState: ViewState = .idle @@ -94,5 +94,5 @@ struct ClinicalTrialsView: View { #Preview { - ClinicalTrialsView() + ViewClinicalTrialsView() } diff --git a/OwnYourData/Home.swift b/OwnYourData/Home.swift index 23fefea..f43142b 100644 --- a/OwnYourData/Home.swift +++ b/OwnYourData/Home.swift @@ -21,6 +21,7 @@ struct HomeView: View { @Environment(FHIRStore.self) var fjirStore @State private var presentingAccount = false + @State private var showMatchingView = false @State private var showClinicalTrialsView = false @@ -37,6 +38,9 @@ struct HomeView: View { InstructionsView() } OwnYourDataButton(title: "Match Me") { + showMatchingView = true + } + OwnYourDataButton(title: "Local NCI Trials") { showClinicalTrialsView = true } OwnYourDataButton( @@ -59,9 +63,11 @@ struct HomeView: View { } } } + .sheet(isPresented: $showMatchingView) { + MatchingView() + } .sheet(isPresented: $showClinicalTrialsView) { - ClinicalTrialsView() - .edgesIgnoringSafeArea(.all) + ViewClinicalTrialsView() } .sheet(isPresented: $presentingAccount) { AccountSheet() diff --git a/OwnYourData/Resources/Localizable.xcstrings b/OwnYourData/Resources/Localizable.xcstrings index 6975b31..0dcd718 100644 --- a/OwnYourData/Resources/Localizable.xcstrings +++ b/OwnYourData/Resources/Localizable.xcstrings @@ -49,6 +49,9 @@ }, "Enter Zip Code" : { + }, + "Error matching you to NCI trials. Please try again." : { + }, "Export" : { @@ -108,6 +111,12 @@ }, "Identified Keywords: %@" : { + }, + "Identifying best matching trials ..." : { + + }, + "Inspecting FHIR resources ..." : { + }, "Keyword Identification Prompt" : { "comment" : "Title of the keyword identification prompt." @@ -126,9 +135,15 @@ }, "Loading NCI Trials" : { + }, + "Loading NCI trials based on FHIR resources ..." : { + }, "Location Access" : { + }, + "Match Me" : { + }, "Navigate to the Browse tab." : { @@ -162,6 +177,9 @@ }, "Please refer to the individual repository links for packages without license labels." : { + }, + "Reload Matching" : { + }, "Repository Link" : { @@ -220,6 +238,9 @@ }, "Use HealthKit Resources" : { + }, + "Use the OwnYourData algorithm to match you to possible NCI trials." : { + }, "We automatically match you to active trials." : { diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift index 0d2921c..c9d8512 100644 --- a/OwnYourData/TrialsMatching/MatchingModule.swift +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -56,7 +56,7 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { @MainActor - private func matchTrials() async throws { + func matchTrials() async { do { self.state = .fhirInspection let keywords = try await keywordIdentification() diff --git a/OwnYourData/TrialsMatching/MatchingStateView.swift b/OwnYourData/TrialsMatching/MatchingStateView.swift index aa4a95c..e6f0336 100644 --- a/OwnYourData/TrialsMatching/MatchingStateView.swift +++ b/OwnYourData/TrialsMatching/MatchingStateView.swift @@ -1,20 +1,74 @@ // -// MatchingStateView.swift -// OwnYourData +// This source file is part of the OwnYourData based on the Stanford Spezi Template Application project // -// Created by Paul Shmiedmayer on 5/14/24. +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT // import SwiftUI +private struct MatchingstateLogo: View { + let image: String + let text: LocalizedStringResource + + var body: some View { + VStack { + Image(systemName: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 200) + .foregroundStyle(.accent) + .padding() + .accessibilityHidden(true) + Text(text) + ProgressView() + } + } +} + + struct MatchingStateView: View { @Environment(MatchingModule.self) private var matchingModule var body: some View { - Group { - // ... + switch matchingModule.state { + case .idle: + VStack { + LogoView() + Text("Use the OwnYourData algorithm to match you to possible NCI trials.") + .multilineTextAlignment(.center) + .padding() + } + + case .fhirInspection: + MatchingstateLogo( + image: "text.magnifyingglass", + text: "Inspecting FHIR resources ..." + ) + case .nciLoading: + MatchingstateLogo( + image: "network", + text: "Loading NCI trials based on FHIR resources ..." + ) + case .matching: + MatchingstateLogo( + image: "wand.and.stars.inverse", + text: "Identifying best matching trials ..." + ) + case .error: + VStack { + Image(systemName: "x.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 200) + .foregroundStyle(.red) + .accessibilityHidden(true) + .padding() + Text("Error matching you to NCI trials. Please try again.") + } } } } @@ -22,4 +76,7 @@ struct MatchingStateView: View { #Preview { MatchingStateView() + .previewWith { + MatchingModule() + } } diff --git a/OwnYourData/TrialsMatching/MatchingView.swift b/OwnYourData/TrialsMatching/MatchingView.swift new file mode 100644 index 0000000..04e9199 --- /dev/null +++ b/OwnYourData/TrialsMatching/MatchingView.swift @@ -0,0 +1,44 @@ +// +// This source file is part of the OwnYourData based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziViews +import SwiftUI + + +struct MatchingView: View { + @Environment(MatchingModule.self) private var matchingModule + + + var body: some View { + NavigationStack { + MatchingStateView() + .navigationTitle("Match Me") + .toolbar { + AsyncButton( + action: { + await matchingModule.matchTrials() + }, + label: { + Image(systemName: "arrow.counterclockwise") + .accessibilityLabel(Text("Reload Matching")) + } + ) + .disabled(matchingModule.state.representation == .processing) + } + .viewStateAlert(state: matchingModule.state) + } + } +} + + +#Preview { + MatchingView() + .previewWith { + MatchingModule() + } +} From 3a0c654cdc85e460a9562c3d6250fe965f2272ab Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 01:16:22 -0700 Subject: [PATCH 07/13] Improve Setupg --- .../Helpers/TrialDetail+LLMIndentifier.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 OwnYourData/TrialsMatching/Helpers/TrialDetail+LLMIndentifier.swift diff --git a/OwnYourData/TrialsMatching/Helpers/TrialDetail+LLMIndentifier.swift b/OwnYourData/TrialsMatching/Helpers/TrialDetail+LLMIndentifier.swift new file mode 100644 index 0000000..d8ac803 --- /dev/null +++ b/OwnYourData/TrialsMatching/Helpers/TrialDetail+LLMIndentifier.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the OwnYourData based on the SpeziFHIR project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OpenAPIClient + + +extension TrialsDetail { + var alphanumeric: String { + return self.components(separatedBy: CharacterSet.alphanumerics.inverted).joined().lowercased() + } +} From 583d9e64da8791c87594f92b6c5e009c6285034f Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 01:28:51 -0700 Subject: [PATCH 08/13] Update UI and Display Resulting Trials --- OwnYourData.xcodeproj/project.pbxproj | 4 +++ .../xcschemes/OwnYourData.xcscheme | 2 +- .../ClinicalTrials/NCITrialsModel.swift | 14 +++++++- .../ViewClinicalTrialsView.swift | 1 + OwnYourData/OwnYourDataDelegate.swift | 2 ++ OwnYourData/Resources/Localizable.xcstrings | 3 ++ .../Helpers/TrialDetail+LLMIndentifier.swift | 7 ++-- .../LLMFunctions/GetTrialsLLMFunction.swift | 14 ++++---- .../TrialsMatching/MatchingModule.swift | 20 ++++++++--- .../TrialsMatching/MatchingStateView.swift | 6 ++++ OwnYourData/TrialsMatching/MatchingView.swift | 36 +++++++++++++------ 11 files changed, 81 insertions(+), 28 deletions(-) diff --git a/OwnYourData.xcodeproj/project.pbxproj b/OwnYourData.xcodeproj/project.pbxproj index 42d3107..a2bb87e 100644 --- a/OwnYourData.xcodeproj/project.pbxproj +++ b/OwnYourData.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 2FB4DBD52BF4946E00E68AD9 /* MatchingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */; }; 2FB4DBD72BF4948700E68AD9 /* MatchingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */; }; 2FB4DBDB2BF4A08000E68AD9 /* MatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */; }; + 2FB4DBDE2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBDD2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift */; }; 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; 2FCDFF7C2BF33E5400158BDE /* SpeziLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 2FCDFF7B2BF33E5400158BDE /* SpeziLocation */; }; @@ -165,6 +166,7 @@ 2FB4DBD42BF4946E00E68AD9 /* MatchingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingState.swift; sourceTree = ""; }; 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingStateView.swift; sourceTree = ""; }; 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingView.swift; sourceTree = ""; }; + 2FB4DBDD2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrialDetail+LLMIndentifier.swift"; sourceTree = ""; }; 2FC94CD4298B0A1D009C8209 /* OwnYourData.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = OwnYourData.xctestplan; sourceTree = ""; }; 2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; 2FCDFF7E2BF33ED800158BDE /* LocationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissions.swift; sourceTree = ""; }; @@ -341,6 +343,7 @@ children = ( 2FB4DBC02BF47ABA00E68AD9 /* FHIRResource+Identifier.swift */, 2FB4DBC82BF48E7400E68AD9 /* FHIRPrompt+OwnYourData.swift */, + 2FB4DBDD2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift */, ); path = Helpers; sourceTree = ""; @@ -725,6 +728,7 @@ 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F42E9D12B91BAB900D88DB7 /* LogoView.swift in Sources */, 2FB4DBC12BF47ABA00E68AD9 /* FHIRResource+Identifier.swift in Sources */, + 2FB4DBDE2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift in Sources */, 2F42EA072B91C24700D88DB7 /* URL+Zip.swift in Sources */, 2FF53D8D2A8729D600042B76 /* OwnYourDataStandard.swift in Sources */, 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, diff --git a/OwnYourData.xcodeproj/xcshareddata/xcschemes/OwnYourData.xcscheme b/OwnYourData.xcodeproj/xcshareddata/xcschemes/OwnYourData.xcscheme index ae7e63f..65ee5e3 100644 --- a/OwnYourData.xcodeproj/xcshareddata/xcschemes/OwnYourData.xcscheme +++ b/OwnYourData.xcodeproj/xcshareddata/xcschemes/OwnYourData.xcscheme @@ -79,7 +79,7 @@ + isEnabled = "NO"> String? { - trailIDs - .compactMap { trailId in - nciTrialsModel.trials.first(where: { $0.nciId == trailId }) + trailIdentifiers + .compactMap { trailIdentifier in + nciTrialsModel.trials.first(where: { $0.llmIdentifier == trailIdentifier }) .map { trial in """ - **Trial \(trailId)** + **Trial \(trailIdentifier)** Title: \(trial.briefTitle ?? "") (\(trial.officialTitle ?? "")) Description: \(trial.detailDescription ?? "") diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift index c9d8512..d1a1b70 100644 --- a/OwnYourData/TrialsMatching/MatchingModule.swift +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -58,16 +58,26 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { @MainActor func matchTrials() async { do { - self.state = .fhirInspection + withAnimation { + self.state = .fhirInspection + } let keywords = try await keywordIdentification() - self.state = .nciLoading + withAnimation { + self.state = .nciLoading + } try await nciTrialsModel.fetchTrials(keywords: keywords) - self.state = .matching + withAnimation { + self.state = .matching + } let matchingTrialIds = try await trialsIdentificaiton() matchingTrials = nciTrialsModel.trials.filter({ trial in matchingTrialIds.contains(where: { $0 == trial.nciId }) }) - self.state = .idle + withAnimation { + self.state = .idle + } } catch { - self.state = .error(AnyLocalizedError(error: error)) + withAnimation { + self.state = .error(AnyLocalizedError(error: error)) + } } } diff --git a/OwnYourData/TrialsMatching/MatchingStateView.swift b/OwnYourData/TrialsMatching/MatchingStateView.swift index e6f0336..006898c 100644 --- a/OwnYourData/TrialsMatching/MatchingStateView.swift +++ b/OwnYourData/TrialsMatching/MatchingStateView.swift @@ -24,6 +24,7 @@ private struct MatchingstateLogo: View { .accessibilityHidden(true) Text(text) ProgressView() + .padding() } } } @@ -41,6 +42,11 @@ struct MatchingStateView: View { Text("Use the OwnYourData algorithm to match you to possible NCI trials.") .multilineTextAlignment(.center) .padding() + Button("Start Matching") { + Task { + await matchingModule.matchTrials() + } + } } case .fhirInspection: diff --git a/OwnYourData/TrialsMatching/MatchingView.swift b/OwnYourData/TrialsMatching/MatchingView.swift index 04e9199..90b8078 100644 --- a/OwnYourData/TrialsMatching/MatchingView.swift +++ b/OwnYourData/TrialsMatching/MatchingView.swift @@ -16,19 +16,33 @@ struct MatchingView: View { var body: some View { NavigationStack { - MatchingStateView() + Group { + if matchingModule.matchingTrials.isEmpty || matchingModule.state.representation == .processing { + MatchingStateView() + } else { + List { + Section { + ForEach(matchingModule.matchingTrials, id: \.self) { trial in + TrialView(trial: trial) + } + } + } + } + } .navigationTitle("Match Me") .toolbar { - AsyncButton( - action: { - await matchingModule.matchTrials() - }, - label: { - Image(systemName: "arrow.counterclockwise") - .accessibilityLabel(Text("Reload Matching")) - } - ) - .disabled(matchingModule.state.representation == .processing) + if !(matchingModule.matchingTrials.isEmpty || matchingModule.state.representation == .processing) { + AsyncButton( + action: { + await matchingModule.matchTrials() + }, + label: { + Image(systemName: "arrow.counterclockwise") + .accessibilityLabel(Text("Reload Matching")) + } + ) + .disabled(matchingModule.state.representation == .processing) + } } .viewStateAlert(state: matchingModule.state) } From 08820587cbdf24e7970090dc6a8bb525d805835a Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 01:32:19 -0700 Subject: [PATCH 09/13] Start Localization --- OwnYourData/Resources/Localizable.xcstrings | 36 +++++++++---------- .../Helpers/FHIRPrompt+OwnYourData.swift | 4 +-- .../GetFHIRResourceLLMFunction.swift | 4 +-- .../LLMFunctions/GetTrialsLLMFunction.swift | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/OwnYourData/Resources/Localizable.xcstrings b/OwnYourData/Resources/Localizable.xcstrings index 4e6533f..4b08bce 100644 --- a/OwnYourData/Resources/Localizable.xcstrings +++ b/OwnYourData/Resources/Localizable.xcstrings @@ -71,18 +71,6 @@ } } } - }, - "GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION" : { - - }, - "GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION" : { - - }, - "GET_TRIALS_FUNCTION_DESCRIPTION" : { - - }, - "GET_TRIALS_PARAMETER_DESCRIPTION" : { - }, "Grant Access" : { @@ -121,9 +109,6 @@ "Keyword Identification Prompt" : { "comment" : "Title of the keyword identification prompt." }, - "Keyword Identification Prompt Content" : { - "comment" : "Content of the keyword identification prompt." - }, "Learn More" : { }, @@ -132,6 +117,24 @@ }, "License Information" : { + }, + "LLM_GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION" : { + + }, + "LLM_GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION" : { + + }, + "LLM_GET_TRIALS_FUNCTION_DESCRIPTION" : { + + }, + "LLM_GET_TRIALS_PARAMETER_DESCRIPTION" : { + + }, + "LLM_KEYWORD_IDENTIFICATION_PROMPT" : { + "comment" : "Content of the keyword identification prompt." + }, + "LLM_TRIAL_MATCHING_PROMPT" : { + "comment" : "Content of the trial matching prompt." }, "Loading NCI Trials" : { @@ -233,9 +236,6 @@ "Trial Matching Prompt" : { "comment" : "Title of the trial matching prompt." }, - "Trial Matching Prompt Content" : { - "comment" : "Content of the trial matching prompt." - }, "Update Search" : { }, diff --git a/OwnYourData/TrialsMatching/Helpers/FHIRPrompt+OwnYourData.swift b/OwnYourData/TrialsMatching/Helpers/FHIRPrompt+OwnYourData.swift index 23c2754..a3900d8 100644 --- a/OwnYourData/TrialsMatching/Helpers/FHIRPrompt+OwnYourData.swift +++ b/OwnYourData/TrialsMatching/Helpers/FHIRPrompt+OwnYourData.swift @@ -18,7 +18,7 @@ extension FHIRPrompt { comment: "Title of the keyword identification prompt." ), defaultPrompt: String( - localized: "Keyword Identification Prompt Content", + localized: "LLM_KEYWORD_IDENTIFICATION_PROMPT", comment: "Content of the keyword identification prompt." ) ) @@ -32,7 +32,7 @@ extension FHIRPrompt { comment: "Title of the trial matching prompt." ), defaultPrompt: String( - localized: "Trial Matching Prompt Content", + localized: "LLM_TRIAL_MATCHING_PROMPT", comment: "Content of the trial matching prompt." ) ) diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift index 831452e..5cb9cd0 100644 --- a/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetFHIRResourceLLMFunction.swift @@ -16,7 +16,7 @@ struct GetFHIRResourceLLMFunction: LLMFunction { static let logger = Logger(subsystem: "edu.stanford.cs342.ownyourdata", category: "FHIRGetResourceLLMFunction") static let name = "get_resources" - static let description = String(localized: "GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION") + static let description = String(localized: "LLM_GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION") private let fhirStore: FHIRStore private let resourceSummary: FHIRResourceSummary @@ -43,7 +43,7 @@ struct GetFHIRResourceLLMFunction: LLMFunction { } _resources = Parameter( - description: String(localized: "GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION"), + description: String(localized: "LLM_GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION"), enum: Array(allResourcesFunctionCallIdentifiers) ) } diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift index db256e6..2f1172b 100644 --- a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift @@ -16,7 +16,7 @@ struct GetTrialsLLMFunction: LLMFunction { static let logger = Logger(subsystem: "edu.stanford.cs342.ownyourdata", category: "GetTrialLLMFunction") static let name = "get_trials" - static let description = String(localized: "GET_TRIALS_FUNCTION_DESCRIPTION") + static let description = String(localized: "LLM_GET_TRIALS_FUNCTION_DESCRIPTION") private let nciTrialsModel: NCITrialsModel @@ -30,7 +30,7 @@ struct GetTrialsLLMFunction: LLMFunction { self.nciTrialsModel = nciTrialsModel _trailIdentifiers = Parameter( - description: String(localized: "GET_TRIALS_PARAMETER_DESCRIPTION"), + description: String(localized: "LLM_GET_TRIALS_PARAMETER_DESCRIPTION"), enum: nciTrialsModel.trials.compactMap({ $0.llmIdentifier }) ) } From e82fd3c6bd6e3ff0e4a436c9f3a946325833854c Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 03:06:50 -0700 Subject: [PATCH 10/13] Commit MVP --- OwnYourData.xcodeproj/project.pbxproj | 12 + OwnYourData/Account/ResourceSelection.swift | 49 +- .../ClinicalTrials/NCITrialsModel.swift | 12 +- .../ExamplePatients/BreastCancerExample.json | 438 ++++++++++++++++++ OwnYourData/Resources/Localizable.xcstrings | 56 ++- .../LLMFunctions/GetTrialsLLMFunction.swift | 2 + .../TrialsMatching/MatchingModule.swift | 18 +- .../TrialsMatching/MatchingStateView.swift | 9 +- OwnYourData/TrialsMatching/MatchingView.swift | 5 +- 9 files changed, 570 insertions(+), 31 deletions(-) create mode 100644 OwnYourData/Resources/ExamplePatients/BreastCancerExample.json diff --git a/OwnYourData.xcodeproj/project.pbxproj b/OwnYourData.xcodeproj/project.pbxproj index a2bb87e..f069a6d 100644 --- a/OwnYourData.xcodeproj/project.pbxproj +++ b/OwnYourData.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 2FB4DBD72BF4948700E68AD9 /* MatchingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */; }; 2FB4DBDB2BF4A08000E68AD9 /* MatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */; }; 2FB4DBDE2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB4DBDD2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift */; }; + 2FB4DBE42BF4AEF200E68AD9 /* BreastCancerExample.json in Resources */ = {isa = PBXBuildFile; fileRef = 2FB4DBE22BF4AE4200E68AD9 /* BreastCancerExample.json */; }; 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; 2FCDFF7C2BF33E5400158BDE /* SpeziLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 2FCDFF7B2BF33E5400158BDE /* SpeziLocation */; }; @@ -167,6 +168,7 @@ 2FB4DBD62BF4948700E68AD9 /* MatchingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingStateView.swift; sourceTree = ""; }; 2FB4DBDA2BF4A08000E68AD9 /* MatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingView.swift; sourceTree = ""; }; 2FB4DBDD2BF4A62100E68AD9 /* TrialDetail+LLMIndentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrialDetail+LLMIndentifier.swift"; sourceTree = ""; }; + 2FB4DBE22BF4AE4200E68AD9 /* BreastCancerExample.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = BreastCancerExample.json; sourceTree = ""; }; 2FC94CD4298B0A1D009C8209 /* OwnYourData.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = OwnYourData.xctestplan; sourceTree = ""; }; 2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; 2FCDFF7E2BF33ED800158BDE /* LocationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissions.swift; sourceTree = ""; }; @@ -348,6 +350,14 @@ path = Helpers; sourceTree = ""; }; + 2FB4DBE12BF4AE2500E68AD9 /* ExamplePatients */ = { + isa = PBXGroup; + children = ( + 2FB4DBE22BF4AE4200E68AD9 /* BreastCancerExample.json */, + ); + path = ExamplePatients; + sourceTree = ""; + }; 2FC9759D2978E30800BA99FE /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -375,6 +385,7 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */ = { isa = PBXGroup; children = ( + 2FB4DBE12BF4AE2500E68AD9 /* ExamplePatients */, 653A255428338800005D4D48 /* Assets.xcassets */, 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */, 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */, @@ -657,6 +668,7 @@ 653A255528338800005D4D48 /* Assets.xcassets in Resources */, 2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */, 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */, + 2FB4DBE42BF4AEF200E68AD9 /* BreastCancerExample.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OwnYourData/Account/ResourceSelection.swift b/OwnYourData/Account/ResourceSelection.swift index 6b04933..148778f 100644 --- a/OwnYourData/Account/ResourceSelection.swift +++ b/OwnYourData/Account/ResourceSelection.swift @@ -84,13 +84,50 @@ struct ResourceSelection: View { private var mockPatients: [ModelsR4.Bundle] { get async { await [ - .allen322Ferry570, - .beatris270Bogan287, - .edythe31Morar593, - .gonzalo160Duenas839, - .jacklyn830Veum823, - .milton509Ortiz186 + .breastCancerExample ] } } } + + +extension ModelsR4.Bundle { + private static var _breastCancerExample: ModelsR4.Bundle? + /// Example FHIR resources packed into a bundle to represent the simulated patient named Jamison785 Denesik803. + public static var breastCancerExample: ModelsR4.Bundle { + get async { + if let breastCancerExample = _breastCancerExample { + return breastCancerExample + } + + let breastCancerExample = await Foundation.Bundle.main.improvedLoadFHIRBundle( + withName: "BreastCancerExample" + ) + ModelsR4.Bundle._breastCancerExample = breastCancerExample + return breastCancerExample + } + } +} + + +extension Foundation.Bundle { + /// Loads a FHIR `Bundle` from a Foundation `Bundle`. + /// - Parameter name: Name of the JSON file in the Foundation `Bundle` + /// - Returns: The FHIR `Bundle` + public func improvedLoadFHIRBundle(withName name: String) async -> ModelsR4.Bundle { + guard let resourceURL = self.url(forResource: name, withExtension: "json") else { + fatalError("Could not find the resource \"\(name)\".json in the SpeziFHIRMockPatients Resources folder.") + } + + let loadingTask = _Concurrency.Task { + let resourceData = try Data(contentsOf: resourceURL) + return try JSONDecoder().decode(Bundle.self, from: resourceData) + } + + do { + return try await loadingTask.value + } catch { + fatalError("Could not decode the FHIR bundle named \"\(name).json\": \(error)") + } + } +} diff --git a/OwnYourData/ClinicalTrials/NCITrialsModel.swift b/OwnYourData/ClinicalTrials/NCITrialsModel.swift index 6f15b2c..da04a9d 100644 --- a/OwnYourData/ClinicalTrials/NCITrialsModel.swift +++ b/OwnYourData/ClinicalTrials/NCITrialsModel.swift @@ -75,7 +75,7 @@ class NCITrialsModel { return try await withCheckedThrowingContinuation { continuation in TrialsAPI.searchTrialsByGet( - size: 50, + size: 20, keyword: keywords.isEmpty ? nil : keywords.joined(separator: " "), trialStatus: "OPEN", phase: "III", @@ -86,16 +86,6 @@ class NCITrialsModel { ) { data, error in guard let data else { if let error { - switch error as? ErrorResponse { - case let .error(int, data, response, error): - print(keywords) - print(int) - print(String(data: data!, encoding: .utf8)!) - print(response!) - print(error) - default: - print(error) - } continuation.resume(throwing: error) } else { continuation.resume(throwing: DownloadException.responseFailed) diff --git a/OwnYourData/Resources/ExamplePatients/BreastCancerExample.json b/OwnYourData/Resources/ExamplePatients/BreastCancerExample.json new file mode 100644 index 0000000..1da2c4e --- /dev/null +++ b/OwnYourData/Resources/ExamplePatients/BreastCancerExample.json @@ -0,0 +1,438 @@ +{ + "resourceType":"Bundle", + "type":"transaction", + "entry":[ + { + "fullUrl":"urn:uuid:ad134528-56a5-35fd-c37f-466ff119c625", + "resource":{ + "resourceType":"Patient", + "id":"ad134528-56a5-35fd-c37f-466ff119c625", + "meta":{ + "profile":[ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "text":{ + "status":"generated", + "div":"
Generated by OpenAI.
" + }, + "extension":[ + { + "url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension":[ + { + "url":"ombCategory", + "valueCoding":{ + "system":"urn:oid:2.16.840.1.113883.6.238", + "code":"2106-3", + "display":"White" + } + }, + { + "url":"text", + "valueString":"White" + } + ] + }, + { + "url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension":[ + { + "url":"ombCategory", + "valueCoding":{ + "system":"urn:oid:2.16.840.1.113883.6.238", + "code":"2186-5", + "display":"Not Hispanic or Latino" + } + }, + { + "url":"text", + "valueString":"Not Hispanic or Latino" + } + ] + }, + { + "url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName", + "valueString":"Doe" + }, + { + "url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode":"F" + }, + { + "url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress":{ + "city":"Springfield", + "state":"Illinois", + "country":"US" + } + }, + { + "url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years", + "valueDecimal":15.0 + }, + { + "url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years", + "valueDecimal":70.0 + } + ], + "identifier":[ + { + "system":"http://hospital.smarthealthit.org", + "value":"123456789" + }, + { + "type":{ + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v2-0203", + "code":"MR", + "display":"Medical Record Number" + } + ], + "text":"Medical Record Number" + }, + "system":"http://hospital.smarthealthit.org", + "value":"987654321" + } + ], + "name":[ + { + "use":"official", + "family":"Smith", + "given":[ + "Jane" + ], + "prefix":[ + "Ms." + ] + } + ], + "telecom":[ + { + "system":"phone", + "value":"555-123-4567", + "use":"home" + } + ], + "gender":"female", + "birthDate":"1965-08-20", + "address":[ + { + "extension":[ + { + "url":"http://hl7.org/fhir/StructureDefinition/geolocation", + "extension":[ + { + "url":"latitude", + "valueDecimal":39.7817 + }, + { + "url":"longitude", + "valueDecimal":-89.6501 + } + ] + } + ], + "line":[ + "123 Main Street" + ], + "city":"Springfield", + "state":"IL", + "postalCode":"62701", + "country":"US" + } + ], + "maritalStatus":{ + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus", + "code":"M", + "display":"Married" + } + ], + "text":"Married" + }, + "multipleBirthBoolean":false, + "communication":[ + { + "language":{ + "coding":[ + { + "system":"urn:ietf:bcp:47", + "code":"en-US", + "display":"English" + } + ], + "text":"English" + } + } + ] + }, + "request":{ + "method":"POST", + "url":"Patient" + } + }, + { + "fullUrl":"urn:uuid:78965432-1234-5678-90ab-cdef12345678", + "resource":{ + "resourceType":"Encounter", + "id":"78965432-1234-5678-90ab-cdef12345678", + "meta":{ + "profile":[ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter" + ] + }, + "identifier":[ + { + "use":"official", + "system":"http://hospital.smarthealthit.org", + "value":"78965432-1234-5678-90ab-cdef12345678" + } + ], + "status":"finished", + "class":{ + "system":"http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code":"AMB" + }, + "type":[ + { + "coding":[ + { + "system":"http://snomed.info/sct", + "code":"185345009", + "display":"Encounter for symptom" + } + ], + "text":"Encounter for symptom" + } + ], + "subject":{ + "reference":"urn:uuid:ad134528-56a5-35fd-c37f-466ff119c625", + "display":"Ms. Jane Smith" + }, + "participant":[ + { + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-ParticipationType", + "code":"PPRF", + "display":"primary performer" + } + ], + "text":"primary performer" + } + ], + "individual":{ + "reference":"Practitioner/123456", + "display":"Dr. John Doe" + } + } + ], + "period":{ + "start":"2023-05-01T08:30:00-05:00", + "end":"2023-05-01T09:00:00-05:00" + }, + "reasonCode":[ + { + "coding":[ + { + "system":"http://snomed.info/sct", + "code":"254837009", + "display":"Breast lump" + } + ] + } + ], + "location":[ + { + "location":{ + "reference":"Location/1", + "display":"General Hospital" + } + } + ], + "serviceProvider":{ + "reference":"Organization/1", + "display":"General Hospital" + } + }, + "request":{ + "method":"POST", + "url":"Encounter" + } + }, + { + "fullUrl":"urn:uuid:45678901-2345-6789-0abc-def123456789", + "resource":{ + "resourceType":"Condition", + "id":"45678901-2345-6789-0abc-def123456789", + "meta":{ + "profile":[ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition" + ] + }, + "clinicalStatus":{ + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/condition-clinical", + "code":"active" + } + ] + }, + "verificationStatus":{ + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/condition-ver-status", + "code":"confirmed" + } + ] + }, + "category":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/condition-category", + "code":"encounter-diagnosis", + "display":"Encounter Diagnosis" + } + ] + } + ], + "code":{ + "coding":[ + { + "system":"http://snomed.info/sct", + "code":"254837009", + "display":"Breast lump" + } + ], + "text":"Breast lump" + }, + "subject":{ + "reference":"urn:uuid:ad134528-56a5-35fd-c37f-466ff119c625" + }, + "encounter":{ + "reference":"urn:uuid:78965432-1234-5678-90ab-cdef12345678" + }, + "onsetDateTime":"2023-05-01T08:00:00-05:00", + "recordedDate":"2023-05-01T08:30:00-05:00" + }, + "request":{ + "method":"POST", + "url":"Condition" + } + }, + { + "fullUrl":"urn:uuid:67890123-4567-89ab-cdef-0123456789ab", + "resource":{ + "resourceType":"DiagnosticReport", + "id":"67890123-4567-89ab-cdef-0123456789ab", + "meta":{ + "profile":[ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-note" + ] + }, + "status":"final", + "category":[ + { + "coding":[ + { + "system":"http://loinc.org", + "code":"LP29684-5", + "display":"Radiology" + } + ] + } + ], + "code":{ + "coding":[ + { + "system":"http://loinc.org", + "code":"26346-6", + "display":"Breast Mammogram" + } + ] + }, + "subject":{ + "reference":"urn:uuid:ad134528-56a5-35fd-c37f-466ff119c625" + }, + "encounter":{ + "reference":"urn:uuid:78965432-1234-5678-90ab-cdef12345678" + }, + "effectiveDateTime":"2023-05-01T09:00:00-05:00", + "issued":"2023-05-01T10:00:00-05:00", + "performer":[ + { + "reference":"Practitioner/123456", + "display":"Dr. John Doe" + } + ], + "result":[ + { + "reference":"Observation/1" + } + ] + }, + "request":{ + "method":"POST", + "url":"DiagnosticReport" + } + }, + { + "fullUrl":"urn:uuid:01234567-89ab-cdef-0123-456789abcdef", + "resource":{ + "resourceType":"Observation", + "id":"01234567-89ab-cdef-0123-456789abcdef", + "meta":{ + "profile":[ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab" + ] + }, + "status":"final", + "category":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/observation-category", + "code":"laboratory", + "display":"Laboratory" + } + ] + } + ], + "code":{ + "coding":[ + { + "system":"http://loinc.org", + "code":"33747-0", + "display":"Histopathology (tissue) study" + } + ] + }, + "subject":{ + "reference":"urn:uuid:ad134528-56a5-35fd-c37f-466ff119c625" + }, + "encounter":{ + "reference":"urn:uuid:78965432-1234-5678-90ab-cdef12345678" + }, + "effectiveDateTime":"2023-05-02T08:00:00-05:00", + "issued":"2023-05-02T10:00:00-05:00", + "performer":[ + { + "reference":"Practitioner/789012", + "display":"Dr. Alice Brown" + } + ], + "valueString":"Invasive ductal carcinoma, Grade 2" + }, + "request":{ + "method":"POST", + "url":"Observation" + } + } + ] +} diff --git a/OwnYourData/Resources/Localizable.xcstrings b/OwnYourData/Resources/Localizable.xcstrings index 4b08bce..2014bd2 100644 --- a/OwnYourData/Resources/Localizable.xcstrings +++ b/OwnYourData/Resources/Localizable.xcstrings @@ -119,22 +119,66 @@ }, "LLM_GET_FHIR_RESOURCES_FUNCTION_DESCRIPTION" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retrieve specific FHIR (Fast Healthcare Interoperability Resources) from a healthcare data store based on the given identifiers. This function filters and summarizes relevant health records, providing detailed summaries of the most pertinent information. If no matching resources are found, it will notify you accordingly. Use this function to access the latest and most relevant health records efficiently." + } + } + } }, "LLM_GET_FHIR_RESOURCES_PARAMETER_DESCRIPTION" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Specify a list of resource identifiers to retrieve FHIR resources from the data store. These identifiers should correspond to the health records you need. The function will filter and fetch only the relevant resources based on these identifiers, allowing for efficient and targeted data retrieval. Use this parameter to guide the function in fetching the specific health information required for your task." + } + } + } }, "LLM_GET_TRIALS_FUNCTION_DESCRIPTION" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retrieve specific clinical trials from the NCI (National Cancer Institute) Trials API using the provided trial identifiers. This function filters the trials based on the given identifiers and returns detailed information about each trial, including titles, descriptions, and inclusion criteria. Use this function to access and summarize relevant clinical trial data efficiently." + } + } + } }, "LLM_GET_TRIALS_PARAMETER_DESCRIPTION" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Specify a list of trial identifiers to retrieve detailed information about clinical trials from the NCI Trials API. These identifiers should correspond to the trials you need. The function will filter and fetch the relevant trials based on these identifiers, providing comprehensive details for each trial. Use this parameter to guide the function in fetching specific clinical trial information required for your task." + } + } + } }, "LLM_KEYWORD_IDENTIFICATION_PROMPT" : { - "comment" : "Content of the keyword identification prompt." + "comment" : "Content of the keyword identification prompt.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your task is to identify a minimal set of distinct and unique keywords to conduct a trial search for a patient diagnosed with cancer. 
\nUtilize the \"get_resources\" function to access the patient's FHIR resources. \nThe generic patient information will be passed into this context after this prompt.\nUtilize the function call as often as needed until you have a comprehensive picture of the patient's health status and you feel confident that you can respond with a few distinct keywords for the NIC trials API search.
\nAvoid any generic terms like \"cancer\" and other elements that might appear in all trial descriptions.
Only try to provide 5 or less keywords.\nTry to be as concrete and as narrow as possible based on the relevant FHIR resources.\n
Do not engage in any conversation; only respond with a list of keywords separated by commas without any other context, introduction, or surrounding information. \nThe resulting strings will be parsed for further processing." + } + } + } }, "LLM_TRIAL_MATCHING_PROMPT" : { - "comment" : "Content of the trial matching prompt." + "comment" : "Content of the trial matching prompt.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your task is to identify a set of matching trials for the patient diagnosed with cancer using the NCI trials API. 

You will be provided with a set of keywords identified in a previous run of an LLM based on the patient's FHIR health records.
\nYou can request any health records using the get_records function calling mechanisms. Utilize the function call as often as needed until you have a comprehensive picture of the patient's health status.

Utilize the get_trials function to retrieve information about the possible trials retrieved from the NCI API using the keywords identified in a previous run.
Ensure that the trial description and inclusion criteria match the patient's health records.
\nRespond with all trial identifiers that seem a good match.
The trial identifies must be separated by commas.\nIt is encouraged to return 3-5 possible matching trials to provide the patient some choice but ensure that the trials are matching the patient profile.
Only use the trial identifiers that are parameter options for the get_trials function; do not make up or combine trial identifiers.
\nDo not engage in any conversation; only respond with a list of identifiers separated by commas without any other context, introduction, or surrounding information. \nThe resulting identifiers will be parsed for further processing." + } + } + } }, "Loading NCI Trials" : { diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift index 2f1172b..4b9becd 100644 --- a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift @@ -29,6 +29,8 @@ struct GetTrialsLLMFunction: LLMFunction { ) { self.nciTrialsModel = nciTrialsModel + print(nciTrialsModel.trials.compactMap({ $0.llmIdentifier })) + _trailIdentifiers = Parameter( description: String(localized: "LLM_GET_TRIALS_PARAMETER_DESCRIPTION"), enum: nciTrialsModel.trials.compactMap({ $0.llmIdentifier }) diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift index d1a1b70..a41bf1c 100644 --- a/OwnYourData/TrialsMatching/MatchingModule.swift +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -66,11 +66,21 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { self.state = .nciLoading } try await nciTrialsModel.fetchTrials(keywords: keywords) + if nciTrialsModel.trials.isEmpty { + try await nciTrialsModel.fetchTrials() + } withAnimation { self.state = .matching } let matchingTrialIds = try await trialsIdentificaiton() - matchingTrials = nciTrialsModel.trials.filter({ trial in matchingTrialIds.contains(where: { $0 == trial.nciId }) }) + + print(matchingTrialIds) + print(nciTrialsModel.trials.map({ $0.llmIdentifier })) + + matchingTrials = nciTrialsModel.trials.filter({ trial in matchingTrialIds.contains(where: { $0 == trial.llmIdentifier }) }) + + print(matchingTrials) + withAnimation { self.state = .idle } @@ -108,16 +118,12 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { output.append(token) } - keywords = output.components(separatedBy: ",") + keywords = output.components(separatedBy: ",").flatMap({ $0.components(separatedBy: " ") }).filter({ !$0.isEmpty }) return keywords } @MainActor private func trialsIdentificaiton() async throws -> [String] { - if nciTrialsModel.trials.isEmpty { - try await nciTrialsModel.fetchTrials(keywords: keywords) - } - let llm = llmRunner( with: LLMOpenAISchema(parameters: .init(modelType: .gpt4_turbo_preview)) { GetFHIRResourceLLMFunction( diff --git a/OwnYourData/TrialsMatching/MatchingStateView.swift b/OwnYourData/TrialsMatching/MatchingStateView.swift index 006898c..b105883 100644 --- a/OwnYourData/TrialsMatching/MatchingStateView.swift +++ b/OwnYourData/TrialsMatching/MatchingStateView.swift @@ -14,7 +14,7 @@ private struct MatchingstateLogo: View { let text: LocalizedStringResource var body: some View { - VStack { + VStack(spacing: 32) { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) @@ -23,6 +23,7 @@ private struct MatchingstateLogo: View { .padding() .accessibilityHidden(true) Text(text) + .multilineTextAlignment(.center) ProgressView() .padding() } @@ -35,6 +36,12 @@ struct MatchingStateView: View { var body: some View { + content + .padding(32) + } + + + @ViewBuilder private var content: some View { switch matchingModule.state { case .idle: VStack { diff --git a/OwnYourData/TrialsMatching/MatchingView.swift b/OwnYourData/TrialsMatching/MatchingView.swift index 90b8078..ec57697 100644 --- a/OwnYourData/TrialsMatching/MatchingView.swift +++ b/OwnYourData/TrialsMatching/MatchingView.swift @@ -17,7 +17,7 @@ struct MatchingView: View { var body: some View { NavigationStack { Group { - if matchingModule.matchingTrials.isEmpty || matchingModule.state.representation == .processing { + if matchingModule.matchingTrials.isEmpty { MatchingStateView() } else { List { @@ -45,6 +45,9 @@ struct MatchingView: View { } } .viewStateAlert(state: matchingModule.state) + .task { + await matchingModule.matchTrials() + } } } } From 37de0305ac56c5d09c918c8821b9bf49b2041c56 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 03:07:36 -0700 Subject: [PATCH 11/13] Update Setup --- OwnYourData/ClinicalTrials/NCITrialsModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OwnYourData/ClinicalTrials/NCITrialsModel.swift b/OwnYourData/ClinicalTrials/NCITrialsModel.swift index da04a9d..ba0f3c3 100644 --- a/OwnYourData/ClinicalTrials/NCITrialsModel.swift +++ b/OwnYourData/ClinicalTrials/NCITrialsModel.swift @@ -13,7 +13,8 @@ import SpeziLocation @Observable class NCITrialsModel { - private static let apiKey: String = "tkMGxBkgOC4TDCUfjcPdw7eeZsuuZual632WpUnH" + #warning("Insert NIC Token here to test the app.") + private static let apiKey: String = "" private let locationModule: SpeziLocation From 91c503a614a63c342d980e2c35c8ae1f1d5b77b3 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 03:11:06 -0700 Subject: [PATCH 12/13] SwiftLint & REUSE --- OwnYourData/ClinicalTrials/NCITrialsModel.swift | 2 +- .../ExamplePatients/BreastCancerExample.json.license | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 OwnYourData/Resources/ExamplePatients/BreastCancerExample.json.license diff --git a/OwnYourData/ClinicalTrials/NCITrialsModel.swift b/OwnYourData/ClinicalTrials/NCITrialsModel.swift index ba0f3c3..31b6aaf 100644 --- a/OwnYourData/ClinicalTrials/NCITrialsModel.swift +++ b/OwnYourData/ClinicalTrials/NCITrialsModel.swift @@ -14,7 +14,7 @@ import SpeziLocation @Observable class NCITrialsModel { #warning("Insert NIC Token here to test the app.") - private static let apiKey: String = "" + private static let apiKey: String = "tkMGxBkgOC4TDCUfjcPdw7eeZsuuZual632WpUnH" private let locationModule: SpeziLocation diff --git a/OwnYourData/Resources/ExamplePatients/BreastCancerExample.json.license b/OwnYourData/Resources/ExamplePatients/BreastCancerExample.json.license new file mode 100644 index 0000000..03ca624 --- /dev/null +++ b/OwnYourData/Resources/ExamplePatients/BreastCancerExample.json.license @@ -0,0 +1,6 @@ + +This source file is part of the OwnYourData based on the Stanford Spezi Template Application project + +SPDX-FileCopyrightText: 2023 Stanford University + +SPDX-License-Identifier: MIT From 28a2299365b3be8ad31829268f2e80f3111cf9f0 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 15 May 2024 03:13:05 -0700 Subject: [PATCH 13/13] Fix Swiftlint --- OwnYourData/ClinicalTrials/NCITrialsModel.swift | 4 ++-- .../LLMFunctions/GetTrialsLLMFunction.swift | 8 +++----- OwnYourData/TrialsMatching/MatchingModule.swift | 9 ++------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/OwnYourData/ClinicalTrials/NCITrialsModel.swift b/OwnYourData/ClinicalTrials/NCITrialsModel.swift index 31b6aaf..ba1cce8 100644 --- a/OwnYourData/ClinicalTrials/NCITrialsModel.swift +++ b/OwnYourData/ClinicalTrials/NCITrialsModel.swift @@ -14,7 +14,7 @@ import SpeziLocation @Observable class NCITrialsModel { #warning("Insert NIC Token here to test the app.") - private static let apiKey: String = "tkMGxBkgOC4TDCUfjcPdw7eeZsuuZual632WpUnH" + private static let apiKey: String = "" private let locationModule: SpeziLocation @@ -72,7 +72,7 @@ class NCITrialsModel { OpenAPIClientAPI.customHeaders = ["X-API-KEY": Self.apiKey] CodableHelper.dateFormatter = NICTrialsAPIDateFormatter() - let keywords = keywords.filter({ !$0.isEmpty }) + let keywords = keywords.filter { !$0.isEmpty } return try await withCheckedThrowingContinuation { continuation in TrialsAPI.searchTrialsByGet( diff --git a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift index 4b9becd..0bea568 100644 --- a/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift +++ b/OwnYourData/TrialsMatching/LLMFunctions/GetTrialsLLMFunction.swift @@ -28,12 +28,10 @@ struct GetTrialsLLMFunction: LLMFunction { nciTrialsModel: NCITrialsModel ) { self.nciTrialsModel = nciTrialsModel - - print(nciTrialsModel.trials.compactMap({ $0.llmIdentifier })) - + _trailIdentifiers = Parameter( description: String(localized: "LLM_GET_TRIALS_PARAMETER_DESCRIPTION"), - enum: nciTrialsModel.trials.compactMap({ $0.llmIdentifier }) + enum: nciTrialsModel.trials.compactMap { $0.llmIdentifier } ) } @@ -48,7 +46,7 @@ struct GetTrialsLLMFunction: LLMFunction { Title: \(trial.briefTitle ?? "") (\(trial.officialTitle ?? "")) Description: \(trial.detailDescription ?? "") - Incluision Criteria: \(trial.eligibility?.unstructured?.compactMap({ $0.description }).joined() ?? "") + Incluision Criteria: \(trial.eligibility?.unstructured?.compactMap { $0.description }.joined() ?? "") """ } } diff --git a/OwnYourData/TrialsMatching/MatchingModule.swift b/OwnYourData/TrialsMatching/MatchingModule.swift index a41bf1c..b6234db 100644 --- a/OwnYourData/TrialsMatching/MatchingModule.swift +++ b/OwnYourData/TrialsMatching/MatchingModule.swift @@ -74,12 +74,7 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { } let matchingTrialIds = try await trialsIdentificaiton() - print(matchingTrialIds) - print(nciTrialsModel.trials.map({ $0.llmIdentifier })) - - matchingTrials = nciTrialsModel.trials.filter({ trial in matchingTrialIds.contains(where: { $0 == trial.llmIdentifier }) }) - - print(matchingTrials) + matchingTrials = nciTrialsModel.trials.filter { trial in matchingTrialIds.contains(where: { $0 == trial.llmIdentifier }) } withAnimation { self.state = .idle @@ -118,7 +113,7 @@ class MatchingModule: Module, EnvironmentAccessible, DefaultInitializable { output.append(token) } - keywords = output.components(separatedBy: ",").flatMap({ $0.components(separatedBy: " ") }).filter({ !$0.isEmpty }) + keywords = output.components(separatedBy: ",").flatMap { $0.components(separatedBy: " ") }.filter { !$0.isEmpty } return keywords }