main
swift 107 lines 3.5 KB
Raw
1 import Ed
2 import Foundation
3 import UIKit
4
5 @MainActor
6 final class ChatViewModel: ObservableObject {
7 @Published var input = "What is my battery level?"
8 @Published var transcript = ""
9 @Published var status = "Not loaded"
10 @Published var isLoaded = false
11 @Published var isBusy = false
12 @Published var showApproval = false
13 @Published var approvalSummary = ""
14
15 private let notes = NotesStore()
16 private lazy var agent = EdAgent { [weak self] call in
17 guard let self else { return .deny }
18 return await self.requestApproval(for: call)
19 }
20 private var approvalContinuation: CheckedContinuation<EdApprovalDecision, Never>?
21
22 func load() async {
23 isBusy = true
24 defer { isBusy = false }
25 do {
26 try await agent.register(BatteryTool())
27 try await agent.register(SaveNoteTool(notes: notes))
28 _ = try await agent.loadDefaultAgentModel(
29 systemPrompt: "You are a concise assistant inside an iOS app. Use tools when useful."
30 )
31 isLoaded = true
32 status = "Ready"
33 } catch {
34 status = "Load failed: \(error.localizedDescription)"
35 }
36 }
37
38 func send() async {
39 let message = input
40 input = ""
41 isBusy = true
42 defer { isBusy = false }
43 do {
44 let reply = try await agent.run(message)
45 transcript += "You: \(message)\n\nEd: \(reply.text)\n\n"
46 status = "Finished in \(reply.duration) with \(reply.toolRounds) tool round(s)"
47 } catch {
48 status = "Turn failed: \(error.localizedDescription)"
49 }
50 }
51
52 func resolveApproval(_ decision: EdApprovalDecision) {
53 showApproval = false
54 approvalContinuation?.resume(returning: decision)
55 approvalContinuation = nil
56 }
57
58 private func requestApproval(for call: EdToolCall) async -> EdApprovalDecision {
59 approvalSummary = "\(call.name) wants to run with: \(call.arguments)"
60 showApproval = true
61 return await withCheckedContinuation { continuation in
62 approvalContinuation = continuation
63 }
64 }
65 }
66
67 private struct BatteryTool: EdTool {
68 let definition = EdToolDefinition.readOnly(
69 name: "battery_level",
70 description: "Read the device battery percentage",
71 parametersSchema: #"{"type":"object","properties":{},"additionalProperties":false}"#
72 )
73
74 func execute(arguments: String) async throws -> String {
75 await MainActor.run {
76 UIDevice.current.isBatteryMonitoringEnabled = true
77 let level = UIDevice.current.batteryLevel
78 return level < 0 ? "Battery level is unavailable." : "Battery is at \(Int(level * 100)) percent."
79 }
80 }
81 }
82
83 private actor NotesStore {
84 private var notes: [String] = []
85
86 func save(_ note: String) -> Int {
87 notes.append(note)
88 return notes.count
89 }
90 }
91
92 private struct SaveNoteTool: EdTool {
93 struct Arguments: Decodable { let text: String }
94
95 let notes: NotesStore
96 let definition = EdToolDefinition.mutating(
97 name: "save_note",
98 description: "Save a note in the app",
99 parametersSchema: #"{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false}"#
100 )
101
102 func execute(arguments: String) async throws -> String {
103 let decoded = try JSONDecoder().decode(Arguments.self, from: Data(arguments.utf8))
104 let count = await notes.save(decoded.text)
105 return "Saved note number \(count)."
106 }
107 }