Skip to content

Commit 374f2a5

Browse files
committed
Add chat plugin /airun
1 parent e487690 commit 374f2a5

File tree

5 files changed

+161
-3
lines changed

5 files changed

+161
-3
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import Environment
2+
import Foundation
3+
import OpenAIService
4+
import Terminal
5+
6+
public actor AITerminalChatPlugin: ChatPlugin {
7+
public static var command: String { "airun" }
8+
public nonisolated var name: String { "AI Terminal" }
9+
10+
let chatGPTService: any ChatGPTServiceType
11+
var terminal: TerminalType = Terminal()
12+
var isCancelled = false
13+
weak var delegate: ChatPluginDelegate?
14+
var isStarted = false
15+
var command: String?
16+
17+
public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) {
18+
self.chatGPTService = chatGPTService
19+
self.delegate = delegate
20+
}
21+
22+
public func send(content: String) async {
23+
if !isStarted {
24+
isStarted = true
25+
delegate?.pluginDidStart(self)
26+
}
27+
28+
do {
29+
if let command {
30+
await chatGPTService.mutateHistory { history in
31+
history.append(.init(role: .user, content: content))
32+
}
33+
delegate?.pluginDidStartResponding(self)
34+
if try await checkConfirmation(content: content) {
35+
delegate?.pluginDidEndResponding(self)
36+
delegate?.pluginDidEnd(self)
37+
delegate?.shouldStartAnotherPlugin(
38+
TerminalChatPlugin.self,
39+
withContent: command
40+
)
41+
} else {
42+
delegate?.pluginDidEndResponding(self)
43+
delegate?.pluginDidEnd(self)
44+
await chatGPTService.mutateHistory { history in
45+
history.append(.init(role: .assistant, content: "Cancelled"))
46+
}
47+
}
48+
} else {
49+
await chatGPTService.mutateHistory { history in
50+
history.append(.init(role: .user, content: "Run a command to \(content)"))
51+
}
52+
delegate?.pluginDidStartResponding(self)
53+
let result = try await generateCommand(task: content)
54+
command = result
55+
await chatGPTService.mutateHistory { history in
56+
history.append(.init(role: .assistant, content: """
57+
Confirm to run?
58+
```
59+
\(result)
60+
```
61+
"""))
62+
}
63+
delegate?.pluginDidEndResponding(self)
64+
}
65+
} catch {
66+
await chatGPTService.mutateHistory { history in
67+
history.append(.init(role: .assistant, content: error.localizedDescription))
68+
}
69+
delegate?.pluginDidEndResponding(self)
70+
delegate?.pluginDidEnd(self)
71+
}
72+
}
73+
74+
public func cancel() async {
75+
isCancelled = true
76+
}
77+
78+
public func stopResponding() async {}
79+
80+
func callAIFunction(
81+
function: String,
82+
args: [Any?],
83+
description: String
84+
) async throws -> String {
85+
let args = args.map { arg -> String in
86+
if let arg = arg {
87+
return String(describing: arg)
88+
} else {
89+
return "None"
90+
}
91+
}
92+
let argsString = args.joined(separator: ", ")
93+
let service = ChatGPTService(
94+
systemPrompt: "You are now the following python function: ```# \(description)\n\(function)```\n\nOnly respond with your `return` value."
95+
)
96+
return try await service.sendAndWait(content: argsString)
97+
}
98+
99+
func generateCommand(task: String) async throws -> String {
100+
let f = "def generate_terminal_command(task: str) -> string:"
101+
let d = """
102+
Available environment variables:
103+
- $PROJECT_ROOT: the root path of the project
104+
- $FILE_PATH: the currently editing file
105+
106+
Current directory path is the project root.
107+
108+
The return value should not be embedded in a markdown code block.
109+
110+
Generate a terminal command to solve the given task on macOS. If one command is not enough, you can use && to concatenate multiple commands.
111+
"""
112+
113+
return try await callAIFunction(function: f, args: [task], description: d)
114+
.replacingOccurrences(of: "`", with: "")
115+
.replacingOccurrences(of: "\n", with: "")
116+
}
117+
118+
func checkConfirmation(content: String) async throws -> Bool {
119+
let f = "def check_confirmation(content: str) -> bool:"
120+
let d = """
121+
Check if the given content is a phrase or sentence that considered a confirmation to run a command.
122+
123+
For example: "Yes", "Confirm", "True", "Please run it". It can be in any language.
124+
"""
125+
126+
let result = try await callAIFunction(function: f, args: [content], description: d)
127+
return result.lowercased().contains("true")
128+
}
129+
}

