From 08eda4d34b483642bc88bf9610d7fe176356873d Mon Sep 17 00:00:00 2001 From: Renat Notfullin Date: Mon, 22 Apr 2024 00:26:54 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20of=20response=20str?= =?UTF-8?q?eam=20Add=20gpt-4-turbo=20model=20to=20the=20list=20of=20models?= =?UTF-8?q?.=20Fix=20bug=20when=20ChatGPT=20may=20respond=20with=20generat?= =?UTF-8?q?ed=20chat=20name=20instead=20of=20intended=20answer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macai.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 7 +- macai/Configuration/AppConstants.swift | 1 + macai/UI/Chat/ChatBubbleView.swift | 22 ++- macai/UI/Chat/ChatView.swift | 162 +++++++++++++++++- .../UI/Preferences/TabChatSettingsView.swift | 1 + .../Preferences/TabGeneralSettingsView.swift | 15 ++ 7 files changed, 199 insertions(+), 17 deletions(-) diff --git a/macai.xcodeproj/project.pbxproj b/macai.xcodeproj/project.pbxproj index 340b0dd..0896bd9 100644 --- a/macai.xcodeproj/project.pbxproj +++ b/macai.xcodeproj/project.pbxproj @@ -611,7 +611,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.3.1; + CURRENT_PROJECT_VERSION = 1.4.0; DEVELOPMENT_TEAM = ZRB8WDV435; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -630,7 +630,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = notfullin.com.macai; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -653,7 +653,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.3.1; + CURRENT_PROJECT_VERSION = 1.4.0; DEVELOPMENT_TEAM = ZRB8WDV435; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -672,7 +672,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4.0; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = notfullin.com.macai; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/macai.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macai.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f6b320f..92dd35f 100644 --- a/macai.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macai.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "71519be76e607ad4e7e6a1d261eb9bd084df0cad40df3f9ae9eb024341840c59", "pins" : [ { "identity" : "attributedtext", @@ -32,10 +33,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "631846cc829f0f0cae327df9bafe5a32b7ddadce", - "version" : "2.4.0" + "revision" : "47d3d90aee3c52b6f61d04ceae426e607df62347", + "version" : "2.5.2" } } ], - "version" : 2 + "version" : 3 } diff --git a/macai/Configuration/AppConstants.swift b/macai/Configuration/AppConstants.swift index 9328223..47f9b6a 100644 --- a/macai/Configuration/AppConstants.swift +++ b/macai/Configuration/AppConstants.swift @@ -13,6 +13,7 @@ struct AppConstants { static let chatGptDefaultModel = "gpt-3.5-turbo" static let chatGptContextSize: Double = 10 static let chatGptSystemMessage: String = String(format: "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Knowledge cutoff: 2021-09-01. Current date: %@", getCurrentFormattedDate()) + static let chatGptGenerateChatInstruction: String = "Return a short chat name as summary for this chat based on the previous message content and system message if it's not default. Start chat name with one appropriate emoji. Don't answer to my message, just generate a name." } func getCurrentFormattedDate() -> String { diff --git a/macai/UI/Chat/ChatBubbleView.swift b/macai/UI/Chat/ChatBubbleView.swift index deb1f21..6581b8d 100644 --- a/macai/UI/Chat/ChatBubbleView.swift +++ b/macai/UI/Chat/ChatBubbleView.swift @@ -23,6 +23,7 @@ struct ChatBubbleView: View { @State var waitingForResponse: Bool? @State var error = false @State var initialMessage = false + @State var isStreaming: Bool? @State private var isPencilIconVisible = false @State private var wobbleAmount = 0.0 @Environment(\.colorScheme) var colorScheme @@ -81,7 +82,6 @@ struct ChatBubbleView: View { ForEach(0.. Void) async throws { + let (stream, response) = try await URLSession.shared.bytes(for: request) + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200...299: + #if DEBUG + print("Got successful response code from server") + #endif + case 400...599: + self.waitingForResponse = false + self.lastMessageError = true + return + default: + print("Unhandled status code: \(httpResponse.statusCode)") + return + } + } else { + throw URLError(.badServerResponse) + } + for try await line in stream.lines { + if let lineData = line.data(using: .utf8) { + let prefix = "data: " + var index = line.startIndex + if line.starts(with: prefix) { + index = line.index(line.startIndex, offsetBy: prefix.count) + } + let jsonData = String(line[index...]).trimmingCharacters(in: .whitespacesAndNewlines) + if let jsonData = jsonData.data(using: .utf8) { + processLine(jsonData) // Process the JSON data + self.waitingForResponse = false + } + } + } } + private func generateChatNameIfNeeded() { guard self.chat.name == "", useChatGptForNames, self.chat.messages.count > 0 else { #if DEBUG @@ -188,8 +299,8 @@ extension ChatView { #endif return } - let requestContent = "Return a short chat name as summary for this chat based on the previous message content and system message if it's not default. Start chat name with one appropriate emoji. Don't answer to my message, just generate a name." - let request = prepareRequest(with: requestContent, model: "gpt-3.5-turbo") + let requestContent = AppConstants.chatGptGenerateChatInstruction + let request = prepareRequest(with: requestContent, model: "gpt-3.5-turbo", forceStreamFalse: true) send(using: request) { data, response, error in DispatchQueue.main.async { @@ -203,6 +314,15 @@ extension ChatView { let chatName = messageContent.trimmingCharacters(in: .whitespacesAndNewlines) self.chat.name = chatName self.viewContext.saveWithRetry(attempts: 3) + + // remove 'generate chat name' instruction from requestMessages + #if DEBUG + print("Length of requestMessages before deletion: \(self.chat.requestMessages.count)") + #endif + self.chat.requestMessages = self.chat.requestMessages.filter { $0["content"] != AppConstants.chatGptGenerateChatInstruction } + #if DEBUG + print("Length of requestMessages after deletion: \(self.chat.requestMessages.count)") + #endif } } } @@ -222,7 +342,7 @@ extension ChatView { newMessage = "" } - private func prepareRequest(with messageBody: String, model: String = "") -> URLRequest { + private func prepareRequest(with messageBody: String, model: String = "", forceStreamFalse: Bool = false) -> URLRequest { var request = URLRequest(url: url!) request.httpMethod = "POST" request.setValue("Bearer \(gptToken)", forHTTPHeaderField: "Authorization") @@ -242,6 +362,7 @@ extension ChatView { let jsonDict: [String: Any] = [ "model": (model != "") ? model : gptModel, + "stream": forceStreamFalse ? false : useStream, "messages": Array(chat.requestMessages.prefix(1) + chat.requestMessages.suffix(Int(chatContext) > chat.requestMessages.count - 1 ? chat.requestMessages.count - 1 : Int(chatContext))) ] @@ -320,6 +441,29 @@ extension ChatView { } private func updateUIWithResponse(content: String, role: String) { + if useStream { + let sortedMessages = chat.messages.sorted(by: { $0.timestamp < $1.timestamp }) + if let lastMessage = sortedMessages.last { + if lastMessage.own { + addNewMessageToChat(content: content, role: role) + } else { + lastMessage.body += content + lastMessage.own = false + lastMessage.timestamp = Date() + lastMessage.waitingForResponse = false + // Force the view to update + self.chat.objectWillChange.send() + self.viewContext.saveWithRetry(attempts: 3) + } + } + + } else { + addNewMessageToChat(content: content, role: role) + } + + } + + private func addNewMessageToChat(content: String, role: String) { let receivedMessage = MessageEntity(context: self.viewContext) receivedMessage.id = Int64(self.chat.messages.count + 1) receivedMessage.name = "ChatGPT" diff --git a/macai/UI/Preferences/TabChatSettingsView.swift b/macai/UI/Preferences/TabChatSettingsView.swift index bbb6b3b..d232eb5 100644 --- a/macai/UI/Preferences/TabChatSettingsView.swift +++ b/macai/UI/Preferences/TabChatSettingsView.swift @@ -42,6 +42,7 @@ struct ChatSettingsView: View { Picker("", selection: $selectedGptModel) { Text("gpt-3.5-turbo").tag("gpt-3.5-turbo") Text("gpt-3.5-turbo-0301").tag("gpt-3.5-turbo-0301") + Text("gpt-4-turbo").tag("gpt-4-turbo") Text("gpt-4").tag("gpt-4") Text("gpt-4-0314").tag("gpt-4-0314") Text("gpt-4-32k").tag("gpt-4-32k") diff --git a/macai/UI/Preferences/TabGeneralSettingsView.swift b/macai/UI/Preferences/TabGeneralSettingsView.swift index 34c7769..406927f 100644 --- a/macai/UI/Preferences/TabGeneralSettingsView.swift +++ b/macai/UI/Preferences/TabGeneralSettingsView.swift @@ -12,6 +12,7 @@ struct GeneralSettingsView: View { @AppStorage("apiUrl") var apiUrl: String = AppConstants.apiUrlChatCompletions @AppStorage("chatContext") var chatContext: Double = AppConstants.chatGptContextSize @AppStorage("useChatGptForNames") var useChatGptForNames: Bool = false + @AppStorage("useStream") var useStream: Bool = true @Binding var lampColor: Color @FocusState private var isFocused: Bool @@ -107,6 +108,20 @@ struct GeneralSettingsView: View { .buttonStyle(PlainButtonStyle()) .help("Chat name will be generated based on chat messages. To reduce API costs, model chat-gpt-3.5-turbo will be used for this purpose.") + Spacer() + } + } + Toggle(isOn: $useStream) { + HStack { + Text("Use stream responses") + Button(action: { + }) { + Image(systemName: "questionmark.circle") + .foregroundColor(.blue) + } + .buttonStyle(PlainButtonStyle()) + .help("If on, the ChatGPT response will be streamed to the client. This will allow you to see the response in real-time. If off, the response will be sent to the client only after the model has finished processing.") + Spacer() } }