diff --git a/docs/ASSISTANT-V2.md b/docs/ASSISTANT-V2.md new file mode 100644 index 0000000..beb0e3e --- /dev/null +++ b/docs/ASSISTANT-V2.md @@ -0,0 +1,527 @@ +# Assistant V2 — ObsiGate comme second cerveau & assistant personnel + +> **Statut :** 📐 Spécification de conception (non implémentée) +> **Dernière mise à jour :** 2026-10-08 +> **IDs proposés :** ObsiGate **#191 → #201** (liste « Assistant V2 ») +> **Portée :** évolution de l'assistant IA existant en assistant à mémoire long terme, +> outillé (connecteurs externes), planifié (rappels, briefing) et proactif (insights). +> **Hors portée :** toute intégration avec l'infrastructure externe du homelab +> (Honcho, Hermes, Ollama partagé, ntfy homelab, cron système). ObsiGate est **autonome** : +> un seul conteneur, ses propres stores, ses propres clés. + +--- + +## 1. Vision + +Transformer ObsiGate d'un portail de notes avec assistant vers un **second cerveau +conversationnel et autonome** : + +1. **Il se souvient** — mémoire long terme structurée (faits, préférences, décisions, + entités), consolidée chaque nuit, **visible et éditable en markdown dans le vault** + (pas de boîte noire). +2. **Il agit** — lit/écrit le vault, mais aussi les services externes connectés : + Gmail, Google Agenda, Teams, Discord, Notion, Drive… (cadre de connecteurs unifié). +3. **Il anticipe** — rappels conversationnels, briefing quotidien généré par l'agent, + détection de boucles ouvertes et d'engagements non tenus. +4. **Il centralise** — ObsiGate devient le point d'entrée unique prise de notes + + assistant ; tout le reste (desktop Tauri, MCP externe, PWA mobile) n'est qu'un + accès à *ce même* cerveau. + +Le modèle économique de la conception : **Muse/Snow/ChatGPT-Memory rendent l'effet +« l'assistant me connaît » ; ici la mémoire est auditable (markdown dans le vault, +audit log, dédupli, réversibilité) et les données ne quittent jamais le stack ObsiGate +sauf appel explicite aux API des services que Bruno a connectés lui-même.** + +--- + +## 2. Principes directeurs (non négociables) + +| # | Principe | Conséquence technique | +|---|---|---| +| P1 | **Autonomie du stack** | Tout vit dans le conteneur `obsigate` + son volume `data/`. Aucun nouveau conteneur runtime (Ollama est retiré — voir §9). | +| P2 | **Zéro framework d'agent externe** | Décision déjà prise en #92 : boucle maison `backend/agent/loop.py`. L'Assistant V2 l'étend, ne la remplace pas. | +| P3 | **Un outil = un `@tool`** | Toute nouvelle capacité passe par le registre typé (`backend/tools/registry.py`) : risque, scope, rate-limit, confirmation, audit, labels + i18n. | +| P4 | **Mémoire = markdown d'abord** | Les artefacts durables (profil, insights, briefings, rappels) sont des notes du vault ; la base SQLite n'est que l'index reconstructible. | +| P5 | **Humain dans la boucle** | `WRITE`/`DANGEROUS` exigent confirmation (#91/#92) ; un fait de confiance faible reste `pending` tant que l'utilisateur ne le valide pas. | +| P6 | **Secrets jamais committés** | Pattern existant : `data/api_keys.json` / `data/connector_secrets.json` (0600) ou variables `OBSIGATE_*` dans `.env` (`env_file`, jamais dans le compose). | +| P7 | **i18n FR/EN systématique** | Toute clé d'interface (`ai.step.*`, config connecteurs, cartes mémoire) existe dans les deux locales. | +| P8 | **Le desktop Tauri reste fonctionnel** | Les connecteurs se dégradent silencieusement hors ligne (capability = absente si pas de token) ; aucune dépendance à un service réseau au démarrage. | +| P9 | **Sécurité existante réutilisée** | `_resolve_safe_path()`, `require_auth`/`require_vault_access`, SSRF guards (`validate_webhook_url`, `is_safe_target`), `secret_redactor` avant tout appel LLM. | + +--- + +## 3. Inventaire de l'existant (socle sur lequel on bâtit) + +Constat clé : **l'ossature « agent » existe déjà**. Ce qui manque, c'est la mémoire +long terme, les connecteurs, la couche proactive et l'ordonnancement de haut niveau. + +| Brique | Fichier(s) | État | Rôle dans la V2 | +|---|---|---|---| +| Boucle agent multi-étapes + tool calling natif | `backend/agent/loop.py` (`run_agent`), `backend/ai_chat.py` (payload `tools`/`tool_choice`, conversion Gemini, fallback si refus) | ✅ livré (#91/#92) | Moteur de l'assistant V2 — inchangé | +| Registre d'outils typé | `backend/tools/registry.py` (`@tool`, `ToolRisk` READ/WRITE/DANGEROUS, `ToolScope` IN_APP/MCP, confirmation auto si non-READ, rate-limit, audit, schéma OpenAI) | ✅ | Cadre d'insertion de **tous** les nouveaux outils | +| Outils vault (~26) | `backend/tools/service.py`, `backend/services/*` (files, mutations, search, vaults, duplicates…) | ✅ | Outils natifs de l'assistant | +| Recherche TF-IDF + sémantique embeddings (RRF) | `backend/search.py`, `backend/semantic_search.py`, `requirements-semantic.txt` (sentence-transformers **local, optionnel**, FAISS) | ✅ (#70) | Retrieval de la mémoire épisodique ; remplace le besoin Ollama embeddings | +| Historique de conversation persistant | `backend/ai_history.py` (`data/sessions`, par utilisateur) | ✅ (#95) | **Mémoire épisodique** brute — source de la consolidation | +| Serveur MCP externe | `backend/mcp/server.py` (Streamable HTTP `/mcp`, propose/apply) | ✅ (#79) | Même registre exposé — les outils V2 en profitent gratis | +| Scheduler type cron | `backend/scheduler.py` (tick asyncio 60 s, `data/scheduled_tasks.json`, `interval_hours`/`daily_time`/`once_at`, actions `create_file`/`append_to_file`/`notify`) + outils `create/list/delete/run_scheduled_task` (`backend/tools/scheduled.py`) | ✅ (#170) | Socle des rappels & briefing — à étendre (§8) | +| Notifications externes | `backend/notify.py` (canaux **Discord webhook, Telegram, SMTP, webhook générique**, triggers, secrets 0600) + outil `notify_external` | ✅ (#168) | Bras de sortie des rappels/briefing | +| Web Push PWA | `backend/push.py` (VAPID) + `frontend/sw.js` | ✅ (#67) | Deuxième canal de rappel, sans dépendance externe | +| Sources connectées (premier précédent) | `backend/tools/connected.py` (Gitea/GitHub, READ), page Configurations #103, `backend/tools/secrets.py` | ✅ (#92/#103) | Patron à généraliser en cadre connecteurs (§7) | +| Web : recherche + fetch + crawl | `backend/tools/web.py` (Tavily/Brave/SerpAPI/Exa/SearXNG, `fetch_url`), `backend/tools/crawler.py` (`crawl_site`) | ✅ | Outils « connaissances hors vault » | +| Production de documents | `backend/tools/documents.py` (`create_xlsx/docx/csv/pdf`) | ✅ | Actions de l'assistant | +| Doublons | `backend/tools/duplicates.py` (`find_duplicates`, `merge_duplicate_notes` DANGEROUS) | ✅ (#166) | Hygiène de la mémoire | +| Skills / slash-commandes | `backend/skills.py` (`/cmd`, `data/skills.json`) | ✅ | Vecteur naturel des prompts de consolidation & briefing | +| Cache SQLite | `backend/tools/webcache.py` (précédent `sqlite3` stdlib dans `data/`) | ✅ | Patron du store mémoire (§5) | +| Fournisseurs LLM | `backend/ai.py` (`PROVIDERS` : deepseek, openrouter, gemini, nvidia, qwencloud, xiaomi, mistral, ollama), `reload_ai_config()`, clés `data/api_keys.json` + fallback env | ✅ | Garder le multi-provider ; **retirer le conteneur ollama** (§9) | +| Audit + redaction | `backend/audit.py`, `backend/tools/audit.py`, `backend/secret_redactor.py` | ✅ | Traçabilité mémoire/connecteurs | + +**Gaps** (→ §5–§8) : mémoire sémantique (faits) · miroir markdown · consolidation · +cadre connecteurs générique + OAuth2 · Gmail/Calendar/Teams/Discord(in)/Notion/Drive · +rappels conversationnels · agent programmé (briefing) · insights proactifs. + +--- + +## 4. Architecture cible + +``` + ┌────────────────────────────────────────────┐ + │ FRONTEND (SPA + PWA + Tauri) │ + │ Assistant panel · Cartes mémoire · Config │ + │ « Sources connectées » · Briefings │ + └───────────────┬────────────────────────────┘ + │ REST + SSE (existant) +┌──────────────────────────────────────▼─────────────────────────────────────┐ +│ BACKEND obsigate (1 conteneur) │ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ ┌────────────────────────────┐ │ +│ │ agent/loop.py│◄──│ tools/registry.py│◄──│ TOUS les outils V2 (@tool) │ │ +│ │ (run_agent) │ │ risque/scope/ │ │ vault · web · docs (exist.)│ │ +│ └──────┬───────┘ │ audit/limit │ │ memory.* (nouveau §5) │ │ +│ │ └──────────────────┘ │ connector.* (§7) │ │ +│ ┌──────┴────────┐ │ reminder.* (§8) │ │ +│ │ ai_chat.py │ │ scheduled.* (étendu) │ │ +│ │ providers API │ └────────────────────────────┘ │ +│ └──────┬────────┘ │ +│ │ │ +│ ┌──────▼───────────┐ ┌────────────────────┐ ┌───────────────────────┐ │ +│ │ scheduler.py │ │ memory/ │ │ connectors/ │ │ +│ │ tick 60 s │ │ store SQLite │ │ base OAuth2+PKCE │ │ +│ │ + agent_prompt │─►│ (faits) │ │ gmail/gcal/teams/ │ │ +│ │ (nouv. action) │ │ consolidator │ │ discord/notion/drive │ │ +│ └──────┬───────────┘ └─────────┬──────────┘ └───────────┬───────────┘ │ +│ │ notify.broadcast │ miroir markdown │ HTTPS API │ +│ │ + Web Push ▼ ▼ │ +│ ┌──────▼──────────┐ ┌────────────────────┐ ┌─────────────────────┐ │ +│ │ Vault Obsidian │ │ Vault Obsidian │ │ Services externes │ │ +│ │ (5 volumes NFS) │ │ 99_Assistant/*.md │ │ (Google/MS/Discord) │ │ +│ └─────────────────┘ └────────────────────┘ └─────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +Nouveaux modules backend (alignés sur l'organisation existante) : + +``` +backend/ + memory/ + store.py # SQLite data/memory.sqlite3 — CRUD faits + runs + recall.py # fusion recall: facts + sémantique (#70) + épisodes (ai_history) + consolidator.py # job « dreaming » nocturne (prompt + parsing + dédup) + mirror.py # rendu markdown 99_Assistant/ + parse des edits humains + connectors/ + base.py # ConnectorInterface: id, scopes, authorize_url, exchange, + # refresh, capabilities(), call() (retry + rate-limit + audit) + oauth.py # flux code + PKCE, redirect /api/connectors/{id}/callback + registry.py # registre des connecteurs installés (comme tools/registry) + gmail.py gcal.py teams.py discord.py notion.py drive.py + routers/ + connectors.py # CRUD admin: installation, statut, test de connexion + memory.py # API faits (liste/édit/suppression/validation pending) + tools/ + memory.py # @tool memory.* (§5.4) + reminders.py # @tool reminder.* (§8.1) + connectors.py # @tool par capability connecteur (§7.4) +``` + +--- + +## 5. Mémoire long terme — #192 (socle), #193 (miroir), #194 (consolidation) + +### 5.1 Modèle de données (SQLite, `data/memory.sqlite3`) + +Précédent : `webcache.py`. Une seule table applicative + une table d'audit des runs. + +```sql +CREATE TABLE facts ( + id TEXT PRIMARY KEY, -- f- + user_id TEXT NOT NULL, + subject TEXT NOT NULL, -- ex: "santé", "Projet FlowDeck", "bruno" + predicate TEXT NOT NULL, -- ex: "rdv", "préférence", "décision" + value TEXT NOT NULL, -- contenu FR court (< 500 car.) + kind TEXT NOT NULL CHECK (kind IN + ('preference','decision','entity','event','habit','goal','open_loop')), + confidence REAL NOT NULL DEFAULT 0.5, -- 0..1 ; < 0.7 → status pending + status TEXT NOT NULL DEFAULT 'confirmed' + CHECK (status IN ('pending','confirmed','rejected','expired')), + source TEXT NOT NULL, -- 'consolidation' | 'user' | 'chat' + source_ref TEXT, -- session id ou chemin de note + supersedes TEXT, -- id du fait remplacé (dédup temporel) + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL, -- reinforcement par usage + expires_at TEXT -- nullable (ex: rdv passé) +); +CREATE INDEX idx_facts_subject ON facts(user_id, subject); +CREATE INDEX idx_facts_status ON facts(user_id, status); + +CREATE TABLE consolidation_runs ( + id TEXT PRIMARY KEY, started_at TEXT, finished_at TEXT, + sessions_consumed INTEGER, notes_changed INTEGER, + facts_added INTEGER, facts_updated INTEGER, error TEXT +); +``` + +Règles : +- **Dédup temporel** : un nouveau fait `(subject, predicate)` avec valeur différente + crée une ligne et marque l'ancienne `superseded` (via `supersedes`, status `expired`). + Historique conservé, recall = dernier `confirmed` non expiré. +- **Renforcement** : `last_seen_at` bump quand le fait est réaffirmé en chat ; + les facts non revus depuis N jours (180) et non `decision` deviennent `expired`. +- **PII santé/famille** : `consolidator` force `status='pending'` (validation humaine + via cartes) si la note source appartient aux dossiers sensibles configurables + (`OBSIGATE_MEMORY_REVIEW_FOLDERS`, ex. `Santé,Famille`). +- Backup : `data/` étant déjà dans le cycle de backups existant, rien de neuf. + +### 5.2 Miroir markdown dans le vault — #193 + +Dossier **`99_Assistant/`** (créé à la première activation, dans le vault principal +configurable `OBSIGATE_MEMORY_VAULT=Main`, dossier racine configurable) : + +| Fichier | Contenu | Écrit par | Éditable par Bruno | +|---|---|---|---| +| `Profil.md` | Préférences/habitus/goals confirmés, groupés par sujet (H2) | mirror (consolidation) | ✅ — les edits sont respectés (§5.3 règles de merge) | +| `Insights.md` | Détections proactes #201 (boucles ouvertes, engagements, patterns), horodatées | mirror | ✅ (suppression = `rejected` en base) | +| `Rappels.md` | Table des rappels actifs (liens vers tâches planifiées) | mirror scheduler | lecture seule (UI dédiée) | +| `Briefings/YYYY-MM-DD.md` | Briefing quotidien #200 | agent programmé | ✅ (archivage, jamais régénéré) | +| `Journal.md` | Dernières consolidations (nb faits ajoutés/validés) | mirror | lecture seule | + +Le miroir utilise des **balises HTML de section** : + +```markdown + +…bloc régénéré… + +``` + +Règle de merge : à chaque écriture, le mirror compare le contenu précédent généré +(hash stocké en base) avec le contenu actuel du fichier. Si l'utilisateur a modifié +l'intérieur d'une section gérée → la modification est **re-parse en faits `source='user'`** +(confiance 1.0, status confirmed) puis la section est régénérée à partir de la base. +Tout ce qui est hors balises est laissé intact. C'est le mécanisme qui rend la mémoire +*auditable et contestable* — l'anti-boîte-noire. + +### 5.3 Consolidation nocturne (« dreaming ») — #194 + +Job planifié via le scheduler existant (§8.3), action `agent_prompt` dédiée : + +1. **Collecte incrémentale** : sessions `ai_history` créées/modifiées depuis le dernier + run + fichiers `.md` modifiés depuis le dernier run (via l'index/watcher mtime) — + plafonds : 200 fichiers × 30 Ko (comme bookslm), contenu `redact_file_content()` avant LLM. +2. **Extraction** (1 appel LLM, provider par défaut = cheap model) : prompt système + d'extraction → JSON `[{subject, predicate, value, kind, confidence}]` validé Pydantic ; + rejet des valeurs non sourcées. +3. **Réconciliation** (déterministe, stdlib) : dédup/supersede par `(subject,predicate)`, + plancher de confiance, dossiers sensibles → pending. +4. **Écriture** : `facts` + `mirror.py` → sections gérées de `Profil.md`. +5. **Cartes de validation** : les faits `pending` deviennent des cartes dans le panneau + Assistant (« Valider / Modifier / Oublier »), persistées jusqu'à action. + +Coût : 1–3 appels API/nuit (deepseek-chat ≈ quelques centimes). Budget max : +`OBSIGATE_CONSOLIDATION_MAX_TOKENS` (défaut 30 000/jour) pour bornes dures. + +### 5.4 Outils mémoire (registés `@tool`) — #192 + +| Outil | Risque | Description (schéma) | +|---|---|---| +| `memory_recall` | READ | `{query, subject?, kind?}` → faits confirmed pertinents + fusion RRF avec sémantique #70 sur le vault. Injecté **automatiquement** dans le prompt système de chaque chat (voir §5.5). | +| `memory_save` | WRITE | `{subject, predicate, value, kind, confidence=0.9}` — le modèle propose, la carte de confirmation affiche le fait ; Bruno valide → `source='chat'`. | +| `memory_update` | WRITE | `{fact_id, value | status}` — correction/revalidation. | +| `memory_forget` | DANGEROUS | `{fact_id}` — status `rejected`, supprimé du miroir, tracé à l'audit. | +| `profile_get` | READ | `{section?}` → contenu courant de `Profil.md` (plus riche que le recall brut). | + +### 5.5 Injection dans le contexte (le « ça me connaît » quotidien) + +Au démarrage de chaque session agent (`bookslm_routes` → futur endpoint `/agent`), +construction d'un bloc mémoire coiffant le prompt système, budget ≤ 2 500 tokens : + +``` + +Profil (résumé géré) : préférences, habitudes, objectifs actifs… +Faits saillants liés au sujet : +Rappels actifs : +Dernier briefing : <3 puces> + +``` + +Ce bloc est **visible** dans la section « étapes » du panneau (label « Mémoire »), +comme les autres étapes d'outils — pas de magie cachée. + +--- + +## 6. Rattachement MCP / Desktop / Skills + +- Tous les nouveaux outils `@tool` sont automatiquement exposés au **serveur MCP** + (`/mcp`, scopes IN_APP+MCP par défaut, propose/apply déjà gérés) → Claude Desktop / + Cursor bénéficient gratis de `memory.*`, `reminder.*`, `connector.*`. +- **Skills `/`** : nouvelles built-in `/profil` (montre Profil.md), `/rappel ` + (crée un rappel), `/brief` (régénère le briefing du jour), `/consolider` + (déclenche le run de consolidation à la demande), `/connecteurs` (état des sources). +- **Desktop Tauri** : les connecteurs sans token enregistré n'apparaissent pas dans le + registre de schémas envoyés au LLM (`list_tools` filtré par capabilities actives) ; + zéro appel réseau au démarrage. + +--- + +## 7. Connecteurs externes — #195 (cadre), #196–#198 (implémentations) + +### 7.1 Cadre (`backend/connectors/`) + +Décision : **remplacer la logique ad hoc de `connected.py` (Gitea/GitHub par variables +d'env) par un cadre général** — tout en gardant ces deux sources fonctionnelles +(migration interne, même UX « Sources connectées » #103 étendue). + +Concepts : + +- **Connector** = classe (id, label, docs URL, scopes OAuth, capabilities nommées). +- **Installation** = entrée `data/connectors.json` `{id, type, status, config (public), + created_at}` ; secrets dans `data/connector_secrets.json` (0600) ou + `OBSIGATE_CONNECTOR_SECRET_` — **même pattern strict que #168/#103**. +- **Auth** : soit *token statique* (Discord webhook, Notion key, GitHub), soit + **OAuth2 authorization-code + PKCE** (Google, Microsoft) : + `GET /api/connectors/{id}/authorize` → redirection → `GET /api/connectors/{id}/callback` + (échange + stockage `refresh_token` chiffré avec `data/secret.key` existant) → + `POST /api/connectors/{id}/test` (validation par un appel READ bon marché). +- **Refresh** transparent dans `call()` (anticipation du expiry, 1 retry sur 401). +- **Capabilities → tools** : chaque capability expose un `@tool` (dynamiquement + enregistré si le connecteur est installé), préfixé : + `gmail.search_messages`, `gcal.list_events`, `teams.list_chats`… + Risque par capability (lecture READ / écriture WRITE / envoi DANGEROUS). +- **Budget & limites** : rate-limit par connecteur via `tools/ratelimit.py`, + timeout httpx 15 s, retry backoff (pattern #92), cache SQLite des réponses GET + bornées (pattern `webcache.py`, TTL 5 min pour le contexte de chat). +- **Audit** : chaque `call()` journalisé (connecteur, capability, args tronqués, + latence, statut) via `tools/audit.py`. + +### 7.2 Matrice des connecteurs cibles + +| Connecteur | Auth | Capabilités (→ outil) | Risque | Phase | +|---|---|---|---|---| +| **Google — Calendar** | OAuth2 PKCE | `gcal.list_events` (période, requête), `gcal.get_event`, `gcal.create_event`, `gcal.update_event` | READ, READ, WRITE, WRITE | #196 | +| **Google — Gmail** | OAuth2 PKCE | `gmail.search_messages` (query `is:unread…`), `gmail.read_message`, `gmail.create_draft`, ~~`gmail.send`~~ | READ, READ, WRITE, 🔴 non exposé v1 | #196 | +| **Google — Drive** | OAuth2 PKCE | `drive.search`, `drive.read_file` (import md/docx → vault) | READ | #198 (rattache backlog #167) | +| **Microsoft — Teams / Graph** | Entra ID client, OAuth2 | `teams.list_chats`, `teams.read_messages`, `teams.send_message` | READ, READ, WRITE | #197 | +| **Microsoft — Outlook** | idem Graph | `outlook.search_messages`, `outlook.list_events` (calendriers persos MS) | READ | #197 (optionnel, même jeton) | +| **Discord** | webhook (sortie, ✅ #168) + **bot token lecture** (entrée, scopes `read`) | `discord.send` (existant via notify), `discord.list_messages`, `discord.search` (REST, pas de gateway websocket en v1) | WRITE, READ, READ | #198 | +| **Notion** | API key (user token) | `notion.search`, `notion.read_page`, `notion.append_blocks` | READ, READ, WRITE | #198 | +| **Slack** | webhook sortie + bot token lecture REST | `slack.send`, `slack.search_messages` | WRITE, READ | option future | +| **Gitea / GitHub** | token (existant) | migrés sous le cadre | READ | #195 (migration) | +| **Todoist / tasks externes** | token | — | — | hors v1 ; les tâches restent **dans le vault** (checklists) | + +Règle transversale : **la prise de notes reste le vault**. Les connecteurs *nourrissent* +l'assistant (contexte) et reçoivent des *actions* (événement, brouillon, message) ; ils +ne remplacent jamais la note. C'est l'inverse des apps chat généralistes. + +### 7.3 Utilisation des connecteurs par l'assistant + +- L'agent choisit les outils selon les demandes (tool-calling existant, `tool_choice=auto`). +- **Contexte briefing** (§8.2) : agrège `gcal.list_events(jour)` + rappels + mails + non lus (si Gmail connecté) + notes modifiées → note de briefing. +- **Demande type** : « est-ce que j'ai une réponse du dentiste ? » → + `gmail.search_messages(from:...)` → cite le mail → propose la note. +- **Confidentialité** : le contenu des connecteurs entre dans la conversation mais + **jamais** dans la consolidation mémoire par défaut (seuls les faits *explicitement* + confirmés par l'utilisateur via `memory_save` entrent en base). + +### 7.4 UI « Sources connectées » (#103 étendue) + +Page Configurations : carte par connecteur (statut, scopes, bouton Connect/Test/ +Déconnecter), journal des derniers appels (audit filtré), budget tokens. Textes FR/EN. + +--- + +## 8. Rappels, briefing, agent programmé — #199 (rappels), #200 (agent+briefing) + +### 8.1 Rappels conversationnels — #199 + +Le scheduler #170 sait déjà planifier `notify`/`create_file`. Les rappels = **action +typée de haut niveau**, pour que l'assistant puisse les gérer naturellement : + +- Nouvelle action scheduler `reminder` : `{user_id, text, deliver: [channels…], + recurring: none|daily|weekly|cron}` → au due : `notify.broadcast` + Web Push + + append à `99_Assistant/Rappels.md` (historique `Rappels.md` → section « tenus »). +- Outils : `reminder_create` (WRITE, `{texte, quand (ISO ou relatif « in 3 weeks » + résolu côté serveur par la LLM→date), récurrence, canaux}`), `reminder_list` (READ), + `reminder_snooze` (WRITE, +`{task_id, delta}` — le snooze bump `last_snoozed_count`, + signal pour #201), `reminder_cancel` (WRITE). +- UX : panneau Assistant « Cloche » + section latérale dédiée ; les rappels dus sont + injectés en contexte mémoire (§5.5) pour que l'assistant puisse en parler de lui-même. + +### 8.2 Agent programmé — nouveau type d'action `agent_prompt` — #200 + +Brique générique manquante : le scheduler doit pouvoir **lancer le run_agent** avec un +prompt, pas seulement des actions primitives. + +- Sécurité de la tâche non interactive (pas de confirmation humaine possible) : + **whitelist de capacités** — READ sans limite ; WRITE restreint aux chemins + `99_Assistant/**` (le prompt d'un scheduled-agent ne peut écrire ailleurs) ; + DANGEROUS et `connectors.*` en écriture **interdits**. Paramètre + `allowed_extra_tools: []` pour élargir explicitement à la création. +- Plafonds : max 8 itérations d'outils, budget tokens par tâche, `last_error` + + broadcast `schedule_failure` (déjà en place), `last_run_at` persisté. +- Le **briefing du matin** = built-in : skill `/brief` + tâche `daily_time 07:00` + (fuseau `OBSIGATE_TZ`) → prompt agrégateur (§7.3) → écrit `Briefings/YYYY-MM-DD.md` + + notification (canaux de l'utilisateur + Web Push). +- Autres prompts programmatifs naturels : « résumé hebdo des notes modifiées », + « veille » (crawl d'une page + diff dans une note) — mais chacun reste une tâche + créée par l'utilisateur ou l'assistant, pas de magie par défaut. + +### 8.3 Le job de consolidation passe par `agent_prompt` + +#194 s'implémente comme tâche built-in `daily_time 23:30` + whitelist +`memory.*`/`profile_get` — l'infrastructure d'ordonnancement de #200 sert la mémoire. +(Ordre de livraison : #200 avant #194, cf. §10.) + +--- + +## 9. Retrait d'Ollama — #191 + +Objectif utilisateur : ObsiGate ne doit pas hériter de l'infra (ni Ollama, ni Honcho) ; +et le conteneur Ollama (1,4 Go+ de poids, 2 modèles, RAM) ne sert plus. + +1. `docker-compose.yml` : supprimer le service `ollama`, le volume `ollama_data`, + les vars `OLLAMA_BASE_URL`/`OLLAMA_MODEL`, et la mention « port 11434 » de l'en-tête. + Idem `docker-compose.test-linux.yml` / `docker-compose.test-win.yml`. +2. `backend/ai.py` / `model_capabilities.py` : **conserver** le provider `ollama` + (option de configuration, utile au desktop hors-ligne qui pointe vers un ollama + local de l'utilisateur) mais retirer tout défaut pointant vers le réseau interne. + Les tests d'isolation : `OBSIGATE_*` ne référence plus `http://ollama:11434`. +3. Embeddings : par **sentence-transformers local optionnel** (déjà #70) — le mode + par défaut de l'Assistant V2 fonctionne **sans embeddings** (TF-IDF + RRF + faits + SQLite), les embeddings étant un raffinement auto-détecté (`SemanticIndex` existe, + avec fallback API provider si configurée). +4. Migration : `docker compose down && up -d` (règle .env connue P17/P29) ; supprimer + le volume `ollama_data` (procédure documentée dans le CHANGELOG + ROADMAP de la tache #191). + +--- + +## 10. Phasage & IDs + +> Chaque tranche est livrable indépendamment, CI verte + DoD `DELIVERY_WORKFLOW.md`, +> i18n FR/EN, tests, guide utilisateur `docs/GUIDES/`. + +| ID | Tranche | Contenu | Effort | Dépend | Critères d'acceptation | +|---|---|---|---|---|---| +| #191 | **Purge Ollama** | compose ×3, docs, notes migration | 0,5 j | — | plus aucun conteneur ollama ; AI editor + agent OK sur deepseek/openrouter/gemini | +| #192 | **Mémoire socle** | `memory/store.py`, `recall.py`, 5 outils `@tool`, tests | 3–4 j | #191 | recall/save/update/forget fonctionnent en chat ; audit + confirmation OK | +| #193 | **Miroir markdown** | `mirror.py`, `99_Assistant/`, balises gérées, re-parse edits | 2–3 j | #192 | édition manuelle de Profil.md → faits `source='user'` ; réécritures idempotentes | +| #200 | **Agent programmé + briefing** | action `agent_prompt` (whitelist), skill `/brief`, tâches built-in | 3–4 j | #192 | briefing 07:00 → note + notif ; WRITE limité `99_Assistant/**` prouvé par tests | +| #194 | **Consolidation nocturne** | `consolidator.py`, incrémental, pending/cartes, budget tokens | 3–4 j | #200, #193 | 1 nuit test → ≥1 fait utile extrait ; aucun secret extrait (redaction test) ; budget respecté | +| #199 | **Rappels** | action `reminder`, 4 outils, UI cloche, snooze-count | 2–3 j | #200 | « rappelle-moi dans 3 semaines » → rappel réel ; snooze ≥3 → signal visible #201 | +| #195 | **Cadre connecteurs** | `connectors/` base+oauth+registry, UI Sources, migration Gitea/GitHub | 4–5 j | — | OAuth2 PKCE de bout en bout avec un provider de test ; secrets 0600 | +| #196 | **Google Calendar + Gmail** | capabilities §7.2, context briefing étendu | 3–4 j | #195 | agenda du jour cité dans briefing ; recherche mail via chat avec confirmation WRITE nulle | +| #197 | **Microsoft Teams/Outlook** | Graph client min, capabilities READ + send | 3–4 j | #195 | lire un chat Teams et y répondre via carte de confirmation | +| #198 | **Discord read + Notion + Drive** | capacités restantes §7.2 | 3–5 j | #195 | import page Notion → note vault ; `drive.read_file` OK | +| #201 | **Proactivité / insights** | détecteurs stdlib (open loops, snoozes, pourrissement TODO, récurrences) → `Insights.md` + suggestions en contexte | 3–4 j | #194, #199 | un scenario « note à faire > 21 jours » et « snooze ×3 » génèrent des insights réels et acceptables/refusables | + +Ordre recommandé : `#191 → #192 → #193 → #200 → #194 → #199` (la mémoire et le +briefing rendent l'assistant utile **sans** connecteur) puis connecteurs `#195 → #196…` +et enfin `#201` (le coaching a besoin de la mémoire garnie). + +--- + +## 11. Sécurité & confidentialité (rappel explicite) + +- **Secrets** : uniquement `data/connector_secrets.json` (0600, chiffrés `secret.key`) + ou env `OBSIGATE_CONNECTOR_SECRET_*` — jamais le compose, jamais git (#168/#103). +- **SSRF** : toutes les URLs configurées par l'utilisateur passent + `validate_webhook_url`/`is_safe_target` ; les endpoints des providers OAuth sont + une allowlist codée (accounts.google.com, login.microsoftonline.com…). +- **Chiffrement au repos** optionnel : même pattern que #172 (backlog existant) — + si activé, s'applique à `connector_secrets.json` + `memory.sqlite3`. +- **Redaction** : `redact_file_content()` systématique avant LLM pour toute note + consolidée ; jamais le contenu brut des connecteurs dans les prompts hors + appel d'outil explicite. +- **Consentement par tâche planifiée** : les tâches `agent_prompt` listent leurs + capacités autorisées dans l'UI, avec journal `last_run` dépliable. +- **Effacement** : `memory_forget` + purge des sessions #95 + export de la base + (`GET /api/memory/export` JSON) → réversibilité complète RGPD-like. + +--- + +## 12. Risques & arbitrages assumés + +| Risque | Mitigation | +|---|---| +| Qualité variable de l'extraction de faits → mémoire « bruitée » | seuil confiance + pending/cartes ; dossiers sensibles → validation manuelle ; `last_seen` decay ; effacement facile | +| Coût LLM (consolidation + briefing quotidiens) | cheap model dédié par var `OBSIGATE_AGENT_BACKGROUND_MODEL` ; budgets durs en tokens ; aggregation déterministe (stdlib) avant LLM | +| Dérive du scheduler qui exécute des prompts non surveillés | whitelist capacités + budget itérations + plafond chemins ; journal d'audit par tâche | +| Élargissement du scope backend (main.py déjà volumineux) | nouveaux modules dédiés (`memory/`, `connectors/`) ; aucune logique dans `main.py` ; routers séparés | +| OAuth app Google/Microsoft à créer manuellement (client id) | guide `docs/GUIDES/CONNECTEURS_GOOGLE_MS.md` pas-à-pas FR/EN ; possible de rester sur tokens statiques Notion/GitHub sans projet cloud | +| Éditions humaines du miroir non re-parsables (markdown libre) | balises `managed` strictes ; sections hors-balises intouchables ; re-parse = faits, pas diff sémantique | +| Tauri desktop + connecteurs = surface réseau | capabilities absentes du registre tant que non installées ; offline-first global inchangé | + +--- + +## 13. Définition de terminé (par tranche, conforme DoD dépôt) + +Pour chaque ID : tests unitaires backend (+tests de non-régression pour tout bug), +tests JSDOM si UI, E2E Playwright si flux UI touché, ruff/mypy 0 erreur, i18n FR/EN, +labels `ai.step.*` pour tout nouvel outil, mise à jour `CHANGELOG.md` `[Unreleased]`, +`docs/ROADMAP.md` (statut + fiche `docs/features/assistant-v2-.md` par tranche), +guide utilisateur `docs/GUIDES/` si exposé à l'utilisateur, CI verte, commit conventionnel +référençant l'ID. + +--- + +## Annexe A — Variables d'environnement nouvelles + +``` +# Mémoire +OBSIGATE_MEMORY_ENABLED=true +OBSIGATE_MEMORY_VAULT=Main # vault où vit 99_Assistant/ +OBSIGATE_MEMORY_REVIEW_FOLDERS=Santé,Famille # forcent status=pending +OBSIGATE_CONSOLIDATION_MODEL= # cheap model, défaut = provider courant +OBSIGATE_CONSOLIDATION_MAX_TOKENS=30000 +# Briefing / agent programmé +OBSIGATE_TZ=America/Toronto +OBSIGATE_BRIEFING_TIME=07:00 +# Connecteurs (ou UI Sources connectées — stockage prioritaire) +GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET # app Google Cloud (consent screen) +AZURE_CLIENT_ID / AZURE_CLIENT_SECRET # app Entra ID +OBSIGATE_NOTION_API_KEY= +OBSIGATE_DISCORD_BOT_TOKEN= # lecture REST uniquement +``` + +## Annexe B — Prompt de consolidation (extrait) + +``` +Tu extrais des faits durables à partir de notes et conversations récentes d'un +journal personnel. Pour chaque fait : subject, predicate, value (une phrase, +français), kind ∈ preference|decision|entity|event|habit|goal|open_loop, +confidence 0..1. Règles : +- Uniquement ce qui est explicitement affirmé ou fortement implicite ; +- Rien de sensible sans appui textuel direct (santé, finances, familles) ; +- Jamais de mot de passe, token, numéro personnel (signalés comme refusés) ; +- Si un fait contredit un fait existant, produit le nouveau avec note de supersede. +Sortie : JSON strict {facts: [...]} — aucun commentaire. +``` + +## Annexe C — Prompt du briefing (extrait) + +``` +Contexte fourni : agenda du jour (gcal), rappels dus, mails non lus importants +(gmail si connecté), notes modifiées hier, faits pending à valider. +Produis une note markdown Briefings/YYYY-MM-DD.md : +- Aujourd'hui : événements + rappels (liens vault si applicable) +- À trancher : 2–4 questions ouvertes détectées dans les notes récentes +- Mémoire : 1–3 faits nouveaux à valider (citez la source note) +Pas d'invention. Chaque affirmation cite sa source (chemin ou sujet connecteur). +```