Skip to content

Commit

Permalink
Allow user to select local LLM during onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
vishnuravi committed Apr 4, 2024
1 parent b46cc73 commit dad7ab8
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 23 deletions.
8 changes: 8 additions & 0 deletions HealthGPT.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
63439A4E2BA6069E008BDBFD /* SpeziLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 63439A4D2BA6069E008BDBFD /* SpeziLLM */; };
63439A502BA6069E008BDBFD /* SpeziLLMOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 63439A4F2BA6069E008BDBFD /* SpeziLLMOpenAI */; };
634523F12BAEF62E00A6E2A1 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 634523F02BAEF62D00A6E2A1 /* Localizable.xcstrings */; };
63497B6C2BBF095A001F8419 /* LLMSourceSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63497B6B2BBF095A001F8419 /* LLMSourceSelection.swift */; };
63497B6E2BBF0C0B001F8419 /* LLMSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63497B6D2BBF0C0B001F8419 /* LLMSource.swift */; };
63DAAF712BBEB14A009E5E19 /* SpeziLLMLocal in Frameworks */ = {isa = PBXBuildFile; productRef = 63DAAF702BBEB14A009E5E19 /* SpeziLLMLocal */; };
63DAAF732BBEB14A009E5E19 /* SpeziLLMLocalDownload in Frameworks */ = {isa = PBXBuildFile; productRef = 63DAAF722BBEB14A009E5E19 /* SpeziLLMLocalDownload */; };
63DAAF752BBEB24D009E5E19 /* LLMLocalDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DAAF742BBEB24D009E5E19 /* LLMLocalDownload.swift */; };
Expand Down Expand Up @@ -100,6 +102,8 @@
2F5E32BC297E05EA003432F8 /* HealthGPTAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthGPTAppDelegate.swift; sourceTree = "<group>"; };
2FAEC07F297F583900C11C42 /* HealthGPT.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthGPT.entitlements; sourceTree = "<group>"; };
634523F02BAEF62D00A6E2A1 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
63497B6B2BBF095A001F8419 /* LLMSourceSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMSourceSelection.swift; sourceTree = "<group>"; };
63497B6D2BBF0C0B001F8419 /* LLMSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMSource.swift; sourceTree = "<group>"; };
63DAAF742BBEB24D009E5E19 /* LLMLocalDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMLocalDownload.swift; sourceTree = "<group>"; };
653A254D283387FE005D4D48 /* HealthGPT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HealthGPT.app; sourceTree = BUILT_PRODUCTS_DIR; };
653A2550283387FE005D4D48 /* HealthGPTApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthGPTApplication.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -170,6 +174,8 @@
275DEFDA29EEC6CA0079D453 /* String+ModuleLocalized.swift */,
275DEFDC29EEC6DC0079D453 /* Welcome.swift */,
63DAAF742BBEB24D009E5E19 /* LLMLocalDownload.swift */,
63497B6B2BBF095A001F8419 /* LLMSourceSelection.swift */,
63497B6D2BBF0C0B001F8419 /* LLMSource.swift */,
);
path = Onboarding;
sourceTree = "<group>";
Expand Down Expand Up @@ -482,11 +488,13 @@
272FB34E2A03CDFF0010B98D /* HealthDataFetcher+Process.swift in Sources */,
275DEFD029EEC5D20079D453 /* FeatureFlags.swift in Sources */,
272FB3522A03D14C0010B98D /* HealthData.swift in Sources */,
63497B6E2BBF0C0B001F8419 /* LLMSource.swift in Sources */,
F381BD3C2A4DE41900BCEB69 /* HealthDataInterpreter.swift in Sources */,
275DEFF229EECA030079D453 /* Binding+Negate.swift in Sources */,
2F5E32BD297E05EA003432F8 /* HealthGPTAppDelegate.swift in Sources */,
275DEFD529EEC6870079D453 /* HealthKitPermissions.swift in Sources */,
272FB3542A03F1860010B98D /* PromptGenerator.swift in Sources */,
63497B6C2BBF095A001F8419 /* LLMSourceSelection.swift in Sources */,
275DEFDB29EEC6CA0079D453 /* String+ModuleLocalized.swift in Sources */,
2719BC8029F0A5DD00995C31 /* UIApplication+Keyboard.swift in Sources */,
E22B62EF29E8CE1E008F3484 /* HealthGPTView.swift in Sources */,
Expand Down
22 changes: 5 additions & 17 deletions HealthGPT/HealthGPT/HealthDataInterpreter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,16 @@ class HealthDataInterpreter: DefaultInitializable, Module, EnvironmentAccessible
@ObservationIgnored private var systemPrompt = ""

