diff --git a/Package.swift b/Package.swift index b926ca3..644cb64 100644 --- a/Package.swift +++ b/Package.swift @@ -30,14 +30,17 @@ let package = Package( .package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.5.0")), .package(url: "https://github.com/StanfordSpezi/SpeziOnboarding", .upToNextMinor(from: "0.7.0")), .package(url: "https://github.com/StanfordSpezi/SpeziSpeech", .upToNextMinor(from: "0.1.1")), - .package(url: "https://github.com/StanfordSpezi/SpeziChat", .upToNextMinor(from: "0.1.1")) + .package(url: "https://github.com/StanfordSpezi/SpeziChat", .upToNextMinor(from: "0.1.1")), + // .package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.6.2")) + .package(url: "https://github.com/StanfordSpezi/SpeziViews", branch: "feature/view-state-mapper") ], targets: [ .target( name: "SpeziLLM", dependencies: [ .product(name: "Spezi", package: "Spezi"), - .product(name: "SpeziChat", package: "SpeziChat") + .product(name: "SpeziChat", package: "SpeziChat"), + .product(name: "SpeziViews", package: "SpeziViews") ] ), .target( @@ -67,7 +70,8 @@ let package = Package( .target( name: "SpeziLLMLocalDownload", dependencies: [ - .product(name: "SpeziOnboarding", package: "SpeziOnboarding") + .product(name: "SpeziOnboarding", package: "SpeziOnboarding"), + .product(name: "SpeziViews", package: "SpeziViews") ] ), .target( diff --git a/README.md b/README.md index f3d67cc..8cd5e64 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ SPDX-License-Identifier: MIT [![Build and Test](https://github.com/StanfordSpezi/SpeziLLM/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziLLM/actions/workflows/build-and-test.yml) [![codecov](https://codecov.io/gh/StanfordSpezi/SpeziLLM/branch/main/graph/badge.svg?token=pptLyqtoNR)](https://codecov.io/gh/StanfordSpezi/SpeziLLM) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7954213.svg)](https://doi.org/10.5281/zenodo.7954213) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7954213.svg)](https://doi.org/10.5281/zenodo.7954213) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziLLM%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziLLM) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziLLM%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziLLM) diff --git a/Sources/SpeziLLM/LLM.swift b/Sources/SpeziLLM/LLM.swift index b787e9d..a61e832 100644 --- a/Sources/SpeziLLM/LLM.swift +++ b/Sources/SpeziLLM/LLM.swift @@ -14,7 +14,7 @@ public protocol LLM { /// The type of the ``LLM`` as represented by the ``LLMHostingType``. var type: LLMHostingType { get async } /// The state of the ``LLM`` indicated by the ``LLMState``. - var state: LLMState { get async } + @MainActor var state: LLMState { get } /// Performs any setup-related actions for the ``LLM``. diff --git a/Sources/SpeziLLM/LLMError.swift b/Sources/SpeziLLM/LLMError.swift index 6d0e89b..edc41b9 100644 --- a/Sources/SpeziLLM/LLMError.swift +++ b/Sources/SpeziLLM/LLMError.swift @@ -9,7 +9,7 @@ import Foundation -/// The ``LLMError`` describes possible errors that occure during the execution of the ``LLM`` via the ``LLMRunner``. +/// The ``LLMError`` describes possible errors that occur during the execution of the ``LLM`` via the ``LLMRunner``. public enum LLMError: LocalizedError { /// Indicates that the local model file is not found. case modelNotFound diff --git a/Sources/SpeziLLM/LLMState+OperationState.swift b/Sources/SpeziLLM/LLMState+OperationState.swift new file mode 100644 index 0000000..5c2583d --- /dev/null +++ b/Sources/SpeziLLM/LLMState+OperationState.swift @@ -0,0 +1,26 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziViews + +// Needs to be in a separate file as an extension in the file of the ``LLMState`` will lead to +// the "Circular reference resolving attached macro 'Observable'" error during compiling (see https://github.com/apple/swift/issues/66450) +/// Maps the ``LLMState`` to the SpeziViews `ViewState` via the conformance to the SpeziViews `OperationState` protocol. +extension LLMState: OperationState { + public var viewState: ViewState { + switch self { + case .uninitialized, .ready: + .idle + case .generating, .loading: + .processing + case .error(let error): + .error(error) + } + } +} diff --git a/Sources/SpeziLLM/Mock/LLMMock.swift b/Sources/SpeziLLM/Mock/LLMMock.swift index 7b31423..e2dcb45 100644 --- a/Sources/SpeziLLM/Mock/LLMMock.swift +++ b/Sources/SpeziLLM/Mock/LLMMock.swift @@ -12,15 +12,16 @@ import Foundation /// A mock SpeziLLM ``LLM`` that is used for testing and preview purposes. public actor LLMMock: LLM { public let type: LLMHostingType = .local - public var state: LLMState = .uninitialized + @MainActor public var state: LLMState = .uninitialized public init() {} public func setup(runnerConfig: LLMRunnerConfiguration) async throws { - /// Set ``LLMState`` to ready - self.state = .ready + await MainActor.run { + self.state = .ready + } } public func generate(prompt: String, continuation: AsyncThrowingStream.Continuation) async { diff --git a/Sources/SpeziLLM/SpeziLLM.docc/SpeziLLM.md b/Sources/SpeziLLM/SpeziLLM.docc/SpeziLLM.md new file mode 100644 index 0000000..9f1dc26 --- /dev/null +++ b/Sources/SpeziLLM/SpeziLLM.docc/SpeziLLM.md @@ -0,0 +1,23 @@ +# ``SpeziLLM`` + + + +Provides base LLM execution capabilities within the Spezi ecosystem. + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/SpeziLLM/Views/LLMChatView.swift b/Sources/SpeziLLM/Views/LLMChatView.swift index dae4578..b519bc5 100644 --- a/Sources/SpeziLLM/Views/LLMChatView.swift +++ b/Sources/SpeziLLM/Views/LLMChatView.swift @@ -7,6 +7,7 @@ // import SpeziChat +import SpeziViews import SwiftUI @@ -16,9 +17,10 @@ public struct LLMChatView: View { @Environment(LLMRunner.self) private var runner /// Represents the chat content that is displayed. @State private var chat: Chat = [] - /// Indicates if the input field is disabled + /// Indicates if the input field is disabled. @State private var inputDisabled = false - + /// Indicates the state of the view, get's derived from the ``LLM/state``. + @State private var viewState: ViewState = .idle /// A SpeziLLM ``LLM`` that is used for the text generation within the chat view private let model: any LLM @@ -46,6 +48,8 @@ public struct LLMChatView: View { } } } + .map(state: model.state, to: $viewState) + .viewStateAlert(state: $viewState) } diff --git a/Sources/SpeziLLMLocal/LLMLlama+Generation.swift b/Sources/SpeziLLMLocal/LLMLlama+Generation.swift index 33810c2..dffac90 100644 --- a/Sources/SpeziLLMLocal/LLMLlama+Generation.swift +++ b/Sources/SpeziLLMLocal/LLMLlama+Generation.swift @@ -26,8 +26,10 @@ extension LLMLlama { func _generate( // swiftlint:disable:this identifier_name function_body_length prompt: String, continuation: AsyncThrowingStream.Continuation - ) { - self.state = .generating + ) async { + await MainActor.run { + self.state = .generating + } // Log the most important parameters of the LLM Self.logger.debug("n_length = \(self.parameters.maxOutputLength, privacy: .public), n_ctx = \(self.contextParameters.contextWindowSize, privacy: .public), n_batch = \(self.contextParameters.batchSize, privacy: .public), n_kv_req = \(self.parameters.maxOutputLength, privacy: .public)") @@ -115,7 +117,9 @@ extension LLMLlama { if nextTokenId == llama_token_eos(self.model) || batchTokenIndex == self.parameters.maxOutputLength { self.generatedText.append(self.EOS) continuation.finish() - self.state = .ready + await MainActor.run { + self.state = .ready + } return } @@ -136,7 +140,9 @@ extension LLMLlama { let decodeOutput = llama_decode(self.context, batch) if decodeOutput != 0 { // = 0 Success, > 0 Warning, < 0 Error Self.logger.error("Decoding of generated output failed. Output: \(decodeOutput, privacy: .public)") - self.state = .error(error: .generationError) + await MainActor.run { + self.state = .error(error: .generationError) + } continuation.finish(throwing: LLMError.generationError) return } @@ -149,6 +155,8 @@ extension LLMLlama { llama_print_timings(self.context) continuation.finish() - self.state = .ready + await MainActor.run { + self.state = .ready + } } } diff --git a/Sources/SpeziLLMLocal/LLMLlama.swift b/Sources/SpeziLLMLocal/LLMLlama.swift index c7450cb..56b7a3d 100644 --- a/Sources/SpeziLLMLocal/LLMLlama.swift +++ b/Sources/SpeziLLMLocal/LLMLlama.swift @@ -21,7 +21,7 @@ public actor LLMLlama: LLM { /// A Swift Logger that logs important information from the ``LLMLlama``. static let logger = Logger(subsystem: "edu.stanford.spezi", category: "SpeziLLM") public let type: LLMHostingType = .local - public var state: LLMState = .uninitialized + @MainActor public var state: LLMState = .uninitialized /// Parameters of the llama.cpp ``LLM``. let parameters: LLMParameters @@ -60,10 +60,14 @@ public actor LLMLlama: LLM { public func setup(runnerConfig: LLMRunnerConfiguration) async throws { - self.state = .loading + await MainActor.run { + self.state = .loading + } guard let model = llama_load_model_from_file(modelPath.path().cString(using: .utf8), parameters.llamaCppRepresentation) else { - self.state = .error(error: LLMError.modelNotFound) + await MainActor.run { + self.state = .error(error: LLMError.modelNotFound) + } throw LLMError.modelNotFound } self.model = model @@ -72,15 +76,19 @@ public actor LLMLlama: LLM { let trainingContextWindow = llama_n_ctx_train(model) guard self.contextParameters.contextWindowSize <= trainingContextWindow else { Self.logger.warning("Model was trained on only \(trainingContextWindow, privacy: .public) context tokens, not the configured \(self.contextParameters.contextWindowSize, privacy: .public) context tokens") - self.state = .error(error: LLMError.generationError) + await MainActor.run { + self.state = .error(error: LLMError.generationError) + } throw LLMError.modelNotFound } - self.state = .ready + await MainActor.run { + self.state = .ready + } } public func generate(prompt: String, continuation: AsyncThrowingStream.Continuation) async { - _generate(prompt: prompt, continuation: continuation) + await _generate(prompt: prompt, continuation: continuation) } diff --git a/Sources/SpeziLLMLocal/SpeziLLMLocal.docc/SpeziLLMLocal.md b/Sources/SpeziLLMLocal/SpeziLLMLocal.docc/SpeziLLMLocal.md new file mode 100644 index 0000000..f4b5c30 --- /dev/null +++ b/Sources/SpeziLLMLocal/SpeziLLMLocal.docc/SpeziLLMLocal.md @@ -0,0 +1,23 @@ +# ``SpeziLLMLocal`` + + + +Provides Large Language Model (LLM) execution capabilities on the local device. + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadError.swift b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadError.swift new file mode 100644 index 0000000..6d61613 --- /dev/null +++ b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadError.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +/// The ``LLMLocalDownloadError`` describes possible errors that occur during downloading models via the ``LLMLocalDownloadManager``. +public enum LLMLocalDownloadError: LocalizedError { + /// Indicates an unknown error during downloading the model + case unknownError + + + public var errorDescription: String? { + String(localized: LocalizedStringResource("LLM_DOWNLOAD_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + } + + public var recoverySuggestion: String? { + String(localized: LocalizedStringResource("LLM_DOWNLOAD_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + } + + public var failureReason: String? { + String(localized: LocalizedStringResource("LLM_DOWNLOAD_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + } +} diff --git a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager+OperationState.swift b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager+OperationState.swift new file mode 100644 index 0000000..e105d58 --- /dev/null +++ b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager+OperationState.swift @@ -0,0 +1,26 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziViews + +// Needs to be in a separate file as an extension in the file of the ``LLMLocalDownloadManager`` will lead to +// the "Circular reference resolving attached macro 'Observable'" error during compiling (see https://github.com/apple/swift/issues/66450) +/// Maps the ``LLMLocalDownloadManager/DownloadState`` to the SpeziViews `ViewState` via the conformance to the SpeziViews `OperationState` protocol. +extension LLMLocalDownloadManager.DownloadState: OperationState { + public var viewState: ViewState { + switch self { + case .idle, .downloaded: + .idle + case .downloading: + .processing + case .error(let error): + .error(error) + } + } +} diff --git a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager.swift b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager.swift index d705e17..aa6156b 100644 --- a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager.swift +++ b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManager.swift @@ -7,10 +7,21 @@ // import Foundation +import Observation +import SpeziViews -/// Manages the download of an LLM to the local device. -public final class LLMLocalDownloadManager: NSObject, ObservableObject { +/// The ``LLMLocalDownloadManager`` manages the download and storage of Large Language Models (LLM) to the local device. +/// +/// One configures the ``LLMLocalDownloadManager`` via the ``LLMLocalDownloadManager/init(llmDownloadUrl:llmStorageUrl:)`` initializer, +/// passing a download `URL` as well as a storage `URL` to the ``LLMLocalDownloadManager``. +/// The download of a model is started via ``LLMLocalDownloadManager/startDownload()`` and can be cancelled (early) via ``LLMLocalDownloadManager/cancelDownload()``. +/// +/// The current state of the ``LLMLocalDownloadManager`` is exposed via the ``LLMLocalDownloadManager/state`` property which +/// is of type ``LLMLocalDownloadManager/DownloadState``, containing cases such as ``LLMLocalDownloadManager/DownloadState/downloading(progress:)`` +/// which includes the progress of the download or ``LLMLocalDownloadManager/DownloadState/downloaded(storageUrl:)`` which indicates that the download has finished. +@Observable +public final class LLMLocalDownloadManager: NSObject { /// Defaults of possible LLMs to download via the ``LLMLocalDownloadManager``. public enum LLMUrlDefaults { /// LLama 2 7B model in its chat variation (~3.5GB) @@ -51,8 +62,8 @@ public final class LLMLocalDownloadManager: NSObject, ObservableObject { public enum DownloadState: Equatable { case idle case downloading(progress: Double) - case downloaded - case error(Error?) + case downloaded(storageUrl: URL) + case error(LocalizedError) public static func == (lhs: LLMLocalDownloadManager.DownloadState, rhs: LLMLocalDownloadManager.DownloadState) -> Bool { @@ -67,15 +78,15 @@ public final class LLMLocalDownloadManager: NSObject, ObservableObject { } /// The delegate handling the download manager tasks. - private var downloadDelegate: LLMLocalDownloadManagerDelegate? // swiftlint:disable:this weak_delegate + @ObservationIgnored private var downloadDelegate: LLMLocalDownloadManagerDelegate? // swiftlint:disable:this weak_delegate /// The `URLSessionDownloadTask` that handles the download of the model. - private var downloadTask: URLSessionDownloadTask? + @ObservationIgnored private var downloadTask: URLSessionDownloadTask? /// Remote `URL` from where the LLM file should be downloaded. private let llmDownloadUrl: URL /// Local `URL` where the downloaded model is stored. let llmStorageUrl: URL /// Indicates the current state of the ``LLMLocalDownloadManager``. - @MainActor @Published public var state: DownloadState = .idle + @MainActor public var state: DownloadState = .idle /// Creates a ``LLMLocalDownloadManager`` that helps with downloading LLM files from remote servers. @@ -102,4 +113,9 @@ public final class LLMLocalDownloadManager: NSObject, ObservableObject { downloadTask?.resume() } + + /// Cancels the download of a specified model via a `URLSessionDownloadTask`. + public func cancelDownload() { + downloadTask?.cancel() + } } diff --git a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManagerDelegate.swift b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManagerDelegate.swift index cfc485f..99aaaa1 100644 --- a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManagerDelegate.swift +++ b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadManagerDelegate.swift @@ -8,10 +8,12 @@ import Foundation import os +import SpeziViews -/// Delegate of the ``LLMLocalDownloadManager`` that conforms to the `URLSessionDownloadDelegate`. + +/// Delegate of the ``LLMLocalDownloadManager`` implementing the methods of the`URLSessionDownloadDelegate` conformance. class LLMLocalDownloadManagerDelegate: NSObject, URLSessionDownloadDelegate { - /// A Swift Logger that logs important information from the `LocalLLMDownloadManager`. + /// A Swift `Logger` that logs important information from the `LocalLLMDownloadManager`. private static let logger = Logger(subsystem: "edu.stanford.spezi", category: "SpeziLLM") /// A `weak` reference to the ``LLMLocalDownloadManager``. private weak var manager: LLMLocalDownloadManager? @@ -48,11 +50,17 @@ class LLMLocalDownloadManagerDelegate: NSObject, URLSessionDownloadDelegate { do { _ = try FileManager.default.replaceItemAt(self.storageUrl, withItemAt: location) Task { @MainActor in - self.manager?.state = .downloaded + self.manager?.state = .downloaded(storageUrl: self.storageUrl) } } catch { Task { @MainActor in - self.manager?.state = .error(error) + self.manager?.state = .error( + AnyLocalizedError( + error: error, + defaultErrorDescription: + LocalizedStringResource("LLM_DOWNLOAD_FAILED_ERROR", bundle: .atURL(from: .module)) + ) + ) } Self.logger.error("\(String(describing: error))") } @@ -60,11 +68,31 @@ class LLMLocalDownloadManagerDelegate: NSObject, URLSessionDownloadDelegate { /// Indicates an error during the model download func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - if let error = error { + // The `error` property is set for client-side errors (e.g. couldn't resolve host name), + // the `task.error` property is set in the case of server-side errors + if let error = error ?? task.error { Task { @MainActor in - self.manager?.state = .error(error) + self.manager?.state = .error( + AnyLocalizedError( + error: error, + defaultErrorDescription: LocalizedStringResource("LLM_DOWNLOAD_FAILED_ERROR", bundle: .atURL(from: .module)) + ) + ) } Self.logger.error("\(String(describing: error))") + } else { + Task { @MainActor in + self.manager?.state = .error( + AnyLocalizedError( + error: LLMLocalDownloadError.unknownError, + defaultErrorDescription: LocalizedStringResource("LLM_DOWNLOAD_FAILED_ERROR", bundle: .atURL(from: .module)) + ) + ) + } + assertionFailure(""" + The download of the LLM via the `LLMLocalDownloadManager` has failed with an unknown error. + This error is unexpected and violates the integrity of the `LLMLocalDownloadManager` error state. + """) } } } diff --git a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadView.swift b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadView.swift index e74e9e9..32db22e 100644 --- a/Sources/SpeziLLMLocalDownload/LLMLocalDownloadView.swift +++ b/Sources/SpeziLLMLocalDownload/LLMLocalDownloadView.swift @@ -13,26 +13,20 @@ import SwiftUI /// Onboarding LLM Download view public struct LLMLocalDownloadView: View { - @StateObject private var downloadManager: LLMLocalDownloadManager + /// The ``LLMLocalDownloadManager`` manages the download and storage of the models. + @State private var downloadManager: LLMLocalDownloadManager + /// The action that should be performed when pressing the primary button of the view. private let action: () async throws -> Void + /// Indicates the state of the view, get's derived from the ``LLMLocalDownloadManager/state``. + @State private var viewState: ViewState = .idle + public var body: some View { OnboardingView( contentView: { VStack { - OnboardingTitleView( - title: .init("LLM_DOWNLOAD_TITLE", bundle: .atURL(from: .module)), - subtitle: .init("LLM_DOWNLOAD_SUBTITLE", bundle: .atURL(from: .module)) - ) - Spacer() - Image(systemName: "shippingbox") - .font(.system(size: 100)) - .foregroundColor(.accentColor) - .accessibilityHidden(true) - Text("LLM_DOWNLOAD_DESCRIPTION", bundle: .module) - .multilineTextAlignment(.center) - .padding(.vertical, 16) + informationView if !modelExists { downloadButton @@ -42,10 +36,11 @@ public struct LLMLocalDownloadView: View { } } else { Group { - if downloadManager.state != .downloaded { - Text("LLM_ALREADY_DOWNLOADED_DESCRIPTION", bundle: .module) - } else if downloadManager.state == .downloaded { + switch downloadManager.state { + case .downloaded: Text("LLM_DOWNLOADED_DESCRIPTION", bundle: .module) + default: + Text("LLM_ALREADY_DOWNLOADED_DESCRIPTION", bundle: .module) } } .multilineTextAlignment(.center) @@ -65,10 +60,29 @@ public struct LLMLocalDownloadView: View { .disabled(!modelExists) } ) + .map(state: downloadManager.state, to: $viewState) + .viewStateAlert(state: $viewState) .navigationBarBackButtonHidden(isDownloading) } - private var downloadButton: some View { + /// Presents information about the model download. + @MainActor @ViewBuilder private var informationView: some View { + OnboardingTitleView( + title: .init("LLM_DOWNLOAD_TITLE", bundle: .atURL(from: .module)), + subtitle: .init("LLM_DOWNLOAD_SUBTITLE", bundle: .atURL(from: .module)) + ) + Spacer() + Image(systemName: "shippingbox") + .font(.system(size: 100)) + .foregroundColor(.accentColor) + .accessibilityHidden(true) + Text("LLM_DOWNLOAD_DESCRIPTION", bundle: .module) + .multilineTextAlignment(.center) + .padding(.vertical, 16) + } + + /// Button which starts the download of the model. + @MainActor private var downloadButton: some View { Button(action: downloadManager.startDownload) { Text("LLM_DOWNLOAD_BUTTON", bundle: .module) .padding(.horizontal) @@ -79,7 +93,8 @@ public struct LLMLocalDownloadView: View { .padding() } - private var downloadProgressView: some View { + /// A progress view indicating the state of the download + @MainActor private var downloadProgressView: some View { VStack { ProgressView(value: downloadProgress, total: 100.0) { Text("LLM_DOWNLOADING_PROGRESS_TEXT", bundle: .module) @@ -93,7 +108,7 @@ public struct LLMLocalDownloadView: View { } /// A `Bool` flag indicating if the model is currently being downloaded - private var isDownloading: Bool { + @MainActor private var isDownloading: Bool { if case .downloading = self.downloadManager.state { return true } @@ -102,7 +117,7 @@ public struct LLMLocalDownloadView: View { } /// Represents the download progress of the model in percent (from 0 to 100) - private var downloadProgress: Double { + @MainActor private var downloadProgress: Double { if case .downloading(let progress) = self.downloadManager.state { return progress } else if case .downloaded = self.downloadManager.state { @@ -131,7 +146,7 @@ public struct LLMLocalDownloadView: View { llmStorageUrl: URL = .cachesDirectory.appending(path: "llm.gguf"), action: @escaping () async throws -> Void ) { - self._downloadManager = StateObject( + self._downloadManager = State( wrappedValue: LLMLocalDownloadManager( llmDownloadUrl: llmDownloadUrl, llmStorageUrl: llmStorageUrl diff --git a/Sources/SpeziLLMLocalDownload/Resources/Localizable.xcstrings b/Sources/SpeziLLMLocalDownload/Resources/Localizable.xcstrings index eb73544..cca2de1 100644 --- a/Sources/SpeziLLMLocalDownload/Resources/Localizable.xcstrings +++ b/Sources/SpeziLLMLocalDownload/Resources/Localizable.xcstrings @@ -41,6 +41,46 @@ } } }, + "LLM_DOWNLOAD_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "An unexpected error has occurred during downloading the LLM." + } + } + } + }, + "LLM_DOWNLOAD_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The system indicates an unknown download error." + } + } + } + }, + "LLM_DOWNLOAD_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please retry the download of the LLM." + } + } + } + }, + "LLM_DOWNLOAD_FAILED_ERROR" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "An error occurred during the download of the LLM." + } + } + } + }, "LLM_DOWNLOAD_NEXT_BUTTON" : { "localizations" : { "en" : { diff --git a/Sources/SpeziLLMLocalDownload/SpeziLLMLocalDownload.docc/SpeziLLMLocalDownload.md b/Sources/SpeziLLMLocalDownload/SpeziLLMLocalDownload.docc/SpeziLLMLocalDownload.md new file mode 100644 index 0000000..2cb2ca7 --- /dev/null +++ b/Sources/SpeziLLMLocalDownload/SpeziLLMLocalDownload.docc/SpeziLLMLocalDownload.md @@ -0,0 +1,23 @@ +# ``SpeziLLMLocalDownload`` + + + +Provides download and storage functionality for Large Language Models (LLMs). + +## Overview + + + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/SpeziLLMLocalHelpers/SpeziLLMLocalHelpers.docc/SpeziLLMLocalHelpers.md b/Sources/SpeziLLMLocalHelpers/SpeziLLMLocalHelpers.docc/SpeziLLMLocalHelpers.md new file mode 100644 index 0000000..f86940e --- /dev/null +++ b/Sources/SpeziLLMLocalHelpers/SpeziLLMLocalHelpers.docc/SpeziLLMLocalHelpers.md @@ -0,0 +1,19 @@ +# ``SpeziLLMLocalDownload`` + + + +Provides reusable LLM download and storage capabilities. + +## Overview + + + + diff --git a/Sources/SpeziLLMLocalHelpers/tokenize.cpp b/Sources/SpeziLLMLocalHelpers/tokenize.cpp index 026c23c..ab893a7 100644 --- a/Sources/SpeziLLMLocalHelpers/tokenize.cpp +++ b/Sources/SpeziLLMLocalHelpers/tokenize.cpp @@ -10,6 +10,7 @@ #include "tokenize.h" +/// Tokenize a `String` via a given `llama_context`. std::vector llama_tokenize_with_context( const struct llama_context * ctx, const std::string & text, @@ -18,6 +19,7 @@ std::vector llama_tokenize_with_context( return llama_tokenize(ctx, text, add_bos, special); } +/// Tokenize a `String` via a given `llama_model`. std::vector llama_tokenize_with_model( const struct llama_model * model, const std::string & text, diff --git a/Sources/SpeziLLMLocalHelpers/vector.cpp b/Sources/SpeziLLMLocalHelpers/vector.cpp index 071a1bf..72f554d 100644 --- a/Sources/SpeziLLMLocalHelpers/vector.cpp +++ b/Sources/SpeziLLMLocalHelpers/vector.cpp @@ -9,6 +9,7 @@ #include "vector.hpp" +/// Create an empty `vector` of `llama_seq_id`s that serve as a buffer for batch processing. const std::vector getLlamaSeqIdVector() { const std::vector vec = { 0 }; return vec; diff --git a/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md b/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md index 458ae88..90d8c18 100644 --- a/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md +++ b/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md @@ -10,6 +10,8 @@ # --> +Interact with Large Language Models (LLMs) from OpenAI. + ## Overview A module that allows you to interact with GPT-based large language models (LLMs) from OpenAI within your Spezi application.