From dea46ab5c82470a12887c92e6181e4eae6d5f21d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 9 Sep 2023 22:27:53 +0800 Subject: [PATCH 01/67] Add Copilot for Xcode Plus.xcworkspace --- .gitignore | 1 + Copilot for Xcode.xcodeproj/project.pbxproj | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c915adee..8f023f85 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,4 @@ Python/site-packages/* !Python/site-packages/install.sh Python/VERSIONS +Copilot for Xcode Plus.xcworkspace diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index 759f2944..ca8e5bfe 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -175,7 +175,6 @@ C861E61F2994F6390056CB02 /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = ""; }; C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCommand.swift; sourceTree = ""; }; C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorCommand.swift; sourceTree = ""; }; - C87903302A5D2E6400FE6F42 /* Pro */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pro; sourceTree = ""; }; C87B03A3293B24AB00C77EAE /* Copilot-for-Xcode-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Copilot-for-Xcode-Info.plist"; sourceTree = SOURCE_ROOT; }; C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptSuggestionCommand.swift; sourceTree = ""; }; C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RejectSuggestionCommand.swift; sourceTree = ""; }; @@ -273,7 +272,6 @@ C8CD828229B88006008D044D /* TestPlan.xctestplan */, C81D181E2A1B509B006C1B70 /* Tool */, C8189B282938979000C9DCDA /* Core */, - C87903302A5D2E6400FE6F42 /* Pro */, C8189B182938972F00C9DCDA /* Copilot for Xcode */, C81458922939EFDC00135263 /* EditorExtension */, C8216B71298036EC00AD38C7 /* Helper */, From 7288b61fb0d0441e98cffb5fdb417125afc768c5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 9 Sep 2023 23:44:32 +0800 Subject: [PATCH 02/67] Add workspace.didUpdateFilespace --- Core/Sources/Service/RealtimeSuggestionController.swift | 4 ++-- .../WorkspaceExtension/SuggestionWorkspacePlugin.swift | 4 ++++ Tool/Sources/Workspace/Workspace.swift | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index 0d631e07..2c96b96f 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -192,10 +192,10 @@ public actor RealtimeSuggestionController { func notifyEditingFileChange(editor: AXUIElement) async { guard let fileURL = try? await Environment.fetchCurrentFileURL(), - let (workspace, filespace) = try? await Service.shared.workspacePool + let (workspace, _) = try? await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) else { return } - workspace.suggestionPlugin?.notifyUpdateFile(filespace: filespace, content: editor.value) + await workspace.didUpdateFilespace(fileURL: fileURL, content: editor.value) } } diff --git a/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift b/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift index 94f54db4..b0abcbab 100644 --- a/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift +++ b/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift @@ -85,6 +85,10 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { override func didSaveFilespace(_ filespace: Filespace) { notifySaveFile(filespace: filespace) } + + override func didUpdateFilespace(_ filespace: Filespace, content: String) { + notifyUpdateFile(filespace: filespace, content: content) + } override func didCloseFilespace(_ fileURL: URL) { Task { diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index e6ffb14b..505659f4 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -38,6 +38,7 @@ open class WorkspacePlugin { open func didOpenFilespace(_: Filespace) {} open func didSaveFilespace(_: Filespace) {} + open func didUpdateFilespace(_: Filespace, content: String) {} open func didCloseFilespace(_: URL) {} } @@ -133,5 +134,13 @@ public final class Workspace { public func closeFilespace(fileURL: URL) { filespaces[fileURL] = nil } + + @WorkspaceActor + public func didUpdateFilespace(fileURL: URL, content: String) { + guard let filespace = filespaces[fileURL] else { return } + for plugin in plugins.values { + plugin.didUpdateFilespace(filespace, content: content) + } + } } From 16c76b49ce565bd993f6ba82a1299f7701beea2e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 10 Sep 2023 16:17:04 +0800 Subject: [PATCH 03/67] Update --- Tool/Sources/Environment/Environment.swift | 24 ++++++++++++++-------- Tool/Sources/Workspace/Workspace.swift | 18 +++++++++++++--- Tool/Sources/Workspace/WorkspacePool.swift | 14 ++++++------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Tool/Sources/Environment/Environment.swift b/Tool/Sources/Environment/Environment.swift index 97b762fe..e0d86bca 100644 --- a/Tool/Sources/Environment/Environment.swift +++ b/Tool/Sources/Environment/Environment.swift @@ -41,8 +41,8 @@ public enum Environment { return false } } - - public static var fetchCurrentProjectRootURLFromXcode: () async throws -> URL? = { + + public static var fetchCurrentWorkspaceURLFromXcode: () async throws -> URL? = { if let xcode = ActiveApplicationMonitor.shared.activeXcode ?? ActiveApplicationMonitor.shared.latestXcode { @@ -53,11 +53,6 @@ public enum Environment { let path = child.description let trimmedNewLine = path.trimmingCharacters(in: .newlines) var url = URL(fileURLWithPath: trimmedNewLine) - while !FileManager.default.fileIsDirectory(atPath: url.path) || - !url.pathExtension.isEmpty - { - url = url.deletingLastPathComponent() - } return url } } @@ -66,6 +61,19 @@ public enum Environment { return nil } + public static var fetchCurrentProjectRootURLFromXcode: () async throws -> URL? = { + if var url = try await fetchCurrentWorkspaceURLFromXcode() { + while !FileManager.default.fileIsDirectory(atPath: url.path) || + !url.pathExtension.isEmpty + { + url = url.deletingLastPathComponent() + } + return url + } + + return nil + } + public static var guessProjectRootURLForFile: (_ fileURL: URL) async throws -> URL = { fileURL in var currentURL = fileURL @@ -250,7 +258,7 @@ func runAppleScript(_ appleScript: String) async throws -> String { } } -extension FileManager { +public extension FileManager { func fileIsDirectory(atPath path: String) -> Bool { var isDirectory: ObjCBool = false let exists = fileExists(atPath: path, isDirectory: &isDirectory) diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index 505659f4..3963c59b 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -30,6 +30,7 @@ public class WorkspacePropertyValues { open class WorkspacePlugin { public private(set) weak var workspace: Workspace? public var projectRootURL: URL { workspace?.projectRootURL ?? URL(fileURLWithPath: "/") } + public var workspaceURL: URL { workspace?.workspaceURL ?? projectRootURL } public var filespaces: [URL: Filespace] { workspace?.filespaces ?? [:] } public init(workspace: Workspace) { @@ -57,6 +58,7 @@ public final class Workspace { var additionalProperties = WorkspacePropertyValues() public internal(set) var plugins = [ObjectIdentifier: WorkspacePlugin]() + public let workspaceURL: URL public let projectRootURL: URL public let openedFileRecoverableStorage: OpenedFileRecoverableStorage public private(set) var lastSuggestionUpdateTime = Environment.now() @@ -84,8 +86,18 @@ public final class Workspace { plugins[ObjectIdentifier(type)] as? P } - init(projectRootURL: URL) { - self.projectRootURL = projectRootURL + init(workspaceURL: URL) { + self.workspaceURL = workspaceURL + self.projectRootURL = { + var url = workspaceURL + while !FileManager.default.fileIsDirectory(atPath: url.path) || + !url.pathExtension.isEmpty + { + url = url.deletingLastPathComponent() + } + return url + + }() openedFileRecoverableStorage = .init(projectRootURL: projectRootURL) let openedFiles = openedFileRecoverableStorage.openedFiles Task { @WorkspaceActor in @@ -134,7 +146,7 @@ public final class Workspace { public func closeFilespace(fileURL: URL) { filespaces[fileURL] = nil } - + @WorkspaceActor public func didUpdateFilespace(fileURL: URL, content: String) { guard let filespace = filespaces[fileURL] else { return } diff --git a/Tool/Sources/Workspace/WorkspacePool.swift b/Tool/Sources/Workspace/WorkspacePool.swift index 4f17941a..9cd8c5b7 100644 --- a/Tool/Sources/Workspace/WorkspacePool.swift +++ b/Tool/Sources/Workspace/WorkspacePool.swift @@ -56,14 +56,14 @@ public class WorkspacePool { } // If we know which project is opened. - if let currentProjectURL = try await Environment.fetchCurrentProjectRootURLFromXcode() { - if let existed = workspaces[currentProjectURL] { + if let currentWorkspaceURL = try await Environment.fetchCurrentWorkspaceURLFromXcode() { + if let existed = workspaces[currentWorkspaceURL] { let filespace = existed.createFilespaceIfNeeded(fileURL: fileURL) return (existed, filespace) } - let new = createNewWorkspace(projectRootURL: currentProjectURL) - workspaces[currentProjectURL] = new + let new = createNewWorkspace(workspaceURL: currentWorkspaceURL) + workspaces[currentWorkspaceURL] = new let filespace = new.createFilespaceIfNeeded(fileURL: fileURL) return (new, filespace) } @@ -93,7 +93,7 @@ public class WorkspacePool { return workspace } } - return createNewWorkspace(projectRootURL: workspaceURL) + return createNewWorkspace(workspaceURL: workspaceURL) }() let filespace = workspace.createFilespaceIfNeeded(fileURL: fileURL) @@ -121,8 +121,8 @@ extension WorkspacePool { workspace.plugins[id] = nil } - func createNewWorkspace(projectRootURL: URL) -> Workspace { - let new = Workspace(projectRootURL: projectRootURL) + func createNewWorkspace(workspaceURL: URL) -> Workspace { + let new = Workspace(workspaceURL: workspaceURL) for (id, plugin) in plugins { addPlugin(plugin, id: id, to: new) } From a4439e628c2fcd7ee726a637e83bb96de38f938c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 11 Sep 2023 15:48:39 +0800 Subject: [PATCH 04/67] Change type of Filespace.language --- .../Service/GUI/WidgetDataSource.swift | 2 +- .../Contents.swift | 45 ------------------- .../timeline.xctimeline | 12 ----- Tool/Sources/Workspace/Filespace.swift | 2 +- 4 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 Playground.playground/Pages/ASTParsing.xcplaygroundpage/Contents.swift delete mode 100644 Playground.playground/Pages/ASTParsing.xcplaygroundpage/timeline.xctimeline diff --git a/Core/Sources/Service/GUI/WidgetDataSource.swift b/Core/Sources/Service/GUI/WidgetDataSource.swift index a6517f51..8b449020 100644 --- a/Core/Sources/Service/GUI/WidgetDataSource.swift +++ b/Core/Sources/Service/GUI/WidgetDataSource.swift @@ -19,7 +19,7 @@ extension WidgetDataSource: SuggestionWidgetDataSource { { return .init( code: suggestion.text, - language: filespace.language, + language: filespace.language.rawValue, startLineIndex: suggestion.position.line, suggestionCount: filespace.suggestions.count, currentSuggestionIndex: filespace.suggestionIndex, diff --git a/Playground.playground/Pages/ASTParsing.xcplaygroundpage/Contents.swift b/Playground.playground/Pages/ASTParsing.xcplaygroundpage/Contents.swift deleted file mode 100644 index a50a91ab..00000000 --- a/Playground.playground/Pages/ASTParsing.xcplaygroundpage/Contents.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import SwiftUI -import AppKit -import ASTParser -import PlaygroundSupport - -struct ParsingForm: View { - @State var filePath: String = "" - @State var result: String = "" - - var body: some View { - Form { - Section("Input") { - TextField("File Path", text: $filePath) - Button("Parse") { - result = "" - Task { - do { - let fileContent = try String(contentsOfFile: filePath) - let parser = ASTParser(language: .swift) - let tree = parser.parse(fileContent) - result = tree?.dump() ?? "N/A" - print(result) - } catch { - result = error.localizedDescription - } - } - } - } - - Section("Result") { - Text(result) - .fontDesign(.monospaced) - .textSelection(.enabled) - } - } - .formStyle(.grouped) - .frame(width: 600, height: 800) - } -} - -PlaygroundPage.current.needsIndefiniteExecution = true -PlaygroundPage.current.setLiveView(NSHostingController(rootView: ParsingForm())) -// protocol_declaration, class_declaration, function_declaration, property_declaration, computed_property -// type_identifier, simple_identifier (for variables and funcs) diff --git a/Playground.playground/Pages/ASTParsing.xcplaygroundpage/timeline.xctimeline b/Playground.playground/Pages/ASTParsing.xcplaygroundpage/timeline.xctimeline deleted file mode 100644 index 9d435df4..00000000 --- a/Playground.playground/Pages/ASTParsing.xcplaygroundpage/timeline.xctimeline +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index 11d02740..57060305 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -47,7 +47,7 @@ public struct FilespaceCodeMetadata: Equatable { @dynamicMemberLookup public final class Filespace { public let fileURL: URL - public private(set) lazy var language: String = languageIdentifierFromFileURL(fileURL).rawValue + public private(set) lazy var language: CodeLanguage = languageIdentifierFromFileURL(fileURL) public var codeMetadata: FilespaceCodeMetadata = .init() public internal(set) var suggestions: [CodeSuggestion] = [] { didSet { refreshUpdateTime() } From cede9e0e2f8c191f199014230c867c6bb816fc68 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 11 Sep 2023 22:13:13 +0800 Subject: [PATCH 05/67] Add EnhancedWorkspacePlugin --- Core/Sources/Service/Service.swift | 8 +++++--- Pro | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 0f345bf3..fdefc1a8 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,5 +1,6 @@ import Foundation #if canImport(KeyBindingManager) +import EnhancedWorkspace import KeyBindingManager #endif import Workspace @@ -16,9 +17,10 @@ public final class Service { @WorkspaceActor let workspacePool = { let it = WorkspacePool() - it.registerPlugin { - SuggestionServiceWorkspacePlugin(workspace: $0) - } + it.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } + #if canImport(EnhancedWorkspace) + it.registerPlugin { EnhancedWorkspacePlugin(workspace: $0) } + #endif return it }() diff --git a/Pro b/Pro index 90d5c1c3..9d3f8aec 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 90d5c1c304b671241a6fb3deaec99c2997fcb943 +Subproject commit 9d3f8aecb794e14d8fcef3a29a03afb6079a8ecf From 85872fa2c31c56dd9b1d0d12f6c494d09ec4b304 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 14 Sep 2023 16:54:10 +0800 Subject: [PATCH 06/67] Move ChatContextCollector to Tool --- Core/Package.swift | 18 ++++-------------- Pro | 2 +- Tool/Package.swift | 9 +++++++++ .../ChatContextCollector.swift | 0 4 files changed, 14 insertions(+), 15 deletions(-) rename {Core => Tool}/Sources/ChatContextCollector/ChatContextCollector.swift (100%) diff --git a/Core/Package.swift b/Core/Package.swift index 081430b7..a484c6bc 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -171,7 +171,6 @@ let package = Package( name: "ChatService", dependencies: [ "ChatPlugin", - "ChatContextCollector", // plugins "MathChatPlugin", @@ -183,6 +182,7 @@ let package = Package( "ActiveDocumentChatContextCollector", "SystemInfoChatContextCollector", + .product(name: "ChatContextCollector", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), .product(name: "Environment", package: "Tool"), .product(name: "Parsing", package: "swift-parsing"), @@ -199,16 +199,6 @@ let package = Package( .product(name: "Terminal", package: "Tool"), ] ), - .target( - name: "ChatContextCollector", - dependencies: [ - .product(name: "SuggestionModel", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "Environment", package: "Tool"), - .product(name: "OpenAIService", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - ] - ), .target( name: "ChatGPTChatTab", @@ -346,7 +336,7 @@ let package = Package( .target( name: "WebChatContextCollector", dependencies: [ - "ChatContextCollector", + .product(name: "ChatContextCollector", package: "Tool"), .product(name: "LangChain", package: "Tool"), .product(name: "OpenAIService", package: "Tool"), .product(name: "ExternalServices", package: "Tool"), @@ -358,7 +348,7 @@ let package = Package( .target( name: "SystemInfoChatContextCollector", dependencies: [ - "ChatContextCollector", + .product(name: "ChatContextCollector", package: "Tool"), .product(name: "OpenAIService", package: "Tool"), ], path: "Sources/ChatContextCollectors/SystemInfoChatContextCollector" @@ -367,7 +357,7 @@ let package = Package( .target( name: "ActiveDocumentChatContextCollector", dependencies: [ - "ChatContextCollector", + .product(name: "ChatContextCollector", package: "Tool"), .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "FocusedCodeFinder", package: "Tool"), diff --git a/Pro b/Pro index 9d3f8aec..d940918b 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9d3f8aecb794e14d8fcef3a29a03afb6079a8ecf +Subproject commit d940918b4873bdd82a9a4bb11449f531fa1fd77f diff --git a/Tool/Package.swift b/Tool/Package.swift index 7868368c..fa6ae660 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -14,6 +14,7 @@ let package = Package( .library(name: "Logger", targets: ["Logger"]), .library(name: "OpenAIService", targets: ["OpenAIService"]), .library(name: "ChatTab", targets: ["ChatTab"]), + .library(name: "ChatContextCollector", targets: ["ChatContextCollector"]), .library(name: "Environment", targets: ["Environment"]), .library(name: "SuggestionModel", targets: ["SuggestionModel"]), .library(name: "ASTParser", targets: ["ASTParser"]), @@ -214,6 +215,14 @@ let package = Package( ] ), + .target( + name: "ChatContextCollector", + dependencies: [ + "SuggestionModel", + "OpenAIService", + ] + ), + .target(name: "BingSearchService"), // MARK: - OpenAI diff --git a/Core/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift similarity index 100% rename from Core/Sources/ChatContextCollector/ChatContextCollector.swift rename to Tool/Sources/ChatContextCollector/ChatContextCollector.swift From 2a686d30df296e674ccdd7ab7db8488ec904e1c9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 17 Sep 2023 16:47:50 +0800 Subject: [PATCH 07/67] Track workspaceURL --- .../OpenAIPromptToCodeService.swift | 3 +- Core/Sources/Service/Service.swift | 23 ++++---- .../Sources/SuggestionWidget/WidgetView.swift | 2 +- ExtensionService/AppDelegate+Menu.swift | 6 +- .../ActiveApplicationMonitor.swift | 2 + Tool/Sources/Environment/Environment.swift | 12 ++-- .../SuggestionModel/EditorInformation.swift | 9 ++- Tool/Sources/Workspace/WorkspacePool.swift | 38 +++++++++++++ .../XcodeInspector/XcodeInspector.swift | 57 ++++++++++++++----- .../XcodeInspector/XcodeWindowInspector.swift | 46 ++++++++------- 10 files changed, 141 insertions(+), 57 deletions(-) diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index 064399fa..6ed62d1e 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -42,7 +42,8 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { selectedContent: code, selectedLines: [], documentURL: source.documentURL, - projectURL: source.projectRootURL, + workspaceURL: source.projectRootURL, + projectRootURL: source.projectRootURL, relativePath: "", language: source.language ) diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index fdefc1a8..c07ead7e 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,9 +1,11 @@ +import Dependencies import Foundation +import Workspace + #if canImport(KeyBindingManager) import EnhancedWorkspace import KeyBindingManager #endif -import Workspace @globalActor public enum ServiceActor { public actor TheActor {} @@ -15,15 +17,7 @@ public final class Service { public static let shared = Service() @WorkspaceActor - let workspacePool = { - let it = WorkspacePool() - it.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } - #if canImport(EnhancedWorkspace) - it.registerPlugin { EnhancedWorkspacePlugin(workspace: $0) } - #endif - return it - }() - + let workspacePool: WorkspacePool @MainActor public let guiController = GraphicalUserInterfaceController() public let realtimeSuggestionController = RealtimeSuggestionController() @@ -33,6 +27,8 @@ public final class Service { #endif private init() { + @Dependency(\.workspacePool) var workspacePool + scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController) #if canImport(KeyBindingManager) keyBindingManager = .init( @@ -44,6 +40,13 @@ public final class Service { } ) #endif + + workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } + #if canImport(EnhancedWorkspace) + workspacePool.registerPlugin { EnhancedWorkspacePlugin(workspace: $0) } + #endif + + self.workspacePool = workspacePool } @MainActor diff --git a/Core/Sources/SuggestionWidget/WidgetView.swift b/Core/Sources/SuggestionWidget/WidgetView.swift index 11dbf5f8..5596ae89 100644 --- a/Core/Sources/SuggestionWidget/WidgetView.swift +++ b/Core/Sources/SuggestionWidget/WidgetView.swift @@ -216,7 +216,7 @@ extension WidgetContextMenu { @ViewBuilder var enableSuggestionForProject: some View { WithViewStore(store) { _ in - let projectPath = xcodeInspector.activeProjectURL.path + let projectPath = xcodeInspector.activeProjectRootURL.path if disableSuggestionFeatureGlobally { let matchedPath = suggestionFeatureEnabledProjectList.first { path in projectPath.hasPrefix(path) diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift index a3a26425..aac35e21 100644 --- a/ExtensionService/AppDelegate+Menu.swift +++ b/ExtensionService/AppDelegate+Menu.swift @@ -95,7 +95,8 @@ extension AppDelegate: NSMenuDelegate { case xcodeInspectorDebugMenuIdentifier: let inspector = XcodeInspector.shared menu.items.removeAll() - menu.items.append(.text("Active Project: \(inspector.activeProjectURL)")) + menu.items.append(.text("Active Project: \(inspector.activeProjectRootURL)")) + menu.items.append(.text("Active Workspace: \(inspector.activeWorkspaceURL)")) menu.items.append(.text("Active Document: \(inspector.activeDocumentURL)")) for xcode in inspector.xcodes { let item = NSMenuItem( @@ -107,7 +108,8 @@ extension AppDelegate: NSMenuDelegate { let xcodeMenu = NSMenu() item.submenu = xcodeMenu xcodeMenu.items.append(.text("Is Active: \(xcode.isActive)")) - xcodeMenu.items.append(.text("Active Project: \(xcode.projectURL)")) + xcodeMenu.items.append(.text("Active Project: \(xcode.projectRootURL)")) + xcodeMenu.items.append(.text("Active Workspace: \(xcode.workspaceURL)")) xcodeMenu.items.append(.text("Active Document: \(xcode.documentURL)")) for (key, workspace) in xcode.realtimeWorkspaces { diff --git a/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift b/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift index 9dc7e6ee..bc7167b6 100644 --- a/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift +++ b/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift @@ -20,6 +20,8 @@ public final class ActiveApplicationMonitor { private var continuations: [UUID: AsyncStream.Continuation] = [:] private init() { + activeApplication = NSWorkspace.shared.runningApplications.first(where: \.isActive) + Task { let sequence = NSWorkspace.shared.notificationCenter .notifications(named: NSWorkspace.didActivateApplicationNotification) diff --git a/Tool/Sources/Environment/Environment.swift b/Tool/Sources/Environment/Environment.swift index e0d86bca..254b4474 100644 --- a/Tool/Sources/Environment/Environment.swift +++ b/Tool/Sources/Environment/Environment.swift @@ -63,12 +63,7 @@ public enum Environment { public static var fetchCurrentProjectRootURLFromXcode: () async throws -> URL? = { if var url = try await fetchCurrentWorkspaceURLFromXcode() { - while !FileManager.default.fileIsDirectory(atPath: url.path) || - !url.pathExtension.isEmpty - { - url = url.deletingLastPathComponent() - } - return url + return try await guessProjectRootURLForFile(url) } return nil @@ -78,17 +73,18 @@ public enum Environment { fileURL in var currentURL = fileURL var firstDirectoryURL: URL? + var lastGitDirectoryURL: URL? while currentURL.pathComponents.count > 1 { defer { currentURL.deleteLastPathComponent() } guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue } if firstDirectoryURL == nil { firstDirectoryURL = currentURL } let gitURL = currentURL.appendingPathComponent(".git") if FileManager.default.fileIsDirectory(atPath: gitURL.path) { - return currentURL + lastGitDirectoryURL = currentURL } } - return firstDirectoryURL ?? fileURL + return lastGitDirectoryURL ?? firstDirectoryURL ?? fileURL } public static var fetchCurrentFileURL: () async throws -> URL = { diff --git a/Tool/Sources/SuggestionModel/EditorInformation.swift b/Tool/Sources/SuggestionModel/EditorInformation.swift index 60ba65d2..c0964f8d 100644 --- a/Tool/Sources/SuggestionModel/EditorInformation.swift +++ b/Tool/Sources/SuggestionModel/EditorInformation.swift @@ -55,7 +55,8 @@ public struct EditorInformation { public let selectedContent: String public let selectedLines: [String] public let documentURL: URL - public let projectURL: URL + public let workspaceURL: URL + public let projectRootURL: URL public let relativePath: String public let language: CodeLanguage @@ -64,7 +65,8 @@ public struct EditorInformation { selectedContent: String, selectedLines: [String], documentURL: URL, - projectURL: URL, + workspaceURL: URL, + projectRootURL: URL, relativePath: String, language: CodeLanguage ) { @@ -72,7 +74,8 @@ public struct EditorInformation { self.selectedContent = selectedContent self.selectedLines = selectedLines self.documentURL = documentURL - self.projectURL = projectURL + self.workspaceURL = workspaceURL + self.projectRootURL = projectRootURL self.relativePath = relativePath self.language = language } diff --git a/Tool/Sources/Workspace/WorkspacePool.swift b/Tool/Sources/Workspace/WorkspacePool.swift index 9cd8c5b7..7c8c239b 100644 --- a/Tool/Sources/Workspace/WorkspacePool.swift +++ b/Tool/Sources/Workspace/WorkspacePool.swift @@ -1,5 +1,17 @@ import Environment import Foundation +import Dependencies + +public struct WorkspacePoolDependencyKey: DependencyKey { + public static var liveValue: WorkspacePool = .init() +} + +public extension DependencyValues { + var workspacePool: WorkspacePool { + get { self[WorkspacePoolDependencyKey.self] } + set { self[WorkspacePoolDependencyKey.self] = newValue } + } +} @globalActor public enum WorkspaceActor { public actor TheActor {} @@ -7,6 +19,17 @@ import Foundation } public class WorkspacePool { + public enum Error: Swift.Error, LocalizedError { + case invalidWorkspaceURL(URL) + + public var errorDescription: String? { + switch self { + case .invalidWorkspaceURL(let url): + return "Invalid workspace URL: \(url)" + } + } + } + public internal(set) var workspaces: [URL: Workspace] = [:] var plugins = [ObjectIdentifier: (Workspace) -> WorkspacePlugin]() @@ -45,6 +68,21 @@ public class WorkspacePool { } return nil } + + @WorkspaceActor + public func fetchOrCreateWorkspace(workspaceURL: URL) async throws -> Workspace { + guard workspaceURL != URL(fileURLWithPath: "/") else { + throw Error.invalidWorkspaceURL(workspaceURL) + } + + if let existed = workspaces[workspaceURL] { + return existed + } + + let new = createNewWorkspace(workspaceURL: workspaceURL) + workspaces[workspaceURL] = new + return new + } @WorkspaceActor public func fetchOrCreateWorkspaceAndFilespace(fileURL: URL) async throws diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 2d271206..60711fdc 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -17,8 +17,9 @@ public final class XcodeInspector: ObservableObject { @Published public internal(set) var activeXcode: XcodeAppInstanceInspector? @Published public internal(set) var latestActiveXcode: XcodeAppInstanceInspector? @Published public internal(set) var xcodes: [XcodeAppInstanceInspector] = [] - @Published public internal(set) var activeProjectURL = URL(fileURLWithPath: "/") + @Published public internal(set) var activeProjectRootURL = URL(fileURLWithPath: "/") @Published public internal(set) var activeDocumentURL = URL(fileURLWithPath: "/") + @Published public internal(set) var activeWorkspaceURL = URL(fileURLWithPath: "/") @Published public internal(set) var focusedWindow: XcodeWindowInspector? @Published public internal(set) var focusedEditor: SourceEditor? @Published public internal(set) var focusedElement: AXUIElement? @@ -26,8 +27,9 @@ public final class XcodeInspector: ObservableObject { public var focusedEditorContent: EditorInformation? { let editorContent = XcodeInspector.shared.focusedEditor?.content - let documentURL = XcodeInspector.shared.activeDocumentURL - let projectURL = XcodeInspector.shared.activeProjectURL + let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL + let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL + let projectURL = XcodeInspector.shared.activeProjectRootURL let language = languageIdentifierFromFileURL(documentURL) let relativePath = documentURL.path .replacingOccurrences(of: projectURL.path, with: "") @@ -42,7 +44,8 @@ public final class XcodeInspector: ObservableObject { selectedContent: selectedContent, selectedLines: selectedLines, documentURL: documentURL, - projectURL: projectURL, + workspaceURL: workspaceURL, + projectRootURL: projectURL, relativePath: relativePath, language: language ) @@ -53,14 +56,19 @@ public final class XcodeInspector: ObservableObject { selectedContent: "", selectedLines: [], documentURL: documentURL, - projectURL: projectURL, + workspaceURL: workspaceURL, + projectRootURL: projectURL, relativePath: relativePath, language: language ) } public var realtimeActiveDocumentURL: URL { - latestActiveXcode?.realtimeDocumentURL ?? URL(fileURLWithPath: "/") + latestActiveXcode?.realtimeDocumentURL ?? activeDocumentURL + } + + public var realtimeActiveWorkspaceURL: URL { + latestActiveXcode?.realtimeWorkspaceURL ?? activeWorkspaceURL } init() { @@ -141,7 +149,8 @@ public final class XcodeInspector: ObservableObject { activeDocumentURL = xcode.documentURL focusedWindow = xcode.focusedWindow completionPanel = xcode.completionPanel - activeProjectURL = xcode.projectURL + activeProjectRootURL = xcode.projectRootURL + activeWorkspaceURL = xcode.workspaceURL focusedWindow = xcode.focusedWindow let setFocusedElement = { [weak self] in @@ -178,9 +187,13 @@ public final class XcodeInspector: ObservableObject { xcode.$documentURL.sink { [weak self] url in self?.activeDocumentURL = url }.store(in: &activeXcodeCancellable) + + xcode.$workspaceURL.sink { [weak self] url in + self?.activeWorkspaceURL = url + }.store(in: &activeXcodeCancellable) - xcode.$projectURL.sink { [weak self] url in - self?.activeProjectURL = url + xcode.$projectRootURL.sink { [weak self] url in + self?.activeProjectRootURL = url }.store(in: &activeXcodeCancellable) xcode.$focusedWindow.sink { [weak self] window in @@ -207,7 +220,8 @@ public class AppInstanceInspector: ObservableObject { public final class XcodeAppInstanceInspector: AppInstanceInspector { @Published public var focusedWindow: XcodeWindowInspector? @Published public var documentURL: URL = .init(fileURLWithPath: "/") - @Published public var projectURL: URL = .init(fileURLWithPath: "/") + @Published public var workspaceURL: URL = .init(fileURLWithPath: "/") + @Published public var projectRootURL: URL = .init(fileURLWithPath: "/") @Published public var workspaces = [WorkspaceIdentifier: Workspace]() public var realtimeWorkspaces: [WorkspaceIdentifier: WorkspaceInfo] { updateWorkspaceInfo() @@ -226,6 +240,17 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { return WorkspaceXcodeWindowInspector.extractDocumentURL(windowElement: window) ?? URL(fileURLWithPath: "/") } + + public var realtimeWorkspaceURL: URL { + guard let window = appElement.focusedWindow, + window.identifier == "Xcode.WorkspaceWindow" + else { + return URL(fileURLWithPath: "/") + } + + return WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) + ?? URL(fileURLWithPath: "/") + } var _version: String? public var version: String? { @@ -284,17 +309,23 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { focusedWindowObservations.removeAll() documentURL = window.documentURL - projectURL = window.projectURL + workspaceURL = window.workspaceURL + projectRootURL = window.projectRootURL window.$documentURL .filter { $0 != .init(fileURLWithPath: "/") } .sink { [weak self] url in self?.documentURL = url }.store(in: &focusedWindowObservations) - window.$projectURL + window.$workspaceURL + .filter { $0 != .init(fileURLWithPath: "/") } + .sink { [weak self] url in + self?.workspaceURL = url + }.store(in: &focusedWindowObservations) + window.$projectRootURL .filter { $0 != .init(fileURLWithPath: "/") } .sink { [weak self] url in - self?.projectURL = url + self?.projectRootURL = url }.store(in: &focusedWindowObservations) } } else { diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 217a9cbd..029d30e0 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -15,7 +15,8 @@ public class XcodeWindowInspector: ObservableObject { public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { let app: NSRunningApplication @Published var documentURL: URL = .init(fileURLWithPath: "/") - @Published var projectURL: URL = .init(fileURLWithPath: "/") + @Published var workspaceURL: URL = .init(fileURLWithPath: "/") + @Published var projectRootURL: URL = .init(fileURLWithPath: "/") private var updateTabsTask: Task? private var focusedElementChangedTask: Task? @@ -23,7 +24,7 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { updateTabsTask?.cancel() focusedElementChangedTask?.cancel() } - + public func refresh() { updateURLs() } @@ -34,7 +35,7 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { focusedElementChangedTask = Task { @MainActor in updateURLs() - + Task { @MainActor in // prevent that documentURL may not be available yet try await Task.sleep(nanoseconds: 500_000_000) @@ -42,7 +43,7 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { updateURLs() } } - + let notifications = AXNotificationStream( app: app, notificationNames: kAXFocusedUIElementChangedNotification @@ -54,18 +55,23 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { } } } - + func updateURLs() { let documentURL = Self.extractDocumentURL(windowElement: uiElement) if let documentURL { self.documentURL = documentURL } + let workspaceURL = Self.extractWorkspaceURL(windowElement: uiElement) + if let workspaceURL { + self.workspaceURL = workspaceURL + } let projectURL = Self.extractProjectURL( windowElement: uiElement, - fileURL: documentURL + workspaceURL: workspaceURL, + documentURL: documentURL ) if let projectURL { - self.projectURL = projectURL + projectRootURL = projectURL } } @@ -84,37 +90,39 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { return nil } - static func extractProjectURL( - windowElement: AXUIElement, - fileURL: URL? + static func extractWorkspaceURL( + windowElement: AXUIElement ) -> URL? { for child in windowElement.children { if child.description.starts(with: "/"), child.description.count > 1 { let path = child.description let trimmedNewLine = path.trimmingCharacters(in: .newlines) - var url = URL(fileURLWithPath: trimmedNewLine) - while !FileManager.default.fileIsDirectory(atPath: url.path) || - !url.pathExtension.isEmpty - { - url = url.deletingLastPathComponent() - } + let url = URL(fileURLWithPath: trimmedNewLine) return url } } + return nil + } - guard var currentURL = fileURL else { return nil } + static func extractProjectURL( + windowElement: AXUIElement, + workspaceURL: URL?, + documentURL: URL? + ) -> URL? { + guard var currentURL = workspaceURL ?? documentURL else { return nil } var firstDirectoryURL: URL? + var lastGitDirectoryURL: URL? while currentURL.pathComponents.count > 1 { defer { currentURL.deleteLastPathComponent() } guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue } if firstDirectoryURL == nil { firstDirectoryURL = currentURL } let gitURL = currentURL.appendingPathComponent(".git") if FileManager.default.fileIsDirectory(atPath: gitURL.path) { - return currentURL + lastGitDirectoryURL = currentURL } } - return firstDirectoryURL ?? fileURL + return lastGitDirectoryURL ?? firstDirectoryURL ?? workspaceURL } } From 37ea5c0800dc53dd3e5cc59d31af6dd9f8f80f45 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 17 Sep 2023 16:48:08 +0800 Subject: [PATCH 08/67] Add ProChatContextCollectors --- Core/Package.swift | 4 +++- Core/Sources/ChatService/AllContextCollector.swift | 2 ++ Pro | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index a484c6bc..814b6267 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -188,7 +188,9 @@ let package = Package( .product(name: "Parsing", package: "swift-parsing"), .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), - ] + ].pro([ + "ProService", + ]) ), .testTarget(name: "ChatServiceTests", dependencies: ["ChatService"]), .target( diff --git a/Core/Sources/ChatService/AllContextCollector.swift b/Core/Sources/ChatService/AllContextCollector.swift index 600c8f74..2402da7d 100644 --- a/Core/Sources/ChatService/AllContextCollector.swift +++ b/Core/Sources/ChatService/AllContextCollector.swift @@ -2,10 +2,12 @@ import ActiveDocumentChatContextCollector import ChatContextCollector import SystemInfoChatContextCollector import WebChatContextCollector +import ProChatContextCollectors let allContextCollectors: [any ChatContextCollector] = [ SystemInfoChatContextCollector(), ActiveDocumentChatContextCollector(), WebChatContextCollector(), + ProChatContextCollectors(), ] diff --git a/Pro b/Pro index d940918b..a2af7c72 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d940918b4873bdd82a9a4bb11449f531fa1fd77f +Subproject commit a2af7c72476a8130058d5910ecf42580c9da15a5 From 8c2f2b8fea78ca6a51529c44c8993570330273e7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 17 Sep 2023 16:48:19 +0800 Subject: [PATCH 09/67] Remove useless schemes --- .../xcschemes/CopilotModelTests.xcscheme | 52 ------------------- .../xcschemes/CopilotServiceTests.xcscheme | 52 ------------------- .../xcschemes/ServiceTests.xcscheme | 52 ------------------- 3 files changed, 156 deletions(-) delete mode 100644 Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotModelTests.xcscheme delete mode 100644 Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotServiceTests.xcscheme delete mode 100644 Core/.swiftpm/xcode/xcshareddata/xcschemes/ServiceTests.xcscheme diff --git a/Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotModelTests.xcscheme b/Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotModelTests.xcscheme deleted file mode 100644 index 68a22f51..00000000 --- a/Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotModelTests.xcscheme +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotServiceTests.xcscheme b/Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotServiceTests.xcscheme deleted file mode 100644 index c21246c2..00000000 --- a/Core/.swiftpm/xcode/xcshareddata/xcschemes/CopilotServiceTests.xcscheme +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Core/.swiftpm/xcode/xcshareddata/xcschemes/ServiceTests.xcscheme b/Core/.swiftpm/xcode/xcshareddata/xcschemes/ServiceTests.xcscheme deleted file mode 100644 index f548b29e..00000000 --- a/Core/.swiftpm/xcode/xcshareddata/xcschemes/ServiceTests.xcscheme +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - From e187a35e970d1115270f5fbd3811859edd945e82 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 17 Sep 2023 19:05:02 +0800 Subject: [PATCH 10/67] Add function calling agent --- Tool/Sources/LangChain/Agent.swift | 13 +- Tool/Sources/LangChain/AgentTool.swift | 16 ++ Tool/Sources/LangChain/Agents/ChatAgent.swift | 14 +- .../Agents/FunctionCallingChatAgent.swift | 160 ++++++++++++++++++ 4 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index daf954a7..3bd15b5b 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -100,7 +100,9 @@ public protocol Agent { func validateTools(tools: [AgentTool]) throws func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad - func parseOutput(_ output: String) -> AgentNextStep + func extraPlan(input: AgentInput) + func prepareForEarlyStopWithGenerate() + func parseOutput(_ output: ChatModelChain>.Output) async -> AgentNextStep } public extension Agent { @@ -115,8 +117,9 @@ public extension Agent { callbackManagers: [CallbackManager] ) async throws -> AgentNextStep { let input = getFullInputs(input: input, intermediateSteps: intermediateSteps) + extraPlan(input: input) let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) - return parseOutput(output.content ?? "") + return await parseOutput(output) } func returnStoppedResponse( @@ -139,14 +142,14 @@ public extension Agent { (Please continue with `Final Answer:`) """ let input = AgentInput(input: input, thoughts: .text(thoughts)) + prepareForEarlyStopWithGenerate() let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) - let reply = output.content ?? "" - let nextAction = parseOutput(reply) + let nextAction = await parseOutput(output) switch nextAction { case let .finish(finish): return finish case .actions: - return AgentFinish(returnValue: reply, log: reply) + return AgentFinish(returnValue: output.content ?? "", log: output.content ?? "") } } } diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift index 170cd9ca..12bde7eb 100644 --- a/Tool/Sources/LangChain/AgentTool.swift +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -1,4 +1,5 @@ import Foundation +import OpenAIService public protocol AgentTool { var name: String { get } @@ -30,3 +31,18 @@ public struct SimpleAgentTool: AgentTool { } } +public struct FunctionCallingAgentTool: AgentTool { + public let function: any ChatGPTFunction + public var name: String { function.name } + public var description: String { function.description } + public let returnDirectly: Bool + + public init(function: any ChatGPTFunction, returnDirectly: Bool = false) { + self.function = function + self.returnDirectly = returnDirectly + } + + public func run(input: String) async throws -> String { + try await function.call(argumentsJsonString: input).botReadableContent + } +} diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift index fb24dc4a..24b115c6 100644 --- a/Tool/Sources/LangChain/Agents/ChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -102,12 +102,22 @@ public class ChatAgent: Agent { (Please continue with `Thought:` or `Final Answer:`) """) } - + public func validateTools(tools: [AgentTool]) throws { // no validation } - public func parseOutput(_ text: String) -> AgentNextStep { + public func extraPlan(input: AgentInput) { + // do nothing + } + + public func prepareForEarlyStopWithGenerate() { + // do nothing + } + + public func parseOutput(_ output: ChatMessage) async -> AgentNextStep { + let text = output.content ?? "" + func parseFinalAnswerIfPossible() -> AgentNextStep? { let throughAnswerParser = PrefixThrough("Final Answer:") var parsableContent = text[...] diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift new file mode 100644 index 00000000..dcd77e34 --- /dev/null +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -0,0 +1,160 @@ +import Foundation +import Logger +import OpenAIService + +public class FunctionCallingChatAgent: Agent { + struct EndFunction: ChatGPTFunction { + struct Argument: Codable { + let finalAnswer: String + } + + typealias Result = String + + var name: String { "sendFinalAnswer" } + var description: String { "Send the final answer to user" } + var argumentSchema: JSONSchemaValue { + [ + .type: "object", + .properties: [ + "finalAnswer": [ + .type: "string", + .description: "the final answer to send to user", + ], + ], + .required: ["finalAnswer"], + ] + } + + var reportProgress: (String) async -> Void = { _ in } + func prepare() async {} + func call(arguments: Argument) async throws -> Result { + return arguments.finalAnswer + } + } + + struct FunctionProvider: ChatGPTFunctionProvider { + var tools: [AgentTool] = [] + var functionTools: [any ChatGPTFunction] = [] + var functions: [any ChatGPTFunction] { + functionTools + [EndFunction()] + } + + var functionCallStrategy: FunctionCallStrategy? = nil + } + + public typealias Input = String + public var observationPrefix: String { "Observation: " } + public var llmPrefix: String { "Thought: " } + public let chatModelChain: ChatModelChain> + var functionProvider: FunctionProvider + + public init( + configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), + memory: ChatGPTMemory = ConversationChatGPTMemory(systemPrompt: ""), + functions: [any ChatGPTFunction] = [], + tools: [AgentTool] = [] + ) { + functionProvider = .init(tools: tools, functionTools: functions) + chatModelChain = .init( + chatModel: OpenAIChat( + configuration: configuration.overriding { + $0.runFunctionsAutomatically = false + }, + memory: memory, + functionProvider: functionProvider, + stream: false + ), + stops: ["Observation:"], + promptTemplate: { agentInput in + [ + .init( + role: .system, + content: """ + Respond to the human as helpfully and accurately as possible. \ + Format final answer to be more readable, in a ordered list if possible. \ + + Begin! + """ + ), + agentInput.thoughts.isEmpty + ? .init(role: .user, content: agentInput.input) + : .init( + role: .user, + content: """ + \(agentInput.input) + + \({ + switch agentInput.thoughts { + case let .text(text): + return text + case let .messages(messages): + return messages.map { message in + """ + \(message) + """ + }.joined(separator: "\n") + } + }()) + """ + ), + ] + } + ) + } + + public func extraPlan(input: AgentInput) { + // no extra plan + } + + public func prepareForEarlyStopWithGenerate() { + functionProvider.functionTools = [] + functionProvider.tools = [] + functionProvider.functionCallStrategy = .name("finalAnswer") + } + + public func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { + 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`): + \(baseScratchpad) + (Please continue with `Thought:` or call a function) + """) + } + + public func validateTools(tools: [AgentTool]) throws { + // no validation + } + + public func parseOutput(_ message: ChatMessage) async -> AgentNextStep { + if message.role == .function, let functionCall = message.functionCall { + if let function = functionProvider.functionTools.first(where: { + $0.name == functionCall.name + }) { + do { + let result = try await function + .call(argumentsJsonString: functionCall.arguments) + return .actions([.init( + toolName: functionCall.name, + toolInput: result.botReadableContent, + log: result.botReadableContent + )]) + } catch { + return .actions([.init( + toolName: functionCall.name, + toolInput: error.localizedDescription, + log: error.localizedDescription + )]) + } + } + } + + return await ChatAgent( + chatModel: chatModelChain.chatModel, + tools: functionProvider.tools, + preferredLanguage: "" + ) + .parseOutput(message) + } +} + From 477c6fc75040186b5841631d04461a9f77e0e066 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 17 Sep 2023 22:44:28 +0800 Subject: [PATCH 11/67] Update function call base agent --- Tool/Sources/LangChain/Agent.swift | 47 +++--- Tool/Sources/LangChain/AgentExecutor.swift | 43 ++++- Tool/Sources/LangChain/Agents/ChatAgent.swift | 15 +- .../Agents/FunctionCallingChatAgent.swift | 147 ++++++++++++------ 4 files changed, 169 insertions(+), 83 deletions(-) diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 3bd15b5b..46485199 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -21,18 +21,18 @@ public struct AgentAction: Equatable { } public extension CallbackEvents { - struct AgentDidFinish: CallbackEvent { - public let info: AgentFinish + struct AgentDidFinish: CallbackEvent { + public let info: AgentFinish } - - var agentDidFinish: AgentDidFinish.Type { - AgentDidFinish.self + + func agentDidFinish() -> AgentDidFinish.Type { + AgentDidFinish.self } struct AgentActionDidStart: CallbackEvent { public let info: AgentAction } - + var agentActionDidStart: AgentActionDidStart.Type { AgentActionDidStart.self } @@ -40,25 +40,30 @@ public extension CallbackEvents { struct AgentActionDidEnd: CallbackEvent { public let info: AgentAction } - + var agentActionDidEnd: AgentActionDidEnd.Type { AgentActionDidEnd.self } } -public struct AgentFinish: Equatable { - public var returnValue: String +public struct AgentFinish { + public enum ReturnValue { + case success(Output) + case failure(String) + } + + public var returnValue: ReturnValue public var log: String - public init(returnValue: String, log: String) { + public init(returnValue: ReturnValue, log: String) { self.returnValue = returnValue self.log = log } } -public enum AgentNextStep: Equatable { +public enum AgentNextStep { case actions([AgentAction]) - case finish(AgentFinish) + case finish(AgentFinish) } public enum AgentScratchPad: Equatable { @@ -94,6 +99,7 @@ public enum AgentEarlyStopHandleType: Equatable { public protocol Agent { associatedtype Input + associatedtype Output: AgentOutputParsable var chatModelChain: ChatModelChain> { get } var observationPrefix: String { get } var llmPrefix: String { get } @@ -101,8 +107,9 @@ public protocol Agent { func validateTools(tools: [AgentTool]) throws func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad func extraPlan(input: AgentInput) - func prepareForEarlyStopWithGenerate() - func parseOutput(_ output: ChatModelChain>.Output) async -> AgentNextStep + func prepareForEarlyStopWithGenerate() -> String + func parseOutput(_ output: ChatModelChain>.Output) async + -> AgentNextStep } public extension Agent { @@ -115,7 +122,7 @@ public extension Agent { input: Input, intermediateSteps: [AgentAction], callbackManagers: [CallbackManager] - ) async throws -> AgentNextStep { + ) async throws -> AgentNextStep { let input = getFullInputs(input: input, intermediateSteps: intermediateSteps) extraPlan(input: input) let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) @@ -127,11 +134,11 @@ public extension Agent { earlyStoppedHandleType: AgentEarlyStopHandleType, intermediateSteps: [AgentAction], callbackManagers: [CallbackManager] - ) async throws -> AgentFinish { + ) async throws -> AgentFinish { switch earlyStoppedHandleType { case .force: return AgentFinish( - returnValue: "Agent stopped due to iteration limit or time limit.", + returnValue: .failure("Agent stopped due to iteration limit or time limit."), log: "" ) case .generate: @@ -139,17 +146,17 @@ public extension Agent { thoughts += """ \(llmPrefix)I now need to return a final answer based on the previous steps: - (Please continue with `Final Answer:`) + \(prepareForEarlyStopWithGenerate()) """ let input = AgentInput(input: input, thoughts: .text(thoughts)) - prepareForEarlyStopWithGenerate() + let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) let nextAction = await parseOutput(output) switch nextAction { case let .finish(finish): return finish case .actions: - return AgentFinish(returnValue: output.content ?? "", log: output.content ?? "") + return .init(returnValue: .failure(output.content ?? ""), log: output.content ?? "") } } } diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index 40540f79..3be528e3 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -1,9 +1,23 @@ import Foundation -public actor AgentExecutor: Chain where InnerAgent.Input == String { +public protocol AgentOutputParsable { + static func parse(_ string: String) throws -> Self + var botReadableContent: String { get } +} + +extension String: AgentOutputParsable { + public static func parse(_ string: String) throws -> String { string } + public var botReadableContent: String { self } +} + +public actor AgentExecutor: Chain + where InnerAgent.Input == String, InnerAgent.Output: AgentOutputParsable +{ public typealias Input = String public struct Output { - let finalOutput: String + typealias FinalOutput = AgentFinish.ReturnValue + + let finalOutput: FinalOutput let intermediateSteps: [AgentAction] } @@ -96,7 +110,10 @@ public actor AgentExecutor: Chain where InnerAgent.Input == S } public nonisolated func parseOutput(_ output: Output) -> String { - output.finalOutput + switch output.finalOutput { + case let .failure(error): return error + case let .success(output): return output.botReadableContent + } } public func cancel() { @@ -109,7 +126,7 @@ struct InvalidToolError: Error {} extension AgentExecutor { func end( - output: AgentFinish, + output: AgentFinish, intermediateSteps: [AgentAction], callbackManagers: [CallbackManager] ) -> Output { @@ -120,18 +137,21 @@ extension AgentExecutor { return .init(finalOutput: finalOutput, intermediateSteps: intermediateSteps) } + /// Plan the scratch pad and let the agent decide what to do next func takeNextStep( input: Input, intermediateSteps: [AgentAction], callbackManagers: [CallbackManager] - ) async throws -> AgentNextStep { + ) async throws -> AgentNextStep { let output = try await agent.plan( input: input, intermediateSteps: intermediateSteps, callbackManagers: callbackManagers ) switch output { + // If the output says finish, then return the output immediately. case .finish: return output + // If the output contains actions, run them, and append the results to the scratch pad. case let .actions(actions): let completedActions = try await withThrowingTaskGroup(of: AgentAction.self) { taskGroup in @@ -157,10 +177,19 @@ extension AgentExecutor { } } - func getToolFinish(action: AgentAction) -> AgentFinish? { + func getToolFinish(action: AgentAction) -> AgentFinish? { guard let tool = tools[action.toolName] else { return nil } guard tool.returnDirectly else { return nil } - return .init(returnValue: action.observation ?? "", log: "") + + do { + let result = try InnerAgent.Output.parse(action.observation ?? "") + return .init(returnValue: .success(result), log: action.observation ?? "") + } catch { + return .init( + returnValue: .failure(action.observation ?? "no observation"), + log: action.observation ?? "" + ) + } } } diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift index 24b115c6..bbedbbba 100644 --- a/Tool/Sources/LangChain/Agents/ChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -35,6 +35,7 @@ private func formatInstruction(toolsNames: String, preferredLanguage: String) -> public class ChatAgent: Agent { public typealias Input = String + public typealias Output = String public var observationPrefix: String { "Observation: " } public var llmPrefix: String { "Thought: " } public let chatModelChain: ChatModelChain> @@ -111,28 +112,28 @@ public class ChatAgent: Agent { // do nothing } - public func prepareForEarlyStopWithGenerate() { - // do nothing + public func prepareForEarlyStopWithGenerate() -> String { + "(Please continue with `Final Answer:`)" } - public func parseOutput(_ output: ChatMessage) async -> AgentNextStep { + public func parseOutput(_ output: ChatMessage) async -> AgentNextStep { let text = output.content ?? "" - func parseFinalAnswerIfPossible() -> AgentNextStep? { + func parseFinalAnswerIfPossible() -> AgentNextStep? { let throughAnswerParser = PrefixThrough("Final Answer:") var parsableContent = text[...] do { _ = try throughAnswerParser.parse(&parsableContent) let answer = String(parsableContent) let output = answer.trimmingCharacters(in: .whitespacesAndNewlines) - return .finish(AgentFinish(returnValue: output, log: text)) + return .finish(AgentFinish(returnValue: .success(output), log: text)) } catch { Logger.langchain.info("Could not parse LLM output final answer: \(error)") return nil } } - func parseNextActionIfPossible() -> AgentNextStep? { + func parseNextActionIfPossible() -> AgentNextStep? { let throughActionBlockParser = PrefixThrough(""" Action: ``` @@ -179,7 +180,7 @@ public class ChatAgent: Agent { answer = "Sorry, I don't know." } - return .finish(AgentFinish(returnValue: String(answer), log: text)) + return .finish(AgentFinish(returnValue: .success(String(answer)), log: text)) } } diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index dcd77e34..ef692bd6 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -2,44 +2,70 @@ import Foundation import Logger import OpenAIService -public class FunctionCallingChatAgent: Agent { - struct EndFunction: ChatGPTFunction { - struct Argument: Codable { - let finalAnswer: String +public class FunctionCallingChatAgent: Agent { + public struct EndFunction: ChatGPTFunction { + public typealias Argument = Output + public typealias Result = String + public var name: String { "sendFinalAnswer" } + public var description: String { "Send the final answer to user" } + public let argumentSchema: JSONSchemaValue + public var reportProgress: (String) async -> Void = { _ in } + public func prepare() async {} + public func call(arguments: Argument) async throws -> Result { "" } + public init(argumentSchema: JSONSchemaValue) { + self.argumentSchema = argumentSchema + } + } + + public struct OtherToolFunction: ChatGPTFunction { + public struct Argument: Decodable { + public var __arg1: String } - typealias Result = String - - var name: String { "sendFinalAnswer" } - var description: String { "Send the final answer to user" } - var argumentSchema: JSONSchemaValue { - [ - .type: "object", - .properties: [ - "finalAnswer": [ - .type: "string", - .description: "the final answer to send to user", - ], + public typealias Result = String + public var name: String { tool.name } + public var description: String { tool.description } + public var argumentSchema: JSONSchemaValue { [ + .type: "object", + // This is a hack to get around the fact that some tools + // do not expose an args_schema, and expect an argument + // which is a string. + // And Open AI does not support an array type for the + // parameters. + .properties: [ + "__arg1": [ + "title": "__arg1", + .type: "string", ], - .required: ["finalAnswer"], - ] + ], + .required: ["__arg1"], + ] } + public var reportProgress: (String) async -> Void = { _ in } + public func prepare() async {} + public func call(arguments: Argument) async throws -> Result { + try await tool.run(input: arguments.__arg1) } - var reportProgress: (String) async -> Void = { _ in } - func prepare() async {} - func call(arguments: Argument) async throws -> Result { - return arguments.finalAnswer + let tool: AgentTool + public init(tool: AgentTool) { + self.tool = tool } } struct FunctionProvider: ChatGPTFunctionProvider { var tools: [AgentTool] = [] var functionTools: [any ChatGPTFunction] = [] + var endFunction: EndFunction var functions: [any ChatGPTFunction] { - functionTools + [EndFunction()] + shouldFinish + ? [endFunction] + : functionTools + tools.map(OtherToolFunction.init) + [endFunction] } - var functionCallStrategy: FunctionCallStrategy? = nil + var shouldFinish = false + var functionCallStrategy: FunctionCallStrategy? { + shouldFinish ? .name(endFunction.name) : nil + } } public typealias Input = String @@ -51,10 +77,16 @@ public class FunctionCallingChatAgent: Agent { public init( configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), memory: ChatGPTMemory = ConversationChatGPTMemory(systemPrompt: ""), - functions: [any ChatGPTFunction] = [], - tools: [AgentTool] = [] + tools: [AgentTool] = [], + endFunction: EndFunction ) { - functionProvider = .init(tools: tools, functionTools: functions) + let functions = tools.compactMap { $0 as? FunctionCallingAgentTool }.map(\.function) + let otherTools = tools.filter { !($0 is FunctionCallingAgentTool) } + functionProvider = .init( + tools: otherTools, + functionTools: functions, + endFunction: endFunction + ) chatModelChain = .init( chatModel: OpenAIChat( configuration: configuration.overriding { @@ -106,10 +138,9 @@ public class FunctionCallingChatAgent: Agent { // no extra plan } - public func prepareForEarlyStopWithGenerate() { - functionProvider.functionTools = [] - functionProvider.tools = [] - functionProvider.functionCallStrategy = .name("finalAnswer") + public func prepareForEarlyStopWithGenerate() -> String { + functionProvider.shouldFinish = true + return "(call sendFinalAnswer to finish)" } public func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { @@ -126,35 +157,53 @@ public class FunctionCallingChatAgent: Agent { // no validation } - public func parseOutput(_ message: ChatMessage) async -> AgentNextStep { + public func parseOutput(_ message: ChatMessage) async -> AgentNextStep { if message.role == .function, let functionCall = message.functionCall { if let function = functionProvider.functionTools.first(where: { $0.name == functionCall.name }) { - do { - let result = try await function - .call(argumentsJsonString: functionCall.arguments) - return .actions([.init( - toolName: functionCall.name, - toolInput: result.botReadableContent, - log: result.botReadableContent - )]) - } catch { - return .actions([.init( - toolName: functionCall.name, - toolInput: error.localizedDescription, - log: error.localizedDescription - )]) + if function.name == functionProvider.endFunction.name { + do { + let output = try Output.parse(functionCall.arguments) + return .finish(.init( + returnValue: .success(output), + log: functionCall.arguments + )) + } catch { + return .finish(.init( + returnValue: .failure(error.localizedDescription), + log: functionCall.arguments + )) + } + } else { + return .actions([ + .init( + toolName: function.name, + toolInput: functionCall.arguments, + log: functionCall.arguments + ), + ]) } } } + + // fallback to normal agent. - return await ChatAgent( + let stringBaseOutput = await ChatAgent( chatModel: chatModelChain.chatModel, tools: functionProvider.tools, preferredLanguage: "" - ) - .parseOutput(message) + ).parseOutput(message) + + switch stringBaseOutput { + case let .actions(actions): + return .actions(actions) + case let .finish(finish): + switch finish.returnValue { + case let .failure(x), let .success(x): + return .finish(.init(returnValue: .failure(x), log: finish.log)) + } + } } } From eb71146cc03ceb97e9e9672146a818a675dddca0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 00:41:26 +0800 Subject: [PATCH 12/67] Fix latestActiveXcode initialization --- Tool/Sources/XcodeInspector/XcodeInspector.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 60711fdc..39f83dea 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -77,6 +77,7 @@ public final class XcodeInspector: ObservableObject { .filter { $0.isXcode } .map(XcodeAppInstanceInspector.init(runningApplication:)) let activeXcode = xcodes.first(where: \.isActive) + latestActiveXcode = activeXcode ?? xcodes.first activeApplication = activeXcode ?? runningApplications .first(where: \.isActive) .map(AppInstanceInspector.init(runningApplication:)) From a08cca822818c5c5bd4990365b9852992b9a47b1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 00:42:04 +0800 Subject: [PATCH 13/67] Support no memory for OpenAIChat --- .../Sources/LangChain/Agents/FunctionCallingChatAgent.swift | 5 ++--- Tool/Sources/LangChain/ChatModel/OpenAIChat.swift | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index ef692bd6..e6efd443 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -76,7 +76,6 @@ public class FunctionCallingChatAgent: public init( configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), - memory: ChatGPTMemory = ConversationChatGPTMemory(systemPrompt: ""), tools: [AgentTool] = [], endFunction: EndFunction ) { @@ -92,7 +91,7 @@ public class FunctionCallingChatAgent: configuration: configuration.overriding { $0.runFunctionsAutomatically = false }, - memory: memory, + memory: nil, functionProvider: functionProvider, stream: false ), @@ -158,7 +157,7 @@ public class FunctionCallingChatAgent: } public func parseOutput(_ message: ChatMessage) async -> AgentNextStep { - if message.role == .function, let functionCall = message.functionCall { + if message.role == .assistant, let functionCall = message.functionCall { if let function = functionProvider.functionTools.first(where: { $0.name == functionCall.name }) { diff --git a/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift b/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift index bb9c7752..af7c52bd 100644 --- a/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift +++ b/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift @@ -3,13 +3,13 @@ import OpenAIService public struct OpenAIChat: ChatModel { public var configuration: ChatGPTConfiguration - public var memory: ChatGPTMemory + public var memory: ChatGPTMemory? public var functionProvider: ChatGPTFunctionProvider public var stream: Bool public init( configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), - memory: ChatGPTMemory = ConversationChatGPTMemory(systemPrompt: ""), + memory: ChatGPTMemory? = ConversationChatGPTMemory(systemPrompt: ""), functionProvider: ChatGPTFunctionProvider = NoChatGPTFunctionProvider(), stream: Bool ) { @@ -24,6 +24,8 @@ public struct OpenAIChat: ChatModel { stops: [String], callbackManagers: [CallbackManager] ) async throws -> ChatMessage { + let memory = memory ?? EmptyChatGPTMemory() + let service = ChatGPTService( memory: memory, configuration: configuration, From 57925ce03b1a3c4bf800f073a6d8548f2d481a0f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 00:42:08 +0800 Subject: [PATCH 14/67] Update --- Tool/Sources/LangChain/AgentExecutor.swift | 45 +++++++++++++++------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index 3be528e3..69efcd84 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -1,23 +1,13 @@ import Foundation -public protocol AgentOutputParsable { - static func parse(_ string: String) throws -> Self - var botReadableContent: String { get } -} - -extension String: AgentOutputParsable { - public static func parse(_ string: String) throws -> String { string } - public var botReadableContent: String { self } -} - public actor AgentExecutor: Chain where InnerAgent.Input == String, InnerAgent.Output: AgentOutputParsable { public typealias Input = String public struct Output { - typealias FinalOutput = AgentFinish.ReturnValue + public typealias FinalOutput = AgentFinish.ReturnValue - let finalOutput: FinalOutput + public let finalOutput: FinalOutput let intermediateSteps: [AgentAction] } @@ -180,7 +170,7 @@ extension AgentExecutor { func getToolFinish(action: AgentAction) -> AgentFinish? { guard let tool = tools[action.toolName] else { return nil } guard tool.returnDirectly else { return nil } - + do { let result = try InnerAgent.Output.parse(action.observation ?? "") return .init(returnValue: .success(result), log: action.observation ?? "") @@ -193,3 +183,32 @@ extension AgentExecutor { } } +// MARK: - AgentOutputParsable + +public protocol AgentOutputParsable { + static func parse(_ string: String) throws -> Self + var botReadableContent: String { get } +} + +extension String: AgentOutputParsable { + public static func parse(_ string: String) throws -> String { string } + public var botReadableContent: String { self } +} + +extension Int: AgentOutputParsable { + public static func parse(_ string: String) throws -> Int { + guard let int = Int(string) else { return 0 } + return int + } + + public var botReadableContent: String { String(self) } +} + +extension Double: AgentOutputParsable { + public static func parse(_ string: String) throws -> Double { + guard let double = Double(string) else { return 0 } + return double + } + + public var botReadableContent: String { String(self) } +} From 59e9a042bfb64b7514b56b65ca7c0c208b0bc1f5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 14:05:05 +0800 Subject: [PATCH 15/67] Make AgentScratchPad generic --- Tool/Sources/LangChain/Agent.swift | 58 ++++---------- Tool/Sources/LangChain/AgentTool.swift | 35 +++++++-- Tool/Sources/LangChain/Agents/ChatAgent.swift | 52 +++++++------ .../Agents/FunctionCallingChatAgent.swift | 78 ++++++++++--------- Tool/Sources/OpenAIService/Models.swift | 4 + 5 files changed, 118 insertions(+), 109 deletions(-) diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 46485199..8acbe61e 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -66,25 +66,15 @@ public enum AgentNextStep { case finish(AgentFinish) } -public enum AgentScratchPad: Equatable { - case text(String) - case messages([String]) - - var isEmpty: Bool { - switch self { - case let .text(text): - return text.isEmpty - case let .messages(messages): - return messages.isEmpty - } - } +public struct AgentScratchPad: Equatable { + var content: Content } -public struct AgentInput { +public struct AgentInput { var input: T - var thoughts: AgentScratchPad + var thoughts: AgentScratchPad - public init(input: T, thoughts: AgentScratchPad) { + public init(input: T, thoughts: AgentScratchPad) { self.input = input self.thoughts = thoughts } @@ -100,20 +90,24 @@ public enum AgentEarlyStopHandleType: Equatable { public protocol Agent { associatedtype Input associatedtype Output: AgentOutputParsable - var chatModelChain: ChatModelChain> { get } + associatedtype ScratchPadContent: Equatable + var chatModelChain: ChatModelChain> { get } var observationPrefix: String { get } var llmPrefix: String { get } func validateTools(tools: [AgentTool]) throws - func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad - func extraPlan(input: AgentInput) - func prepareForEarlyStopWithGenerate() -> String - func parseOutput(_ output: ChatModelChain>.Output) async + func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad + func constructFinalScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad + func extraPlan(input: AgentInput) + func parseOutput(_ output: ChatModelChain>.Output) async -> AgentNextStep } public extension Agent { - func getFullInputs(input: Input, intermediateSteps: [AgentAction]) -> AgentInput { + func getFullInputs( + input: Input, + intermediateSteps: [AgentAction] + ) -> AgentInput { let thoughts = constructScratchpad(intermediateSteps: intermediateSteps) return AgentInput(input: input, thoughts: thoughts) } @@ -142,14 +136,8 @@ public extension Agent { log: "" ) case .generate: - var thoughts = constructBaseScratchpad(intermediateSteps: intermediateSteps) - thoughts += """ - - \(llmPrefix)I now need to return a final answer based on the previous steps: - \(prepareForEarlyStopWithGenerate()) - """ - let input = AgentInput(input: input, thoughts: .text(thoughts)) - + let thoughts = constructFinalScratchpad(intermediateSteps: intermediateSteps) + let input = AgentInput(input: input, thoughts: thoughts) let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) let nextAction = await parseOutput(output) switch nextAction { @@ -160,16 +148,4 @@ public extension Agent { } } } - - func constructBaseScratchpad(intermediateSteps: [AgentAction]) -> String { - var thoughts = "" - for step in intermediateSteps { - thoughts += """ - \(step.log) - \(observationPrefix)\(step.observation ?? "") - """ - } - return thoughts - } } - diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift index 12bde7eb..331f8613 100644 --- a/Tool/Sources/LangChain/AgentTool.swift +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -31,18 +31,37 @@ public struct SimpleAgentTool: AgentTool { } } -public struct FunctionCallingAgentTool: AgentTool { - public let function: any ChatGPTFunction - public var name: String { function.name } - public var description: String { function.description } - public let returnDirectly: Bool - - public init(function: any ChatGPTFunction, returnDirectly: Bool = false) { +public struct FunctionCallingAgentTool: AgentTool, ChatGPTFunction { + public func call(arguments: F.Arguments) async throws -> F.Result { + try await function.call(arguments: arguments) + } + + public var argumentSchema: OpenAIService.JSONSchemaValue { function.argumentSchema } + + public func prepare() async { await function.prepare() } + + public var reportProgress: (String) async -> Void { + get { function.reportProgress } + set { function.reportProgress = newValue } + } + + public typealias Arguments = F.Arguments + public typealias Result = F.Result + + public var function: F + public var name: String + public var description: String + public var returnDirectly: Bool + + public init(function: F, returnDirectly: Bool = false) { self.function = function + name = function.name + description = function.description self.returnDirectly = returnDirectly } - + public func run(input: String) async throws -> String { try await function.call(argumentsJsonString: input).botReadableContent } } + diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift index bbedbbba..2bdc87c5 100644 --- a/Tool/Sources/LangChain/Agents/ChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -36,9 +36,10 @@ private func formatInstruction(toolsNames: String, preferredLanguage: String) -> public class ChatAgent: Agent { public typealias Input = String public typealias Output = String + public typealias ScratchPadContent = String public var observationPrefix: String { "Observation: " } public var llmPrefix: String { "Thought: " } - public let chatModelChain: ChatModelChain> + public let chatModelChain: ChatModelChain> let tools: [AgentTool] public init(chatModel: ChatModel, tools: [AgentTool], preferredLanguage: String) { @@ -68,54 +69,61 @@ public class ChatAgent: Agent { Begin! Reminder to always use the exact characters `Final Answer` when responding. """ ), - agentInput.thoughts.isEmpty + agentInput.thoughts.content.isEmpty ? .init(role: .user, content: agentInput.input) : .init( role: .user, content: """ \(agentInput.input) - \({ - switch agentInput.thoughts { - case let .text(text): - return text - case let .messages(messages): - return messages.map { message in - """ - \(message) - """ - }.joined(separator: "\n") - } - }()) + \(agentInput.thoughts.content) """ ), ] } ) } + + func constructBaseScratchpad(intermediateSteps: [AgentAction]) -> String { + var thoughts = "" + for step in intermediateSteps { + thoughts += """ + \(step.log) + \(observationPrefix)\(step.observation ?? "") + """ + } + return thoughts + } - public func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { + public func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) - if baseScratchpad.isEmpty { return .text("") } - return .text(""" + if baseScratchpad.isEmpty { return .init(content: "") } + return .init(content: """ 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:` or `Final Answer:`) """) } + + public func constructFinalScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { + let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) + if baseScratchpad.isEmpty { return .init(content: "") } + return .init(content: """ + This was your previous work (but I haven't seen any of it! I only see what you return as `Final Answer`): + \(baseScratchpad) + \(llmPrefix)I now need to return a final answer based on the previous steps: + "(Please continue with `Final Answer:`)" + """) + } public func validateTools(tools: [AgentTool]) throws { // no validation } - public func extraPlan(input: AgentInput) { + public func extraPlan(input: AgentInput) { // do nothing } - public func prepareForEarlyStopWithGenerate() -> String { - "(Please continue with `Final Answer:`)" - } - public func parseOutput(_ output: ChatMessage) async -> AgentNextStep { let text = output.content ?? "" diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index e6efd443..88c080b3 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -3,11 +3,13 @@ import Logger import OpenAIService public class FunctionCallingChatAgent: Agent { + public typealias ScratchPadContent = [ChatMessage] + public struct EndFunction: ChatGPTFunction { public typealias Argument = Output public typealias Result = String - public var name: String { "sendFinalAnswer" } - public var description: String { "Send the final answer to user" } + public var name: String { "saveFinalAnswer" } + public var description: String { "Save the final answer when it's ready" } public let argumentSchema: JSONSchemaValue public var reportProgress: (String) async -> Void = { _ in } public func prepare() async {} @@ -71,7 +73,7 @@ public class FunctionCallingChatAgent: public typealias Input = String public var observationPrefix: String { "Observation: " } public var llmPrefix: String { "Thought: " } - public let chatModelChain: ChatModelChain> + public let chatModelChain: ChatModelChain> var functionProvider: FunctionProvider public init( @@ -79,8 +81,8 @@ public class FunctionCallingChatAgent: tools: [AgentTool] = [], endFunction: EndFunction ) { - let functions = tools.compactMap { $0 as? FunctionCallingAgentTool }.map(\.function) - let otherTools = tools.filter { !($0 is FunctionCallingAgentTool) } + let functions = tools.compactMap { $0 as? (any ChatGPTFunction) } + let otherTools = tools.filter { !($0 is (any ChatGPTFunction)) } functionProvider = .init( tools: otherTools, functionTools: functions, @@ -102,38 +104,18 @@ public class FunctionCallingChatAgent: role: .system, content: """ Respond to the human as helpfully and accurately as possible. \ - Format final answer to be more readable, in a ordered list if possible. \ + Save the final answer when it's ready Begin! """ ), - agentInput.thoughts.isEmpty - ? .init(role: .user, content: agentInput.input) - : .init( - role: .user, - content: """ - \(agentInput.input) - - \({ - switch agentInput.thoughts { - case let .text(text): - return text - case let .messages(messages): - return messages.map { message in - """ - \(message) - """ - }.joined(separator: "\n") - } - }()) - """ - ), - ] + .init(role: .user, content: agentInput.input) + ] + agentInput.thoughts.content } ) } - public func extraPlan(input: AgentInput) { + public func extraPlan(input: AgentInput) { // no extra plan } @@ -142,14 +124,34 @@ public class FunctionCallingChatAgent: return "(call sendFinalAnswer to finish)" } - public func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { - 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`): - \(baseScratchpad) - (Please continue with `Thought:` or call a function) - """) + public func constructScratchpad( + intermediateSteps: [AgentAction] + ) -> AgentScratchPad { + let baseScratchpad = intermediateSteps.flatMap { + [ + ChatMessage( + role: .assistant, + content: nil, + functionCall: .init(name: $0.toolName, arguments: $0.toolInput) + ), + ChatMessage(role: .function, content: $0.observation), + ] + } + return .init(content: baseScratchpad) + } + + public func constructFinalScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { + let baseScratchpad = intermediateSteps.flatMap { + [ + ChatMessage( + role: .assistant, + content: nil, + functionCall: .init(name: $0.toolName, arguments: $0.toolInput) + ), + ChatMessage(role: .function, content: $0.observation), + ] + } + return .init(content: baseScratchpad) } public func validateTools(tools: [AgentTool]) throws { @@ -185,7 +187,7 @@ public class FunctionCallingChatAgent: } } } - + // fallback to normal agent. let stringBaseOutput = await ChatAgent( diff --git a/Tool/Sources/OpenAIService/Models.swift b/Tool/Sources/OpenAIService/Models.swift index 7a3475c2..1911f054 100644 --- a/Tool/Sources/OpenAIService/Models.swift +++ b/Tool/Sources/OpenAIService/Models.swift @@ -18,6 +18,10 @@ public struct ChatMessage: Equatable, Codable { public struct FunctionCall: Codable, Equatable { public var name: String public var arguments: String + public init(name: String, arguments: String) { + self.name = name + self.arguments = arguments + } } /// The role of a message. From 5e8596cbfb75e95ecc245c1df2a6fd9c5ab324dd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 14:07:52 +0800 Subject: [PATCH 16/67] Rename cases of ReturnValue --- Tool/Sources/LangChain/Agent.swift | 8 ++++---- Tool/Sources/LangChain/AgentExecutor.swift | 8 ++++---- Tool/Sources/LangChain/Agents/ChatAgent.swift | 4 ++-- .../LangChain/Agents/FunctionCallingChatAgent.swift | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 8acbe61e..9fcff9d1 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -48,8 +48,8 @@ public extension CallbackEvents { public struct AgentFinish { public enum ReturnValue { - case success(Output) - case failure(String) + case structured(Output) + case unstructured(String) } public var returnValue: ReturnValue @@ -132,7 +132,7 @@ public extension Agent { switch earlyStoppedHandleType { case .force: return AgentFinish( - returnValue: .failure("Agent stopped due to iteration limit or time limit."), + returnValue: .unstructured("Agent stopped due to iteration limit or time limit."), log: "" ) case .generate: @@ -144,7 +144,7 @@ public extension Agent { case let .finish(finish): return finish case .actions: - return .init(returnValue: .failure(output.content ?? ""), log: output.content ?? "") + return .init(returnValue: .unstructured(output.content ?? ""), log: output.content ?? "") } } } diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index 69efcd84..0a109e7d 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -101,8 +101,8 @@ public actor AgentExecutor: Chain public nonisolated func parseOutput(_ output: Output) -> String { switch output.finalOutput { - case let .failure(error): return error - case let .success(output): return output.botReadableContent + case let .unstructured(error): return error + case let .structured(output): return output.botReadableContent } } @@ -173,10 +173,10 @@ extension AgentExecutor { do { let result = try InnerAgent.Output.parse(action.observation ?? "") - return .init(returnValue: .success(result), log: action.observation ?? "") + return .init(returnValue: .structured(result), log: action.observation ?? "") } catch { return .init( - returnValue: .failure(action.observation ?? "no observation"), + returnValue: .unstructured(action.observation ?? "no observation"), log: action.observation ?? "" ) } diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift index 2bdc87c5..b78ff113 100644 --- a/Tool/Sources/LangChain/Agents/ChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -134,7 +134,7 @@ public class ChatAgent: Agent { _ = try throughAnswerParser.parse(&parsableContent) let answer = String(parsableContent) let output = answer.trimmingCharacters(in: .whitespacesAndNewlines) - return .finish(AgentFinish(returnValue: .success(output), log: text)) + return .finish(AgentFinish(returnValue: .structured(output), log: text)) } catch { Logger.langchain.info("Could not parse LLM output final answer: \(error)") return nil @@ -188,7 +188,7 @@ public class ChatAgent: Agent { answer = "Sorry, I don't know." } - return .finish(AgentFinish(returnValue: .success(String(answer)), log: text)) + return .finish(AgentFinish(returnValue: .structured(String(answer)), log: text)) } } diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index 88c080b3..0466bc1a 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -167,12 +167,12 @@ public class FunctionCallingChatAgent: do { let output = try Output.parse(functionCall.arguments) return .finish(.init( - returnValue: .success(output), + returnValue: .structured(output), log: functionCall.arguments )) } catch { return .finish(.init( - returnValue: .failure(error.localizedDescription), + returnValue: .unstructured(error.localizedDescription), log: functionCall.arguments )) } @@ -201,8 +201,8 @@ public class FunctionCallingChatAgent: return .actions(actions) case let .finish(finish): switch finish.returnValue { - case let .failure(x), let .success(x): - return .finish(.init(returnValue: .failure(x), log: finish.log)) + case let .unstructured(x), let .structured(x): + return .finish(.init(returnValue: .unstructured(x), log: finish.log)) } } } From e2c469fa9ef02276a9f4758f4d9539a83cea03b4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 15:18:26 +0800 Subject: [PATCH 17/67] Adjust agent --- Tool/Sources/LangChain/Agent.swift | 3 + Tool/Sources/LangChain/AgentExecutor.swift | 15 ++++ .../Agents/FunctionCallingChatAgent.swift | 83 +++++++++++-------- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 9fcff9d1..2913d2a1 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -64,6 +64,7 @@ public struct AgentFinish { public enum AgentNextStep { case actions([AgentAction]) case finish(AgentFinish) + case thought(String) } public struct AgentScratchPad: Equatable { @@ -145,6 +146,8 @@ public extension Agent { return finish case .actions: return .init(returnValue: .unstructured(output.content ?? ""), log: output.content ?? "") + case let .thought(content): + return .init(returnValue: .unstructured(content), log: content) } } } diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index 0a109e7d..2941d958 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -57,12 +57,14 @@ public actor AgentExecutor: Chain } while shouldContinue() { + try Task.checkCancellation() let nextStepOutput = try await takeNextStep( input: input, intermediateSteps: intermediateSteps, callbackManagers: callbackManagers ) + try Task.checkCancellation() switch nextStepOutput { case let .finish(finish): return end( @@ -82,6 +84,8 @@ public actor AgentExecutor: Chain callbackManagers: callbackManagers ) } + case .thought: + break } iterations += 1 } @@ -156,6 +160,7 @@ extension AgentExecutor { } var completedActions = [AgentAction]() for try await action in taskGroup { + try Task.checkCancellation() completedActions.append(action) callbackManagers .forEach { $0.send(CallbackEvents.AgentActionDidEnd(info: action)) } @@ -164,6 +169,15 @@ extension AgentExecutor { } return .actions(completedActions) + case let .thought(content): + return .actions([ + .init( + toolName: "Thought", + toolInput: content, + log: "Thought: \(content)", + observation: nil + ), + ]) } } @@ -212,3 +226,4 @@ extension Double: AgentOutputParsable { public var botReadableContent: String { String(self) } } + diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index 0466bc1a..f7be5d0c 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -4,7 +4,7 @@ import OpenAIService public class FunctionCallingChatAgent: Agent { public typealias ScratchPadContent = [ChatMessage] - + public struct EndFunction: ChatGPTFunction { public typealias Argument = Output public typealias Result = String @@ -79,7 +79,8 @@ public class FunctionCallingChatAgent: public init( configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), tools: [AgentTool] = [], - endFunction: EndFunction + endFunction: EndFunction, + extraSystemPrompt: String = "" ) { let functions = tools.compactMap { $0 as? (any ChatGPTFunction) } let otherTools = tools.filter { !($0 is (any ChatGPTFunction)) } @@ -103,13 +104,13 @@ public class FunctionCallingChatAgent: .init( role: .system, content: """ - Respond to the human as helpfully and accurately as possible. \ - Save the final answer when it's ready - - Begin! + Gather information using functions, and generate a final answer to my query as helpfully and accurately as possible. + You don't ask me for additional information. + \(extraSystemPrompt) + When you have the final answer, you MUST call `\(endFunction.name)` to save it. """ ), - .init(role: .user, content: agentInput.input) + .init(role: .user, content: agentInput.input), ] + agentInput.thoughts.content } ) @@ -118,49 +119,57 @@ public class FunctionCallingChatAgent: public func extraPlan(input: AgentInput) { // no extra plan } - - public func prepareForEarlyStopWithGenerate() -> String { - functionProvider.shouldFinish = true - return "(call sendFinalAnswer to finish)" - } - - public func constructScratchpad( + + func constructBaseScratchpad( intermediateSteps: [AgentAction] - ) -> AgentScratchPad { - let baseScratchpad = intermediateSteps.flatMap { - [ + ) -> ScratchPadContent { + return intermediateSteps.flatMap { + if let observation = $0.observation { + return [ + ChatMessage( + role: .assistant, + content: nil, + functionCall: .init(name: $0.toolName, arguments: $0.toolInput) + ), + ChatMessage(role: .function, content: observation, name: $0.toolName), + ] + } + return [ + ChatMessage(role: .assistant, content: $0.toolInput), ChatMessage( - role: .assistant, - content: nil, - functionCall: .init(name: $0.toolName, arguments: $0.toolInput) + role: .user, + content: "Please continue, call \(functionProvider.endFunction.name) when you are done." ), - ChatMessage(role: .function, content: $0.observation), ] } - return .init(content: baseScratchpad) } - - public func constructFinalScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { - let baseScratchpad = intermediateSteps.flatMap { - [ - ChatMessage( - role: .assistant, - content: nil, - functionCall: .init(name: $0.toolName, arguments: $0.toolInput) - ), - ChatMessage(role: .function, content: $0.observation), - ] - } + + public func constructScratchpad( + intermediateSteps: [AgentAction] + ) -> AgentScratchPad { + functionProvider.shouldFinish = false + let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) return .init(content: baseScratchpad) } + public func constructFinalScratchpad( + intermediateSteps: [AgentAction] + ) -> AgentScratchPad { + functionProvider.shouldFinish = true + let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) + return .init(content: baseScratchpad + [ + ChatMessage(role: .assistant, content: "Now I need to save the final answer"), + ChatMessage(role: .user, content: "Please continue"), + ]) + } + public func validateTools(tools: [AgentTool]) throws { // no validation } public func parseOutput(_ message: ChatMessage) async -> AgentNextStep { - if message.role == .assistant, let functionCall = message.functionCall { - if let function = functionProvider.functionTools.first(where: { + if let functionCall = message.functionCall { + if let function = functionProvider.functions.first(where: { $0.name == functionCall.name }) { if function.name == functionProvider.endFunction.name { @@ -204,6 +213,8 @@ public class FunctionCallingChatAgent: case let .unstructured(x), let .structured(x): return .finish(.init(returnValue: .unstructured(x), log: finish.log)) } + case let .thought(content): + return .finish(.init(returnValue: .unstructured(content), log: content)) } } } From c1f1250d7a085c142edf5790d102825b8d6191c1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 15:25:14 +0800 Subject: [PATCH 18/67] Update prompt --- Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index f7be5d0c..eeef9189 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -104,7 +104,7 @@ public class FunctionCallingChatAgent: .init( role: .system, content: """ - Gather information using functions, and generate a final answer to my query as helpfully and accurately as possible. + Gather information using functions, and generate a final answer to my query as concisely, helpfully and accurately as possible. You don't ask me for additional information. \(extraSystemPrompt) When you have the final answer, you MUST call `\(endFunction.name)` to save it. From 67c200e1d9e754adc59ead9700742bda79aaf93e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 15:25:52 +0800 Subject: [PATCH 19/67] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index a2af7c72..05159046 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a2af7c72476a8130058d5910ecf42580c9da15a5 +Subproject commit 0515904698b1312eaec44716938497f687548ae6 From ff01a983495e63b66a5d3bc3e13c33a7bec790d5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Sep 2023 22:56:51 +0800 Subject: [PATCH 20/67] Add ReActFunctionCallingChatAgent --- Pro | 2 +- Tool/Sources/LangChain/Agent.swift | 13 ++++++----- Tool/Sources/LangChain/AgentExecutor.swift | 22 ++++++++----------- .../Agents/FunctionCallingChatAgent.swift | 2 -- Tool/Sources/LangChain/Chains/LLMChain.swift | 6 ++--- Tool/Sources/Logger/Logger.swift | 7 +++++- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Pro b/Pro index 05159046..36828ea5 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 0515904698b1312eaec44716938497f687548ae6 +Subproject commit 36828ea5bb7da12b5a78cde0bf4b6393d1f3c991 diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 2913d2a1..3559b2ad 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -64,16 +64,19 @@ public struct AgentFinish { public enum AgentNextStep { case actions([AgentAction]) case finish(AgentFinish) - case thought(String) } public struct AgentScratchPad: Equatable { - var content: Content + public var content: Content + + public init(content: Content) { + self.content = content + } } public struct AgentInput { - var input: T - var thoughts: AgentScratchPad + public var input: T + public var thoughts: AgentScratchPad public init(input: T, thoughts: AgentScratchPad) { self.input = input @@ -146,8 +149,6 @@ public extension Agent { return finish case .actions: return .init(returnValue: .unstructured(output.content ?? ""), log: output.content ?? "") - case let .thought(content): - return .init(returnValue: .unstructured(content), log: content) } } } diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index 2941d958..26f250dd 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -18,19 +18,22 @@ public actor AgentExecutor: Chain var earlyStopHandleType: AgentEarlyStopHandleType var now: () -> Date = { Date() } var isCancelled = false + var initialSteps: [AgentAction] public init( agent: InnerAgent, tools: [AgentTool], maxIteration: Int? = 10, maxExecutionTime: Double? = nil, - earlyStopHandleType: AgentEarlyStopHandleType = .force + earlyStopHandleType: AgentEarlyStopHandleType = .force, + initialSteps: [AgentAction] = [] ) { self.agent = agent self.tools = tools.reduce(into: [:]) { $0[$1.name] = $1 } self.maxIteration = maxIteration self.maxExecutionTime = maxExecutionTime self.earlyStopHandleType = earlyStopHandleType + self.initialSteps = initialSteps } public func callLogic( @@ -41,7 +44,7 @@ public actor AgentExecutor: Chain let startTime = now().timeIntervalSince1970 var iterations = 0 - var intermediateSteps: [AgentAction] = [] + var intermediateSteps: [AgentAction] = initialSteps func shouldContinue() -> Bool { if isCancelled { return false } @@ -84,8 +87,6 @@ public actor AgentExecutor: Chain callbackManagers: callbackManagers ) } - case .thought: - break } iterations += 1 } @@ -152,6 +153,10 @@ extension AgentExecutor { for action in actions { callbackManagers .forEach { $0.send(CallbackEvents.AgentActionDidStart(info: action)) } + if action.observation != nil { + taskGroup.addTask { action } + continue + } guard let tool = tools[action.toolName] else { throw InvalidToolError() } taskGroup.addTask { let observation = try await tool.run(input: action.toolInput) @@ -169,15 +174,6 @@ extension AgentExecutor { } return .actions(completedActions) - case let .thought(content): - return .actions([ - .init( - toolName: "Thought", - toolInput: content, - log: "Thought: \(content)", - observation: nil - ), - ]) } } diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift index eeef9189..7b4ff47d 100644 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift @@ -213,8 +213,6 @@ public class FunctionCallingChatAgent: case let .unstructured(x), let .structured(x): return .finish(.init(returnValue: .unstructured(x), log: finish.log)) } - case let .thought(content): - return .finish(.init(returnValue: .unstructured(content), log: content)) } } } diff --git a/Tool/Sources/LangChain/Chains/LLMChain.swift b/Tool/Sources/LangChain/Chains/LLMChain.swift index 1fc546c2..1201bd45 100644 --- a/Tool/Sources/LangChain/Chains/LLMChain.swift +++ b/Tool/Sources/LangChain/Chains/LLMChain.swift @@ -3,9 +3,9 @@ import Foundation public class ChatModelChain: Chain { public typealias Output = ChatMessage - var chatModel: ChatModel - var promptTemplate: (Input) -> [ChatMessage] - var stops: [String] + public internal(set) var chatModel: ChatModel + public internal(set) var promptTemplate: (Input) -> [ChatMessage] + public internal(set) var stops: [String] public init( chatModel: ChatModel, diff --git a/Tool/Sources/Logger/Logger.swift b/Tool/Sources/Logger/Logger.swift index e78bf3a0..805e871d 100644 --- a/Tool/Sources/Logger/Logger.swift +++ b/Tool/Sources/Logger/Logger.swift @@ -10,6 +10,7 @@ enum LogLevel: String { public final class Logger { private let subsystem: String private let category: String + private let osLog: OSLog public static let service = Logger(category: "Service") public static let ui = Logger(category: "UI") @@ -25,6 +26,7 @@ public final class Logger { public init(subsystem: String = "com.intii.CopilotForXcode", category: String) { self.subsystem = subsystem self.category = category + osLog = OSLog(subsystem: subsystem, category: category) } func log(level: LogLevel, message: String) { @@ -38,7 +40,6 @@ public final class Logger { osLogType = .error } - let osLog = OSLog(subsystem: subsystem, category: category) os_log("%{public}@", log: osLog, type: osLogType, message as CVarArg) } @@ -57,4 +58,8 @@ public final class Logger { public func error(_ error: Error) { log(level: .error, message: error.localizedDescription) } + + public func signpost(_ type: OSSignpostType, name: StaticString) { + os_signpost(type, log: osLog, name: name) + } } From 144e4e63ef89a3939183937ff8111fec75244dcd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 19 Sep 2023 00:58:29 +0800 Subject: [PATCH 21/67] ReActFunctionCallingChatAgent --- Pro | 2 +- Tool/Sources/LangChain/Agent.swift | 2 +- Tool/Sources/LangChain/AgentExecutor.swift | 8 +++----- Tool/Sources/LangChain/Callback.swift | 16 ++++++++++------ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Pro b/Pro index 36828ea5..8dfae2b2 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 36828ea5bb7da12b5a78cde0bf4b6393d1f3c991 +Subproject commit 8dfae2b250861ab0b8f0e17310f34f5e0bb9e978 diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 3559b2ad..3fa565a8 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -25,7 +25,7 @@ public extension CallbackEvents { public let info: AgentFinish } - func agentDidFinish() -> AgentDidFinish.Type { + static func agentDidFinish() -> AgentDidFinish.Type { AgentDidFinish.self } diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index 26f250dd..f8b17462 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -25,7 +25,7 @@ public actor AgentExecutor: Chain tools: [AgentTool], maxIteration: Int? = 10, maxExecutionTime: Double? = nil, - earlyStopHandleType: AgentEarlyStopHandleType = .force, + earlyStopHandleType: AgentEarlyStopHandleType = .generate, initialSteps: [AgentAction] = [] ) { self.agent = agent @@ -151,8 +151,7 @@ extension AgentExecutor { let completedActions = try await withThrowingTaskGroup(of: AgentAction.self) { taskGroup in for action in actions { - callbackManagers - .forEach { $0.send(CallbackEvents.AgentActionDidStart(info: action)) } + callbackManagers.send(CallbackEvents.AgentActionDidStart(info: action)) if action.observation != nil { taskGroup.addTask { action } continue @@ -167,8 +166,7 @@ extension AgentExecutor { for try await action in taskGroup { try Task.checkCancellation() completedActions.append(action) - callbackManagers - .forEach { $0.send(CallbackEvents.AgentActionDidEnd(info: action)) } + callbackManagers.send(CallbackEvents.AgentActionDidEnd(info: action)) } return completedActions } diff --git a/Tool/Sources/LangChain/Callback.swift b/Tool/Sources/LangChain/Callback.swift index 3c0a6561..8d526961 100644 --- a/Tool/Sources/LangChain/Callback.swift +++ b/Tool/Sources/LangChain/Callback.swift @@ -10,6 +10,10 @@ public struct CallbackEvents { } public struct CallbackManager { + struct Observer { + let handler: (Event.Info) -> Void + } + fileprivate var observers = [Any]() public init() {} @@ -24,19 +28,19 @@ public struct CallbackManager { _: Event.Type = Event.self, _ handler: @escaping (Event.Info) -> Void ) { - observers.append(handler) + observers.append(Observer(handler: handler)) } public mutating func on( _: KeyPath, _ handler: @escaping (Event.Info) -> Void ) { - observers.append(handler) + observers.append(Observer(handler: handler)) } public func send(_ event: Event) { - for case let observer as ((Event.Info) -> Void) in observers { - observer(event.info) + for case let observer as Observer in observers { + observer.handler(event.info) } } @@ -44,8 +48,8 @@ public struct CallbackManager { _: KeyPath, _ info: Event.Info ) { - for case let observer as ((Event.Info) -> Void) in observers { - observer(info) + for case let observer as Observer in observers { + observer.handler(info) } } } From 87f876abf83007a97fbda8dea1dd5448302d9305 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 19 Sep 2023 14:02:26 +0800 Subject: [PATCH 22/67] Add StructuredOutputChatModelChain --- Tool/Sources/LangChain/Agent.swift | 2 - .../Agents/FunctionCallingChatAgent.swift | 219 ------------------ Tool/Sources/LangChain/Callback.swift | 19 ++ .../StructuredOutputChatModelChain.swift | 106 +++++++++ 4 files changed, 125 insertions(+), 221 deletions(-) delete mode 100644 Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift create mode 100644 Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 3fa565a8..4beb7532 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -96,8 +96,6 @@ public protocol Agent { associatedtype Output: AgentOutputParsable associatedtype ScratchPadContent: Equatable var chatModelChain: ChatModelChain> { get } - var observationPrefix: String { get } - var llmPrefix: String { get } func validateTools(tools: [AgentTool]) throws func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad diff --git a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift b/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift deleted file mode 100644 index 7b4ff47d..00000000 --- a/Tool/Sources/LangChain/Agents/FunctionCallingChatAgent.swift +++ /dev/null @@ -1,219 +0,0 @@ -import Foundation -import Logger -import OpenAIService - -public class FunctionCallingChatAgent: Agent { - public typealias ScratchPadContent = [ChatMessage] - - public struct EndFunction: ChatGPTFunction { - public typealias Argument = Output - public typealias Result = String - public var name: String { "saveFinalAnswer" } - public var description: String { "Save the final answer when it's ready" } - public let argumentSchema: JSONSchemaValue - public var reportProgress: (String) async -> Void = { _ in } - public func prepare() async {} - public func call(arguments: Argument) async throws -> Result { "" } - public init(argumentSchema: JSONSchemaValue) { - self.argumentSchema = argumentSchema - } - } - - public struct OtherToolFunction: ChatGPTFunction { - public struct Argument: Decodable { - public var __arg1: String - } - - public typealias Result = String - public var name: String { tool.name } - public var description: String { tool.description } - public var argumentSchema: JSONSchemaValue { [ - .type: "object", - // This is a hack to get around the fact that some tools - // do not expose an args_schema, and expect an argument - // which is a string. - // And Open AI does not support an array type for the - // parameters. - .properties: [ - "__arg1": [ - "title": "__arg1", - .type: "string", - ], - ], - .required: ["__arg1"], - ] } - public var reportProgress: (String) async -> Void = { _ in } - public func prepare() async {} - public func call(arguments: Argument) async throws -> Result { - try await tool.run(input: arguments.__arg1) - } - - let tool: AgentTool - public init(tool: AgentTool) { - self.tool = tool - } - } - - struct FunctionProvider: ChatGPTFunctionProvider { - var tools: [AgentTool] = [] - var functionTools: [any ChatGPTFunction] = [] - var endFunction: EndFunction - var functions: [any ChatGPTFunction] { - shouldFinish - ? [endFunction] - : functionTools + tools.map(OtherToolFunction.init) + [endFunction] - } - - var shouldFinish = false - var functionCallStrategy: FunctionCallStrategy? { - shouldFinish ? .name(endFunction.name) : nil - } - } - - public typealias Input = String - public var observationPrefix: String { "Observation: " } - public var llmPrefix: String { "Thought: " } - public let chatModelChain: ChatModelChain> - var functionProvider: FunctionProvider - - public init( - configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), - tools: [AgentTool] = [], - endFunction: EndFunction, - extraSystemPrompt: String = "" - ) { - let functions = tools.compactMap { $0 as? (any ChatGPTFunction) } - let otherTools = tools.filter { !($0 is (any ChatGPTFunction)) } - functionProvider = .init( - tools: otherTools, - functionTools: functions, - endFunction: endFunction - ) - chatModelChain = .init( - chatModel: OpenAIChat( - configuration: configuration.overriding { - $0.runFunctionsAutomatically = false - }, - memory: nil, - functionProvider: functionProvider, - stream: false - ), - stops: ["Observation:"], - promptTemplate: { agentInput in - [ - .init( - role: .system, - content: """ - Gather information using functions, and generate a final answer to my query as concisely, helpfully and accurately as possible. - You don't ask me for additional information. - \(extraSystemPrompt) - When you have the final answer, you MUST call `\(endFunction.name)` to save it. - """ - ), - .init(role: .user, content: agentInput.input), - ] + agentInput.thoughts.content - } - ) - } - - public func extraPlan(input: AgentInput) { - // no extra plan - } - - func constructBaseScratchpad( - intermediateSteps: [AgentAction] - ) -> ScratchPadContent { - return intermediateSteps.flatMap { - if let observation = $0.observation { - return [ - ChatMessage( - role: .assistant, - content: nil, - functionCall: .init(name: $0.toolName, arguments: $0.toolInput) - ), - ChatMessage(role: .function, content: observation, name: $0.toolName), - ] - } - return [ - ChatMessage(role: .assistant, content: $0.toolInput), - ChatMessage( - role: .user, - content: "Please continue, call \(functionProvider.endFunction.name) when you are done." - ), - ] - } - } - - public func constructScratchpad( - intermediateSteps: [AgentAction] - ) -> AgentScratchPad { - functionProvider.shouldFinish = false - let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) - return .init(content: baseScratchpad) - } - - public func constructFinalScratchpad( - intermediateSteps: [AgentAction] - ) -> AgentScratchPad { - functionProvider.shouldFinish = true - let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) - return .init(content: baseScratchpad + [ - ChatMessage(role: .assistant, content: "Now I need to save the final answer"), - ChatMessage(role: .user, content: "Please continue"), - ]) - } - - public func validateTools(tools: [AgentTool]) throws { - // no validation - } - - public func parseOutput(_ message: ChatMessage) async -> AgentNextStep { - if let functionCall = message.functionCall { - if let function = functionProvider.functions.first(where: { - $0.name == functionCall.name - }) { - if function.name == functionProvider.endFunction.name { - do { - let output = try Output.parse(functionCall.arguments) - return .finish(.init( - returnValue: .structured(output), - log: functionCall.arguments - )) - } catch { - return .finish(.init( - returnValue: .unstructured(error.localizedDescription), - log: functionCall.arguments - )) - } - } else { - return .actions([ - .init( - toolName: function.name, - toolInput: functionCall.arguments, - log: functionCall.arguments - ), - ]) - } - } - } - - // fallback to normal agent. - - let stringBaseOutput = await ChatAgent( - chatModel: chatModelChain.chatModel, - tools: functionProvider.tools, - preferredLanguage: "" - ).parseOutput(message) - - switch stringBaseOutput { - case let .actions(actions): - return .actions(actions) - case let .finish(finish): - switch finish.returnValue { - case let .unstructured(x), let .structured(x): - return .finish(.init(returnValue: .unstructured(x), log: finish.log)) - } - } - } -} - diff --git a/Tool/Sources/LangChain/Callback.swift b/Tool/Sources/LangChain/Callback.swift index 8d526961..e2d0a8d9 100644 --- a/Tool/Sources/LangChain/Callback.swift +++ b/Tool/Sources/LangChain/Callback.swift @@ -6,6 +6,15 @@ public protocol CallbackEvent { } public struct CallbackEvents { + public struct UnTypedEvent: CallbackEvent { + public var info: String + init(info: String) { + self.info = info + } + } + + public var untyped: UnTypedEvent.Type { UnTypedEvent.self } + private init() {} } @@ -52,6 +61,12 @@ public struct CallbackManager { observer.handler(info) } } + + public func send(_ string: String) { + for case let observer as Observer in observers { + observer.handler(string) + } + } } public extension [CallbackManager] { @@ -65,5 +80,9 @@ public extension [CallbackManager] { ) { for cb in self { cb.send(keyPath, info) } } + + func send(_ event: String) { + for cb in self { cb.send(event) } + } } diff --git a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift new file mode 100644 index 00000000..3ba43eb5 --- /dev/null +++ b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift @@ -0,0 +1,106 @@ +import Foundation +import Logger +import OpenAIService + +/// This is an agent used to get a structured output. +public class StructuredOutputChatModelChain: Chain { + public struct EndFunction: ChatGPTFunction { + public typealias Argument = Output + public typealias Result = String + public var name: String { "saveFinalAnswer" } + public var description: String { "Save the final answer when it's ready" } + public let argumentSchema: JSONSchemaValue + public var reportProgress: (String) async -> Void = { _ in } + public func prepare() async {} + public func call(arguments: Argument) async throws -> Result { "" } + public init(argumentSchema: JSONSchemaValue) { + self.argumentSchema = argumentSchema + } + } + + struct FunctionProvider: ChatGPTFunctionProvider { + var endFunction: EndFunction + var functions: [any ChatGPTFunction] { + [endFunction] + } + + var functionCallStrategy: FunctionCallStrategy? { + .name(endFunction.name) + } + } + + public typealias Input = String + public let chatModelChain: ChatModelChain + var functionProvider: FunctionProvider + + public init( + configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), + tools: [AgentTool] = [], + endFunction: EndFunction, + extraSystemPrompt: String = "" + ) { + functionProvider = .init( + endFunction: endFunction + ) + chatModelChain = .init( + chatModel: OpenAIChat( + configuration: configuration.overriding { + $0.runFunctionsAutomatically = false + }, + memory: nil, + functionProvider: functionProvider, + stream: false + ), + stops: ["Observation:"], + promptTemplate: { input in + [ + .init( + role: .system, + content: """ + You are a helpful assistant + Generate a final answer to my query as concisely, helpfully and accurately as possible. + You don't ask me for additional information. + \(extraSystemPrompt) + """ + ), + .init(role: .user, content: input), + ] + } + ) + } + + public func callLogic( + _ input: String, + callbackManagers: [CallbackManager] + ) async throws -> Output? { + let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) + return await parseOutput(output) + } + + public func parseOutput(_ output: Output?) -> String { + return String(describing: output) + } + + public func parseOutput(_ message: ChatMessage) async -> Output? { + if let functionCall = message.functionCall { + if let function = functionProvider.functions.first(where: { + $0.name == functionCall.name + }) { + if function.name == functionProvider.endFunction.name { + do { + let result = try JSONDecoder().decode( + Output.self, + from: functionCall.arguments.data(using: .utf8) ?? Data() + ) + return result + } catch { + return nil + } + } + } + } + + return nil + } +} + From 314841b93dbc5a4a444782deffb540d59dc3c5d8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 19 Sep 2023 14:02:38 +0800 Subject: [PATCH 23/67] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 8dfae2b2..f17934c1 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8dfae2b250861ab0b8f0e17310f34f5e0bb9e978 +Subproject commit f17934c1b63005f187a1fda43de9001bdfcfcddb From cfd11d8a5ef3aa6e8ee9eef1f1b5834a7f84d8bd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 19 Sep 2023 23:27:42 +0800 Subject: [PATCH 24/67] Update --- Tool/Sources/LangChain/AgentTool.swift | 2 +- Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift index 331f8613..23171fef 100644 --- a/Tool/Sources/LangChain/AgentTool.swift +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -56,7 +56,7 @@ public struct FunctionCallingAgentTool: AgentTool, ChatGPTFu public init(function: F, returnDirectly: Bool = false) { self.function = function name = function.name - description = function.description + description = "Run an action: \(function.description)" self.returnDirectly = returnDirectly } diff --git a/Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift b/Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift index 6ffc000e..6769ba3d 100644 --- a/Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift +++ b/Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift @@ -1,7 +1,7 @@ import Foundation public struct JSONSchemaKey: Codable, Hashable, Sendable, Equatable, ExpressibleByStringLiteral { - var key: String + public var key: String public init(stringLiteral: String) { key = stringLiteral From 48acf0498ef8faaa8d6116e5144b0e3426344c55 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 20 Sep 2023 22:15:42 +0800 Subject: [PATCH 25/67] Adjust API of ChatGPTFunction --- .../Functions/ExpandFocusRangeFunction.swift | 16 +++--- .../MoveToCodeAroundLineFunction.swift | 13 ++--- .../Functions/MoveToFocusedCodeFunction.swift | 21 ++++---- .../QueryWebsiteFunction.swift | 23 ++++++--- .../SearchFunction.swift | 13 ++--- Pro | 2 +- Tool/Sources/LangChain/Agent.swift | 24 +++++++-- Tool/Sources/LangChain/AgentTool.swift | 41 +++++++++++---- .../Chains/RefineDocumentChain.swift | 15 +----- .../RelevantInformationExtractionChain.swift | 23 +-------- .../StructuredOutputChatModelChain.swift | 8 +-- .../OpenAIService/ChatGPTService.swift | 16 +++--- .../FucntionCall/ChatGPTFunction.swift | 51 ++++++++++++++++--- 13 files changed, 157 insertions(+), 109 deletions(-) diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift index 136c35a7..a70efe36 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift @@ -13,13 +13,11 @@ struct ExpandFocusRangeFunction: ChatGPTFunction { "Editing Document Context is updated to display code at \(range)." } } - + struct E: Error, LocalizedError { var errorDescription: String? } - var reportProgress: (String) async -> Void = { _ in } - var name: String { "expandFocusRange" } @@ -32,18 +30,21 @@ struct ExpandFocusRangeFunction: ChatGPTFunction { .type: "object", .properties: [:], ] } - + weak var contextCollector: ActiveDocumentChatContextCollector? - + init(contextCollector: ActiveDocumentChatContextCollector) { self.contextCollector = contextCollector } - func prepare() async { + func prepare(reportProgress: @escaping (String) async -> Void) async { await reportProgress("Finding the focused code..") } - func call(arguments: Arguments) async throws -> Result { + func call( + arguments: Arguments, + reportProgress: @escaping (String) async -> Void + ) async throws -> Result { await reportProgress("Finding the focused code..") contextCollector?.activeDocumentContext?.expandFocusedRangeToContextRange() guard let newContext = contextCollector?.activeDocumentContext?.focusedContext else { @@ -56,3 +57,4 @@ struct ExpandFocusRangeFunction: ChatGPTFunction { return .init(range: newContext.codeRange) } } + diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift index 952b9b61..42ee50a2 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift @@ -15,13 +15,11 @@ struct MoveToCodeAroundLineFunction: ChatGPTFunction { "Editing Document Context is updated to display code at \(range)." } } - + struct E: Error, LocalizedError { var errorDescription: String? } - var reportProgress: (String) async -> Void = { _ in } - var name: String { "getCodeAtLine" } @@ -36,7 +34,7 @@ struct MoveToCodeAroundLineFunction: ChatGPTFunction { "line": [ .type: "number", .description: "The line number in the file", - ] + ], ], .required: ["line"], ] } @@ -47,11 +45,14 @@ struct MoveToCodeAroundLineFunction: ChatGPTFunction { self.contextCollector = contextCollector } - func prepare() async { + func prepare(reportProgress: @escaping (String) async -> Void) async { await reportProgress("Finding code around..") } - func call(arguments: Arguments) async throws -> Result { + func call( + arguments: Arguments, + reportProgress: @escaping (String) async -> Void + ) async throws -> Result { await reportProgress("Finding code around line \(arguments.line)..") contextCollector?.activeDocumentContext?.moveToCodeAroundLine(arguments.line) guard let newContext = contextCollector?.activeDocumentContext?.focusedContext else { diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift index af667524..3b3096a2 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift @@ -4,7 +4,7 @@ import OpenAIService import SuggestionModel struct MoveToFocusedCodeFunction: ChatGPTFunction { - struct Arguments: Codable {} + typealias Arguments = NoArguments struct Result: ChatGPTFunctionResult { var range: CursorRange @@ -13,13 +13,11 @@ struct MoveToFocusedCodeFunction: ChatGPTFunction { "Editing Document Context is updated to display code at \(range)." } } - + struct E: Error, LocalizedError { var errorDescription: String? } - var reportProgress: (String) async -> Void = { _ in } - var name: String { "moveToFocusedCode" } @@ -28,22 +26,20 @@ struct MoveToFocusedCodeFunction: ChatGPTFunction { "Move editing document context to the selected or focused code" } - var argumentSchema: JSONSchemaValue { [ - .type: "object", - .properties: [:], - ] } - weak var contextCollector: ActiveDocumentChatContextCollector? - + init(contextCollector: ActiveDocumentChatContextCollector) { self.contextCollector = contextCollector } - func prepare() async { + func prepare(reportProgress: @escaping (String) async -> Void) async { await reportProgress("Finding the focused code..") } - func call(arguments: Arguments) async throws -> Result { + func call( + arguments: Arguments, + reportProgress: @escaping (String) async -> Void + ) async throws -> Result { await reportProgress("Finding the focused code..") contextCollector?.activeDocumentContext?.moveToFocusedCode() guard let newContext = contextCollector?.activeDocumentContext?.focusedContext else { @@ -56,3 +52,4 @@ struct MoveToFocusedCodeFunction: ChatGPTFunction { return .init(range: newContext.codeRange) } } + diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift index ed2b84c0..e4a22903 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift @@ -17,8 +17,6 @@ struct QueryWebsiteFunction: ChatGPTFunction { } } - var reportProgress: (String) async -> Void = { _ in } - var name: String { "queryWebsite" } @@ -47,11 +45,14 @@ struct QueryWebsiteFunction: ChatGPTFunction { ] } - func prepare() async { + func prepare(reportProgress: @escaping (String) async -> Void) async { await reportProgress("Reading..") } - func call(arguments: Arguments) async throws -> Result { + func call( + arguments: Arguments, + reportProgress: @escaping (String) async -> Void + ) async throws -> Result { do { let embedding = OpenAIEmbedding(configuration: UserPreferenceEmbeddingConfiguration()) @@ -61,10 +62,13 @@ struct QueryWebsiteFunction: ChatGPTFunction { group.addTask { // 1. grab the website content await reportProgress("Loading \(url)..") - + if let database = await TemporaryUSearch.view(identifier: urlString) { await reportProgress("Getting relevant information..") - let qa = QAInformationRetrievalChain(vectorStore: database, embedding: embedding) + let qa = QAInformationRetrievalChain( + vectorStore: database, + embedding: embedding + ) return try await qa.call(.init(arguments.query)).information } let loader = WebLoader(urls: [url]) @@ -83,7 +87,10 @@ struct QueryWebsiteFunction: ChatGPTFunction { try await database.set(embeddedDocuments) // 4. generate answer await reportProgress("Getting relevant information..") - let qa = QAInformationRetrievalChain(vectorStore: database, embedding: embedding) + let qa = QAInformationRetrievalChain( + vectorStore: database, + embedding: embedding + ) let result = try await qa.call(.init(arguments.query)) return result.information } @@ -101,7 +108,7 @@ struct QueryWebsiteFunction: ChatGPTFunction { .joined(separator: "\n") ) """) - + return all } diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift index 348753b3..99c88312 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift @@ -28,10 +28,8 @@ struct SearchFunction: ChatGPTFunction { }.joined(separator: "\n") } } - - let maxTokens: Int - var reportProgress: (String) async -> Void = { _ in } + let maxTokens: Int var name: String { "searchWeb" @@ -62,11 +60,14 @@ struct SearchFunction: ChatGPTFunction { ] } - func prepare() async { + func prepare(reportProgress: @escaping ReportProgress) async { await reportProgress("Searching..") } - func call(arguments: Arguments) async throws -> Result { + func call( + arguments: Arguments, + reportProgress: @escaping ReportProgress + ) async throws -> Result { await reportProgress("Searching \(arguments.query)") do { @@ -74,7 +75,7 @@ struct SearchFunction: ChatGPTFunction { subscriptionKey: UserDefaults.shared.value(for: \.bingSearchSubscriptionKey), searchURL: UserDefaults.shared.value(for: \.bingSearchEndpoint) ) - + let result = try await bingSearch.search( query: arguments.query, numberOfResult: maxTokens > 5000 ? 5 : 3, diff --git a/Pro b/Pro index f17934c1..d02bbce0 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit f17934c1b63005f187a1fda43de9001bdfcfcddb +Subproject commit d02bbce00428d326bb91bddd946240714781f838 diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 4beb7532..640efa8f 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -44,6 +44,19 @@ public extension CallbackEvents { var agentActionDidEnd: AgentActionDidEnd.Type { AgentActionDidEnd.self } + + struct AgentFunctionCallingToolReportProgress: CallbackEvent { + public struct Info { + let functionName: String + let progress: String + } + + public let info: Info + } + + var agentFunctionCallingToolReportProgress: AgentFunctionCallingToolReportProgress.Type { + AgentFunctionCallingToolReportProgress.self + } } public struct AgentFinish { @@ -68,7 +81,7 @@ public enum AgentNextStep { public struct AgentScratchPad: Equatable { public var content: Content - + public init(content: Content) { self.content = content } @@ -99,7 +112,8 @@ public protocol Agent { func validateTools(tools: [AgentTool]) throws func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad - func constructFinalScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad + func constructFinalScratchpad(intermediateSteps: [AgentAction]) + -> AgentScratchPad func extraPlan(input: AgentInput) func parseOutput(_ output: ChatModelChain>.Output) async -> AgentNextStep @@ -146,8 +160,12 @@ public extension Agent { case let .finish(finish): return finish case .actions: - return .init(returnValue: .unstructured(output.content ?? ""), log: output.content ?? "") + return .init( + returnValue: .unstructured(output.content ?? ""), + log: output.content ?? "" + ) } } } } + diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift index 23171fef..d4907f86 100644 --- a/Tool/Sources/LangChain/AgentTool.swift +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -31,18 +31,17 @@ public struct SimpleAgentTool: AgentTool { } } -public struct FunctionCallingAgentTool: AgentTool, ChatGPTFunction { +public class FunctionCallingAgentTool: AgentTool { public func call(arguments: F.Arguments) async throws -> F.Result { - try await function.call(arguments: arguments) + try await function.call(arguments: arguments, reportProgress: reportProgress) } public var argumentSchema: OpenAIService.JSONSchemaValue { function.argumentSchema } - public func prepare() async { await function.prepare() } - - public var reportProgress: (String) async -> Void { - get { function.reportProgress } - set { function.reportProgress = newValue } + public func prepare() async { + await function.prepare(reportProgress: { [weak self] p in + self?.reportProgress(p) + }) } public typealias Arguments = F.Arguments @@ -53,15 +52,37 @@ public struct FunctionCallingAgentTool: AgentTool, ChatGPTFu public var description: String public var returnDirectly: Bool - public init(function: F, returnDirectly: Bool = false) { + let callbackManagers: [CallbackManager] + + public init( + function: F, + returnDirectly: Bool = false, + callbackManagers: [CallbackManager] = [] + ) { self.function = function + self.callbackManagers = callbackManagers name = function.name - description = "Run an action: \(function.description)" + description = function.description self.returnDirectly = returnDirectly } + func reportProgress(_ progress: String) { + callbackManagers.send( + CallbackEvents.AgentFunctionCallingToolReportProgress(info: .init( + functionName: name, + progress: progress + )) + ) + } + public func run(input: String) async throws -> String { - try await function.call(argumentsJsonString: input).botReadableContent + try await function.call( + argumentsJsonString: input, + reportProgress: { [weak self] p in + self?.reportProgress(p) + } + ) + .botReadableContent } } diff --git a/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift b/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift index 41829694..3c38eb3a 100644 --- a/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift +++ b/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift @@ -46,15 +46,8 @@ public final class RefineDocumentChain: Chain { var functions: [any ChatGPTFunction] = [RespondFunction()] } - struct RespondFunction: ChatGPTFunction { + struct RespondFunction: ChatGPTArgumentsCollectingFunction { typealias Arguments = IntermediateAnswer - - struct Result: ChatGPTFunctionResult { - var botReadableContent: String { "" } - } - - var reportProgress: (String) async -> Void = { _ in } - var name: String = "respond" var description: String = "Respond with the refined answer" var argumentSchema: JSONSchemaValue { @@ -77,12 +70,6 @@ public final class RefineDocumentChain: Chain { .required: ["answer", "more", "usefulness"], ] } - - func prepare() async {} - - func call(arguments: Arguments) async throws -> Result { - return Result() - } } func buildChatModel() -> ChatModelChain { diff --git a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift index 47bd7bbd..b28de4e4 100644 --- a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift +++ b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift @@ -19,29 +19,10 @@ public final class RelevantInformationExtractionChain: Chain { var functions: [any ChatGPTFunction] = [NoneFunction()] } - struct NoneFunction: ChatGPTFunction { - struct Arguments: Decodable {} - - struct Result: ChatGPTFunctionResult { - var botReadableContent: String { "" } - } - - var reportProgress: (String) async -> Void = { _ in } - + struct NoneFunction: ChatGPTArgumentsCollectingFunction { + typealias Arguments = NoArguments var name: String = "noInformationFound" var description: String = "Call when you can't find any relevant information from the document, or the question was not mentioned in the document" - var argumentSchema: JSONSchemaValue { - return [ - .type: "object", - .properties: .hash([:]) - ] - } - - func prepare() async {} - - func call(arguments: Arguments) async throws -> Result { - return Result() - } } func buildChatModel() -> ChatModelChain { diff --git a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift index 3ba43eb5..89983dcd 100644 --- a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift +++ b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift @@ -4,15 +4,11 @@ import OpenAIService /// This is an agent used to get a structured output. public class StructuredOutputChatModelChain: Chain { - public struct EndFunction: ChatGPTFunction { - public typealias Argument = Output - public typealias Result = String + public struct EndFunction: ChatGPTArgumentsCollectingFunction { + public typealias Arguments = Output public var name: String { "saveFinalAnswer" } public var description: String { "Save the final answer when it's ready" } public let argumentSchema: JSONSchemaValue - public var reportProgress: (String) async -> Void = { _ in } - public func prepare() async {} - public func call(arguments: Argument) async throws -> Result { "" } public init(argumentSchema: JSONSchemaValue) { self.argumentSchema = argumentSchema } diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index f8f598c8..b1f4ed1e 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -365,12 +365,11 @@ extension ChatGPTService { name: call.name ) await memory.appendMessage(responseMessage) - function.reportProgress = { [weak self] summary in + await function.prepare { [weak self] summary in await self?.memory.updateMessage(id: messageId) { message in message.summary = summary } } - await function.prepare() } /// Run a function call from the bot, and insert the result in memory. @@ -395,15 +394,14 @@ extension ChatGPTService { await memory.appendMessage(responseMessage) - function.reportProgress = { [weak self] summary in - await self?.memory.updateMessage(id: messageId) { message in - message.summary = summary - } - } - do { // Run the function - let result = try await function.call(argumentsJsonString: call.arguments) + let result = try await function.call(argumentsJsonString: call.arguments) { + [weak self] summary in + await self?.memory.updateMessage(id: messageId) { message in + message.summary = summary + } + } await memory.updateMessage(id: messageId) { message in message.content = result.botReadableContent diff --git a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift b/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift index f2b4f759..12dea023 100644 --- a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift +++ b/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift @@ -15,9 +15,13 @@ extension String: ChatGPTFunctionResult { public var botReadableContent: String { self } } +public struct NoChatGPTFunctionArguments: Decodable {} + public protocol ChatGPTFunction { + typealias NoArguments = NoChatGPTFunctionArguments associatedtype Arguments: Decodable associatedtype Result: ChatGPTFunctionResult + typealias ReportProgress = (String) async -> Void /// The name of this function. /// May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters. @@ -27,19 +31,54 @@ public protocol ChatGPTFunction { /// The arguments schema that the function take in [JSON schema](https://json-schema.org). var argumentSchema: JSONSchemaValue { get } /// Prepare to call the function - func prepare() async + func prepare(reportProgress: @escaping ReportProgress) async /// Call the function with the given arguments. - func call(arguments: Arguments) async throws -> Result - /// The message to present in different phases. - var reportProgress: (String) async -> Void { get set } + func call(arguments: Arguments, reportProgress: @escaping ReportProgress) async throws + -> Result } public extension ChatGPTFunction { /// Call the function with the given arguments in JSON. - func call(argumentsJsonString: String) async throws -> Result { + func call( + argumentsJsonString: String, + reportProgress: @escaping ReportProgress + ) async throws -> Result { let arguments = try JSONDecoder() .decode(Arguments.self, from: argumentsJsonString.data(using: .utf8) ?? Data()) - return try await call(arguments: arguments) + return try await call(arguments: arguments, reportProgress: reportProgress) + } +} + +public extension ChatGPTFunction where Arguments == NoArguments { + var argumentSchema: JSONSchemaValue { + [.type: "object", .properties: [:]] + } +} + +/// This kind of function is only used to get a structured output from the bot. +public protocol ChatGPTArgumentsCollectingFunction: ChatGPTFunction where Result == String {} + +public extension ChatGPTArgumentsCollectingFunction { + @available( + *, + deprecated, + message: "This function is only used to get a structured output from the bot." + ) + func prepare(reportProgress: @escaping ReportProgress = { _ in }) async { + assertionFailure("This function is only used to get a structured output from the bot.") + } + + @available( + *, + deprecated, + message: "This function is only used to get a structured output from the bot." + ) + func call( + arguments: Arguments, + reportProgress: @escaping ReportProgress = { _ in } + ) async throws -> Result { + assertionFailure("This function is only used to get a structured output from the bot.") + return "" } } From d4c811c3ec06f026d048c93378455b38448c7b7e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 00:18:27 +0800 Subject: [PATCH 26/67] Add realtimeActiveProjectURL --- Pro | 2 +- .../XcodeInspector/XcodeInspector.swift | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Pro b/Pro index d02bbce0..0034a4e8 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d02bbce00428d326bb91bddd946240714781f838 +Subproject commit 0034a4e8c8d5b56c4ca08bf635a2fef593773aff diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 39f83dea..cd3be2ab 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -70,6 +70,10 @@ public final class XcodeInspector: ObservableObject { public var realtimeActiveWorkspaceURL: URL { latestActiveXcode?.realtimeWorkspaceURL ?? activeWorkspaceURL } + + public var realtimeActiveProjectURL: URL { + latestActiveXcode?.realtimeProjectURL ?? activeWorkspaceURL + } init() { let runningApplications = NSWorkspace.shared.runningApplications @@ -231,26 +235,35 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { @Published public private(set) var completionPanel: AXUIElement? - public var realtimeDocumentURL: URL { + public var realtimeDocumentURL: URL? { guard let window = appElement.focusedWindow, window.identifier == "Xcode.WorkspaceWindow" else { - return URL(fileURLWithPath: "/") + return nil } return WorkspaceXcodeWindowInspector.extractDocumentURL(windowElement: window) - ?? URL(fileURLWithPath: "/") } - public var realtimeWorkspaceURL: URL { + public var realtimeWorkspaceURL: URL? { guard let window = appElement.focusedWindow, window.identifier == "Xcode.WorkspaceWindow" else { - return URL(fileURLWithPath: "/") + return nil } return WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) - ?? URL(fileURLWithPath: "/") + } + + public var realtimeProjectURL: URL? { + guard let window = appElement.focusedWindow else { return URL(fileURLWithPath: "/") } + let workspaceURL = realtimeWorkspaceURL + let documentURL = realtimeDocumentURL + return WorkspaceXcodeWindowInspector.extractProjectURL( + windowElement: window, + workspaceURL: workspaceURL, + documentURL: documentURL + ) } var _version: String? From 8334ce8d820ed7bd3778d852a044aa558bd1ad20 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 00:33:02 +0800 Subject: [PATCH 27/67] Make active project/workspace/document URL optional --- .../CustomCommandTemplateProcessor.swift | 2 +- Core/Sources/Service/GUI/ChatTabFactory.swift | 9 ++-- .../RealtimeSuggestionController.swift | 5 +- .../FeatureReducers/PanelFeature.swift | 8 +-- .../Sources/SuggestionWidget/WidgetView.swift | 9 ++-- ExtensionService/AppDelegate+Menu.swift | 18 ++++--- Pro | 2 +- .../XcodeInspector/XcodeInspector.swift | 49 +++++++++---------- 8 files changed, 53 insertions(+), 49 deletions(-) diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift index 5f611b01..e2f565d7 100644 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift @@ -34,7 +34,7 @@ struct CustomCommandTemplateProcessor { func getEditorInformation() -> EditorInformation { let editorContent = XcodeInspector.shared.focusedEditor?.content let documentURL = XcodeInspector.shared.activeDocumentURL - let language = languageIdentifierFromFileURL(documentURL) + let language = documentURL.map(languageIdentifierFromFileURL) ?? .plaintext return .init( editorContent: editorContent, diff --git a/Core/Sources/Service/GUI/ChatTabFactory.swift b/Core/Sources/Service/GUI/ChatTabFactory.swift index b574ff5b..6164f7c9 100644 --- a/Core/Sources/Service/GUI/ChatTabFactory.swift +++ b/Core/Sources/Service/GUI/ChatTabFactory.swift @@ -45,11 +45,10 @@ enum ChatTabFactory { let content = editor.content return .init( selectedText: content.selectedContent, - language: languageIdentifierFromFileURL( - XcodeInspector.shared - .activeDocumentURL - ) - .rawValue, + language: ( + XcodeInspector.shared.activeDocumentURL + .map(languageIdentifierFromFileURL) ?? .plaintext + ).rawValue, fileContent: content.content ) }, diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index 2c96b96f..e0069d84 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -109,8 +109,9 @@ public actor RealtimeSuggestionController { await self.triggerPrefetchDebounced() await self.notifyEditingFileChange(editor: focusElement) case kAXSelectedTextChangedNotification: - guard let sourceEditor = await sourceEditor else { continue } - let fileURL = XcodeInspector.shared.activeDocumentURL + guard let sourceEditor = await sourceEditor, + let fileURL = XcodeInspector.shared.activeDocumentURL + else { continue } await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded( fileURL: fileURL, sourceEditor: sourceEditor diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index df6fbdb7..271ee785 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -52,9 +52,9 @@ public struct PanelFeature: ReducerProtocol { switch action { case .presentSuggestion: return .run { send in - guard let provider = await fetchSuggestionProvider( - fileURL: xcodeInspector.activeDocumentURL - ) else { return } + guard let fileURL = xcodeInspector.activeDocumentURL, + let provider = await fetchSuggestionProvider(fileURL: fileURL) + else { return } await send(.presentSuggestionProvider(provider, displayContent: true)) } @@ -96,7 +96,7 @@ public struct PanelFeature: ReducerProtocol { case .switchToAnotherEditorAndUpdateContent: state.content.error = nil return .run { send in - let fileURL = xcodeInspector.activeDocumentURL + guard let fileURL = xcodeInspector.realtimeActiveDocumentURL else { return } if let suggestion = await fetchSuggestionProvider(fileURL: fileURL) { await send(.presentSuggestionProvider(suggestion, displayContent: false)) } diff --git a/Core/Sources/SuggestionWidget/WidgetView.swift b/Core/Sources/SuggestionWidget/WidgetView.swift index 5596ae89..ed37d3f3 100644 --- a/Core/Sources/SuggestionWidget/WidgetView.swift +++ b/Core/Sources/SuggestionWidget/WidgetView.swift @@ -10,7 +10,7 @@ struct WidgetView: View { @State var isHovering: Bool = false var onOpenChatClicked: () -> Void = {} var onCustomCommandClicked: (CustomCommand) -> Void = { _ in } - + @AppStorage(\.hideCircularWidget) var hideCircularWidget var body: some View { @@ -216,8 +216,9 @@ extension WidgetContextMenu { @ViewBuilder var enableSuggestionForProject: some View { WithViewStore(store) { _ in - let projectPath = xcodeInspector.activeProjectRootURL.path - if disableSuggestionFeatureGlobally { + if let projectPath = xcodeInspector.activeProjectRootURL?.path, + disableSuggestionFeatureGlobally + { let matchedPath = suggestionFeatureEnabledProjectList.first { path in projectPath.hasPrefix(path) } @@ -243,7 +244,7 @@ extension WidgetContextMenu { var disableSuggestionForLanguage: some View { WithViewStore(store) { _ in let fileURL = xcodeInspector.activeDocumentURL - let fileLanguage = languageIdentifierFromFileURL(fileURL) + let fileLanguage = fileURL.map(languageIdentifierFromFileURL) ?? .plaintext let matched = suggestionFeatureDisabledLanguageList.first { rawValue in fileLanguage.rawValue == rawValue } diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift index aac35e21..7951c756 100644 --- a/ExtensionService/AppDelegate+Menu.swift +++ b/ExtensionService/AppDelegate+Menu.swift @@ -95,9 +95,12 @@ extension AppDelegate: NSMenuDelegate { case xcodeInspectorDebugMenuIdentifier: let inspector = XcodeInspector.shared menu.items.removeAll() - menu.items.append(.text("Active Project: \(inspector.activeProjectRootURL)")) - menu.items.append(.text("Active Workspace: \(inspector.activeWorkspaceURL)")) - menu.items.append(.text("Active Document: \(inspector.activeDocumentURL)")) + menu.items + .append(.text("Active Project: \(inspector.activeProjectRootURL?.path ?? "N/A")")) + menu.items + .append(.text("Active Workspace: \(inspector.activeWorkspaceURL?.path ?? "N/A")")) + menu.items + .append(.text("Active Document: \(inspector.activeDocumentURL?.path ?? "N/A")")) for xcode in inspector.xcodes { let item = NSMenuItem( title: "Xcode \(xcode.runningApplication.processIdentifier)", @@ -108,9 +111,12 @@ extension AppDelegate: NSMenuDelegate { let xcodeMenu = NSMenu() item.submenu = xcodeMenu xcodeMenu.items.append(.text("Is Active: \(xcode.isActive)")) - xcodeMenu.items.append(.text("Active Project: \(xcode.projectRootURL)")) - xcodeMenu.items.append(.text("Active Workspace: \(xcode.workspaceURL)")) - xcodeMenu.items.append(.text("Active Document: \(xcode.documentURL)")) + xcodeMenu.items + .append(.text("Active Project: \(xcode.projectRootURL?.path ?? "N/A")")) + xcodeMenu.items + .append(.text("Active Workspace: \(xcode.workspaceURL?.path ?? "N/A")")) + xcodeMenu.items + .append(.text("Active Document: \(xcode.documentURL?.path ?? "N/A")")) for (key, workspace) in xcode.realtimeWorkspaces { let workspaceItem = NSMenuItem( diff --git a/Pro b/Pro index 0034a4e8..27e94aad 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 0034a4e8c8d5b56c4ca08bf635a2fef593773aff +Subproject commit 27e94aad79e10a07221288d36b9054f506e30e77 diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index cd3be2ab..75d696be 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -17,22 +17,23 @@ public final class XcodeInspector: ObservableObject { @Published public internal(set) var activeXcode: XcodeAppInstanceInspector? @Published public internal(set) var latestActiveXcode: XcodeAppInstanceInspector? @Published public internal(set) var xcodes: [XcodeAppInstanceInspector] = [] - @Published public internal(set) var activeProjectRootURL = URL(fileURLWithPath: "/") - @Published public internal(set) var activeDocumentURL = URL(fileURLWithPath: "/") - @Published public internal(set) var activeWorkspaceURL = URL(fileURLWithPath: "/") + @Published public internal(set) var activeProjectRootURL: URL? = nil + @Published public internal(set) var activeDocumentURL: URL? = nil + @Published public internal(set) var activeWorkspaceURL: URL? = nil @Published public internal(set) var focusedWindow: XcodeWindowInspector? @Published public internal(set) var focusedEditor: SourceEditor? @Published public internal(set) var focusedElement: AXUIElement? @Published public internal(set) var completionPanel: AXUIElement? public var focusedEditorContent: EditorInformation? { + guard let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL, + let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, + let projectURL = XcodeInspector.shared.activeProjectRootURL + else { return nil } + let editorContent = XcodeInspector.shared.focusedEditor?.content - let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL - let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL - let projectURL = XcodeInspector.shared.activeProjectRootURL let language = languageIdentifierFromFileURL(documentURL) - let relativePath = documentURL.path - .replacingOccurrences(of: projectURL.path, with: "") + let relativePath = documentURL.path.replacingOccurrences(of: projectURL.path, with: "") if let editorContent, let range = editorContent.selections.first { let (selectedContent, selectedLines) = EditorInformation.code( @@ -63,15 +64,15 @@ public final class XcodeInspector: ObservableObject { ) } - public var realtimeActiveDocumentURL: URL { + public var realtimeActiveDocumentURL: URL? { latestActiveXcode?.realtimeDocumentURL ?? activeDocumentURL } - - public var realtimeActiveWorkspaceURL: URL { + + public var realtimeActiveWorkspaceURL: URL? { latestActiveXcode?.realtimeWorkspaceURL ?? activeWorkspaceURL } - - public var realtimeActiveProjectURL: URL { + + public var realtimeActiveProjectURL: URL? { latestActiveXcode?.realtimeProjectURL ?? activeWorkspaceURL } @@ -192,7 +193,7 @@ public final class XcodeInspector: ObservableObject { xcode.$documentURL.sink { [weak self] url in self?.activeDocumentURL = url }.store(in: &activeXcodeCancellable) - + xcode.$workspaceURL.sink { [weak self] url in self?.activeWorkspaceURL = url }.store(in: &activeXcodeCancellable) @@ -224,9 +225,9 @@ public class AppInstanceInspector: ObservableObject { public final class XcodeAppInstanceInspector: AppInstanceInspector { @Published public var focusedWindow: XcodeWindowInspector? - @Published public var documentURL: URL = .init(fileURLWithPath: "/") - @Published public var workspaceURL: URL = .init(fileURLWithPath: "/") - @Published public var projectRootURL: URL = .init(fileURLWithPath: "/") + @Published public var documentURL: URL? = nil + @Published public var workspaceURL: URL? = nil + @Published public var projectRootURL: URL? = nil @Published public var workspaces = [WorkspaceIdentifier: Workspace]() public var realtimeWorkspaces: [WorkspaceIdentifier: WorkspaceInfo] { updateWorkspaceInfo() @@ -238,25 +239,21 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { public var realtimeDocumentURL: URL? { guard let window = appElement.focusedWindow, window.identifier == "Xcode.WorkspaceWindow" - else { - return nil - } + else { return nil } return WorkspaceXcodeWindowInspector.extractDocumentURL(windowElement: window) } - + public var realtimeWorkspaceURL: URL? { guard let window = appElement.focusedWindow, window.identifier == "Xcode.WorkspaceWindow" - else { - return nil - } + else { return nil } return WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) } - + public var realtimeProjectURL: URL? { - guard let window = appElement.focusedWindow else { return URL(fileURLWithPath: "/") } + guard let window = appElement.focusedWindow else { return nil } let workspaceURL = realtimeWorkspaceURL let documentURL = realtimeDocumentURL return WorkspaceXcodeWindowInspector.extractProjectURL( From 40fe5d42ed652a047f1738813c50f37866f5576e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 11:11:05 +0800 Subject: [PATCH 28/67] Update AgentExecutor --- Tool/Sources/LangChain/Agent.swift | 4 ++-- Tool/Sources/LangChain/AgentExecutor.swift | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift index 640efa8f..6eefec57 100644 --- a/Tool/Sources/LangChain/Agent.swift +++ b/Tool/Sources/LangChain/Agent.swift @@ -47,8 +47,8 @@ public extension CallbackEvents { struct AgentFunctionCallingToolReportProgress: CallbackEvent { public struct Info { - let functionName: String - let progress: String + public let functionName: String + public let progress: String } public let info: Info diff --git a/Tool/Sources/LangChain/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift index f8b17462..cfa9d4af 100644 --- a/Tool/Sources/LangChain/AgentExecutor.swift +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -158,8 +158,13 @@ extension AgentExecutor { } guard let tool = tools[action.toolName] else { throw InvalidToolError() } taskGroup.addTask { - let observation = try await tool.run(input: action.toolInput) - return action.observationAvailable(observation) + do { + let observation = try await tool.run(input: action.toolInput) + return action.observationAvailable(observation) + } catch { + let observation = error.localizedDescription + return action.observationAvailable(observation) + } } } var completedActions = [AgentAction]() From b30d42b0eec3d1246f6d1a4c6d40c3e0aab85752 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 11:11:22 +0800 Subject: [PATCH 29/67] Update --- .../LangChain/VectorStore/TemporaryUSearch.swift | 4 ++-- .../Configuration/EmbeddingConfiguration.swift | 1 + .../UserPreferenceEmbeddingConfiguration.swift | 14 +++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Tool/Sources/LangChain/VectorStore/TemporaryUSearch.swift b/Tool/Sources/LangChain/VectorStore/TemporaryUSearch.swift index f5a3cb3c..e0505bf8 100644 --- a/Tool/Sources/LangChain/VectorStore/TemporaryUSearch.swift +++ b/Tool/Sources/LangChain/VectorStore/TemporaryUSearch.swift @@ -18,11 +18,11 @@ public actor TemporaryUSearch: VectorStore { let index: USearchIndex var documents: [USearchLabel: LabeledDocument] = [:] - public init(identifier: String) { + public init(identifier: String, dimensions: Int = 1536 /* text-embedding-ada-002 */ ) { self.identifier = calculateMD5Hash(identifier) index = .init( metric: .IP, - dimensions: 1536, // text-embedding-ada-002 + dimensions: UInt32(dimensions), connectivity: 16, quantization: .F32 ) diff --git a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift index 0ad7cc07..eb068966 100644 --- a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift @@ -7,6 +7,7 @@ public protocol EmbeddingConfiguration { var model: EmbeddingModel { get } var apiKey: String { get } var maxToken: Int { get } + var dimensions: Int { get } } public extension EmbeddingConfiguration { diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift index 396cfd98..1ffd701f 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift @@ -14,6 +14,11 @@ public struct UserPreferenceEmbeddingConfiguration: EmbeddingConfiguration { model.info.maxTokens } + #warning("TODO: Support different dimensions.") + public var dimensions: Int { + 1536 // text-embedding-ada-002 + } + public init() {} } @@ -24,15 +29,18 @@ public class OverridingEmbeddingConfiguration< public var modelId: String? public var model: EmbeddingModel? public var maxTokens: Int? + public var dimensions: Int? public init( modelId: String? = nil, model: EmbeddingModel? = nil, - maxTokens: Int? = nil + maxTokens: Int? = nil, + dimensions: Int? = nil ) { self.modelId = modelId self.model = model self.maxTokens = maxTokens + self.dimensions = dimensions } } @@ -56,5 +64,9 @@ public class OverridingEmbeddingConfiguration< public var maxToken: Int { overriding.maxTokens ?? configuration.maxToken } + + public var dimensions: Int { + overriding.dimensions ?? configuration.dimensions + } } From bf94948ae67072a3dad85d76be6def3b58c384d5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 11:11:38 +0800 Subject: [PATCH 30/67] Update QAInformationRetrievalChain to support multiple vector store --- .../LangChain/Chains/RetrievalQA.swift | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/Tool/Sources/LangChain/Chains/RetrievalQA.swift b/Tool/Sources/LangChain/Chains/RetrievalQA.swift index 9cdcbd4b..86faa098 100644 --- a/Tool/Sources/LangChain/Chains/RetrievalQA.swift +++ b/Tool/Sources/LangChain/Chains/RetrievalQA.swift @@ -2,8 +2,9 @@ import Foundation import OpenAIService public final class QAInformationRetrievalChain: Chain { - let vectorStore: VectorStore + let vectorStores: [VectorStore] let embedding: Embeddings + let maxCount: Int public struct Output { public var information: String @@ -12,10 +13,22 @@ public final class QAInformationRetrievalChain: Chain { public init( vectorStore: VectorStore, - embedding: Embeddings + embedding: Embeddings, + maxCount: Int = 5 ) { - self.vectorStore = vectorStore + vectorStores = [vectorStore] self.embedding = embedding + self.maxCount = maxCount + } + + public init( + vectorStores: [VectorStore], + embedding: Embeddings, + maxCount: Int = 5 + ) { + self.vectorStores = vectorStores + self.embedding = embedding + self.maxCount = maxCount } public func callLogic( @@ -23,13 +36,28 @@ public final class QAInformationRetrievalChain: Chain { callbackManagers: [CallbackManager] ) async throws -> Output { let embeddedQuestion = try await embedding.embed(query: input) - let documents = try await vectorStore.searchWithDistance( - embeddings: embeddedQuestion, - count: 5 - ).filter { item in - item.distance < 0.31 - } + let documentsSlice = await withTaskGroup( + of: [(document: Document, distance: Float)].self + ) { group in + for vectorStore in vectorStores { + group.addTask { + (try? await vectorStore.searchWithDistance( + embeddings: embeddedQuestion, + count: 5 + ).filter { item in + item.distance < 0.31 + }) ?? [] + } + } + var result = [(document: Document, distance: Float)]() + for await items in group { + result.append(contentsOf: items) + } + return result + }.sorted { $0.distance < $1.distance }.prefix(maxCount) + let documents = Array(documentsSlice) + callbackManagers.send(CallbackEvents.RetrievalQADidExtractRelevantContent(info: documents)) let relevantInformationChain = RelevantInformationExtractionChain() From 14c8d0735561da11b965b5428c856534e242a6b9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:11:16 +0800 Subject: [PATCH 31/67] Add dimensions to embedding model --- Tool/Sources/AIModel/EmbeddingModel.swift | 4 ++++ .../UserPreferenceEmbeddingConfiguration.swift | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/AIModel/EmbeddingModel.swift b/Tool/Sources/AIModel/EmbeddingModel.swift index 174280d8..f690980e 100644 --- a/Tool/Sources/AIModel/EmbeddingModel.swift +++ b/Tool/Sources/AIModel/EmbeddingModel.swift @@ -29,6 +29,8 @@ public struct EmbeddingModel: Codable, Equatable, Identifiable { public var baseURL: String @FallbackDecoding public var maxTokens: Int + @FallbackDecoding + public var dimensions: Int @FallbackDecoding public var modelName: String public var azureOpenAIDeploymentName: String { @@ -40,11 +42,13 @@ public struct EmbeddingModel: Codable, Equatable, Identifiable { apiKeyName: String = "", baseURL: String = "", maxTokens: Int = 8192, + dimensions: Int = 1536, modelName: String = "" ) { self.apiKeyName = apiKeyName self.baseURL = baseURL self.maxTokens = maxTokens + self.dimensions = dimensions self.modelName = modelName } } diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift index 1ffd701f..e29e7ea7 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift @@ -14,9 +14,12 @@ public struct UserPreferenceEmbeddingConfiguration: EmbeddingConfiguration { model.info.maxTokens } - #warning("TODO: Support different dimensions.") public var dimensions: Int { - 1536 // text-embedding-ada-002 + let dimensions = model.info.dimensions + if dimensions <= 0 { + return 1536 + } + return dimensions } public init() {} From c74a50bb70cf9493ddf405de81763fdcf5d6ef6c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:11:24 +0800 Subject: [PATCH 32/67] Update --- .../FucntionCall/ChatGPTFunction.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift b/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift index 12dea023..420fc180 100644 --- a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift +++ b/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift @@ -80,6 +80,19 @@ public extension ChatGPTArgumentsCollectingFunction { assertionFailure("This function is only used to get a structured output from the bot.") return "" } + + @available( + *, + deprecated, + message: "This function is only used to get a structured output from the bot." + ) + func call( + argumentsJsonString: String, + reportProgress: @escaping ReportProgress + ) async throws -> Result { + assertionFailure("This function is only used to get a structured output from the bot.") + return "" + } } struct ChatGPTFunctionSchema: Codable, Equatable { From 01fed71ba17a129a87f80bf7905e1d6397da3eb5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:11:31 +0800 Subject: [PATCH 33/67] Add comment --- Tool/Sources/Logger/Logger.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tool/Sources/Logger/Logger.swift b/Tool/Sources/Logger/Logger.swift index 805e871d..f4d97d1e 100644 --- a/Tool/Sources/Logger/Logger.swift +++ b/Tool/Sources/Logger/Logger.swift @@ -20,6 +20,7 @@ public final class Logger { public static let codeium = Logger(category: "Codeium") public static let langchain = Logger(category: "LangChain") #if DEBUG + /// Use a temp logger to log something temporary. I won't be available in release builds. public static let temp = Logger(category: "Temp") #endif From b0605ba2da4c2bc951d80071ab68beed0128e01e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:11:43 +0800 Subject: [PATCH 34/67] Make the keys publick --- Tool/Sources/LangChain/DocumentLoader/TextLoader.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/LangChain/DocumentLoader/TextLoader.swift b/Tool/Sources/LangChain/DocumentLoader/TextLoader.swift index f506c4ad..1c588fe6 100644 --- a/Tool/Sources/LangChain/DocumentLoader/TextLoader.swift +++ b/Tool/Sources/LangChain/DocumentLoader/TextLoader.swift @@ -3,10 +3,11 @@ import Foundation /// Load a text document from local file. public struct TextLoader: DocumentLoader { - enum MetadataKeys { - static let filename = "filename" - static let `extension` = "extension" - static let contentModificationDate = "contentModificationDate" + public enum MetadataKeys { + public static let filename = "filename" + public static let `extension` = "extension" + public static let contentModificationDate = "contentModificationDate" + public static let filePath = "filePath" } let url: URL @@ -38,6 +39,7 @@ public struct TextLoader: DocumentLoader { MetadataKeys.contentModificationDate: .number( (modificationDate ?? Date()).timeIntervalSince1970 ), + MetadataKeys.filePath: .string(url.path), ])] } } From 4860fc83f92f4dbf7954b53b07b06a133089178b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:11:56 +0800 Subject: [PATCH 35/67] Add a method to access typed metadata keys --- Tool/Sources/LangChain/DocumentLoader/DocumentLoader.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tool/Sources/LangChain/DocumentLoader/DocumentLoader.swift b/Tool/Sources/LangChain/DocumentLoader/DocumentLoader.swift index 1c9b4627..020f5993 100644 --- a/Tool/Sources/LangChain/DocumentLoader/DocumentLoader.swift +++ b/Tool/Sources/LangChain/DocumentLoader/DocumentLoader.swift @@ -9,6 +9,11 @@ public struct Document: Codable { self.pageContent = pageContent self.metadata = metadata } + + public func metadata(_ keyPath: KeyPath) -> JSONValue? { + let key = Key.self[keyPath: keyPath] + return metadata[key] + } } public protocol DocumentLoader { From ba3b360102de8a67809e60c232682623ee200ba1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:12:15 +0800 Subject: [PATCH 36/67] Simplify usage of StructuredOutputChatModelChain --- .../StructuredOutputChatModelChain.swift | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift index 89983dcd..9783e182 100644 --- a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift +++ b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift @@ -5,12 +5,44 @@ import OpenAIService /// This is an agent used to get a structured output. public class StructuredOutputChatModelChain: Chain { public struct EndFunction: ChatGPTArgumentsCollectingFunction { - public typealias Arguments = Output - public var name: String { "saveFinalAnswer" } + public struct Arguments: Decodable { + var finalAnswer: Output + } + + public var name: String { "FinalAnswer" } public var description: String { "Save the final answer when it's ready" } - public let argumentSchema: JSONSchemaValue - public init(argumentSchema: JSONSchemaValue) { - self.argumentSchema = argumentSchema + public var argumentSchema: JSONSchemaValue { + return [ + .type: "object", + .properties: [ + "finalAnswer": .hash(finalAnswerSchema), + ], + .required: ["finalAnswer"], + ] + } + + public let finalAnswerSchema: [String: JSONSchemaValue] + + public init(argumentSchema: [String: JSONSchemaValue]) { + finalAnswerSchema = argumentSchema + } + + public init() where Output == String { + finalAnswerSchema = [ + JSONSchemaKey.type.key: "string", + ] + } + + public init() where Output == Int { + finalAnswerSchema = [ + JSONSchemaKey.type.key: "number", + ] + } + + public init() where Output == Double { + finalAnswerSchema = [ + JSONSchemaKey.type.key: "number", + ] } } @@ -79,20 +111,14 @@ public class StructuredOutputChatModelChain: Chain { public func parseOutput(_ message: ChatMessage) async -> Output? { if let functionCall = message.functionCall { - if let function = functionProvider.functions.first(where: { - $0.name == functionCall.name - }) { - if function.name == functionProvider.endFunction.name { - do { - let result = try JSONDecoder().decode( - Output.self, - from: functionCall.arguments.data(using: .utf8) ?? Data() - ) - return result - } catch { - return nil - } - } + do { + let result = try JSONDecoder().decode( + EndFunction.Arguments.self, + from: functionCall.arguments.data(using: .utf8) ?? Data() + ) + return result.finalAnswer + } catch { + return nil } } From 74df8f788046a9b8a3c35f362af4f890b8e88c99 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:12:43 +0800 Subject: [PATCH 37/67] Adjust implementation of QAInformationRetrievalChain --- .../RelevantInformationExtractionChain.swift | 65 ++++++++++++++++--- .../LangChain/Chains/RetrievalQA.swift | 21 ++++-- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift index b28de4e4..d55cafd4 100644 --- a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift +++ b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift @@ -15,44 +15,74 @@ public final class RelevantInformationExtractionChain: Chain { public typealias Output = String class FunctionProvider: ChatGPTFunctionProvider { - var functionCallStrategy: FunctionCallStrategy? = .auto - var functions: [any ChatGPTFunction] = [NoneFunction()] + var functionCallStrategy: FunctionCallStrategy? = .name("saveFinalAnswer") + var functions: [any ChatGPTFunction] = [FinalAnswer()] } - struct NoneFunction: ChatGPTArgumentsCollectingFunction { - typealias Arguments = NoArguments - var name: String = "noInformationFound" - var description: String = "Call when you can't find any relevant information from the document, or the question was not mentioned in the document" + struct FinalAnswer: ChatGPTArgumentsCollectingFunction { + struct Arguments: Decodable { + var relevantInformation: String + var noRelevantInformationFound: Bool? + } + + var name: String = "saveFinalAnswer" + var description: String = + "save the relevant information" + var argumentSchema: JSONSchemaValue { + [ + .type: "object", + .properties: [ + "relevantInformation": [.type: "string"], + "noRelevantInformationFound": [.type: "boolean"], + ], + .required: ["relevantInformation", "noRelevantInformationFound"], + ] + } + } + + let filterMetadata: (String) -> Bool + let hint: String + + init(filterMetadata: @escaping (String) -> Bool = { _ in true }, hint: String) { + self.filterMetadata = filterMetadata + self.hint = hint } func buildChatModel() -> ChatModelChain { .init( chatModel: OpenAIChat( configuration: UserPreferenceChatGPTConfiguration().overriding { - $0.temperature = 0 + $0.temperature = 0.5 $0.runFunctionsAutomatically = false }, memory: EmptyChatGPTMemory(), functionProvider: FunctionProvider(), stream: false ) - ) { input in [ + ) { [filterMetadata, hint] input in [ .init( role: .system, content: """ Extract the relevant information from the Document according to the Question. + The information may not directly answer the question, but it should be relevant to the question, \ + please think carefully and make you decision. Make the information clear, concise and short. If found code, wrap it in markdown code block. + \(hint) """ ), .init( role: .user, content: """ Question:### + (how, when, what or why) \(input.question) ### Document:### - \(input.document) + \(input.document.metadata.filter { key, _ in + filterMetadata(key) + }) + \(input.document.pageContent) ### """ ), @@ -73,6 +103,22 @@ public final class RelevantInformationExtractionChain: Chain { taskInput, callbackManagers: callbackManagers ) + + if let functionCall = output.functionCall { + do { + let arguments = try JSONDecoder().decode( + FinalAnswer.Arguments.self, + from: functionCall.arguments.data(using: .utf8) ?? Data() + ) + if arguments.noRelevantInformationFound ?? false { + return "" + } + return arguments.relevantInformation + } catch { + return output.content ?? "" + } + } + return output.content ?? "" } @@ -119,3 +165,4 @@ public extension CallbackEvents { RelevantInformationExtractionChainDidExtractPartialRelevantContent.self } } + diff --git a/Tool/Sources/LangChain/Chains/RetrievalQA.swift b/Tool/Sources/LangChain/Chains/RetrievalQA.swift index 86faa098..f9c399f3 100644 --- a/Tool/Sources/LangChain/Chains/RetrievalQA.swift +++ b/Tool/Sources/LangChain/Chains/RetrievalQA.swift @@ -5,6 +5,8 @@ public final class QAInformationRetrievalChain: Chain { let vectorStores: [VectorStore] let embedding: Embeddings let maxCount: Int + let filterMetadata: (String) -> Bool + let hint: String public struct Output { public var information: String @@ -14,21 +16,29 @@ public final class QAInformationRetrievalChain: Chain { public init( vectorStore: VectorStore, embedding: Embeddings, - maxCount: Int = 5 + maxCount: Int = 5, + filterMetadata: @escaping (String) -> Bool = { _ in true }, + hint: String = "" ) { vectorStores = [vectorStore] self.embedding = embedding self.maxCount = maxCount + self.filterMetadata = filterMetadata + self.hint = hint } public init( vectorStores: [VectorStore], embedding: Embeddings, - maxCount: Int = 5 + maxCount: Int = 5, + filterMetadata: @escaping (String) -> Bool = { _ in true }, + hint: String = "" ) { self.vectorStores = vectorStores self.embedding = embedding self.maxCount = maxCount + self.filterMetadata = filterMetadata + self.hint = hint } public func callLogic( @@ -57,10 +67,13 @@ public final class QAInformationRetrievalChain: Chain { }.sorted { $0.distance < $1.distance }.prefix(maxCount) let documents = Array(documentsSlice) - + callbackManagers.send(CallbackEvents.RetrievalQADidExtractRelevantContent(info: documents)) - let relevantInformationChain = RelevantInformationExtractionChain() + let relevantInformationChain = RelevantInformationExtractionChain( + filterMetadata: filterMetadata, + hint: hint + ) let relevantInformation = try await relevantInformationChain.run( .init(question: input, documents: documents), callbackManagers: callbackManagers From de74bfbb26db876bdfac6655ee42559558db0769 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:12:51 +0800 Subject: [PATCH 38/67] Fix FunctionCallingAgentTool --- Tool/Sources/LangChain/AgentTool.swift | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift index d4907f86..d221adad 100644 --- a/Tool/Sources/LangChain/AgentTool.swift +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -31,19 +31,13 @@ public struct SimpleAgentTool: AgentTool { } } -public class FunctionCallingAgentTool: AgentTool { +public class FunctionCallingAgentTool: AgentTool, ChatGPTFunction { public func call(arguments: F.Arguments) async throws -> F.Result { try await function.call(arguments: arguments, reportProgress: reportProgress) } public var argumentSchema: OpenAIService.JSONSchemaValue { function.argumentSchema } - public func prepare() async { - await function.prepare(reportProgress: { [weak self] p in - self?.reportProgress(p) - }) - } - public typealias Arguments = F.Arguments public typealias Result = F.Result @@ -76,7 +70,10 @@ public class FunctionCallingAgentTool: AgentTool { } public func run(input: String) async throws -> String { - try await function.call( + await prepare(reportProgress: { [weak self] p in + self?.reportProgress(p) + }) + return try await call( argumentsJsonString: input, reportProgress: { [weak self] p in self?.reportProgress(p) @@ -84,5 +81,18 @@ public class FunctionCallingAgentTool: AgentTool { ) .botReadableContent } + + public func prepare(reportProgress: @escaping ReportProgress) async { + await function.prepare(reportProgress: { [weak self] p in + self?.reportProgress(p) + }) + } + + public func call( + arguments: F.Arguments, + reportProgress: @escaping ReportProgress + ) async throws -> F.Result { + try await function.call(arguments: arguments, reportProgress: reportProgress) + } } From a0230b45356caa21134266b697e49685acfbe763 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Sep 2023 16:16:34 +0800 Subject: [PATCH 39/67] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 27e94aad..b7952d4e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 27e94aad79e10a07221288d36b9054f506e30e77 +Subproject commit b7952d4ef34d0a9da1167954207f1cfea22b2404 From 12cd285e7cbf4687ed654a6ca156e81015e94a6d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 22 Sep 2023 23:48:58 +0800 Subject: [PATCH 40/67] WIP --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index b7952d4e..9be768dc 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit b7952d4ef34d0a9da1167954207f1cfea22b2404 +Subproject commit 9be768dcf9a124a2f4283b6a71361fad8abb9142 From 4a702ab5f6c5478b8ef3763e31a1032ff5f0d936 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 25 Sep 2023 03:28:58 +0800 Subject: [PATCH 41/67] Update --- Pro | 2 +- Tool/Sources/LangChain/Callback.swift | 2 +- .../Chains/CombineAnswersChain.swift | 70 +++++++++++++++++++ ...wift => QAInformationRetrievalChain.swift} | 0 .../StructuredOutputChatModelChain.swift | 6 +- 5 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 Tool/Sources/LangChain/Chains/CombineAnswersChain.swift rename Tool/Sources/LangChain/Chains/{RetrievalQA.swift => QAInformationRetrievalChain.swift} (100%) diff --git a/Pro b/Pro index 9be768dc..9835ebad 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9be768dcf9a124a2f4283b6a71361fad8abb9142 +Subproject commit 9835ebad1c4c5362dc9b4e29763f563f0a2f4c3e diff --git a/Tool/Sources/LangChain/Callback.swift b/Tool/Sources/LangChain/Callback.swift index e2d0a8d9..57a1708b 100644 --- a/Tool/Sources/LangChain/Callback.swift +++ b/Tool/Sources/LangChain/Callback.swift @@ -8,7 +8,7 @@ public protocol CallbackEvent { public struct CallbackEvents { public struct UnTypedEvent: CallbackEvent { public var info: String - init(info: String) { + public init(info: String) { self.info = info } } diff --git a/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift b/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift new file mode 100644 index 00000000..d199b4f2 --- /dev/null +++ b/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift @@ -0,0 +1,70 @@ +import Foundation +import Logger +import OpenAIService + +public class CombineAnswersChain: Chain { + public struct Input: Decodable { + public var question: String + public var answers: [String] + public init(question: String, answers: [String]) { + self.question = question + self.answers = answers + } + } + + public typealias Output = String + public let chatModelChain: ChatModelChain + + public init( + configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), + extraInstructions: String = "" + ) { + chatModelChain = .init( + chatModel: OpenAIChat( + configuration: configuration.overriding { + $0.runFunctionsAutomatically = false + }, + memory: nil, + stream: false + ), + stops: ["Observation:"], + promptTemplate: { input in + [ + .init( + role: .system, + content: """ + You are a helpful assistant. + Your job is to combine multiple answers from different sources to one question. + \(extraInstructions) + """ + ), + .init(role: .user, content: """ + Question: \(input.question) + + Answers: + \(input.answers.joined(separator: "\n\(String(repeating: "-", count: 32))\n")) + + What is the combined answer? + """), + ] + } + ) + } + + public func callLogic( + _ input: Input, + callbackManagers: [CallbackManager] + ) async throws -> String { + let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) + return await parseOutput(output) + } + + public func parseOutput(_ message: ChatMessage) async -> String { + return message.content ?? "No answer." + } + + public func parseOutput(_ output: String) -> String { + output + } +} + diff --git a/Tool/Sources/LangChain/Chains/RetrievalQA.swift b/Tool/Sources/LangChain/Chains/QAInformationRetrievalChain.swift similarity index 100% rename from Tool/Sources/LangChain/Chains/RetrievalQA.swift rename to Tool/Sources/LangChain/Chains/QAInformationRetrievalChain.swift diff --git a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift index 9783e182..9c938cce 100644 --- a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift +++ b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift @@ -63,9 +63,8 @@ public class StructuredOutputChatModelChain: Chain { public init( configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), - tools: [AgentTool] = [], endFunction: EndFunction, - extraSystemPrompt: String = "" + promptTemplate: ((String) -> [ChatMessage])? = nil ) { functionProvider = .init( endFunction: endFunction @@ -80,7 +79,7 @@ public class StructuredOutputChatModelChain: Chain { stream: false ), stops: ["Observation:"], - promptTemplate: { input in + promptTemplate: promptTemplate ?? { input in [ .init( role: .system, @@ -88,7 +87,6 @@ public class StructuredOutputChatModelChain: Chain { You are a helpful assistant Generate a final answer to my query as concisely, helpfully and accurately as possible. You don't ask me for additional information. - \(extraSystemPrompt) """ ), .init(role: .user, content: input), From 3967eb41599515771b689a2ef9e72e600971cc92 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 17:59:24 +0800 Subject: [PATCH 42/67] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 9835ebad..2b33ae61 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9835ebad1c4c5362dc9b4e29763f563f0a2f4c3e +Subproject commit 2b33ae616c8b72d31b667184b23a562a9174e9ac From dffdf5fcb65f99e95aee4ff278d9ba32dca5a79d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 18:05:58 +0800 Subject: [PATCH 43/67] Update pro package --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 2b33ae61..8212f43e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 2b33ae616c8b72d31b667184b23a562a9174e9ac +Subproject commit 8212f43ee2ec7df0c013770b58ceb559ea116792 From 268c818c1504ce74d0aa5dbb0b3aabd139a163fa Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 18:35:16 +0800 Subject: [PATCH 44/67] Add settings keys for prompt to code chat model and embedding model --- .../OpenAIService/ChatGPTService.swift | 30 ++++++++++++------- .../Configuration/ChatGPTConfiguration.swift | 7 +++-- .../EmbeddingConfiguration.swift | 7 +++-- .../UserPreferenceChatGPTConfiguration.swift | 22 ++++++++++---- ...UserPreferenceEmbeddingConfiguration.swift | 28 ++++++++++++----- .../OpenAIService/EmbeddingService.swift | 8 +++-- Tool/Sources/Preferences/Keys.swift | 8 +++++ 7 files changed, 80 insertions(+), 30 deletions(-) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index b1f4ed1e..0dc01d6b 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -10,12 +10,18 @@ public protocol ChatGPTServiceType { } public enum ChatGPTServiceError: Error, LocalizedError { + case chatModelNotAvailable + case embeddingModelNotAvailable case endpointIncorrect case responseInvalid case otherError(String) public var errorDescription: String? { switch self { + case .chatModelNotAvailable: + return "Chat model is not available, please add a model in the settings." + case .embeddingModelNotAvailable: + return "Embedding model is not available, please add a model in the settings." case .endpointIncorrect: return "ChatGPT endpoint is incorrect" case .responseInvalid: @@ -180,8 +186,12 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream enabled. func sendMemory() async throws -> AsyncThrowingStream { - guard let url = URL(string: configuration.endpoint) - else { throw ChatGPTServiceError.endpointIncorrect } + guard let model = configuration.model else { + throw ChatGPTServiceError.chatModelNotAvailable + } + guard let url = URL(string: configuration.endpoint) else { + throw ChatGPTServiceError.endpointIncorrect + } await memory.refresh() @@ -197,8 +207,6 @@ extension ChatGPTService { } let remainingTokens = await memory.remainingTokens - let model = configuration.model - let requestBody = CompletionRequestBody( model: model.info.modelName, messages: messages, @@ -287,8 +295,12 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream disabled. func sendMemoryAndWait() async throws -> ChatMessage? { - guard let url = URL(string: configuration.endpoint) - else { throw ChatGPTServiceError.endpointIncorrect } + guard let model = configuration.model else { + throw ChatGPTServiceError.chatModelNotAvailable + } + guard let url = URL(string: configuration.endpoint) else { + throw ChatGPTServiceError.endpointIncorrect + } await memory.refresh() @@ -304,8 +316,6 @@ extension ChatGPTService { } let remainingTokens = await memory.remainingTokens - let model = configuration.model - let requestBody = CompletionRequestBody( model: model.info.modelName, messages: messages, @@ -357,7 +367,7 @@ extension ChatGPTService { /// When a function call is detected, but arguments are not yet ready, we can call this /// to insert a message placeholder in memory. func prepareFunctionCall(_ call: ChatMessage.FunctionCall, messageId: String) async { - guard var function = functionProvider.function(named: call.name) else { return } + guard let function = functionProvider.function(named: call.name) else { return } let responseMessage = ChatMessage( id: messageId, role: .function, @@ -380,7 +390,7 @@ extension ChatGPTService { ) async -> String { let messageId = messageId ?? uuidGenerator() - guard var function = functionProvider.function(named: call.name) else { + guard let function = functionProvider.function(named: call.name) else { return await fallbackFunctionCall(call, messageId: messageId) } diff --git a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift index 503b8e8f..aa441c67 100644 --- a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift @@ -4,7 +4,7 @@ import Preferences import Keychain public protocol ChatGPTConfiguration { - var model: ChatModel { get } + var model: ChatModel? { get } var temperature: Double { get } var apiKey: String { get } var stop: [String] { get } @@ -15,11 +15,12 @@ public protocol ChatGPTConfiguration { public extension ChatGPTConfiguration { var endpoint: String { - model.endpoint + model?.endpoint ?? "" } var apiKey: String { - (try? Keychain.apiKey.get(model.info.apiKeyName)) ?? "" + guard let name = model?.info.apiKeyName else { return "" } + return (try? Keychain.apiKey.get(name)) ?? "" } func overriding( diff --git a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift index eb068966..ae452729 100644 --- a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift @@ -4,7 +4,7 @@ import Keychain import Preferences public protocol EmbeddingConfiguration { - var model: EmbeddingModel { get } + var model: EmbeddingModel? { get } var apiKey: String { get } var maxToken: Int { get } var dimensions: Int { get } @@ -12,11 +12,12 @@ public protocol EmbeddingConfiguration { public extension EmbeddingConfiguration { var endpoint: String { - model.endpoint + model?.endpoint ?? "" } var apiKey: String { - (try? Keychain.apiKey.get(model.info.apiKeyName)) ?? "" + guard let name = model?.info.apiKeyName else { return "" } + return (try? Keychain.apiKey.get(name)) ?? "" } func overriding( diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index bc499d6f..4b83895b 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -3,19 +3,29 @@ import Foundation import Preferences public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { + public var chatModelKey: KeyPath>? + public var temperature: Double { min(max(0, UserDefaults.shared.value(for: \.chatGPTTemperature)), 2) } - public var model: ChatModel { + public var model: ChatModel? { let models = UserDefaults.shared.value(for: \.chatModels) + + if let chatModelKey { + let id = UserDefaults.shared.value(for: chatModelKey) + if let model = models.first(where: { $0.id == id }) { + return model + } + } + let id = UserDefaults.shared.value(for: \.defaultChatFeatureChatModelId) return models.first { $0.id == id } - ?? models.first ?? .init(id: "", name: "", format: .openAI, info: .init()) + ?? models.first } public var maxTokens: Int { - model.info.maxTokens + model?.info.maxTokens ?? 0 } public var stop: [String] { @@ -30,7 +40,9 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { true } - public init() {} + public init(chatModelKey: KeyPath>? = nil) { + self.chatModelKey = chatModelKey + } } public class OverridingChatGPTConfiguration: ChatGPTConfiguration { @@ -77,7 +89,7 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { overriding.temperature ?? configuration.temperature } - public var model: ChatModel { + public var model: ChatModel? { if let model = overriding.model { return model } let models = UserDefaults.shared.value(for: \.chatModels) guard let id = overriding.modelId, diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift index e29e7ea7..1c8ab2f9 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift @@ -3,26 +3,40 @@ import Foundation import Preferences public struct UserPreferenceEmbeddingConfiguration: EmbeddingConfiguration { - public var model: EmbeddingModel { + public var embeddingModelKey: KeyPath>? + + public var model: EmbeddingModel? { let models = UserDefaults.shared.value(for: \.embeddingModels) + + if let embeddingModelKey { + let id = UserDefaults.shared.value(for: embeddingModelKey) + if let model = models.first(where: { $0.id == id }) { + return model + } + } + let id = UserDefaults.shared.value(for: \.defaultChatFeatureEmbeddingModelId) return models.first { $0.id == id } - ?? models.first ?? .init(id: "", name: "", format: .openAI, info: .init()) + ?? models.first } public var maxToken: Int { - model.info.maxTokens + model?.info.maxTokens ?? 0 } public var dimensions: Int { - let dimensions = model.info.dimensions + let dimensions = model?.info.dimensions ?? 0 if dimensions <= 0 { return 1536 } return dimensions } - public init() {} + public init( + embeddingModelKey: KeyPath>? = nil + ) { + self.embeddingModelKey = embeddingModelKey + } } public class OverridingEmbeddingConfiguration< @@ -55,7 +69,7 @@ public class OverridingEmbeddingConfiguration< self.configuration = configuration } - public var model: EmbeddingModel { + public var model: EmbeddingModel? { if let model = overriding.model { return model } let models = UserDefaults.shared.value(for: \.embeddingModels) guard let id = overriding.modelId, @@ -67,7 +81,7 @@ public class OverridingEmbeddingConfiguration< public var maxToken: Int { overriding.maxTokens ?? configuration.maxToken } - + public var dimensions: Int { overriding.dimensions ?? configuration.dimensions } diff --git a/Tool/Sources/OpenAIService/EmbeddingService.swift b/Tool/Sources/OpenAIService/EmbeddingService.swift index a17b0863..d3bd1c8d 100644 --- a/Tool/Sources/OpenAIService/EmbeddingService.swift +++ b/Tool/Sources/OpenAIService/EmbeddingService.swift @@ -41,10 +41,12 @@ public struct EmbeddingService { } public func embed(text: [String]) async throws -> EmbeddingResponse { + guard let model = configuration.model else { + throw ChatGPTServiceError.embeddingModelNotAvailable + } guard let url = URL(string: configuration.endpoint) else { throw ChatGPTServiceError.endpointIncorrect } - let model = configuration.model var request = URLRequest(url: url) request.httpMethod = "POST" let encoder = JSONEncoder() @@ -90,10 +92,12 @@ public struct EmbeddingService { } public func embed(tokens: [[Int]]) async throws -> EmbeddingResponse { + guard let model = configuration.model else { + throw ChatGPTServiceError.embeddingModelNotAvailable + } guard let url = URL(string: configuration.endpoint) else { throw ChatGPTServiceError.endpointIncorrect } - let model = configuration.model var request = URLRequest(url: url) request.httpMethod = "POST" let encoder = JSONEncoder() diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index fc739f64..5feb0c14 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -264,6 +264,14 @@ public extension UserDefaultPreferenceKeys { var promptToCodeGenerateDescriptionInUserPreferredLanguage: PreferenceKey { .init(defaultValue: true, key: "PromptToCodeGenerateDescriptionInUserPreferredLanguage") } + + var promptToCodeChatModelId: PreferenceKey { + .init(defaultValue: "", key: "PromptToCodeChatModelId") + } + + var promptToCodeEmbeddingModelId: PreferenceKey { + .init(defaultValue: "", key: "PromptToCodeEmbeddingModelId") + } } // MARK: - Suggestion From d16f291e7fd5d78e5a82dc6d46c90294f3c1595c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 18:41:59 +0800 Subject: [PATCH 45/67] Add pickers for prompt to code chat/embedding model --- .../FeatureSettings/ChatSettingsView.swift | 4 +- .../PromptToCodeSettingsView.swift | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 6a4c83fc..880ab20d 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -44,7 +44,7 @@ struct ChatSettingsView: View { var chatSettingsForm: some View { Form { Picker( - "Chat Feature Provider", + "Chat Model", selection: $settings.defaultChatFeatureChatModelId ) { if !settings.chatModels @@ -63,7 +63,7 @@ struct ChatSettingsView: View { } Picker( - "Embedding Feature Provider", + "Embedding Model", selection: $settings.defaultChatFeatureEmbeddingModelId ) { if !settings.embeddingModels diff --git a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift index 126c19ad..de42264b 100644 --- a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift @@ -10,6 +10,13 @@ struct PromptToCodeSettingsView: View { var promptToCodeGenerateDescription @AppStorage(\.promptToCodeGenerateDescriptionInUserPreferredLanguage) var promptToCodeGenerateDescriptionInUserPreferredLanguage + @AppStorage(\.promptToCodeChatModelId) + var promptToCodeChatModelId + @AppStorage(\.promptToCodeEmbeddingModelId) + var promptToCodeEmbeddingModelId + + @AppStorage(\.chatModels) var chatModels + @AppStorage(\.embeddingModels) var embeddingModels init() {} } @@ -18,6 +25,50 @@ struct PromptToCodeSettingsView: View { var body: some View { VStack(alignment: .center) { Form { + Picker( + "Chat Model", + selection: $settings.promptToCodeChatModelId + ) { + Text("Same as Chat Feature").tag("") + + if !settings.chatModels + .contains(where: { $0.id == settings.promptToCodeChatModelId }), + !settings.promptToCodeChatModelId.isEmpty + { + Text( + (settings.chatModels.first?.name).map { "\($0) (Default)" } + ?? "No Model Found" + ) + .tag(settings.promptToCodeChatModelId) + } + + ForEach(settings.chatModels, id: \.id) { chatModel in + Text(chatModel.name).tag(chatModel.id) + } + } + + Picker( + "Embedding Model", + selection: $settings.promptToCodeEmbeddingModelId + ) { + Text("Same as Chat Feature").tag("") + + if !settings.embeddingModels + .contains(where: { $0.id == settings.promptToCodeEmbeddingModelId }), + !settings.promptToCodeEmbeddingModelId.isEmpty + { + Text( + (settings.embeddingModels.first?.name).map { "\($0) (Default)" } + ?? "No Model Found" + ) + .tag(settings.promptToCodeEmbeddingModelId) + } + + ForEach(settings.embeddingModels, id: \.id) { embeddingModel in + Text(embeddingModel.name).tag(embeddingModel.id) + } + } + Toggle(isOn: $settings.promptToCodeGenerateDescription) { Text("Generate Description") } From 74cc12d3a1e58c2db1894fbf85fbef2720d24f38 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 18:44:01 +0800 Subject: [PATCH 46/67] Update PromtToCodeService to use the defined models --- .../PromptToCodeService/OpenAIPromptToCodeService.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index 6ed62d1e..57c48195 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -171,8 +171,9 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { What is your requirement? """ - let configuration = UserPreferenceChatGPTConfiguration() - .overriding(.init(temperature: 0)) + let configuration = + UserPreferenceChatGPTConfiguration(chatModelKey: \.promptToCodeChatModelId) + .overriding(.init(temperature: 0)) let memory = AutoManagedChatGPTMemory( systemPrompt: systemPrompt, configuration: configuration, From d38a481f40b1eeb89f6ee0896a339cdc525de612 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 18:48:17 +0800 Subject: [PATCH 47/67] Add description to the function calling toggle --- .../ChatModelManagement/ChatModelEditView.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index 2427b442..4a815566 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -115,6 +115,13 @@ struct ChatModelEditView: View { "Supports Function Calling", isOn: viewStore.$supportsFunctionCalling ) + + Text( + "Function calling is required by some features, if this model doesn't support function calling, you should turn it off to avoid undefined behaviors." + ) + .foregroundColor(.secondary) + .font(.callout) + .dynamicHeightTextInFormWorkaround() } } From 46bf8aae1871f26bf49a0cea69d70e9a38dd2b2e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 26 Sep 2023 23:11:52 +0800 Subject: [PATCH 48/67] Fix a racing issue --- .../Filespace+SuggestionService.swift | 2 ++ .../SuggestionWorkspacePlugin.swift | 12 ------------ Tool/Sources/Workspace/Filespace.swift | 6 ++++-- Tool/Sources/Workspace/Workspace.swift | 5 +++-- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift b/Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift index a9382dd4..9d8437e5 100644 --- a/Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift +++ b/Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift @@ -13,6 +13,7 @@ struct FilespaceSuggestionSnapshotKey: FilespacePropertyKey { } extension FilespacePropertyValues { + @WorkspaceActor var suggestionSourceSnapshot: FilespaceSuggestionSnapshot { get { self[FilespaceSuggestionSnapshotKey.self] } set { self[FilespaceSuggestionSnapshotKey.self] = newValue } @@ -20,6 +21,7 @@ extension FilespacePropertyValues { } extension Filespace { + @WorkspaceActor func resetSnapshot() { // swiftformat:disable redundantSelf self.suggestionSourceSnapshot = FilespaceSuggestionSnapshotKey.createDefaultValue() diff --git a/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift b/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift index b0abcbab..9dd2d63f 100644 --- a/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift +++ b/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift @@ -57,18 +57,6 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { return true } - func canAutoTriggerGetSuggestions( - forFileAt fileURL: URL, - lines: [String], - cursorPosition: CursorPosition - ) -> Bool { - guard isRealtimeSuggestionEnabled else { return false } - guard let filespace = filespaces[fileURL] else { return true } - if lines.hashValue != filespace.suggestionSourceSnapshot.linesHash { return true } - if cursorPosition != filespace.suggestionSourceSnapshot.cursorPosition { return true } - return false - } - override init(workspace: Workspace) { super.init(workspace: workspace) diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index 57060305..2683ce13 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -8,8 +8,9 @@ public protocol FilespacePropertyKey { } public final class FilespacePropertyValues { - var storage: [ObjectIdentifier: Any] = [:] + private var storage: [ObjectIdentifier: Any] = [:] + @WorkspaceActor public subscript(_ key: K.Type) -> K.Value { get { if let value = storage[ObjectIdentifier(key)] as? K.Value { @@ -65,7 +66,7 @@ public final class Filespace { } private(set) var lastSuggestionUpdateTime: Date = Environment.now() - var additionalProperties = FilespacePropertyValues() + private var additionalProperties = FilespacePropertyValues() let fileSaveWatcher: FileSaveWatcher let onClose: (URL) -> Void @@ -87,6 +88,7 @@ public final class Filespace { } } + @WorkspaceActor public subscript( dynamicMember dynamicMember: WritableKeyPath ) -> K { diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index 3963c59b..c26626a5 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -10,8 +10,9 @@ public protocol WorkspacePropertyKey { } public class WorkspacePropertyValues { - var storage: [ObjectIdentifier: Any] = [:] + private var storage: [ObjectIdentifier: Any] = [:] + @WorkspaceActor public subscript(_ key: K.Type) -> K.Value { get { if let value = storage[ObjectIdentifier(key)] as? K.Value { @@ -56,7 +57,7 @@ public final class Workspace { } } - var additionalProperties = WorkspacePropertyValues() + private var additionalProperties = WorkspacePropertyValues() public internal(set) var plugins = [ObjectIdentifier: WorkspacePlugin]() public let workspaceURL: URL public let projectRootURL: URL From 9ee64dc4fa2ecb585e863e06b6e9b5e4e39d917c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:12:25 +0800 Subject: [PATCH 49/67] Update pro package --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 8212f43e..62eba155 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8212f43ee2ec7df0c013770b58ceb559ea116792 +Subproject commit 62eba155815f12bc509d46c75190313ca1193e39 From cc0d5d30078a06b2937a83b76475875b9141632c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:12:46 +0800 Subject: [PATCH 50/67] Remove generic of OverridingEmbeddingConfiguration --- .../Configuration/EmbeddingConfiguration.swift | 10 +++++----- .../UserPreferenceEmbeddingConfiguration.swift | 15 ++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift index ae452729..fdaba303 100644 --- a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift @@ -21,15 +21,15 @@ public extension EmbeddingConfiguration { } func overriding( - _ overrides: OverridingEmbeddingConfiguration.Overriding - ) -> OverridingEmbeddingConfiguration { + _ overrides: OverridingEmbeddingConfiguration.Overriding + ) -> OverridingEmbeddingConfiguration { .init(overriding: self, with: overrides) } func overriding( - _ update: (inout OverridingEmbeddingConfiguration.Overriding) -> Void = { _ in } - ) -> OverridingEmbeddingConfiguration { - var overrides = OverridingEmbeddingConfiguration.Overriding() + _ update: (inout OverridingEmbeddingConfiguration.Overriding) -> Void = { _ in } + ) -> OverridingEmbeddingConfiguration { + var overrides = OverridingEmbeddingConfiguration.Overriding() update(&overrides) return .init(overriding: self, with: overrides) } diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift index 1c8ab2f9..de400ba5 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift @@ -7,14 +7,14 @@ public struct UserPreferenceEmbeddingConfiguration: EmbeddingConfiguration { public var model: EmbeddingModel? { let models = UserDefaults.shared.value(for: \.embeddingModels) - + if let embeddingModelKey { let id = UserDefaults.shared.value(for: embeddingModelKey) if let model = models.first(where: { $0.id == id }) { return model } } - + let id = UserDefaults.shared.value(for: \.defaultChatFeatureEmbeddingModelId) return models.first { $0.id == id } ?? models.first @@ -39,9 +39,7 @@ public struct UserPreferenceEmbeddingConfiguration: EmbeddingConfiguration { } } -public class OverridingEmbeddingConfiguration< - Configuration: EmbeddingConfiguration ->: EmbeddingConfiguration { +public class OverridingEmbeddingConfiguration: EmbeddingConfiguration { public struct Overriding { public var modelId: String? public var model: EmbeddingModel? @@ -61,10 +59,13 @@ public class OverridingEmbeddingConfiguration< } } - private let configuration: Configuration + private let configuration: EmbeddingConfiguration public var overriding = Overriding() - public init(overriding configuration: Configuration, with overrides: Overriding = .init()) { + public init( + overriding configuration: any EmbeddingConfiguration, + with overrides: Overriding = .init() + ) { overriding = overrides self.configuration = configuration } From 30b556d1d4f6b97918eee1b43cbaaed71bcede49 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:13:35 +0800 Subject: [PATCH 51/67] Support changing temperature and model from chat tab --- .../ChatGPTChatTab/ChatContextMenu.swift | 94 ++++++++++++++++++- .../ChatGPTChatTab/ChatGPTChatTab.swift | 21 +++-- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 10 +- .../Sources/ChatGPTChatTab/ChatProvider.swift | 22 +++++ 4 files changed, 138 insertions(+), 9 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift index 6ead6a06..ab9a2aa7 100644 --- a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift +++ b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift @@ -4,7 +4,7 @@ import SwiftUI struct ChatTabItemView: View { @ObservedObject var chat: ChatProvider - + var body: some View { Text(chat.title) } @@ -13,6 +13,9 @@ struct ChatTabItemView: View { struct ChatContextMenu: View { @ObservedObject var chat: ChatProvider @AppStorage(\.customCommands) var customCommands + @AppStorage(\.chatModels) var chatModels + @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatModelId + @AppStorage(\.chatGPTTemperature) var defaultTemperature var body: some View { currentSystemPrompt @@ -21,6 +24,11 @@ struct ChatContextMenu: View { Divider() + chatModel + temperature + + Divider() + customCommandMenu } @@ -52,6 +60,89 @@ struct ChatContextMenu: View { } } + @ViewBuilder + var chatModel: some View { + Menu("Chat Model") { + Button(action: { + chat.chatModelId = nil + }) { + HStack { + if let defaultModel = chatModels.first(where: { $0.id == defaultChatModelId }) { + Text("Default (\(defaultModel.name))") + if chat.chatModelId == nil { + Image(systemName: "checkmark") + } + } else { + Text("No Model Available") + } + } + } + + if let id = chat.chatModelId, + !chatModels.map(\.id).contains(id) + { + Button(action: { + chat.chatModelId = nil + chat.objectWillChange.send() + }) { + HStack { + Text("Default (Selected Model Not Found)") + Image(systemName: "checkmark") + } + } + } + + Divider() + + ForEach(chatModels, id: \.id) { model in + Button(action: { + chat.chatModelId = model.id + chat.objectWillChange.send() + }) { + HStack { + Text(model.name) + if model.id == chat.chatModelId { + Image(systemName: "checkmark") + } + } + } + } + } + } + + @ViewBuilder + var temperature: some View { + Menu("Temperature") { + Button(action: { + chat.temperature = nil + }) { + HStack { + Text( + "Default (\(defaultTemperature.formatted(.number.precision(.fractionLength(1)))))" + ) + if chat.temperature == nil { + Image(systemName: "checkmark") + } + } + } + + Divider() + + ForEach(Array(stride(from: 0.0, through: 2.0, by: 0.1)), id: \.self) { value in + Button(action: { + chat.temperature = value + }) { + HStack { + Text("\(value.formatted(.number.precision(.fractionLength(1))))") + if value == chat.temperature { + Image(systemName: "checkmark") + } + } + } + } + } + } + var customCommandMenu: some View { Menu("Custom Commands") { ForEach( @@ -73,3 +164,4 @@ struct ChatContextMenu: View { } } } + diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 8c72e72b..17532b24 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -44,7 +44,7 @@ public class ChatGPTChatTab: ChatTab { public func buildTabItem() -> any View { ChatTabItemView(chat: provider) } - + public func buildMenu() -> any View { ChatContextMenu(chat: provider) } @@ -95,33 +95,42 @@ public class ChatGPTChatTab: ChatTab { public func start() { chatTabViewStore.send(.updateTitle("Chat")) - + service.$systemPrompt.removeDuplicates().sink { _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) } }.store(in: &cancellable) - + service.$extraSystemPrompt.removeDuplicates().sink { _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) } }.store(in: &cancellable) - + provider.$history.sink { [weak self] _ in Task { @MainActor [weak self] in if let title = self?.provider.title { self?.chatTabViewStore.send(.updateTitle(title)) } - self?.chatTabViewStore.send(.tabContentUpdated) } }.store(in: &cancellable) + + provider.objectWillChange.debounce(for: .seconds(1), scheduler: DispatchQueue.main) + .sink { [weak self] _ in + Task { @MainActor [weak self] in + self?.chatTabViewStore.send(.tabContentUpdated) + } + }.store(in: &cancellable) } } extension ChatProvider { convenience init(service: ChatService) { - self.init(pluginIdentifiers: service.allPluginCommands) + self.init( + configuration: service.configuration, + pluginIdentifiers: service.allPluginCommands + ) let cancellable = service.objectWillChange.sink { [weak self] in guard let self else { return } diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 147c2446..f2278b1b 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -1,4 +1,5 @@ import AppKit +import OpenAIService import MarkdownUI import SharedUIComponents import SwiftUI @@ -112,7 +113,7 @@ private struct Instruction: View { Markdown( """ Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. - + \( useCodeScopeByDefaultInChatContext ? "Scope **`@code`** is enabled by default." @@ -398,7 +399,7 @@ struct ChatPanelInputArea: View { EmptyView() } .keyboardShortcut(KeyEquivalent.return, modifiers: [.shift]) - + Button(action: { isInputAreaFocused = true }) { @@ -580,6 +581,7 @@ struct ChatPanel_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( + configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), history: ChatPanel_Preview.history, isReceivingMessage: true )) @@ -591,6 +593,7 @@ struct ChatPanel_Preview: PreviewProvider { struct ChatPanel_EmptyChat_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( + configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), history: [], isReceivingMessage: false )) @@ -623,6 +626,7 @@ struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter { struct ChatPanel_InputText_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( + configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), history: ChatPanel_Preview.history, isReceivingMessage: false )) @@ -636,6 +640,7 @@ struct ChatPanel_InputMultilineText_Preview: PreviewProvider { static var previews: some View { ChatPanel( chat: .init( + configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), history: ChatPanel_Preview.history, isReceivingMessage: false ), @@ -650,6 +655,7 @@ struct ChatPanel_InputMultilineText_Preview: PreviewProvider { struct ChatPanel_Light_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( + configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), history: ChatPanel_Preview.history, isReceivingMessage: true )) diff --git a/Core/Sources/ChatGPTChatTab/ChatProvider.swift b/Core/Sources/ChatGPTChatTab/ChatProvider.swift index 03768e98..cce47476 100644 --- a/Core/Sources/ChatGPTChatTab/ChatProvider.swift +++ b/Core/Sources/ChatGPTChatTab/ChatProvider.swift @@ -8,8 +8,28 @@ public final class ChatProvider: ObservableObject { public let id = UUID() @Published public var history: [ChatMessage] = [] @Published public var isReceivingMessage = false + public var temperature: Double? { + get { + configuration.overriding.temperature + } + set { + configuration.overriding.temperature = newValue + objectWillChange.send() + } + } + public var chatModelId: String? { + get { + configuration.overriding.modelId + } + set { + configuration.overriding.modelId = newValue + objectWillChange.send() + } + } + private let configuration: OverridingChatGPTConfiguration public var pluginIdentifiers: [String] = [] public var systemPrompt = "" + public var title: String { let defaultTitle = "Chat" guard let lastMessageText = history @@ -38,6 +58,7 @@ public final class ChatProvider: ObservableObject { public var onSetAsExtraPrompt: (MessageID) -> Void public init( + configuration: OverridingChatGPTConfiguration, history: [ChatMessage] = [], isReceivingMessage: Bool = false, pluginIdentifiers: [String] = [], @@ -50,6 +71,7 @@ public final class ChatProvider: ObservableObject { onRunCustomCommand: @escaping (CustomCommand) -> Void = { _ in }, onSetAsExtraPrompt: @escaping (MessageID) -> Void = { _ in } ) { + self.configuration = configuration self.history = history self.isReceivingMessage = isReceivingMessage self.pluginIdentifiers = pluginIdentifiers From ce539f96c182a866ac5437a788e7bfa7dff9c20b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:28:07 +0800 Subject: [PATCH 52/67] Fix unit tests --- .../xcschemes/ExtensionService.xcscheme | 3 ++ Core/Package.swift | 1 + Pro | 2 +- TestPlan.xctestplan | 14 -------- Tool/Sources/LangChain/Agent.swift | 6 ++++ .../Tests/LangChainTests/ChatAgentTests.swift | 34 +++++++++---------- .../ChatGPTStreamTests.swift | 17 +++++----- 7 files changed, 37 insertions(+), 40 deletions(-) diff --git a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme index d9c4e45f..2cc5df6c 100644 --- a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme +++ b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme @@ -46,6 +46,9 @@ reference = "container:TestPlan.xctestplan" default = "YES"> + + { } } +extension AgentFinish.ReturnValue: Equatable where Output: Equatable {} + +extension AgentFinish: Equatable where Output: Equatable {} + public enum AgentNextStep { case actions([AgentAction]) case finish(AgentFinish) } +extension AgentNextStep: Equatable where Output: Equatable {} + public struct AgentScratchPad: Equatable { public var content: Content diff --git a/Tool/Tests/LangChainTests/ChatAgentTests.swift b/Tool/Tests/LangChainTests/ChatAgentTests.swift index 24918f61..58dcbc14 100644 --- a/Tool/Tests/LangChainTests/ChatAgentTests.swift +++ b/Tool/Tests/LangChainTests/ChatAgentTests.swift @@ -12,24 +12,24 @@ private struct FakeChatModel: ChatModel { } final class ChatAgentParseOutputTests: XCTestCase { - func test_parsing_well_formatted_final_answer() throws { + func test_parsing_well_formatted_final_answer() async throws { let finalAnswer = """ Final Answer: The answer is 42. Because 42 is the answer to everything. """ let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") - let result = agent.parseOutput(finalAnswer) + let result = await agent.parseOutput(.init(role: .assistant, content: finalAnswer)) XCTAssertEqual(result, .finish(.init( - returnValue: """ + returnValue: .structured(""" The answer is 42. Because 42 is the answer to everything. - """, + """), log: finalAnswer ))) } - func test_parsing_final_answer_with_random_prefix() throws { + func test_parsing_final_answer_with_random_prefix() async throws { let finalAnswer = """ Now I have the final answer. Final Answer: The answer is 42. @@ -37,17 +37,17 @@ final class ChatAgentParseOutputTests: XCTestCase { """ let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") - let result = agent.parseOutput(finalAnswer) + let result = await agent.parseOutput(.init(role: .assistant, content: finalAnswer)) XCTAssertEqual(result, .finish(.init( - returnValue: """ + returnValue: .structured(""" The answer is 42. Because 42 is the answer to everything. - """, + """), log: finalAnswer ))) } - func test_parsing_action() throws { + func test_parsing_action() async throws { let reply = """ Question: How to setup langchain python? Thought: I am not familiar with langchain python, I should use the Search tool to find more information on how to set it up. @@ -61,7 +61,7 @@ final class ChatAgentParseOutputTests: XCTestCase { """ let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") - let result = agent.parseOutput(reply) + let result = await agent.parseOutput(.init(role: .assistant, content: reply)) XCTAssertEqual(result, .actions([ .init( toolName: "Search", @@ -71,7 +71,7 @@ final class ChatAgentParseOutputTests: XCTestCase { ])) } - func test_parsing_broken_action_and_return_everything_ahead_of_it() { + func test_parsing_broken_action_and_return_everything_ahead_of_it() async { let reply = """ Question: How to setup langchain python? Thought: I am not familiar with langchain python, I should use the Search tool to find more information on how to set it up. @@ -82,26 +82,26 @@ final class ChatAgentParseOutputTests: XCTestCase { """ let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") - let result = agent.parseOutput(reply) + let result = await agent.parseOutput(.init(role: .assistant, content: reply)) XCTAssertEqual(result, .finish(.init( - returnValue: """ + returnValue: .structured(""" Question: How to setup langchain python? Thought: I am not familiar with langchain python, I should use the Search tool to find more information on how to set it up. - """, + """), log: reply ))) } - func test_parsing_simple_reply_that_does_not_follow_the_format() { + func test_parsing_simple_reply_that_does_not_follow_the_format() async { let reply = """ The answer is 42. Because 42 is the answer to everything. """ let agent = ChatAgent(chatModel: FakeChatModel(), tools: [], preferredLanguage: "") - let result = agent.parseOutput(reply) + let result = await agent.parseOutput(.init(role: .assistant, content: reply)) XCTAssertEqual(result, .finish(.init( - returnValue: reply, + returnValue: .structured(reply), log: reply ))) } diff --git a/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift b/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift index d55e2c9c..e40442cc 100644 --- a/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift +++ b/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift @@ -139,7 +139,7 @@ final class ChatGPTStreamTests: XCTestCase { .init(name: $0.name, description: $0.description, parameters: $0.argumentSchema) }, "Function schema is not submitted") } - + func test_handling_multiple_function_call() async throws { let memory = ConversationChatGPTMemory(systemPrompt: "system", systemMessageId: "s") let configuration = UserPreferenceChatGPTConfiguration().overriding() @@ -339,29 +339,30 @@ extension ChatGPTStreamTests { } var name: String { "function" } - + var description: String { "description" } var argumentSchema: JSONSchemaValue { [ - .type: ["null"] + .type: ["null"], ] } - - var reportProgress: (String) async -> Void = { print($0) } - func prepare() async { + func prepare(reportProgress: @escaping ReportProgress) async { print("Function will be called") } - func call(arguments: Parameters) async throws -> String { + func call( + arguments: Parameters, + reportProgress: @escaping ReportProgress + ) async throws -> String { "Function is called." } } struct FunctionProvider: ChatGPTFunctionProvider { var functionCallStrategy: OpenAIService.FunctionCallStrategy? { nil } - + var functions: [any ChatGPTFunction] { [EmptyFunction()] } } } From 363e2f8353342844112ac08198ce46db2abd27a7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:40:01 +0800 Subject: [PATCH 53/67] Update pro package --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 812ed60b..8fad5c29 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 812ed60b7c7bb0cefff479ebf046c1c059f977ff +Subproject commit 8fad5c296030922a4ac5acaef178dfb79a5f30e2 From f43065e91784b5856b55252d2d86db478fdbe2a0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:46:50 +0800 Subject: [PATCH 54/67] Bump GitHub Copilot to 1.10.3 --- .../GitHubCopilotService/GitHubCopilotInstallationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift b/Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift index 79d98d19..f2837db6 100644 --- a/Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift +++ b/Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift @@ -10,7 +10,7 @@ public struct GitHubCopilotInstallationManager { return URL(string: link)! } - static let latestSupportedVersion = "1.10.2" + static let latestSupportedVersion = "1.10.3" public init() {} From 91ff0ebd63291a40ef2284651ca17c17b57baa9f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:47:07 +0800 Subject: [PATCH 55/67] Bump Codeium to 1.2.93 --- Core/Sources/CodeiumService/CodeiumInstallationManager.swift | 2 +- Core/Sources/HostApp/AccountSettings/CodeiumView.swift | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Core/Sources/CodeiumService/CodeiumInstallationManager.swift b/Core/Sources/CodeiumService/CodeiumInstallationManager.swift index 4ade251d..27d15ad0 100644 --- a/Core/Sources/CodeiumService/CodeiumInstallationManager.swift +++ b/Core/Sources/CodeiumService/CodeiumInstallationManager.swift @@ -3,7 +3,7 @@ import Terminal public struct CodeiumInstallationManager { private static var isInstalling = false - static let latestSupportedVersion = "1.2.85" + static let latestSupportedVersion = "1.2.93" public init() {} diff --git a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift index 2ca683c4..7c7b12cb 100644 --- a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift +++ b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift @@ -208,8 +208,6 @@ struct CodeiumView: View { } } - Divider() - Form { Toggle("Codeium Enterprise Mode", isOn: $viewModel.codeiumEnterpriseMode) TextField("Codeium Portal URL", text: $viewModel.codeiumPortalUrl) From 46d91fb110c01f750c43f543b74a0430a081c5f1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 15:48:22 +0800 Subject: [PATCH 56/67] Bump version to 0.24.0 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 9b74baa7..58c50109 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.23.2 -APP_BUILD = 241 +APP_VERSION = 0.24.0 +APP_BUILD = 250 From edc8c29301be248f558dbe32b6bc0d00b50b1c55 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 16:02:25 +0800 Subject: [PATCH 57/67] Check if to include pro package with a configuration file --- .gitignore | 1 + Core/Package.swift | 14 +++++++++----- Core/Sources/ChatService/AllContextCollector.swift | 9 ++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 8f023f85..eb99039a 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ Python/site-packages/* Python/VERSIONS Copilot for Xcode Plus.xcworkspace +PLUS diff --git a/Core/Package.swift b/Core/Package.swift index 3aca231d..fc288376 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -405,14 +405,18 @@ func isProIncluded(file: StaticString = #file) -> Bool { let rootURL = fileURL .deletingLastPathComponent() .deletingLastPathComponent() - let folderURL = rootURL.appendingPathComponent("Pro") - if !FileManager.default.fileExists(atPath: folderURL.path) { + let confURL = rootURL.appendingPathComponent("PLUS") + if !FileManager.default.fileExists(atPath: confURL.path) { return false } - let packageManifestURL = folderURL.appendingPathComponent("Package.swift") - if !FileManager.default.fileExists(atPath: packageManifestURL.path) { + do { + let content = String( + data: try Data(contentsOf: confURL), + encoding: .utf8 + ) + return content?.hasPrefix("YES") ?? false + } catch { return false } - return true } diff --git a/Core/Sources/ChatService/AllContextCollector.swift b/Core/Sources/ChatService/AllContextCollector.swift index 2402da7d..ec365e9d 100644 --- a/Core/Sources/ChatService/AllContextCollector.swift +++ b/Core/Sources/ChatService/AllContextCollector.swift @@ -2,12 +2,19 @@ import ActiveDocumentChatContextCollector import ChatContextCollector import SystemInfoChatContextCollector import WebChatContextCollector +#if canImport(ProChatContextCollectors) import ProChatContextCollectors - let allContextCollectors: [any ChatContextCollector] = [ SystemInfoChatContextCollector(), ActiveDocumentChatContextCollector(), WebChatContextCollector(), ProChatContextCollectors(), ] +#else +let allContextCollectors: [any ChatContextCollector] = [ + SystemInfoChatContextCollector(), + ActiveDocumentChatContextCollector(), + WebChatContextCollector(), +] +#endif From d1710433b2ed783ec5ff7cf3a45ff1575d605215 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 16:30:49 +0800 Subject: [PATCH 58/67] Prevent opening multiple prompt to code panel --- .../WindowBaseCommandHandler.swift | 3 ++- .../FeatureReducers/PromptToCodeGroup.swift | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index f7cd2227..91c72710 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -410,7 +410,8 @@ extension WindowBaseCommandHandler { let viewStore = Service.shared.guiController.viewStore _ = await Task { @MainActor in - viewStore.send(.promptToCodeGroup(.createPromptToCode(.init( + // if there is already a prompt to code presenting, we should not present another one + viewStore.send(.promptToCodeGroup(.activateOrCreatePromptToCode(.init( code: code, selectionRange: selection, language: codeLanguage, diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index d6a05d69..0680030c 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -67,6 +67,8 @@ public struct PromptToCodeGroup: ReducerProtocol { } public enum Action: Equatable { + /// Activate the prompt to code if it exists or create it if it doesn't + case activateOrCreatePromptToCode(PromptToCodeInitialState) case createPromptToCode(PromptToCodeInitialState) case updatePromptToCodeRange(id: PromptToCode.State.ID, range: CursorRange) case discardAcceptedPromptToCodeIfNotContinuous(id: PromptToCode.State.ID) @@ -80,6 +82,13 @@ public struct PromptToCodeGroup: ReducerProtocol { public var body: some ReducerProtocol { Reduce { state, action in switch action { + case let .activateOrCreatePromptToCode(s): + guard state.activePromptToCode == nil else { + return .none + } + return .run { send in + await send(.createPromptToCode(s)) + } case let .createPromptToCode(s): let newPromptToCode = PromptToCode.State( code: s.code, From 9b6f79803de1a22f7bc08db9b67eccfa2975fe9f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 21:36:47 +0800 Subject: [PATCH 59/67] Fix project url detection when it's a git worktree, workspace, xcproject or playground --- Tool/Sources/Environment/Environment.swift | 10 ++++++++++ Tool/Sources/XcodeInspector/XcodeWindowInspector.swift | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/Tool/Sources/Environment/Environment.swift b/Tool/Sources/Environment/Environment.swift index 254b4474..93c43531 100644 --- a/Tool/Sources/Environment/Environment.swift +++ b/Tool/Sources/Environment/Environment.swift @@ -69,6 +69,7 @@ public enum Environment { return nil } + #warning("TODO: Use WorkspaceXcodeWindowInspector.extractProjectURL instead.") public static var guessProjectRootURLForFile: (_ fileURL: URL) async throws -> URL = { fileURL in var currentURL = fileURL @@ -77,10 +78,19 @@ public enum Environment { while currentURL.pathComponents.count > 1 { defer { currentURL.deleteLastPathComponent() } guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue } + guard currentURL.pathExtension != "xcodeproj" else { continue } + guard currentURL.pathExtension != "xcworkspace" else { continue } + guard currentURL.pathExtension != "playground" else { continue } if firstDirectoryURL == nil { firstDirectoryURL = currentURL } let gitURL = currentURL.appendingPathComponent(".git") if FileManager.default.fileIsDirectory(atPath: gitURL.path) { lastGitDirectoryURL = currentURL + } else if let text = try? String(contentsOf: gitURL) { + if !text.hasPrefix("gitdir: ../"), // it's not a sub module + text.range(of: "/.git/worktrees/") != nil // it's a git worktree + { + lastGitDirectoryURL = currentURL + } } } diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 029d30e0..48786c78 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -115,10 +115,19 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { while currentURL.pathComponents.count > 1 { defer { currentURL.deleteLastPathComponent() } guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue } + guard currentURL.pathExtension != "xcodeproj" else { continue } + guard currentURL.pathExtension != "xcworkspace" else { continue } + guard currentURL.pathExtension != "playground" else { continue } if firstDirectoryURL == nil { firstDirectoryURL = currentURL } let gitURL = currentURL.appendingPathComponent(".git") if FileManager.default.fileIsDirectory(atPath: gitURL.path) { lastGitDirectoryURL = currentURL + } else if let text = try? String(contentsOf: gitURL) { + if !text.hasPrefix("gitdir: ../"), // it's not a sub module + text.range(of: "/.git/worktrees/") != nil // it's a git worktree + { + lastGitDirectoryURL = currentURL + } } } From 07cf6e867ca2d02ec5f808e62c3cabfff195b449 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 21:37:33 +0800 Subject: [PATCH 60/67] Adjust prompt --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 8fad5c29..f4048359 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8fad5c296030922a4ac5acaef178dfb79a5f30e2 +Subproject commit f404835924c6da474013478e99f5bf6d6799d43f From b291d99ed394d8d77a798d54a11d278d2dbd0a0b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 22:22:56 +0800 Subject: [PATCH 61/67] Update chat instruction --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index f2278b1b..ad537aa4 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -132,6 +132,7 @@ private struct Instruction: View { | `@file` | Read the metadata of the editing file | | `@code` | Read the code and metadata in the editing file | | `@web` (beta) | Search on Bing or query from a web page | + | `@project` | Experimental. Access content of the project | To use scopes, you can prefix a message with `@code`. From 20bf45d7de8bd5d0b7e195135872bb4f073ca6a7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 22:42:12 +0800 Subject: [PATCH 62/67] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index f4048359..87a4e994 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit f404835924c6da474013478e99f5bf6d6799d43f +Subproject commit 87a4e9949b60f57b2d2db6acaadf77d80fe2b82a From 5aedc754861ee0f40d4e19d6224cedca5a0a5449 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 23:12:32 +0800 Subject: [PATCH 63/67] Fix that activateOrCreatePromptToCode is not making the panel active --- .../SuggestionWidget/FeatureReducers/PanelFeature.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index 271ee785..e090a676 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -114,7 +114,8 @@ public struct PanelFeature: ReducerProtocol { state.content.suggestion = nil return .none - case .sharedPanel(.promptToCodeGroup(.createPromptToCode)): + case .sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode)), + .sharedPanel(.promptToCodeGroup(.createPromptToCode)): let hasPromptToCode = state.content.promptToCode != nil return .run { send in await send(.displayPanelContent) From 17d202afa473de62b77d4fb1ebd3773108c454b2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Sep 2023 23:35:31 +0800 Subject: [PATCH 64/67] Add toggle to disable enhanced workspace --- Core/Sources/HostApp/DebugView.swift | 5 +++++ Core/Sources/Service/Service.swift | 8 +++++--- Pro | 2 +- Tool/Sources/Preferences/Keys.swift | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Core/Sources/HostApp/DebugView.swift b/Core/Sources/HostApp/DebugView.swift index 142be6ca..b94d8ae1 100644 --- a/Core/Sources/HostApp/DebugView.swift +++ b/Core/Sources/HostApp/DebugView.swift @@ -15,6 +15,7 @@ final class DebugSettings: ObservableObject { @AppStorage(\.disableGitHubCopilotSettingsAutoRefreshOnAppear) var disableGitHubCopilotSettingsAutoRefreshOnAppear @AppStorage(\.useUserDefaultsBaseAPIKeychain) var useUserDefaultsBaseAPIKeychain + @AppStorage(\.disableEnhancedWorkspace) var disableEnhancedWorkspace init() {} } @@ -59,6 +60,10 @@ struct DebugSettingsView: View { Text("Store API keys in UserDefaults") } + Toggle(isOn: $settings.disableEnhancedWorkspace) { + Text("Disable Enhanced Workspace") + } + Button("Reset Migration Version to 0") { UserDefaults.shared.set(nil, forKey: "OldMigrationVersion") } diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index c07ead7e..9c586e6b 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -28,7 +28,7 @@ public final class Service { private init() { @Dependency(\.workspacePool) var workspacePool - + scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController) #if canImport(KeyBindingManager) keyBindingManager = .init( @@ -43,9 +43,11 @@ public final class Service { workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } #if canImport(EnhancedWorkspace) - workspacePool.registerPlugin { EnhancedWorkspacePlugin(workspace: $0) } + if !UserDefaults.shared.value(for: \.disableEnhancedWorkspace) { + workspacePool.registerPlugin { EnhancedWorkspacePlugin(workspace: $0) } + } #endif - + self.workspacePool = workspacePool } diff --git a/Pro b/Pro index 87a4e994..6b43f38a 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 87a4e9949b60f57b2d2db6acaadf77d80fe2b82a +Subproject commit 6b43f38aaa12fab3c051eb94c20a8d7192a6020b diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 5feb0c14..eee92784 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -499,5 +499,12 @@ public extension UserDefaultPreferenceKeys { key: "FeatureFlag-DisableGitHubCopilotSettingsAutoRefreshOnAppear" ) } + + var disableEnhancedWorkspace: FeatureFlag { + .init( + defaultValue: false, + key: "FeatureFlag-DisableEnhancedWorkspace" + ) + } } From 4fed754a381655a8e766de9eec8fbc9bafcd9042 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 28 Sep 2023 01:10:15 +0800 Subject: [PATCH 65/67] Fix project URL --- Tool/Package.swift | 1 + Tool/Sources/Environment/Environment.swift | 3 ++- Tool/Sources/Workspace/Workspace.swift | 15 +++++---------- .../XcodeInspector/XcodeInspector.swift | 18 +++--------------- .../XcodeInspector/XcodeWindowInspector.swift | 4 +--- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/Tool/Package.swift b/Tool/Package.swift index fa6ae660..5f5218a6 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -189,6 +189,7 @@ let package = Package( "Environment", "Logger", "Preferences", + "XcodeInspector" ] ), diff --git a/Tool/Sources/Environment/Environment.swift b/Tool/Sources/Environment/Environment.swift index 93c43531..f1315daf 100644 --- a/Tool/Sources/Environment/Environment.swift +++ b/Tool/Sources/Environment/Environment.swift @@ -41,7 +41,8 @@ public enum Environment { return false } } - + + #warning("TODO: Use XcodeInspector instead.") public static var fetchCurrentWorkspaceURLFromXcode: () async throws -> URL? = { if let xcode = ActiveApplicationMonitor.shared.activeXcode ?? ActiveApplicationMonitor.shared.latestXcode diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index c26626a5..bbffbecc 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -3,6 +3,7 @@ import Foundation import Preferences import SuggestionModel import UserDefaultsObserver +import XcodeInspector public protocol WorkspacePropertyKey { associatedtype Value @@ -89,16 +90,10 @@ public final class Workspace { init(workspaceURL: URL) { self.workspaceURL = workspaceURL - self.projectRootURL = { - var url = workspaceURL - while !FileManager.default.fileIsDirectory(atPath: url.path) || - !url.pathExtension.isEmpty - { - url = url.deletingLastPathComponent() - } - return url - - }() + self.projectRootURL = WorkspaceXcodeWindowInspector.extractProjectURL( + workspaceURL: workspaceURL, + documentURL: nil + ) ?? workspaceURL openedFileRecoverableStorage = .init(projectRootURL: projectRootURL) let openedFiles = openedFileRecoverableStorage.openedFiles Task { @WorkspaceActor in diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 75d696be..08d74805 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -24,7 +24,7 @@ public final class XcodeInspector: ObservableObject { @Published public internal(set) var focusedEditor: SourceEditor? @Published public internal(set) var focusedElement: AXUIElement? @Published public internal(set) var completionPanel: AXUIElement? - + public var focusedEditorContent: EditorInformation? { guard let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL, let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, @@ -253,11 +253,9 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { } public var realtimeProjectURL: URL? { - guard let window = appElement.focusedWindow else { return nil } let workspaceURL = realtimeWorkspaceURL let documentURL = realtimeDocumentURL return WorkspaceXcodeWindowInspector.extractProjectURL( - windowElement: window, workspaceURL: workspaceURL, documentURL: documentURL ) @@ -469,18 +467,8 @@ extension XcodeAppInstanceInspector { /// Use the project path as the workspace identifier. static func workspaceIdentifier(_ window: AXUIElement) -> WorkspaceIdentifier { - for child in window.children { - if child.description.starts(with: "/"), child.description.count > 1 { - let path = child.description - let trimmedNewLine = path.trimmingCharacters(in: .newlines) - var url = URL(fileURLWithPath: trimmedNewLine) - while !FileManager.default.fileIsDirectory(atPath: url.path) || - !url.pathExtension.isEmpty - { - url = url.deletingLastPathComponent() - } - return WorkspaceIdentifier.url(url) - } + if let url = WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) { + return WorkspaceIdentifier.url(url) } return WorkspaceIdentifier.unknown } diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 48786c78..8c7b2b2d 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -66,7 +66,6 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { self.workspaceURL = workspaceURL } let projectURL = Self.extractProjectURL( - windowElement: uiElement, workspaceURL: workspaceURL, documentURL: documentURL ) @@ -104,8 +103,7 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { return nil } - static func extractProjectURL( - windowElement: AXUIElement, + public static func extractProjectURL( workspaceURL: URL?, documentURL: URL? ) -> URL? { From 4affcf02891d299c5c11e450dbcf6d6b2fa110f1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 28 Sep 2023 14:35:46 +0800 Subject: [PATCH 66/67] Update appcast.xml --- appcast.xml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/appcast.xml b/appcast.xml index b8e4aa63..55f4365b 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,16 +3,28 @@ Copilot for Xcode - - 0.23.2 - Sat, 09 Sep 2023 22:07:35 +0800 - 241 - 0.23.2 - 12.0 - + + 0.24.0 + Thu, 28 Sep 2023 01:35:21 +0800 + 250 + 0.24.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.24.0 + + + + + + 0.23.2 + Sat, 09 Sep 2023 22:07:35 +0800 + 241 + 0.23.2 + 12.0 + https://github.com/intitni/CopilotForXcode/releases/tag/0.23.2 - + From dfd69e8bf28c9f7a6677eadbc4658d817758fbd9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 28 Sep 2023 14:41:45 +0800 Subject: [PATCH 67/67] Update --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved index ea699cb1..c5870abd 100644 --- a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -275,8 +275,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", - "version" : "0.8.5" + "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", + "version" : "0.9.0" } } ],