Skip to content

Commit df8a478

Browse files
committed
Merge branch 'feature/re-implement-math-plugin-in-swift' into develop
2 parents b32c782 + 3f979b6 commit df8a478

13 files changed

Lines changed: 277 additions & 114 deletions

File tree

Core/Package.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ let package = Package(
182182

183183
// plugins
184184
"MathChatPlugin",
185+
"SearchChatPlugin",
185186

186187
.product(name: "Preferences", package: "Tool"),
187188
]
@@ -319,6 +320,17 @@ let package = Package(
319320
],
320321
path: "Sources/ChatPlugins/MathChatPlugin"
321322
),
323+
324+
.target(
325+
name: "SearchChatPlugin",
326+
dependencies: [
327+
"ChatPlugin",
328+
"OpenAIService",
329+
.product(name: "LangChain", package: "Tool"),
330+
.product(name: "PythonKit", package: "PythonKit"),
331+
],
332+
path: "Sources/ChatPlugins/SearchChatPlugin"
333+
),
322334
]
323335
)
324336

Core/Sources/ChatPlugin/AskChatGPT.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import Foundation
22
import OpenAIService
33

44
/// Quickly ask a question to ChatGPT.
5-
func askChatGPT(systemPrompt: String, question: String) async throws -> String? {
6-
let service = ChatGPTService(systemPrompt: systemPrompt)
5+
public func askChatGPT(
6+
systemPrompt: String,
7+
question: String,
8+
temperature: Double? = nil
9+
) async throws -> String? {
10+
let service = ChatGPTService(systemPrompt: systemPrompt, temperature: temperature)
711
return try await service.sendAndWait(content: question)
812
}

Core/Sources/ChatPlugin/SearchChatPlugin.swift

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import Preferences
3+
4+
@MainActor
5+
var translationCache = [String: String]()
6+
7+
public func translate(text: String, cache: Bool = true) async -> String {
8+
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
9+
if language.isEmpty { return text }
10+
11+
let key = "\(language)-\(text)"
12+
if cache, let cached = await translationCache[key] {
13+
return cached
14+
}
15+
16+
if let translated = try? await askChatGPT(
17+
systemPrompt: """
18+
You are a translator. Your job is to translate the message into \(language). The reply should only contain the translated content.
19+
User: ###${{some text}}###
20+
Assistant: ${{translated text}}
21+
""",
22+
question: "###\(text)###"
23+
) {
24+
if cache {
25+
let storeTask = Task { @MainActor in
26+
translationCache[key] = translated
27+
}
28+
_ = await storeTask.result
29+
}
30+
return translated
31+
}
32+
return text
33+
}
34+

Core/Sources/ChatPlugins/MathChatPlugin/MathChatPlugin.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ public actor MathChatPlugin: ChatPlugin {
2222
delegate?.pluginDidStartResponding(self)
2323

2424
let id = "\(Self.command)-\(UUID().uuidString)"
25-
var reply = ChatMessage(id: id, role: .assistant, content: "Calculating...")
25+
async let translatedCalculating = translate(text: "Calculating...")
26+
async let translatedAnswer = translate(text: "Answer:")
27+
var reply = ChatMessage(id: id, role: .assistant, content: await translatedCalculating)
2628

2729
await chatGPTService.mutateHistory { history in
2830
history.append(.init(role: .user, content: originalMessage, summary: content))
@@ -31,11 +33,12 @@ public actor MathChatPlugin: ChatPlugin {
3133

3234
do {
3335
let result = try await solveMathProblem(content)
36+
let formattedResult = "\(await translatedAnswer) \(result)"
3437
await chatGPTService.mutateHistory { history in
3538
if history.last?.id == id {
3639
history.removeLast()
3740
}
38-
reply.content = result
41+
reply.content = formattedResult
3942
history.append(reply)
4043
}
4144
} catch {
Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,100 @@
1+
import ChatPlugin
12
import Foundation
23
import LangChain
3-
import PythonHelper
4-
import PythonKit
5-
6-
func solveMathProblem(_ problem: String) async throws -> String {
7-
#if DEBUG
8-
let verbose = true
9-
#else
10-
let verbose = false
11-
#endif
12-
13-
struct E: Error, LocalizedError {
14-
var errorDescription: String? {
15-
"Failed to parse answer."
4+
import Logger
5+
import OpenAIService
6+
7+
let systemPrompt = """
8+
Translate a math problem into a expression that can be executed using Python's numexpr library.
9+
Use the output of running this code to answer the question.
10+
11+
Question: ${{Question with math problem.}}
12+
```text
13+
${{single line mathematical expression that solves the problem}}
14+
```
15+
...numexpr.evaluate(text)...
16+
```output
17+
${{Output of running the code}}
18+
```
19+
Answer: ${{Answer}}
20+
21+
Begin.
22+
23+
Question: What is 37593 * 67?
24+
```text
25+
37593 * 67
26+
```
27+
...numexpr.evaluate("37593 * 67")...
28+
```output
29+
2518731
30+
```
31+
Answer: 2518731
32+
33+
Question: 37593^(1/5)
34+
```text
35+
37593**(1/5)
36+
```
37+
...numexpr.evaluate("37593**(1/5)")...
38+
```output
39+
8.222831614237718
40+
```
41+
Answer: 8.222831614237718
42+
"""
43+
44+
/// Extract the math problem with ChatGPT, and pass it to python to get the result.
45+
///
46+
/// [llm_math in
47+
/// LangChain](https://github.com/hwchase17/langchain/blob/master/langchain/chains/llm_math/base.py)
48+
///
49+
/// The logic is basically the same as the LLMMathChain provided in LangChain.
50+
func solveMathProblem(_ question: String) async throws -> String {
51+
guard let reply = try await askChatGPT(
52+
systemPrompt: systemPrompt,
53+
question: "Question: \(question)",
54+
temperature: 0
55+
) else { return "No answer." }
56+
57+
// parse inside text code block
58+
let codeBlockRegex = try NSRegularExpression(pattern: "```text\n(.*?)\n```", options: [])
59+
let codeBlockMatches = codeBlockRegex.matches(
60+
in: reply,
61+
options: [],
62+
range: NSRange(reply.startIndex..<reply.endIndex, in: reply)
63+
)
64+
if let firstMatch = codeBlockMatches.first, let textRange = Range(
65+
firstMatch.range(at: 1),
66+
in: reply
67+
) {
68+
let text = reply[textRange]
69+
let expression = String(text)
70+
let task = Task { try evaluateWithPython(expression) }
71+
if let answer = try await task.value {
72+
return answer
1673
}
1774
}
1875

19-
let task = Task {
20-
try runPython {
21-
let langchain = try Python.attemptImportOnPythonThread("langchain")
22-
let LLMMathChain = langchain.LLMMathChain
23-
let llm = try LangChainChatModel.DynamicChatOpenAI(temperature: 0)
24-
let llmMath = LLMMathChain.from_llm(llm, verbose: verbose)
25-
let result = try llmMath.run.throwing.dynamicallyCall(withArguments: problem)
26-
let answer = String(result)
27-
if let answer { return answer }
28-
29-
throw E()
30-
}
76+
// parse after Answer:
77+
let answerRegex = try NSRegularExpression(pattern: "Answer: (.*)", options: [])
78+
let answerMatches = answerRegex.matches(
79+
in: reply,
80+
options: [],
81+
range: NSRange(reply.startIndex..<reply.endIndex, in: reply)
82+
)
83+
if let firstMatch = answerMatches.first, let answerRange = Range(
84+
firstMatch.range(at: 1),
85+
in: reply
86+
) {
87+
let answer = reply[answerRange]
88+
return String(answer)
3189
}
32-
33-
return try await task.value
90+
91+
return reply
92+
}
93+
94+
func evaluateWithPython(_ expression: String) throws -> String? {
95+
let mathExpression = NSExpression(format: expression)
96+
let value = mathExpression.expressionValue(with: nil, context: nil)
97+
Logger.service.debug(String(describing: value))
98+
return (value as? Int).flatMap(String.init)
3499
}
35100

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
import LangChain
3+
import PythonHelper
4+
import PythonKit
5+
6+
func search(_ query: String) async throws -> String {
7+
return ""
8+
}
9+
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import ChatPlugin
2+
import Environment
3+
import Foundation
4+
import OpenAIService
5+
6+
public actor SearchChatPlugin: ChatPlugin {
7+
public static var command: String { "search" }
8+
public nonisolated var name: String { "Search" }
9+
10+
let chatGPTService: any ChatGPTServiceType
11+
var isCancelled = false
12+
weak var delegate: ChatPluginDelegate?
13+
14+
public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) {
15+
self.chatGPTService = chatGPTService
16+
self.delegate = delegate
17+
}
18+
19+
public func send(content: String, originalMessage: String) async {
20+
delegate?.pluginDidStart(self)
21+
delegate?.pluginDidStartResponding(self)
22+
23+
let id = "\(Self.command)-\(UUID().uuidString)"
24+
var reply = ChatMessage(id: id, role: .assistant, content: "Calculating...")
25+
26+
await chatGPTService.mutateHistory { history in
27+
history.append(.init(role: .user, content: originalMessage, summary: content))
28+
history.append(reply)
29+
}
30+
31+
do {
32+
let result = try await search(content)
33+
await chatGPTService.mutateHistory { history in
34+
if history.last?.id == id {
35+
history.removeLast()
36+
}
37+
reply.content = result
38+
history.append(reply)
39+
}
40+
} catch {
41+
await chatGPTService.mutateHistory { history in
42+
if history.last?.id == id {
43+
history.removeLast()
44+
}
45+
reply.content = error.localizedDescription
46+
history.append(reply)
47+
}
48+
}
49+
50+
delegate?.pluginDidEndResponding(self)
51+
delegate?.pluginDidEnd(self)
52+
}
53+
54+
public func cancel() async {
55+
isCancelled = true
56+
}
57+
58+
public func stopResponding() async {
59+
isCancelled = true
60+
}
61+
}
62+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import ChatPlugin
22
import MathChatPlugin
3+
import SearchChatPlugin
34

45
let allPlugins: [ChatPlugin.Type] = [
56
TerminalChatPlugin.self,
67
AITerminalChatPlugin.self,
78
MathChatPlugin.self,
9+
SearchChatPlugin.self,
810
]

Core/Sources/GitHubCopilotService/GitHubCopilotService.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public class GitHubCopilotBaseService {
127127
}()
128128
}
129129
let localServer = CopilotLocalProcessServer(executionParameters: executionParams)
130-
130+
131131
localServer.logMessages = UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog)
132132
localServer.notificationHandler = { _, respond in
133133
respond(.timeout)
@@ -162,9 +162,9 @@ public class GitHubCopilotBaseService {
162162

163163
return (server, localServer)
164164
}()
165-
165+
166166
self.server = server
167-
self.localProcessServer = localServer
167+
localProcessServer = localServer
168168
}
169169

170170
public static func createFoldersIfNeeded() throws -> (
@@ -242,6 +242,12 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService,
242242
}
243243
}
244244

245+
@globalActor public enum GitHubCopilotSuggestionActor {
246+
public actor TheActor {}
247+
public static let shared = TheActor()
248+
}
249+
250+
@GitHubCopilotSuggestionActor
245251
public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
246252
GitHubCopilotSuggestionServiceType
247253
{
@@ -313,7 +319,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
313319

314320
return try await task.value
315321
}
316-
322+
317323
public func cancelRequest() async {
318324
await localProcessServer?.cancelOngoingTasks()
319325
}
@@ -380,7 +386,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
380386
// Logger.service.debug("Close \(uri)")
381387
try await server.sendNotification(.didCloseTextDocument(.init(uri: uri)))
382388
}
383-
389+
384390
public func terminate() async {
385391
// automatically handled
386392
}

0 commit comments

Comments
 (0)