SPB Git

spb/poche Public

Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.

Swift 100%
3.9 KB · 111 lines swift
Raw Blame History
1//2//  ChatViewModel.swift3//  Poche4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//89import Foundation10import Observation1112@MainActor13@Observable14final class ChatViewModel {15    private(set) var turns: [ChatTurn] = []16    var draft = ""1718    private let agent: AgentSession19    private let confirm: ConfirmCenter20    private let store: PocheStore21    private var conversation: ConversationRecord?2223    var isBusy: Bool { agent.isResponding }24    var contextUsage: Double { agent.usageRatio }2526    init(agent: AgentSession, confirm: ConfirmCenter, store: PocheStore) {27        self.agent = agent28        self.confirm = confirm29        self.store = store3031        // The Confirm layer reports back so (a) the user sees a truthful32        // "done" notice only after execution, and (b) the model learns the33        // outcome on its next turn instead of believing its own proposal.34        confirm.onExecuted = { [weak self] summary in35            self?.appendSystemNotice("✓ " + summary)36            self?.agent.noteEvent("Action confirmée et exécutée : \(summary)")37        }38        confirm.onDismissed = { [weak self] summary in39            self?.appendSystemNotice("Proposition annulée — dis-moi ce que tu veux changer.")40            self?.agent.noteEvent("L'utilisateur a annulé la proposition : \(summary)")41        }42        agent.onSummary = { [weak self] summary in43            guard let self, let conversation = self.conversation else { return }44            self.store.updateSummary(of: conversation, to: summary)45        }46    }4748    /// Sends a given text (welcome suggestions, driving hooks).49    func send(_ text: String) async {50        draft = text51        await send()52    }5354    func send() async {55        let text = draft.trimmingCharacters(in: .whitespacesAndNewlines)56        guard !text.isEmpty, !agent.isResponding else { return }57        draft = ""5859        turns.append(ChatTurn(role: .user, text: text, status: .complete))60        persist(role: "user", text: text)6162        turns.append(ChatTurn(role: .assistant, text: "", status: .streaming))63        let index = turns.count - 16465        let outcome = await agent.respond(to: text) { [weak self] partial in66            self?.turns[index].text = partial67        }6869        switch outcome {70        case .completed(let full):71            turns[index].text = full72            turns[index].status = .complete73            persist(role: "assistant", text: full)74        case .refused(let message):75            turns[index].text = message76            turns[index].status = .refused77        case .failed(let message):78            turns[index].text = message79            turns[index].status = .failed80        }81    }8283    #if DEBUG84    /// Visual-verification hook (simulator only): fills the thread with85    /// representative turns without touching the model.86    func injectDemoTurns() {87        turns = [88            ChatTurn(role: .user, text: "Rappelle-moi d'appeler le dentiste demain à 9 h", status: .complete),89            ChatTurn(role: .assistant, text: "Je te propose un rappel « Appeler le dentiste » pour demain à 9 h. Confirme sur la carte ci-dessous.", status: .complete),90            ChatTurn(role: .system, text: "✓ Rappel « Appeler le dentiste » créé", status: .complete),91            ChatTurn(role: .user, text: "Parfait, et qu'est-ce que j'ai cette semaine ?", status: .complete),92            ChatTurn(role: .assistant, text: "", status: .streaming),93        ]94    }95    #endif9697    private func appendSystemNotice(_ text: String) {98        turns.append(ChatTurn(role: .system, text: text, status: .complete))99        persist(role: "system", text: text)100    }101102    private func persist(role: String, text: String) {103        if conversation == nil {104            conversation = store.newConversation()105        }106        if let conversation {107            store.appendTurn(to: conversation, role: role, text: text)108        }109    }110}111