From 105eaf12ff16f391f4d0363243deba45a538887c Mon Sep 17 00:00:00 2001 From: apgupta3303 <98926720+apgupta3303@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:41:39 -0400 Subject: [PATCH] Llm filter data (#67) # LLM Filter Data* ## :recycle: Current situation & Problem *Link any open issues or pull requests (PRs) related to this PR. Please ensure that all non-trivial PRs are first tracked and discussed in an existing GitHub issue or discussion.* ## :gear: Release Notes *This PR creates a class that filters the data by removing things within "()". It uses and LLM and starts to cut down on filtering by content, but the system prompts need to be more tailored towards filtering the data better. It applies this class to conditions and allergies ## :books: Documentation *Please ensure that you properly document any additions in conformance to [Spezi Documentation Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).* *You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.* ## :white_check_mark: Testing *Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.* *This section describes important information about the tests and why some elements might not be testable.* ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md). --------- Co-authored-by: nriedman <108841122+nriedman@users.noreply.github.com> --- Intake.xcodeproj/project.pbxproj | 10 +- .../xcshareddata/swiftpm/Package.resolved | 7 +- Intake/Allergy Records/AddAllergy.swift | 38 ----- Intake/Allergy Records/AllergyRecords.swift | 86 +++++++++-- Intake/Allergy Records/ReactionPDF.swift | 26 +--- .../Allergy Records/ReactionSectionView.swift | 56 +++++++ Intake/Intake.swift | 8 + Intake/LLMFiltering.swift | 137 ++++++++++++++++++ .../Medical History/MedicalHistoryView.swift | 66 +++++++-- Intake/Resources/Localizable.xcstrings | 3 + 10 files changed, 354 insertions(+), 83 deletions(-) create mode 100644 Intake/Allergy Records/ReactionSectionView.swift create mode 100644 Intake/LLMFiltering.swift diff --git a/Intake.xcodeproj/project.pbxproj b/Intake.xcodeproj/project.pbxproj index a4453e4..9dc56b1 100644 --- a/Intake.xcodeproj/project.pbxproj +++ b/Intake.xcodeproj/project.pbxproj @@ -95,6 +95,8 @@ 5A2B9FAB2B69E430005CA63F /* FHIRStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2B9FAA2B69E430005CA63F /* FHIRStore+Extensions.swift */; }; 5A2B9FB62B6AFE5D005CA63F /* AllergyRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2B9FB52B6AFE5D005CA63F /* AllergyRecords.swift */; }; 5A2B9FBC2B6C7B29005CA63F /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2B9FBB2B6C7B29005CA63F /* ReactionView.swift */; }; + 5AAB83A72B9C04E70008407A /* LLMFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAB83A62B9C04E70008407A /* LLMFiltering.swift */; }; + 5AAB83B32B9EBB070008407A /* ReactionSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAB83B22B9EBB070008407A /* ReactionSectionView.swift */; }; 5AEA5F212B82DDD000F1577A /* LLMAssistantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA5F202B82DDD000F1577A /* LLMAssistantView.swift */; }; 5AEA5F2C2B8680F300F1577A /* AddAllergy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA5F2B2B8680F300F1577A /* AddAllergy.swift */; }; 5AEA5F3B2B90081B00F1577A /* ScrollablePDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA5F3A2B90081B00F1577A /* ScrollablePDF.swift */; }; @@ -112,8 +114,8 @@ A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; }; A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; ACAA47812B571C800032D21F /* Questionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = ACAA47802B571C7F0032D21F /* Questionnaire.json */; }; - ACF862BE2B96E29600ACBA1E /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF862BD2B96E29600ACBA1E /* ExportView.swift */; }; ACDF32ED2B9D0F4300B127E2 /* MenstrualHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFFA1D02B8FD8BB0006E6D4 /* MenstrualHistory.swift */; }; + ACF862BE2B96E29600ACBA1E /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF862BD2B96E29600ACBA1E /* ExportView.swift */; }; ACFFA1CE2B8FD7190006E6D4 /* SmokingHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFFA1CD2B8FD7190006E6D4 /* SmokingHistory.swift */; }; F42AB1D22B6379B5002E13A6 /* SpeziLLM in Frameworks */ = {isa = PBXBuildFile; productRef = F42AB1D12B6379B5002E13A6 /* SpeziLLM */; }; F42AB1D42B6379B5002E13A6 /* SpeziLLMLocal in Frameworks */ = {isa = PBXBuildFile; productRef = F42AB1D32B6379B5002E13A6 /* SpeziLLMLocal */; }; @@ -211,6 +213,8 @@ 5A2B9FAA2B69E430005CA63F /* FHIRStore+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FHIRStore+Extensions.swift"; sourceTree = ""; }; 5A2B9FB52B6AFE5D005CA63F /* AllergyRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllergyRecords.swift; sourceTree = ""; }; 5A2B9FBB2B6C7B29005CA63F /* ReactionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; + 5AAB83A62B9C04E70008407A /* LLMFiltering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMFiltering.swift; sourceTree = ""; }; + 5AAB83B22B9EBB070008407A /* ReactionSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSectionView.swift; sourceTree = ""; }; 5AEA5F202B82DDD000F1577A /* LLMAssistantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMAssistantView.swift; sourceTree = ""; }; 5AEA5F2B2B8680F300F1577A /* AddAllergy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAllergy.swift; sourceTree = ""; }; 5AEA5F3A2B90081B00F1577A /* ScrollablePDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollablePDF.swift; sourceTree = ""; }; @@ -479,6 +483,7 @@ 5AEA5F2B2B8680F300F1577A /* AddAllergy.swift */, 5AEA5F492B96F63A00F1577A /* AllergyLLMAssistant.swift */, 5A2B9FBB2B6C7B29005CA63F /* ReactionView.swift */, + 5AAB83B22B9EBB070008407A /* ReactionSectionView.swift */, 5AEA5F462B93034A00F1577A /* ReactionPDF.swift */, 51A360152B965819004E7E12 /* AllergyLLMAssistant.swift */, ); @@ -537,6 +542,7 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */, 2FC9759D2978E30800BA99FE /* Supporting Files */, 5AEA5F3A2B90081B00F1577A /* ScrollablePDF.swift */, + 5AAB83A62B9C04E70008407A /* LLMFiltering.swift */, 5AEA5F412B90710B00F1577A /* EditPatient.swift */, ); path = Intake; @@ -889,8 +895,10 @@ 566155292AB8447C00209B80 /* Package+LicenseType.swift in Sources */, 5680DD392AB8983D004E6D4A /* PackageCell.swift in Sources */, 5AEA5F422B90710B00F1577A /* EditPatient.swift in Sources */, + 5AAB83A72B9C04E70008407A /* LLMFiltering.swift in Sources */, F42AB1EC2B6DBF21002E13A6 /* SummaryView.swift in Sources */, 2F5E32BD297E05EA003432F8 /* IntakeDelegate.swift in Sources */, + 5AAB83B32B9EBB070008407A /* ReactionSectionView.swift in Sources */, 511827962B740192002033A0 /* SurgeryView.swift in Sources */, 2FE5DC5229EDD7FA004B9AB4 /* IntakeScheduler.swift in Sources */, 5AEA5F3B2B90081B00F1577A /* ScrollablePDF.swift in Sources */, diff --git a/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9f82483..9fb77a5 100644 --- a/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "cdbe60a3382a8a962c7fcc00210e56e13314cb3e246d0e01fe8e25a1268623d8", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -231,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziHealthKit.git", "state" : { - "revision" : "35628084d3977aa897015b0b0c21cfe4d556f1aa", - "version" : "0.5.2" + "revision" : "1e9cb5a6036ac7f4ff37ea1c3ed4898103339ad1", + "version" : "0.5.3" } }, { @@ -380,5 +379,5 @@ } } ], - "version" : 3 + "version" : 2 } diff --git a/Intake/Allergy Records/AddAllergy.swift b/Intake/Allergy Records/AddAllergy.swift index 11117c5..ba6efdb 100644 --- a/Intake/Allergy Records/AddAllergy.swift +++ b/Intake/Allergy Records/AddAllergy.swift @@ -54,44 +54,6 @@ struct EditAllergyView: View { } } -struct ReactionSectionView: View { - @Environment(DataStore.self) private var data - var index: Int - - var body: some View { - Form { // Use Form instead of List - Section(header: headerTitle) { - @Bindable var data = data - ForEach($data.allergyData[index].reaction) { $item in - HStack { - TextField("Reactions", text: $item.reaction) - } - } - .onDelete(perform: delete) - Button(action: { - data.allergyData[index].reaction.append(ReactionItem(reaction: "")) - }) { - HStack { - Image(systemName: "plus.circle.fill") - .accessibilityLabel(Text("ADD_REACTION")) - Text("Add Field") - } - } - } - } - } - - private var headerTitle: some View { - HStack { - Text("Reactions") - Spacer() - EditButton() - } - } - func delete(at offsets: IndexSet) { - data.allergyData[index].reaction.remove(atOffsets: offsets) - } -} // #Preview { // EditAllergyView(allergyItem: AllergyItem(allergy: "", reaction: []), showingReaction: <#T##Binding#>, allergyRecords: <#T##Binding<[AllergyItem]>#>, showingReaction: .constant(true), allergyRecords: .constant([])) diff --git a/Intake/Allergy Records/AllergyRecords.swift b/Intake/Allergy Records/AllergyRecords.swift index 2526036..e6c14b1 100644 --- a/Intake/Allergy Records/AllergyRecords.swift +++ b/Intake/Allergy Records/AllergyRecords.swift @@ -13,6 +13,8 @@ import Foundation import ModelsR4 import SpeziFHIR +import SpeziLLM +import SpeziLLMOpenAI import SwiftUI struct AllergyItem: Identifiable, Equatable { @@ -25,10 +27,6 @@ struct AllergyItem: Identifiable, Equatable { } } -// struct ReactionViewDetails { -// var showingReaction: Bool -// var -// } struct ChatButton: View { // Use @Binding to create a two-way binding to the parent view's showingChat state @@ -54,20 +52,35 @@ struct AllergyList: View { @Environment(FHIRStore.self) private var fhirStore @Environment(NavigationPathWrapper.self) private var navigationPath @Environment(DataStore.self) private var data + @Environment(LoadedWrapper.self) private var loaded + @State private var showingReaction = false @State private var selectedIndex = 0 @State private var showingChat = false @State private var presentingAccount = false + + @LLMSessionProvider var session: LLMOpenAISession var body: some View { - VStack { - allergyForm - SubmitButton(nextView: NavigationViews.menstrual) - .padding() + if loaded.allergyData { + VStack { + allergyForm + SubmitButton(nextView: NavigationViews.menstrual) + .padding() + } + .sheet(isPresented: $showingChat, content: chatSheetView) + .sheet(isPresented: $showingReaction, content: editAllergySheetView) + } else { + ProgressView() + .task { + do { + try await loadAllergies() + } catch { + print("Failed to load") + } + loaded.allergyData = true + } } - .onAppear(perform: loadAllergies) - .sheet(isPresented: $showingChat, content: chatSheetView) - .sheet(isPresented: $showingReaction, content: editAllergySheetView) } private var allergyForm: some View { Form { @@ -102,7 +115,37 @@ struct AllergyList: View { } } } + + init() { + let systemPrompt = """ + You are a helpful assistant that filters lists of allergies. You will be given\ + an array of strings. Each string will be the name of a allergy. + + For example, if you are given the following list: + Mammography (procedure), Certification procedure (procedure), Cytopathology\ + procedure, preparation of smear, genital source (procedure), Transplant of kidney\ + (procedure), + + you should return something like this: + Transplant of kidney, Mammography. + In your response, return only the name of the allergy. Remove words in parenthesis + like (disorder), so "Aortic valve stenosis (disorder)" would turn to "Aortic valve stenosis". + + Do not make anything up, and do not change the name of the condition under any + circumstances. Thank you! + """ + + self._session = LLMSessionProvider( + schema: LLMOpenAISchema( + parameters: .init( + modelType: .gpt3_5Turbo, + systemPrompt: systemPrompt + ) + ) + ) + } + private func allergyEntryRow(index: Int) -> some View { HStack { Text(data.allergyData[index].allergy) @@ -145,8 +188,20 @@ struct AllergyList: View { private func editAllergySheetView() -> some View { EditAllergyView(index: selectedIndex, showingReaction: $showingReaction) } + + private func removeTextWithinParentheses(from string: String) -> String { + let pattern = "\\s*\\([^)]+\\)" + do { + let regex = try NSRegularExpression(pattern: pattern) + let range = NSRange(string.startIndex..., in: string) + return regex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "") + } catch { + print("Invalid regex: \(error.localizedDescription)") + return string + } + } - private func loadAllergies() { + private func loadAllergies() async throws { var allergies: [FHIRString] = [] var allReactions: [[ReactionItem]] = [] let intolerances = fhirStore.allergyIntolerances @@ -162,7 +217,9 @@ struct AllergyList: View { for reaction in reactions { let manifestations = reaction.manifestation for manifestation in manifestations { - reactionsForAllergy.append(ReactionItem(reaction: manifestation.text?.value?.string ?? "Default")) + var reactionName = manifestation.text?.value?.string + reactionName = removeTextWithinParentheses(from: reactionName ?? "") + reactionsForAllergy.append(ReactionItem(reaction: reactionName ?? "")) } } } @@ -180,6 +237,9 @@ struct AllergyList: View { ) } } + + let filter = LLMFiltering(session: session, data: data) + try await filter.filterAllergies() } func delete(at offsets: IndexSet) { diff --git a/Intake/Allergy Records/ReactionPDF.swift b/Intake/Allergy Records/ReactionPDF.swift index 2862afe..4a48329 100644 --- a/Intake/Allergy Records/ReactionPDF.swift +++ b/Intake/Allergy Records/ReactionPDF.swift @@ -22,26 +22,16 @@ struct ReactionPDF: View { @Binding private var showingReaction: Bool var body: some View { NavigationView { - ReactionSectionView(index: index) -// VStack { -// Form { -// ForEach(data.allergyData[index].reaction) { item in -// Text(item.reaction) -// } -// } -// .navigationTitle("Medical History") -//// .navigationTitle("\(data.allergyData[index].allergy) Reactions") -// .navigationBarItems(trailing: EditButton()) -// } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - HStack { - Text("\(data.allergyData[index].allergy) Reactions") - .lineLimit(1) // Ensure the title is single-lined - .truncationMode(.tail) - .font(.headline) // Adjust the font size if needed + VStack { + Form { + if data.allergyData[index].reaction.isEmpty { + Text("No Reactions") + } + ForEach(data.allergyData[index].reaction) { item in + Text(item.reaction) } } + .navigationTitle("\(data.allergyData[index].allergy) Reactions") } } } diff --git a/Intake/Allergy Records/ReactionSectionView.swift b/Intake/Allergy Records/ReactionSectionView.swift new file mode 100644 index 0000000..7ad3d80 --- /dev/null +++ b/Intake/Allergy Records/ReactionSectionView.swift @@ -0,0 +1,56 @@ +// +// ReactionSectionView.swift +// Intake +// +// Created by Akash Gupta on 3/10/24. +// +// This source file is part of the Intake based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziFHIR +import SwiftUI + + +struct ReactionSectionView: View { + @Environment(DataStore.self) private var data + var index: Int + + var body: some View { + Form { // Use Form instead of List + Section(header: headerTitle) { + @Bindable var data = data + ForEach($data.allergyData[index].reaction) { $item in + HStack { + TextField("Reactions", text: $item.reaction) + } + } + .onDelete(perform: delete) + Button(action: { + data.allergyData[index].reaction.append(ReactionItem(reaction: "")) + }) { + HStack { + Image(systemName: "plus.circle.fill") + .accessibilityLabel(Text("ADD_REACTION")) + Text("Add Field") + } + } + } + } + } + + private var headerTitle: some View { + HStack { + Text("Reactions") + Spacer() + EditButton() + } + } + func delete(at offsets: IndexSet) { + data.allergyData[index].reaction.remove(atOffsets: offsets) + } +} diff --git a/Intake/Intake.swift b/Intake/Intake.swift index 9065352..3e7f87a 100644 --- a/Intake/Intake.swift +++ b/Intake/Intake.swift @@ -52,6 +52,12 @@ class ReachedEndWrapper { var surgeriesLoaded = false } +@Observable +class LoadedWrapper { + var conditionData = false + var allergyData = false +} + @main struct Intake: App { @UIApplicationDelegateAdaptor(IntakeDelegate.self) var appDelegate @@ -60,6 +66,7 @@ struct Intake: App { let navigationPath = NavigationPathWrapper() let data = DataStore() let reachedEnd = ReachedEndWrapper() + let loaded = LoadedWrapper() var body: some Scene { WindowGroup { @@ -78,6 +85,7 @@ struct Intake: App { .environment(navigationPath) .environment(data) .environment(reachedEnd) + .environment(loaded) } } } diff --git a/Intake/LLMFiltering.swift b/Intake/LLMFiltering.swift new file mode 100644 index 0000000..95191a8 --- /dev/null +++ b/Intake/LLMFiltering.swift @@ -0,0 +1,137 @@ +// +// LLMFiltering.swift +// Intake +// +// Created by Akash Gupta on 3/8/24. +// +// +// This source file is part of the Intake based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziLLM +import SpeziLLMOpenAI +import SwiftUI + + +class LLMFiltering { + private var LLMFiltering = true + private var session: LLMOpenAISession + private var data: DataStore + + init(session: LLMOpenAISession, data: DataStore) { + self.session = session + self.data = data + } + + func filter(surgeries: [String]) async -> [String] { + let stopWords = [ + "screen", + "medication", + "examination", + "assess", + "development", + "notification", + "clarification", + "discussion ", + "option", + "review", + "evaluation", + "management", + "consultation", + "referral", + "interpretation", + "discharge", + "certification", + "preparation" + ] + + let manualFilter = surgeries.filter { !self.containsAnyWords(item: $0.lowercased(), words: stopWords) } + + if !self.LLMFiltering { + return manualFilter + } + + do { + return try await self.LLMFilter(names: manualFilter) + } catch { + print("Error filtering with LLM: \(error)") + print("Returning manually filtered surgeries") + return manualFilter + } + } + + func containsAnyWords(item: String, words: [String]) -> Bool { + words.contains { item.contains($0) } + } + + func LLMFilter(names: [String]) async throws -> [String] { + let LLMResponse = try await self.queryLLM(names: names) + let filteredNames = LLMResponse.components(separatedBy: ", ") + + return filteredNames + } + + func queryLLM(names: [String]) async throws -> String { + var responseText = "" + + await MainActor.run { + session.context.append(userInput: names.joined(separator: ", ")) + } + for try await token in try await session.generate() { + responseText.append(token) + } + + return responseText + } + + + func filterSurgeries() async throws -> [SurgeryItem] { + @Environment(DataStore.self) var data + let filteredNames = try await self.LLMFilter(names: []) + let filteredSurgeries = data.surgeries.filter { self.containsAnyWords(item: $0.surgeryName, words: filteredNames) } + var cleaned = filteredSurgeries + for index in cleaned.indices { + let oldName = cleaned[index].surgeryName + if let newName: String = filteredNames.first(where: { oldName.contains($0) }) { + cleaned[index].surgeryName = newName + } + } + return cleaned + } + + func filterConditions() async throws { + let conditions = data.conditionData.map { $0.condition } + let filteredNames = try await self.LLMFilter(names: conditions) + let filteredConditions = data.conditionData.filter { self.containsAnyWords(item: $0.condition, words: filteredNames) } + var cleaned = filteredConditions + + for index in cleaned.indices { + print(index) + let oldName = cleaned[index].condition + if let newName: String = filteredNames.first(where: { oldName.contains($0) }) { + cleaned[index].condition = newName + } + } + data.conditionData = cleaned + } + + func filterAllergies() async throws { + let allergies = data.allergyData.map { $0.allergy } + let filteredNames = try await self.LLMFilter(names: allergies) + let filteredAllergies = data.allergyData.filter { self.containsAnyWords(item: $0.allergy, words: filteredNames) } + var cleaned = filteredAllergies + + for index in cleaned.indices { + let oldName = cleaned[index].allergy + if let newName: String = filteredNames.first(where: { oldName.contains($0) }) { + cleaned[index].allergy = newName + } + } + data.allergyData = cleaned + } +} diff --git a/Intake/Medical History/MedicalHistoryView.swift b/Intake/Medical History/MedicalHistoryView.swift index 221ff72..e1a0bb6 100644 --- a/Intake/Medical History/MedicalHistoryView.swift +++ b/Intake/Medical History/MedicalHistoryView.swift @@ -13,6 +13,8 @@ import Foundation import ModelsR4 import SpeziFHIR +import SpeziLLM +import SpeziLLMOpenAI import SwiftUI struct MedicalHistoryItem: Identifiable, Equatable { @@ -25,19 +27,34 @@ struct MedicalHistoryView: View { @Environment(FHIRStore.self) private var fhirStore @Environment(NavigationPathWrapper.self) private var navigationPath @Environment(DataStore.self) private var data + @Environment(LoadedWrapper.self) private var loaded + @State private var showAddSheet = false @State private var showingChat = false + + @LLMSessionProvider var session: LLMOpenAISession var body: some View { - VStack { - medicalHistoryForm - SubmitButton(nextView: NavigationViews.surgical) - .padding() + if loaded.conditionData { + VStack { + medicalHistoryForm + SubmitButton(nextView: NavigationViews.surgical) + .padding() + } + .sheet(isPresented: $showingChat, content: chatSheetView) + } else { + ProgressView() + .task { + do { + try await loadConditions() + } catch { + print("Failed to load") + } + loaded.conditionData = true + } } - .onAppear(perform: loadConditions) - .sheet(isPresented: $showingChat, content: chatSheetView) } - + private var medicalHistoryForm: some View { Form { Section(header: Text("Please list conditions you have had")) { @@ -91,6 +108,36 @@ struct MedicalHistoryView: View { .foregroundColor(.gray) } + init() { + let systemPrompt = """ + You are a helpful assistant that filters lists of conditions. You will be given\ + an array of strings. Each string will be the name of a condition. + + For example, if you are given the following list: + Mammography (procedure), Certification procedure (procedure), Cytopathology\ + procedure, preparation of smear, genital source (procedure), Transplant of kidney\ + (procedure), + + you should return something like this: + Transplant of kidney, Mammography. + + In your response, return only the name of the condition. Remove words in parenthesis + like (disorder), so "Aortic valve stenosis (disorder)" would turn to "Aortic valve stenosis". + + Do not make anything up, and do not change the name of the condition under any + circumstances. Thank you! + """ + + self._session = LLMSessionProvider( + schema: LLMOpenAISchema( + parameters: .init( + modelType: .gpt3_5Turbo, + systemPrompt: systemPrompt + ) + ) + ) + } + private func addConditionAction() { data.conditionData.append(MedicalHistoryItem(condition: "", active: false)) } @@ -123,7 +170,7 @@ struct MedicalHistoryView: View { ) } - private func loadConditions() { + private func loadConditions() async throws { let conditions = fhirStore.conditions var active = "" let invalid = [ @@ -150,7 +197,8 @@ struct MedicalHistoryView: View { } } } - print(data.conditionData) + let filter = LLMFiltering(session: session, data: data) + try await filter.filterConditions() } func delete(at offsets: IndexSet) { diff --git a/Intake/Resources/Localizable.xcstrings b/Intake/Resources/Localizable.xcstrings index b5821ee..fafec63 100644 --- a/Intake/Resources/Localizable.xcstrings +++ b/Intake/Resources/Localizable.xcstrings @@ -515,6 +515,9 @@ }, "No past surgeries" : { + }, + "No Reactions" : { + }, "NOTIFICATION_PERMISSIONS_BUTTON" : { "extractionState" : "stale",