Skip to content

Commit

Permalink
Llm chat/finalize (#41)
Browse files Browse the repository at this point in the history
# *LLM Cheief Complaint Functionality, Prompt, and Summary View *

## ♻️ Current situation & Problem
Before the pull request, we had a minimal implementation of the LLM
chatbot. Our summary appeared but it looked ugly, and the LLM did not
ask very detailed or specific questions.


## ⚙️ Release Notes 
The LLM makes a summary of the chief complaint with a textbox that the
patient is able to edit. The summary is now much more robust, and the
LLM asks the patient more relevant questions than it did before. It's
not limited anymore to just duration and severity. The textbox is now
editable.


## 📚 Documentation
We commented out the naviagtion stack for now and plan on incorporating
that after merging. We changed the LLM function call to now be more
non-parametric and dynamic. We got rid of the parameters and instructed
it to give us a summary instead. With less structure, we are able to
harness all of the LLM's abilities.

## ✅ Testing
We have tested the LLM on a variety of medical concerns that are not
included in the examples given in the system prompt, and we have gotten
really great results with specific questions to the complaint.


## 📝 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 <[email protected]>
  • Loading branch information
ninaboord and nriedman authored Feb 7, 2024
1 parent 4c61cbe commit 4294074
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 108 deletions.
4 changes: 4 additions & 0 deletions Intake.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
F42AB1DF2B637C9D002E13A6 /* LLMInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */; };
F42AB1E52B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1E42B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift */; };
F42AB1EC2B6DBF21002E13A6 /* SummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1EB2B6DBF20002E13A6 /* SummaryView.swift */; };
F42AB1F22B71B4D2002E13A6 /* AllergyViewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1F12B71B4D0002E13A6 /* AllergyViewTest.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -154,6 +155,7 @@
F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMInteraction.swift; sourceTree = "<group>"; };
F42AB1E42B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMOpenAITokenOnboarding.swift; sourceTree = "<group>"; };
F42AB1EB2B6DBF20002E13A6 /* SummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryView.swift; sourceTree = "<group>"; };
F42AB1F12B71B4D0002E13A6 /* AllergyViewTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllergyViewTest.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -392,6 +394,7 @@
F42AB1DB2B637C8C002E13A6 /* LLMOnboardingView.swift */,
F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */,
F42AB1EB2B6DBF20002E13A6 /* SummaryView.swift */,
F42AB1F12B71B4D0002E13A6 /* AllergyViewTest.swift */,
);
path = ChiefComplaint;
sourceTree = "<group>";
Expand Down Expand Up @@ -619,6 +622,7 @@
2F4FC8D729EE69D300BFFE26 /* MockUpload.swift in Sources */,
2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */,
F42AB1DF2B637C9D002E13A6 /* LLMInteraction.swift in Sources */,
F42AB1F22B71B4D2002E13A6 /* AllergyViewTest.swift in Sources */,
2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */,
2FE5DC3529EDD7CA004B9AB4 /* Consent.swift in Sources */,
2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */,
Expand Down
25 changes: 25 additions & 0 deletions Intake/ChiefComplaint/AllergyViewTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// AllergyView.swift
// Intake
//
// Created by Nick Riedman on 2/5/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 SwiftUI

struct AllergyViewTest: View {
var body: some View {
VStack {
Text("Success!")
.padding()
.multilineTextAlignment(.center)
}
}
}
173 changes: 103 additions & 70 deletions Intake/ChiefComplaint/LLMInteraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,106 +18,139 @@ import SpeziLLMOpenAI
import SwiftUI

