Skip to content

Commit 81cae6b

Browse files
committed
Merge branch 'feature/chat-service-to-support-new-api' into develop
2 parents beba1ef + 013fc9e commit 81cae6b

16 files changed

Lines changed: 466 additions & 66 deletions

File tree

Core/Package.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,16 @@ let package = Package(
184184
"MathChatPlugin",
185185
"SearchChatPlugin",
186186
"ShortcutChatPlugin",
187-
187+
188+
// context collectors
189+
"WebChatContextCollector",
190+
191+
.product(name: "Parsing", package: "swift-parsing"),
188192
.product(name: "OpenAIService", package: "Tool"),
189193
.product(name: "Preferences", package: "Tool"),
190194
]
191195
),
196+
.testTarget(name: "ChatServiceTests", dependencies: ["ChatService"]),
192197
.target(
193198
name: "ChatPlugin",
194199
dependencies: [
@@ -313,6 +318,7 @@ let package = Package(
313318
"ChatPlugin",
314319
.product(name: "OpenAIService", package: "Tool"),
315320
.product(name: "LangChain", package: "Tool"),
321+
.product(name: "ExternalServices", package: "Tool"),
316322
.product(name: "PythonKit", package: "PythonKit"),
317323
],
318324
path: "Sources/ChatPlugins/SearchChatPlugin"
@@ -327,6 +333,19 @@ let package = Package(
327333
],
328334
path: "Sources/ChatPlugins/ShortcutChatPlugin"
329335
),
336+
337+
// MAKR: - Chat Context Collector
338+
339+
.target(
340+
name: "WebChatContextCollector",
341+
dependencies: [
342+
"ChatContextCollector",
343+
.product(name: "OpenAIService", package: "Tool"),
344+
.product(name: "ExternalServices", package: "Tool"),
345+
.product(name: "Preferences", package: "Tool"),
346+
],
347+
path: "Sources/ChatContextCollectors/WebChatContextCollector"
348+
)
330349
]
331350
)
332351

