Skip to content

Commit

Permalink
Update to Spezi 0.8.0 (#37)
Browse files Browse the repository at this point in the history
# Update to Spezi 0.8.0

## ⚙️ Release Notes 
- Small maintenance PR to update the Package to Spezi 0.8.0


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
PSchmiedmayer authored Nov 12, 2023
1 parent 769ad38 commit b71b4a3
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 134 deletions.
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
name: "SpeziML",
defaultLocalization: "en",
platforms: [
.iOS(.v16)
.iOS(.v17)
],
products: [
.library(name: "SpeziOpenAI", targets: ["SpeziOpenAI"]),
Expand All @@ -24,9 +24,9 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/MacPaw/OpenAI", .upToNextMinor(from: "0.2.4")),
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.4.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziOnboarding", .upToNextMinor(from: "0.6.0"))
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.5.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziOnboarding", .upToNextMinor(from: "0.7.0"))
],
targets: [
.target(
Expand Down
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ First, you will need to add the SpeziML Swift package to
[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or
[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). When adding the package, select the `SpeziOpenAI` target to add.

### 2. Register the Open AI Component
### 2. Register the Open AI Module

> [!IMPORTANT]
> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure.
You can configure the `OpenAIComponent` in the `SpeziAppDelegate` as follows.
You can configure the `OpenAIModule` in the `SpeziAppDelegate` as follows.
In the example, we configure the `OpenAIModule` to use the GPT-4 model with a default API key.

```swift
import Spezi
Expand All @@ -53,18 +54,32 @@ import SpeziOpenAI
class ExampleDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration {
OpenAIComponent(apiToken: "API_KEY", openAIModel: .gpt4)
OpenAIModule(apiToken: "API_KEY", openAIModel: .gpt4)
}
}
}
```

In the example above, we have configured the `OpenAIComponent` to use the GPT-4 model with a default API key. Note that the choice of model and API key are persisted across application launches. The `apiToken` and `openAIModel` can also be accessed and changed at runtime.
The OpenAIModule injects an ``OpenAIModel`` in the SwiftUI environment to make it accessible thoughout your application.

```swift
class ExampleView: View {
@Environment(OpenAIModel.self) var model


var body: some View {
// ...
}
}
```

> [!NOTE]
> The choice of model and API key are persisted across application launches. The `apiToken` and `openAIModel` can also be accessed and changed at runtime.
The `SpeziOpenAI` package also provides an `OpenAIAPIKeyOnboardingStep` that can be used to allow the user to provide their API key during the onboarding process instead (see `Examples` below). If using the `OpenAIAPIKeyOnboardingStep`, the `apiToken` property can be omitted here.

> [!NOTE]
> You can learn more about a [`Component` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component).
> You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module).
## Examples

Expand All @@ -78,13 +93,13 @@ import SpeziOpenAI
import SwiftUI

struct OpenAIChatView: View {
@EnvironmentObject private var openAIComponent: OpenAIComponent
@Environment(OpenAIModel.self) var model
@State private var chat: [Chat]

var body: some View {
ChatView($chat)
.onChange(of: chat) { _ in
let chatStreamResults = try await openAIComponent.queryAPI(withChat: chat)
let chatStreamResults = try await model.queryAPI(withChat: chat)

for try await chatStreamResult in chatStreamResults {
for choice in chatStreamResult.choices {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziOpenAI/MessageInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public struct MessageInputView: View {
.onAppear {
messageViewHeight = proxy.size.height
}
.onChange(of: message) { _ in
.onChange(of: message) {
messageViewHeight = proxy.size.height
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziOpenAI/MessagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public struct MessagesView: View {
.onAppear {
scrollToBottom(scrollViewProxy)
}
.onChange(of: chat) { _ in
.onChange(of: chat) {
scrollToBottom(scrollViewProxy)
}
.onReceive(keyboardPublisher) { _ in
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziOpenAI/OpenAIAPIKeyOnboardingStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftUI

/// View to display an onboarding step for the user to enter an OpenAI API Key.
public struct OpenAIAPIKeyOnboardingStep: View {
@EnvironmentObject private var openAI: OpenAIComponent
@Environment(OpenAIModel.self) private var openAI
private let actionText: String
private let action: () -> Void

Expand Down Expand Up @@ -64,7 +64,7 @@ public struct OpenAIAPIKeyOnboardingStep: View {
},
actionView: {
OnboardingActionsView(
actionText,
verbatim: actionText,
action: {
action()
}
Expand Down
94 changes: 0 additions & 94 deletions Sources/SpeziOpenAI/OpenAIComponent.swift

This file was deleted.

101 changes: 101 additions & 0 deletions Sources/SpeziOpenAI/OpenAIModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// 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 OpenAI
@_exported import struct OpenAI.Model
@_exported import struct OpenAI.ChatStreamResult
import Foundation
import Observation
import SpeziSecureStorage


/// View model responsible for to coordinate the interactions with the OpenAI GPT API.
@Observable
public class OpenAIModel {
private enum Defaults {
static let defaultModel: Model = .gpt3_5Turbo
}


private let secureStorage: SecureStorage


/// The OpenAI GPT Model type that is used to interact with the OpenAI API
public var openAIModel: String {
get {
access(keyPath: \.openAIModel)
return UserDefaults.standard.value(forKey: OpenAIConstants.modelStorageKey) as? Model ?? Defaults.defaultModel
}
set {
withMutation(keyPath: \.openAIModel) {
UserDefaults.standard.set(newValue, forKey: OpenAIConstants.modelStorageKey)
}
}
}

/// The API token used to interact with the OpenAI API
public var apiToken: String? {
get {
access(keyPath: \.apiToken)
return try? secureStorage.retrieveCredentials(OpenAIConstants.credentialsUsername, server: OpenAIConstants.credentialsServer)?.password
}
set {
withMutation(keyPath: \.apiToken) {
if let newValue {
try? secureStorage.store(
credentials: Credentials(username: OpenAIConstants.credentialsUsername, password: newValue),
server: OpenAIConstants.credentialsServer
)
} else {
try? secureStorage.deleteCredentials(OpenAIConstants.credentialsUsername, server: OpenAIConstants.credentialsServer)
}
}
}
}


init(secureStorage: SecureStorage, apiToken defaultToken: String? = nil, openAIModel model: Model? = nil) {
self.secureStorage = secureStorage

if UserDefaults.standard.object(forKey: OpenAIConstants.modelStorageKey) == nil {
self.openAIModel = model ?? Defaults.defaultModel
}

if let apiTokenFromStorage = try? secureStorage.retrieveCredentials(
OpenAIConstants.credentialsUsername,
server: OpenAIConstants.credentialsServer
)?.password {
self.apiToken = apiTokenFromStorage
} else {
self.apiToken = defaultToken
}
}


/// Queries the OpenAI API using the provided messages.
///
/// - Parameters:
/// - chat: A collection of chat messages used in the conversation.
/// - chatFunctionDeclaration: OpenAI functions that should be injected in the OpenAI query.
///
/// - Returns: The content of the response from the API.
public func queryAPI(
withChat chat: [Chat],
withFunction chatFunctionDeclaration: [ChatFunctionDeclaration] = []
) async throws -> AsyncThrowingStream<ChatStreamResult, Error> {
guard let apiToken, !apiToken.isEmpty else {
throw OpenAIError.noAPIToken
}

let functions = chatFunctionDeclaration.isEmpty ? nil : chatFunctionDeclaration

let openAIClient = OpenAI(apiToken: apiToken)
let query = ChatQuery(model: openAIModel, messages: chat, functions: functions)
return openAIClient.chatsStream(query: query)
}
}
5 changes: 3 additions & 2 deletions Sources/SpeziOpenAI/OpenAIModelSelectionOnboardingStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct OpenAIModelSelectionOnboardingStep: View {
}


@EnvironmentObject private var openAI: OpenAIComponent
@Environment(OpenAIModel.self) private var openAI
private let actionText: String
private let action: () -> Void
private let models: [ModelSelection]
Expand All @@ -43,6 +43,7 @@ public struct OpenAIModelSelectionOnboardingStep: View {
)
},
contentView: {
@Bindable var openAI = openAI
Picker(String(localized: "OPENAI_MODEL_SELECTION_DESCRIPTION", bundle: .module), selection: $openAI.openAIModel) {
ForEach(models) { model in
Text(model.description)
Expand All @@ -54,7 +55,7 @@ public struct OpenAIModelSelectionOnboardingStep: View {
},
actionView: {
OnboardingActionsView(
actionText,
verbatim: actionText,
action: {
action()
}
Expand Down
43 changes: 43 additions & 0 deletions Sources/SpeziOpenAI/OpenAIModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// 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 OpenAI
import Spezi
import SpeziSecureStorage


/// `OpenAIModule` is a module responsible for to coordinate the interactions with the OpenAI GPT API.
public class OpenAIModule: Module, DefaultInitializable {
@Module.Model private var model: OpenAIModel
@Dependency private var secureStorage: SecureStorage


private var defaultAPIToken: String?
private var defaultOpenAIModel: Model?


/// Initializes a new instance of `OpenAIGPT` with the specified API token and OpenAI model.
///
/// - Parameters:
/// - apiToken: The API token for the OpenAI API.
/// - openAIModel: The OpenAI model to use for querying.
public init(apiToken: String? = nil, openAIModel: Model? = nil) {
defaultAPIToken = apiToken
defaultOpenAIModel = openAIModel
}

public required convenience init() {
self.init(apiToken: nil, openAIModel: nil)
}


public func configure() {
self.model = OpenAIModel(secureStorage: secureStorage, apiToken: defaultAPIToken, openAIModel: defaultOpenAIModel)
}
}
Loading

0 comments on commit b71b4a3

Please sign in to comment.