Skip to content

Commit fad5fae

Browse files
committed
Merge branch 'feature/multiple-chat-tab' into develop
2 parents 8f8909d + aebdaca commit fad5fae

34 files changed

Lines changed: 1065 additions & 735 deletions

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@
496496
C8A3AE572A28852D0046E809 /* Sign Python STD */ = {
497497
isa = PBXShellScriptBuildPhase;
498498
alwaysOutOfDate = 1;
499-
buildActionMask = 2147483647;
499+
buildActionMask = 8;
500500
files = (
501501
);
502502
inputFileListPaths = (
@@ -508,14 +508,14 @@
508508
);
509509
outputPaths = (
510510
);
511-
runOnlyForDeploymentPostprocessing = 0;
511+
runOnlyForDeploymentPostprocessing = 1;
512512
shellPath = /bin/sh;
513513
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";
514514
};
515515
C8A3B1782A2894E10046E809 /* Sign Python Site Packages */ = {
516516
isa = PBXShellScriptBuildPhase;
517517
alwaysOutOfDate = 1;
518-
buildActionMask = 2147483647;
518+
buildActionMask = 8;
519519
files = (
520520
);
521521
inputFileListPaths = (
@@ -527,7 +527,7 @@
527527
);
528528
outputPaths = (
529529
);
530-
runOnlyForDeploymentPostprocessing = 0;
530+
runOnlyForDeploymentPostprocessing = 1;
531531
shellPath = /bin/sh;
532532
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";
533533
};

Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ let package = Package(
9393
.product(name: "OpenAIService", package: "Tool"),
9494
.product(name: "Preferences", package: "Tool"),
9595
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
96+
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
9697
// .product(name: "PythonKit", package: "PythonKit"),
9798
]
9899
),
@@ -224,6 +225,7 @@ let package = Package(
224225
name: "ChatTab",
225226
dependencies: [
226227
"SharedUIComponents",
228+
"ChatService",
227229
.product(name: "OpenAIService", package: "Tool"),
228230
.product(name: "Logger", package: "Tool"),
229231
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
@@ -240,6 +242,7 @@ let package = Package(
240242
.product(name: "Preferences", package: "Tool"),
241243
]
242244
),
245+
.testTarget(name: "SharedUIComponentsTests", dependencies: ["SharedUIComponents"]),
243246

244247
.target(
245248
name: "SuggestionWidget",

Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift

Lines changed: 67 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -56,72 +56,73 @@ struct QueryWebsiteFunction: ChatGPTFunction {
5656
}
5757

5858
func call(arguments: Arguments) async throws -> Result {
59-
do {
60-
throw CancellationError()
61-
let embedding = OpenAIEmbedding(
62-
configuration: UserPreferenceEmbeddingConfiguration()
63-
)
64-
65-
let queryEmbeddings = try await embedding.embed(query: arguments.query)
66-
let searchCount = UserDefaults.shared.value(for: \.chatGPTMaxToken) > 5000 ? 3 : 2
67-
68-
let result = try await withThrowingTaskGroup(
69-
of: [(document: Document, distance: Float)].self
70-
) { group in
71-
for urlString in arguments.urls {
72-
guard let url = URL(string: urlString) else { continue }
73-
group.addTask {
74-
if let database = await TemporaryUSearch.view(identifier: urlString) {
75-
return try await database.searchWithDistance(
76-
embeddings: queryEmbeddings,
77-
count: searchCount
78-
)
79-
}
80-
// 1. grab the website content
81-
await reportProgress("Loading \(url)..")
82-
print("== load \(url)")
83-
let loader = WebLoader(urls: [url])
84-
let documents = try await loader.load()
85-
await reportProgress("Processing \(url)..")
86-
print("== loaded \(url), documents: \(documents.count)")
87-
// 2. split the content
88-
let splitter = RecursiveCharacterTextSplitter(
89-
chunkSize: 1000,
90-
chunkOverlap: 100
91-
)
92-
let splitDocuments = try await splitter.transformDocuments(documents)
93-
print("== split \(url), documents: \(splitDocuments.count)")
94-
// 3. embedding and store in db
95-
await reportProgress("Embedding \(url)..")
96-
let embeddedDocuments = try await embedding.embed(documents: splitDocuments)
97-
print("== embedded \(url)")
98-
let database = TemporaryUSearch(identifier: urlString)
99-
try await database.set(embeddedDocuments)
100-
print("== save to database \(url)")
101-
let result = try await database.searchWithDistance(
102-
embeddings: queryEmbeddings,
103-
count: searchCount
104-
)
105-
print("== result of \(url): \(result)")
106-
return result
107-
}
108-
}
109-
110-
var all = [(document: Document, distance: Float)]()
111-
for try await result in group {
112-
all.append(contentsOf: result)
113-
}
114-
await reportProgress("Finish reading websites.")
115-
return all
116-
.sorted { $0.distance < $1.distance }
117-
.prefix(searchCount)
118-
}
119-
120-
return .init(relevantDocuments: result.map(\.document))
121-
} catch {
122-
await reportProgress("Failed reading websites.")
123-
throw error
124-
}
59+
throw CancellationError()
60+
// do {
61+
// throw CancellationError()
62+
// let embedding = OpenAIEmbedding(
63+
// configuration: UserPreferenceEmbeddingConfiguration()
64+
// )
65+
//
66+
// let queryEmbeddings = try await embedding.embed(query: arguments.query)
67+
// let searchCount = UserDefaults.shared.value(for: \.chatGPTMaxToken) > 5000 ? 3 : 2
68+
//
69+
// let result = try await withThrowingTaskGroup(
70+
// of: [(document: Document, distance: Float)].self
71+
// ) { group in
72+
// for urlString in arguments.urls {
73+
// guard let url = URL(string: urlString) else { continue }
74+
// group.addTask {
75+
// if let database = await TemporaryUSearch.view(identifier: urlString) {
76+
// return try await database.searchWithDistance(
77+
// embeddings: queryEmbeddings,
78+
// count: searchCount
79+
// )
80+
// }
81+
// // 1. grab the website content
82+
// await reportProgress("Loading \(url)..")
83+
// print("== load \(url)")
84+
// let loader = WebLoader(urls: [url])
85+
// let documents = try await loader.load()
86+
// await reportProgress("Processing \(url)..")
87+
// print("== loaded \(url), documents: \(documents.count)")
88+
// // 2. split the content
89+
// let splitter = RecursiveCharacterTextSplitter(
90+
// chunkSize: 1000,
91+
// chunkOverlap: 100
92+
// )
93+
// let splitDocuments = try await splitter.transformDocuments(documents)
94+
// print("== split \(url), documents: \(splitDocuments.count)")
95+
// // 3. embedding and store in db
96+
// await reportProgress("Embedding \(url)..")
97+
// let embeddedDocuments = try await embedding.embed(documents: splitDocuments)
98+
// print("== embedded \(url)")
99+
// let database = TemporaryUSearch(identifier: urlString)
100+
// try await database.set(embeddedDocuments)
101+
// print("== save to database \(url)")
102+
// let result = try await database.searchWithDistance(
103+
// embeddings: queryEmbeddings,
104+
// count: searchCount
105+
// )
106+
// print("== result of \(url): \(result)")
107+
// return result
108+
// }
109+
// }
110+
//
111+
// var all = [(document: Document, distance: Float)]()
112+
// for try await result in group {
113+
// all.append(contentsOf: result)
114+
// }
115+
// await reportProgress("Finish reading websites.")
116+
// return all
117+
// .sorted { $0.distance < $1.distance }
118+
// .prefix(searchCount)
119+
// }
120+
//
121+
// return .init(relevantDocuments: result.map(\.document))
122+
// } catch {
123+
// await reportProgress("Failed reading websites.")
124+
// throw error
125+
// }
125126
}
126127
}
127128

Core/Sources/ChatService/ChatService.swift

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ChatPlugin
33
import Combine
44
import Foundation
55
import OpenAIService
6+
import Preferences
67

78
public final class ChatService: ObservableObject {
89
public let memory: AutoManagedChatGPTMemory
@@ -89,10 +90,10 @@ public final class ChatService: ObservableObject {
8990
await pluginController.stopResponding()
9091
await chatGPTService.stopReceivingMessage()
9192
isReceivingMessage = false
92-
93+
9394
// if it's stopped before the function finishes, remove the function call.
9495
await memory.mutateHistory { history in
95-
if history.last?.role == .assistant && history.last?.functionCall != nil {
96+
if history.last?.role == .assistant, history.last?.functionCall != nil {
9697
history.removeLast()
9798
}
9899
}
@@ -149,5 +150,62 @@ public final class ChatService: ObservableObject {
149150
public func mutateHistory(_ mutator: @escaping (inout [ChatMessage]) -> Void) async {
150151
await memory.mutateHistory(mutator)
151152
}
153+
154+
public func handleCustomCommand(_ command: CustomCommand) async throws {
155+
struct CustomCommandInfo {
156+
var specifiedSystemPrompt: String?
157+
var extraSystemPrompt: String?
158+
var sendingMessageImmediately: String?
159+
var name: String?
160+
}
161+
162+
let info: CustomCommandInfo? = {
163+
switch command.feature {
164+
case let .chatWithSelection(extraSystemPrompt, prompt, useExtraSystemPrompt):
165+
let updatePrompt = useExtraSystemPrompt ?? true
166+
return .init(
167+
extraSystemPrompt: updatePrompt ? extraSystemPrompt : nil,
168+
sendingMessageImmediately: prompt,
169+
name: command.name
170+
)
171+
case let .customChat(systemPrompt, prompt):
172+
return .init(
173+
specifiedSystemPrompt: systemPrompt,
174+
extraSystemPrompt: "",
175+
sendingMessageImmediately: prompt,
176+
name: command.name
177+
)
178+
case .promptToCode:
179+
return nil
180+
}
181+
}()
182+
183+
guard let info else { return }
184+
185+
let templateProcessor = CustomCommandTemplateProcessor()
186+
mutateSystemPrompt(info.specifiedSystemPrompt.map(templateProcessor.process))
187+
mutateExtraSystemPrompt(info.extraSystemPrompt.map(templateProcessor.process) ?? "")
188+
189+
let customCommandPrefix = {
190+
if let name = info.name { return "[\(name)] " }
191+
return ""
192+
}()
193+
194+
if info.specifiedSystemPrompt != nil || info.extraSystemPrompt != nil {
195+
await mutateHistory { history in
196+
history.append(.init(
197+
role: .assistant,
198+
content: "",
199+
summary: "\(customCommandPrefix)System prompt is updated."
200+
))
201+
}
202+
}
203+
204+
if let sendingMessageImmediately = info.sendingMessageImmediately,
205+
!sendingMessageImmediately.isEmpty
206+
{
207+
try await send(content: templateProcessor.process(sendingMessageImmediately))
208+
}
209+
}
152210
}
153211

Core/Sources/Service/CustomCommandTemplateProcessor.swift renamed to Core/Sources/ChatService/CustomCommandTemplateProcessor.swift

File renamed without changes.

Core/Sources/Service/GUI/ChatProvider+Service.swift renamed to Core/Sources/ChatTab/ChatGPT/ChatGPTChatTab.swift

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
import ChatService
2-
import ChatTab
32
import Combine
43
import Foundation
5-
import OpenAIService
6-
import SuggestionWidget
4+
import SwiftUI
5+
6+
/// A chat tab that provides a context aware chat bot, powered by ChatGPT.
7+
public class ChatGPTChatTab: ChatTab {
8+
public let service: ChatService
9+
public let provider: ChatProvider
10+
private var cancellable = Set<AnyCancellable>()
11+
12+
public func buildView() -> any View {
13+
ChatPanel(chat: provider)
14+
}
15+
16+
public init(service: ChatService = .init()) {
17+
self.service = service
18+
provider = .init(service: service)
19+
super.init(id: "Chat-" + provider.id.uuidString, title: "Chat")
20+
21+
provider.$history.sink { [weak self] _ in
22+
if let title = self?.provider.title {
23+
self?.title = title
24+
}
25+
}.store(in: &cancellable)
26+
}
27+
}
728

829
extension ChatProvider {
9-
convenience init(
10-
service: ChatService,
11-
fileURL: URL,
12-
onCloseChat: @escaping () -> Void,
13-
onSwitchContext: @escaping () -> Void
14-
) {
30+
convenience init(service: ChatService) {
1531
self.init(pluginIdentifiers: service.allPluginCommands)
1632

1733
let cancellable = service.objectWillChange.sink { [weak self] in
@@ -46,11 +62,7 @@ extension ChatProvider {
4662
onMessageSend = { [cancellable] message in
4763
_ = cancellable
4864
Task {
49-
do {
50-
_ = try await service.send(content: message)
51-
} catch {
52-
PresentInWindowSuggestionPresenter().presentError(error)
53-
}
65+
try await service.send(content: message)
5466
}
5567
}
5668
onStop = {
@@ -65,17 +77,6 @@ extension ChatProvider {
6577
}
6678
}
6779

68-
onClose = {
69-
Task {
70-
await service.stopReceivingMessage()
71-
onCloseChat()
72-
}
73-
}
74-
75-
self.onSwitchContext = {
76-
onSwitchContext()
77-
}
78-
7980
onDeleteMessage = { id in
8081
Task {
8182
await service.deleteMessage(id: id)
@@ -84,11 +85,7 @@ extension ChatProvider {
8485

8586
onResendMessage = { id in
8687
Task {
87-
do {
88-
try await service.resendMessage(id: id)
89-
} catch {
90-
PresentInWindowSuggestionPresenter().presentError(error)
91-
}
88+
try await service.resendMessage(id: id)
9289
}
9390
}
9491

@@ -100,8 +97,7 @@ extension ChatProvider {
10097

10198
onRunCustomCommand = { command in
10299
Task {
103-
let commandHandler = PseudoCommandHandler()
104-
await commandHandler.handleCustomCommand(command)
100+
try await service.handleCustomCommand(command)
105101
}
106102
}
107103

0 commit comments

Comments
 (0)