Core/Sources/ChatPlugins/ChatPlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ public protocol ChatPluginDelegate: AnyObject {
1717
func pluginDidEnd(_ plugin: ChatPlugin)
1818
func pluginDidStartResponding(_ plugin: ChatPlugin)
1919
func pluginDidEndResponding(_ plugin: ChatPlugin)
20+
func shouldStartAnotherPlugin(_ type: ChatPlugin.Type, withContent: String)
2021
}

Core/Sources/ChatService/ChatService.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import OpenAIService
66
public final class ChatService: ObservableObject {
77
public let chatGPTService: any ChatGPTServiceType
88
let plugins = registerPlugins(
9-
TerminalChatPlugin.self
9+
TerminalChatPlugin.self,
10+
AITerminalChatPlugin.self
1011
)
1112
var runningPlugin: ChatPlugin?
1213
var cancellable = Set<AnyCancellable>()
@@ -27,8 +28,9 @@ public final class ChatService: ObservableObject {
2728
let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content))
2829
if let match = matches.first {
2930
let command = String(content[Range(match.range(at: 1), in: content)!])
30-
if command == "/exit" {
31+
if command == "exit" {
3132
if let plugin = runningPlugin {
33+
runningPlugin = nil
3234
_ = await chatGPTService.mutateHistory { history in
3335
history.append(.init(
3436
role: .user,
@@ -50,18 +52,24 @@ public final class ChatService: ObservableObject {
5052
))
5153
}
5254
}
55+
} else if let runningPlugin {
56+
await runningPlugin.send(content: content)
5357
} else if let pluginType = plugins[command] {
5458
let plugin = pluginType.init(inside: chatGPTService, delegate: self)
5559
await plugin.send(content: String(content.dropFirst(command.count + 1)))
60+
} else {
61+
_ = try await chatGPTService.send(content: content, summary: nil)
5662
}
63+
} else if let runningPlugin {
64+
await runningPlugin.send(content: content)
5765
} else {
5866
_ = try await chatGPTService.send(content: content, summary: nil)
5967
}
6068
}
6169

6270
public func stopReceivingMessage() async {
6371
if let runningPlugin {
64-
await runningPlugin.cancel()
72+
await runningPlugin.stopResponding()
6573
}
6674
await chatGPTService.stopReceivingMessage()
6775
}
@@ -98,6 +106,13 @@ extension ChatService: ChatPluginDelegate {
98106
public func pluginDidEnd(_: ChatPlugin) {
99107
runningPlugin = nil
100108
}
109+
110+
public func shouldStartAnotherPlugin(_ type: ChatPlugin.Type, withContent content: String) {
111+
let plugin = type.init(inside: chatGPTService, delegate: self)
112+
Task {
113+
await plugin.send(content: content)
114+
}
115+
}
101116
}
102117

103118
func registerPlugins(_ plugins: ChatPlugin.Type...) -> [String: ChatPlugin.Type] {

Core/Sources/OpenAIService/ChatGPTService.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,18 @@ public actor ChatGPTService: ChatGPTServiceType {
172172
}
173173
}
174174
}
175+
176+
public func sendAndWait(
177+
content: String,
178+
summary: String? = nil
179+
) async throws -> String {
180+
let stream = try await send(content: content, summary: summary)
181+
var content = ""
182+
for try await fragment in stream {
183+
content.append(fragment)
184+
}
185+
return content
186+
}
175187

176188
public func stopReceivingMessage() {
177189
cancelTask?()

Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ struct ChatPanelInputArea: View {
227227
.textFieldStyle(.plain)
228228
.padding(8)
229229
.onSubmit {
230+
if chat.isReceivingMessage { return }
230231
if typedMessage.isEmpty { return }
231232
chat.send(typedMessage)
232233
typedMessage = ""

0 commit comments

Comments
 (0)