Skip to content

Commit

Permalink
Merge branch 'MacPaw' into MacPaw.streamable_symmetry
Browse files Browse the repository at this point in the history
  • Loading branch information
James J Kalafus committed Feb 15, 2024
2 parents 04ff962 + 224f4ef commit 2d4e9d5
Show file tree
Hide file tree
Showing 35 changed files with 1,660 additions and 622 deletions.
32 changes: 16 additions & 16 deletions Demo/DemoChat/Sources/ChatStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public final class ChatStore: ObservableObject {
return
}

let weatherFunction = ChatFunctionDeclaration(
let weatherFunction = ChatQuery.ChatCompletionToolParam(function: .init(
name: "getWeatherData",
description: "Get the current weather in a given location",
parameters: .init(
Expand All @@ -95,38 +95,38 @@ public final class ChatStore: ObservableObject {
],
required: ["location"]
)
)
))

let functions = [weatherFunction]

let chatsStream: AsyncThrowingStream<ChatStreamResult, Error> = openAIClient.chatsStream(
query: ChatQuery(
model: model,
messages: conversation.messages.map { message in
Chat(role: message.role, content: message.content)
},
functions: functions
ChatQuery.ChatCompletionMessageParam(role: message.role, content: message.content)!
}, model: model,
tools: functions
)
)