struct LLMInteraction: View {
@Observable
class StringBox: Equatable {
var llmResponseSummary: String

init() {
self.llmResponseSummary = ""
}

static func == (lhs: LLMInteraction.StringBox, rhs: LLMInteraction.StringBox) -> Bool {
lhs.llmResponseSummary == rhs.llmResponseSummary
}
}


struct SummarizeFunction: LLMFunction {
static let name: String = "summarize_complaint"
static let description: String = """
When there is enough information to give to the doctor,\
summarize the conversation into a concise Chief Complaint.
"""
@Parameter(description: "The primary medical concern that the patient is experiencing.") var medicalConcern: String

@Parameter(description: "The severity of the primary medical concern.") var severity: String


@Parameter(description: "The duration of the primary medical concern.") var duration: String
@Parameter(description: "A summary of the patient's primary concern.") var patientSummary: String

static let desc: String = """
Extra important information relevant to the primary\
medical concern that the doctor should be aware of.
"""
@Parameter(description: desc) var supplementaryInfo: String
let stringBox: StringBox

@Binding var chiefComplaint: String
init(stringBox: StringBox) {
self.stringBox = stringBox
}

func execute() async throws -> String? {
let summary = """
Here is the summary that will be provided to your doctor:\n
Primary concern: \(medicalConcern)\n
Severity: \(severity)\n
Duration: \(duration)\n
Extra Info: \(supplementaryInfo)\n
"""
chiefComplaint = summary
return summary
let summary = patientSummary
self.stringBox.llmResponseSummary = summary
return nil
}
}

@State private var chiefComplaint: String? = "blah blah blah"
@State private var shouldNavigateToSummaryView = true
@Binding var presentingAccount: Bool
@Environment(LLMRunner.self) var runner: LLMRunner

@State var showOnboarding = true
@State var greeting = true
@State var stringBox: StringBox
@State var showSheet = false

@State var model: LLM = LLMOpenAI(
parameters: .init(
modelType: .gpt3_5Turbo,
systemPrompt: """
You are acting as an intake person at a clinic and need to work with\
the patient to help clarify their chief complaint into a concise,\
specific complaint.
You should always ask about severity and duration if the patient does not include this information.
Additionally, help guide the patient into providing information specific to the condition that the define.\
For example, if the patient is experiencing leg pain, you should prompt them to be more\
specific about laterality and location. You should also ask if the pain is dull or sharp,\
and encourage them to rate their pain on a scale of 1 to 10. For a cough, for example, you\
should inquire whether the cough is wet or dry, as well as any other characteristics of the\
cough that might allow a doctor to rule out diagnoses.
Please use everyday layman terms and avoid using complex medical terminology.\
Only ask one question or prompt at a time, and keep your responses brief (one to two short sentences).
"""
)
) {
// SummarizeFunction()
}
@State var model: LLM

var body: some View {
NavigationStack {
LLMChatView(
model: model
)
.navigationTitle("Chief Complaint")
.toolbar {
if AccountButton.shouldDisplay {
AccountButton(isPresented: $presentingAccount)
}
}
.sheet(isPresented: $showOnboarding) {
LLMOnboardingView(showOnboarding: $showOnboarding)
LLMChatView(
model: model
)
.navigationTitle("Chief Complaint")
.toolbar {
if AccountButton.shouldDisplay {
AccountButton(isPresented: $presentingAccount)
}
.onAppear {
}
.sheet(isPresented: $showOnboarding) {
LLMOnboardingView(showOnboarding: $showOnboarding)
}
.onAppear {
if greeting {
let assistantMessage = ChatEntity(role: .assistant, content: "Hello! What brings you to the doctor's office?")
model.context.insert(assistantMessage, at: 0)
}
.onChange(of: chiefComplaint) { _, newChiefComplaint in
if let newChiefComplaint = newChiefComplaint {
shouldNavigateToSummaryView = true
}
}
NavigationLink(
destination: SummaryView(chiefComplaint: chiefComplaint ?? "No Chief Complaint"),
label: {
EmptyView()
}
)
greeting = false
}
.onChange(of: self.stringBox.llmResponseSummary) { _, _ in
self.showSheet = true
}
.sheet(isPresented: $showSheet) {
SummaryView(chiefComplaint: self.stringBox.llmResponseSummary)
}
}

init(presentingAccount: Binding<Bool>) {
// swiftlint:disable closure_end_indentation
self._presentingAccount = presentingAccount
let stringBoxTemp = StringBox()
self.stringBox = stringBoxTemp
self.model = LLMOpenAI(
parameters: .init(
modelType: .gpt3_5Turbo,
systemPrompt: """
Pretend you are a nurse. Your job is to gather information about the medical concern of a patient.\
Your job is to provide a summary of the patient’s chief medical complaint to the doctor so that the doctor\
has all of the information they need to begin the appointment. Ask questions specific to the concern of the\
patient in order to help clarify their chief complaint into a concise, specific concern. Ask the patient to\
elaborate a little bit if you feel that they are not providing sufficient information. You should always ask about\
severity and onset, and if relevant to the specific condition, you might specific questions about the location,\
laterality, triggers, character, timing, description, progression, and associated symptoms unique to the complaint.\
Ask with empathy.\
Headache:\
Onset: When did the headache start? Location: Where is the pain located? Duration: How long does each headache episode last?\
Severity: On a scale of 1 to 10, how would you rate the pain? Triggers: Are there any specific triggers\
that seem to bring on the headache? Associated Symptoms: Do you experience nausea, vomiting, sensitivity to light or sound?\
Abdominal Pain:\
Location: Where is the pain located abdomen? Character: How would you describe the pain (e.g., sharp, dull, cramping)?\
Severity: On a scale of 1 to 10, how severe is the pain? Timing: Does the pain come and go, or is it constant?\
Associated Symptoms: Any nausea, vomiting, or other changes in your bowl?\
Fever:\
Temperature: What is your current temperature?\
Onset: When did the fever start?\
Duration: How long have you had the fever?\
Associated Symptoms: Any chills, sweating, body aches?\
Recent Travel or Exposure: Have you traveled recently?\
Have you been around anyone who was sick?
Rash:\
Onset: When did the rash first appear? Location: Where is the rash located on your body? Description: How would you\
describe the rash (e.g., raised, itchy, red)? Progression: Has the rash changed in appearance since it first appeared?\
Associated Symptoms: Any fever, itching, pain?\
Joint Pain:\
Location: Which joints are affected? Onset: When did the joint pain start? Character: How would you describe the pain\
(e.g., sharp, dull, achy)? Timing: Does the pain occur at specific times of the day or with certain activities?\
Associated Symptoms: Any swelling, redness, stiffness?\
Fatigue: Onset: When did you start feeling fatigued? Duration: How long have you been experiencing fatigue?\
Severity: On a scale of 1 to 10, how would you rate your fatigue? Triggers: Is there anything that seems to\
make your fatigue better or worse? Associated Symptoms: Any changes in appetite, sleep disturbances?\
As you can see by the examples, you should ask questions specific to the patient's symptoms. If relevant, you should\
ask follow-up questions to the patient's responses in order to gather more information if you feel it is needed. \
Please use everyday layman terms and avoid using complex medical terminology.\
Only ask one question or prompt at a time, and keep your questions brief (one to two short sentences).
"""
)
) {
SummarizeFunction(stringBox: stringBoxTemp)
}
// swiftlint:enable closure_end_indentation
}
}

#Preview {
LLMInteraction(presentingAccount: .constant(true))
LLMInteraction(presentingAccount: .constant(false))
.previewWith {
LLMRunner {
LLMOpenAIRunnerSetupTask()
Expand Down
42 changes: 36 additions & 6 deletions Intake/ChiefComplaint/SummaryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,44 @@ import Foundation
import SwiftUI

struct SummaryView: View {
let chiefComplaint: String
@State var chiefComplaint: String
// var navigationPath: NavigationPath

var body: some View {
VStack {
Text(chiefComplaint)
.padding()
.multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 20) {
Text("Primary Concern")
.font(.title)
.padding(.top, 50)

Text("Here is a summary of your primary concern:")

TextEditor(text: $chiefComplaint)
.frame(height: 150)
.background(Color.white)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.secondary, lineWidth: 1)
)
.padding(.horizontal)

Button(action: {
// Save output to Fhirstore and navigate to next screen
// navigationPath.append(NavigationViews.allergies)
}) {
Text("Submit")
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(8)
}
.padding(.horizontal)
}
.navigationTitle("Summary")
.padding()
}

init(chiefComplaint: String) {
self.chiefComplaint = chiefComplaint
}
}
40 changes: 31 additions & 9 deletions Intake/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,47 @@ import SpeziAccount
import SpeziMockWebService
import SwiftUI

enum NavigationViews: String {
case allergies
case surgical
case medical
case social
case medication
case chat
}

struct HomeView: View {
enum Tabs: String {
case schedule
case form
case contact
case mockUpload
case summary
}

static var accountEnabled: Bool {
!FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding
}


@AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule
@State private var presentingAccount = false


// @State var navigationPath = NavigationPath()

var body: some View {
// NavigationStack(path: $navigationPath) {
// LLMInteraction(presentingAccount: $presentingAccount)
// .navigationDestination(for: NavigationViews.self) { view in
// switch view {
// case .allergies: AllergyViewTest()
// default: SummaryView(chiefComplaint: "blah blah blah")
// // Fill in rest from NavigationView
// }
// }
// }
// .environmentObject(navigationPath)


TabView(selection: $selectedTab) {
ScheduleView(presentingAccount: $presentingAccount)
.tag(Tabs.schedule)
Expand All @@ -53,13 +75,13 @@ struct HomeView: View {
Label("Create Form", systemImage: "captions.bubble.fill")
}
}
.sheet(isPresented: $presentingAccount) {
AccountSheet()
}
.accountRequired(Self.accountEnabled) {
AccountSheet()
}
.verifyRequiredAccountDetails(Self.accountEnabled)
.sheet(isPresented: $presentingAccount) {
AccountSheet()
}
.accountRequired(Self.accountEnabled) {
AccountSheet()
}
.verifyRequiredAccountDetails(Self.accountEnabled)
}
}

Expand Down
Loading

0 comments on commit 4294074

Please sign in to comment.