From 61e76d2c5a57ff7e5f9e6734ce72a0f480e5aa9d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 1 Jun 2023 15:16:49 +0800 Subject: [PATCH 01/49] Display instructions in the chat panel --- .../SuggestionPanelContent/ChatPanel.swift | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index 6a035138..c4443099 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -61,6 +61,9 @@ struct ChatPanelToolbar: View { struct ChatPanelMessages: View { @ObservedObject var chat: ChatProvider + @AppStorage(\.chatFontSize) var chatFontSize + @AppStorage(\.useSelectionScopeByDefaultInChatContext) + var useSelectionScopeByDefaultInChatContext var body: some View { List { @@ -74,12 +77,46 @@ struct ChatPanelMessages: View { } if chat.history.isEmpty { - Text("New Chat") - .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical) - .scaleEffect(x: -1, y: -1, anchor: .center) - .foregroundStyle(.secondary) - .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) + Group { + if useSelectionScopeByDefaultInChatContext { + Markdown( + """ + Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + + Currently, I have the ability to read the following details from the active editor: + - The **selected code**. + - The **relative path** of the file. + - The **error and warning** labels. + - The text cursor location. + + If you'd like me to examine the entire file, simply add `/file` to the beginning of your message. + """ + ) + } else { + Markdown( + """ + Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + + Currently, I have the ability to read the following details from the active editor: + - The **relative path** of the file. + - The **error and warning** labels. + - The text cursor location. + + If you would like me to examine the selected code, please prefix your message with `/selection`. If you would like me to examine the entire file, please prefix your message with `/file`. + """ + ) + } + } + .textSelection(.enabled) + .markdownTheme(.custom(fontSize: chatFontSize)) + .opacity(0.8) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .scaleEffect(x: -1, y: -1, anchor: .center) } ForEach(chat.history.reversed(), id: \.id) { message in @@ -177,7 +214,7 @@ private struct UserMessage: View { Button("Send Again") { chat.resendMessage(id: id) } - + Button("Set as Extra System Prompt") { chat.setAsExtraPrompt(id: id) } @@ -234,7 +271,7 @@ private struct BotMessage: View { NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) } - + Button("Set as Extra System Prompt") { chat.setAsExtraPrompt(id: id) } From cd3ff1df0849315d1faad7bd049cb942fa1eb141 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 00:16:42 +0800 Subject: [PATCH 02/49] Make chat instruction always visible --- .../SuggestionPanelContent/ChatPanel.swift | 95 ++++++++++--------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index c4443099..c14eb8a1 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -61,9 +61,6 @@ struct ChatPanelToolbar: View { struct ChatPanelMessages: View { @ObservedObject var chat: ChatProvider - @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.useSelectionScopeByDefaultInChatContext) - var useSelectionScopeByDefaultInChatContext var body: some View { List { @@ -76,48 +73,7 @@ struct ChatPanelMessages: View { .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) } - if chat.history.isEmpty { - Group { - if useSelectionScopeByDefaultInChatContext { - Markdown( - """ - Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. - - Currently, I have the ability to read the following details from the active editor: - - The **selected code**. - - The **relative path** of the file. - - The **error and warning** labels. - - The text cursor location. - - If you'd like me to examine the entire file, simply add `/file` to the beginning of your message. - """ - ) - } else { - Markdown( - """ - Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. - - Currently, I have the ability to read the following details from the active editor: - - The **relative path** of the file. - - The **error and warning** labels. - - The text cursor location. - - If you would like me to examine the selected code, please prefix your message with `/selection`. If you would like me to examine the entire file, please prefix your message with `/file`. - """ - ) - } - } - .textSelection(.enabled) - .markdownTheme(.custom(fontSize: chatFontSize)) - .opacity(0.8) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - .scaleEffect(x: -1, y: -1, anchor: .center) - } + Instruction() ForEach(chat.history.reversed(), id: \.id) { message in let text = message.text.isEmpty && !message.isUser ? "..." : message @@ -172,6 +128,55 @@ private struct StopRespondingButton: View { } } +private struct Instruction: View { + @AppStorage(\.useSelectionScopeByDefaultInChatContext) + var useSelectionScopeByDefaultInChatContext + @AppStorage(\.chatFontSize) var chatFontSize + + var body: some View { + Group { + if useSelectionScopeByDefaultInChatContext { + Markdown( + """ + Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + + Currently, I have the ability to read the following details from the active editor: + - The **selected code**. + - The **relative path** of the file. + - The **error and warning** labels. + - The text cursor location. + + If you'd like me to examine the entire file, simply add `/file` to the beginning of your message. + """ + ) + } else { + Markdown( + """ + Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + + Currently, I have the ability to read the following details from the active editor: + - The **relative path** of the file. + - The **error and warning** labels. + - The text cursor location. + + If you would like me to examine the selected code, please prefix your message with `/selection`. If you would like me to examine the entire file, please prefix your message with `/file`. + """ + ) + } + } + .textSelection(.enabled) + .markdownTheme(.custom(fontSize: chatFontSize)) + .opacity(0.8) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .scaleEffect(x: -1, y: -1, anchor: .center) + } +} + private struct UserMessage: View { let id: String let text: String From 1f452d4296c8ddbbb1cf4e9fcf53f1fbe228d4f8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 01:09:57 +0800 Subject: [PATCH 03/49] Remove LazyVStack --- .../SuggestionPanelContent/CodeBlock.swift | 17 +---------------- .../PromptToCodePanel.swift | 11 +++++------ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlock.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlock.swift index d53d9fce..77eae975 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlock.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlock.swift @@ -10,8 +10,6 @@ struct CodeBlock: View { let firstLinePrecedingSpaceCount: Int let fontSize: Double - @AppStorage(\.disableLazyVStack) var disableLazyVStack - init( code: String, language: String, @@ -39,21 +37,8 @@ struct CodeBlock: View { highlightedCode = result.code } - @ViewBuilder - func vstack(@ViewBuilder content: () -> some View) -> some View { - if disableLazyVStack { - VStack(spacing: 2) { - content() - } - } else { - LazyVStack(spacing: 2) { - content() - } - } - } - var body: some View { - vstack { + VStack(spacing: 2) { ForEach(0.. Date: Mon, 22 May 2023 15:27:43 +0800 Subject: [PATCH 04/49] Add new package Tool --- Copilot for Xcode.xcodeproj/project.pbxproj | 2 ++ Tool/.gitignore | 9 +++++++ Tool/Package.swift | 28 +++++++++++++++++++++ Tool/README.md | 3 +++ Tool/Sources/Tool/Tool.swift | 6 +++++ Tool/Tests/ToolTests/ToolTests.swift | 11 ++++++++ 6 files changed, 59 insertions(+) create mode 100644 Tool/.gitignore create mode 100644 Tool/Package.swift create mode 100644 Tool/README.md create mode 100644 Tool/Sources/Tool/Tool.swift create mode 100644 Tool/Tests/ToolTests/ToolTests.swift diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index 40f6c2d0..c16c9939 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -143,6 +143,7 @@ C8189B202938973000C9DCDA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; C8189B222938973000C9DCDA /* Copilot_for_Xcode.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Copilot_for_Xcode.entitlements; sourceTree = ""; }; C8189B282938979000C9DCDA /* Core */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Core; sourceTree = ""; }; + C81D181E2A1B509B006C1B70 /* Tool */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Tool; sourceTree = ""; }; C81E867D296FE4420026E908 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; C8216B70298036EC00AD38C7 /* Helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Helper; sourceTree = BUILT_PRODUCTS_DIR; }; C8216B72298036EC00AD38C7 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; @@ -248,6 +249,7 @@ C81458AD293A009600135263 /* Config.xcconfig */, C81458AE293A009800135263 /* Config.debug.xcconfig */, C8CD828229B88006008D044D /* TestPlan.xctestplan */, + C81D181E2A1B509B006C1B70 /* Tool */, C8189B282938979000C9DCDA /* Core */, C8189B182938972F00C9DCDA /* Copilot for Xcode */, C81458922939EFDC00135263 /* EditorExtension */, diff --git a/Tool/.gitignore b/Tool/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/Tool/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Tool/Package.swift b/Tool/Package.swift new file mode 100644 index 00000000..f62d20b8 --- /dev/null +++ b/Tool/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Tool", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Tool", + targets: ["Tool"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Tool", + dependencies: []), + .testTarget( + name: "ToolTests", + dependencies: ["Tool"]), + ] +) diff --git a/Tool/README.md b/Tool/README.md new file mode 100644 index 00000000..1d22e107 --- /dev/null +++ b/Tool/README.md @@ -0,0 +1,3 @@ +# Tool + +A description of this package. diff --git a/Tool/Sources/Tool/Tool.swift b/Tool/Sources/Tool/Tool.swift new file mode 100644 index 00000000..48242061 --- /dev/null +++ b/Tool/Sources/Tool/Tool.swift @@ -0,0 +1,6 @@ +public struct Tool { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/Tool/Tests/ToolTests/ToolTests.swift b/Tool/Tests/ToolTests/ToolTests.swift new file mode 100644 index 00000000..92299fb0 --- /dev/null +++ b/Tool/Tests/ToolTests/ToolTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import Tool + +final class ToolTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Tool().text, "Hello, World!") + } +} From 1cd60d1d2cc705bda1dc71ea65a6352208abf9b8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 22 May 2023 15:32:58 +0800 Subject: [PATCH 05/49] Add target LangChainService --- .../xcshareddata/swiftpm/Package.resolved | 9 ++++++++ Tool/Package.swift | 22 ++++++++++--------- .../{Tool => LangChainService}/Tool.swift | 0 .../ToolTests.swift | 0 4 files changed, 21 insertions(+), 10 deletions(-) rename Tool/Sources/{Tool => LangChainService}/Tool.swift (100%) rename Tool/Tests/{ToolTests => LangChainServiceTests}/ToolTests.swift (100%) diff --git a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 084a8217..5fd264b5 100644 --- a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -90,6 +90,15 @@ "version" : "0.3.1" } }, + { + "identity" : "pythonkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pvieito/PythonKit.git", + "state" : { + "revision" : "81f621d094a7c8923207efe5178f50dba1b56c39", + "version" : "0.3.1" + } + }, { "identity" : "sparkle", "kind" : "remoteSourceControl", diff --git a/Tool/Package.swift b/Tool/Package.swift index f62d20b8..d71e9454 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -6,23 +6,25 @@ import PackageDescription let package = Package( name: "Tool", products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "Tool", - targets: ["Tool"]), + targets: ["LangChainService"] + ), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/pvieito/PythonKit.git", from: "0.3.1"), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "Tool", - dependencies: []), + name: "LangChainService", + dependencies: [ + "PythonKit", + ] + ), .testTarget( - name: "ToolTests", - dependencies: ["Tool"]), + name: "LangChainServiceTests", + dependencies: ["LangChainService"] + ), ] ) + diff --git a/Tool/Sources/Tool/Tool.swift b/Tool/Sources/LangChainService/Tool.swift similarity index 100% rename from Tool/Sources/Tool/Tool.swift rename to Tool/Sources/LangChainService/Tool.swift diff --git a/Tool/Tests/ToolTests/ToolTests.swift b/Tool/Tests/LangChainServiceTests/ToolTests.swift similarity index 100% rename from Tool/Tests/ToolTests/ToolTests.swift rename to Tool/Tests/LangChainServiceTests/ToolTests.swift From 57856af0cfac9a01f94d07aea8962d44827315a7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 22 May 2023 16:53:24 +0800 Subject: [PATCH 06/49] Move Terminal to Tool --- .../xcshareddata/swiftpm/Package.resolved | 9 ------- Core/Package.swift | 14 ++++++---- Tool/Package.swift | 26 +++++++++++-------- .../Sources/Terminal/Terminal.swift | 0 4 files changed, 24 insertions(+), 25 deletions(-) rename {Core => Tool}/Sources/Terminal/Terminal.swift (100%) diff --git a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5fd264b5..084a8217 100644 --- a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -90,15 +90,6 @@ "version" : "0.3.1" } }, - { - "identity" : "pythonkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pvieito/PythonKit.git", - "state" : { - "revision" : "81f621d094a7c8923207efe5178f50dba1b56c39", - "version" : "0.3.1" - } - }, { "identity" : "sparkle", "kind" : "remoteSourceControl", diff --git a/Core/Package.swift b/Core/Package.swift index ec943396..c357c27d 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -47,6 +47,7 @@ let package = Package( ), ], dependencies: [ + .package(path: "../Tool"), .package(url: "https://github.com/ChimeHQ/LanguageClient", from: "0.3.1"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), .package(url: "https://github.com/raspu/Highlightr", from: "2.1.0"), @@ -184,7 +185,11 @@ let package = Package( ), .target( name: "ChatPlugins", - dependencies: ["OpenAIService", "Environment", "Terminal"] + dependencies: [ + "OpenAIService", + "Environment", + .product(name: "Terminal", package: "Tool"), + ] ), .target( name: "ChatContextCollector", @@ -226,12 +231,11 @@ let package = Package( .target(name: "DisplayLink"), .target(name: "ActiveApplicationMonitor"), .target(name: "AXNotificationStream"), - .target(name: "Terminal"), .target( name: "UpdateChecker", dependencies: [ "Logger", - "Sparkle" + "Sparkle", ] ), .target(name: "AXExtension"), @@ -260,7 +264,7 @@ let package = Package( "SuggestionModel", "XPCShared", "Preferences", - "Terminal", + .product(name: "Terminal", package: "Tool"), ] ), .testTarget( @@ -293,7 +297,7 @@ let package = Package( "SuggestionModel", "Preferences", "KeychainAccess", - "Terminal", + .product(name: "Terminal", package: "Tool"), "Configs", ] ), diff --git a/Tool/Package.swift b/Tool/Package.swift index d71e9454..1a37573b 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -1,26 +1,30 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Tool", + platforms: [.macOS(.v12)], products: [ - .library( - name: "Tool", - targets: ["LangChainService"] - ), - ], - dependencies: [ - .package(url: "https://github.com/pvieito/PythonKit.git", from: "0.3.1"), + .library(name: "Terminal", targets: ["Terminal"]), + .library(name: "LangChainService", targets: ["LangChainService"]), ], + dependencies: [], targets: [ + // MARK: - Helpers + + .target(name: "Terminal"), + + // MARK: - Services + .target( name: "LangChainService", - dependencies: [ - "PythonKit", - ] + dependencies: [] ), + + // MARK: - Tests + .testTarget( name: "LangChainServiceTests", dependencies: ["LangChainService"] diff --git a/Core/Sources/Terminal/Terminal.swift b/Tool/Sources/Terminal/Terminal.swift similarity index 100% rename from Core/Sources/Terminal/Terminal.swift rename to Tool/Sources/Terminal/Terminal.swift From 19f53cb68689cafc32fa0eeee4ac45dfc6e525bd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 1 Jun 2023 17:15:18 +0800 Subject: [PATCH 07/49] Successfully embedded Python langchain --- .gitignore | 7 +++ Copilot for Xcode.xcodeproj/project.pbxproj | 57 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++ Core/Package.swift | 2 + ExtensionService/AppDelegate.swift | 17 +++--- ExtensionService/InitializePython.swift | 25 ++++++++ Tool/Package.swift | 14 +++-- site-packages/install.sh | 11 ++++ site-packages/requirements.txt | 2 + 9 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 ExtensionService/InitializePython.swift create mode 100755 site-packages/install.sh create mode 100644 site-packages/requirements.txt diff --git a/.gitignore b/.gitignore index 43fb9ab3..2b6dd734 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,11 @@ iOSInjectionProject/ # End of https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager + Secrets.xcconfig +Python.xcframework +python-stdlib +site-packages/* +!site-packages/requirements.txt +!site-packages/install.sh + diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index c16c9939..da4cb7ad 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -38,6 +38,10 @@ C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; }; C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; }; + C8A3AE522A2884DA0046E809 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8A3AE512A2883430046E809 /* Python.xcframework */; }; + C8A3AE592A2885A70046E809 /* InitializePython.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8A3AE582A2885A70046E809 /* InitializePython.swift */; }; + C8A3AE5B2A288AF90046E809 /* site-packages in Resources */ = {isa = PBXBuildFile; fileRef = C8A3AE5A2A288AF90046E809 /* site-packages */; }; + C8A3B1772A288FA90046E809 /* python-stdlib in Resources */ = {isa = PBXBuildFile; fileRef = C8A3B1762A288FA90046E809 /* python-stdlib */; }; C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; }; /* End PBXBuildFile section */ @@ -165,6 +169,10 @@ C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextSuggestionCommand.swift; sourceTree = ""; }; C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviousSuggestionCommand.swift; sourceTree = ""; }; C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = ""; }; + C8A3AE512A2883430046E809 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; + C8A3AE582A2885A70046E809 /* InitializePython.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializePython.swift; sourceTree = ""; }; + C8A3AE5A2A288AF90046E809 /* site-packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "site-packages"; sourceTree = ""; }; + C8A3B1762A288FA90046E809 /* python-stdlib */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "python-stdlib"; sourceTree = ""; }; C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWithSelection.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -201,6 +209,7 @@ buildActionMask = 2147483647; files = ( C861E61E2994F6150056CB02 /* Service in Frameworks */, + C8A3AE522A2884DA0046E809 /* Python.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -255,6 +264,9 @@ C81458922939EFDC00135263 /* EditorExtension */, C8216B71298036EC00AD38C7 /* Helper */, C861E60F2994F6070056CB02 /* ExtensionService */, + C8A3AE512A2883430046E809 /* Python.xcframework */, + C8A3B1762A288FA90046E809 /* python-stdlib */, + C8A3AE5A2A288AF90046E809 /* site-packages */, C814588D2939EFDC00135263 /* Frameworks */, C8189B172938972F00C9DCDA /* Products */, ); @@ -306,6 +318,7 @@ C81291D92994FE7900196E12 /* Info.plist */, C861E61F2994F6390056CB02 /* ServiceDelegate.swift */, C861E6102994F6070056CB02 /* AppDelegate.swift */, + C8A3AE582A2885A70046E809 /* InitializePython.swift */, C81291D52994FE6900196E12 /* Main.storyboard */, C861E6142994F6080056CB02 /* Assets.xcassets */, C861E6192994F6080056CB02 /* ExtensionService.entitlements */, @@ -390,6 +403,8 @@ C861E60A2994F6070056CB02 /* Sources */, C861E60B2994F6070056CB02 /* Frameworks */, C861E60C2994F6070056CB02 /* Resources */, + C8A3AE572A28852D0046E809 /* Sign Python STD */, + C8A3B1782A2894E10046E809 /* Sign Python Site Packages */, ); buildRules = ( ); @@ -474,11 +489,52 @@ files = ( C861E6152994F6080056CB02 /* Assets.xcassets in Resources */, C81291D72994FE6900196E12 /* Main.storyboard in Resources */, + C8A3B1772A288FA90046E809 /* python-stdlib in Resources */, + C8A3AE5B2A288AF90046E809 /* site-packages in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + C8A3AE572A28852D0046E809 /* Sign Python STD */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Sign Python STD"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\necho \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\nfind \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; + }; + C8A3B1782A2894E10046E809 /* Sign Python Site Packages */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Sign Python Site Packages"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\necho \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\nfind \"$CODESIGNING_FOLDER_PATH/Contents/Resources/site-packages\" -type f \\( -name \"*.so\" -o -name \"*.dylib\" \\) -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ C81458882939EFDC00135263 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -522,6 +578,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C8A3AE592A2885A70046E809 /* InitializePython.swift in Sources */, C861E6202994F63A0056CB02 /* ServiceDelegate.swift in Sources */, C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */, ); diff --git a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 084a8217..dbf8e6c2 100644 --- a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -90,6 +90,15 @@ "version" : "0.3.1" } }, + { + "identity" : "pythonkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pvieito/PythonKit.git", + "state" : { + "branch" : "master", + "revision" : "060e1c8b0d14e4d241b3623fdbe83d0e3c81a993" + } + }, { "identity" : "sparkle", "kind" : "remoteSourceControl", diff --git a/Core/Package.swift b/Core/Package.swift index c357c27d..c229120f 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -56,6 +56,7 @@ let package = Package( .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), .package(url: "https://github.com/alfianlosari/GPTEncoder", from: "1.0.4"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), + .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), ], targets: [ // MARK: - Main @@ -92,6 +93,7 @@ let package = Package( "ServiceUpdateMigration", "UserDefaultsObserver", .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), + .product(name: "PythonKit", package: "PythonKit"), ] ), .testTarget( diff --git a/ExtensionService/AppDelegate.swift b/ExtensionService/AppDelegate.swift index 501ff6a8..eb59fa88 100644 --- a/ExtensionService/AppDelegate.swift +++ b/ExtensionService/AppDelegate.swift @@ -30,19 +30,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationDidFinishLaunching(_: Notification) { if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return } - _ = GraphicalUserInterfaceController.shared - _ = RealtimeSuggestionController.shared - _ = XcodeInspector.shared - AXIsProcessTrustedWithOptions([ - kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true, - ] as CFDictionary) - setupQuitOnUpdate() - setupQuitOnUserTerminated() +// _ = GraphicalUserInterfaceController.shared +// _ = RealtimeSuggestionController.shared +// _ = XcodeInspector.shared +// AXIsProcessTrustedWithOptions([ +// kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true, +// ] as CFDictionary) +// setupQuitOnUpdate() +// setupQuitOnUserTerminated() xpcListener = setupXPCListener() Logger.service.info("XPC Service started.") NSApp.setActivationPolicy(.accessory) buildStatusBarMenu() DependencyUpdater().update() + initializePython() Task { do { try await ServiceUpdateMigrator().migrate() diff --git a/ExtensionService/InitializePython.swift b/ExtensionService/InitializePython.swift new file mode 100644 index 00000000..27b2919d --- /dev/null +++ b/ExtensionService/InitializePython.swift @@ -0,0 +1,25 @@ +import Foundation +import Python +import PythonKit + +@available( *, deprecated, message: "Testing" ) +func initializePython() { + guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), + let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil), + let libDynloadPath = Bundle.main.path( + forResource: "python-stdlib/lib-dynload", + ofType: nil + ) else { return } + setenv("PYTHONHOME", stdLibPath, 1) + setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) + Py_Initialize() + + let sys = Python.import("sys") + print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") + print("Python Encoding: \(sys.getdefaultencoding().upper())") + print("Python Path: \(sys.path)") + + let llms = Python.import("langchain.llms") + print(llms.OpenAI) +} + diff --git a/Tool/Package.swift b/Tool/Package.swift index 1a37573b..8e36b29e 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -10,17 +10,24 @@ let package = Package( .library(name: "Terminal", targets: ["Terminal"]), .library(name: "LangChainService", targets: ["LangChainService"]), ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), + ], targets: [ // MARK: - Helpers - .target(name: "Terminal"), + .target( + name: "Terminal", + dependencies: [] + ), // MARK: - Services .target( name: "LangChainService", - dependencies: [] + dependencies: [ + .product(name: "PythonKit", package: "PythonKit") + ] ), // MARK: - Tests @@ -31,4 +38,3 @@ let package = Package( ), ] ) - diff --git a/site-packages/install.sh b/site-packages/install.sh new file mode 100755 index 00000000..ea188e9c --- /dev/null +++ b/site-packages/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +shopt -s extglob + +rm -rfv !("requirements.txt"|"install.sh") +python3.11 -m pip install -r requirements.txt -t . + +rm pip/__init__.py +cp ../pip_init.py pip/__init__.py +find . -name "__pycache__" -exec rm -rf {} \; +find "*.so" -delete \ No newline at end of file diff --git a/site-packages/requirements.txt b/site-packages/requirements.txt new file mode 100644 index 00000000..5046bf5e --- /dev/null +++ b/site-packages/requirements.txt @@ -0,0 +1,2 @@ +openai +langchain From 02ef3bb73a13c193512657a1e66f3079b207cf49 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 3 Jun 2023 23:59:03 +0800 Subject: [PATCH 08/49] Rename package ChatPlugins to ChatPlugin --- Core/Package.swift | 5 +-- .../AITerminalChatPlugin.swift | 0 .../AskChatGPT.swift | 0 .../CallAIFunction.swift | 0 .../ChatPlugin.swift | 0 .../Sources/ChatPlugin/SearchChatPlugin.swift | 31 +++++++++++++++++++ .../TerminalChatPlugin.swift | 0 .../ChatService/ChatPluginController.swift | 6 ++-- Core/Sources/ChatService/ChatService.swift | 2 +- .../{Tool.swift => Schema.swift} | 0 10 files changed, 38 insertions(+), 6 deletions(-) rename Core/Sources/{ChatPlugins => ChatPlugin}/AITerminalChatPlugin.swift (100%) rename Core/Sources/{ChatPlugins => ChatPlugin}/AskChatGPT.swift (100%) rename Core/Sources/{ChatPlugins => ChatPlugin}/CallAIFunction.swift (100%) rename Core/Sources/{ChatPlugins => ChatPlugin}/ChatPlugin.swift (100%) create mode 100644 Core/Sources/ChatPlugin/SearchChatPlugin.swift rename Core/Sources/{ChatPlugins => ChatPlugin}/TerminalChatPlugin.swift (100%) rename Tool/Sources/LangChainService/{Tool.swift => Schema.swift} (100%) diff --git a/Core/Package.swift b/Core/Package.swift index c229120f..a0223044 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -177,7 +177,7 @@ let package = Package( .target( name: "ChatService", dependencies: [ - "ChatPlugins", + "ChatPlugin", "ChatContextCollector", "OpenAIService", "Environment", @@ -186,11 +186,12 @@ let package = Package( ] ), .target( - name: "ChatPlugins", + name: "ChatPlugin", dependencies: [ "OpenAIService", "Environment", .product(name: "Terminal", package: "Tool"), + .product(name: "PythonKit", package: "PythonKit"), ] ), .target( diff --git a/Core/Sources/ChatPlugins/AITerminalChatPlugin.swift b/Core/Sources/ChatPlugin/AITerminalChatPlugin.swift similarity index 100% rename from Core/Sources/ChatPlugins/AITerminalChatPlugin.swift rename to Core/Sources/ChatPlugin/AITerminalChatPlugin.swift diff --git a/Core/Sources/ChatPlugins/AskChatGPT.swift b/Core/Sources/ChatPlugin/AskChatGPT.swift similarity index 100% rename from Core/Sources/ChatPlugins/AskChatGPT.swift rename to Core/Sources/ChatPlugin/AskChatGPT.swift diff --git a/Core/Sources/ChatPlugins/CallAIFunction.swift b/Core/Sources/ChatPlugin/CallAIFunction.swift similarity index 100% rename from Core/Sources/ChatPlugins/CallAIFunction.swift rename to Core/Sources/ChatPlugin/CallAIFunction.swift diff --git a/Core/Sources/ChatPlugins/ChatPlugin.swift b/Core/Sources/ChatPlugin/ChatPlugin.swift similarity index 100% rename from Core/Sources/ChatPlugins/ChatPlugin.swift rename to Core/Sources/ChatPlugin/ChatPlugin.swift diff --git a/Core/Sources/ChatPlugin/SearchChatPlugin.swift b/Core/Sources/ChatPlugin/SearchChatPlugin.swift new file mode 100644 index 00000000..fd47abde --- /dev/null +++ b/Core/Sources/ChatPlugin/SearchChatPlugin.swift @@ -0,0 +1,31 @@ +import Environment +import Foundation +import OpenAIService +import PythonKit + +public actor SearchChatPlugin: ChatPlugin { + public static var command: String { "search" } + public nonisolated var name: String { "Search" } + + let chatGPTService: any ChatGPTServiceType + var isCancelled = false + weak var delegate: ChatPluginDelegate? + + public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) { + self.chatGPTService = chatGPTService + self.delegate = delegate + } + + public func send(content: String, originalMessage: String) async { + + } + + public func cancel() async { + isCancelled = true + } + + public func stopResponding() async { + isCancelled = true + } +} + diff --git a/Core/Sources/ChatPlugins/TerminalChatPlugin.swift b/Core/Sources/ChatPlugin/TerminalChatPlugin.swift similarity index 100% rename from Core/Sources/ChatPlugins/TerminalChatPlugin.swift rename to Core/Sources/ChatPlugin/TerminalChatPlugin.swift diff --git a/Core/Sources/ChatService/ChatPluginController.swift b/Core/Sources/ChatService/ChatPluginController.swift index e90c11d1..2bf05a74 100644 --- a/Core/Sources/ChatService/ChatPluginController.swift +++ b/Core/Sources/ChatService/ChatPluginController.swift @@ -1,4 +1,4 @@ -import ChatPlugins +import ChatPlugin import Combine import Foundation import OpenAIService @@ -102,13 +102,13 @@ final class ChatPluginController { // MARK: - ChatPluginDelegate extension ChatPluginController: ChatPluginDelegate { - public func pluginDidStartResponding(_: ChatPlugins.ChatPlugin) { + public func pluginDidStartResponding(_: ChatPlugin) { Task { await chatGPTService.markReceivingMessage(true) } } - public func pluginDidEndResponding(_: ChatPlugins.ChatPlugin) { + public func pluginDidEndResponding(_: ChatPlugin) { Task { await chatGPTService.markReceivingMessage(false) } diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 86ba8fb9..f5749dc0 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -1,5 +1,5 @@ import ChatContextCollector -import ChatPlugins +import ChatPlugin import Combine import Foundation import OpenAIService diff --git a/Tool/Sources/LangChainService/Tool.swift b/Tool/Sources/LangChainService/Schema.swift similarity index 100% rename from Tool/Sources/LangChainService/Tool.swift rename to Tool/Sources/LangChainService/Schema.swift From 0ccd6aa1b5f992f2226dd0a49c98a9820a14d982 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 3 Jun 2023 23:59:47 +0800 Subject: [PATCH 09/49] Fix build phase to always sign Python on build --- Copilot for Xcode.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index da4cb7ad..b103f1d3 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -499,6 +499,7 @@ /* Begin PBXShellScriptBuildPhase section */ C8A3AE572A28852D0046E809 /* Sign Python STD */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -517,6 +518,7 @@ }; C8A3B1782A2894E10046E809 /* Sign Python Site Packages */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); From 651bf1b8cb00d524dd56c7638bd20f0a710c98f4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 4 Jun 2023 02:05:00 +0800 Subject: [PATCH 10/49] Move Preferences and Configs to Tool --- Core/Package.swift | 45 ++++++++++++------- Tool/Package.swift | 21 +++++---- .../Sources/Configs/Configurations.swift | 0 .../Sources/Preferences/AppStorage.swift | 0 .../Preferences/ChatFeatureProvider.swift | 0 .../Sources/Preferences/ChatGPTModel.swift | 0 .../Sources/Preferences/CustomCommand.swift | 0 {Core => Tool}/Sources/Preferences/Keys.swift | 0 .../Sources/Preferences/Locale.swift | 0 .../Sources/Preferences/NodeRunner.swift | 0 .../Preferences/PresentationMode.swift | 0 .../PromptToCodeFeatureProvider.swift | 0 .../SuggestionFeatureProvider.swift | 0 .../SuggestionWidgetPositionMode.swift | 0 .../Sources/Preferences/UserDefaults.swift | 0 .../Preferences/WidgetColorScheme.swift | 0 .../ToolTests.swift | 0 17 files changed, 42 insertions(+), 24 deletions(-) rename {Core => Tool}/Sources/Configs/Configurations.swift (100%) rename {Core => Tool}/Sources/Preferences/AppStorage.swift (100%) rename {Core => Tool}/Sources/Preferences/ChatFeatureProvider.swift (100%) rename {Core => Tool}/Sources/Preferences/ChatGPTModel.swift (100%) rename {Core => Tool}/Sources/Preferences/CustomCommand.swift (100%) rename {Core => Tool}/Sources/Preferences/Keys.swift (100%) rename {Core => Tool}/Sources/Preferences/Locale.swift (100%) rename {Core => Tool}/Sources/Preferences/NodeRunner.swift (100%) rename {Core => Tool}/Sources/Preferences/PresentationMode.swift (100%) rename {Core => Tool}/Sources/Preferences/PromptToCodeFeatureProvider.swift (100%) rename {Core => Tool}/Sources/Preferences/SuggestionFeatureProvider.swift (100%) rename {Core => Tool}/Sources/Preferences/SuggestionWidgetPositionMode.swift (100%) rename {Core => Tool}/Sources/Preferences/UserDefaults.swift (100%) rename {Core => Tool}/Sources/Preferences/WidgetColorScheme.swift (100%) rename Tool/Tests/{LangChainServiceTests => LangChainTests}/ToolTests.swift (100%) diff --git a/Core/Package.swift b/Core/Package.swift index a0223044..d53fd870 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -26,7 +26,6 @@ let package = Package( "SuggestionModel", "Client", "XPCShared", - "Preferences", "Logger", ] ), @@ -38,7 +37,6 @@ let package = Package( "GitHubCopilotService", "Client", "XPCShared", - "Preferences", "LaunchAgentManager", "Logger", "UpdateChecker", @@ -65,10 +63,10 @@ let package = Package( name: "Client", dependencies: [ "SuggestionModel", - "Preferences", "XPCShared", "Logger", "GitHubCopilotService", + .product(name: "Preferences", package: "Tool"), ] ), .target( @@ -78,7 +76,6 @@ let package = Package( "SuggestionService", "GitHubCopilotService", "OpenAIService", - "Preferences", "XPCShared", "CGEventObserver", "DisplayLink", @@ -92,6 +89,7 @@ let package = Package( "PromptToCodeService", "ServiceUpdateMigration", "UserDefaultsObserver", + .product(name: "Preferences", package: "Tool"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "PythonKit", package: "PythonKit"), ] @@ -103,9 +101,9 @@ let package = Package( "Client", "GitHubCopilotService", "SuggestionInjector", - "Preferences", "XPCShared", "Environment", + .product(name: "Preferences", package: "Tool"), ] ), .target( @@ -116,19 +114,18 @@ let package = Package( "SuggestionService", ] ), - .target(name: "Preferences", dependencies: ["Configs"]), // MARK: - Host App .target( name: "HostApp", dependencies: [ - "Preferences", "Client", "GitHubCopilotService", "CodeiumService", "SuggestionModel", "LaunchAgentManager", + .product(name: "Preferences", package: "Tool"), ] ), @@ -182,7 +179,11 @@ let package = Package( "OpenAIService", "Environment", "XcodeInspector", - "Preferences", + + // plugins + "MathChatPlugin", + + .product(name: "Preferences", package: "Tool"), ] ), .target( @@ -199,9 +200,9 @@ let package = Package( dependencies: [ "OpenAIService", "Environment", - "Preferences", "SuggestionModel", "XcodeInspector", + .product(name: "Preferences", package: "Tool"), ] ), @@ -226,7 +227,6 @@ let package = Package( // MARK: - Helpers - .target(name: "Configs"), .target(name: "CGEventObserver"), .target(name: "Logger"), .target(name: "FileChangeChecker"), @@ -244,7 +244,10 @@ let package = Package( .target(name: "AXExtension"), .target( name: "ServiceUpdateMigration", - dependencies: ["Preferences", "GitHubCopilotService"] + dependencies: [ + "GitHubCopilotService", + .product(name: "Preferences", package: "Tool"), + ] ), .target(name: "UserDefaultsObserver"), .target( @@ -266,7 +269,7 @@ let package = Package( "LanguageClient", "SuggestionModel", "XPCShared", - "Preferences", + .product(name: "Preferences", package: "Tool"), .product(name: "Terminal", package: "Tool"), ] ), @@ -281,8 +284,8 @@ let package = Package( name: "OpenAIService", dependencies: [ "Logger", - "Preferences", "GPTEncoder", + .product(name: "Preferences", package: "Tool"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), ] ), @@ -298,12 +301,24 @@ let package = Package( dependencies: [ "LanguageClient", "SuggestionModel", - "Preferences", "KeychainAccess", + .product(name: "Preferences", package: "Tool"), .product(name: "Terminal", package: "Tool"), - "Configs", ] ), + + // MARK: - Chat Plugins + + .target( + name: "MathChatPlugin", + dependencies: [ + "ChatPlugin", + "OpenAIService", + .product(name: "LangChain", package: "Tool"), + .product(name: "PythonKit", package: "PythonKit"), + ], + path: "Sources/ChatPlugins/MathChatPlugin" + ), ] ) diff --git a/Tool/Package.swift b/Tool/Package.swift index 8e36b29e..82201adb 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -8,7 +8,8 @@ let package = Package( platforms: [.macOS(.v12)], products: [ .library(name: "Terminal", targets: ["Terminal"]), - .library(name: "LangChainService", targets: ["LangChainService"]), + .library(name: "LangChain", targets: ["LangChain"]), + .library(name: "Preferences", targets: ["Preferences", "Configs"]), ], dependencies: [ .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), @@ -16,25 +17,27 @@ let package = Package( targets: [ // MARK: - Helpers - .target( - name: "Terminal", - dependencies: [] - ), + .target(name: "Configs"), + + .target(name: "Preferences", dependencies: ["Configs"]), + + .target(name: "Terminal"), // MARK: - Services .target( - name: "LangChainService", + name: "LangChain", dependencies: [ - .product(name: "PythonKit", package: "PythonKit") + .product(name: "PythonKit", package: "PythonKit"), ] ), // MARK: - Tests .testTarget( - name: "LangChainServiceTests", - dependencies: ["LangChainService"] + name: "LangChainTests", + dependencies: ["LangChain"] ), ] ) + diff --git a/Core/Sources/Configs/Configurations.swift b/Tool/Sources/Configs/Configurations.swift similarity index 100% rename from Core/Sources/Configs/Configurations.swift rename to Tool/Sources/Configs/Configurations.swift diff --git a/Core/Sources/Preferences/AppStorage.swift b/Tool/Sources/Preferences/AppStorage.swift similarity index 100% rename from Core/Sources/Preferences/AppStorage.swift rename to Tool/Sources/Preferences/AppStorage.swift diff --git a/Core/Sources/Preferences/ChatFeatureProvider.swift b/Tool/Sources/Preferences/ChatFeatureProvider.swift similarity index 100% rename from Core/Sources/Preferences/ChatFeatureProvider.swift rename to Tool/Sources/Preferences/ChatFeatureProvider.swift diff --git a/Core/Sources/Preferences/ChatGPTModel.swift b/Tool/Sources/Preferences/ChatGPTModel.swift similarity index 100% rename from Core/Sources/Preferences/ChatGPTModel.swift rename to Tool/Sources/Preferences/ChatGPTModel.swift diff --git a/Core/Sources/Preferences/CustomCommand.swift b/Tool/Sources/Preferences/CustomCommand.swift similarity index 100% rename from Core/Sources/Preferences/CustomCommand.swift rename to Tool/Sources/Preferences/CustomCommand.swift diff --git a/Core/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift similarity index 100% rename from Core/Sources/Preferences/Keys.swift rename to Tool/Sources/Preferences/Keys.swift diff --git a/Core/Sources/Preferences/Locale.swift b/Tool/Sources/Preferences/Locale.swift similarity index 100% rename from Core/Sources/Preferences/Locale.swift rename to Tool/Sources/Preferences/Locale.swift diff --git a/Core/Sources/Preferences/NodeRunner.swift b/Tool/Sources/Preferences/NodeRunner.swift similarity index 100% rename from Core/Sources/Preferences/NodeRunner.swift rename to Tool/Sources/Preferences/NodeRunner.swift diff --git a/Core/Sources/Preferences/PresentationMode.swift b/Tool/Sources/Preferences/PresentationMode.swift similarity index 100% rename from Core/Sources/Preferences/PresentationMode.swift rename to Tool/Sources/Preferences/PresentationMode.swift diff --git a/Core/Sources/Preferences/PromptToCodeFeatureProvider.swift b/Tool/Sources/Preferences/PromptToCodeFeatureProvider.swift similarity index 100% rename from Core/Sources/Preferences/PromptToCodeFeatureProvider.swift rename to Tool/Sources/Preferences/PromptToCodeFeatureProvider.swift diff --git a/Core/Sources/Preferences/SuggestionFeatureProvider.swift b/Tool/Sources/Preferences/SuggestionFeatureProvider.swift similarity index 100% rename from Core/Sources/Preferences/SuggestionFeatureProvider.swift rename to Tool/Sources/Preferences/SuggestionFeatureProvider.swift diff --git a/Core/Sources/Preferences/SuggestionWidgetPositionMode.swift b/Tool/Sources/Preferences/SuggestionWidgetPositionMode.swift similarity index 100% rename from Core/Sources/Preferences/SuggestionWidgetPositionMode.swift rename to Tool/Sources/Preferences/SuggestionWidgetPositionMode.swift diff --git a/Core/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift similarity index 100% rename from Core/Sources/Preferences/UserDefaults.swift rename to Tool/Sources/Preferences/UserDefaults.swift diff --git a/Core/Sources/Preferences/WidgetColorScheme.swift b/Tool/Sources/Preferences/WidgetColorScheme.swift similarity index 100% rename from Core/Sources/Preferences/WidgetColorScheme.swift rename to Tool/Sources/Preferences/WidgetColorScheme.swift diff --git a/Tool/Tests/LangChainServiceTests/ToolTests.swift b/Tool/Tests/LangChainTests/ToolTests.swift similarity index 100% rename from Tool/Tests/LangChainServiceTests/ToolTests.swift rename to Tool/Tests/LangChainTests/ToolTests.swift From 513468d96002215e098c9cf1c24b1b2d73a41336 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 4 Jun 2023 02:05:26 +0800 Subject: [PATCH 11/49] Add LangChainChatModel and ReadablePythonError --- .../LangChain/LangChainChatModel.swift | 43 +++++++++++++++++++ .../LangChain/ReadablePythonError.swift | 34 +++++++++++++++ Tool/Sources/LangChainService/Schema.swift | 6 --- 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 Tool/Sources/LangChain/LangChainChatModel.swift create mode 100644 Tool/Sources/LangChain/ReadablePythonError.swift delete mode 100644 Tool/Sources/LangChainService/Schema.swift diff --git a/Tool/Sources/LangChain/LangChainChatModel.swift b/Tool/Sources/LangChain/LangChainChatModel.swift new file mode 100644 index 00000000..4ff378be --- /dev/null +++ b/Tool/Sources/LangChain/LangChainChatModel.swift @@ -0,0 +1,43 @@ +import Foundation +import Preferences +import PythonKit + +public enum LangChainChatModel { + public static func DynamicChatOpenAI( + temperature: Double + ) throws -> PythonObject { + switch UserDefaults.shared.value(for: \.chatFeatureProvider) { + case .openAI: + let model = UserDefaults.shared.value(for: \.chatGPTModel) + let apiBaseURL = UserDefaults.shared.value(for: \.openAIBaseURL) + let apiKey = UserDefaults.shared.value(for: \.openAIAPIKey) + return try withReadableThrowingPython { + let chatModels = try Python.attemptImport("langchain.chat_models") + let ChatOpenAI = chatModels.ChatOpenAI + return ChatOpenAI( + temperature: temperature, + model: model, + openai_api_base: "\(apiBaseURL)/v1", + openai_api_key: apiKey + ) + } + case .azureOpenAI: + let apiBaseURL = UserDefaults.shared.value(for: \.azureOpenAIBaseURL) + let apiKey = UserDefaults.shared.value(for: \.azureOpenAIAPIKey) + let deployment = UserDefaults.shared.value(for: \.azureChatGPTDeployment) + return try withReadableThrowingPython { + let chatModels = try Python.attemptImport("langchain.chat_models") + let ChatOpenAI = chatModels.AzureChatOpenAI + return ChatOpenAI( + temperature: temperature, + openai_api_type: "azure", + openai_api_version: "2023-03-15-preview", + deployment_name: deployment, + openai_api_base: apiBaseURL, + openai_api_key: apiKey + ) + } + } + } +} + diff --git a/Tool/Sources/LangChain/ReadablePythonError.swift b/Tool/Sources/LangChain/ReadablePythonError.swift new file mode 100644 index 00000000..bffd696b --- /dev/null +++ b/Tool/Sources/LangChain/ReadablePythonError.swift @@ -0,0 +1,34 @@ +import Foundation +import PythonKit + +public struct ReadablePythonError: Error, LocalizedError { + public var error: PythonError + + public init(_ error: PythonError) { + self.error = error + } + + public var errorDescription: String? { + switch error { + case let .exception(object, _): + return "\(object)" + case let .invalidCall(object): + return "Invalid call: \(object)" + case let .invalidModule(module): + return "Invalid module: \(module)" + } + } +} + +public func withReadableThrowingPython( + _ closure: () throws -> T +) throws -> T { + do { + return try closure() + } catch let error as PythonError { + throw ReadablePythonError(error) + } catch { + throw error + } +} + diff --git a/Tool/Sources/LangChainService/Schema.swift b/Tool/Sources/LangChainService/Schema.swift deleted file mode 100644 index 48242061..00000000 --- a/Tool/Sources/LangChainService/Schema.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct Tool { - public private(set) var text = "Hello, World!" - - public init() { - } -} From ede713556f80ffefc7aba8a6508b3b2d23aaea97 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 4 Jun 2023 02:06:14 +0800 Subject: [PATCH 12/49] Add MathChatPlugin --- .../MathChatPlugin/MathChatPlugin.swift | 63 +++++++++++++++++++ .../MathChatPlugin/SolveMathProblem.swift | 34 ++++++++++ Core/Sources/ChatService/AllPlugins.swift | 8 +++ .../ChatService/ChatPluginController.swift | 8 ++- Core/Sources/ChatService/ChatService.swift | 7 +-- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift create mode 100644 Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift create mode 100644 Core/Sources/ChatService/AllPlugins.swift diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift new file mode 100644 index 00000000..c920fa86 --- /dev/null +++ b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift @@ -0,0 +1,63 @@ +import ChatPlugin +import Environment +import Foundation +import OpenAIService + +/// Use Python to solve math problems. +public actor MathChatPlugin: ChatPlugin { + public static var command: String { "math" } + public nonisolated var name: String { "Math" } + + let chatGPTService: any ChatGPTServiceType + var isCancelled = false + weak var delegate: ChatPluginDelegate? + + public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) { + self.chatGPTService = chatGPTService + self.delegate = delegate + } + + public func send(content: String, originalMessage: String) async { + delegate?.pluginDidStart(self) + delegate?.pluginDidStartResponding(self) + + let id = "\(Self.command)-\(UUID().uuidString)" + var reply = ChatMessage(id: id, role: .assistant, content: "Calculating...") + + await chatGPTService.mutateHistory { history in + history.append(.init(role: .user, content: originalMessage, summary: content)) + history.append(reply) + } + + do { + let result = try await solveMathProblem(content) + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + reply.content = result + history.append(reply) + } + } catch { + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + reply.content = error.localizedDescription + history.append(reply) + } + } + + delegate?.pluginDidEndResponding(self) + delegate?.pluginDidEnd(self) + } + + public func cancel() async { + isCancelled = true + } + + public func stopResponding() async { + isCancelled = true + } +} + diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift new file mode 100644 index 00000000..c117dd89 --- /dev/null +++ b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift @@ -0,0 +1,34 @@ +import Foundation +import LangChain +import PythonKit + +func solveMathProblem(_ problem: String) async throws -> String { + #if DEBUG + let verbose = true + #else + let verbose = false + #endif + + struct E: Error, LocalizedError { + var errorDescription: String? { + "Failed to parse answer." + } + } + + let task = Task { + try withReadableThrowingPython { + let langchain = try Python.attemptImport("langchain") + let LLMMathChain = langchain.LLMMathChain + let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) + let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) + let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) + let answer = String(result) + if let answer { return answer } + + throw E() + } + } + + return try await task.value +} + diff --git a/Core/Sources/ChatService/AllPlugins.swift b/Core/Sources/ChatService/AllPlugins.swift new file mode 100644 index 00000000..21027cd3 --- /dev/null +++ b/Core/Sources/ChatService/AllPlugins.swift @@ -0,0 +1,8 @@ +import ChatPlugin +import MathChatPlugin + +let allPlugins: [ChatPlugin.Type] = [ + TerminalChatPlugin.self, + AITerminalChatPlugin.self, + MathChatPlugin.self, +] diff --git a/Core/Sources/ChatService/ChatPluginController.swift b/Core/Sources/ChatService/ChatPluginController.swift index 2bf05a74..061e3d6b 100644 --- a/Core/Sources/ChatService/ChatPluginController.swift +++ b/Core/Sources/ChatService/ChatPluginController.swift @@ -7,8 +7,8 @@ final class ChatPluginController { let chatGPTService: any ChatGPTServiceType let plugins: [String: ChatPlugin.Type] var runningPlugin: ChatPlugin? - - init(chatGPTService: any ChatGPTServiceType, plugins: ChatPlugin.Type...) { + + init(chatGPTService: any ChatGPTServiceType, plugins: [ChatPlugin.Type]) { self.chatGPTService = chatGPTService var all = [String: ChatPlugin.Type]() for plugin in plugins { @@ -17,6 +17,10 @@ final class ChatPluginController { self.plugins = all } + convenience init(chatGPTService: any ChatGPTServiceType, plugins: ChatPlugin.Type...) { + self.init(chatGPTService: chatGPTService, plugins: plugins) + } + /// Handle the message in a plugin if required. Return false if no plugin handles the message. func handleContent(_ content: String) async throws -> Bool { // look for the prefix of content, see if there is something like /command. diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index f5749dc0..0c8c690a 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -25,12 +25,7 @@ public final class ChatService: ObservableObject { public init(chatGPTService: T) { self.chatGPTService = chatGPTService - pluginController = ChatPluginController( - chatGPTService: chatGPTService, - plugins: - TerminalChatPlugin.self, - AITerminalChatPlugin.self - ) + pluginController = ChatPluginController(chatGPTService: chatGPTService, plugins: allPlugins) contextController = DynamicContextController( chatGPTService: chatGPTService, contextCollectors: ActiveDocumentChatContextCollector() From e83e885c495ae0ee79020a5f6da793c1b5e18492 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 4 Jun 2023 18:36:10 +0800 Subject: [PATCH 13/49] Update --- ExtensionService/AppDelegate.swift | 16 ++++++++-------- ExtensionService/InitializePython.swift | 12 ++---------- site-packages/requirements.txt | 2 ++ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/ExtensionService/AppDelegate.swift b/ExtensionService/AppDelegate.swift index eb59fa88..2e9780a2 100644 --- a/ExtensionService/AppDelegate.swift +++ b/ExtensionService/AppDelegate.swift @@ -30,14 +30,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationDidFinishLaunching(_: Notification) { if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return } -// _ = GraphicalUserInterfaceController.shared -// _ = RealtimeSuggestionController.shared -// _ = XcodeInspector.shared -// AXIsProcessTrustedWithOptions([ -// kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true, -// ] as CFDictionary) -// setupQuitOnUpdate() -// setupQuitOnUserTerminated() + _ = GraphicalUserInterfaceController.shared + _ = RealtimeSuggestionController.shared + _ = XcodeInspector.shared + AXIsProcessTrustedWithOptions([ + kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true, + ] as CFDictionary) + setupQuitOnUpdate() + setupQuitOnUserTerminated() xpcListener = setupXPCListener() Logger.service.info("XPC Service started.") NSApp.setActivationPolicy(.accessory) diff --git a/ExtensionService/InitializePython.swift b/ExtensionService/InitializePython.swift index 27b2919d..93ef236e 100644 --- a/ExtensionService/InitializePython.swift +++ b/ExtensionService/InitializePython.swift @@ -2,24 +2,16 @@ import Foundation import Python import PythonKit -@available( *, deprecated, message: "Testing" ) func initializePython() { guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil), let libDynloadPath = Bundle.main.path( forResource: "python-stdlib/lib-dynload", ofType: nil - ) else { return } + ) + else { return } setenv("PYTHONHOME", stdLibPath, 1) setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) Py_Initialize() - - let sys = Python.import("sys") - print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") - print("Python Encoding: \(sys.getdefaultencoding().upper())") - print("Python Path: \(sys.path)") - - let llms = Python.import("langchain.llms") - print(llms.OpenAI) } diff --git a/site-packages/requirements.txt b/site-packages/requirements.txt index 5046bf5e..214e842d 100644 --- a/site-packages/requirements.txt +++ b/site-packages/requirements.txt @@ -1,2 +1,4 @@ openai langchain +readability-lxml +tiktoken From 72eb8eab114844067e6dfaab3862cd8b4d3acdb1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 4 Jun 2023 18:39:58 +0800 Subject: [PATCH 14/49] Adjust folder structure --- .gitignore | 10 +++++----- Copilot for Xcode.xcodeproj/project.pbxproj | 14 +++++++++++--- Python/module.modulemap | 5 +++++ {site-packages => Python/site-packages}/install.sh | 0 .../site-packages}/requirements.txt | 0 5 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 Python/module.modulemap rename {site-packages => Python/site-packages}/install.sh (100%) rename {site-packages => Python/site-packages}/requirements.txt (100%) diff --git a/.gitignore b/.gitignore index 2b6dd734..c9f108e9 100644 --- a/.gitignore +++ b/.gitignore @@ -125,9 +125,9 @@ iOSInjectionProject/ https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager Secrets.xcconfig -Python.xcframework -python-stdlib -site-packages/* -!site-packages/requirements.txt -!site-packages/install.sh +Python/Python.xcframework +Python/python-stdlib +Python/site-packages/* +!Python/site-packages/requirements.txt +!Python/site-packages/install.sh diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index b103f1d3..7e2699d6 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -264,9 +264,7 @@ C81458922939EFDC00135263 /* EditorExtension */, C8216B71298036EC00AD38C7 /* Helper */, C861E60F2994F6070056CB02 /* ExtensionService */, - C8A3AE512A2883430046E809 /* Python.xcframework */, - C8A3B1762A288FA90046E809 /* python-stdlib */, - C8A3AE5A2A288AF90046E809 /* site-packages */, + C81BBF5A2A2CA0B8000B4F61 /* Python */, C814588D2939EFDC00135263 /* Frameworks */, C8189B172938972F00C9DCDA /* Products */, ); @@ -303,6 +301,16 @@ path = "Preview Content"; sourceTree = ""; }; + C81BBF5A2A2CA0B8000B4F61 /* Python */ = { + isa = PBXGroup; + children = ( + C8A3AE512A2883430046E809 /* Python.xcframework */, + C8A3B1762A288FA90046E809 /* python-stdlib */, + C8A3AE5A2A288AF90046E809 /* site-packages */, + ); + path = Python; + sourceTree = ""; + }; C8216B71298036EC00AD38C7 /* Helper */ = { isa = PBXGroup; children = ( diff --git a/Python/module.modulemap b/Python/module.modulemap new file mode 100644 index 00000000..d89495b6 --- /dev/null +++ b/Python/module.modulemap @@ -0,0 +1,5 @@ +module Python { + umbrella header "Python.h" + export * + link "Python" +} \ No newline at end of file diff --git a/site-packages/install.sh b/Python/site-packages/install.sh similarity index 100% rename from site-packages/install.sh rename to Python/site-packages/install.sh diff --git a/site-packages/requirements.txt b/Python/site-packages/requirements.txt similarity index 100% rename from site-packages/requirements.txt rename to Python/site-packages/requirements.txt From 878cda667c43947000035f5fa63c12dc8e84083d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 4 Jun 2023 23:21:17 +0800 Subject: [PATCH 15/49] Add script to install python --- .gitignore | 1 + Makefile | 12 ++++++++++++ Python/site-packages/install.sh | 4 ++-- VERSIONS | 8 ++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 VERSIONS diff --git a/.gitignore b/.gitignore index c9f108e9..c915adee 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ Python/site-packages/* !Python/site-packages/requirements.txt !Python/site-packages/install.sh +Python/VERSIONS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..d985bfb3 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +setup: setup-langchain + +setup-langchain: + cd Python; \ + curl -L https://github.com/beeware/Python-Apple-support/releases/download/3.11-b1/Python-3.11-macOS-support.b1.tar.gz -o Python-3.11-macOS-support.b1.tar.gz; \ + tar -xzvf Python-3.11-macOS-support.b1.tar.gz; \ + rm Python-3.11-macOS-support.b1.tar.gz; \ + cp module.modulemap Python.xcframework/macos-arm64_x86_64/Headers/module.modulemap + cd Python/site-packages; \ + sh ./install.sh + +.PHONY: setup setup-langchain \ No newline at end of file diff --git a/Python/site-packages/install.sh b/Python/site-packages/install.sh index ea188e9c..97ce7cda 100755 --- a/Python/site-packages/install.sh +++ b/Python/site-packages/install.sh @@ -7,5 +7,5 @@ python3.11 -m pip install -r requirements.txt -t . rm pip/__init__.py cp ../pip_init.py pip/__init__.py -find . -name "__pycache__" -exec rm -rf {} \; -find "*.so" -delete \ No newline at end of file +find . -name "__pycache__" -exec rm -rf {} \; || true +find "*.so" -delete || true diff --git a/VERSIONS b/VERSIONS new file mode 100644 index 00000000..505965bf --- /dev/null +++ b/VERSIONS @@ -0,0 +1,8 @@ +Python version: 3.11.0 +Build: b1 +Min macOS version: 10.15 +--------------------- +libFFI: macOS native +BZip2: 1.0.8 +OpenSSL: 3.0.5 +XZ: 5.2.6 From 204b4f368c90d4077b2a2f110c08c94be6e003f7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 00:11:26 +0800 Subject: [PATCH 16/49] Fix importing large packages like langchain --- .../MathChatPlugin/SolveMathProblem.swift | 23 +++-- ExtensionService/InitializePython.swift | 25 ++++++ Tool/Package.swift | 10 ++- .../LangChain/LangChainChatModel.swift | 41 ++++----- .../LangChain/ReadablePythonError.swift | 34 ------- Tool/Sources/PythonHelper/PythonThread.swift | 90 +++++++++++++++++++ Tool/Sources/PythonHelper/RunPython.swift | 70 +++++++++++++++ 7 files changed, 223 insertions(+), 70 deletions(-) delete mode 100644 Tool/Sources/LangChain/ReadablePythonError.swift create mode 100644 Tool/Sources/PythonHelper/PythonThread.swift create mode 100644 Tool/Sources/PythonHelper/RunPython.swift diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift index c117dd89..eff83078 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift @@ -1,6 +1,7 @@ import Foundation import LangChain import PythonKit +import PythonHelper func solveMathProblem(_ problem: String) async throws -> String { #if DEBUG @@ -15,20 +16,16 @@ func solveMathProblem(_ problem: String) async throws -> String { } } - let task = Task { - try withReadableThrowingPython { - let langchain = try Python.attemptImport("langchain") - let LLMMathChain = langchain.LLMMathChain - let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) - let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) - let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) - let answer = String(result) - if let answer { return answer } + return try await runPython { + let langchain = try Python.attemptImportOnPythonThread("langchain") + let LLMMathChain = langchain.LLMMathChain + let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) + let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) + let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) + let answer = String(result) + if let answer { return answer } - throw E() - } + throw E() } - - return try await task.value } diff --git a/ExtensionService/InitializePython.swift b/ExtensionService/InitializePython.swift index 93ef236e..2150e18c 100644 --- a/ExtensionService/InitializePython.swift +++ b/ExtensionService/InitializePython.swift @@ -1,6 +1,7 @@ import Foundation import Python import PythonKit +import PythonHelper func initializePython() { guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), @@ -10,8 +11,32 @@ func initializePython() { ofType: nil ) else { return } + setenv("PYTHONHOME", stdLibPath, 1) setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) + + // Initialize python Py_Initialize() + + // Immediately release the thread, so that we can ensure the GIL state later. + // We may not recover the thread because all future tasks will be done in the Python Thread. + let _ = PyEval_SaveThread() + + // Setup GIL state guard. + PythonHelper.PyGILState_Guard = { closure in + let gilState = PyGILState_Ensure() + try closure() + PyGILState_Release(gilState) + } + + Task { + // All future task should run inside runPython. + try await runPython { + let sys = Python.import("sys") + print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") + } + } } +let queue = DispatchQueue(label: "") + diff --git a/Tool/Package.swift b/Tool/Package.swift index 82201adb..f13d471d 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -8,7 +8,7 @@ let package = Package( platforms: [.macOS(.v12)], products: [ .library(name: "Terminal", targets: ["Terminal"]), - .library(name: "LangChain", targets: ["LangChain"]), + .library(name: "LangChain", targets: ["LangChain", "PythonHelper"]), .library(name: "Preferences", targets: ["Preferences", "Configs"]), ], dependencies: [ @@ -27,6 +27,14 @@ let package = Package( .target( name: "LangChain", + dependencies: [ + "PythonHelper", + .product(name: "PythonKit", package: "PythonKit"), + ] + ), + + .target( + name: "PythonHelper", dependencies: [ .product(name: "PythonKit", package: "PythonKit"), ] diff --git a/Tool/Sources/LangChain/LangChainChatModel.swift b/Tool/Sources/LangChain/LangChainChatModel.swift index 4ff378be..fc8722b7 100644 --- a/Tool/Sources/LangChain/LangChainChatModel.swift +++ b/Tool/Sources/LangChain/LangChainChatModel.swift @@ -1,5 +1,6 @@ import Foundation import Preferences +import PythonHelper import PythonKit public enum LangChainChatModel { @@ -11,32 +12,28 @@ public enum LangChainChatModel { let model = UserDefaults.shared.value(for: \.chatGPTModel) let apiBaseURL = UserDefaults.shared.value(for: \.openAIBaseURL) let apiKey = UserDefaults.shared.value(for: \.openAIAPIKey) - return try withReadableThrowingPython { - let chatModels = try Python.attemptImport("langchain.chat_models") - let ChatOpenAI = chatModels.ChatOpenAI - return ChatOpenAI( - temperature: temperature, - model: model, - openai_api_base: "\(apiBaseURL)/v1", - openai_api_key: apiKey - ) - } + let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") + let ChatOpenAI = chatModels.ChatOpenAI + return ChatOpenAI( + temperature: temperature, + model: model, + openai_api_base: "\(apiBaseURL)/v1", + openai_api_key: apiKey + ) case .azureOpenAI: let apiBaseURL = UserDefaults.shared.value(for: \.azureOpenAIBaseURL) let apiKey = UserDefaults.shared.value(for: \.azureOpenAIAPIKey) let deployment = UserDefaults.shared.value(for: \.azureChatGPTDeployment) - return try withReadableThrowingPython { - let chatModels = try Python.attemptImport("langchain.chat_models") - let ChatOpenAI = chatModels.AzureChatOpenAI - return ChatOpenAI( - temperature: temperature, - openai_api_type: "azure", - openai_api_version: "2023-03-15-preview", - deployment_name: deployment, - openai_api_base: apiBaseURL, - openai_api_key: apiKey - ) - } + let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") + let ChatOpenAI = chatModels.AzureChatOpenAI + return ChatOpenAI( + temperature: temperature, + openai_api_type: "azure", + openai_api_version: "2023-03-15-preview", + deployment_name: deployment, + openai_api_base: apiBaseURL, + openai_api_key: apiKey + ) } } } diff --git a/Tool/Sources/LangChain/ReadablePythonError.swift b/Tool/Sources/LangChain/ReadablePythonError.swift deleted file mode 100644 index bffd696b..00000000 --- a/Tool/Sources/LangChain/ReadablePythonError.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import PythonKit - -public struct ReadablePythonError: Error, LocalizedError { - public var error: PythonError - - public init(_ error: PythonError) { - self.error = error - } - - public var errorDescription: String? { - switch error { - case let .exception(object, _): - return "\(object)" - case let .invalidCall(object): - return "Invalid call: \(object)" - case let .invalidModule(module): - return "Invalid module: \(module)" - } - } -} - -public func withReadableThrowingPython( - _ closure: () throws -> T -) throws -> T { - do { - return try closure() - } catch let error as PythonError { - throw ReadablePythonError(error) - } catch { - throw error - } -} - diff --git a/Tool/Sources/PythonHelper/PythonThread.swift b/Tool/Sources/PythonHelper/PythonThread.swift new file mode 100644 index 00000000..fe9ea677 --- /dev/null +++ b/Tool/Sources/PythonHelper/PythonThread.swift @@ -0,0 +1,90 @@ +import Foundation +import PythonKit + +final class PythonThread: Thread { + static let shared = { + let thread = PythonThread( + target: PythonThread.self, + selector: #selector(PythonThread.setup), + object: nil + ) + thread.name = "Python Thread" + thread.stackSize = 1_048_576 // so that langchain can be correctly imported. + return thread + }() + + @objc static func setup() { + CFRunLoopRun() + } + + @objc static func runPythonJob(_ job: PythonJob) { + job.run() + } + + func runPython(_ closure: @escaping () -> Void) { + if !isExecuting { + start() + } + + if Thread.current.isEqual(self) { + closure() + } else { + PythonThread.perform( + #selector(PythonThread.runPythonJob), + on: self, + with: PythonJob(closure: closure), + waitUntilDone: false + ) + } + } + + func runPythonAndWait(_ closure: @escaping () throws -> T) throws -> T { + if !isExecuting { + start() + } + + if Thread.current.isEqual(self) { + return try closure() + } else { + let job = PythonJob(closure: closure) + PythonThread.perform( + #selector(PythonThread.runPythonJob), + on: self, + with: job, + waitUntilDone: true + ) + guard let result = job.result else { + throw FailedToGetPythonJobResultError() + } + switch result { + case let .success(value): + if let value = value as? T { + return value + } else { + throw FailedToGetPythonJobResultError() + } + case let .failure(error): + throw error + } + } + } +} + +struct FailedToGetPythonJobResultError: Error, LocalizedError { + var errorDescription: String? { + "Failed to get PythonJob result." + } +} + +final class PythonJob: NSObject { + let closure: () throws -> Any + var result: Result? + init(closure: @escaping () throws -> Any) { + self.closure = closure + } + + func run() { + result = Result(catching: closure) + } +} + diff --git a/Tool/Sources/PythonHelper/RunPython.swift b/Tool/Sources/PythonHelper/RunPython.swift new file mode 100644 index 00000000..2a4d095f --- /dev/null +++ b/Tool/Sources/PythonHelper/RunPython.swift @@ -0,0 +1,70 @@ +import Foundation +import PythonKit + +public var PyGILState_Guard: ((() throws -> Void) throws -> Void)! = nil + +let pythonQueue = DispatchQueue(label: "Python Queue") + +public func runPython( + usePythonThread: Bool = false, + _ closure: @escaping () throws -> T +) async throws -> T { + return try await withUnsafeThrowingContinuation { con in + if usePythonThread { + PythonThread.shared.runPython { + do { + try PyGILState_Guard { + con.resume(returning: try closure()) + } + } catch let error as PythonError { + con.resume(throwing: ReadablePythonError(error)) + } catch { + con.resume(throwing: error) + } + } + } else { + pythonQueue.async { + do { + try PyGILState_Guard { + con.resume(returning: try closure()) + } + } catch let error as PythonError { + con.resume(throwing: ReadablePythonError(error)) + } catch { + con.resume(throwing: error) + } + } + } + } +} + +public extension PythonInterface { + func attemptImportOnPythonThread(_ name: String) throws -> PythonObject { + try PythonThread.shared.runPythonAndWait { + let module = try Python.attemptImport(name) + return module + } + } +} + +public struct ReadablePythonError: Error, LocalizedError { + public var error: PythonError + + public init(_ error: PythonError) { + self.error = error + } + + public var errorDescription: String? { + switch error { + case let .exception(object, _): + return "\(object)" + case let .invalidCall(object): + return "Invalid call: \(object)" + case let .invalidModule(module): + return "Invalid module: \(module)" + } + } +} + + + From 1c52b21a854d64c1ebbdc3ef827c17b681e9acb8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 12:27:09 +0800 Subject: [PATCH 17/49] Make runPython sync --- .../MathChatPlugin/SolveMathProblem.swift | 24 +++++---- ExtensionService/InitializePython.swift | 19 +++---- Tool/Sources/PythonHelper/RunPython.swift | 50 ++++++++----------- 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift index eff83078..83369f61 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift @@ -1,7 +1,7 @@ import Foundation import LangChain -import PythonKit import PythonHelper +import PythonKit func solveMathProblem(_ problem: String) async throws -> String { #if DEBUG @@ -16,16 +16,20 @@ func solveMathProblem(_ problem: String) async throws -> String { } } - return try await runPython { - let langchain = try Python.attemptImportOnPythonThread("langchain") - let LLMMathChain = langchain.LLMMathChain - let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) - let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) - let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) - let answer = String(result) - if let answer { return answer } + let task = Task { + try runPython { + let langchain = try Python.attemptImportOnPythonThread("langchain") + let LLMMathChain = langchain.LLMMathChain + let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) + let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) + let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) + let answer = String(result) + if let answer { return answer } - throw E() + throw E() + } } + + return try await task.value } diff --git a/ExtensionService/InitializePython.swift b/ExtensionService/InitializePython.swift index 2150e18c..e2e39b3f 100644 --- a/ExtensionService/InitializePython.swift +++ b/ExtensionService/InitializePython.swift @@ -1,7 +1,7 @@ import Foundation import Python -import PythonKit import PythonHelper +import PythonKit func initializePython() { guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), @@ -14,24 +14,21 @@ func initializePython() { setenv("PYTHONHOME", stdLibPath, 1) setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) - + // Initialize python Py_Initialize() - + // Immediately release the thread, so that we can ensure the GIL state later. // We may not recover the thread because all future tasks will be done in the Python Thread. - let _ = PyEval_SaveThread() - + _ = PyEval_SaveThread() + // Setup GIL state guard. - PythonHelper.PyGILState_Guard = { closure in - let gilState = PyGILState_Ensure() - try closure() - PyGILState_Release(gilState) - } + PythonHelper.gilStateEnsure = { PyGILState_Ensure() } + PythonHelper.gilStateRelease = { gilState in PyGILState_Release(gilState as! PyGILState_STATE) } Task { // All future task should run inside runPython. - try await runPython { + try runPython { let sys = Python.import("sys") print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") } diff --git a/Tool/Sources/PythonHelper/RunPython.swift b/Tool/Sources/PythonHelper/RunPython.swift index 2a4d095f..8e175e16 100644 --- a/Tool/Sources/PythonHelper/RunPython.swift +++ b/Tool/Sources/PythonHelper/RunPython.swift @@ -1,40 +1,36 @@ import Foundation import PythonKit -public var PyGILState_Guard: ((() throws -> Void) throws -> Void)! = nil +public var gilStateEnsure: (() -> Any)! +public var gilStateRelease: ((Any) -> Void)! +func gilStateGuard(_ closure: @escaping () throws -> T) throws -> T { + let state = gilStateEnsure() + do { + let result = try closure() + gilStateRelease(state) + return result + } catch { + gilStateRelease(state) + throw error + } +} let pythonQueue = DispatchQueue(label: "Python Queue") public func runPython( usePythonThread: Bool = false, _ closure: @escaping () throws -> T -) async throws -> T { - return try await withUnsafeThrowingContinuation { con in - if usePythonThread { - PythonThread.shared.runPython { - do { - try PyGILState_Guard { - con.resume(returning: try closure()) - } - } catch let error as PythonError { - con.resume(throwing: ReadablePythonError(error)) - } catch { - con.resume(throwing: error) - } - } - } else { - pythonQueue.async { - do { - try PyGILState_Guard { - con.resume(returning: try closure()) - } - } catch let error as PythonError { - con.resume(throwing: ReadablePythonError(error)) - } catch { - con.resume(throwing: error) - } +) throws -> T { + if usePythonThread { + return try PythonThread.shared.runPythonAndWait { + return try gilStateGuard { + try closure() } } + } else { + return try gilStateGuard { + try closure() + } } } @@ -66,5 +62,3 @@ public struct ReadablePythonError: Error, LocalizedError { } } - - From 3a2fe5de7f77db75cfa829599431031555578d2b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 14:21:35 +0800 Subject: [PATCH 18/49] Remove pythonQueue --- Tool/Sources/PythonHelper/RunPython.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tool/Sources/PythonHelper/RunPython.swift b/Tool/Sources/PythonHelper/RunPython.swift index 8e175e16..d3970ea6 100644 --- a/Tool/Sources/PythonHelper/RunPython.swift +++ b/Tool/Sources/PythonHelper/RunPython.swift @@ -15,8 +15,6 @@ func gilStateGuard(_ closure: @escaping () throws -> T) throws -> T { } } -let pythonQueue = DispatchQueue(label: "Python Queue") - public func runPython( usePythonThread: Bool = false, _ closure: @escaping () throws -> T From 8cd56908f63fd6c6c17b0dc9699cd7470bf07380 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 14:21:44 +0800 Subject: [PATCH 19/49] Update DEVELOPMENT.md --- DEVELOPMENT.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e6c21976..26d72e9d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -27,9 +27,8 @@ Most of the logics are implemented inside the package `Core`. ## Building and Archiving the App -This project includes a Git submodule, `copilot.vim`, so you will need to either initialize the submodule or download it from the [copilot.vim](https://github.com/github/copilot.vim) repository. - -Finally, archive the Copilot for Xcode target. +1. Run `make setup` to setup the project. (You may need to install the specific version of python to install the dependencies, please check `Python/site-packages/install.sh` for details.) +2. Build or archive the Copilot for Xcode target. ## Testing Extension @@ -45,6 +44,24 @@ For new tests, they should be added to the `TestPlan.xctestplan`. To create a chat plugin, please use the `TerminalChatPlugin` as an example. You should add your plugin to the target `ChatPlugin` and register it in `ChatService`. +## LangChain and Python + +The app uses PythonKit to execute Python code. + +When running Python code, ensure that you wrap it inside `runPython`. This will automatically insert `GilStateEnsure` and `GilStateRelease` for you. + +```swift +import PythonHelper + +try runPython { + // access python here +} +``` + +Instead of throwing a `PythonError`, `runPython` will throw a `ReadablePythonError`. + +If importing a Python module causes the app to crash, it is usually due to the thread's stack size being too small. To resolve this, try importing with `Python.attemptImportOnPythonThread`, which is defined in `PythonHelper`, or simply import from the main thread. + ## Code Style We use SwiftFormat to format the code. From 0ffa5dfa67b4786970ba20c40f1ffb7b647430c5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 14:25:01 +0800 Subject: [PATCH 20/49] Update README.md --- README.md | 3 ++- Tool/Sources/LangChain/LangChainChatModel.swift | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab2bc089..d4809574 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,8 @@ If you need to end a plugin, you can just type | Command | Description | |:---:|---| | `/run` | Runs the command under the project root. You can also use environment variable `PROJECT_ROOT` to get the project root and `FILE_PATH` to get the editing file path.| -| `/airun` | Create a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | +| `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | +| `/math` | Solves a math problem in natural language | ### Prompt to Code diff --git a/Tool/Sources/LangChain/LangChainChatModel.swift b/Tool/Sources/LangChain/LangChainChatModel.swift index fc8722b7..d5185ea6 100644 --- a/Tool/Sources/LangChain/LangChainChatModel.swift +++ b/Tool/Sources/LangChain/LangChainChatModel.swift @@ -4,6 +4,7 @@ import PythonHelper import PythonKit public enum LangChainChatModel { + /// Dynamically create a ChatOpenAI object based on the user's preferences. public static func DynamicChatOpenAI( temperature: Double ) throws -> PythonObject { From 386bddfda67960c97b6627386c0bd21943ddb830 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 14:48:11 +0800 Subject: [PATCH 21/49] Simplify python initialization --- ExtensionService/InitializePython.swift | 33 +++++++++++------------ Tool/Sources/PythonHelper/RunPython.swift | 30 +++++++++++++++++++-- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/ExtensionService/InitializePython.swift b/ExtensionService/InitializePython.swift index e2e39b3f..0d9b1058 100644 --- a/ExtensionService/InitializePython.swift +++ b/ExtensionService/InitializePython.swift @@ -2,7 +2,9 @@ import Foundation import Python import PythonHelper import PythonKit +import Logger +@MainActor func initializePython() { guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil), @@ -10,30 +12,27 @@ func initializePython() { forResource: "python-stdlib/lib-dynload", ofType: nil ) - else { return } - - setenv("PYTHONHOME", stdLibPath, 1) - setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) - - // Initialize python - Py_Initialize() - - // Immediately release the thread, so that we can ensure the GIL state later. - // We may not recover the thread because all future tasks will be done in the Python Thread. - _ = PyEval_SaveThread() + else { + Logger.service.info("Python is not installed!") + return + } - // Setup GIL state guard. - PythonHelper.gilStateEnsure = { PyGILState_Ensure() } - PythonHelper.gilStateRelease = { gilState in PyGILState_Release(gilState as! PyGILState_STATE) } + PythonHelper.initializePython( + sitePackagePath: sitePackagePath, + stdLibPath: stdLibPath, + libDynloadPath: libDynloadPath, + Py_Initialize: Py_Initialize, + PyEval_SaveThread: PyEval_SaveThread, + PyGILState_Ensure: PyGILState_Ensure, + PyGILState_Release: PyGILState_Release + ) Task { // All future task should run inside runPython. try runPython { let sys = Python.import("sys") - print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") + Logger.service.info("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") } } } -let queue = DispatchQueue(label: "") - diff --git a/Tool/Sources/PythonHelper/RunPython.swift b/Tool/Sources/PythonHelper/RunPython.swift index d3970ea6..e83db7ba 100644 --- a/Tool/Sources/PythonHelper/RunPython.swift +++ b/Tool/Sources/PythonHelper/RunPython.swift @@ -1,8 +1,8 @@ import Foundation import PythonKit -public var gilStateEnsure: (() -> Any)! -public var gilStateRelease: ((Any) -> Void)! +var gilStateEnsure: (() -> Any)! +var gilStateRelease: ((Any) -> Void)! func gilStateGuard(_ closure: @escaping () throws -> T) throws -> T { let state = gilStateEnsure() do { @@ -15,6 +15,32 @@ func gilStateGuard(_ closure: @escaping () throws -> T) throws -> T { } } +@MainActor +var isPythonInitialized = false +@MainActor +public func initializePython( + sitePackagePath: String, + stdLibPath: String, + libDynloadPath: String, + Py_Initialize: () -> Void, + PyEval_SaveThread: () -> ThreadState, + PyGILState_Ensure: @escaping () -> GilState, + PyGILState_Release: @escaping (GilState) -> Void +) { + guard !isPythonInitialized else { return } + setenv("PYTHONHOME", stdLibPath, 1) + setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) + isPythonInitialized = true + // Initialize python + Py_Initialize() + // Immediately release the thread, so that we can ensure the GIL state later. + // We may not recover the thread because all future tasks will be done in the other threads. + _ = PyEval_SaveThread() + // Setup GIL state guard. + gilStateEnsure = { PyGILState_Ensure() } + gilStateRelease = { gilState in PyGILState_Release(gilState as! GilState) } +} + public func runPython( usePythonThread: Bool = false, _ closure: @escaping () throws -> T From c1b98de156058137bfd4eb95cff2b9f407b7d896 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 15:01:53 +0800 Subject: [PATCH 22/49] Adjust chat instruction --- .../SuggestionPanelContent/ChatPanel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index c14eb8a1..ddc32f4e 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -73,8 +73,6 @@ struct ChatPanelMessages: View { .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) } - Instruction() - ForEach(chat.history.reversed(), id: \.id) { message in let text = message.text.isEmpty && !message.isUser ? "..." : message .text @@ -90,6 +88,8 @@ struct ChatPanelMessages: View { } } .listItemTint(.clear) + + Instruction() Spacer() } @@ -146,7 +146,7 @@ private struct Instruction: View { - The **error and warning** labels. - The text cursor location. - If you'd like me to examine the entire file, simply add `/file` to the beginning of your message. + If you'd like me to examine the entire file, simply add `@file` to the beginning of your message. """ ) } else { @@ -159,7 +159,7 @@ private struct Instruction: View { - The **error and warning** labels. - The text cursor location. - If you would like me to examine the selected code, please prefix your message with `/selection`. If you would like me to examine the entire file, please prefix your message with `/file`. + If you would like me to examine the selected code, please prefix your message with `@selection`. If you would like me to examine the entire file, please prefix your message with `@file`. """ ) } From 02ba264c6da515803b514e2354bbc1195d27e3e8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 16:44:33 +0800 Subject: [PATCH 23/49] Disable library validation until I now how to correctly sign everything --- ExtensionService/ExtensionService.entitlements | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ExtensionService/ExtensionService.entitlements b/ExtensionService/ExtensionService.entitlements index 5a41052f..ae1430f1 100644 --- a/ExtensionService/ExtensionService.entitlements +++ b/ExtensionService/ExtensionService.entitlements @@ -6,6 +6,8 @@ $(TeamIdentifierPrefix)group.$(BUNDLE_IDENTIFIER_BASE) + com.apple.security.cs.disable-library-validation + keychain-access-groups $(AppIdentifierPrefix)$(BUNDLE_IDENTIFIER_BASE).Shared From 08306e4ba0796b8973905486ced7e78e7901162b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 17:02:42 +0800 Subject: [PATCH 24/49] Add default system prompt settings --- Core/Sources/ChatService/ChatService.swift | 18 ++---- Core/Sources/HostApp/CustomCommandView.swift | 64 +------------------ .../FeatureSettings/ChatSettingsView.swift | 8 +++ .../SharedComponents/EditableText.swift | 62 ++++++++++++++++++ Tool/Sources/Preferences/Keys.swift | 24 +++++-- 5 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 Core/Sources/HostApp/SharedComponents/EditableText.swift diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 0c8c690a..048c459b 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -4,23 +4,13 @@ import Combine import Foundation import OpenAIService -let defaultSystemPrompt = """ -You are an AI programming assistant. -Your reply should be concise, clear, informative and logical. -You MUST reply in the format of markdown. -You MUST embed every code you provide in a markdown code block. -You MUST add the programming language name at the start of the markdown code block. -If you are asked to help perform a task, you MUST think step-by-step, then describe each step concisely. -If you are asked to explain code, you MUST explain it step-by-step in a ordered list. -Make your answer short and structured. -""" - public final class ChatService: ObservableObject { public let chatGPTService: any ChatGPTServiceType let pluginController: ChatPluginController let contextController: DynamicContextController var cancellable = Set() - @Published public internal(set) var systemPrompt = defaultSystemPrompt + @Published public internal(set) var systemPrompt = UserDefaults.shared + .value(for: \.defaultChatSystemPrompt) @Published public internal(set) var extraSystemPrompt = "" public init(chatGPTService: T) { @@ -58,7 +48,7 @@ public final class ChatService: ObservableObject { } public func resetPrompt() async { - systemPrompt = defaultSystemPrompt + systemPrompt = UserDefaults.shared.value(for: \.defaultChatSystemPrompt) extraSystemPrompt = "" } @@ -89,7 +79,7 @@ public final class ChatService: ObservableObject { /// Setting it to `nil` to reset the system prompt public func mutateSystemPrompt(_ newPrompt: String?) { - systemPrompt = newPrompt ?? defaultSystemPrompt + systemPrompt = newPrompt ?? UserDefaults.shared.value(for: \.defaultChatSystemPrompt) } public func mutateExtraSystemPrompt(_ newPrompt: String) { diff --git a/Core/Sources/HostApp/CustomCommandView.swift b/Core/Sources/HostApp/CustomCommandView.swift index 79cecf98..46f0c5eb 100644 --- a/Core/Sources/HostApp/CustomCommandView.swift +++ b/Core/Sources/HostApp/CustomCommandView.swift @@ -330,34 +330,6 @@ struct EditCustomCommandView: View { } .padding(.bottom) .background(.regularMaterial) - .sheet(isPresented: .init(get: { editingContentInFullScreen != nil }, set: { - if $0 == false { - editingContentInFullScreen = nil - } - }), content: { - VStack { - if let editingContentInFullScreen { - TextEditor(text: editingContentInFullScreen) - .font(Font.system(.body, design: .monospaced)) - .padding(4) - .frame(minHeight: 120) - .multilineTextAlignment(.leading) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - } - - Button(action: { - editingContentInFullScreen = nil - }) { - Text("Done") - } - } - .padding() - .frame(width: 600, height: 500) - .background(Color(nsColor: .windowBackgroundColor)) - }) } } @@ -365,7 +337,7 @@ struct EditCustomCommandView: View { var promptTextField: some View { VStack(alignment: .leading, spacing: 4) { Text("Prompt") - editableText($prompt) + EditableText(text: $prompt) } .padding(.vertical, 4) } @@ -378,7 +350,7 @@ struct EditCustomCommandView: View { } else { Text(title ?? "System Prompt") } - editableText($systemPrompt) + EditableText(text: $systemPrompt) } .padding(.vertical, 4) } @@ -390,38 +362,6 @@ struct EditCustomCommandView: View { var generateDescriptionToggle: some View { Toggle("Generate Description", isOn: $generatingPromptToCodeDescription) } - - func editableText(_ binding: Binding) -> some View { - Button(action: { - editingContentInFullScreen = binding - }) { - HStack(alignment: .top) { - Text(binding.wrappedValue) - .font(Font.system(.body, design: .monospaced)) - .padding(4) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .background { - RoundedRectangle(cornerRadius: 4) - .fill(Color(nsColor: .textBackgroundColor)) - } - .overlay { - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) - } - Image(systemName: "square.and.pencil") - .resizable() - .scaledToFit() - .frame(width: 14) - .padding(4) - .background( - Color.primary.opacity(0.1), - in: RoundedRectangle(cornerRadius: 4) - ) - } - } - .buttonStyle(.plain) - } } // MARK: - Previews diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 1c56d883..8f747551 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -19,6 +19,7 @@ struct ChatSettingsView: View { @AppStorage(\.chatFeatureProvider) var chatFeatureProvider @AppStorage(\.chatGPTModel) var chatGPTModel + @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt init() {} } @@ -122,6 +123,13 @@ struct ChatSettingsView: View { Text("9 Messages").tag(9) Text("11 Messages").tag(11) } + + VStack(alignment: .leading, spacing: 4) { + Text("Default System Prompt") + EditableText(text: $settings.defaultChatSystemPrompt) + .lineLimit(6) + } + .padding(.vertical, 4) }.onAppear { checkMaxToken() }.onChange(of: settings.chatFeatureProvider) { _ in diff --git a/Core/Sources/HostApp/SharedComponents/EditableText.swift b/Core/Sources/HostApp/SharedComponents/EditableText.swift new file mode 100644 index 00000000..b5186ea0 --- /dev/null +++ b/Core/Sources/HostApp/SharedComponents/EditableText.swift @@ -0,0 +1,62 @@ +import Foundation +import SwiftUI + +struct EditableText: View { + var text: Binding + @State var isEditing: Bool = false + + var body: some View { + Button(action: { + isEditing = true + }) { + HStack(alignment: .top) { + Text(text.wrappedValue) + .font(Font.system(.body, design: .monospaced)) + .padding(4) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 4) + .fill(Color(nsColor: .textBackgroundColor)) + } + .overlay { + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) + } + Image(systemName: "square.and.pencil") + .resizable() + .scaledToFit() + .frame(width: 14) + .padding(4) + .background( + Color.primary.opacity(0.1), + in: RoundedRectangle(cornerRadius: 4) + ) + } + } + .buttonStyle(.plain) + .sheet(isPresented: $isEditing) { + VStack { + TextEditor(text: text) + .font(Font.system(.body, design: .monospaced)) + .padding(4) + .frame(minHeight: 120) + .multilineTextAlignment(.leading) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + ) + + Button(action: { + isEditing = false + }) { + Text("Done") + } + } + .padding() + .frame(width: 600, height: 500) + .background(Color(nsColor: .windowBackgroundColor)) + } + } +} + diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index d0a916ac..a6cd4f4b 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -105,11 +105,11 @@ public extension UserDefaultPreferenceKeys { var azureOpenAIAPIKey: PreferenceKey { .init(defaultValue: "", key: "AzureOpenAIAPIKey") } - + var azureOpenAIBaseURL: PreferenceKey { .init(defaultValue: "", key: "AzureOpenAIBaseURL") } - + var azureChatGPTDeployment: PreferenceKey { .init(defaultValue: "", key: "AzureChatGPTDeployment") } @@ -205,7 +205,7 @@ public extension UserDefaultPreferenceKeys { var chatFeatureProvider: PreferenceKey { .init(defaultValue: .openAI, key: "ChatFeatureProvider") } - + var chatFontSize: PreferenceKey { .init(defaultValue: 12, key: "ChatFontSize") } @@ -225,10 +225,26 @@ public extension UserDefaultPreferenceKeys { var maxEmbeddableFileInChatContextLineCount: PreferenceKey { .init(defaultValue: 100, key: "MaxEmbeddableFileInChatContextLineCount") } - + var useSelectionScopeByDefaultInChatContext: PreferenceKey { .init(defaultValue: true, key: "UseSelectionScopeByDefaultInChatContext") } + + var defaultChatSystemPrompt: PreferenceKey { + .init( + defaultValue: """ + You are an AI programming assistant. + Your reply should be concise, clear, informative and logical. + You MUST reply in the format of markdown. + You MUST embed every code you provide in a markdown code block. + You MUST add the programming language name at the start of the markdown code block. + If you are asked to help perform a task, you MUST think step-by-step, then describe each step concisely. + If you are asked to explain code, you MUST explain it step-by-step in a ordered list. + Make your answer short and structured. + """, + key: "DefaultChatSystemPrompt" + ) + } } // MARK: - Custom Commands From b94ab81a9dbf0d4271d3ac866a77e91535de3d7d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 17:33:49 +0800 Subject: [PATCH 25/49] Support using {{selected_code}} in custom chat command --- .../CustomCommandTemplateProcessor.swift | 37 +++++++++++++++++++ .../WindowBaseCommandHandler.swift | 19 ++++++---- .../Sources/XcodeInspector/SourceEditor.swift | 16 ++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 Core/Sources/Service/CustomCommandTemplateProcessor.swift diff --git a/Core/Sources/Service/CustomCommandTemplateProcessor.swift b/Core/Sources/Service/CustomCommandTemplateProcessor.swift new file mode 100644 index 00000000..6e40973d --- /dev/null +++ b/Core/Sources/Service/CustomCommandTemplateProcessor.swift @@ -0,0 +1,37 @@ +import Foundation +import SuggestionModel +import XcodeInspector + +struct CustomCommandTemplateProcessor { + func process(_ text: String) -> String { + let info = getEditorInformation() + if let editorContent = info.editorContent { + let updatedText = text.replacingOccurrences(of: "{{selected_code}}", with: """ + ```\(info.language.rawValue) + \(editorContent.selectedContent.trimmingCharacters(in: ["\n"])) + ``` + """) + return updatedText + } else { + let updatedText = text.replacingOccurrences(of: "{{selected_code}}", with: "") + return updatedText + } + } + + struct EditorInformation { + let editorContent: SourceEditor.Content? + let language: CodeLanguage + } + + func getEditorInformation() -> EditorInformation { + let editorContent = XcodeInspector.shared.focusedEditor?.content + let documentURL = XcodeInspector.shared.activeDocumentURL + let language = languageIdentifierFromFileURL(documentURL) + + return .init( + editorContent: editorContent, + language: language + ) + } +} + diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index a899c840..6761d5cd 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -1,12 +1,12 @@ import ChatService -import SuggestionModel -import GitHubCopilotService import Environment import Foundation +import GitHubCopilotService import LanguageServerProtocol import Logger import OpenAIService import SuggestionInjector +import SuggestionModel import SuggestionWidget import XPCShared @@ -53,7 +53,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { forFileAt: fileURL, editor: editor ) - + try Task.checkCancellation() if filespace.presentingSuggestion != nil { @@ -388,7 +388,7 @@ extension WindowBaseCommandHandler { presenter.presentPromptToCode(fileURL: fileURL) } - + private func startChat( specifiedSystemPrompt: String?, extraSystemPrompt: String?, @@ -402,15 +402,16 @@ extension WindowBaseCommandHandler { let chat = WidgetDataSource.shared.createChatIfNeeded(for: focusedElementURI) - chat.mutateSystemPrompt(specifiedSystemPrompt) - chat.mutateExtraSystemPrompt(extraSystemPrompt ?? "") + let templateProcessor = CustomCommandTemplateProcessor() + chat.mutateSystemPrompt(specifiedSystemPrompt.map(templateProcessor.process)) + chat.mutateExtraSystemPrompt(extraSystemPrompt.map(templateProcessor.process) ?? "") Task { let customCommandPrefix = { if let name { return "[\(name)] " } return "" }() - + if specifiedSystemPrompt != nil || extraSystemPrompt != nil { await chat.chatGPTService.mutateHistory { history in history.append(.init( @@ -422,10 +423,12 @@ extension WindowBaseCommandHandler { } if let sendingMessageImmediately, !sendingMessageImmediately.isEmpty { - try await chat.send(content: sendingMessageImmediately) + try await chat + .send(content: templateProcessor.process(sendingMessageImmediately)) } } presenter.presentChatRoom(fileURL: focusedElementURI) } } + diff --git a/Core/Sources/XcodeInspector/SourceEditor.swift b/Core/Sources/XcodeInspector/SourceEditor.swift index d5b3e772..1d5c8668 100644 --- a/Core/Sources/XcodeInspector/SourceEditor.swift +++ b/Core/Sources/XcodeInspector/SourceEditor.swift @@ -16,6 +16,22 @@ public class SourceEditor { public var cursorPosition: CursorPosition /// Line annotations of the source editor. public var lineAnnotations: [String] + + public var selectedContent: String { + if let range = selections.first { + let startIndex = min( + max(0, range.start.line), + lines.endIndex - 1 + ) + let endIndex = min( + max(startIndex, range.end.line), + lines.endIndex - 1 + ) + let selectedContent = lines[startIndex...endIndex] + return selectedContent.joined() + } + return "" + } } let runningApplication: NSRunningApplication From 1b837d70ef02a61d9d4ef54534435c31217c4711 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 5 Jun 2023 17:34:09 +0800 Subject: [PATCH 26/49] Add default custom command Send Selected Code to Chat --- Tool/Sources/Preferences/Keys.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index a6cd4f4b..a33eebec 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -271,6 +271,17 @@ public extension UserDefaultPreferenceKeys { generateDescription: true ) ), + .init( + commandId: "BuiltInCustomCommandSendCodeToChat", + name: "Send Selected Code to Chat", + feature: .chatWithSelection( + extraSystemPrompt: "", + prompt: """ + {{selected_code}} + """, + useExtraSystemPrompt: true + ) + ), ], key: "CustomCommands") } } From 32c388ab285e58ae1b8eec733846f00cbc86f579 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 6 Jun 2023 01:02:07 +0800 Subject: [PATCH 27/49] Update --- Core/Package.swift | 12 +++ .../Sources/ChatPlugin/SearchChatPlugin.swift | 31 ------ .../SearchChatPlugin/SearchAgent.swift | 99 +++++++++++++++++++ .../SearchChatPlugin/SearchChatPlugin.swift | 62 ++++++++++++ Core/Sources/ChatService/AllPlugins.swift | 2 + Tool/Sources/PythonHelper/RunPython.swift | 4 +- 6 files changed, 177 insertions(+), 33 deletions(-) delete mode 100644 Core/Sources/ChatPlugin/SearchChatPlugin.swift create mode 100644 Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift create mode 100644 Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift diff --git a/Core/Package.swift b/Core/Package.swift index d53fd870..f832f950 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -182,6 +182,7 @@ let package = Package( // plugins "MathChatPlugin", + "SearchChatPlugin", .product(name: "Preferences", package: "Tool"), ] @@ -319,6 +320,17 @@ let package = Package( ], path: "Sources/ChatPlugins/MathChatPlugin" ), + + .target( + name: "SearchChatPlugin", + dependencies: [ + "ChatPlugin", + "OpenAIService", + .product(name: "LangChain", package: "Tool"), + .product(name: "PythonKit", package: "PythonKit"), + ], + path: "Sources/ChatPlugins/SearchChatPlugin" + ), ] ) diff --git a/Core/Sources/ChatPlugin/SearchChatPlugin.swift b/Core/Sources/ChatPlugin/SearchChatPlugin.swift deleted file mode 100644 index fd47abde..00000000 --- a/Core/Sources/ChatPlugin/SearchChatPlugin.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Environment -import Foundation -import OpenAIService -import PythonKit - -public actor SearchChatPlugin: ChatPlugin { - public static var command: String { "search" } - public nonisolated var name: String { "Search" } - - let chatGPTService: any ChatGPTServiceType - var isCancelled = false - weak var delegate: ChatPluginDelegate? - - public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) { - self.chatGPTService = chatGPTService - self.delegate = delegate - } - - public func send(content: String, originalMessage: String) async { - - } - - public func cancel() async { - isCancelled = true - } - - public func stopResponding() async { - isCancelled = true - } -} - diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift new file mode 100644 index 00000000..ec634505 --- /dev/null +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift @@ -0,0 +1,99 @@ +import Foundation +import LangChain +import PythonHelper +import PythonKit + +func search(_ query: String) async throws -> String { + #if DEBUG + let verbose = true + #else + let verbose = false + #endif + + let task = Task { + try runPython { + let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) + let utilities = try Python.attemptImportOnPythonThread("langchain.utilities") + let BingSearchAPIWrapper = utilities.BingSearchAPIWrapper + let agents = try Python.attemptImportOnPythonThread("langchain.agents") + let Tool = agents.Tool + let initializeAgent = agents.initialize_agent + let AgentType = agents.AgentType + + let bingSearch = BingSearchAPIWrapper( + bing_subscription_key: "f1eaef707e9443ddb08df2cfb6ac1eb5", + bing_search_url: "https://api.bing.microsoft.com/v7.0/search/", + k: 5 + ) + + var links = [String]() + + let getSearchResult = PythonInstanceMethod { arguments -> String in + guard let query = arguments.first else { return "Empty" } + let results = bingSearch.results(query, 5) + let resultString = results.enumerated().map { "[\($0)]:###\($1["snippet"])###" } + .joined(separator: "\n") + links = results.map { + let url = String($0["link"]) ?? "N/A" + let title = String($0["title"]) ?? "Unknown Title" + return "[\(title)](\(url))" + } + return resultString + } + + let ff = PythonClass("FF", members: [ + "run": PythonInstanceMethod { arguments -> String in + guard let query = arguments.first else { return "Empty" } + let results = bingSearch.results(query, 5) + let resultString = results.enumerated().map { "[\($0)]:###\($1["snippet"])###" } + .joined(separator: "\n") + links = results.map { + let url = String($0["link"]) ?? "N/A" + let title = String($0["title"]) ?? "Unknown Title" + return "[\(title)](\(url))" + } + return resultString + } + ]) + + let fi = ff.pythonObject() + + print(fi.run) + print(getSearchResult.pythonObject) + + let tools = [ + Tool( + name: "Search", + func: fi.run, + description: "useful for when you need to answer questions about current events. You should ask targeted questions" + ), + ] + + let chain = initializeAgent( + tools, llm, + agent: AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, + verbose: verbose, + max_iterations: 1, + early_stopping_method: "generate", + agent_kwargs: ["system_message_prefix": "Respond to the human as helpfully and accurately as possible. Wrap any code block in thought in . Format final answer to be more readable, in a ordered list if possible. You have access to the following tools:"] + ) + + let trimmedQuery = query.trimmingCharacters(in: [" ", "\n"]) + do { + let result = try chain.run.throwing.dynamicallyCall(withArguments: trimmedQuery) + return (String(result) ?? "", links) + } catch { + return (error.localizedDescription, links) + } + } + } + + let (answer, links) = try await task.value + + return """ + \(answer) + ------ + \(links.map { "- \($0)" }.joined(separator: "\n")) + """ +} + diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift new file mode 100644 index 00000000..92a5989a --- /dev/null +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift @@ -0,0 +1,62 @@ +import ChatPlugin +import Environment +import Foundation +import OpenAIService + +public actor SearchChatPlugin: ChatPlugin { + public static var command: String { "search" } + public nonisolated var name: String { "Search" } + + let chatGPTService: any ChatGPTServiceType + var isCancelled = false + weak var delegate: ChatPluginDelegate? + + public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) { + self.chatGPTService = chatGPTService + self.delegate = delegate + } + + public func send(content: String, originalMessage: String) async { + delegate?.pluginDidStart(self) + delegate?.pluginDidStartResponding(self) + + let id = "\(Self.command)-\(UUID().uuidString)" + var reply = ChatMessage(id: id, role: .assistant, content: "Calculating...") + + await chatGPTService.mutateHistory { history in + history.append(.init(role: .user, content: originalMessage, summary: content)) + history.append(reply) + } + + do { + let result = try await search(content) + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + reply.content = result + history.append(reply) + } + } catch { + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + reply.content = error.localizedDescription + history.append(reply) + } + } + + delegate?.pluginDidEndResponding(self) + delegate?.pluginDidEnd(self) + } + + public func cancel() async { + isCancelled = true + } + + public func stopResponding() async { + isCancelled = true + } +} + diff --git a/Core/Sources/ChatService/AllPlugins.swift b/Core/Sources/ChatService/AllPlugins.swift index 21027cd3..949bd4da 100644 --- a/Core/Sources/ChatService/AllPlugins.swift +++ b/Core/Sources/ChatService/AllPlugins.swift @@ -1,8 +1,10 @@ import ChatPlugin import MathChatPlugin +import SearchChatPlugin let allPlugins: [ChatPlugin.Type] = [ TerminalChatPlugin.self, AITerminalChatPlugin.self, MathChatPlugin.self, + SearchChatPlugin.self, ] diff --git a/Tool/Sources/PythonHelper/RunPython.swift b/Tool/Sources/PythonHelper/RunPython.swift index e83db7ba..21146c44 100644 --- a/Tool/Sources/PythonHelper/RunPython.swift +++ b/Tool/Sources/PythonHelper/RunPython.swift @@ -5,12 +5,11 @@ var gilStateEnsure: (() -> Any)! var gilStateRelease: ((Any) -> Void)! func gilStateGuard(_ closure: @escaping () throws -> T) throws -> T { let state = gilStateEnsure() + defer { gilStateRelease(state) } do { let result = try closure() - gilStateRelease(state) return result } catch { - gilStateRelease(state) throw error } } @@ -30,6 +29,7 @@ public func initializePython( guard !isPythonInitialized else { return } setenv("PYTHONHOME", stdLibPath, 1) setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath):\(sitePackagePath)", 1) + setenv("PYTHONIOENCODING", "utf-8", 1) isPythonInitialized = true // Initialize python Py_Initialize() From 6eef4133a9c846f021eaeeb81aaac4f037eac5fc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 6 Jun 2023 11:33:12 +0800 Subject: [PATCH 28/49] Remove the current implementation of the new plugins --- .../MathChatPlugin/SolveMathProblem.swift | 55 +++++------ .../SearchChatPlugin/SearchAgent.swift | 92 +------------------ .../LangChain/LangChainChatModel.swift | 80 ++++++++-------- Tool/Tests/LangChainTests/ToolTests.swift | 7 +- 4 files changed, 71 insertions(+), 163 deletions(-) diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift index 83369f61..af25453d 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift @@ -4,32 +4,33 @@ import PythonHelper import PythonKit func solveMathProblem(_ problem: String) async throws -> String { - #if DEBUG - let verbose = true - #else - let verbose = false - #endif - - struct E: Error, LocalizedError { - var errorDescription: String? { - "Failed to parse answer." - } - } - - let task = Task { - try runPython { - let langchain = try Python.attemptImportOnPythonThread("langchain") - let LLMMathChain = langchain.LLMMathChain - let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) - let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) - let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) - let answer = String(result) - if let answer { return answer } - - throw E() - } - } - - return try await task.value +// #if DEBUG +// let verbose = true +// #else +// let verbose = false +// #endif +// +// struct E: Error, LocalizedError { +// var errorDescription: String? { +// "Failed to parse answer." +// } +// } +// +// let task = Task { +// try runPython { +// let langchain = try Python.attemptImportOnPythonThread("langchain") +// let LLMMathChain = langchain.LLMMathChain +// let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) +// let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) +// let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) +// let answer = String(result) +// if let answer { return answer } +// +// throw E() +// } +// } +// +// return try await task.value + return "N/A" } diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift index ec634505..fc5402f1 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift @@ -4,96 +4,6 @@ import PythonHelper import PythonKit func search(_ query: String) async throws -> String { - #if DEBUG - let verbose = true - #else - let verbose = false - #endif - - let task = Task { - try runPython { - let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) - let utilities = try Python.attemptImportOnPythonThread("langchain.utilities") - let BingSearchAPIWrapper = utilities.BingSearchAPIWrapper - let agents = try Python.attemptImportOnPythonThread("langchain.agents") - let Tool = agents.Tool - let initializeAgent = agents.initialize_agent - let AgentType = agents.AgentType - - let bingSearch = BingSearchAPIWrapper( - bing_subscription_key: "f1eaef707e9443ddb08df2cfb6ac1eb5", - bing_search_url: "https://api.bing.microsoft.com/v7.0/search/", - k: 5 - ) - - var links = [String]() - - let getSearchResult = PythonInstanceMethod { arguments -> String in - guard let query = arguments.first else { return "Empty" } - let results = bingSearch.results(query, 5) - let resultString = results.enumerated().map { "[\($0)]:###\($1["snippet"])###" } - .joined(separator: "\n") - links = results.map { - let url = String($0["link"]) ?? "N/A" - let title = String($0["title"]) ?? "Unknown Title" - return "[\(title)](\(url))" - } - return resultString - } - - let ff = PythonClass("FF", members: [ - "run": PythonInstanceMethod { arguments -> String in - guard let query = arguments.first else { return "Empty" } - let results = bingSearch.results(query, 5) - let resultString = results.enumerated().map { "[\($0)]:###\($1["snippet"])###" } - .joined(separator: "\n") - links = results.map { - let url = String($0["link"]) ?? "N/A" - let title = String($0["title"]) ?? "Unknown Title" - return "[\(title)](\(url))" - } - return resultString - } - ]) - - let fi = ff.pythonObject() - - print(fi.run) - print(getSearchResult.pythonObject) - - let tools = [ - Tool( - name: "Search", - func: fi.run, - description: "useful for when you need to answer questions about current events. You should ask targeted questions" - ), - ] - - let chain = initializeAgent( - tools, llm, - agent: AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, - verbose: verbose, - max_iterations: 1, - early_stopping_method: "generate", - agent_kwargs: ["system_message_prefix": "Respond to the human as helpfully and accurately as possible. Wrap any code block in thought in . Format final answer to be more readable, in a ordered list if possible. You have access to the following tools:"] - ) - - let trimmedQuery = query.trimmingCharacters(in: [" ", "\n"]) - do { - let result = try chain.run.throwing.dynamicallyCall(withArguments: trimmedQuery) - return (String(result) ?? "", links) - } catch { - return (error.localizedDescription, links) - } - } - } - - let (answer, links) = try await task.value - - return """ - \(answer) - ------ - \(links.map { "- \($0)" }.joined(separator: "\n")) - """ + return "" } diff --git a/Tool/Sources/LangChain/LangChainChatModel.swift b/Tool/Sources/LangChain/LangChainChatModel.swift index d5185ea6..37dbae90 100644 --- a/Tool/Sources/LangChain/LangChainChatModel.swift +++ b/Tool/Sources/LangChain/LangChainChatModel.swift @@ -1,41 +1,41 @@ -import Foundation -import Preferences -import PythonHelper -import PythonKit - -public enum LangChainChatModel { - /// Dynamically create a ChatOpenAI object based on the user's preferences. - public static func DynamicChatOpenAI( - temperature: Double - ) throws -> PythonObject { - switch UserDefaults.shared.value(for: \.chatFeatureProvider) { - case .openAI: - let model = UserDefaults.shared.value(for: \.chatGPTModel) - let apiBaseURL = UserDefaults.shared.value(for: \.openAIBaseURL) - let apiKey = UserDefaults.shared.value(for: \.openAIAPIKey) - let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") - let ChatOpenAI = chatModels.ChatOpenAI - return ChatOpenAI( - temperature: temperature, - model: model, - openai_api_base: "\(apiBaseURL)/v1", - openai_api_key: apiKey - ) - case .azureOpenAI: - let apiBaseURL = UserDefaults.shared.value(for: \.azureOpenAIBaseURL) - let apiKey = UserDefaults.shared.value(for: \.azureOpenAIAPIKey) - let deployment = UserDefaults.shared.value(for: \.azureChatGPTDeployment) - let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") - let ChatOpenAI = chatModels.AzureChatOpenAI - return ChatOpenAI( - temperature: temperature, - openai_api_type: "azure", - openai_api_version: "2023-03-15-preview", - deployment_name: deployment, - openai_api_base: apiBaseURL, - openai_api_key: apiKey - ) - } - } -} +//import Foundation +//import Preferences +//import PythonHelper +//import PythonKit +// +//public enum LangChainChatModel { +// /// Dynamically create a ChatOpenAI object based on the user's preferences. +// public static func DynamicChatOpenAI( +// temperature: Double +// ) throws -> PythonObject { +// switch UserDefaults.shared.value(for: \.chatFeatureProvider) { +// case .openAI: +// let model = UserDefaults.shared.value(for: \.chatGPTModel) +// let apiBaseURL = UserDefaults.shared.value(for: \.openAIBaseURL) +// let apiKey = UserDefaults.shared.value(for: \.openAIAPIKey) +// let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") +// let ChatOpenAI = chatModels.ChatOpenAI +// return ChatOpenAI( +// temperature: temperature, +// model: model, +// openai_api_base: "\(apiBaseURL)/v1", +// openai_api_key: apiKey +// ) +// case .azureOpenAI: +// let apiBaseURL = UserDefaults.shared.value(for: \.azureOpenAIBaseURL) +// let apiKey = UserDefaults.shared.value(for: \.azureOpenAIAPIKey) +// let deployment = UserDefaults.shared.value(for: \.azureChatGPTDeployment) +// let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") +// let ChatOpenAI = chatModels.AzureChatOpenAI +// return ChatOpenAI( +// temperature: temperature, +// openai_api_type: "azure", +// openai_api_version: "2023-03-15-preview", +// deployment_name: deployment, +// openai_api_base: apiBaseURL, +// openai_api_key: apiKey +// ) +// } +// } +//} diff --git a/Tool/Tests/LangChainTests/ToolTests.swift b/Tool/Tests/LangChainTests/ToolTests.swift index 92299fb0..e142cbb3 100644 --- a/Tool/Tests/LangChainTests/ToolTests.swift +++ b/Tool/Tests/LangChainTests/ToolTests.swift @@ -1,11 +1,8 @@ import XCTest -@testable import Tool +@testable import LangChain final class ToolTests: XCTestCase { func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Tool().text, "Hello, World!") + } } From 3f979b6b1c9e4356e9b4c4251649b19f50bfe935 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 6 Jun 2023 14:52:14 +0800 Subject: [PATCH 29/49] Re-implement math chat plugin in Swift --- Core/Sources/ChatPlugin/AskChatGPT.swift | 8 +- Core/Sources/ChatPlugin/Translate.swift | 34 +++++ .../MathChatPlugin/MathChatPlugin.swift | 7 +- .../MathChatPlugin/SolveMathProblem.swift | 128 +++++++++++++----- .../GitHubCopilotService.swift | 16 ++- 5 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 Core/Sources/ChatPlugin/Translate.swift diff --git a/Core/Sources/ChatPlugin/AskChatGPT.swift b/Core/Sources/ChatPlugin/AskChatGPT.swift index 7184eb50..f157c21e 100644 --- a/Core/Sources/ChatPlugin/AskChatGPT.swift +++ b/Core/Sources/ChatPlugin/AskChatGPT.swift @@ -2,7 +2,11 @@ import Foundation import OpenAIService /// Quickly ask a question to ChatGPT. -func askChatGPT(systemPrompt: String, question: String) async throws -> String? { - let service = ChatGPTService(systemPrompt: systemPrompt) +public func askChatGPT( + systemPrompt: String, + question: String, + temperature: Double? = nil +) async throws -> String? { + let service = ChatGPTService(systemPrompt: systemPrompt, temperature: temperature) return try await service.sendAndWait(content: question) } diff --git a/Core/Sources/ChatPlugin/Translate.swift b/Core/Sources/ChatPlugin/Translate.swift new file mode 100644 index 00000000..f3441a9f --- /dev/null +++ b/Core/Sources/ChatPlugin/Translate.swift @@ -0,0 +1,34 @@ +import Foundation +import Preferences + +@MainActor +var translationCache = [String: String]() + +public func translate(text: String, cache: Bool = true) async -> String { + let language = UserDefaults.shared.value(for: \.chatGPTLanguage) + if language.isEmpty { return text } + + let key = "\(language)-\(text)" + if cache, let cached = await translationCache[key] { + return cached + } + + if let translated = try? await askChatGPT( + systemPrompt: """ + You are a translator. Your job is to translate the message into \(language). The reply should only contain the translated content. + User: ###${{some text}}### + Assistant: ${{translated text}} + """, + question: "###\(text)###" + ) { + if cache { + let storeTask = Task { @MainActor in + translationCache[key] = translated + } + _ = await storeTask.result + } + return translated + } + return text +} + diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift index c920fa86..17b5824d 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift @@ -22,7 +22,9 @@ public actor MathChatPlugin: ChatPlugin { delegate?.pluginDidStartResponding(self) let id = "\(Self.command)-\(UUID().uuidString)" - var reply = ChatMessage(id: id, role: .assistant, content: "Calculating...") + async let translatedCalculating = translate(text: "Calculating...") + async let translatedAnswer = translate(text: "Answer:") + var reply = ChatMessage(id: id, role: .assistant, content: await translatedCalculating) await chatGPTService.mutateHistory { history in history.append(.init(role: .user, content: originalMessage, summary: content)) @@ -31,11 +33,12 @@ public actor MathChatPlugin: ChatPlugin { do { let result = try await solveMathProblem(content) + let formattedResult = "\(await translatedAnswer) \(result)" await chatGPTService.mutateHistory { history in if history.last?.id == id { history.removeLast() } - reply.content = result + reply.content = formattedResult history.append(reply) } } catch { diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift index af25453d..b3a3c994 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/SolveMathProblem.swift @@ -1,36 +1,100 @@ +import ChatPlugin import Foundation import LangChain -import PythonHelper -import PythonKit - -func solveMathProblem(_ problem: String) async throws -> String { -// #if DEBUG -// let verbose = true -// #else -// let verbose = false -// #endif -// -// struct E: Error, LocalizedError { -// var errorDescription: String? { -// "Failed to parse answer." -// } -// } -// -// let task = Task { -// try runPython { -// let langchain = try Python.attemptImportOnPythonThread("langchain") -// let LLMMathChain = langchain.LLMMathChain -// let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0) -// let llmMath = LLMMathChain.from_llm(llm, verbose: verbose) -// let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem) -// let answer = String(result) -// if let answer { return answer } -// -// throw E() -// } -// } -// -// return try await task.value - return "N/A" +import Logger +import OpenAIService + +let systemPrompt = """ +Translate a math problem into a expression that can be executed using Python's numexpr library. +Use the output of running this code to answer the question. + +Question: ${{Question with math problem.}} +```text +${{single line mathematical expression that solves the problem}} +``` +...numexpr.evaluate(text)... +```output +${{Output of running the code}} +``` +Answer: ${{Answer}} + +Begin. + +Question: What is 37593 * 67? +```text +37593 * 67 +``` +...numexpr.evaluate("37593 * 67")... +```output +2518731 +``` +Answer: 2518731 + +Question: 37593^(1/5) +```text +37593**(1/5) +``` +...numexpr.evaluate("37593**(1/5)")... +```output +8.222831614237718 +``` +Answer: 8.222831614237718 +""" + +/// Extract the math problem with ChatGPT, and pass it to python to get the result. +/// +/// [llm_math in +/// LangChain](https://github.com/hwchase17/langchain/blob/master/langchain/chains/llm_math/base.py) +/// +/// The logic is basically the same as the LLMMathChain provided in LangChain. +func solveMathProblem(_ question: String) async throws -> String { + guard let reply = try await askChatGPT( + systemPrompt: systemPrompt, + question: "Question: \(question)", + temperature: 0 + ) else { return "No answer." } + + // parse inside text code block + let codeBlockRegex = try NSRegularExpression(pattern: "```text\n(.*?)\n```", options: []) + let codeBlockMatches = codeBlockRegex.matches( + in: reply, + options: [], + range: NSRange(reply.startIndex.. String? { + let mathExpression = NSExpression(format: expression) + let value = mathExpression.expressionValue(with: nil, context: nil) + Logger.service.debug(String(describing: value)) + return (value as? Int).flatMap(String.init) } diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift index 32d38b37..6bbd37f7 100644 --- a/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -127,7 +127,7 @@ public class GitHubCopilotBaseService { }() } let localServer = CopilotLocalProcessServer(executionParameters: executionParams) - + localServer.logMessages = UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) localServer.notificationHandler = { _, respond in respond(.timeout) @@ -162,9 +162,9 @@ public class GitHubCopilotBaseService { return (server, localServer) }() - + self.server = server - self.localProcessServer = localServer + localProcessServer = localServer } public static func createFoldersIfNeeded() throws -> ( @@ -242,6 +242,12 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService, } } +@globalActor public enum GitHubCopilotSuggestionActor { + public actor TheActor {} + public static let shared = TheActor() +} + +@GitHubCopilotSuggestionActor public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, GitHubCopilotSuggestionServiceType { @@ -313,7 +319,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, return try await task.value } - + public func cancelRequest() async { await localProcessServer?.cancelOngoingTasks() } @@ -380,7 +386,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, // Logger.service.debug("Close \(uri)") try await server.sendNotification(.didCloseTextDocument(.init(uri: uri))) } - + public func terminate() async { // automatically handled } From 13f4cbdbff462798f079de568418b8be1d5f4b66 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 15:40:25 +0800 Subject: [PATCH 30/49] LangChain in Swift and a working version of the search plugin --- .../xcshareddata/swiftpm/Package.resolved | 27 +++ Core/Package.swift | 73 +++----- .../MathChatPlugin/MathChatPlugin.swift | 24 +-- .../SearchChatPlugin/SearchAgent.swift | 9 - .../SearchChatPlugin/SearchChatPlugin.swift | 52 +++++- .../SearchChatPlugin/SearchQuery.swift | 131 ++++++++++++++ Tool/Package.swift | 29 ++- .../BingSearchService/BingSearchService.swift | 60 +++++++ Tool/Sources/LangChain/Agent.swift | 134 ++++++++++++++ Tool/Sources/LangChain/AgentExecutor.swift | 165 ++++++++++++++++++ Tool/Sources/LangChain/AgentTool.swift | 32 ++++ Tool/Sources/LangChain/Agents/ChatAgent.swift | 164 +++++++++++++++++ Tool/Sources/LangChain/Chain.swift | 123 +++++++++++++ Tool/Sources/LangChain/Chains/LLMChain.swift | 37 ++++ .../LangChain/ChatModel/ChatModel.swift | 25 +++ .../LangChain/ChatModel/OpenAIChat.swift | 50 ++++++ .../LangChain/LangChainChatModel.swift | 41 ----- {Core => Tool}/Sources/Logger/Logger.swift | 1 + .../OpenAIService/ChatGPTService.swift | 43 +++-- .../Sources/OpenAIService/CompletionAPI.swift | 0 .../OpenAIService/CompletionStreamAPI.swift | 0 .../Sources/OpenAIService/Models.swift | 0 .../Tests/LangChainTests/ChatAgentTests.swift | 109 ++++++++++++ Tool/Tests/LangChainTests/ToolTests.swift | 8 - .../ChatGPTServiceFieldTests.swift | 0 .../ChatGPTServiceTests.swift | 0 .../LimitMessagesTests.swift | 0 27 files changed, 1200 insertions(+), 137 deletions(-) delete mode 100644 Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift create mode 100644 Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift create mode 100644 Tool/Sources/BingSearchService/BingSearchService.swift create mode 100644 Tool/Sources/LangChain/Agent.swift create mode 100644 Tool/Sources/LangChain/AgentExecutor.swift create mode 100644 Tool/Sources/LangChain/AgentTool.swift create mode 100644 Tool/Sources/LangChain/Agents/ChatAgent.swift create mode 100644 Tool/Sources/LangChain/Chain.swift create mode 100644 Tool/Sources/LangChain/Chains/LLMChain.swift create mode 100644 Tool/Sources/LangChain/ChatModel/ChatModel.swift create mode 100644 Tool/Sources/LangChain/ChatModel/OpenAIChat.swift delete mode 100644 Tool/Sources/LangChain/LangChainChatModel.swift rename {Core => Tool}/Sources/Logger/Logger.swift (96%) rename {Core => Tool}/Sources/OpenAIService/ChatGPTService.swift (92%) rename {Core => Tool}/Sources/OpenAIService/CompletionAPI.swift (100%) rename {Core => Tool}/Sources/OpenAIService/CompletionStreamAPI.swift (100%) rename {Core => Tool}/Sources/OpenAIService/Models.swift (100%) create mode 100644 Tool/Tests/LangChainTests/ChatAgentTests.swift delete mode 100644 Tool/Tests/LangChainTests/ToolTests.swift rename {Core => Tool}/Tests/OpenAIServiceTests/ChatGPTServiceFieldTests.swift (100%) rename {Core => Tool}/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift (100%) rename {Core => Tool}/Tests/OpenAIServiceTests/LimitMessagesTests.swift (100%) diff --git a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dbf8e6c2..8afaa8b0 100644 --- a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -135,6 +135,15 @@ "version" : "0.1.0" } }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", + "version" : "0.14.1" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -152,6 +161,24 @@ "revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9", "version" : "2.1.0" } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70", + "version" : "0.12.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", + "version" : "0.8.5" + } } ], "version" : 2 diff --git a/Core/Package.swift b/Core/Package.swift index f832f950..d8185e4d 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -15,7 +15,6 @@ let package = Package( "FileChangeChecker", "LaunchAgentManager", "UpdateChecker", - "Logger", "UserDefaultsObserver", "XcodeInspector", ] @@ -26,7 +25,6 @@ let package = Package( "SuggestionModel", "Client", "XPCShared", - "Logger", ] ), .library( @@ -38,9 +36,7 @@ let package = Package( "Client", "XPCShared", "LaunchAgentManager", - "Logger", "UpdateChecker", - "OpenAIService", ] ), ], @@ -52,7 +48,6 @@ let package = Package( .package(url: "https://github.com/JohnSundell/Splash", branch: "master"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.1.0"), .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), - .package(url: "https://github.com/alfianlosari/GPTEncoder", from: "1.0.4"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), ], @@ -64,8 +59,8 @@ let package = Package( dependencies: [ "SuggestionModel", "XPCShared", - "Logger", "GitHubCopilotService", + .product(name: "Logger", package: "Tool"), .product(name: "Preferences", package: "Tool"), ] ), @@ -75,7 +70,6 @@ let package = Package( "SuggestionModel", "SuggestionService", "GitHubCopilotService", - "OpenAIService", "XPCShared", "CGEventObserver", "DisplayLink", @@ -84,11 +78,12 @@ let package = Package( "Environment", "SuggestionWidget", "AXExtension", - "Logger", "ChatService", + .product(name: "Logger", package: "Tool"), "PromptToCodeService", "ServiceUpdateMigration", "UserDefaultsObserver", + .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "PythonKit", package: "PythonKit"), @@ -125,6 +120,7 @@ let package = Package( "CodeiumService", "SuggestionModel", "LaunchAgentManager", + .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), ] ), @@ -164,8 +160,12 @@ let package = Package( .target( name: "PromptToCodeService", - dependencies: ["OpenAIService", "Environment", "GitHubCopilotService", - "SuggestionModel"] + dependencies: [ + "Environment", + "GitHubCopilotService", + "SuggestionModel", + .product(name: "OpenAIService", package: "Tool"), + ] ), .testTarget(name: "PromptToCodeServiceTests", dependencies: ["PromptToCodeService"]), @@ -176,7 +176,6 @@ let package = Package( dependencies: [ "ChatPlugin", "ChatContextCollector", - "OpenAIService", "Environment", "XcodeInspector", @@ -184,14 +183,15 @@ let package = Package( "MathChatPlugin", "SearchChatPlugin", + .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), ] ), .target( name: "ChatPlugin", dependencies: [ - "OpenAIService", "Environment", + .product(name: "OpenAIService", package: "Tool"), .product(name: "Terminal", package: "Tool"), .product(name: "PythonKit", package: "PythonKit"), ] @@ -199,10 +199,10 @@ let package = Package( .target( name: "ChatContextCollector", dependencies: [ - "OpenAIService", "Environment", "SuggestionModel", "XcodeInspector", + .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), ] ), @@ -218,8 +218,8 @@ let package = Package( "Highlightr", "Splash", "UserDefaultsObserver", - "Logger", "XcodeInspector", + .product(name: "Logger", package: "Tool"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "MarkdownUI", package: "swift-markdown-ui"), ] @@ -229,7 +229,6 @@ let package = Package( // MARK: - Helpers .target(name: "CGEventObserver"), - .target(name: "Logger"), .target(name: "FileChangeChecker"), .target(name: "LaunchAgentManager"), .target(name: "DisplayLink"), @@ -238,8 +237,8 @@ let package = Package( .target( name: "UpdateChecker", dependencies: [ - "Logger", "Sparkle", + .product(name: "Logger", package: "Tool"), ] ), .target(name: "AXExtension"), @@ -256,8 +255,8 @@ let package = Package( dependencies: [ "AXExtension", "Environment", - "Logger", "AXNotificationStream", + .product(name: "Logger", package: "Tool"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), ] ), @@ -279,22 +278,6 @@ let package = Package( dependencies: ["GitHubCopilotService"] ), - // MARK: - OpenAI - - .target( - name: "OpenAIService", - dependencies: [ - "Logger", - "GPTEncoder", - .product(name: "Preferences", package: "Tool"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - ] - ), - .testTarget( - name: "OpenAIServiceTests", - dependencies: ["OpenAIService"] - ), - // MARK: - Codeium .target( @@ -314,23 +297,23 @@ let package = Package( name: "MathChatPlugin", dependencies: [ "ChatPlugin", - "OpenAIService", + .product(name: "OpenAIService", package: "Tool"), .product(name: "LangChain", package: "Tool"), .product(name: "PythonKit", package: "PythonKit"), ], path: "Sources/ChatPlugins/MathChatPlugin" ), - - .target( - name: "SearchChatPlugin", - dependencies: [ - "ChatPlugin", - "OpenAIService", - .product(name: "LangChain", package: "Tool"), - .product(name: "PythonKit", package: "PythonKit"), - ], - path: "Sources/ChatPlugins/SearchChatPlugin" - ), + + .target( + name: "SearchChatPlugin", + dependencies: [ + "ChatPlugin", + .product(name: "OpenAIService", package: "Tool"), + .product(name: "LangChain", package: "Tool"), + .product(name: "PythonKit", package: "PythonKit"), + ], + path: "Sources/ChatPlugins/SearchChatPlugin" + ), ] ) diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift index 17b5824d..319f641d 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift @@ -34,20 +34,24 @@ public actor MathChatPlugin: ChatPlugin { do { let result = try await solveMathProblem(content) let formattedResult = "\(await translatedAnswer) \(result)" - await chatGPTService.mutateHistory { history in - if history.last?.id == id { - history.removeLast() + if !isCancelled { + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + reply.content = formattedResult + history.append(reply) } - reply.content = formattedResult - history.append(reply) } } catch { - await chatGPTService.mutateHistory { history in - if history.last?.id == id { - history.removeLast() + if !isCancelled { + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + reply.content = error.localizedDescription + history.append(reply) } - reply.content = error.localizedDescription - history.append(reply) } } diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift deleted file mode 100644 index fc5402f1..00000000 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchAgent.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import LangChain -import PythonHelper -import PythonKit - -func search(_ query: String) async throws -> String { - return "" -} - diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift index 92a5989a..1f30f3e0 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift @@ -21,22 +21,58 @@ public actor SearchChatPlugin: ChatPlugin { delegate?.pluginDidStartResponding(self) let id = "\(Self.command)-\(UUID().uuidString)" - var reply = ChatMessage(id: id, role: .assistant, content: "Calculating...") + var reply = ChatMessage(id: id, role: .assistant, content: "") await chatGPTService.mutateHistory { history in history.append(.init(role: .user, content: originalMessage, summary: content)) - history.append(reply) } do { - let result = try await search(content) - await chatGPTService.mutateHistory { history in - if history.last?.id == id { - history.removeLast() + let eventStream = try await search(content) + + var actions = [String]() + var finishedActions = Set() + var message = "" + + for try await event in eventStream { + guard !isCancelled else { return } + switch event { + case let .startAction(content): + actions.append(content) + case let .endAction(content): + finishedActions.insert(content) + case let .answerToken(token): + message.append(token) + case let .finishAnswer(answer, links): + message = """ + \(answer) + + \(links.map { "- [\($0.title)](\($0.link))" }.joined(separator: "\n")) + """ + } + + await chatGPTService.mutateHistory { history in + if history.last?.id == id { + history.removeLast() + } + + let actionString = actions.map { + "> \(finishedActions.contains($0) ? "✅" : "🔍") \($0)" + }.joined(separator: "\n>\n") + + if message.isEmpty { + reply.content = actionString + } else { + reply.content = """ + \(actionString) + + \(message) + """ + } + history.append(reply) } - reply.content = result - history.append(reply) } + } catch { await chatGPTService.mutateHistory { history in if history.last?.id == id { diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift new file mode 100644 index 00000000..8e07c920 --- /dev/null +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift @@ -0,0 +1,131 @@ +import BingSearchService +import Foundation +import LangChain +import PythonHelper +import PythonKit + +enum SearchEvent { + case startAction(String) + case endAction(String) + case answerToken(String) + case finishAnswer(String, [(title: String, link: String)]) +} + +func search(_ query: String) async throws -> AsyncThrowingStream { + let bingSearch = BingSearchService( + subscriptionKey: "", + searchURL: "" + ) + + final class LinkStorage { + var links = [(title: String, link: String)]() + } + + let linkStorage = LinkStorage() + + let tools = [ + SimpleAgentTool( + name: "Search", + description: "useful for when you need to answer questions about current events. Don't search for the same thing twice", + run: { + linkStorage.links = [] + let result = try await bingSearch.search(query: $0, numberOfResult: 5) + guard let websites = result.webPages?.value else { return "No search results." } + + var string = "" + for (index, website) in websites.enumerated() { + string.append("[\(index)]:###\(website.snippet)###\n") + linkStorage.links.append((website.name, website.url)) + } + return string + } + ), + ] + + let chatModel = OpenAIChat(temperature: 0, stream: true) + + let agentExecutor = AgentExecutor( + agent: ChatAgent(chatModel: chatModel, tools: tools), + tools: tools, + maxIteration: 2, + earlyStopHandleType: .generate + ) + + class ResultCallbackManager: ChainCallbackManager { + var accumulation: String = "" + var isGeneratingFinalAnswer = false + var onFinalAnswerToken: (String) -> Void + var onAgentActionStart: (String) -> Void + var onAgentActionEnd: (String) -> Void + + init( + onFinalAnswerToken: @escaping (String) -> Void, + onAgentActionStart: @escaping (String) -> Void, + onAgentActionEnd: @escaping (String) -> Void + ) { + self.onFinalAnswerToken = onFinalAnswerToken + self.onAgentActionStart = onAgentActionStart + self.onAgentActionEnd = onAgentActionEnd + } + + func onChainStart(type: T.Type, input: T.Input) where T: LangChain.Chain { + print("Chain \(type) is started with input \(input).") + } + + func onAgentFinish(output: LangChain.AgentFinish) { + print("Agent is finished: \(output.returnValue)") + } + + func onAgentActionStart(action: LangChain.AgentAction) { + print("Agent runs action: \(action.toolName) with input \(action.toolInput)") + onAgentActionStart("\(action.toolName): \(action.toolInput)") + } + + func onAgentActionEnd(action: LangChain.AgentAction) { + print( + """ + Agent finish running action: \ + \(action.toolName) with observation \ + \(action.observation ?? "") + """ + ) + onAgentActionEnd("\(action.toolName): \(action.toolInput)") + } + + func onLLMNewToken(token: String) { + if isGeneratingFinalAnswer { + onFinalAnswerToken(token) + return + } + accumulation.append(token) + if accumulation.hasSuffix("Final Answer: ") { + isGeneratingFinalAnswer = true + accumulation = "" + } + } + } + + return AsyncThrowingStream { continuation in + let callback = ResultCallbackManager( + onFinalAnswerToken: { + continuation.yield(.answerToken($0)) + }, + onAgentActionStart: { + continuation.yield(.startAction($0)) + }, + onAgentActionEnd: { + continuation.yield(.endAction($0)) + } + ) + Task { + do { + let finalAnswer = try await agentExecutor.run(query, callbackManagers: [callback]) + continuation.yield(.finishAnswer(finalAnswer, linkStorage.links)) + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } +} + diff --git a/Tool/Package.swift b/Tool/Package.swift index f13d471d..1088767f 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -8,11 +8,16 @@ let package = Package( platforms: [.macOS(.v12)], products: [ .library(name: "Terminal", targets: ["Terminal"]), - .library(name: "LangChain", targets: ["LangChain", "PythonHelper"]), + .library(name: "LangChain", targets: ["LangChain", "PythonHelper", "BingSearchService"]), .library(name: "Preferences", targets: ["Preferences", "Configs"]), + .library(name: "Logger", targets: ["Logger"]), + .library(name: "OpenAIService", targets: ["OpenAIService"]), ], dependencies: [ .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), + .package(url: "https://github.com/alfianlosari/GPTEncoder", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1") ], targets: [ // MARK: - Helpers @@ -23,16 +28,22 @@ let package = Package( .target(name: "Terminal"), + .target(name: "Logger"), + // MARK: - Services .target( name: "LangChain", dependencies: [ "PythonHelper", + "OpenAIService", + .product(name: "Parsing", package: "swift-parsing"), .product(name: "PythonKit", package: "PythonKit"), ] ), + .target(name: "BingSearchService"), + .target( name: "PythonHelper", dependencies: [ @@ -40,6 +51,22 @@ let package = Package( ] ), + // MARK: - OpenAI + + .target( + name: "OpenAIService", + dependencies: [ + "GPTEncoder", + "Logger", + "Preferences", + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), + ] + ), + .testTarget( + name: "OpenAIServiceTests", + dependencies: ["OpenAIService"] + ), + // MARK: - Tests .testTarget( diff --git a/Tool/Sources/BingSearchService/BingSearchService.swift b/Tool/Sources/BingSearchService/BingSearchService.swift new file mode 100644 index 00000000..4872b5fa --- /dev/null +++ b/Tool/Sources/BingSearchService/BingSearchService.swift @@ -0,0 +1,60 @@ +import Foundation + +public struct BingSearchResult: Codable { + public var webPages: WebPages? + + public struct WebPages: Codable { + public var webSearchUrl: String + public var totalEstimatedMatches: Int + public var value: [WebPageValue] + + public struct WebPageValue: Codable { + public var id: String + public var name: String + public var url: String + public var displayUrl: String + public var snippet: String + } + } +} + +enum BingSearchError: Error, LocalizedError { + case searchURLFormatIncorrect(String) + + var errorDescription: String? { + switch self { + case let .searchURLFormatIncorrect(url): + return "The search URL format is incorrect: \(url)" + } + } +} + +public struct BingSearchService { + public var subscriptionKey: String + public var searchURL: String + + public init(subscriptionKey: String, searchURL: String) { + self.subscriptionKey = subscriptionKey + self.searchURL = searchURL + } + + public func search(query: String, numberOfResult: Int) async throws -> BingSearchResult { + guard let url = URL(string: searchURL) + else { throw BingSearchError.searchURLFormatIncorrect(searchURL) } + + var components = URLComponents(url: url, resolvingAgainstBaseURL: true) + components?.queryItems = [ + .init(name: "q", value: query), + .init(name: "count", value: String(numberOfResult)), + ] + var request = URLRequest(url: components?.url ?? url) + request.httpMethod = "GET" + request.addValue(subscriptionKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key") + + let (data, _) = try await URLSession.shared.data(for: request) + let decoder = JSONDecoder() + let result = try decoder.decode(BingSearchResult.self, from: data) + return result + } +} + diff --git a/Tool/Sources/LangChain/Agent.swift b/Tool/Sources/LangChain/Agent.swift new file mode 100644 index 00000000..559c80fc --- /dev/null +++ b/Tool/Sources/LangChain/Agent.swift @@ -0,0 +1,134 @@ +import Foundation + +public struct AgentAction: Equatable { + public var toolName: String + public var toolInput: String + public var log: String + public var observation: String? + + public init(toolName: String, toolInput: String, log: String, observation: String? = nil) { + self.toolName = toolName + self.toolInput = toolInput + self.log = log + self.observation = observation + } + + public func observationAvailable(_ observation: String) -> AgentAction { + var newAction = self + newAction.observation = observation + return newAction + } +} + +public struct AgentFinish: Equatable { + public var returnValue: String + public var log: String + + public init(returnValue: String, log: String) { + self.returnValue = returnValue + self.log = log + } +} + +public enum AgentNextStep: Equatable { + case actions([AgentAction]) + case finish(AgentFinish) +} + +public enum AgentScratchPad: Equatable { + case text(String) + case messages([String]) + + var isEmpty: Bool { + switch self { + case .text(let text): + return text.isEmpty + case .messages(let messages): + return messages.isEmpty + } + } +} + +public struct AgentInput { + var input: T + var thoughts: AgentScratchPad + + public init(input: T, thoughts: AgentScratchPad) { + self.input = input + self.thoughts = thoughts + } +} + +extension AgentInput: Equatable where T: Equatable {} + +public enum AgentEarlyStopHandleType: Equatable { + case force + case generate +} + +public protocol Agent { + associatedtype Input + var chatModelChain: ChatModelChain> { get } + var observationPrefix: String { get } + var llmPrefix: String { get } + + func validateTools(tools: [AgentTool]) throws + func constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad + func parseOutput(_ output: String) -> AgentNextStep +} + +public extension Agent { + func getFullInputs(input: Input, intermediateSteps: [AgentAction]) -> AgentInput { + let thoughts = constructScratchpad(intermediateSteps: intermediateSteps) + return AgentInput(input: input, thoughts: thoughts) + } + + func plan( + input: Input, + intermediateSteps: [AgentAction], + callbackManagers: [ChainCallbackManager] + ) async throws -> AgentNextStep { + let input = getFullInputs(input: input, intermediateSteps: intermediateSteps) + let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) + return parseOutput(output) + } + + func returnStoppedResponse( + input: Input, + earlyStoppedHandleType: AgentEarlyStopHandleType, + intermediateSteps: [AgentAction], + callbackManagers: [ChainCallbackManager] + ) async throws -> AgentFinish { + switch earlyStoppedHandleType { + case .force: + return AgentFinish( + returnValue: "Agent stopped due to iteration limit or time limit.", + log: "" + ) + case .generate: + var thoughts = constructBaseScratchpad(intermediateSteps: intermediateSteps) + thoughts += "\n\n\(llmPrefix)I now need to return a final answer based on the previous steps:" + let input = AgentInput(input: input, thoughts: .text(thoughts)) + let output = try await chatModelChain.call(input, callbackManagers: callbackManagers) + let nextAction = parseOutput(output) + switch nextAction { + case let .finish(finish): + return finish + case .actions: + return AgentFinish(returnValue: output, log: output) + } + } + } + + 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/AgentExecutor.swift b/Tool/Sources/LangChain/AgentExecutor.swift new file mode 100644 index 00000000..c1cb9255 --- /dev/null +++ b/Tool/Sources/LangChain/AgentExecutor.swift @@ -0,0 +1,165 @@ +import Foundation + +public protocol ChainCallbackManager { + func onChainStart(type: T.Type, input: T.Input) + func onAgentFinish(output: AgentFinish) + func onAgentActionStart(action: AgentAction) + func onAgentActionEnd(action: AgentAction) + func onLLMNewToken(token: String) +} + +public actor AgentExecutor: Chain where InnerAgent.Input == String { + public typealias Input = String + public struct Output { + let finalOutput: String + let intermediateSteps: [AgentAction] + } + + let agent: InnerAgent + let tools: [String: AgentTool] + let maxIteration: Int? + let maxExecutionTime: Double? + let earlyStopHandleType: AgentEarlyStopHandleType + var now: () -> Date = { Date() } + + public init( + agent: InnerAgent, + tools: [AgentTool], + maxIteration: Int? = 10, + maxExecutionTime: Double? = nil, + earlyStopHandleType: AgentEarlyStopHandleType = .force + ) { + self.agent = agent + self.tools = tools.reduce(into: [:]) { $0[$1.name] = $1 } + self.maxIteration = maxIteration + self.maxExecutionTime = maxExecutionTime + self.earlyStopHandleType = earlyStopHandleType + } + + public func callLogic( + _ input: Input, + callbackManagers: [ChainCallbackManager] + ) async throws -> Output { + try agent.validateTools(tools: Array(tools.values)) + + let startTime = now().timeIntervalSince1970 + var iterations = 0 + var intermediateSteps: [AgentAction] = [] + + func shouldContinue() -> Bool { + if let maxIteration = maxIteration, iterations >= maxIteration { + return false + } + if let maxExecutionTime = maxExecutionTime, + now().timeIntervalSince1970 - startTime > maxExecutionTime + { + return false + } + return true + } + + while shouldContinue() { + let nextStepOutput = try await takeNextStep( + input: input, + intermediateSteps: intermediateSteps, + callbackManagers: callbackManagers + ) + + switch nextStepOutput { + case let .finish(finish): + return end( + output: finish, + intermediateSteps: intermediateSteps, + callbackManagers: callbackManagers + ) + case let .actions(actions): + intermediateSteps.append(contentsOf: actions) + if actions.count == 1, + let action = actions.first, + let toolFinish = getToolFinish(action: action) + { + return end( + output: toolFinish, + intermediateSteps: intermediateSteps, + callbackManagers: callbackManagers + ) + } + } + iterations += 1 + } + + let output = try await agent.returnStoppedResponse( + input: input, + earlyStoppedHandleType: earlyStopHandleType, + intermediateSteps: intermediateSteps, + callbackManagers: callbackManagers + ) + return end( + output: output, + intermediateSteps: intermediateSteps, + callbackManagers: callbackManagers + ) + } + + public nonisolated func parseOutput(_ output: Output) -> String { + output.finalOutput + } +} + +struct InvalidToolError: Error {} + +extension AgentExecutor { + func end( + output: AgentFinish, + intermediateSteps: [AgentAction], + callbackManagers: [ChainCallbackManager] + ) -> Output { + for callbackManager in callbackManagers { + callbackManager.onAgentFinish(output: output) + } + let finalOutput = output.returnValue + return .init(finalOutput: finalOutput, intermediateSteps: intermediateSteps) + } + + func takeNextStep( + input: Input, + intermediateSteps: [AgentAction], + callbackManagers: [ChainCallbackManager] + ) async throws -> AgentNextStep { + let output = try await agent.plan( + input: input, + intermediateSteps: intermediateSteps, + callbackManagers: callbackManagers + ) + switch output { + case .finish: return output + case let .actions(actions): + let completedActions = try await withThrowingTaskGroup(of: AgentAction.self) { + taskGroup in + for action in actions { + callbackManagers.forEach { $0.onAgentActionStart(action: action) } + guard let tool = tools[action.toolName] else { throw InvalidToolError() } + taskGroup.addTask { + let observation = try await tool.run(input: action.toolInput) + return action.observationAvailable(observation) + } + } + var completedActions = [AgentAction]() + for try await action in taskGroup { + completedActions.append(action) + callbackManagers.forEach { $0.onAgentActionEnd(action: action) } + } + return completedActions + } + + return .actions(completedActions) + } + } + + 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: "") + } +} + diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift new file mode 100644 index 00000000..170cd9ca --- /dev/null +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -0,0 +1,32 @@ +import Foundation + +public protocol AgentTool { + var name: String { get } + var description: String { get } + var returnDirectly: Bool { get } + func run(input: String) async throws -> String +} + +public struct SimpleAgentTool: AgentTool { + public let name: String + public let description: String + public let returnDirectly: Bool + public let run: (String) async throws -> String + + public init( + name: String, + description: String, + returnDirectly: Bool = false, + run: @escaping (String) async throws -> String + ) { + self.name = name + self.description = description + self.returnDirectly = returnDirectly + self.run = run + } + + public func run(input: String) async throws -> String { + try await run(input) + } +} + diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift new file mode 100644 index 00000000..df65a992 --- /dev/null +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -0,0 +1,164 @@ +import Foundation +import Logger +import Parsing + +private func formatInstruction(toolsNames: String) -> String { + """ + The way you use the tools is by specifying a json blob. + Specifically, this json should have a `action` key (with the name of the tool to use) and a `action_input` key (with the input to the tool going here). + + The only values that should be in the "action" field are: \(toolsNames) + + The $JSON_BLOB should only contain a SINGLE action, do NOT return a list of multiple actions. Here is an example of a valid $JSON_BLOB: + + ``` + { + "action": $TOOL_NAME, + "action_input": $INPUT + } + ``` + + ALWAYS use the following format: + + Question: the input question you must answer + Thought: you should always think about what to do + Action: + ``` + $JSON_BLOB + ``` + Observation: the result of the action + ... (this Thought/Action/Observation can repeat N times) + Thought: I now know the final answer + Final Answer: the final answer to the original input question + """ +} + +public class ChatAgent: Agent { + public typealias Input = String + public var observationPrefix: String { "Observation: " } + public var llmPrefix: String { "Thought: " } + public let chatModelChain: ChatModelChain> + let tools: [AgentTool] + + public init(chatModel: ChatModel, tools: [AgentTool]) { + self.tools = tools + chatModelChain = .init( + chatModel: chatModel, + stops: ["Observation:"], + promptTemplate: { agentInput in + [ + .init( + role: .system, + content: """ + Respond to the human as helpfully and accurately as possible. \ + Wrap any code block in thought in . \ + Format final answer to be more readable, in a ordered list if possible. \ + You have access to the following tools: + + \(tools.map { "\($0.name): \($0.description)" }.joined(separator: "\n")) + + \(formatInstruction(toolsNames: tools.map(\.name).joined(separator: ","))) + + Begin! Reminder to always use the exact characters `Final Answer` when responding. + """ + ), + 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 constructScratchpad(intermediateSteps: [AgentAction]) -> AgentScratchPad { + let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) + if baseScratchpad.isEmpty { return .text("") } + return .text(""" + \(baseScratchpad) + (Continue with your `Thought:`) + """) + } + + public func validateTools(tools: [AgentTool]) throws { + // no validation + } + + public func parseOutput(_ text: String) -> 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)) + } catch { + Logger.langchain.error("Could not parse LLM output final answer: \(error)") + return nil + } + } + + func parseNextActionIfPossible() -> AgentNextStep? { + let throughActionBlockParser = PrefixThrough(""" + Action: + ``` + """) + let throughActionBlockSimplifiedParser = PrefixThrough("```") + let jsonBlobParser = PrefixUpTo("```") + var parsableContent = text[...] + do { + let actionBlockPrefix = try? throughActionBlockParser.parse(&parsableContent) + if actionBlockPrefix == nil { + _ = try throughActionBlockSimplifiedParser.parse(&parsableContent) + } + let jsonBlob = try jsonBlobParser.parse(&parsableContent) + + struct Action: Codable { + let action: String + let action_input: String + } + let response = try JSONDecoder() + .decode(Action.self, from: jsonBlob.data(using: .utf8) ?? Data()) + return .actions([ + AgentAction( + toolName: response.action, + toolInput: response.action_input, + log: text + ), + ]) + } catch { + Logger.langchain.error("Could not parse LLM output next action: \(error)") + return nil + } + } + + if let step = parseFinalAnswerIfPossible() { return step } + if let step = parseNextActionIfPossible() { return step } + + let forceParser = PrefixUpTo("Action:") + var parsableContent = text[...] + let finalAnswer = try? forceParser.parse(&parsableContent) + .trimmingCharacters(in: .whitespacesAndNewlines) + + return .finish(AgentFinish(returnValue: String(finalAnswer ?? text), log: text)) + } +} + diff --git a/Tool/Sources/LangChain/Chain.swift b/Tool/Sources/LangChain/Chain.swift new file mode 100644 index 00000000..07decf8f --- /dev/null +++ b/Tool/Sources/LangChain/Chain.swift @@ -0,0 +1,123 @@ +import Foundation + +public protocol Chain { + associatedtype Input + associatedtype Output + func callLogic(_ input: Input, callbackManagers: [ChainCallbackManager]) async throws -> Output + func parseOutput(_ output: Output) -> String +} + +public extension Chain { + func run(_ input: Input, callbackManagers: [ChainCallbackManager] = []) async throws -> String { + let output = try await call(input, callbackManagers: callbackManagers) + return parseOutput(output) + } + + func call(_ input: Input, callbackManagers: [ChainCallbackManager]) async throws -> Output { + for callbackManager in callbackManagers { + callbackManager.onChainStart(type: Self.self, input: input) + } + return try await callLogic(input, callbackManagers: callbackManagers) + } +} + +public struct SimpleChain: Chain { + let block: (Input) async throws -> Output + let parseOutputBlock: (Output) -> String + + public init( + block: @escaping (Input) async throws -> Output, + parseOutput: @escaping (Output) -> String = { String(describing: $0) } + ) { + self.block = block + parseOutputBlock = parseOutput + } + + public func callLogic( + _ input: Input, + callbackManagers: [ChainCallbackManager] + ) async throws -> Output { + return try await block(input) + } + + public func parseOutput(_ output: Output) -> String { + return parseOutputBlock(output) + } +} + +public struct ConnectedChain: Chain where B.Input == A.Output { + public typealias Input = A.Input + public typealias Output = (B.Output, A.Output) + + public let chainA: A + public let chainB: B + + public func callLogic( + _ input: Input, + callbackManagers: [ChainCallbackManager] = [] + ) async throws -> Output { + let a = try await chainA.call(input, callbackManagers: callbackManagers) + let b = try await chainB.call(a, callbackManagers: callbackManagers) + return (b, a) + } + + public func parseOutput(_ output: Output) -> String { + chainB.parseOutput(output.0) + } +} + +public struct PairedChain: Chain { + public typealias Input = (A.Input, B.Input) + public typealias Output = (A.Output, B.Output) + + public let chainA: A + public let chainB: B + + public func callLogic( + _ input: Input, + callbackManagers: [ChainCallbackManager] = [] + ) async throws -> Output { + async let a = chainA.call(input.0, callbackManagers: callbackManagers) + async let b = chainB.call(input.1, callbackManagers: callbackManagers) + return try await (a, b) + } + + public func parseOutput(_ output: (A.Output, B.Output)) -> String { + [chainA.parseOutput(output.0), chainB.parseOutput(output.1)].joined(separator: "\n") + } +} + +public struct MappedChain: Chain { + public typealias Input = A.Input + public typealias Output = NewOutput + + public let chain: A + public let map: (A.Output) -> NewOutput + + public func callLogic( + _ input: Input, + callbackManagers: [ChainCallbackManager] + ) async throws -> Output { + let output = try await chain.call(input, callbackManagers: callbackManagers) + return map(output) + } + + public func parseOutput(_ output: Output) -> String { + String(describing: output) + } +} + +public extension Chain { + func pair(with another: C) -> PairedChain { + PairedChain(chainA: self, chainB: another) + } + + func chain(to another: C) -> ConnectedChain { + ConnectedChain(chainA: self, chainB: another) + } + + func mapped(_ map: @escaping (Output) -> NewOutput) -> MappedChain { + MappedChain(chain: self, map: map) + } +} + diff --git a/Tool/Sources/LangChain/Chains/LLMChain.swift b/Tool/Sources/LangChain/Chains/LLMChain.swift new file mode 100644 index 00000000..bb81f18c --- /dev/null +++ b/Tool/Sources/LangChain/Chains/LLMChain.swift @@ -0,0 +1,37 @@ +import Foundation + +public class ChatModelChain: Chain { + public typealias Output = String + + var chatModel: ChatModel + var promptTemplate: (Input) -> [ChatMessage] + var stops: [String] + + public init( + chatModel: ChatModel, + stops: [String] = [], + promptTemplate: @escaping (Input) -> [ChatMessage] + ) { + self.chatModel = chatModel + self.promptTemplate = promptTemplate + self.stops = stops + } + + public func callLogic( + _ input: Input, + callbackManagers: [ChainCallbackManager] + ) async throws -> Output { + let prompt = promptTemplate(input) + let output = try await chatModel.generate( + prompt: prompt, + stops: stops, + callbackManagers: callbackManagers + ) + return output + } + + public func parseOutput(_ output: Output) -> String { + output + } +} + diff --git a/Tool/Sources/LangChain/ChatModel/ChatModel.swift b/Tool/Sources/LangChain/ChatModel/ChatModel.swift new file mode 100644 index 00000000..283bfcfc --- /dev/null +++ b/Tool/Sources/LangChain/ChatModel/ChatModel.swift @@ -0,0 +1,25 @@ +import Foundation + +public protocol ChatModel { + func generate( + prompt: [ChatMessage], + stops: [String], + callbackManagers: [ChainCallbackManager] + ) async throws -> String +} + +public struct ChatMessage { + public enum Role { + case system + case user + case assistant + } + + public var role: Role + public var content: String + + public init(role: Role, content: String) { + self.role = role + self.content = content + } +} diff --git a/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift b/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift new file mode 100644 index 00000000..c619d093 --- /dev/null +++ b/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift @@ -0,0 +1,50 @@ +import Foundation +import OpenAIService + +public struct OpenAIChat: ChatModel { + public var temperature: Double + public var stream: Bool + + public init( + temperature: Double = 0.7, + stream: Bool = false + ) { + self.temperature = temperature + self.stream = stream + } + + public func generate( + prompt: [ChatMessage], + stops: [String], + callbackManagers: [ChainCallbackManager] + ) async throws -> String { + let service = ChatGPTService(temperature: temperature, stop: stops) + await service.mutateHistory { history in + for message in prompt { + let role: OpenAIService.ChatMessage.Role = { + switch message.role { + case .system: + return .system + case .user: + return .user + case .assistant: + return .assistant + } + }() + history.append(.init(role: role, content: message.content)) + } + } + if stream { + let stream = try await service.send(content: "") + var message = "" + for try await trunk in stream { + message.append(trunk) + callbackManagers.forEach { $0.onLLMNewToken(token: trunk) } + } + return message + } else { + return try await service.sendAndWait(content: "") ?? "" + } + } +} + diff --git a/Tool/Sources/LangChain/LangChainChatModel.swift b/Tool/Sources/LangChain/LangChainChatModel.swift deleted file mode 100644 index 37dbae90..00000000 --- a/Tool/Sources/LangChain/LangChainChatModel.swift +++ /dev/null @@ -1,41 +0,0 @@ -//import Foundation -//import Preferences -//import PythonHelper -//import PythonKit -// -//public enum LangChainChatModel { -// /// Dynamically create a ChatOpenAI object based on the user's preferences. -// public static func DynamicChatOpenAI( -// temperature: Double -// ) throws -> PythonObject { -// switch UserDefaults.shared.value(for: \.chatFeatureProvider) { -// case .openAI: -// let model = UserDefaults.shared.value(for: \.chatGPTModel) -// let apiBaseURL = UserDefaults.shared.value(for: \.openAIBaseURL) -// let apiKey = UserDefaults.shared.value(for: \.openAIAPIKey) -// let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") -// let ChatOpenAI = chatModels.ChatOpenAI -// return ChatOpenAI( -// temperature: temperature, -// model: model, -// openai_api_base: "\(apiBaseURL)/v1", -// openai_api_key: apiKey -// ) -// case .azureOpenAI: -// let apiBaseURL = UserDefaults.shared.value(for: \.azureOpenAIBaseURL) -// let apiKey = UserDefaults.shared.value(for: \.azureOpenAIAPIKey) -// let deployment = UserDefaults.shared.value(for: \.azureChatGPTDeployment) -// let chatModels = try Python.attemptImportOnPythonThread("langchain.chat_models") -// let ChatOpenAI = chatModels.AzureChatOpenAI -// return ChatOpenAI( -// temperature: temperature, -// openai_api_type: "azure", -// openai_api_version: "2023-03-15-preview", -// deployment_name: deployment, -// openai_api_base: apiBaseURL, -// openai_api_key: apiKey -// ) -// } -// } -//} - diff --git a/Core/Sources/Logger/Logger.swift b/Tool/Sources/Logger/Logger.swift similarity index 96% rename from Core/Sources/Logger/Logger.swift rename to Tool/Sources/Logger/Logger.swift index 92344e4d..e78bf3a0 100644 --- a/Core/Sources/Logger/Logger.swift +++ b/Tool/Sources/Logger/Logger.swift @@ -17,6 +17,7 @@ public final class Logger { public static let updateChecker = Logger(category: "UpdateChecker") public static let gitHubCopilot = Logger(category: "GitHubCopilot") public static let codeium = Logger(category: "Codeium") + public static let langchain = Logger(category: "LangChain") #if DEBUG public static let temp = Logger(category: "Temp") #endif diff --git a/Core/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift similarity index 92% rename from Core/Sources/OpenAIService/ChatGPTService.swift rename to Tool/Sources/OpenAIService/ChatGPTService.swift index f970dd2a..909fe3ab 100644 --- a/Core/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -109,6 +109,7 @@ public actor ChatGPTService: ChatGPTServiceType { didSet { objectWillChange.send() } } + var stop: [String] var uuidGenerator: () -> String = { UUID().uuidString } var cancelTask: Cancellable? var buildCompletionStreamAPI: CompletionStreamAPIBuilder = OpenAICompletionStreamAPI.init @@ -117,10 +118,12 @@ public actor ChatGPTService: ChatGPTServiceType { public init( systemPrompt: String = "", temperature: Double? = nil, + stop: [String] = [], designatedProvider: ChatFeatureProvider? = nil ) { self.systemPrompt = systemPrompt self.temperature = temperature + self.stop = stop self.designatedProvider = designatedProvider } @@ -130,13 +133,16 @@ public actor ChatGPTService: ChatGPTServiceType { ) async throws -> AsyncThrowingStream { guard !isReceivingMessage else { throw CancellationError() } guard let url = URL(string: endpoint) else { throw ChatGPTServiceError.endpointIncorrect } - let newMessage = ChatMessage( - id: uuidGenerator(), - role: .user, - content: content, - summary: summary - ) - history.append(newMessage) + + if !content.isEmpty || summary != nil { + let newMessage = ChatMessage( + id: uuidGenerator(), + role: .user, + content: content, + summary: summary + ) + history.append(newMessage) + } let (messages, remainingTokens) = combineHistoryWithSystemPrompt() @@ -145,6 +151,7 @@ public actor ChatGPTService: ChatGPTServiceType { messages: messages, temperature: temperature ?? defaultTemperature, stream: true, + stop: stop.isEmpty ? nil : stop, max_tokens: maxTokenForReply(model: model, remainingTokens: remainingTokens) ) @@ -217,13 +224,16 @@ public actor ChatGPTService: ChatGPTServiceType { ) async throws -> String? { guard !isReceivingMessage else { throw CancellationError() } guard let url = URL(string: endpoint) else { throw ChatGPTServiceError.endpointIncorrect } - let newMessage = ChatMessage( - id: uuidGenerator(), - role: .user, - content: content, - summary: summary - ) - history.append(newMessage) + + if !content.isEmpty || summary != nil { + let newMessage = ChatMessage( + id: uuidGenerator(), + role: .user, + content: content, + summary: summary + ) + history.append(newMessage) + } let (messages, remainingTokens) = combineHistoryWithSystemPrompt() @@ -232,6 +242,7 @@ public actor ChatGPTService: ChatGPTServiceType { messages: messages, temperature: temperature ?? defaultTemperature, stream: true, + stop: stop.isEmpty ? nil : stop, max_tokens: maxTokenForReply(model: model, remainingTokens: remainingTokens) ) @@ -314,7 +325,9 @@ extension ChatGPTService { all.append(.init(role: message.role, content: message.content)) } - all.append(.init(role: .system, content: systemPrompt)) + if !systemPrompt.isEmpty { + all.append(.init(role: .system, content: systemPrompt)) + } return (all.reversed(), max(minimumReplyTokens, maxTokens - allTokensCount)) } } diff --git a/Core/Sources/OpenAIService/CompletionAPI.swift b/Tool/Sources/OpenAIService/CompletionAPI.swift similarity index 100% rename from Core/Sources/OpenAIService/CompletionAPI.swift rename to Tool/Sources/OpenAIService/CompletionAPI.swift diff --git a/Core/Sources/OpenAIService/CompletionStreamAPI.swift b/Tool/Sources/OpenAIService/CompletionStreamAPI.swift similarity index 100% rename from Core/Sources/OpenAIService/CompletionStreamAPI.swift rename to Tool/Sources/OpenAIService/CompletionStreamAPI.swift diff --git a/Core/Sources/OpenAIService/Models.swift b/Tool/Sources/OpenAIService/Models.swift similarity index 100% rename from Core/Sources/OpenAIService/Models.swift rename to Tool/Sources/OpenAIService/Models.swift diff --git a/Tool/Tests/LangChainTests/ChatAgentTests.swift b/Tool/Tests/LangChainTests/ChatAgentTests.swift new file mode 100644 index 00000000..88c8d22e --- /dev/null +++ b/Tool/Tests/LangChainTests/ChatAgentTests.swift @@ -0,0 +1,109 @@ +import XCTest +@testable import LangChain + +private struct FakeChatModel: ChatModel { + func generate( + prompt: [LangChain.ChatMessage], + stops: [String], + callbackManagers: [LangChain.ChainCallbackManager] + ) async throws -> String { + return "New Message" + } +} + +final class ChatAgentParseOutputTests: XCTestCase { + func test_parsing_well_formatted_final_answer() throws { + let finalAnswer = """ + Final Answer: The answer is 42. + Because 42 is the answer to everything. + """ + + let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let result = agent.parseOutput(finalAnswer) + XCTAssertEqual(result, .finish(.init( + returnValue: """ + The answer is 42. + Because 42 is the answer to everything. + """, + log: finalAnswer + ))) + } + + func test_parsing_final_answer_with_random_prefix() throws { + let finalAnswer = """ + Now I have the final answer. + Final Answer: The answer is 42. + Because 42 is the answer to everything. + """ + + let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let result = agent.parseOutput(finalAnswer) + XCTAssertEqual(result, .finish(.init( + returnValue: """ + The answer is 42. + Because 42 is the answer to everything. + """, + log: finalAnswer + ))) + } + + func test_parsing_action() 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. + Action: + ``` + { + "action": "Search", + "action_input": "how to setup langchain python" + } + ``` + """ + + let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let result = agent.parseOutput(reply) + XCTAssertEqual(result, .actions([ + .init( + toolName: "Search", + toolInput: "how to setup langchain python", + log: reply + ) + ])) + } + + func test_parsing_broken_action_and_return_everything_ahead_of_it() { + 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. + Action: + ``` + lkjaskdjalksjdlkasjdklj + ``` + """ + + let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let result = agent.parseOutput(reply) + XCTAssertEqual(result, .finish(.init( + returnValue: """ + 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() { + let reply = """ + The answer is 42. + Because 42 is the answer to everything. + """ + + let agent = ChatAgent(chatModel: FakeChatModel(), tools: []) + let result = agent.parseOutput(reply) + XCTAssertEqual(result, .finish(.init( + returnValue: reply, + log: reply + ))) + } +} + diff --git a/Tool/Tests/LangChainTests/ToolTests.swift b/Tool/Tests/LangChainTests/ToolTests.swift deleted file mode 100644 index e142cbb3..00000000 --- a/Tool/Tests/LangChainTests/ToolTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest -@testable import LangChain - -final class ToolTests: XCTestCase { - func testExample() throws { - - } -} diff --git a/Core/Tests/OpenAIServiceTests/ChatGPTServiceFieldTests.swift b/Tool/Tests/OpenAIServiceTests/ChatGPTServiceFieldTests.swift similarity index 100% rename from Core/Tests/OpenAIServiceTests/ChatGPTServiceFieldTests.swift rename to Tool/Tests/OpenAIServiceTests/ChatGPTServiceFieldTests.swift diff --git a/Core/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift b/Tool/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift similarity index 100% rename from Core/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift rename to Tool/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift diff --git a/Core/Tests/OpenAIServiceTests/LimitMessagesTests.swift b/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift similarity index 100% rename from Core/Tests/OpenAIServiceTests/LimitMessagesTests.swift rename to Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift From 0945ba990274a2def0a967d69b256516befdb3af Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:01:21 +0800 Subject: [PATCH 31/49] Add settings for bing search --- .../SearchChatPlugin/SearchQuery.swift | 8 ++- .../AccountSettings/BingSearchView.swift | 51 +++++++++++++++++++ Core/Sources/HostApp/ServiceView.swift | 9 ++++ .../BingSearchService/BingSearchService.swift | 22 ++++++-- Tool/Sources/Preferences/Keys.swift | 12 +++++ 5 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 Core/Sources/HostApp/AccountSettings/BingSearchView.swift diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift index 8e07c920..4cb8880d 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift @@ -1,8 +1,6 @@ import BingSearchService import Foundation import LangChain -import PythonHelper -import PythonKit enum SearchEvent { case startAction(String) @@ -13,8 +11,8 @@ enum SearchEvent { func search(_ query: String) async throws -> AsyncThrowingStream { let bingSearch = BingSearchService( - subscriptionKey: "", - searchURL: "" + subscriptionKey: UserDefaults.shared.value(for: \.bingSearchSubscriptionKey), + searchURL: UserDefaults.shared.value(for: \.bingSearchEndpoint) ) final class LinkStorage { @@ -30,7 +28,7 @@ func search(_ query: String) async throws -> AsyncThrowingStream { + .init(defaultValue: "", key: "BingSearchSubscriptionKey") + } + + var bingSearchEndpoint: PreferenceKey { + .init(defaultValue: "https://api.bing.microsoft.com/v7.0/search/", key: "BingSearchEndpoint") + } +} + // MARK: - Custom Commands public extension UserDefaultPreferenceKeys { From ab0ccf70d945ec984278a473c2bca74adb8509c7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:05:36 +0800 Subject: [PATCH 32/49] Fix broken tests --- Core/Package.swift | 1 + .../LanguageIdentifierFromFilePath.swift | 7 ++-- .../FetchSuggestionsTests.swift | 4 ++ TestPlan.xctestplan | 37 +++++++++++-------- .../ChatGPTServiceTests.swift | 3 +- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index d8185e4d..fcfe44af 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -269,6 +269,7 @@ let package = Package( "LanguageClient", "SuggestionModel", "XPCShared", + .product(name: "Logger", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "Terminal", package: "Tool"), ] diff --git a/Core/Sources/SuggestionModel/LanguageIdentifierFromFilePath.swift b/Core/Sources/SuggestionModel/LanguageIdentifierFromFilePath.swift index 75708204..56d91704 100644 --- a/Core/Sources/SuggestionModel/LanguageIdentifierFromFilePath.swift +++ b/Core/Sources/SuggestionModel/LanguageIdentifierFromFilePath.swift @@ -16,7 +16,7 @@ public enum CodeLanguage: RawRepresentable, Codable, CaseIterable, Hashable { return language } } - + public var hashValue: Int { rawValue.hashValue } @@ -30,7 +30,7 @@ public enum CodeLanguage: RawRepresentable, Codable, CaseIterable, Hashable { self = .other(rawValue) } } - + public static var allCases: [CodeLanguage] { var all = LanguageIdentifier.allCases.map(CodeLanguage.builtIn) all.append(.plaintext) @@ -38,7 +38,7 @@ public enum CodeLanguage: RawRepresentable, Codable, CaseIterable, Hashable { } } -extension LanguageIdentifier { +public extension LanguageIdentifier { /// Copied from https://github.com/github/linguist/blob/master/lib/linguist/languages.yml [MIT] var fileExtensions: [String] { switch self { @@ -263,3 +263,4 @@ public func languageIdentifierFromFileURL(_ fileURL: URL) -> CodeLanguage { } return .init(rawValue: fileExtension) ?? .plaintext } + diff --git a/Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift index cd61da45..840c3402 100644 --- a/Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift +++ b/Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift @@ -103,6 +103,10 @@ final class FetchSuggestionTests: XCTestCase { } class TestServer: GitHubCopilotLSP { + func sendNotification(_ notif: LanguageServerProtocol.ClientNotification) async throws { + // unimplemented + } + func sendRequest(_ r: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan index f5104832..7bb78eff 100644 --- a/TestPlan.xctestplan +++ b/TestPlan.xctestplan @@ -25,50 +25,57 @@ { "target" : { "containerPath" : "container:Core", - "identifier" : "CopilotModelTests", - "name" : "CopilotModelTests" + "identifier" : "ServiceTests", + "name" : "ServiceTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "CopilotServiceTests", - "name" : "CopilotServiceTests" + "identifier" : "SuggestionInjectorTests", + "name" : "SuggestionInjectorTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "ServiceTests", - "name" : "ServiceTests" + "identifier" : "SuggestionWidgetTests", + "name" : "SuggestionWidgetTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "SuggestionInjectorTests", - "name" : "SuggestionInjectorTests" + "identifier" : "PromptToCodeServiceTests", + "name" : "PromptToCodeServiceTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "OpenAIServiceTests", - "name" : "OpenAIServiceTests" + "identifier" : "GitHubCopilotServiceTests", + "name" : "GitHubCopilotServiceTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "SuggestionWidgetTests", - "name" : "SuggestionWidgetTests" + "identifier" : "SuggestionModelTests", + "name" : "SuggestionModelTests" } }, { "target" : { - "containerPath" : "container:Core", - "identifier" : "PromptToCodeServiceTests", - "name" : "PromptToCodeServiceTests" + "containerPath" : "container:Tool", + "identifier" : "LangChainTests", + "name" : "LangChainTests" + } + }, + { + "target" : { + "containerPath" : "container:Tool", + "identifier" : "OpenAIServiceTests", + "name" : "OpenAIServiceTests" } } ], diff --git a/Tool/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift b/Tool/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift index b976290a..210f6cea 100644 --- a/Tool/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift +++ b/Tool/Tests/OpenAIServiceTests/ChatGPTServiceTests.swift @@ -45,6 +45,7 @@ final class ChatGPTServiceTests: XCTestCase { requestBody = _requestBody return MockCompletionStreamAPI_Success() } + await service.mutateSystemPrompt("system") let stream = try await service.send(content: "Hello") var all = [String]() for try await text in stream { @@ -58,7 +59,7 @@ final class ChatGPTServiceTests: XCTestCase { } XCTAssertEqual(requestBody?.messages, [ - .init(role: .system, content: ""), + .init(role: .system, content: "system"), .init(role: .user, content: "Hello"), ], "System prompt is included") XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is correct") From e367c4a0a53166eb4b61bba3d85639d39447498a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:07:31 +0800 Subject: [PATCH 33/49] Remove GILState guards Life is simpler without it. We will see after we really start using Python --- Tool/Sources/PythonHelper/RunPython.swift | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tool/Sources/PythonHelper/RunPython.swift b/Tool/Sources/PythonHelper/RunPython.swift index 21146c44..35b62680 100644 --- a/Tool/Sources/PythonHelper/RunPython.swift +++ b/Tool/Sources/PythonHelper/RunPython.swift @@ -33,12 +33,12 @@ public func initializePython( isPythonInitialized = true // Initialize python Py_Initialize() - // Immediately release the thread, so that we can ensure the GIL state later. - // We may not recover the thread because all future tasks will be done in the other threads. - _ = PyEval_SaveThread() - // Setup GIL state guard. - gilStateEnsure = { PyGILState_Ensure() } - gilStateRelease = { gilState in PyGILState_Release(gilState as! GilState) } +// // Immediately release the thread, so that we can ensure the GIL state later. +// // We may not recover the thread because all future tasks will be done in the other threads. +// _ = PyEval_SaveThread() +// // Setup GIL state guard. +// gilStateEnsure = { PyGILState_Ensure() } +// gilStateRelease = { gilState in PyGILState_Release(gilState as! GilState) } } public func runPython( @@ -47,14 +47,14 @@ public func runPython( ) throws -> T { if usePythonThread { return try PythonThread.shared.runPythonAndWait { - return try gilStateGuard { - try closure() - } +// return try gilStateGuard { + return try closure() +// } } } else { - return try gilStateGuard { - try closure() - } +// return try gilStateGuard { + return try closure() +// } } } From ce315f4c3b992d205ad2f47d64389af7cd7f5af8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:12:16 +0800 Subject: [PATCH 34/49] Remove prints --- .../SearchChatPlugin/SearchQuery.swift | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift index 4cb8880d..cda31b32 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift @@ -18,9 +18,9 @@ func search(_ query: String) async throws -> AsyncThrowingStream AsyncThrowingStream(type: T.Type, input: T.Input) where T: LangChain.Chain { - print("Chain \(type) is started with input \(input).") - } + func onChainStart(type: T.Type, input: T.Input) where T: LangChain.Chain {} - func onAgentFinish(output: LangChain.AgentFinish) { - print("Agent is finished: \(output.returnValue)") - } + func onAgentFinish(output: LangChain.AgentFinish) {} func onAgentActionStart(action: LangChain.AgentAction) { - print("Agent runs action: \(action.toolName) with input \(action.toolInput)") onAgentActionStart("\(action.toolName): \(action.toolInput)") } func onAgentActionEnd(action: LangChain.AgentAction) { - print( - """ - Agent finish running action: \ - \(action.toolName) with observation \ - \(action.observation ?? "") - """ - ) onAgentActionEnd("\(action.toolName): \(action.toolInput)") } From cb0b0d82eb04001eb7d131488982c2c5b541479a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:12:22 +0800 Subject: [PATCH 35/49] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d4809574..c1a94c92 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ If you need to end a plugin, you can just type | `/run` | Runs the command under the project root. You can also use environment variable `PROJECT_ROOT` to get the project root and `FILE_PATH` to get the editing file path.| | `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | | `/math` | Solves a math problem in natural language | +| `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | ### Prompt to Code From 3adf852a6b53af65821d5325463f84d362859b5c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:16:46 +0800 Subject: [PATCH 36/49] Rename mapped to map --- Tool/Sources/LangChain/Chain.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/LangChain/Chain.swift b/Tool/Sources/LangChain/Chain.swift index 07decf8f..1db6ee75 100644 --- a/Tool/Sources/LangChain/Chain.swift +++ b/Tool/Sources/LangChain/Chain.swift @@ -116,7 +116,7 @@ public extension Chain { ConnectedChain(chainA: self, chainB: another) } - func mapped(_ map: @escaping (Output) -> NewOutput) -> MappedChain { + func map(_ map: @escaping (Output) -> NewOutput) -> MappedChain { MappedChain(chain: self, map: map) } } From 96a6a0a7d380348b4cc8df079721ca4261cdb702 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 16:39:15 +0800 Subject: [PATCH 37/49] Update CustomCommandTemplateProcessor to support more parameters --- .../CustomCommandTemplateProcessor.swift | 31 ++++++++++++------- Tool/Sources/Preferences/Keys.swift | 2 ++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Core/Sources/Service/CustomCommandTemplateProcessor.swift b/Core/Sources/Service/CustomCommandTemplateProcessor.swift index 6e40973d..5f611b01 100644 --- a/Core/Sources/Service/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/Service/CustomCommandTemplateProcessor.swift @@ -5,22 +5,30 @@ import XcodeInspector struct CustomCommandTemplateProcessor { func process(_ text: String) -> String { let info = getEditorInformation() - if let editorContent = info.editorContent { - let updatedText = text.replacingOccurrences(of: "{{selected_code}}", with: """ - ```\(info.language.rawValue) - \(editorContent.selectedContent.trimmingCharacters(in: ["\n"])) - ``` + let editorContent = info.editorContent + let updatedText = text + .replacingOccurrences(of: "{{selected_code}}", with: """ + \(editorContent?.selectedContent.trimmingCharacters(in: .whitespacesAndNewlines) ?? "") """) - return updatedText - } else { - let updatedText = text.replacingOccurrences(of: "{{selected_code}}", with: "") - return updatedText - } + .replacingOccurrences( + of: "{{active_editor_language}}", + with: info.language.rawValue + ) + .replacingOccurrences( + of: "{{active_editor_file_url}}", + with: info.documentURL?.path ?? "" + ) + .replacingOccurrences( + of: "{{active_editor_file_name}}", + with: info.documentURL?.lastPathComponent ?? "" + ) + return updatedText } struct EditorInformation { let editorContent: SourceEditor.Content? let language: CodeLanguage + let documentURL: URL? } func getEditorInformation() -> EditorInformation { @@ -30,7 +38,8 @@ struct CustomCommandTemplateProcessor { return .init( editorContent: editorContent, - language: language + language: language, + documentURL: documentURL ) } } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index d728d438..6939cdf9 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -289,7 +289,9 @@ public extension UserDefaultPreferenceKeys { feature: .chatWithSelection( extraSystemPrompt: "", prompt: """ + ```{{active_editor_language}} {{selected_code}} + ``` """, useExtraSystemPrompt: true ) From 28fe801e9bb87d00a332a3261072280bbde6cee2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 19:22:57 +0800 Subject: [PATCH 38/49] Add auto complete for chat plugins and scopes --- .../SuggestionWidget/CustomTextEditor.swift | 22 ++++++++++++++-- .../SuggestionPanelContent/ChatPanel.swift | 25 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Core/Sources/SuggestionWidget/CustomTextEditor.swift b/Core/Sources/SuggestionWidget/CustomTextEditor.swift index de750c04..00251edc 100644 --- a/Core/Sources/SuggestionWidget/CustomTextEditor.swift +++ b/Core/Sources/SuggestionWidget/CustomTextEditor.swift @@ -8,19 +8,25 @@ struct CustomTextEditor: NSViewRepresentable { @Binding var text: String let font: NSFont let onSubmit: () -> Void + var completions: (_ text: String, _ words: [String], _ range: NSRange) + -> [String] = { _, _, _ in + [] + } func makeNSView(context: Context) -> NSScrollView { + context.coordinator.completions = completions let textView = (context.coordinator.theTextView.documentView as! NSTextView) textView.delegate = context.coordinator textView.string = text textView.font = font textView.allowsUndo = true textView.drawsBackground = false - + return context.coordinator.theTextView } func updateNSView(_ nsView: NSScrollView, context: Context) { + context.coordinator.completions = completions let textView = (context.coordinator.theTextView.documentView as! NSTextView) guard textView.string != text else { return } textView.string = text @@ -32,6 +38,7 @@ extension CustomTextEditor { var view: CustomTextEditor var theTextView = NSTextView.scrollableTextView() var affectedCharRange: NSRange? + var completions: (String, [String], _ range: NSRange) -> [String] = { _, _, _ in [] } init(_ view: CustomTextEditor) { self.view = view @@ -43,13 +50,14 @@ extension CustomTextEditor { } view.text = textView.string + textView.complete(nil) } func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(NSTextView.insertNewline(_:)) { if let event = NSApplication.shared.currentEvent, !event.modifierFlags.contains(.shift), - event.keyCode == 36 + event.keyCode == 36 // enter { view.onSubmit() return true @@ -66,6 +74,16 @@ extension CustomTextEditor { ) -> Bool { return true } + + func textView( + _ textView: NSTextView, + completions words: [String], + forPartialWordRange charRange: NSRange, + indexOfSelectedItem index: UnsafeMutablePointer? + ) -> [String] { + index?.pointee = -1 + return completions(textView.textStorage?.string ?? "", words, charRange) + } } } diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index ddc32f4e..df9894e7 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -88,7 +88,7 @@ struct ChatPanelMessages: View { } } .listItemTint(.clear) - + Instruction() Spacer() @@ -350,7 +350,28 @@ struct ChatPanelInputArea: View { CustomTextEditor( text: $typedMessage, font: .systemFont(ofSize: 14), - onSubmit: { submitText() } + onSubmit: { submitText() }, + completions: { text, _, range in + if text.isEmpty { return [] } + let availableFeatures = [ + "/run", + "/airun", + "/math", + "/search", + "@selection", + "@file", + ] + return availableFeatures + .filter { $0.hasPrefix(text) && $0 != text } + .compactMap { + guard let index = $0.index( + $0.startIndex, + offsetBy: range.location, + limitedBy: $0.endIndex + ) else { return nil } + return String($0[index...]) + } + } ) .padding(.top, 1) .padding(.bottom, -1) From eac0330ab144528da8527552cf340dd204837b9c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 19:27:01 +0800 Subject: [PATCH 39/49] Add instructions on plugins --- .../SuggestionWidget/SuggestionPanelContent/ChatPanel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index df9894e7..55cb2e3e 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -147,6 +147,8 @@ private struct Instruction: View { - The text cursor location. If you'd like me to examine the entire file, simply add `@file` to the beginning of your message. + + To use plugins, you can start a message with `/pluginName`. """ ) } else { @@ -160,6 +162,8 @@ private struct Instruction: View { - The text cursor location. If you would like me to examine the selected code, please prefix your message with `@selection`. If you would like me to examine the entire file, please prefix your message with `@file`. + + To use plugins, you can start a message with `/pluginName`. """ ) } From 99f6a63493c62e85cdc9c4a058037eed850fcf7f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 19:27:20 +0800 Subject: [PATCH 40/49] Search plugins with lowercased ids --- Core/Sources/ChatService/ChatPluginController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Sources/ChatService/ChatPluginController.swift b/Core/Sources/ChatService/ChatPluginController.swift index 061e3d6b..de65d4e9 100644 --- a/Core/Sources/ChatService/ChatPluginController.swift +++ b/Core/Sources/ChatService/ChatPluginController.swift @@ -12,7 +12,7 @@ final class ChatPluginController { self.chatGPTService = chatGPTService var all = [String: ChatPlugin.Type]() for plugin in plugins { - all[plugin.command] = plugin + all[plugin.command.lowercased()] = plugin } self.plugins = all } @@ -29,7 +29,7 @@ final class ChatPluginController { let regex = try NSRegularExpression(pattern: #"^\/([a-zA-Z0-9]+)"#) let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content)) if let match = matches.first { - let command = String(content[Range(match.range(at: 1), in: content)!]) + let command = String(content[Range(match.range(at: 1), in: content)!]).lowercased() // handle exit plugin if command == "exit" { if let plugin = runningPlugin { From f4397dbb37d1d43963e68f70d91a6713509e060c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 21:29:18 +0800 Subject: [PATCH 41/49] Add cancellation support to agent executor --- .../SearchChatPlugin/SearchChatPlugin.swift | 7 +++++-- .../ChatPlugins/SearchChatPlugin/SearchQuery.swift | 10 +++++++--- Tool/Sources/LangChain/AgentExecutor.swift | 11 +++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift index 1f30f3e0..602b94ae 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchChatPlugin.swift @@ -28,14 +28,17 @@ public actor SearchChatPlugin: ChatPlugin { } do { - let eventStream = try await search(content) + let (eventStream, cancelAgent) = try await search(content) var actions = [String]() var finishedActions = Set() var message = "" for try await event in eventStream { - guard !isCancelled else { return } + guard !isCancelled else { + await cancelAgent() + break + } switch event { case let .startAction(content): actions.append(content) diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift index cda31b32..66e118a3 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift @@ -9,7 +9,9 @@ enum SearchEvent { case finishAnswer(String, [(title: String, link: String)]) } -func search(_ query: String) async throws -> AsyncThrowingStream { +func search(_ query: String) async throws + -> (stream: AsyncThrowingStream, cancel: () async -> Void) +{ let bingSearch = BingSearchService( subscriptionKey: UserDefaults.shared.value(for: \.bingSearchSubscriptionKey), searchURL: UserDefaults.shared.value(for: \.bingSearchEndpoint) @@ -91,7 +93,7 @@ func search(_ query: String) async throws -> AsyncThrowingStream { continuation in + return (AsyncThrowingStream { continuation in let callback = ResultCallbackManager( onFinalAnswerToken: { continuation.yield(.answerToken($0)) @@ -112,6 +114,8 @@ func search(_ query: String) async throws -> AsyncThrowingStream: Chain where InnerAgent.Input == S let tools: [String: AgentTool] let maxIteration: Int? let maxExecutionTime: Double? - let earlyStopHandleType: AgentEarlyStopHandleType + var earlyStopHandleType: AgentEarlyStopHandleType var now: () -> Date = { Date() } + var isCancelled = false public init( agent: InnerAgent, @@ -47,6 +48,7 @@ public actor AgentExecutor: Chain where InnerAgent.Input == S var intermediateSteps: [AgentAction] = [] func shouldContinue() -> Bool { + if isCancelled { return false } if let maxIteration = maxIteration, iterations >= maxIteration { return false } @@ -87,7 +89,7 @@ public actor AgentExecutor: Chain where InnerAgent.Input == S } iterations += 1 } - + let output = try await agent.returnStoppedResponse( input: input, earlyStoppedHandleType: earlyStopHandleType, @@ -104,6 +106,11 @@ public actor AgentExecutor: Chain where InnerAgent.Input == S public nonisolated func parseOutput(_ output: Output) -> String { output.finalOutput } + + public func cancel() { + isCancelled = true + earlyStopHandleType = .force + } } struct InvalidToolError: Error {} From 503e5ed72535a878935c42f2f05d8b21a75d173e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 21:35:40 +0800 Subject: [PATCH 42/49] Add /exit to auto completion --- .../SuggestionWidget/SuggestionPanelContent/ChatPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index 55cb2e3e..5683c2e1 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -362,6 +362,7 @@ struct ChatPanelInputArea: View { "/airun", "/math", "/search", + "/exit", "@selection", "@file", ] From 77fa566d47c2eeb62daae539bd18b95f48a98b21 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 21:35:47 +0800 Subject: [PATCH 43/49] Update README.md --- README.md | 118 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index c1a94c92..aa742258 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Screenshot](/Screenshot.png) -Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copilot, Codeium and ChatGPT support for Xcode. +Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copilot, Codeium and ChatGPT support for Xcode. Buy Me A Coffee @@ -20,10 +20,10 @@ Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copil - [Installation and Setup](#installation-and-setup) - [Install](#install) - [Enable the Extension](#enable-the-extension) + - [Granting Permissions to the App](#granting-permissions-to-the-app) - [Setting Up GitHub Copilot](#setting-up-github-copilot) - [Setting Up Codeium](#setting-up-codeium) - [Setting Up OpenAI API Key](#setting-up-openai-api-key) - - [Granting Permissions to the App](#granting-permissions-to-the-app) - [Managing `CopilotForXcodeExtensionService.app`](#managing-copilotforxcodeextensionserviceapp) - [Update](#update) - [Feature](#feature) @@ -41,14 +41,16 @@ For development instruction, check [Development.md](DEVELOPMENT.md). - Public network connection. For suggestion features: -- For GitHub Copilot users: + +- For GitHub Copilot users: - [Node](https://nodejs.org/) installed to run the Copilot LSP. - Active GitHub Copilot subscription. - For Codeium users: - Active Codeium account. For chat and prompt to code features: -- Valid OpenAI API key. + +- Valid OpenAI API key. ## Permissions Required @@ -73,17 +75,29 @@ Open the app, the app will create a launch agent to setup a background running S ### Enable the Extension -Enable the extension in `System Settings.app`. +Enable the extension in `System Settings.app`. From the Apple menu located in the top-left corner of your screen click `System Settings`. Navigate to `Privacy & Security` then toward the bottom click `Extensions`. Click `Xcode Source Editor` and tick `Copilot`. - + If you are using macOS Monterey, enter the `Extensions` menu in `System Preferences.app` with its dedicated icon. +### Granting Permissions to the App + +The first time the app is open and command run, the extension will ask for the necessary permissions. + +Alternatively, you may manually grant the required permissions by navigating to the `Privacy & Security` tab in the `System Settings.app`. + +- To grant permissions for the Accessibility API, click `Accessibility`, and drag `CopilotForXcodeExtensionService.app` to the list. You can locate the extension app by clicking `Reveal Extension App in Finder` in the host app. + +Accessibility API + +If you encounter an alert requesting permission that you have previously granted, please remove the permission from the list and add it again to re-grant the necessary permissions. + ### Setting Up GitHub Copilot - + 1. In the host app, switch to the service tab and click on GitHub Copilot to access your GitHub Copilot account settings. 2. Click "Install" to install the language server. -3. Optionally setup the path to Node. The default value is just `node`, Copilot for Xcode.app will try to find the Node from the PATH available in a login shell. If your Node is installed somewhere else, you can run `which node` from terminal to get the path. +3. Optionally setup the path to Node. The default value is just `node`, Copilot for Xcode.app will try to find the Node from the PATH available in a login shell. If your Node is installed somewhere else, you can run `which node` from terminal to get the path. 4. Click "Sign In", and you will be directed to a verification website provided by GitHub, and a user code will be pasted into your clipboard. 5. After signing in, go back to the app and click "Confirm Sign-in" to finish. 6. Go to "Feature - Suggestion" and update the feature provider to "GitHub Copilot". @@ -106,25 +120,13 @@ The installed language server is located at `~/Library/Application Support/com.i 1. In the host app, click OpenAI to enter the OpenAI account settings. 2. Enter your api key to the text field. -### Granting Permissions to the App - -The first time the app is open and command run, the extension will ask for the necessary permissions. - -Alternatively, you may manually grant the required permissions by navigating to the `Privacy & Security` tab in the `System Settings.app`. - -- To grant permissions for the Accessibility API, click `Accessibility`, and drag `CopilotForXcodeExtensionService.app` to the list. You can locate the extension app by clicking `Reveal Extension App in Finder` in the host app. - -Accessibility API - -If you encounter an alert requesting permission that you have previously granted, please remove the permission from the list and add it again to re-grant the necessary permissions. - ### Managing `CopilotForXcodeExtensionService.app` This app runs whenever you open `Copilot for Xcode.app` or `Xcode.app`. You can quit it with its menu bar item that looks like a steering wheel. You can also set it to quit automatically when the above 2 apps are closed. -## Update +## Update If the app was installed via Homebrew, you can update it by running: @@ -132,7 +134,7 @@ If the app was installed via Homebrew, you can update it by running: brew upgrade --cask copilot-for-xcode ``` -Alternatively, You can use the in-app updater or download the latest version manually from the latest [release](https://github.com/intitni/CopilotForXcode/releases). +Alternatively, You can use the in-app updater or download the latest version manually from the latest [release](https://github.com/intitni/CopilotForXcode/releases). After updating, please restart Xcode to allow the extension to reload. @@ -144,13 +146,13 @@ If you find that some of the features are no longer working, please first try re ### Suggestion -The app can provide real-time code suggestions based on the files you have opened. It's powered by GitHub Copilot and Codeium. +The app can provide real-time code suggestions based on the files you have opened. It's powered by GitHub Copilot and Codeium. -If you're working on a company project and don't want the suggestion feature to be triggered, you can globally disable it and choose to enable it only for specific projects. +If you're working on a company project and don't want the suggestion feature to be triggered, you can globally disable it and choose to enable it only for specific projects. Whenever your code is updated, the app will automatically fetch suggestions for you, you can cancel this by pressing **Escape**. -*: If a file is already open before the helper app launches, you will need to switch to those files in order to send the open file notification. +\*: If a file is already open before the helper app launches, you will need to switch to those files in order to send the open file notification. #### Commands @@ -167,6 +169,7 @@ Whenever your code is updated, the app will automatically fetch suggestions for This feature is powered by ChatGPT. Please ensure that you have set up your OpenAI account before using it. The chat knows the following information: + - The selected code in the active editor. - The relative path of the file. - The error and warning labels in the active editor. @@ -182,45 +185,48 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha #### Keyboard Shortcuts -| Shortcut | Description | -|:---:|---| -| `⌘W` | Close the chat. | -| `⌘M` | Minimize the chat, you can bring it back with any chat commands or by clicking the circular widget. | -| `⇧↩︎` | Add new line. | +| Shortcut | Description | +| :------: | --------------------------------------------------------------------------------------------------- | +| `⌘W` | Close the chat. | +| `⌘M` | Minimize the chat, you can bring it back with any chat commands or by clicking the circular widget. | +| `⇧↩︎` | Add new line. | #### Chat Scope The chat panel allows for chat scope to temporarily control the context of the conversation for the latest message. To use a scope, simply prefix the message with `@scope`. -| Scope | Description | -|:---:|---| +| Scope | Description | +| :----------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@selection` | Inject the selected code from the active editor into the conversation. This scope will be applied to any message automatically. If you don't want this to be the default behavior, you can turn off the option `Use selection scope by default in chat context.`. | -| `@file` | Inject the content of the file into the conversation. Keep in mind that you may not have enough tokens to inject large files. | +| `@file` | Inject the content of the file into the conversation. Keep in mind that you may not have enough tokens to inject large files. | #### Chat Plugins -The chat panel supports chat plugins that may not require an OpenAI API key. For example, if you need to use the `/run` plugin, you just type +The chat panel supports chat plugins that may not require an OpenAI API key. For example, if you need to use the `/run` plugin, you just type + ``` /run echo hello ``` -If you need to end a plugin, you can just type +If you need to end a plugin, you can just type + ``` /exit ``` -| Command | Description | -|:---:|---| -| `/run` | Runs the command under the project root. You can also use environment variable `PROJECT_ROOT` to get the project root and `FILE_PATH` to get the editing file path.| -| `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | -| `/math` | Solves a math problem in natural language | -| `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | +| Command | Description | +| :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/run` | Runs the command under the project root. You can also use environment variable `PROJECT_ROOT` to get the project root and `FILE_PATH` to get the editing file path. | +| `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | +| `/math` | Solves a math problem in natural language | +| `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | ### Prompt to Code Refactor existing code or write new code using natural language. This feature is recommended when you need to update a specific piece of code. Some example use cases include: + - Improving the code's readability. - Correcting bugs in the code. - Adding documentation to the code. @@ -241,21 +247,30 @@ You can create custom commands that run Chat and Prompt to Code with personalize - Open Chat: Open the chat window and immediately send a message, if provided. You can provide more information through the extra system prompt field. - Custom Chat: Open the chat window and immediately send a message, if provided. You can overwrite the entire system prompt through the system prompt field. +For Open Chat and Custom Chat commands, you can use the following template arguments: + +| Argument | Description | +| ----------------------------- | ---------------------------------------------- | +| `{{selected_code}}` | The currently selected code in the editor. | +| `{{active_editor_language}}` | The programming language of the active editor. | +| `{{active_editor_file_url}}` | The URL of the active file in the editor. | +| `{{active_editor_file_name}}` | The name of the active file in the editor. | + ## Key Bindings It looks like there is no way to add default key bindings to commands, but you can set them up in `Xcode settings > Key Bindings`. You can filter the list by typing `copilot` in the search bar. A [recommended setup](https://github.com/intitni/CopilotForXcode/issues/14) that should cause no conflict is -| Command | Key Binding | -| --- | --- | -| Get Suggestions | `⌥?` | -| Accept Suggestions | `⌥}` | -| Reject Suggestion | `⌥{` | -| Next Suggestion | `⌥>` | -| Previous Suggestion | `⌥<` | -| Open Chat | `⌥"` | -| Explain Selection | `⌥\|` | +| Command | Key Binding | +| ------------------- | ----------- | +| Get Suggestions | `⌥?` | +| Accept Suggestions | `⌥}` | +| Reject Suggestion | `⌥{` | +| Next Suggestion | `⌥>` | +| Previous Suggestion | `⌥<` | +| Open Chat | `⌥"` | +| Explain Selection | `⌥\|` | Essentially using `⌥⇧` as the "access" key combination for all bindings. @@ -281,6 +296,7 @@ fi - The extension uses some dirty tricks to get the file and project/workspace paths. It may fail, it may be incorrect, especially when you have multiple Xcode windows running, and maybe even worse when they are in different displays. I am not sure about that though. - The suggestions are presented as C-style comments in comment mode, they may break your code if you are editing a JSON file or something. -## License +## License Please check [LICENSE](LICENSE) for details. + From f4c7e05dd9b0da255b72292ee92da02d6c8f4279 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 21:47:56 +0800 Subject: [PATCH 44/49] Support setting max search iteration --- .../SearchChatPlugin/SearchQuery.swift | 2 +- .../FeatureSettings/ChatSettingsView.swift | 21 +++++++++++++++++-- Tool/Sources/Preferences/Keys.swift | 4 ++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift index 66e118a3..118b1473 100644 --- a/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift +++ b/Core/Sources/ChatPlugins/SearchChatPlugin/SearchQuery.swift @@ -47,7 +47,7 @@ func search(_ query: String) async throws let agentExecutor = AgentExecutor( agent: ChatAgent(chatModel: chatModel, tools: tools), tools: tools, - maxIteration: 2, + maxIteration: UserDefaults.shared.value(for: \.chatSearchPluginMaxIterations), earlyStopHandleType: .generate ) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 8f747551..617ddd4d 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -20,6 +20,7 @@ struct ChatSettingsView: View { @AppStorage(\.chatFeatureProvider) var chatFeatureProvider @AppStorage(\.chatGPTModel) var chatGPTModel @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt + @AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations init() {} } @@ -36,6 +37,8 @@ struct ChatSettingsView: View { uiForm Divider() contextForm + Divider() + pluginForm } } @@ -123,7 +126,7 @@ struct ChatSettingsView: View { Text("9 Messages").tag(9) Text("11 Messages").tag(11) } - + VStack(alignment: .leading, spacing: 4) { Text("Default System Prompt") EditableText(text: $settings.defaultChatSystemPrompt) @@ -178,7 +181,7 @@ struct ChatSettingsView: View { Toggle(isOn: $settings.useSelectionScopeByDefaultInChatContext) { Text("Use selection scope by default in chat context.") } - + Toggle(isOn: $settings.embedFileContentInChatContextIfNoSelection) { Text("Embed file content in chat context if no code is selected.") } @@ -198,6 +201,20 @@ struct ChatSettingsView: View { } } + @ViewBuilder + var pluginForm: some View { + Form { + TextField(text: .init(get: { + "\(Int(settings.chatSearchPluginMaxIterations))" + }, set: { + settings.chatSearchPluginMaxIterations = Int($0) ?? 0 + })) { + Text("Search Plugin Max Iterations") + } + .textFieldStyle(.roundedBorder) + } + } + var languagePicker: some View { Menu { if !settings.chatGPTLanguage.isEmpty, diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 6939cdf9..1fc39649 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -245,6 +245,10 @@ public extension UserDefaultPreferenceKeys { key: "DefaultChatSystemPrompt" ) } + + var chatSearchPluginMaxIterations: PreferenceKey { + .init(defaultValue: 3, key: "ChatSearchPluginMaxIterations") + } } // MARK: - Bing Search From e71d9367ee79404986262622b02aaaea9954d841 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 21:38:45 +0800 Subject: [PATCH 45/49] Bump version to 0.18.0 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index f45a2716..bfae9a50 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,2 +1,2 @@ -APP_VERSION = 0.17.1 -APP_BUILD = 171 +APP_VERSION = 0.18.0 +APP_BUILD = 180 From 33ecf12be9b9da8ec0630c827c5429244f8a5ec8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 22:39:16 +0800 Subject: [PATCH 46/49] Detach LangChain Python --- Copilot for Xcode.xcodeproj/project.pbxproj | 10 +--- DEVELOPMENT.md | 21 +------- ExtensionService/InitializePython.swift | 58 ++++++++++----------- 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index 7e2699d6..b83f3f4d 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -38,10 +38,7 @@ C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; }; C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; }; - C8A3AE522A2884DA0046E809 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8A3AE512A2883430046E809 /* Python.xcframework */; }; C8A3AE592A2885A70046E809 /* InitializePython.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8A3AE582A2885A70046E809 /* InitializePython.swift */; }; - C8A3AE5B2A288AF90046E809 /* site-packages in Resources */ = {isa = PBXBuildFile; fileRef = C8A3AE5A2A288AF90046E809 /* site-packages */; }; - C8A3B1772A288FA90046E809 /* python-stdlib in Resources */ = {isa = PBXBuildFile; fileRef = C8A3B1762A288FA90046E809 /* python-stdlib */; }; C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; }; /* End PBXBuildFile section */ @@ -209,7 +206,6 @@ buildActionMask = 2147483647; files = ( C861E61E2994F6150056CB02 /* Service in Frameworks */, - C8A3AE522A2884DA0046E809 /* Python.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -497,8 +493,6 @@ files = ( C861E6152994F6080056CB02 /* Assets.xcassets in Resources */, C81291D72994FE6900196E12 /* Main.storyboard in Resources */, - C8A3B1772A288FA90046E809 /* python-stdlib in Resources */, - C8A3AE5B2A288AF90046E809 /* site-packages in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -522,7 +516,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\necho \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\nfind \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; + shellScript = "#set -e\n#echo \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\n#find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; }; C8A3B1782A2894E10046E809 /* Sign Python Site Packages */ = { isa = PBXShellScriptBuildPhase; @@ -541,7 +535,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\necho \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\nfind \"$CODESIGNING_FOLDER_PATH/Contents/Resources/site-packages\" -type f \\( -name \"*.so\" -o -name \"*.dylib\" \\) -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; + shellScript = "#set -e\n#echo \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\n#find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/site-packages\" -type f \\( -name \"*.so\" -o -name \"*.dylib\" \\) -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 26d72e9d..0cb346be 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -27,8 +27,7 @@ Most of the logics are implemented inside the package `Core`. ## Building and Archiving the App -1. Run `make setup` to setup the project. (You may need to install the specific version of python to install the dependencies, please check `Python/site-packages/install.sh` for details.) -2. Build or archive the Copilot for Xcode target. +1. Build or archive the Copilot for Xcode target. ## Testing Extension @@ -44,24 +43,6 @@ For new tests, they should be added to the `TestPlan.xctestplan`. To create a chat plugin, please use the `TerminalChatPlugin` as an example. You should add your plugin to the target `ChatPlugin` and register it in `ChatService`. -## LangChain and Python - -The app uses PythonKit to execute Python code. - -When running Python code, ensure that you wrap it inside `runPython`. This will automatically insert `GilStateEnsure` and `GilStateRelease` for you. - -```swift -import PythonHelper - -try runPython { - // access python here -} -``` - -Instead of throwing a `PythonError`, `runPython` will throw a `ReadablePythonError`. - -If importing a Python module causes the app to crash, it is usually due to the thread's stack size being too small. To resolve this, try importing with `Python.attemptImportOnPythonThread`, which is defined in `PythonHelper`, or simply import from the main thread. - ## Code Style We use SwiftFormat to format the code. diff --git a/ExtensionService/InitializePython.swift b/ExtensionService/InitializePython.swift index 0d9b1058..7f438781 100644 --- a/ExtensionService/InitializePython.swift +++ b/ExtensionService/InitializePython.swift @@ -1,38 +1,38 @@ import Foundation -import Python +//import Python import PythonHelper import PythonKit import Logger @MainActor func initializePython() { - guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), - let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil), - let libDynloadPath = Bundle.main.path( - forResource: "python-stdlib/lib-dynload", - ofType: nil - ) - else { - Logger.service.info("Python is not installed!") - return - } - - PythonHelper.initializePython( - sitePackagePath: sitePackagePath, - stdLibPath: stdLibPath, - libDynloadPath: libDynloadPath, - Py_Initialize: Py_Initialize, - PyEval_SaveThread: PyEval_SaveThread, - PyGILState_Ensure: PyGILState_Ensure, - PyGILState_Release: PyGILState_Release - ) - - Task { - // All future task should run inside runPython. - try runPython { - let sys = Python.import("sys") - Logger.service.info("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") - } - } +// guard let sitePackagePath = Bundle.main.path(forResource: "site-packages", ofType: nil), +// let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil), +// let libDynloadPath = Bundle.main.path( +// forResource: "python-stdlib/lib-dynload", +// ofType: nil +// ) +// else { +// Logger.service.info("Python is not installed!") +// return +// } +// +// PythonHelper.initializePython( +// sitePackagePath: sitePackagePath, +// stdLibPath: stdLibPath, +// libDynloadPath: libDynloadPath, +// Py_Initialize: Py_Initialize, +// PyEval_SaveThread: PyEval_SaveThread, +// PyGILState_Ensure: PyGILState_Ensure, +// PyGILState_Release: PyGILState_Release +// ) +// +// Task { +// // All future task should run inside runPython. +// try runPython { +// let sys = Python.import("sys") +// Logger.service.info("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") +// } +// } } From 8fbf130d489a55d307c1d23b82ec87b691182898 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 7 Jun 2023 22:43:39 +0800 Subject: [PATCH 47/49] Remove the "Calculating..." thing from MathChatPlugin --- Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift index 319f641d..296f564a 100644 --- a/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift +++ b/Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift @@ -22,13 +22,11 @@ public actor MathChatPlugin: ChatPlugin { delegate?.pluginDidStartResponding(self) let id = "\(Self.command)-\(UUID().uuidString)" - async let translatedCalculating = translate(text: "Calculating...") async let translatedAnswer = translate(text: "Answer:") - var reply = ChatMessage(id: id, role: .assistant, content: await translatedCalculating) + var reply = ChatMessage(id: id, role: .assistant, content: "") await chatGPTService.mutateHistory { history in history.append(.init(role: .user, content: originalMessage, summary: content)) - history.append(reply) } do { From a47fa95fcd19298ed9d4d20d110d27cab17c36e9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 Jun 2023 14:19:42 +0800 Subject: [PATCH 48/49] Adjust ChatAgent --- Tool/Sources/LangChain/Agents/ChatAgent.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/LangChain/Agents/ChatAgent.swift b/Tool/Sources/LangChain/Agents/ChatAgent.swift index df65a992..48a04869 100644 --- a/Tool/Sources/LangChain/Agents/ChatAgent.swift +++ b/Tool/Sources/LangChain/Agents/ChatAgent.swift @@ -92,8 +92,9 @@ public class ChatAgent: Agent { let baseScratchpad = constructBaseScratchpad(intermediateSteps: intermediateSteps) if baseScratchpad.isEmpty { return .text("") } return .text(""" + This was your previous work (but I haven't seen any of it! I only see what you return as final answer): \(baseScratchpad) - (Continue with your `Thought:`) + (Please continue with `Thought:`) """) } @@ -111,7 +112,7 @@ public class ChatAgent: Agent { let output = answer.trimmingCharacters(in: .whitespacesAndNewlines) return .finish(AgentFinish(returnValue: output, log: text)) } catch { - Logger.langchain.error("Could not parse LLM output final answer: \(error)") + Logger.langchain.info("Could not parse LLM output final answer: \(error)") return nil } } @@ -145,7 +146,7 @@ public class ChatAgent: Agent { ), ]) } catch { - Logger.langchain.error("Could not parse LLM output next action: \(error)") + Logger.langchain.info("Could not parse LLM output next action: \(error)") return nil } } @@ -158,7 +159,12 @@ public class ChatAgent: Agent { let finalAnswer = try? forceParser.parse(&parsableContent) .trimmingCharacters(in: .whitespacesAndNewlines) - return .finish(AgentFinish(returnValue: String(finalAnswer ?? text), log: text)) + var answer = finalAnswer ?? text + if answer.isEmpty { + answer = "Sorry, I don't know." + } + + return .finish(AgentFinish(returnValue: String(answer), log: text)) } } From 22e3fa47399071cf1930f277ad0c5457f2b037aa Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 Jun 2023 14:36:02 +0800 Subject: [PATCH 49/49] Update appcast.xml --- ExtensionService/ExtensionService.entitlements | 2 -- appcast.xml | 12 ++++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ExtensionService/ExtensionService.entitlements b/ExtensionService/ExtensionService.entitlements index ae1430f1..5a41052f 100644 --- a/ExtensionService/ExtensionService.entitlements +++ b/ExtensionService/ExtensionService.entitlements @@ -6,8 +6,6 @@ $(TeamIdentifierPrefix)group.$(BUNDLE_IDENTIFIER_BASE) - com.apple.security.cs.disable-library-validation - keychain-access-groups $(AppIdentifierPrefix)$(BUNDLE_IDENTIFIER_BASE).Shared diff --git a/appcast.xml b/appcast.xml index 41bb3495..fb6c7c74 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.18.0 + Thu, 08 Jun 2023 14:34:14 +0800 + 180 + 0.18.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.18.0 + + + + 0.17.1 Wed, 31 May 2023 12:30:21 +0800