var functionCallName = ""
var functionCallArguments = ""
var functionCalls = [(name: String, argument: String?)]()
for try await partialChatResult in chatsStream {
for choice in partialChatResult.choices {
let existingMessages = conversations[conversationIndex].messages
// Function calls are also streamed, so we need to accumulate.
if let functionCallDelta = choice.delta.functionCall {
if let nameDelta = functionCallDelta.name {
functionCallName += nameDelta
}
if let argumentsDelta = functionCallDelta.arguments {
functionCallArguments += argumentsDelta
choice.delta.toolCalls?.forEach { toolCallDelta in
if let functionCallDelta = toolCallDelta.function {
if let nameDelta = functionCallDelta.name {
functionCalls.append((nameDelta, functionCallDelta.arguments))
}
}
}
var messageText = choice.delta.content ?? ""
if let finishReason = choice.finishReason,
finishReason == "function_call" {
messageText += "Function call: name=\(functionCallName) arguments=\(functionCallArguments)"
finishReason == .toolCalls
{
functionCalls.forEach { (name: String, argument: String?) in
messageText += "Function call: name=\(name) arguments=\(argument ?? "")\n"
}
}
let message = Message(
id: partialChatResult.id,
Expand Down
65 changes: 65 additions & 0 deletions Demo/DemoChat/Sources/Extensions/View.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// View.swift
//
//
// Created by James J Kalafus on 2024-02-03.
//

import SwiftUI

extension View {

@inlinable public func navigationTitle(_ titleKey: LocalizedStringKey, selectedModel: Binding<String>) -> some View {
self
.navigationTitle(titleKey)
.safeAreaInset(edge: .top) {
HStack {
Text(
"Model: \(selectedModel.wrappedValue)"
)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
}

@inlinable public func modelSelect(selectedModel: Binding<String>, models: [String], showsModelSelectionSheet: Binding<Bool>, help: String) -> some View {
self
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showsModelSelectionSheet.wrappedValue.toggle()
}) {
Image(systemName: "cpu")
}
}
}
.confirmationDialog(
"Select model",
isPresented: showsModelSelectionSheet,
titleVisibility: .visible,
actions: {
ForEach(models, id: \.self) { (model: String) in
Button {
selectedModel.wrappedValue = model
} label: {
Text(model)
}
}

Button("Cancel", role: .cancel) {
showsModelSelectionSheet.wrappedValue = false
}
},
message: {
Text(
"View \(help) for details"
)
.font(.caption)
}
)
}
}
2 changes: 1 addition & 1 deletion Demo/DemoChat/Sources/ImageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import OpenAI
public final class ImageStore: ObservableObject {
public var openAIClient: OpenAIProtocol

@Published var images: [ImagesResult.URLResult] = []
@Published var images: [ImagesResult.Image] = []

public init(
openAIClient: OpenAIProtocol
Expand Down
2 changes: 1 addition & 1 deletion Demo/DemoChat/Sources/MiscStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public final class MiscStore: ObservableObject {
do {
let response = try await openAIClient.moderations(
query: ModerationsQuery(
input: message.content,
input: .init(message.content),
model: .textModerationLatest
)
)
Expand Down
2 changes: 1 addition & 1 deletion Demo/DemoChat/Sources/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import OpenAI

struct Message {
var id: String
var role: Chat.Role
var role: ChatQuery.ChatCompletionMessageParam.Role
var content: String
var createdAt: Date
}
Expand Down
7 changes: 4 additions & 3 deletions Demo/DemoChat/Sources/SpeechStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ public final class SpeechStore: ObservableObject {

@MainActor
func createSpeech(_ query: AudioSpeechQuery) async {
guard let input = query.input, !input.isEmpty else { return }
let input = query.input
guard !input.isEmpty else { return }
do {
let response = try await openAIClient.audioCreateSpeech(query: query)
guard let data = response.audioData else { return }
let data = response.audio
let player = try? AVAudioPlayer(data: data)
let audioObject = AudioObject(prompt: input,
audioPlayer: player,
originResponse: response,
format: query.responseFormat.rawValue)
format: query.responseFormat?.rawValue ?? AudioSpeechQuery.AudioSpeechResponseFormat.mp3.rawValue)
audioObjects.append(audioObject)
} catch {
print(error.localizedDescription)
Expand Down
54 changes: 5 additions & 49 deletions Demo/DemoChat/Sources/UI/DetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct DetailView: View {
@State private var showsModelSelectionSheet = false
@State private var selectedChatModel: Model = .gpt4_0613

private let availableChatModels: [Model] = [.gpt3_5Turbo0613, .gpt4_0613]
private static let availableChatModels: [Model] = [.gpt3_5Turbo, .gpt4]

let conversation: Conversation
let error: Error?
Expand Down Expand Up @@ -65,52 +65,8 @@ struct DetailView: View {

inputBar(scrollViewProxy: scrollViewProxy)
}
.navigationTitle("Chat")
.safeAreaInset(edge: .top) {
HStack {
Text(
"Model: \(selectedChatModel)"
)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showsModelSelectionSheet.toggle()
}) {
Image(systemName: "cpu")
}
}
}
.confirmationDialog(
"Select model",
isPresented: $showsModelSelectionSheet,
titleVisibility: .visible,
actions: {
ForEach(availableChatModels, id: \.self) { model in
Button {
selectedChatModel = model
} label: {
Text(model)
}
}

Button("Cancel", role: .cancel) {
showsModelSelectionSheet = false
}
},
message: {
Text(
"View https://platform.openai.com/docs/models/overview for details"
)
.font(.caption)
}
)
.navigationTitle("Chat", selectedModel: $selectedChatModel)
.modelSelect(selectedModel: $selectedChatModel, models: Self.availableChatModels, showsModelSelectionSheet: $showsModelSelectionSheet, help: "https://platform.openai.com/docs/models/overview")
}
}
}
Expand Down Expand Up @@ -243,7 +199,7 @@ struct ChatBubble: View {
.foregroundColor(userForegroundColor)
.background(userBackgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
case .function:
case .tool:
Text(message.content)
.font(.footnote.monospaced())
.padding(.horizontal, 16)
Expand All @@ -267,7 +223,7 @@ struct DetailView_Previews: PreviewProvider {
Message(id: "1", role: .assistant, content: "Hello, how can I help you today?", createdAt: Date(timeIntervalSinceReferenceDate: 0)),
Message(id: "2", role: .user, content: "I need help with my subscription.", createdAt: Date(timeIntervalSinceReferenceDate: 100)),
Message(id: "3", role: .assistant, content: "Sure, what seems to be the problem with your subscription?", createdAt: Date(timeIntervalSinceReferenceDate: 200)),
Message(id: "4", role: .function, content:
Message(id: "4", role: .tool, content:
"""
get_current_weather({
"location": "Glasgow, Scotland",
Expand Down
10 changes: 5 additions & 5 deletions Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public struct ImageCreationView: View {

@State private var prompt: String = ""
@State private var n: Int = 1
@State private var size: String
private var sizes = ["256x256", "512x512", "1024x1024"]
@State private var size = ImagesQuery.Size.allCases.first!

private var sizes = ImagesQuery.Size.allCases

public init(store: ImageStore) {
self.store = store
Expand All @@ -37,7 +37,7 @@ public struct ImageCreationView: View {
HStack {
Picker("Size", selection: $size) {
ForEach(sizes, id: \.self) {
Text($0)
Text($0.rawValue)
}
}
}
Expand All @@ -56,7 +56,7 @@ public struct ImageCreationView: View {
}
if !$store.images.isEmpty {
Section("Images") {
ForEach($store.images, id: \.self) { image in
ForEach($store.images, id: \.url) { image in
let urlString = image.wrappedValue.url ?? ""
if let imageURL = URL(string: urlString), UIApplication.shared.canOpenURL(imageURL) {
LinkPreview(previewURL: imageURL)
Expand Down
2 changes: 1 addition & 1 deletion Demo/DemoChat/Sources/UI/Misc/ListModelsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public struct ListModelsView: View {

public var body: some View {
NavigationStack {
List($store.availableModels) { row in
List($store.availableModels.wrappedValue, id: \.id) { row in
Text(row.id)
}
.listStyle(.insetGrouped)
Expand Down
19 changes: 12 additions & 7 deletions Demo/DemoChat/Sources/UI/TextToSpeechView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ public struct TextToSpeechView: View {

@State private var prompt: String = ""
@State private var voice: AudioSpeechQuery.AudioSpeechVoice = .alloy
@State private var speed: Double = 1
@State private var speed: Double = AudioSpeechQuery.Speed.normal.rawValue
@State private var responseFormat: AudioSpeechQuery.AudioSpeechResponseFormat = .mp3

@State private var showsModelSelectionSheet = false
@State private var selectedSpeechModel: String = Model.tts_1

private static let availableSpeechModels: [String] = [Model.tts_1, Model.tts_1_hd]

public init(store: SpeechStore) {
self.store = store
}
Expand Down Expand Up @@ -56,7 +60,7 @@ public struct TextToSpeechView: View {
HStack {
Text("Speed: ")
Spacer()
Stepper(value: $speed, in: 0.25...4, step: 0.25) {
Stepper(value: $speed, in: AudioSpeechQuery.Speed.min.rawValue...AudioSpeechQuery.Speed.max.rawValue, step: 0.25) {
HStack {
Spacer()
Text("**\(String(format: "%.2f", speed))**")
Expand All @@ -79,7 +83,7 @@ public struct TextToSpeechView: View {
Section {
HStack {
Button("Create Speech") {
let query = AudioSpeechQuery(model: .tts_1,
let query = AudioSpeechQuery(model: selectedSpeechModel,
input: prompt,
voice: voice,
responseFormat: responseFormat,
Expand All @@ -93,10 +97,11 @@ public struct TextToSpeechView: View {
.disabled(prompt.replacingOccurrences(of: " ", with: "").isEmpty)
Spacer()
}
.modelSelect(selectedModel: $selectedSpeechModel, models: Self.availableSpeechModels, showsModelSelectionSheet: $showsModelSelectionSheet, help: "https://platform.openai.com/docs/models/tts")
}
if !$store.audioObjects.wrappedValue.isEmpty {
Section("Click to play, swipe to save:") {
ForEach(store.audioObjects) { object in
ForEach(store.audioObjects, id: \.id) { object in
HStack {
Text(object.prompt.capitalized)
Spacer()
Expand All @@ -117,7 +122,7 @@ public struct TextToSpeechView: View {
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
presentUserDirectoryDocumentPicker(for: object.originResponse.audioData, filename: "GeneratedAudio.\(object.format)")
presentUserDirectoryDocumentPicker(for: object.originResponse.audio, filename: "GeneratedAudio.\(object.format)")
} label: {
Image(systemName: "square.and.arrow.down")
}
Expand All @@ -129,7 +134,7 @@ public struct TextToSpeechView: View {
}
.listStyle(.insetGrouped)
.scrollDismissesKeyboard(.interactively)
.navigationTitle("Create Speech")
.navigationTitle("Create Speech", selectedModel: $selectedSpeechModel)
}
}

Expand Down
Loading

0 comments on commit 2d4e9d5

Please sign in to comment.