SPB Git

spb/poche Public

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

Swift 100%
12.7 KB · 300 lines markdown
Rendered Raw Blame History
1<!--2  ─────────────────────────────────────────────3   Poche — Agent personnel 100 % on-device4  ─────────────────────────────────────────────5   Author  : Simon-Pierre Boucher6   Contact : contact@spboucher.ai7   File    : README.md8   Purpose : Documentation complète du projet9  ─────────────────────────────────────────────10-->11<div align="center">1213<img src="docs/icon.png" width="96" alt="Icône Poche" />1415# Poche1617**Ton agent, sur ton appareil. Hors ligne. Privé. Rien ne quitte ton iPhone.**1819[![platform](https://img.shields.io/badge/plateforme-iOS%2026%2B-black?logo=apple)](https://developer.apple.com)20[![puce](https://img.shields.io/badge/mat%C3%A9riel-A17%20Pro%20minimum-orange)](#-limitations-connues)21[![swift](https://img.shields.io/badge/Swift-6.0%20strict%20concurrency-F05138?logo=swift&logoColor=white)](https://swift.org)22[![ui](https://img.shields.io/badge/UI-SwiftUI%20%2B%20%40Observable-blue)](https://developer.apple.com/xcode/swiftui/)23[![llm](https://img.shields.io/badge/LLM-Apple%20Foundation%20Models%20(~3B)-5E5CE6)](https://developer.apple.com/documentation/foundationmodels)24[![réseau](https://img.shields.io/badge/requ%C3%AAtes%20r%C3%A9seau%20IA-0-brightgreen)](#-donn%C3%A9es-et-confidentialit%C3%A9)25[![tests](https://img.shields.io/badge/tests-21%2F21%20%E2%9C%93-brightgreen)](#-tests)26[![testflight](https://img.shields.io/badge/TestFlight-1.0.0%20(1)%20upload%C3%A9-0D96F6?logo=apple)](#-build-et-distribution)27[![auteur](https://img.shields.io/badge/%C2%A9-Simon--Pierre%20Boucher-lightgrey)](mailto:contact@spboucher.ai)2829*Agent personnel local : chat, rappels, calendrier, notes, tâches et recherche sémantique —30propulsé exclusivement par le modèle Apple Intelligence embarqué. Aucune API. Aucun abonnement. Aucun serveur.*3132</div>3334---3536## 📱 Captures d'écran3738| Accueil | Conversation + confirmation | Mode sombre |39|:---:|:---:|:---:|40| ![Accueil](docs/screenshots/accueil.png) | ![Conversation](docs/screenshots/conversation.png) | ![Mode sombre](docs/screenshots/sombre.png) |4142---4344## 🧭 Le concept4546Poche est un agent personnel qui vit **entièrement sur l'iPhone**. Le seul moteur47génératif de l'application est **Apple Foundation Models**, le modèle ~3 milliards de48paramètres d'Apple Intelligence, exécuté sur le Neural Engine de l'appareil.4950La règle fondatrice du projet (voir [`CLAUDE.md`](CLAUDE.md), la source de vérité) :5152> Si une fonctionnalité ne peut pas être faite on-device, **elle n'est pas faite**.53> On ne dégrade pas la promesse pour ajouter une capacité.5455Concrètement, c'est un chat en streaming qui sait **agir** : créer des rappels et des56événements (EventKit), enregistrer des notes et des tâches (SwiftData), retrouver tes57données par recherche sémantique locale (NLEmbedding) — chaque écriture passant par une58**carte de confirmation** que l'utilisateur valide explicitement.5960---6162## 📊 Métriques6364| Métrique | Valeur |65|---|---|66| Fichiers Swift (app + extension) | **41** |67| Fichiers Swift (tests) | **5** |68| Lignes de code (app + extension) | **~2 930** |69| Lignes de code (tests) | **~320** |70| Tests automatisés | **21 / 21 ✓** (5 suites) |71| Outils exposés au modèle | **7** (plafond dur : 8) |72| Fenêtre de contexte gérée | **4 096 tokens** (condensation à 70 %, réserve réponse 30 %) |73| Coût des instructions système | **~230 tokens** (mesuré, documenté dans le code) |74| Coût fixe par outil (schéma) | **~120 tokens** |75| Requêtes réseau dans le chemin IA | **0** — vérifié par un test tripwire |76| Cibles | App iOS + Share Extension + App Intents |77| Objectif premier token | **< 400 ms** |7879---8081## 🏗 Architecture8283```84Poche/85├─ App/                 point d'entrée, gate de disponibilité du modèle86├─ Chat/87│  ├─ UI/               fil, bulles, composeur, jauge de contexte, accueil88│  └─ State/            ChatViewModel, tours de conversation89├─ Agent/90│  ├─ Session/          cycle de vie LanguageModelSession + instructions versionnées91│  ├─ Budget/           tokens, condensation, recyclage      ⚠️ cœur du projet92│  ├─ Tools/            un fichier par outil (7 outils)93│  ├─ Confirm/          couche de confirmation               ⚠️ non contournable94│  └─ Schemas/          types @Generable95├─ Data/96│  ├─ Store/            SwiftData — notes, tâches, conversations97│  ├─ Search/           index sémantique local (NLEmbedding)98│  └─ Bridges/          EventKit, App Intents99├─ Shared/              boîte d'échange app ↔ extension (groupe d'apps)100├─ Support/             dictée on-device, veille thermique101└─ Design/              thème, marque, fond102PocheShare/              Share Extension (entrée seulement)103PocheTests/              21 tests, 5 suites104```105106### Le principe non négociable107108**Le modèle ne fait jamais d'effet de bord.** Il propose un appel d'outil ; l'application109valide, affiche, et n'exécute qu'après confirmation de l'utilisateur.110111```mermaid112flowchart LR113    U[Utilisateur] -->|message| S[LanguageModelSession]114    S -->|appel d'outil| T[Outil<br/>valide les arguments]115    T -->|proposition| C[ConfirmCenter<br/>carte de confirmation]116    C -->|Confirmer| E[ActionExecutor<br/>seul chemin d'écriture]117    C -->|Modifier| U118    E --> EK[EventKit]119    E --> SD[SwiftData]120    E -->|résultat réel| S121```122123Il n'existe **aucun chemin de code** qui écrit sans traverser `Confirm/` — pas de mode124expert, pas de préférence pour le désactiver. Les dates sont résolues et validées **en125Swift** (jamais par le modèle) et affichées en clair sur la carte.126127---128129## 🧮 Le budget de contexte — le chantier central1301314 096 tokens pour tout : instructions système, schémas des outils, historique complet132et réponse à venir. Sans gestion, une app de chat locale **casse en quelques minutes**.133134```mermaid135flowchart TD136    A[Nouveau message] --> B{Préflight :<br/>estimation ≥ 70 % ?}137    B -->|non| C[streamResponse]138    B -->|oui| D[Condensation<br/>appel séparé, sortie @Generable]139    D --> E[Recyclage : nouvelle session<br/>instructions + résumé + 3 derniers tours]140    E --> C141    C -->|exceededContextWindowSize| E142    C --> F[Réponse streamée<br/>+ jauge discrète mise à jour]143```1441451. **Mesure continue** — estimateur pessimiste (~3 caractères/token), jauge fine dans l'UI, jamais un chiffre.1462. **À 70 % : condensation** — résumé dense et fidèle via un appel séparé (`@Generable`, jamais de String à parser).1473. **Recyclage invisible** — nouvelle `LanguageModelSession` réamorcée ; l'utilisateur ne voit rien.1484. **Mémoire longue externalisée** — les résumés sont persistés dans SwiftData et réinjectés à la demande via `searchMyData`.1495. **Filet de sécurité**`exceededContextWindowSize` → recycler, rejouer, ne jamais afficher d'erreur technique.150151---152153## 🧰 Catalogue d'outils (7 / plafond 8)154155| Outil | Type | Bridge | Confirmation |156|---|---|---|:---:|157| `createReminder` | écriture | EventKit | ✅ |158| `createCalendarEvent` | écriture | EventKit | ✅ |159| `saveNote` | écriture | SwiftData | ✅ |160| `createTask` | écriture | SwiftData | ✅ |161| `updateTask` | écriture | SwiftData | ✅ |162| `searchMyData` | lecture | SwiftData + NLEmbedding | — |163| `getUpcoming` | lecture | EventKit | — |164165Chaque outil : arguments `@Generable` + `@Guide`, validation hostile (titres tronqués à16680 caractères, dates ISO 8601 résolues en Swift, bornes passé/futur), **sortie plafonnée167(~200 tokens)** — un outil qui renvoie 30 événements tue la session.168169> ⚠️ Il n'existe **aucune API publique pour l'app Notes d'Apple**`saveNote` écrit dans170> les notes internes de Poche (SwiftData), et son nom ne prétend pas le contraire.171> Aucun outil de suppression en v1.172173---174175## 🔒 Données et confidentialité176177- **Tout est local** : conversations, notes, tâches, résumés, index de recherche.178- **Zéro requête réseau dans le chemin IA** — un test automatisé (`NetworkIsolationTests`)179  enregistre un espion `URLProtocol` et **échoue si une seule requête sort** pendant180  l'exercice des outils et de la recherche.181- Recherche sémantique via `NLEmbedding` (français) — pas de service externe, même pour l'indexation.182- Dictée `SFSpeechRecognizer` avec `requiresOnDeviceRecognition = true` — si l'appareil ne183  sait pas transcrire localement, le bouton micro n'existe pas. Pas de repli serveur.184- Chiffrement au repos via Data Protection. Aucune analytique sur le contenu.185- Un refus des garde-fous est un **état normal de l'interface** : message neutre, fil intact.186187---188189## 🧪 Tests190191```bash192xcodebuild test -project Poche.xcodeproj -scheme Poche \193  -destination 'platform=iOS Simulator,name=iPhone 17 Pro'194```195196| Suite | Couverture | Tests |197|---|---|:---:|198| `DateResolverTests` | ISO 8601, date seule → 9 h, passé/garbage/hostile/trop loin rejetés | 6 |199| `ContextBudgetTests` | réserve 30 %, seuil 70 %, prompt plein refusé, estimateur pessimiste | 5 |200| `CreateReminderToolTests` | l'outil patron : propose sans écrire, args invalides/manquants/hostiles | 6 |201| `NetworkIsolationTests` | tripwire : 0 requête réseau dans le chemin de l'agent | 1 |202| `SharedInboxTests` | aller-retour de la boîte partagée, import en notes, boîte absente | 3 |203204**21 / 21 ✓** — build vert sous Swift 6 concurrence stricte (`SWIFT_STRICT_CONCURRENCY=complete`).205206---207208## 🔨 Build et distribution209210### Prérequis211212- Xcode 26+ (SDK iOS 26), [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`)213- Pour l'inférence réelle : iPhone 15 Pro ou plus récent (A17 Pro), Apple Intelligence activé214215### Développement216217```bash218xcodegen generate          # project.yml est la source de vérité du projet Xcode219open Poche.xcodeproj220```221222### TestFlight223224Le build **1.0.0 (1)** est uploadé sur App Store Connect (fiche `ai.spboucher.poche`).225Pour les suivants — bumper `CURRENT_PROJECT_VERSION` dans `project.yml`, puis :226227```bash228xcodegen generate229xcodebuild -project Poche.xcodeproj -scheme Poche \230  -destination 'generic/platform=iOS' -archivePath build/Poche.xcarchive \231  archive -allowProvisioningUpdates232xcodebuild -exportArchive -archivePath build/Poche.xcarchive \233  -exportOptionsPlist build/ExportOptions.plist -exportPath build/export \234  -allowProvisioningUpdates235```236237`ITSAppUsesNonExemptEncryption = false` est déjà déclaré : pas de questionnaire de238conformité à chaque build.239240### Hooks de vérification (DEBUG uniquement)241242```bash243SIMCTL_CHILD_POCHE_AUTOSEND="Bonjour" \244SIMCTL_CHILD_POCHE_DEMO_THREAD=1 \245SIMCTL_CHILD_POCHE_DEMO_CARD=1 \246xcrun simctl launch booted ai.spboucher.poche247```248249Absents d'un build release (`#if DEBUG`).250251---252253## 🚦 États de disponibilité254255L'app gère chaque état du modèle avec son propre écran et sa propre action :256257| État | Écran | Action |258|---|---|---|259| `available` | Chat | — |260| `deviceNotEligible` | Mur définitif, soigné, sans culpabilisation | Jamais de LLM de remplacement |261| `appleIntelligenceNotEnabled` | Explication | Bouton « Ouvrir Réglages » |262| `modelNotReady` | Téléchargement en cours | Bouton « Réessayer » |263264---265266## ⚠️ Limitations connues267268| Limitation | Détail |269|---|---|270| **A17 Pro minimum** | Risque commercial principal — à annoncer sur la fiche App Store, pas au premier lancement |271| **Simulateur** | Sur certains hôtes, le pont Apple Intelligence du simulateur est cassé (`promptTemplateNotFound`) ; l'app l'annonce honnêtement après 2 échecs. Sur appareil réel, tout fonctionne |272| **Pas d'API token count** | Le SDK iOS 26 n'expose pas `tokenCount(for:)` — estimateur pessimiste en attendant (documenté dans `TokenEstimator.swift`) |273| **Modèle ~3B** | Bon en extraction/classification/sortie structurée ; la confirmation systématique est ce qui le rend viable en agent |274275---276277## 🗺 Roadmap278279- [x] Chat nu : session, streaming, états d'indisponibilité280- [x] Budget de contexte : mesure, condensation, recyclage invisible281- [x] `createReminder` + couche Confirm (l'outil patron)282- [x] Stockage local + `searchMyData` (mémoire longue)283- [x] Reste du catalogue (7 outils, chacun testé)284- [x] Dictée on-device, App Intents, Share Extension285- [x] TestFlight 1.0.0 (1)286- [ ] Verrouillage Face ID optionnel à l'ouverture287- [ ] Écran de consultation des notes/tâches288- [ ] iCloud optionnel, désactivé par défaut, chiffré289- [ ] `deleteTask` (quand la confiance dans l'agent sera établie)290291---292293<div align="center">294295**Simon-Pierre Boucher** · [contact@spboucher.ai](mailto:contact@spboucher.ai)296297*Aucune API. Aucun abonnement. Aucun serveur. Juste ton iPhone.*298299</div>300