required init() { }


/// Creates an `LLMSchema`, sets it up for use with an `LLMRunner`, injects the system prompt

/// Creates an `LLMRunner`, from an `LLMSchema` and injects the system prompt
/// into the context, and assigns the resulting `LLMSession` to the `llm` property. For more
/// information, please refer to the [`SpeziLLM`](https://swiftpackageindex.com/StanfordSpezi/SpeziLLM/documentation/spezillm) documentation.
///
/// If the `--mockMode` feature flag is set, this function will use `LLMMockSchema()`, otherwise
/// will use `LLMOpenAISchema` with the model type specified in the `model` parameter.
/// - Parameter model: the type of OpenAI model to use
/// - Parameter schema: the LLMSchema to use
@MainActor
func prepareLLM(with model: LLMOpenAIModelType) async {
var llmSchema: any LLMSchema

if FeatureFlags.mockMode {
llmSchema = LLMMockSchema()
} else if FeatureFlags.localLLM {
llmSchema = LLMLocalSchema(modelPath: .cachesDirectory.appending(path: "llm.gguf"))
} else {
llmSchema = LLMOpenAISchema(parameters: .init(modelType: model))
}

let llm = llmRunner(with: llmSchema)
func prepareLLM(with schema: any LLMSchema) async {
let llm = llmRunner(with: schema)
systemPrompt = await generateSystemPrompt()
llm.context.append(systemMessage: systemPrompt)
self.llm = llm
Expand Down
10 changes: 9 additions & 1 deletion HealthGPT/HealthGPT/HealthGPTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import SpeziLLM
import SpeziLLMOpenAI
import SpeziSpeechSynthesizer
import SwiftUI
import SpeziLLMLocal

Check failure on line 14 in HealthGPT/HealthGPT/HealthGPTView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Sorted Imports Violation: Imports should be sorted (sorted_imports)


struct HealthGPTView: View {
@AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false
@AppStorage(StorageKeys.enableTextToSpeech) private var textToSpeech = StorageKeys.Defaults.enableTextToSpeech
@AppStorage(StorageKeys.llmSource) private var llmSource = StorageKeys.Defaults.llmSource
@AppStorage(StorageKeys.openAIModel) private var openAIModel = LLMOpenAIModelType.gpt4

@Environment(HealthDataInterpreter.self) private var healthDataInterpreter
Expand Down Expand Up @@ -66,7 +68,13 @@ struct HealthGPTView: View {
Text(errorMessage)
}
.task {
await healthDataInterpreter.prepareLLM(with: openAIModel)
if FeatureFlags.mockMode {
await healthDataInterpreter.prepareLLM(with: LLMMockSchema())
} else if FeatureFlags.localLLM || llmSource == .local {
await healthDataInterpreter.prepareLLM(with: LLMLocalSchema(modelPath: .cachesDirectory.appending(path: "llm.gguf")))
} else {
await healthDataInterpreter.prepareLLM(with: LLMOpenAISchema(parameters: .init(modelType: openAIModel)))
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions HealthGPT/HealthGPT/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ struct SettingsView: View {
@Environment(\.dismiss) private var dismiss
@Environment(HealthDataInterpreter.self) private var healthDataInterpreter
@AppStorage(StorageKeys.enableTextToSpeech) private var enableTextToSpeech = StorageKeys.Defaults.enableTextToSpeech
@AppStorage(StorageKeys.llmSource) private var llmSource = StorageKeys.Defaults.llmSource
@AppStorage(StorageKeys.openAIModel) private var openAIModel = LLMOpenAIModelType.gpt4
let logger = Logger(subsystem: "HealthGPT", category: "Settings")


var body: some View {
NavigationStack(path: $path) {
List {
if !FeatureFlags.localLLM {
if !FeatureFlags.localLLM && !(llmSource == .local) {
openAISettings
}

Expand Down Expand Up @@ -105,7 +106,7 @@ struct SettingsView: View {
) { model in
Task {
openAIModel = model
await healthDataInterpreter.prepareLLM(with: model)
await healthDataInterpreter.prepareLLM(with: LLMOpenAISchema(parameters: .init(modelType: model)))
path.removeLast()
}
}
Expand Down
28 changes: 28 additions & 0 deletions HealthGPT/Onboarding/LLMSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// This source file is part of the Stanford HealthGPT project
//
// SPDX-FileCopyrightText: 2024 Stanford University & Project Contributors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation


enum LLMSource: String, CaseIterable, Identifiable, Codable {
case local
case openai

var id: String {
self.rawValue
}

var localizedDescription: LocalizedStringResource {
switch self {
case .local:
LocalizedStringResource("LOCAL_LLM_LABEL")
case .openai:
LocalizedStringResource("OPENAI_LLM_LABEL")
}
}
}
56 changes: 56 additions & 0 deletions HealthGPT/Onboarding/LLMSourceSelection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// This source file is part of the Stanford HealthGPT project
//
// SPDX-FileCopyrightText: 2024 Stanford University & Project Contributors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SpeziOnboarding
import SwiftUI

struct LLMSourceSelection: View {
@Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath
@AppStorage(StorageKeys.llmSource) private var llmSource = StorageKeys.Defaults.llmSource

var body: some View {
OnboardingView(
contentView: {
VStack {
OnboardingTitleView(
title: "LLM_SOURCE_SELECTION_TITLE",
subtitle: "LLM_SOURCE_SELECTION_SUBTITLE"
)
Spacer()
sourceSelector
Spacer()
}
},
actionView: {
OnboardingActionsView(
"LLM_SOURCE_SELECTION_BUTTON"
) {
if llmSource == .local {
onboardingNavigationPath.append(customView: LLMLocalDownload())
} else {
onboardingNavigationPath.append(customView: OpenAIAPIKey())
}
}
}
)
}

private var sourceSelector: some View {
Picker("", selection: $llmSource) {
ForEach(LLMSource.allCases) { source in
Text(source.localizedDescription)
.tag(source)
}
}
.pickerStyle(.inline)
}
}

#Preview {
LLMSourceSelection()
}
4 changes: 2 additions & 2 deletions HealthGPT/Onboarding/OnboardingFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SwiftUI
/// Displays an multi-step onboarding flow for the HealthGPT Application.
struct OnboardingFlow: View {
@AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false
@AppStorage(StorageKeys.llmSource) var llmSource = StorageKeys.Defaults.llmSource


var body: some View {
Expand All @@ -25,8 +26,7 @@ struct OnboardingFlow: View {
if FeatureFlags.localLLM {
LLMLocalDownload()
} else {
OpenAIAPIKey()
OpenAIModelSelection()
LLMSourceSelection()
}

if HKHealthStore.isHealthDataAvailable() {
Expand Down
2 changes: 1 addition & 1 deletion HealthGPT/Onboarding/OpenAIAPIKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct OpenAIAPIKey: View {

var body: some View {
LLMOpenAIAPITokenOnboardingStep {
onboardingNavigationPath.nextStep()
onboardingNavigationPath.append(customView: OpenAIModelSelection())
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions HealthGPT/SharedContext/StorageKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
enum StorageKeys {
enum Defaults {
static let enableTextToSpeech = false
static let llmSource = LLMSource.openai
}

// MARK: - Onboarding
/// A `Bool` flag indicating of the onboarding was completed.
static let onboardingFlowComplete = "onboardingFlow.complete"
/// A `Step` flag indicating the current step in the onboarding process.
static let onboardingFlowStep = "onboardingFlow.step"
/// An `LLMSource` flag indicating the source of the model (local vs. OpenAI)
static let llmSource = "llmsource"
/// An `LLMOpenAIModelType` flag indicating the OpenAI model to use
static let openAIModel = "openAI.model"
/// A `Bool` flag indicating if messages should be spoken.
Expand Down
53 changes: 53 additions & 0 deletions HealthGPT/Supporting Files/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {

},
"API_KEY_SUBTITLE" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -219,6 +222,36 @@
}
}
},
"LLM_SOURCE_SELECTION_BUTTON" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Save Choice"
}
}
}
},
"LLM_SOURCE_SELECTION_SUBTITLE" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Choose an on-device or OpenAI LLM"
}
}
}
},
"LLM_SOURCE_SELECTION_TITLE" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "LLM Source Selection"
}
}
}
},
"LOADING_CHAT_VIEW" : {
"localizations" : {
"en" : {
Expand All @@ -229,6 +262,16 @@
}
}
},
"LOCAL_LLM_LABEL" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "On-device LLM"
}
}
}
},
"MODEL_SELECTION_SUBTITLE" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -282,6 +325,16 @@
}
}
},
"OPENAI_LLM_LABEL" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Open AI LLM"
}
}
}
},
"RESET" : {
"localizations" : {
"en" : {
Expand Down

0 comments on commit dad7ab8

Please sign in to comment.