Le briefing du matin, composé de données — sans un seul appel au modèle.
Constaté le 22 août 2026 : Diapason ne parlait jamais le premier. La routine
morning-digest existait, mais elle demandait au modèle de rédiger, et sur
une machine où Ollama n'a qu'UN créneau, un briefing qui infère est un briefing
qui fait attendre — puis qui invente quand il se trompe.
Or un briefing n'a rien à inventer. « Trois tâches en retard depuis le 19 août,
dont une urgente » est une phrase que les données écrivent toutes seules. Ce
module la compose : instantané, toujours juste, et il ne dispute le GPU à
personne. Le modèle reste disponible pour ce qu'il fait mieux qu'une requête
SQL — répondre à une question.
L'ordre des sections n'est pas cosmétique. Le retard vient EN PREMIER : c'est
la seule chose qu'un briefing peut faire remarquer et qu'on ne verrait pas
autrement. Ce qui est prévu aujourd'hui, l'agenda le montre déjà.
Et quand il n'y a rien, il le dit en une ligne. Un assistant qui meuble pour
justifier son existence apprend à son utilisateur à ne plus le lire.
Classes
Briefing
dataclass
Briefing(titre: str, corps: str, rien_a_signaler: bool)
Ce qu'on montre, et de quoi savoir s'il vaut la peine d'être montré.
Functions:
date_en_francais
date_en_francais(jour: date) -> str
« samedi 22 août ». Sans dépendre du locale, qui est global au processus.
Source code in src/diapason/heartbeat/briefing.py
| def date_en_francais(jour: date) -> str:
"""« samedi 22 août ». Sans dépendre du locale, qui est global au processus."""
return f"{_JOURS[jour.weekday()]} {jour.day} {_MOIS[jour.month - 1]}"
|
composer
composer(
*,
jour: date,
taches_du_jour: Iterable[Mapping[str, Any]] = (),
taches_en_retard: Iterable[Mapping[str, Any]] = (),
habitudes_dues: Iterable[Mapping[str, Any]] = (),
evenements: Iterable[Mapping[str, Any]] = (),
prenom: str = "",
) -> Briefing
Le briefing, à partir de données déjà lues. Aucune entrée/sortie ici.
Séparé de la lecture pour être vérifiable sans base ni agenda : c'est la
composition qu'on veut tenir, pas le pilotage de SQLite.
Source code in src/diapason/heartbeat/briefing.py
| def composer(
*,
jour: date,
taches_du_jour: Iterable[Mapping[str, Any]] = (),
taches_en_retard: Iterable[Mapping[str, Any]] = (),
habitudes_dues: Iterable[Mapping[str, Any]] = (),
evenements: Iterable[Mapping[str, Any]] = (),
prenom: str = "",
) -> Briefing:
"""Le briefing, à partir de données déjà lues. Aucune entrée/sortie ici.
Séparé de la lecture pour être vérifiable sans base ni agenda : c'est la
composition qu'on veut tenir, pas le pilotage de SQLite.
"""
du_jour = [t for t in taches_du_jour if not t.get("done")]
en_retard = list(taches_en_retard)
habitudes = list(habitudes_dues)
rdv = list(evenements)
sections: list[str] = []
# Le retard d'abord : c'est ce qu'on ne verrait pas autrement.
if en_retard:
urgentes = [t for t in en_retard if _urgente(t)]
lignes = []
for t in sorted(en_retard, key=lambda x: (not _urgente(x), _echeance(x))):
depuis = _depuis_quand(_echeance(t), jour)
marque = " (urgent)" if _urgente(t) else ""
lignes.append(f"— {_titre(t)}{marque}{', ' + depuis if depuis else ''}")
entete = f"{len(en_retard)} tâche{'s' if len(en_retard) > 1 else ''} en retard"
if urgentes:
entete += (
f", dont {len(urgentes)} urgente{'s' if len(urgentes) > 1 else ''}"
)
sections.append(entete + " :\n" + "\n".join(_lister(lignes)))
if rdv:
lignes = []
for e in rdv:
heure = str(e.get("start") or e.get("time") or "").strip()
lignes.append(
f"— {heure + ' ' if heure else ''}{_titre(e) or e.get('summary', '')}"
)
sections.append(
f"{len(rdv)} rendez-vous aujourd'hui :\n" + "\n".join(_lister(lignes))
)
if du_jour:
lignes = [f"— {_titre(t)}{' (urgent)' if _urgente(t) else ''}" for t in du_jour]
sections.append(
f"{len(du_jour)} tâche{'s' if len(du_jour) > 1 else ''} prévue"
f"{'s' if len(du_jour) > 1 else ''} aujourd'hui :\n"
+ "\n".join(_lister(lignes))
)
if habitudes:
noms = [str(h.get("name") or h.get("nom") or "").strip() for h in habitudes]
noms = [n for n in noms if n]
if noms:
sections.append("Habitudes du jour : " + ", ".join(noms) + ".")
salut = f"Bonjour{' ' + prenom if prenom else ''}"
entete = f"{salut}. Nous sommes {date_en_francais(jour)}."
if not sections:
# Ne rien meubler. Un assistant qui parle pour exister apprend à son
# utilisateur à ne plus l'écouter.
return Briefing(
titre="Rien de prévu aujourd'hui",
corps=f"{entete} Rien de prévu, rien en retard.",
rien_a_signaler=True,
)
if en_retard:
titre = f"{len(en_retard)} tâche{'s' if len(en_retard) > 1 else ''} en retard"
elif rdv:
titre = f"{len(rdv)} rendez-vous aujourd'hui"
elif du_jour:
titre = f"{len(du_jour)} tâche{'s' if len(du_jour) > 1 else ''} aujourd'hui"
else:
titre = "Habitudes du jour"
return Briefing(
titre=titre,
corps=entete + "\n\n" + "\n\n".join(sections),
rien_a_signaler=False,
)
|
briefing_du_jour
briefing_du_jour(
store: Any = None,
*,
jour: date | None = None,
prenom: str = "",
evenements: Iterable[Mapping[str, Any]] = (),
) -> Briefing
Lit Succès et compose.
Ne lève jamais : un briefing muet vaut mieux qu'une trace d'exception.
Source code in src/diapason/heartbeat/briefing.py
| def briefing_du_jour(
store: Any = None,
*,
jour: date | None = None,
prenom: str = "",
evenements: Iterable[Mapping[str, Any]] = (),
) -> Briefing:
"""Lit Succès et compose.
Ne lève jamais : un briefing muet vaut mieux qu'une trace d'exception.
"""
jour = jour or date.today()
if store is None:
from diapason.succes.workspace import SuccesWorkspaceStore
store = SuccesWorkspaceStore()
try:
du_jour = store.list_tasks(scheduled_date=jour.isoformat())
except Exception: # noqa: BLE001
logger.warning("briefing : tâches du jour illisibles", exc_info=True)
du_jour = []
try:
en_retard = _taches_en_retard(store, jour)
except Exception: # noqa: BLE001
logger.warning("briefing : retards illisibles", exc_info=True)
en_retard = []
try:
habitudes = [
h for h in store.list_habits(on_date=jour.isoformat()) if h.get("due")
]
except Exception: # noqa: BLE001
logger.warning("briefing : habitudes illisibles", exc_info=True)
habitudes = []
return composer(
jour=jour,
taches_du_jour=du_jour,
taches_en_retard=en_retard,
habitudes_dues=habitudes,
evenements=evenements,
prenom=prenom,
)
|