Files
ObsiGate/docs/ASSISTANT-V2.md
bruno 4cdea956d7
CI / lint (push) Successful in 2m48s
CI / security (push) Successful in 1m35s
CI / test (push) Successful in 4m27s
CI / build (push) Successful in 1m26s
CI / e2e (push) Successful in 16m41s
docs: spec Assistant V2 — second cerveau (mémoire, connecteurs, proactivité)
2026-10-08 21:01:45 -04:00

36 KiB
Raw Permalink Blame History

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.

CREATE TABLE facts (
  id            TEXT PRIMARY KEY,          -- f-<uuid4>
  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 :

<!-- obsigate:managed:preferences -->
…bloc régénéré…
<!-- /obsigate:managed:preferences -->

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
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 :

<memoried-context>
Profil (résumé géré) : préférences, habitudes, objectifs actifs…
Faits saillants liés au sujet : <memory_recall(query=requête utilisateur)>
Rappels actifs : <n lignes de Rappels.md>
Dernier briefing : <3 puces>
</memoried-context>

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 <texte> (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_<ID> — 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-<slug>.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).