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
Icône Poche

# Poche

Ton agent, sur ton appareil. Hors ligne. Privé. Rien ne quitte ton iPhone.

platform puce swift ui llm réseau tests testflight auteur

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 Conversation Mode sombre

# 🧭 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, 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

text
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.

flowchart LR
    U[Utilisateur] -->|message| S[LanguageModelSession]
    S -->|appel d'outil| T[Outil<br/>valide les arguments]
    T -->|proposition| C[ConfirmCenter<br/>carte de confirmation]
    C -->|Confirmer| E[ActionExecutor<br/>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.

flowchart TD
    A[Nouveau message] --> B{Préflight :<br/>estimation ≥ 70 % ?}
    B -->|non| C[streamResponse]
    B -->|oui| D[Condensation<br/>appel séparé, sortie @Generable]
    D --> E[Recyclage : nouvelle session<br/>instructions + résumé + 3 derniers tours]
    E --> C
    C -->|exceededContextWindowSize| E
    C --> F[Réponse streamée<br/>+ 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'ApplesaveNote é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 (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

  • Chat nu : session, streaming, états d'indisponibilité
  • Budget de contexte : mesure, condensation, recyclage invisible
  • createReminder + couche Confirm (l'outil patron)
  • Stockage local + searchMyData (mémoire longue)
  • Reste du catalogue (7 outils, chacun testé)
  • Dictée on-device, App Intents, Share Extension
  • 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

Aucune API. Aucun abonnement. Aucun serveur. Juste ton iPhone.