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

528 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-<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** :
```markdown
<!-- 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 | 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 :
```
<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).
```