Icône Poche # Poche **Ton agent, sur ton appareil. Hors ligne. Privé. Rien ne quitte ton iPhone.** [![platform](https://img.shields.io/badge/plateforme-iOS%2026%2B-black?logo=apple)](https://developer.apple.com) [![puce](https://img.shields.io/badge/mat%C3%A9riel-A17%20Pro%20minimum-orange)](#-limitations-connues) [![swift](https://img.shields.io/badge/Swift-6.0%20strict%20concurrency-F05138?logo=swift&logoColor=white)](https://swift.org) [![ui](https://img.shields.io/badge/UI-SwiftUI%20%2B%20%40Observable-blue)](https://developer.apple.com/xcode/swiftui/) [![llm](https://img.shields.io/badge/LLM-Apple%20Foundation%20Models%20(~3B)-5E5CE6)](https://developer.apple.com/documentation/foundationmodels) [![réseau](https://img.shields.io/badge/requ%C3%AAtes%20r%C3%A9seau%20IA-0-brightgreen)](#-donn%C3%A9es-et-confidentialit%C3%A9) [![tests](https://img.shields.io/badge/tests-21%2F21%20%E2%9C%93-brightgreen)](#-tests) [![testflight](https://img.shields.io/badge/TestFlight-1.0.0%20(1)%20upload%C3%A9-0D96F6?logo=apple)](#-build-et-distribution) [![auteur](https://img.shields.io/badge/%C2%A9-Simon--Pierre%20Boucher-lightgrey)](mailto:contact@spboucher.ai) *Agent personnel local : chat, rappels, calendrier, notes, tâches et recherche sémantique — propulsé exclusivement par le modèle Apple Intelligence embarqué. Aucune API. Aucun abonnement. Aucun serveur.*
--- ## 📱 Captures d'écran | Accueil | Conversation + confirmation | Mode sombre | |:---:|:---:|:---:| | ![Accueil](docs/screenshots/accueil.png) | ![Conversation](docs/screenshots/conversation.png) | ![Mode sombre](docs/screenshots/sombre.png) | --- ## 🧭 Le concept Poche est un agent personnel qui vit **entièrement sur l'iPhone**. Le seul moteur génératif de l'application est **Apple Foundation Models**, le modèle ~3 milliards de paramètres d'Apple Intelligence, exécuté sur le Neural Engine de l'appareil. La règle fondatrice du projet (voir [`CLAUDE.md`](CLAUDE.md), la source de vérité) : > Si une fonctionnalité ne peut pas être faite on-device, **elle n'est pas faite**. > On ne dégrade pas la promesse pour ajouter une capacité. Concrètement, c'est un chat en streaming qui sait **agir** : créer des rappels et des événements (EventKit), enregistrer des notes et des tâches (SwiftData), retrouver tes données par recherche sémantique locale (NLEmbedding) — chaque écriture passant par une **carte de confirmation** que l'utilisateur valide explicitement. --- ## 📊 Métriques | Métrique | Valeur | |---|---| | Fichiers Swift (app + extension) | **41** | | Fichiers Swift (tests) | **5** | | Lignes de code (app + extension) | **~2 930** | | Lignes de code (tests) | **~320** | | Tests automatisés | **21 / 21 ✓** (5 suites) | | Outils exposés au modèle | **7** (plafond dur : 8) | | Fenêtre de contexte gérée | **4 096 tokens** (condensation à 70 %, réserve réponse 30 %) | | Coût des instructions système | **~230 tokens** (mesuré, documenté dans le code) | | Coût fixe par outil (schéma) | **~120 tokens** | | Requêtes réseau dans le chemin IA | **0** — vérifié par un test tripwire | | Cibles | App iOS + Share Extension + App Intents | | Objectif premier token | **< 400 ms** | --- ## 🏗 Architecture ``` Poche/ ├─ App/ point d'entrée, gate de disponibilité du modèle ├─ Chat/ │ ├─ UI/ fil, bulles, composeur, jauge de contexte, accueil │ └─ State/ ChatViewModel, tours de conversation ├─ Agent/ │ ├─ Session/ cycle de vie LanguageModelSession + instructions versionnées │ ├─ Budget/ tokens, condensation, recyclage ⚠️ cœur du projet │ ├─ Tools/ un fichier par outil (7 outils) │ ├─ Confirm/ couche de confirmation ⚠️ non contournable │ └─ Schemas/ types @Generable ├─ Data/ │ ├─ Store/ SwiftData — notes, tâches, conversations │ ├─ Search/ index sémantique local (NLEmbedding) │ └─ Bridges/ EventKit, App Intents ├─ Shared/ boîte d'échange app ↔ extension (groupe d'apps) ├─ Support/ dictée on-device, veille thermique └─ Design/ thème, marque, fond PocheShare/ Share Extension (entrée seulement) PocheTests/ 21 tests, 5 suites ``` ### Le principe non négociable **Le modèle ne fait jamais d'effet de bord.** Il propose un appel d'outil ; l'application valide, affiche, et n'exécute qu'après confirmation de l'utilisateur. ```mermaid flowchart LR U[Utilisateur] -->|message| S[LanguageModelSession] S -->|appel d'outil| T[Outil
valide les arguments] T -->|proposition| C[ConfirmCenter
carte de confirmation] C -->|Confirmer| E[ActionExecutor
seul chemin d'écriture] C -->|Modifier| U E --> EK[EventKit] E --> SD[SwiftData] E -->|résultat réel| S ``` Il n'existe **aucun chemin de code** qui écrit sans traverser `Confirm/` — pas de mode expert, pas de préférence pour le désactiver. Les dates sont résolues et validées **en Swift** (jamais par le modèle) et affichées en clair sur la carte. --- ## 🧮 Le budget de contexte — le chantier central 4 096 tokens pour tout : instructions système, schémas des outils, historique complet et réponse à venir. Sans gestion, une app de chat locale **casse en quelques minutes**. ```mermaid flowchart TD A[Nouveau message] --> B{Préflight :
estimation ≥ 70 % ?} B -->|non| C[streamResponse] B -->|oui| D[Condensation
appel séparé, sortie @Generable] D --> E[Recyclage : nouvelle session
instructions + résumé + 3 derniers tours] E --> C C -->|exceededContextWindowSize| E C --> F[Réponse streamée
+ jauge discrète mise à jour] ``` 1. **Mesure continue** — estimateur pessimiste (~3 caractères/token), jauge fine dans l'UI, jamais un chiffre. 2. **À 70 % : condensation** — résumé dense et fidèle via un appel séparé (`@Generable`, jamais de String à parser). 3. **Recyclage invisible** — nouvelle `LanguageModelSession` réamorcée ; l'utilisateur ne voit rien. 4. **Mémoire longue externalisée** — les résumés sont persistés dans SwiftData et réinjectés à la demande via `searchMyData`. 5. **Filet de sécurité** — `exceededContextWindowSize` → recycler, rejouer, ne jamais afficher d'erreur technique. --- ## 🧰 Catalogue d'outils (7 / plafond 8) | Outil | Type | Bridge | Confirmation | |---|---|---|:---:| | `createReminder` | écriture | EventKit | ✅ | | `createCalendarEvent` | écriture | EventKit | ✅ | | `saveNote` | écriture | SwiftData | ✅ | | `createTask` | écriture | SwiftData | ✅ | | `updateTask` | écriture | SwiftData | ✅ | | `searchMyData` | lecture | SwiftData + NLEmbedding | — | | `getUpcoming` | lecture | EventKit | — | Chaque outil : arguments `@Generable` + `@Guide`, validation hostile (titres tronqués à 80 caractères, dates ISO 8601 résolues en Swift, bornes passé/futur), **sortie plafonnée (~200 tokens)** — un outil qui renvoie 30 événements tue la session. > ⚠️ Il n'existe **aucune API publique pour l'app Notes d'Apple** — `saveNote` écrit dans > les notes internes de Poche (SwiftData), et son nom ne prétend pas le contraire. > Aucun outil de suppression en v1. --- ## 🔒 Données et confidentialité - **Tout est local** : conversations, notes, tâches, résumés, index de recherche. - **Zéro requête réseau dans le chemin IA** — un test automatisé (`NetworkIsolationTests`) enregistre un espion `URLProtocol` et **échoue si une seule requête sort** pendant l'exercice des outils et de la recherche. - Recherche sémantique via `NLEmbedding` (français) — pas de service externe, même pour l'indexation. - Dictée `SFSpeechRecognizer` avec `requiresOnDeviceRecognition = true` — si l'appareil ne sait pas transcrire localement, le bouton micro n'existe pas. Pas de repli serveur. - Chiffrement au repos via Data Protection. Aucune analytique sur le contenu. - Un refus des garde-fous est un **état normal de l'interface** : message neutre, fil intact. --- ## 🧪 Tests ```bash xcodebuild test -project Poche.xcodeproj -scheme Poche \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' ``` | Suite | Couverture | Tests | |---|---|:---:| | `DateResolverTests` | ISO 8601, date seule → 9 h, passé/garbage/hostile/trop loin rejetés | 6 | | `ContextBudgetTests` | réserve 30 %, seuil 70 %, prompt plein refusé, estimateur pessimiste | 5 | | `CreateReminderToolTests` | l'outil patron : propose sans écrire, args invalides/manquants/hostiles | 6 | | `NetworkIsolationTests` | tripwire : 0 requête réseau dans le chemin de l'agent | 1 | | `SharedInboxTests` | aller-retour de la boîte partagée, import en notes, boîte absente | 3 | **21 / 21 ✓** — build vert sous Swift 6 concurrence stricte (`SWIFT_STRICT_CONCURRENCY=complete`). --- ## 🔨 Build et distribution ### Prérequis - Xcode 26+ (SDK iOS 26), [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`) - Pour l'inférence réelle : iPhone 15 Pro ou plus récent (A17 Pro), Apple Intelligence activé ### Développement ```bash xcodegen generate # project.yml est la source de vérité du projet Xcode open Poche.xcodeproj ``` ### TestFlight Le build **1.0.0 (1)** est uploadé sur App Store Connect (fiche `ai.spboucher.poche`). Pour les suivants — bumper `CURRENT_PROJECT_VERSION` dans `project.yml`, puis : ```bash xcodegen generate xcodebuild -project Poche.xcodeproj -scheme Poche \ -destination 'generic/platform=iOS' -archivePath build/Poche.xcarchive \ archive -allowProvisioningUpdates xcodebuild -exportArchive -archivePath build/Poche.xcarchive \ -exportOptionsPlist build/ExportOptions.plist -exportPath build/export \ -allowProvisioningUpdates ``` `ITSAppUsesNonExemptEncryption = false` est déjà déclaré : pas de questionnaire de conformité à chaque build. ### Hooks de vérification (DEBUG uniquement) ```bash SIMCTL_CHILD_POCHE_AUTOSEND="Bonjour" \ SIMCTL_CHILD_POCHE_DEMO_THREAD=1 \ SIMCTL_CHILD_POCHE_DEMO_CARD=1 \ xcrun simctl launch booted ai.spboucher.poche ``` Absents d'un build release (`#if DEBUG`). --- ## 🚦 États de disponibilité L'app gère chaque état du modèle avec son propre écran et sa propre action : | État | Écran | Action | |---|---|---| | `available` | Chat | — | | `deviceNotEligible` | Mur définitif, soigné, sans culpabilisation | Jamais de LLM de remplacement | | `appleIntelligenceNotEnabled` | Explication | Bouton « Ouvrir Réglages » | | `modelNotReady` | Téléchargement en cours | Bouton « Réessayer » | --- ## ⚠️ Limitations connues | Limitation | Détail | |---|---| | **A17 Pro minimum** | Risque commercial principal — à annoncer sur la fiche App Store, pas au premier lancement | | **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 | | **Pas d'API token count** | Le SDK iOS 26 n'expose pas `tokenCount(for:)` — estimateur pessimiste en attendant (documenté dans `TokenEstimator.swift`) | | **Modèle ~3B** | Bon en extraction/classification/sortie structurée ; la confirmation systématique est ce qui le rend viable en agent | --- ## 🗺 Roadmap - [x] Chat nu : session, streaming, états d'indisponibilité - [x] Budget de contexte : mesure, condensation, recyclage invisible - [x] `createReminder` + couche Confirm (l'outil patron) - [x] Stockage local + `searchMyData` (mémoire longue) - [x] Reste du catalogue (7 outils, chacun testé) - [x] Dictée on-device, App Intents, Share Extension - [x] TestFlight 1.0.0 (1) - [ ] Verrouillage Face ID optionnel à l'ouverture - [ ] Écran de consultation des notes/tâches - [ ] iCloud optionnel, désactivé par défaut, chiffré - [ ] `deleteTask` (quand la confiance dans l'agent sera établie) ---
**Simon-Pierre Boucher** · [contact@spboucher.ai](mailto:contact@spboucher.ai) *Aucune API. Aucun abonnement. Aucun serveur. Juste ton iPhone.*