Skip to content

Commit 8f94fd6

Browse files
committed
Add naive implementation of GitHubCopilotChatService
1 parent 9fd0312 commit 8f94fd6

5 files changed

Lines changed: 317 additions & 2 deletions

File tree

Tool/Sources/BuiltinExtension/BuiltinExtension.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ChatContextCollector
12
import ChatTab
23
import CopilotForXcodeKit
34
import Foundation
@@ -23,3 +24,29 @@ public extension BuiltinExtension {
2324
var chatTabTypes: [any ChatTab.Type] { [] }
2425
}
2526

27+
// MAKR: - ChatService
28+
29+
/// A temporary protocol for ChatServiceType. Migrate it to CopilotForXcodeKit when finished.
30+
public protocol BuiltinExtensionChatServiceType: ChatServiceType {
31+
typealias Message = ChatServiceMessage
32+
typealias RetrievedContent = ChatContext.RetrievedContent
33+
}
34+
35+
public struct ChatServiceMessage: Codable {
36+
public enum Role: Codable, Equatable {
37+
case system
38+
case user
39+
case assistant
40+
case tool
41+
case other(String)
42+
}
43+
44+
public var role: Role
45+
public var text: String
46+
47+
public init(role: Role, text: String) {
48+
self.role = role
49+
self.text = text
50+
}
51+
}
52+

Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public final class GitHubCopilotExtension: BuiltinExtension {
1212
public var suggestionServiceId: Preferences.BuiltInSuggestionFeatureProvider { .gitHubCopilot }
1313

1414
public let suggestionService: GitHubCopilotSuggestionService?
15+
public let chatService: GitHubCopilotChatService?
1516

1617
private var extensionUsage = ExtensionUsage(
1718
isSuggestionServiceInUse: false,
@@ -29,6 +30,7 @@ public final class GitHubCopilotExtension: BuiltinExtension {
2930
self.workspacePool = workspacePool
3031
serviceLocator = ServiceLocator(workspacePool: workspacePool)
3132
suggestionService = .init(serviceLocator: serviceLocator)
33+
chatService = .init(serviceLocator: serviceLocator)
3234
}
3335

3436
public func workspaceDidOpen(_: WorkspaceInfo) {}

Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public struct GitHubCopilotCodeSuggestion: Codable, Equatable {
4949
public var displayText: String
5050
}
5151

52+
enum GitHubCopilotChatSource: String, Codable {
53+
case panel
54+
case inline
55+
}
56+
5257
enum GitHubCopilotRequest {
5358
struct SetEditorInfo: GitHubCopilotRequestType {
5459
struct Response: Codable {}
@@ -330,5 +335,126 @@ enum GitHubCopilotRequest {
330335
]))
331336
}
332337
}
338+
339+
struct ConversationCreate: GitHubCopilotRequestType {
340+
struct Response: Codable {
341+
var conversationId: String
342+
var turnId: String
343+
}
344+
345+
struct RequestBody: Codable {
346+
var workDoneToken: String
347+
var turns: [Turn]; struct Turn: Codable {
348+
var request: String
349+
var response: String?
350+
}
351+
352+
var capabilities: [Capabilities]; struct Capabilities: Codable {
353+
var allSkills: Bool?
354+
var skills: [String]
355+
}
356+
357+
var options: [String: String]?
358+
var doc: GitHubCopilotDoc?
359+
var computeSuggestions: Bool?
360+
var references: [Reference]?; struct Reference: Codable {
361+
var uri: String
362+
var position: Position?
363+
var visibleRange: CursorRange?
364+
var selectionRange: CursorRange?
365+
var openedAt: Date?
366+
var activatedAt: Date?
367+
}
368+
369+
var source: GitHubCopilotChatSource? // inline or panel
370+
var workspaceFolder: String?
371+
}
372+
373+
let requestBody: RequestBody
374+
375+
var request: ClientRequest {
376+
let data = (try? JSONEncoder().encode(requestBody)) ?? Data()
377+
let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:])
378+
return .custom("conversation/create", .hash([
379+
"doc": dict,
380+
]))
381+
}
382+
}
383+
384+
struct ConversationTurn: GitHubCopilotRequestType {
385+
struct Response: Codable {}
386+
387+
struct RequestBody: Codable {
388+
var workDoneToken: String
389+
var conversationId: String
390+
var message: String
391+
var followUp: FollowUp?; struct FollowUp: Codable {
392+
var id: String
393+
var type: String
394+
}
395+
396+
var options: [String: String]?
397+
var doc: GitHubCopilotDoc?
398+
var computeSuggestions: Bool?
399+
var references: [Reference]?; struct Reference: Codable {
400+
var uri: String
401+
var position: Position?
402+
var visibleRange: CursorRange?
403+
var selectionRange: CursorRange?
404+
var openedAt: Date?
405+
var activatedAt: Date?
406+
}
407+
408+
var workspaceFolder: String?
409+
}
410+
411+
let requestBody: RequestBody
412+
413+
var request: ClientRequest {
414+
let data = (try? JSONEncoder().encode(requestBody)) ?? Data()
415+
let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:])
416+
return .custom("conversation/turn", .hash([
417+
"doc": dict,
418+
]))
419+
}
420+
}
421+
422+
struct ConversationTurnDelete: GitHubCopilotRequestType {
423+
struct Response: Codable {}
424+
425+
struct RequestBody: Codable {
426+
var conversationId: String
427+
var turnId: String
428+
var options: [String: String]?
429+
var source: GitHubCopilotChatSource?
430+
}
431+
432+
let requestBody: RequestBody
433+
434+
var request: ClientRequest {
435+
let data = (try? JSONEncoder().encode(requestBody)) ?? Data()
436+
let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:])
437+
return .custom("conversation/turnDelete", .hash([
438+
"doc": dict,
439+
]))
440+
}
441+
}
442+
443+
struct ConversationDestroy: GitHubCopilotRequestType {
444+
struct Response: Codable {}
445+
446+
struct RequestBody: Codable {
447+
var conversationId: String
448+
var options: [String: String]?
449+
}
450+
451+
let requestBody: RequestBody
452+
453+
var request: ClientRequest {
454+
let data = (try? JSONEncoder().encode(requestBody)) ?? Data()
455+
let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:])
456+
return .custom("conversation/destroy", dict)
457+
}
458+
}
333459
}
334460

Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public class GitHubCopilotBaseService {
9696
let projectRootURL: URL
9797
var server: GitHubCopilotLSP
9898
var localProcessServer: CopilotLocalProcessServer?
99+
@GitHubCopilotSuggestionActor
100+
private var serverNotificationHandlers =
101+
[AnyHashable: (ServerNotification) async throws -> Bool]()
99102

100103
deinit {
101104
localProcessServer?.terminate()
@@ -199,8 +202,22 @@ public class GitHubCopilotBaseService {
199202
let localServer = CopilotLocalProcessServer(executionParameters: executionParams)
200203

201204
localServer.logMessages = UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog)
202-
localServer.notificationHandler = { notification, respond in
203-
respond(.handlerUnavailable(notification.method.rawValue))
205+
localServer.notificationHandler = { [weak self] notification, respond in
206+
Task {
207+
for handler in self?.serverNotificationHandlers.values ?? [] {
208+
do {
209+
let handled = try await handler(notification)
210+
if handled {
211+
respond(nil)
212+
return
213+
}
214+
} catch {
215+
respond(.failure(error))
216+
return
217+
}
218+
}
219+
respond(.handlerUnavailable(notification.method.rawValue))
220+
}
204221
}
205222
let server = InitializingServer(server: localServer)
206223

@@ -286,6 +303,21 @@ public class GitHubCopilotBaseService {
286303

287304
return (supportURL, gitHubCopilotFolderURL, executableFolderURL, supportFolderURL)
288305
}
306+
307+
func registerNotificationHandler(
308+
id: AnyHashable,
309+
_ block: @escaping (ServerNotification) async throws -> Bool
310+
) {
311+
Task { @GitHubCopilotSuggestionActor in
312+
self.serverNotificationHandlers[id] = block
313+
}
314+
}
315+
316+
func unregisterNotificationHandler(id: AnyHashable) {
317+
Task { @GitHubCopilotSuggestionActor in
318+
self.serverNotificationHandlers[id] = nil
319+
}
320+
}
289321
}
290322

291323
public final class GitHubCopilotAuthService: GitHubCopilotBaseService,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import BuiltinExtension
2+
import CopilotForXcodeKit
3+
import Foundation
4+
import XcodeInspector
5+
6+
public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType {
7+
let serviceLocator: any ServiceLocatorType
8+
9+
init(serviceLocator: any ServiceLocatorType) {
10+
self.serviceLocator = serviceLocator
11+
}
12+
13+
/// - note: Let's do it in a naive way for proof of concept. We will create a new chat for each
14+
/// message in this version.
15+
public func sendMessage(
16+
_ message: String,
17+
history: [Message],
18+
references: [RetrievedContent],
19+
workspace: WorkspaceInfo
20+
) async -> AsyncThrowingStream<String, Error> {
21+
guard let service = await serviceLocator.getService(from: workspace)
22+
else { return .finished(throwing: CancellationError()) }
23+
let id = UUID().uuidString
24+
let editorContent = await XcodeInspector.shared.getFocusedEditorContent()
25+
do {
26+
let createResponse = try await service.server
27+
.sendRequest(GitHubCopilotRequest.ConversationCreate(requestBody: .init(
28+
workDoneToken: "",
29+
turns: convertHistory(history: history),
30+
capabilities: [
31+
.init(allSkills: true, skills: []),
32+
],
33+
doc: .init(
34+
source: editorContent?.editorContent?.content ?? "",
35+
tabSize: 1,
36+
indentSize: 4,
37+
insertSpaces: true,
38+
path: editorContent?.documentURL.path ?? "",
39+
uri: editorContent?.documentURL.path ?? "",
40+
relativePath: editorContent?.relativePath ?? "",
41+
languageId: editorContent?.language.rawValue ?? "plaintext",
42+
position: .zero
43+
),
44+
source: .panel,
45+
workspaceFolder: workspace.projectURL.path
46+
)))
47+
48+
let stream = AsyncThrowingStream<String, Error>.init { continuation in
49+
service.registerNotificationHandler(id: id) { notification in
50+
if notification.method.rawValue == "" {
51+
return true
52+
}
53+
54+
return false
55+
}
56+
57+
continuation.onTermination = { _ in
58+
Task {
59+
try await service.server.sendRequest(
60+
GitHubCopilotRequest.ConversationDestroy(requestBody: .init(
61+
conversationId: createResponse.conversationId
62+
))
63+
)
64+
}
65+
}
66+
}
67+
68+
_ = try await service.server
69+
.sendRequest(GitHubCopilotRequest.ConversationTurn(requestBody: .init(
70+
workDoneToken: "",
71+
conversationId: createResponse.conversationId,
72+
message: message
73+
)))
74+
75+
return stream
76+
} catch {
77+
return .finished(throwing: error)
78+
}
79+
}
80+
}
81+
82+
extension GitHubCopilotChatService {
83+
typealias Turn = GitHubCopilotRequest.ConversationCreate.RequestBody.Turn
84+
func convertHistory(history: [Message]) -> [Turn] {
85+
guard let firstIndexOfUserMessage = history.firstIndex(where: { $0.role == .user })
86+
else { return [] }
87+
88+
var currentTurn = Turn(request: "", response: nil)
89+
var turns: [Turn] = []
90+
for i in firstIndexOfUserMessage..<history.endIndex {
91+
let message = history[i]
92+
switch message.role {
93+
case .user:
94+
if currentTurn.response == nil {
95+
if currentTurn.request.isEmpty {
96+
currentTurn.request = message.text
97+
} else {
98+
currentTurn.request += "\n\n\(message.text)"
99+
}
100+
} else { // a valid turn is created
101+
turns.append(currentTurn)
102+
currentTurn = Turn(request: message.text, response: nil)
103+
}
104+
case .assistant:
105+
if let response = currentTurn.response {
106+
currentTurn.response = "\(response)\n\n\(message.text)"
107+
} else {
108+
currentTurn.response = message.text
109+
}
110+
default:
111+
break
112+
}
113+
}
114+
115+
if currentTurn.response == nil {
116+
currentTurn.response = "OK"
117+
}
118+
119+
turns.append(currentTurn)
120+
121+
return turns
122+
}
123+
124+
func createNewMessage(references: [RetrievedContent], message: String) -> String {
125+
return message
126+
}
127+
}
128+

0 commit comments

Comments
 (0)