Core/Sources/ChatContextCollector/ActiveDocumentChatContextCollector.swift

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import XcodeInspector
77
public struct ActiveDocumentChatContextCollector: ChatContextCollector {
88
public init() {}
99

10-
public func generateSystemPrompt(history: [ChatMessage], content prompt: String) -> String {
10+
public func generateContext(
11+
history: [ChatMessage],
12+
scopes: Set<String>,
13+
content: String
14+
) -> ChatContext? {
1115
let content = getEditorInformation()
1216
let relativePath = content.documentURL.path
1317
.replacingOccurrences(of: content.projectURL.path, with: "")
1418
let selectionRange = content.editorContent?.selections.first ?? .outOfScope
1519
let editorContent = {
16-
if prompt.hasPrefix("@file") {
20+
if scopes.contains("file") {
1721
return """
1822
File Content:```\(content.language.rawValue)
1923
\(content.editorContent?.content ?? "")
@@ -53,7 +57,7 @@ public struct ActiveDocumentChatContextCollector: ChatContextCollector {
5357
"""
5458
}
5559

56-
if prompt.hasPrefix("@selection") {
60+
if scopes.contains("selection") {
5761
return """
5862
Selected Code \
5963
(start from line \(selectionRange.start.line)):```\(content.language.rawValue)
@@ -72,27 +76,30 @@ public struct ActiveDocumentChatContextCollector: ChatContextCollector {
7276
"""
7377
}()
7478

75-
return """
76-
Active Document Context:###
77-
Document Relative Path: \(relativePath)
78-
Selection Range Start: \
79-
Line \(selectionRange.start.line) \
80-
Character \(selectionRange.start.character)
81-
Selection Range End: \
82-
Line \(selectionRange.end.line) \
83-
Character \(selectionRange.end.character)
84-
Cursor Position: \
85-
Line \(selectionRange.end.line) \
86-
Character \(selectionRange.end.character)
87-
\(editorContent)
88-
Line Annotations:
89-
\(
90-
content.editorContent?.lineAnnotations
91-
.map { " - \($0)" }
92-
.joined(separator: "\n") ?? "N/A"
79+
return .init(
80+
systemPrompt: """
81+
Active Document Context:###
82+
Document Relative Path: \(relativePath)
83+
Selection Range Start: \
84+
Line \(selectionRange.start.line) \
85+
Character \(selectionRange.start.character)
86+
Selection Range End: \
87+
Line \(selectionRange.end.line) \
88+
Character \(selectionRange.end.character)
89+
Cursor Position: \
90+
Line \(selectionRange.end.line) \
91+
Character \(selectionRange.end.character)
92+
\(editorContent)
93+
Line Annotations:
94+
\(
95+
content.editorContent?.lineAnnotations
96+
.map { " - \($0)" }
97+
.joined(separator: "\n") ?? "N/A"
98+
)
99+
###
100+
""",
101+
functions: []
93102
)
94-
###
95-
"""
96103
}
97104
}
98105

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import Foundation
22
import OpenAIService
33

4+
public struct ChatContext {
5+
public var systemPrompt: String
6+
public var functions: [any ChatGPTFunction]
7+
public init(systemPrompt: String, functions: [any ChatGPTFunction]) {
8+
self.systemPrompt = systemPrompt
9+
self.functions = functions
10+
}
11+
}
12+
413
public protocol ChatContextCollector {
5-
func generateSystemPrompt(history: [ChatMessage], content: String) -> String
14+
func generateContext(
15+
history: [ChatMessage],
16+
scopes: Set<String>,
17+
content: String
18+
) -> ChatContext?
619
}
720

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import BingSearchService
2+
import ChatContextCollector
3+
import Foundation
4+
import OpenAIService
5+
import Preferences
6+
import SuggestionModel
7+
8+
public struct WebChatContextCollector: ChatContextCollector {
9+
public init() {}
10+
11+
public func generateContext(
12+
history: [ChatMessage],
13+
scopes: Set<String>,
14+
content: String
15+
) -> ChatContext? {
16+
guard scopes.contains("web") else { return nil }
17+
return .init(
18+
systemPrompt: "You prefer to answer questions with latest content on the internet.",
19+
functions: [
20+
SearchFunction(),
21+
]
22+
)
23+
}
24+
25+
struct SearchFunction: ChatGPTFunction {
26+
static let dateFormatter = {
27+
let it = DateFormatter()
28+
it.dateFormat = "yyyy-MM-dd"
29+
return it
30+
}()
31+
32+
struct Arguments: Codable {
33+
var query: String
34+
var freshness: String?
35+
}
36+
37+
struct Result: ChatGPTFunctionResult {
38+
var result: BingSearchResult
39+
40+
var botReadableContent: String {
41+
result.webPages.value.enumerated().map {
42+
let (index, page) = $0
43+
return """
44+
\(index + 1). \(page.name) \(page.url)
45+
\(page.snippet)
46+
"""
47+
}.joined(separator: "\n")
48+
}
49+
}
50+
51+
var name: String {
52+
"searchWeb"
53+
}
54+
55+
var description: String {
56+
"Useful for when you need to answer questions about latest information."
57+
}
58+
59+
var argumentSchema: JSONSchemaValue {
60+
let today = Self.dateFormatter.string(from: Date())
61+
return [
62+
.type: "object",
63+
.properties: [
64+
"query": [
65+
.type: "string",
66+
.description: "the search query",
67+
],
68+
"freshness": [
69+
.type: "string",
70+
.description: .string(
71+
"limit the search result to a specific range, use only when user ask the question about current events. Today is \(today). Format: yyyy-MM-dd..yyyy-MM-dd"
72+
),
73+
.examples: ["1919-10-20..1988-10-20"],
74+
],
75+
],
76+
.required: ["query"],
77+
]
78+
}
79+
80+
func message(at phase: ChatGPTFunctionCallPhase) -> String {
81+
func parseArgument(_ string: String) throws -> Arguments {
82+
try JSONDecoder().decode(Arguments.self, from: string.data(using: .utf8) ?? Data())
83+
}
84+
85+
switch phase {
86+
case .detected:
87+
return "Searching.."
88+
case let .processing(argumentsJsonString):
89+
do {
90+
let arguments = try parseArgument(argumentsJsonString)
91+
return "Searching \(arguments.query)"
92+
} catch {
93+
return "Searching.."
94+
}
95+
case let .ended(argumentsJsonString, result):
96+
do {
97+
let arguments = try parseArgument(argumentsJsonString)
98+
if let result = result as? Result {
99+
return """
100+
Finish searching \(arguments.query)
101+
\(
102+
result.result.webPages.value
103+
.map { "- [\($0.name)](\($0.url))" }
104+
.joined(separator: "\n")
105+
)
106+
"""
107+
}
108+
return "Finish searching \(arguments.query)"
109+
} catch {
110+
return "Finish searching"
111+
}
112+
case let .error(argumentsJsonString, _):
113+
do {
114+
let arguments = try parseArgument(argumentsJsonString)
115+
return "Failed searching \(arguments.query)"
116+
} catch {
117+
return "Failed searching"
118+
}
119+
}
120+
}
121+
122+
func call(arguments: Arguments) async throws -> Result {
123+
let bingSearch = BingSearchService(
124+
subscriptionKey: UserDefaults.shared.value(for: \.bingSearchSubscriptionKey),
125+
searchURL: UserDefaults.shared.value(for: \.bingSearchEndpoint)
126+
)
127+
let result = try await bingSearch.search(
128+
query: arguments.query,
129+
numberOfResult: UserDefaults.shared.value(for: \.chatGPTMaxToken) > 5000 ? 5 : 3,
130+
freshness: arguments.freshness
131+
)
132+
133+
let content = result.webPages.value.enumerated().map {
134+
let (index, page) = $0
135+
return """
136+
\(index + 1). \(page.name) \(page.url)
137+
\(page.snippet)
138+
"""
139+
}.joined(separator: "\n")
140+
141+
return .init(result: result)
142+
}
143+
}
144+
}
145+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import ChatContextCollector
2+
import WebChatContextCollector
3+
4+
let allContextCollectors: [any ChatContextCollector] = [
5+
ActiveDocumentChatContextCollector(),
6+
WebChatContextCollector(),
7+
]
8+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
import OpenAIService
3+
4+
final class ChatFunctionProvider {
5+
var functions: [any ChatGPTFunction] = []
6+
7+
init() {}
8+
9+
func removeAll() {
10+
functions = []
11+
}
12+
13+
func append(functions others: [any ChatGPTFunction]) {
14+
functions.append(contentsOf: others)
15+
}
16+
}
17+
18+
extension ChatFunctionProvider: ChatGPTFunctionProvider {}
19+

Core/Sources/ChatService/ChatService.swift

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,51 @@ public final class ChatService: ObservableObject {
1616

1717
let pluginController: ChatPluginController
1818
let contextController: DynamicContextController
19+
let functionProvider: ChatFunctionProvider
1920
var cancellable = Set<AnyCancellable>()
2021

2122
init<T: ChatGPTServiceType>(
2223
memory: AutoManagedChatGPTMemory,
2324
configuration: OverridingChatGPTConfiguration<UserPreferenceChatGPTConfiguration>,
25+
functionProvider: ChatFunctionProvider,
2426
chatGPTService: T
2527
) {
2628
self.memory = memory
2729
self.configuration = configuration
2830
self.chatGPTService = chatGPTService
29-
pluginController = ChatPluginController(chatGPTService: chatGPTService, plugins: allPlugins)
31+
self.functionProvider = functionProvider
32+
pluginController = ChatPluginController(
33+
chatGPTService: chatGPTService,
34+
plugins: allPlugins
35+
)
3036
contextController = DynamicContextController(
3137
memory: memory,
32-
contextCollectors: ActiveDocumentChatContextCollector()
38+
functionProvider: functionProvider,
39+
contextCollectors: allContextCollectors
3340
)
3441

3542
pluginController.chatService = self
3643
}
3744

38-
public init() {
39-
configuration = UserPreferenceChatGPTConfiguration().overriding()
40-
memory = AutoManagedChatGPTMemory(systemPrompt: "", configuration: configuration)
41-
chatGPTService = ChatGPTService(memory: memory, configuration: configuration)
42-
pluginController = ChatPluginController(chatGPTService: chatGPTService, plugins: allPlugins)
43-
contextController = DynamicContextController(
45+
public convenience init() {
46+
let configuration = UserPreferenceChatGPTConfiguration().overriding()
47+
let functionProvider = ChatFunctionProvider()
48+
let memory = AutoManagedChatGPTMemory(
49+
systemPrompt: "",
50+
configuration: configuration,
51+
functionProvider: functionProvider
52+
)
53+
self.init(
4454
memory: memory,
45-
contextCollectors: ActiveDocumentChatContextCollector()
55+
configuration: configuration,
56+
functionProvider: functionProvider,
57+
chatGPTService: ChatGPTService(
58+
memory: memory,
59+
configuration: configuration,
60+
functionProvider: functionProvider
61+
)
4662
)
4763

48-
pluginController.chatService = self
4964
memory.observeHistoryChange { [weak self] in
5065
self?.objectWillChange.send()
5166
}

0 commit comments

Comments
 (0)