Skip to content

Commit

Permalink
Adds support for local LLMs with SpeziLLMLocal
Browse files Browse the repository at this point in the history
  • Loading branch information
vishnuravi committed Apr 4, 2024
1 parent 10b725e commit 5d0a572
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 32 deletions.
42 changes: 24 additions & 18 deletions HealthGPT.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@
2F4E21D829F0518B0067EE98 /* OnboardingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E21D729F0518B0067EE98 /* OnboardingUITests.swift */; };
2F4E23832989D51F0013F3D9 /* HealthGPTAppTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* HealthGPTAppTestingSetup.swift */; };
2F5E32BD297E05EA003432F8 /* HealthGPTAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* HealthGPTAppDelegate.swift */; };
2FF8DBF429F041C500239E1A /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 2FF8DBF329F041C500239E1A /* OpenAI */; };
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 */; };
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 */; };
653A2551283387FE005D4D48 /* HealthGPTApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* HealthGPTApplication.swift */; };
653A255528338800005D4D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; };
E21D86A029EDB17500490C26 /* HealthDataFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21D869F29EDB17500490C26 /* HealthDataFetcher.swift */; };
Expand Down Expand Up @@ -98,6 +100,7 @@
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>"; };
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>"; };
653A255428338800005D4D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -131,14 +134,15 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
63DAAF712BBEB14A009E5E19 /* SpeziLLMLocal in Frameworks */,
63439A4E2BA6069E008BDBFD /* SpeziLLM in Frameworks */,
27859C072A34F17800397C85 /* SpeziOnboarding in Frameworks */,
27859C012A34F15E00397C85 /* XCTSpezi in Frameworks */,
63DAAF732BBEB14A009E5E19 /* SpeziLLMLocalDownload in Frameworks */,
27859C042A34F16B00397C85 /* SpeziHealthKit in Frameworks */,
27859BFF2A34F15E00397C85 /* Spezi in Frameworks */,
27859C102A34F1A900397C85 /* SpeziLocalStorage in Frameworks */,
63439A502BA6069E008BDBFD /* SpeziLLMOpenAI in Frameworks */,
2FF8DBF429F041C500239E1A /* OpenAI in Frameworks */,
27859C122A34F1A900397C85 /* SpeziSecureStorage in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -165,6 +169,7 @@
275DEFD829EEC6B80079D453 /* OnboardingFlow.swift */,
275DEFDA29EEC6CA0079D453 /* String+ModuleLocalized.swift */,
275DEFDC29EEC6DC0079D453 /* Welcome.swift */,
63DAAF742BBEB24D009E5E19 /* LLMLocalDownload.swift */,
);
path = Onboarding;
sourceTree = "<group>";
Expand Down Expand Up @@ -328,7 +333,6 @@
);
name = HealthGPT;
packageProductDependencies = (
2FF8DBF329F041C500239E1A /* OpenAI */,
27859BFE2A34F15E00397C85 /* Spezi */,
27859C002A34F15E00397C85 /* XCTSpezi */,
27859C032A34F16B00397C85 /* SpeziHealthKit */,
Expand All @@ -337,6 +341,8 @@
27859C112A34F1A900397C85 /* SpeziSecureStorage */,
63439A4D2BA6069E008BDBFD /* SpeziLLM */,
63439A4F2BA6069E008BDBFD /* SpeziLLMOpenAI */,
63DAAF702BBEB14A009E5E19 /* SpeziLLMLocal */,
63DAAF722BBEB14A009E5E19 /* SpeziLLMLocalDownload */,
);
productName = TemplateApplication;
productReference = 653A254D283387FE005D4D48 /* HealthGPT.app */;
Expand Down Expand Up @@ -375,7 +381,6 @@
);
mainGroup = 653A2544283387FE005D4D48;
packageReferences = (
2FF8DBF229F041C500239E1A /* XCRemoteSwiftPackageReference "OpenAI" */,
27859BFD2A34F15E00397C85 /* XCRemoteSwiftPackageReference "Spezi" */,
27859C022A34F16A00397C85 /* XCRemoteSwiftPackageReference "SpeziHealthKit" */,
27859C052A34F17800397C85 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */,
Expand Down Expand Up @@ -470,6 +475,7 @@
275DEFD729EEC6A60079D453 /* Disclaimer.swift in Sources */,
275DEFF429EECA180079D453 /* CodableArray+RawRepresentable.swift in Sources */,
2719BC7829F0A31500995C31 /* SettingsView.swift in Sources */,
63DAAF752BBEB24D009E5E19 /* LLMLocalDownload.swift in Sources */,
2F4242992A8B0432006E2B01 /* HealthGPTStandard.swift in Sources */,
2F4242972A8B03A5006E2B01 /* OpenAIModelSelection.swift in Sources */,
2F4E23832989D51F0013F3D9 /* HealthGPTAppTestingSetup.swift in Sources */,
Expand Down Expand Up @@ -725,6 +731,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
"SWIFT_ELicenseRef-TemplateApplication_LOC_STRINGS" = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_INTEROP_MODE = objcxx;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand Down Expand Up @@ -887,6 +894,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
"SWIFT_ELicenseRef-TemplateApplication_LOC_STRINGS" = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_INTEROP_MODE = objcxx;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand Down Expand Up @@ -936,6 +944,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
"SWIFT_ELicenseRef-TemplateApplication_LOC_STRINGS" = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_INTEROP_MODE = objcxx;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand Down Expand Up @@ -1043,20 +1052,12 @@
minimumVersion = 1.0.1;
};
};
2FF8DBF229F041C500239E1A /* XCRemoteSwiftPackageReference "OpenAI" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/MacPaw/OpenAI.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.2.6;
};
};
63439A4C2BA6069E008BDBFD /* XCRemoteSwiftPackageReference "SpeziLLM" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/StanfordSpezi/SpeziLLM";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.7.1;
minimumVersion = 0.8.1;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down Expand Up @@ -1112,11 +1113,6 @@
package = 27859C262A34F2DE00397C85 /* XCRemoteSwiftPackageReference "XCTRuntimeAssertions" */;
productName = XCTRuntimeAssertions;
};
2FF8DBF329F041C500239E1A /* OpenAI */ = {
isa = XCSwiftPackageProductDependency;
package = 2FF8DBF229F041C500239E1A /* XCRemoteSwiftPackageReference "OpenAI" */;
productName = OpenAI;
};
63439A4D2BA6069E008BDBFD /* SpeziLLM */ = {
isa = XCSwiftPackageProductDependency;
package = 63439A4C2BA6069E008BDBFD /* XCRemoteSwiftPackageReference "SpeziLLM" */;
Expand All @@ -1127,6 +1123,16 @@
package = 63439A4C2BA6069E008BDBFD /* XCRemoteSwiftPackageReference "SpeziLLM" */;
productName = SpeziLLMOpenAI;
};
63DAAF702BBEB14A009E5E19 /* SpeziLLMLocal */ = {
isa = XCSwiftPackageProductDependency;
package = 63439A4C2BA6069E008BDBFD /* XCRemoteSwiftPackageReference "SpeziLLM" */;
productName = SpeziLLMLocal;
};
63DAAF722BBEB14A009E5E19 /* SpeziLLMLocalDownload */ = {
isa = XCSwiftPackageProductDependency;
package = 63439A4C2BA6069E008BDBFD /* XCRemoteSwiftPackageReference "SpeziLLM" */;
productName = SpeziLLMLocalDownload;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 653A2545283387FE005D4D48 /* Project object */;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "98a3d0589c1bc602c69147b70f8a44c2f8b44cfc1469b8ed82df6bf8db92de53",
"originHash" : "763dd6c3fb283cd30c2edd3e18fad0c0f9ef9fb226c4b08bdd490e0a7de07181",
"pins" : [
{
"identity" : "llama.cpp",
Expand All @@ -13,10 +13,10 @@
{
"identity" : "openai",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MacPaw/OpenAI.git",
"location" : "https://github.com/StanfordBDHG/OpenAI",
"state" : {
"revision" : "35afc9a6ee127b8f22a85a31aec2036a987478af",
"version" : "0.2.6"
"revision" : "29316babb446c34bb07bf528d96de7eb41e7b03c",
"version" : "0.2.8"
}
},
{
Expand All @@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziChat",
"state" : {
"revision" : "2334583105224b0c04fc36989db82b000021d31d",
"version" : "0.1.9"
"revision" : "aaa10d71431b78ece8bf29f95c0050632714984d",
"version" : "0.2.0"
}
},
{
Expand All @@ -60,8 +60,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziLLM",
"state" : {
"revision" : "dc37b91ed55c9d50eaf58e645d454cb62e3681d1",
"version" : "0.7.2"
"revision" : "cbaf20496e600c985dea2358f35a497fe4964116",
"version" : "0.8.1"
}
},
{
Expand Down
4 changes: 4 additions & 0 deletions HealthGPT.xcodeproj/xcshareddata/xcschemes/HealthGPT.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
argument = "--mockMode"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--localLLM"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--showOnboarding"
isEnabled = "NO">
Expand Down
5 changes: 4 additions & 1 deletion HealthGPT/HealthGPT/HealthDataInterpreter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
import Spezi
import SpeziChat
import SpeziLLM
import SpeziLLMLocal
import SpeziLLMOpenAI
import SpeziSpeechSynthesizer

Expand Down Expand Up @@ -38,6 +39,8 @@ class HealthDataInterpreter: DefaultInitializable, Module, EnvironmentAccessible

if FeatureFlags.mockMode {
llmSchema = LLMMockSchema()
} else if FeatureFlags.localLLM {
llmSchema = LLMLocalSchema(modelPath: .cachesDirectory.appending(path: "llm.gguf"))
} else {
llmSchema = LLMOpenAISchema(parameters: .init(modelType: model))
}
Expand All @@ -52,7 +55,7 @@ class HealthDataInterpreter: DefaultInitializable, Module, EnvironmentAccessible
@MainActor
func queryLLM() async throws {
guard let llm,
llm.context.last?.role == .user || !(llm.context.contains(where: { $0.role == .assistant }) ) else {
llm.context.last?.role == .user || !(llm.context.contains(where: { $0.role == .assistant() }) ) else {
return
}

Expand Down
55 changes: 53 additions & 2 deletions HealthGPT/HealthGPT/HealthGPTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ struct HealthGPTView: View {
var body: some View {
NavigationStack {
if let llm = healthDataInterpreter.llm {
let contextBinding = Binding { llm.context } set: { llm.context = $0 }
let contextBinding = Binding { llm.context.chatEntity } set: { llm.context = $0.llmContextEntity }

ChatView(contextBinding, exportFormat: .text)
.speak(llm.context, muted: !textToSpeech)
.speak(llm.context.chatEntity, muted: !textToSpeech)
.speechToolbarButton(muted: !$textToSpeech)
.viewStateAlert(state: llm.state)
.navigationTitle("WELCOME_TITLE")
Expand Down Expand Up @@ -104,3 +105,53 @@ struct HealthGPTView: View {
}
}
}

extension Array where Element == LLMContextEntity {
var chatEntity: [ChatEntity] {
self.map { llmContextEntity in
let role: ChatEntity.Role

switch llmContextEntity.role {
case .user:
role = .user
case .assistant:
role = .assistant
case .system, .tool:
role = .hidden(type: .unknown)
}

return ChatEntity(
role: role,
content: llmContextEntity.content,
complete: llmContextEntity.complete,
id: llmContextEntity.id,
date: llmContextEntity.date
)
}
}
}

extension Array where Element == ChatEntity {
var llmContextEntity: [LLMContextEntity] {
self.map { chatEntity in
let role: LLMContextEntity.Role

switch chatEntity.role {
case .user:
role = .user
case .assistant:
role = .assistant()
case .hidden:
role = .system
}

return LLMContextEntity(
role: role,
content: chatEntity.content,
complete: chatEntity.complete,
id: chatEntity.id,
date: chatEntity.date
)
}
}
}
5 changes: 4 additions & 1 deletion HealthGPT/HealthGPT/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ struct SettingsView: View {
var body: some View {
NavigationStack(path: $path) {
List {
openAISettings
if !FeatureFlags.localLLM && !FeatureFlags.mockMode {
openAISettings
}

chatSettings
speechSettings
disclaimer
Expand Down
2 changes: 2 additions & 0 deletions HealthGPT/HealthGPTAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HealthKit
import Spezi
import SpeziHealthKit
import SpeziLLM
import SpeziLLMLocal
import SpeziLLMOpenAI
import SpeziSecureStorage
import SpeziSpeechSynthesizer
Expand All @@ -24,6 +25,7 @@ class HealthGPTAppDelegate: SpeziAppDelegate {
}
LLMRunner {
LLMOpenAIPlatform()
LLMLocalPlatform()
LLMMockPlatform()
}
HealthDataInterpreter()
Expand Down
30 changes: 30 additions & 0 deletions HealthGPT/Onboarding/LLMLocalDownload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// 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 SpeziLLMLocalDownload
import SpeziOnboarding
import SwiftUI

struct LLMLocalDownload: View {
@Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath


var body: some View {
LLMLocalDownloadView(
downloadDescription: "The Llama2 7B model will be downloaded for local execution.",
llmDownloadUrl: LLMLocalDownloadManager.LLMUrlDefaults.llama2ChatModelUrl,
llmStorageUrl: .cachesDirectory.appending(path: "llm.gguf")
) {
onboardingNavigationPath.nextStep()
}
}
}

#Preview {
LLMLocalDownload()
}
10 changes: 8 additions & 2 deletions HealthGPT/Onboarding/OnboardingFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ struct OnboardingFlow: View {
OnboardingStack(onboardingFlowComplete: $completedOnboardingFlow) {
Welcome()
Disclaimer()
OpenAIAPIKey()
OpenAIModelSelection()

if FeatureFlags.localLLM {
LLMLocalDownload()
} else {
OpenAIAPIKey()
OpenAIModelSelection()
}

if HKHealthStore.isHealthDataAvailable() {
HealthKitPermissions()
}
Expand Down
2 changes: 2 additions & 0 deletions HealthGPT/SharedContext/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ enum FeatureFlags {
static let showOnboarding = CommandLine.arguments.contains("--showOnboarding")
/// Resets all credentials in Secure Storage when the application is launched in order to facilitate testing of OpenAI API keys.
static let resetSecureStorage = CommandLine.arguments.contains("--resetSecureStorage")
/// Configures SpeziLLM to use a local model stored on the device downloaded during onboarding
static let localLLM = CommandLine.arguments.contains("--localLLM")
/// Configures SpeziLLM to mock all generated responses for development and UI tests
static let mockMode = CommandLine.arguments.contains("--mockMode")
}
3 changes: 3 additions & 0 deletions HealthGPT/Supporting Files/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,9 @@
}
}
}
},
"The Llama2 7B model will be downloaded for local execution." : {

},
"WELCOME_AREA1_DESCRIPTION" : {
"extractionState" : "manual",
Expand Down

0 comments on commit 5d0a572

Please sign in to comment.