36 KiB
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 :
- 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).
- Il agit — lit/écrit le vault, mais aussi les services externes connectés : Gmail, Google Agenda, Teams, Discord, Notion, Drive… (cadre de connecteurs unifié).
- Il anticipe — rappels conversationnels, briefing quotidien généré par l'agent, détection de boucles ouvertes et d'engagements non tenus.
- 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'anciennesuperseded(viasupersedes, statusexpired). Historique conservé, recall = dernierconfirmednon expiré. - Renforcement :
last_seen_atbump quand le fait est réaffirmé en chat ; les facts non revus depuis N jours (180) et nondecisiondeviennentexpired. - PII santé/famille :
consolidatorforcestatus='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 :
- Collecte incrémentale : sessions
ai_historycréées/modifiées depuis le dernier run + fichiers.mdmodifiés depuis le dernier run (via l'index/watcher mtime) — plafonds : 200 fichiers × 30 Ko (comme bookslm), contenuredact_file_content()avant LLM. - 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. - Réconciliation (déterministe, stdlib) : dédup/supersede par
(subject,predicate), plancher de confiance, dossiers sensibles → pending. - Écriture :
facts+mirror.py→ sections gérées deProfil.md. - Cartes de validation : les faits
pendingdeviennent 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
@toolsont 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 dememory.*,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_toolsfiltré 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 dansdata/connector_secrets.json(0600) ouOBSIGATE_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 + stockagerefresh_tokenchiffré avecdata/secret.keyexistant) →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 (patternwebcache.py, TTL 5 min pour le contexte de chat). - Audit : chaque
call()journalisé (connecteur, capability, args tronqués, latence, statut) viatools/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_saveentrent 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(historiqueRappels.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 bumplast_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 etconnectors.*en écriture interdits. Paramètreallowed_extra_tools: []pour élargir explicitement à la création. - Plafonds : max 8 itérations d'outils, budget tokens par tâche,
last_error+ broadcastschedule_failure(déjà en place),last_run_atpersisté. - Le briefing du matin = built-in : skill
/brief+ tâchedaily_time 07:00(fuseauOBSIGATE_TZ) → prompt agrégateur (§7.3) → écritBriefings/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.
docker-compose.yml: supprimer le serviceollama, le volumeollama_data, les varsOLLAMA_BASE_URL/OLLAMA_MODEL, et la mention « port 11434 » de l'en-tête. Idemdocker-compose.test-linux.yml/docker-compose.test-win.yml.backend/ai.py/model_capabilities.py: conserver le providerollama(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 plushttp://ollama:11434.- 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é (
SemanticIndexexiste, avec fallback API provider si configurée). - Migration :
docker compose down && up -d(règle .env connue P17/P29) ; supprimer le volumeollama_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 utilisateurdocs/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éssecret.key) ou envOBSIGATE_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_promptlistent leurs capacités autorisées dans l'UI, avec journallast_rundépliable. - Effacement :
memory_forget+ purge des sessions #95 + export de la base (GET /api/memory/exportJSON) → 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).