diff --git a/AppIcon.png b/AppIcon.png index 592f927b..160db273 100644 Binary files a/AppIcon.png and b/AppIcon.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png index 5af40630..291eaac7 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png index 592f927b..160db273 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png index 09658ce1..4fcd6278 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png index 4a7d88ed..e31a8d3b 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png index 4a7d88ed..e31a8d3b 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png index 6c4293c8..ec264755 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png index 6c4293c8..ec264755 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png index db461044..4b760bc1 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png index db461044..4b760bc1 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png index 42d7a164..8d777985 100644 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png and b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png differ diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift index 118b1473..e1952740 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift @@ -45,7 +45,11 @@ func search(_ query: String) async throws let chatModel = OpenAIChat(temperature: 0, stream: true) let agentExecutor = AgentExecutor( - agent: ChatAgent(chatModel: chatModel, tools: tools), + agent: ChatAgent( + chatModel: chatModel, + tools: tools, + preferredLanguage: UserDefaults.shared.value(for: \.chatGPTLanguage) + ), tools: tools, maxIteration: UserDefaults.shared.value(for: \.chatSearchPluginMaxIterations), earlyStopHandleType: .generate diff --git a/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift new file mode 100644 index 00000000..d30b5ce8 --- /dev/null +++ b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift @@ -0,0 +1,127 @@ +import ChatPlugin +import Environment +import Foundation +import OpenAIService +import Parsing +import Terminal + +public actor ShortcutInputChatPlugin: ChatPlugin { + public static var command: String { "shortcutInput" } + public nonisolated var name: String { "Shortcut Input" } + + let chatGPTService: any ChatGPTServiceType + var terminal: TerminalType = Terminal() + var isCancelled = false + weak var delegate: ChatPluginDelegate? + + public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) { + self.chatGPTService = chatGPTService + self.delegate = delegate + } + + public func send(content: String, originalMessage: String) async { + delegate?.pluginDidStart(self) + delegate?.pluginDidStartResponding(self) + + defer { + delegate?.pluginDidEndResponding(self) + delegate?.pluginDidEnd(self) + } + + let id = "\(Self.command)-\(UUID().uuidString)" + + var content = content[...] + let firstParenthesisParser = PrefixThrough("(") + let shortcutNameParser = PrefixUpTo(")") + + _ = try? firstParenthesisParser.parse(&content) + let shortcutName = try? shortcutNameParser.parse(&content) + _ = try? PrefixThrough(")").parse(&content) + + guard let shortcutName, !shortcutName.isEmpty else { + let id = "\(Self.command)-\(UUID().uuidString)" + let reply = ChatMessage( + id: id, + role: .assistant, + content: "Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`." + ) + await chatGPTService.mutateHistory { history in + history.append(reply) + } + return + } + + var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines) + if input.isEmpty { + // if no input detected, use the previous message as input + input = await chatGPTService.history.last?.content ?? "" + } + + do { + if isCancelled { throw CancellationError() } + + let env = ProcessInfo.processInfo.environment + let shell = env["SHELL"] ?? "/bin/bash" + let temporaryURL = FileManager.default.temporaryDirectory + let temporaryInputFileURL = temporaryURL + .appendingPathComponent("\(id)-input.txt") + let temporaryOutputFileURL = temporaryURL + .appendingPathComponent("\(id)-output") + + try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8) + + let command = """ + shortcuts run "\(shortcutName)" \ + -i "\(temporaryInputFileURL.path)" \ + -o "\(temporaryOutputFileURL.path)" + """ + + _ = try await terminal.runCommand( + shell, + arguments: ["-i", "-l", "-c", command], + currentDirectoryPath: "/", + environment: [:] + ) + + await Task.yield() + + if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) { + let data = try Data(contentsOf: temporaryOutputFileURL) + if let text = String(data: data, encoding: .utf8) { + if text.isEmpty { return } + let stream = try await chatGPTService.send(content: text, summary: nil) + for try await _ in stream {} + } else { + let text = """ + [View File](\(temporaryOutputFileURL)) + """ + let stream = try await chatGPTService.send(content: text, summary: nil) + for try await _ in stream {} + } + + return + } + } catch { + let id = "\(Self.command)-\(UUID().uuidString)" + let reply = ChatMessage( + id: id, + role: .assistant, + content: error.localizedDescription + ) + await chatGPTService.mutateHistory { history in + history.append(reply) + } + } + } + + public func cancel() async { + isCancelled = true + await terminal.terminate() + } + + public func stopResponding() async { + isCancelled = true + await terminal.terminate() + } +} + diff --git a/Core/Sources/ChatService/AllPlugins.swift b/Core/Sources/ChatService/AllPlugins.swift index efb4bfd5..108200a2 100644 --- a/Core/Sources/ChatService/AllPlugins.swift +++ b/Core/Sources/ChatService/AllPlugins.swift @@ -9,5 +9,6 @@ let allPlugins: [ChatPlugin.Type] = [ MathChatPlugin.self, SearchChatPlugin.self, ShortcutChatPlugin.self, + ShortcutInputChatPlugin.self, ] diff --git a/Core/Sources/ChatService/ChatPluginController.swift b/Core/Sources/ChatService/ChatPluginController.swift index de65d4e9..9825ec8e 100644 --- a/Core/Sources/ChatService/ChatPluginController.swift +++ b/Core/Sources/ChatService/ChatPluginController.swift @@ -7,6 +7,7 @@ final class ChatPluginController { let chatGPTService: any ChatGPTServiceType let plugins: [String: ChatPlugin.Type] var runningPlugin: ChatPlugin? + weak var chatService: ChatService? init(chatGPTService: any ChatGPTServiceType, plugins: [ChatPlugin.Type]) { self.chatGPTService = chatGPTService @@ -107,15 +108,11 @@ final class ChatPluginController { extension ChatPluginController: ChatPluginDelegate { public func pluginDidStartResponding(_: ChatPlugin) { - Task { - await chatGPTService.markReceivingMessage(true) - } + chatService?.isReceivingMessage = true } public func pluginDidEndResponding(_: ChatPlugin) { - Task { - await chatGPTService.markReceivingMessage(false) - } + chatService?.isReceivingMessage = false } public func pluginDidStart(_ plugin: ChatPlugin) { diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 048c459b..d482d458 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -6,9 +6,11 @@ import OpenAIService public final class ChatService: ObservableObject { public let chatGPTService: any ChatGPTServiceType + public var allPluginCommands: [String] { allPlugins.map { $0.command } } let pluginController: ChatPluginController let contextController: DynamicContextController var cancellable = Set() + @Published public internal(set) var isReceivingMessage = false @Published public internal(set) var systemPrompt = UserDefaults.shared .value(for: \.defaultChatSystemPrompt) @Published public internal(set) var extraSystemPrompt = "" @@ -21,12 +23,14 @@ public final class ChatService: ObservableObject { contextCollectors: ActiveDocumentChatContextCollector() ) + pluginController.chatService = self chatGPTService.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() }.store(in: &cancellable) } public func send(content: String) async throws { + guard !isReceivingMessage else { throw CancellationError() } let handledInPlugin = try await pluginController.handleContent(content) if handledInPlugin { return } try await contextController.updatePromptToMatchContent(systemPrompt: """ @@ -34,17 +38,22 @@ public final class ChatService: ObservableObject { \(extraSystemPrompt) """, content: content) - _ = try await chatGPTService.send(content: content, summary: nil) + let stream = try await chatGPTService.send(content: content, summary: nil) + isReceivingMessage = true + for try await _ in stream {} + isReceivingMessage = false } public func stopReceivingMessage() async { await pluginController.stopResponding() await chatGPTService.stopReceivingMessage() + isReceivingMessage = false } public func clearHistory() async { await pluginController.cancel() await chatGPTService.clearHistory() + isReceivingMessage = false } public func resetPrompt() async { diff --git a/Core/Sources/Service/GUI/ChatProvider+Service.swift b/Core/Sources/Service/GUI/ChatProvider+Service.swift index 0e157111..5dc13f3b 100644 --- a/Core/Sources/Service/GUI/ChatProvider+Service.swift +++ b/Core/Sources/Service/GUI/ChatProvider+Service.swift @@ -11,7 +11,7 @@ extension ChatProvider { onCloseChat: @escaping () -> Void, onSwitchContext: @escaping () -> Void ) { - self.init() + self.init(pluginIdentifiers: service.allPluginCommands) let cancellable = service.objectWillChange.sink { [weak self] in guard let self else { return } @@ -23,7 +23,7 @@ extension ChatProvider { text: message.summary ?? message.content ) } - self.isReceivingMessage = await service.chatGPTService.isReceivingMessage + self.isReceivingMessage = service.isReceivingMessage self.systemPrompt = service.systemPrompt self.extraSystemPrompt = service.extraSystemPrompt } diff --git a/Core/Sources/SuggestionWidget/ChatProvider.swift b/Core/Sources/SuggestionWidget/ChatProvider.swift index 11895239..33102223 100644 --- a/Core/Sources/SuggestionWidget/ChatProvider.swift +++ b/Core/Sources/SuggestionWidget/ChatProvider.swift @@ -7,6 +7,7 @@ public final class ChatProvider: ObservableObject { let id = UUID() @Published public var history: [ChatMessage] = [] @Published public var isReceivingMessage = false + public var pluginIdentifiers: [String] = [] public var systemPrompt = "" public var extraSystemPrompt = "" public var onMessageSend: (String) -> Void @@ -23,6 +24,7 @@ public final class ChatProvider: ObservableObject { public init( history: [ChatMessage] = [], isReceivingMessage: Bool = false, + pluginIdentifiers: [String] = [], onMessageSend: @escaping (String) -> Void = { _ in }, onStop: @escaping () -> Void = {}, onClear: @escaping () -> Void = {}, @@ -36,6 +38,7 @@ public final class ChatProvider: ObservableObject { ) { self.history = history self.isReceivingMessage = isReceivingMessage + self.pluginIdentifiers = pluginIdentifiers self.onMessageSend = onMessageSend self.onStop = onStop self.onClear = onClear @@ -59,6 +62,7 @@ public final class ChatProvider: ObservableObject { public func triggerCustomCommand(_ command: CustomCommand) { onRunCustomCommand(command) } + public func setAsExtraPrompt(id: MessageID) { onSetAsExtraPrompt(id) } } diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index 14031bf5..69d5d8ff 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -355,29 +355,7 @@ struct ChatPanelInputArea: View { text: $typedMessage, font: .systemFont(ofSize: 14), onSubmit: { submitText() }, - completions: { text, _, range in - if text.isEmpty { return [] } - let availableFeatures = [ - "/run", - "/airun", - "/math", - "/search", - "/shortcut", - "/exit", - "@selection", - "@file", - ] - return availableFeatures - .filter { $0.hasPrefix(text) && $0 != text } - .compactMap { - guard let index = $0.index( - $0.startIndex, - offsetBy: range.location, - limitedBy: $0.endIndex - ) else { return nil } - return String($0[index...]) - } - } + completions: chatAutoCompletion ) .padding(.top, 1) .padding(.bottom, -1) @@ -420,6 +398,28 @@ struct ChatPanelInputArea: View { chat.send(typedMessage) typedMessage = "" } + + func chatAutoCompletion(text: String, proposed: [String], range: NSRange) -> [String] { + guard text.count == 1 else { return [] } + let plugins = chat.pluginIdentifiers.map { "/\($0)" } + let availableFeatures = plugins + [ + "/exit", + "@selection", + "@file", + ] + + let result: [String] = availableFeatures + .filter { $0.hasPrefix(text) && $0 != text } + .compactMap { + guard let index = $0.index( + $0.startIndex, + offsetBy: range.location, + limitedBy: $0.endIndex + ) else { return nil } + return String($0[index...]) + } + return result + } } struct ChatContextMenu: View { @@ -700,3 +700,6 @@ struct ChatPanel_Light_Preview: PreviewProvider { } } + + + diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png index 5af40630..291eaac7 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png index 592f927b..160db273 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@128w.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png index 09658ce1..4fcd6278 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@16w.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png index 4a7d88ed..e31a8d3b 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w 1.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png index 4a7d88ed..e31a8d3b 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@256w.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png index 6c4293c8..ec264755 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w 1.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png index 6c4293c8..ec264755 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@32w.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png index db461044..4b760bc1 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w 1.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png index db461044..4b760bc1 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@512w.png differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png index 42d7a164..8d777985 100644 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png and b/ExtensionService/Assets.xcassets/AppIcon.appiconset/1024 x 1024 your icon@64w.png differ diff --git a/README.md b/README.md index cefeb04b..b111744b 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,8 @@ If you need to end a plugin, you can just type | `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | | `/math` | Solves a math problem in natural language | | `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | -| `/shortcut(shortcut name)` | Run a shortcut from the Shortcuts.app, and use the following message as the input. If the message is empty, it will use the previous message as input. The output of the shortcut will be printed as a reply from the bot | +| `/shortcut(shortcut name)` | Run a shortcut from the Shortcuts.app, and use the following message as the input. If the message is empty, it will use the previous message as input. The output of the shortcut will be printed as a reply from the bot. | +| `/shortcutInput(shortcut name)` | Run a shortcut from the Shortcuts.app, and use the following message as the input. If the message is empty, it will use the previous message as input. The output of the shortcut will be send to the bot as a user message. | ### Prompt to Code diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 559c80fc..242416a2 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -107,7 +107,11 @@ public extension Agent { ) case .generate: var thoughts = constructBaseScratchpad(intermediateSteps: intermediateSteps) - thoughts += "\n\n\(llmPrefix)I now need to return a final answer based on the previous steps:" + thoughts += """ + + \(llmPrefix)I now need to return a final answer based on the previous steps: + (Please continue with `Final Answer:`) + """ let input = AgentInput(input: input, thoughts: .text(thoughts)) let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) let nextAction = parseOutput(output) diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift index 48a04869..0196e1ac 100644 --- a/Tool/Sources/LangChain/Agents/ChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -2,7 +2,7 @@ import Foundation import Logger import Parsing -private func formatInstruction(toolsNames: String) -> String { +private func formatInstruction(toolsNames: String, preferredLanguage: String) -> String { """ The way you use the tools is by specifying a json blob. Specifically, this json should have a `action` key (with the name of the tool to use) and a `action_input` key (with the input to the tool going here). @@ -29,7 +29,7 @@ private func formatInstruction(toolsNames: String) -> String { Observation: the result of the action ... (this Thought/Action/Observation can repeat N times) Thought: I now know the final answer - Final Answer: the final answer to the original input question + Final Answer: the final answer to the original input question \(preferredLanguage) """ } @@ -40,7 +40,7 @@ public class ChatAgent: Agent { public let chatModelChain: ChatModelChain> let tools: [AgentTool] - public init(chatModel: ChatModel, tools: [AgentTool]) { + public init(chatModel: ChatModel, tools: [AgentTool], preferredLanguage: String) { self.tools = tools chatModelChain = .init( chatModel: chatModel, @@ -57,7 +57,12 @@ public class ChatAgent: Agent { \(tools.map { "\($0.name): \($0.description)" }.joined(separator: "\n")) - \(formatInstruction(toolsNames: tools.map(\.name).joined(separator: ","))) + \(formatInstruction( + toolsNames: tools.map(\.name).joined(separator: ","), + preferredLanguage: preferredLanguage.isEmpty + ? "" + : "(in \(preferredLanguage)" + )) Begin! Reminder to always use the exact characters `Final Answer` when responding. """ @@ -92,9 +97,9 @@ public class ChatAgent: Agent { let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) if baseScratchpad.isEmpty { return .text("") } return .text(""" - This was your previous work (but I haven't seen any of it! I only see what you return as final answer): + This was your previous work (but I haven't seen any of it! I only see what you return as `Final Answer`): \(baseScratchpad) - (Please continue with `Thought:`) + (Please continue with `Thought:` or `Final Answer:`) """) } @@ -158,12 +163,12 @@ public class ChatAgent: Agent { var parsableContent = text[...] let finalAnswer = try? forceParser.parse(&parsableContent) .trimmingCharacters(in: .whitespacesAndNewlines) - + var answer = finalAnswer ?? text if answer.isEmpty { answer = "Sorry, I don't know." } - + return .finish(AgentFinish(returnValue: String(answer), log: text)) } } diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 909fe3ab..82f19b50 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -4,14 +4,12 @@ import GPTEncoder import Preferences public protocol ChatGPTServiceType: ObservableObject { - var isReceivingMessage: Bool { get async } var history: [ChatMessage] { get async } func send(content: String, summary: String?) async throws -> AsyncThrowingStream func stopReceivingMessage() async func clearHistory() async func mutateSystemPrompt(_ newPrompt: String) async func mutateHistory(_ mutate: (inout [ChatMessage]) -> Void) async - func markReceivingMessage(_ receiving: Bool) async } public enum ChatGPTServiceError: Error, LocalizedError { @@ -105,10 +103,6 @@ public actor ChatGPTService: ChatGPTServiceType { didSet { objectWillChange.send() } } - public internal(set) var isReceivingMessage = false { - didSet { objectWillChange.send() } - } - var stop: [String] var uuidGenerator: () -> String = { UUID().uuidString } var cancelTask: Cancellable? @@ -131,7 +125,6 @@ public actor ChatGPTService: ChatGPTServiceType { content: String, summary: String? = nil ) async throws -> AsyncThrowingStream { - guard !isReceivingMessage else { throw CancellationError() } guard let url = URL(string: endpoint) else { throw ChatGPTServiceError.endpointIncorrect } if !content.isEmpty || summary != nil { @@ -155,8 +148,6 @@ public actor ChatGPTService: ChatGPTServiceType { max_tokens: maxTokenForReply(model: model, remainingTokens: remainingTokens) ) - isReceivingMessage = true - let api = buildCompletionStreamAPI( apiKey, designatedProvider ?? UserDefaults.shared.value(for: \.chatFeatureProvider), @@ -168,10 +159,6 @@ public actor ChatGPTService: ChatGPTServiceType { Task { do { let (trunks, cancel) = try await api() - guard isReceivingMessage else { - continuation.finish() - return - } cancelTask = cancel for try await trunk in trunks { guard let delta = trunk.choices.first?.delta else { continue } @@ -199,19 +186,15 @@ public actor ChatGPTService: ChatGPTServiceType { } continuation.finish() - isReceivingMessage = false } catch let error as CancellationError { - isReceivingMessage = false continuation.finish(throwing: error) } catch let error as NSError where error.code == NSURLErrorCancelled { - isReceivingMessage = false continuation.finish(throwing: error) } catch { history.append(.init( role: .assistant, content: error.localizedDescription )) - isReceivingMessage = false continuation.finish(throwing: error) } } @@ -222,7 +205,6 @@ public actor ChatGPTService: ChatGPTServiceType { content: String, summary: String? = nil ) async throws -> String? { - guard !isReceivingMessage else { throw CancellationError() } guard let url = URL(string: endpoint) else { throw ChatGPTServiceError.endpointIncorrect } if !content.isEmpty || summary != nil { @@ -246,9 +228,6 @@ public actor ChatGPTService: ChatGPTServiceType { max_tokens: maxTokenForReply(model: model, remainingTokens: remainingTokens) ) - isReceivingMessage = true - defer { isReceivingMessage = false } - let api = buildCompletionAPI( apiKey, designatedProvider ?? UserDefaults.shared.value(for: \.chatFeatureProvider), @@ -273,7 +252,6 @@ public actor ChatGPTService: ChatGPTServiceType { public func stopReceivingMessage() { cancelTask?() cancelTask = nil - isReceivingMessage = false } public func clearHistory() { @@ -288,10 +266,6 @@ public actor ChatGPTService: ChatGPTServiceType { public func mutateHistory(_ mutate: (inout [ChatMessage]) -> Void) async { mutate(&history) } - - public func markReceivingMessage(_ receiving: Bool) { - isReceivingMessage = receiving - } } extension ChatGPTService { diff --git a/Tool/Sources/Preferences/ChatGPTModel.swift b/Tool/Sources/Preferences/ChatGPTModel.swift index f2d365d1..3f454240 100644 --- a/Tool/Sources/Preferences/ChatGPTModel.swift +++ b/Tool/Sources/Preferences/ChatGPTModel.swift @@ -1,24 +1,20 @@ import Foundation public enum ChatGPTModel: String { + case gpt35Turbo = "gpt-3.5-turbo" + case gpt35Turbo16k = "gpt-3.5-turbo-16k" case gpt4 = "gpt-4" - - case gpt40314 = "gpt-4-0314" - case gpt432k = "gpt-4-32k" - - case gpt432k0314 = "gpt-4-32k-0314" - - case gpt35Turbo = "gpt-3.5-turbo" - + case gpt40314 = "gpt-4-0314" + case gpt40613 = "gpt-4-0613" case gpt35Turbo0301 = "gpt-3.5-turbo-0301" + case gpt35Turbo0613 = "gpt-3.5-turbo-0613" + case gpt35Turbo16k0613 = "gpt-3.5-turbo-16k-0613" + case gpt432k0314 = "gpt-4-32k-0314" + case gpt432k0613 = "gpt-4-32k-0613" } public extension ChatGPTModel { - var endpoint: String { - "https://api.openai.com/v1/chat/completions" - } - var maxToken: Int { switch self { case .gpt4: @@ -33,6 +29,16 @@ public extension ChatGPTModel { return 4096 case .gpt35Turbo0301: return 4096 + case .gpt35Turbo0613: + return 4096 + case .gpt35Turbo16k: + return 16384 + case .gpt35Turbo16k0613: + return 16384 + case .gpt40613: + return 8192 + case .gpt432k0613: + return 32768 } } } diff --git a/Tool/Tests/LangChainTests/ChatAgentTests.swift b/Tool/Tests/LangChainTests/ChatAgentTests.swift index 88c8d22e..0ec636f1 100644 --- a/Tool/Tests/LangChainTests/ChatAgentTests.swift +++ b/Tool/Tests/LangChainTests/ChatAgentTests.swift @@ -18,7 +18,7 @@ final class ChatAgentParseOutputTests: XCTestCase { Because 42 is the answer to everything. """ - let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") let result = agent.parseOutput(finalAnswer) XCTAssertEqual(result, .finish(.init( returnValue: """ @@ -36,7 +36,7 @@ final class ChatAgentParseOutputTests: XCTestCase { Because 42 is the answer to everything. """ - let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") let result = agent.parseOutput(finalAnswer) XCTAssertEqual(result, .finish(.init( returnValue: """ @@ -60,7 +60,7 @@ final class ChatAgentParseOutputTests: XCTestCase { ``` """ - let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") let result = agent.parseOutput(reply) XCTAssertEqual(result, .actions([ .init( @@ -81,7 +81,7 @@ final class ChatAgentParseOutputTests: XCTestCase { ``` """ - let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") let result = agent.parseOutput(reply) XCTAssertEqual(result, .finish(.init( returnValue: """ @@ -98,7 +98,7 @@ final class ChatAgentParseOutputTests: XCTestCase { Because 42 is the answer to everything. """ - let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") let result = agent.parseOutput(reply) XCTAssertEqual(result, .finish(.init( returnValue: reply, diff --git a/Version.xcconfig b/Version.xcconfig index f31677c4..827d79cf 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,2 +1,2 @@ -APP_VERSION = 0.18.1 -APP_BUILD = 181 +APP_VERSION = 0.18.2 +APP_BUILD = 182 diff --git a/appcast.xml b/appcast.xml index 2704adfa..4e4117f7 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.18.2 + Wed, 14 Jun 2023 18:45:02 +0800 + 182 + 0.18.2 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.18.2 + + + + 0.18.1 Sat, 10 Jun 2023 17:03:06 +0800 @@ -228,10 +240,7 @@ https://github.com/intitni/CopilotForXcode/releases/tag/0.11.0 - + @@ -243,10 +252,7 @@ https://github.com/intitni/CopilotForXcode/releases/tag/0.10.0 - + @@ -258,11 +264,7 @@ https://github.com/intitni/CopilotForXcode/releases/tag/0.9.0 - +