Ver código fonte

Mémoire des membres : sans réaction 📝, documentation finalisée

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 3 dias atrás
pai
commit
4fb058562d
5 arquivos alterados com 65 adições e 74 exclusões
  1. 6 4
      README.md
  2. 7 11
      bot/claude_client.py
  3. 0 13
      bot/discord_bot.py
  4. 1 2
      bot/memory.py
  5. 51 44
      changelog.txt

+ 6 - 4
README.md

@@ -63,11 +63,11 @@ Il te faut :
    - dans **Privileged Gateway Intents**, active **Message Content Intent**. Les intents « Presence » et « Server Members » ne servent pas.
 3. Onglet **OAuth2 > URL Generator** :
    - coche les scopes **bot** et **applications.commands** (ce dernier pour les commandes comme `/reset` et `/memoire`) ;
-   - dans les permissions, coche **View Channels**, **Send Messages** et **Read Message History**, plus **Add Reactions** avec `MEMORY=on` ;
+   - dans les permissions, coche **View Channels**, **Send Messages** et **Read Message History** ;
    - ouvre l'adresse générée et invite le bot sur ton serveur.
 4. Dans Discord, active le mode développeur (**Paramètres utilisateur > Avancés > Mode développeur**), puis fais un clic droit sur le salon du bot > **Copier l'identifiant du salon** : c'est le `CHANNEL_ID`.
 
-Le bot n'a besoin d'aucune autre permission : il n'utilise que les emojis et stickers du serveur. Pour un bot déjà invité, **Add Reactions** s'ajoute à son rôle dans les paramètres du serveur.
+Le bot n'a besoin d'aucune autre permission : il n'utilise que les emojis et stickers du serveur.
 
 ## Installation
 
@@ -104,6 +104,7 @@ Tous les réglages se trouvent dans le `.env`, et chacun est expliqué dans `.en
 | `CONTEXT_MESSAGE_LIMIT` | Messages lus autour d'une mention | `20` |
 | `WEB_SEARCH` | Recherche web et lecture de liens (`on` ou `off`) ; prévois alors un `API_TIMEOUT` d'au moins 90 secondes | `off` |
 | `PRESENCE` | Statut Discord et absences (`on` ou `off`) ; les `PRESENCE_*` règlent les chances de chaque statut et la nuit | `on` |
+| `MEMORY` | Mémoire des membres (`on` ou `off`) ; `MEMORY_MEMBER_LIMIT` et `MEMORY_SERVER_LIMIT` règlent la taille des fiches et du fichier général (3000 et 5000 caractères) | `off` |
 | `PDF_MAX_PAGES` | Pages maximales d'un PDF (0 = PDF refusés) | `20` |
 | `PERSONALITY_FILE` | Fichier de la personnalité | `personality_prompt.txt` |
 | `TIMEZONE` | Fuseau horaire de l'heure donnée à Claude | `Europe/Paris` |
@@ -122,7 +123,7 @@ Pas besoin d'y recopier la liste des emojis du serveur : le bot la donne lui-mê
 
 Le `Dockerfile` construit une image prête à lancer. À chaque push sur `main`, GitHub Actions la construit et la publie sur Docker Hub (`<compte>/chatbot-maisonnette`, avec les secrets `DOCKER_USERNAME` et `DOCKER_PASSWORD` du dépôt).
 
-L'image ne contient ni `.env`, ni personnalité `*.local.txt`, ni historique. Les réglages se passent en variables d'environnement, et les fichiers à garder entre deux redémarrages doivent être sur un volume, par exemple :
+L'image ne contient ni `.env`, ni personnalité `*.local.txt`, ni historique, ni mémoire des membres. Les réglages se passent en variables d'environnement, et les fichiers à garder entre deux redémarrages doivent être sur un volume. Le dossier `memories/` se crée à côté de `HISTORY_FILE`, donc sur le même volume. Par exemple :
 
 ```
 docker run -d --name chatbot \
@@ -142,7 +143,8 @@ Chaque message envoyé à Claude est facturé selon le nombre de tokens lus (per
 - la personnalité et la mémoire sont mises en cache : relues dans les 5 minutes, elles coûtent 10 % du prix normal ;
 - les images, PDF et fichiers ne sont payés qu'une fois, au message qui les contient ;
 - `claude-haiku-5-5` coûte environ 20 fois moins cher que `claude-sonnet-5-5` ; `THINKING=off` et un `EFFORT` bas réduisent aussi les réponses ;
-- chaque page de PDF coûte souvent plusieurs milliers de tokens, et chaque recherche web est payante en plus.
+- chaque page de PDF coûte souvent plusieurs milliers de tokens, et chaque recherche web est payante en plus ;
+- avec `MEMORY=on`, les fiches jointes sont relues à chaque message, hors cache : environ 850 tokens par fiche pleine de 3000 caractères. Chaque note demande en plus un court appel, la conversation étant alors relue en cache.
 
 Le fichier `bot.log` (`LOG_FILE`) garde l'activité du bot, avec pour chaque appel les tokens utilisés et le coût estimé en dollars. Les tarifs de Sonnet, Haiku et Opus 5.5 sont connus du bot ; pour un autre modèle, renseigne `PRICE_INPUT_PER_MTOK` et `PRICE_OUTPUT_PER_MTOK`.
 

+ 7 - 11
bot/claude_client.py

@@ -70,7 +70,8 @@ MEMBER_MEMORY_NOTE = (
     "général sur le serveur (blagues récurrentes, événements, habitudes). Quand tu apprends sur quelqu'un "
     "quelque chose qui vaut la peine d'être retenu, note-le dans sa fiche, quelle que soit la personne qui "
     "te l'apprend ; une info donnée par quelqu'un d'autre que l'intéressé se note avec sa source "
-    "(« d'après Toto »). Inutile de dire dans ta réponse tout ce que tu retiens. "
+    "(« d'après Toto »). Ne dis pas que tu notes quelque chose : c'est plus tard, quand tu t'en souviens, qu'on doit "
+    "découvrir que tu l'as retenu. "
     "Écris en style télégraphique, une ligne par fait, sans doublon, et mets à jour la ligne existante plutôt "
     "que d'en ajouter une (« jeu préféré : Zelda » devient « jeu préféré : Elden Ring »). La première ligne "
     "d'une fiche est « Surnoms : … » : complète-la dès que tu comprends qu'un surnom désigne ce membre. "
@@ -160,7 +161,6 @@ class ImageData:
 class Answer:
     text: str
     web_used: bool  # Claude a fait au moins une recherche web ou lu une page
-    memory_written: bool = False  # Claude a noté quelque chose dans sa mémoire
 
 
 class ClaudeClient:
@@ -293,7 +293,6 @@ class ClaudeClient:
 
         responses = [await self._call(params, attachments)]
         pauses = operations = 0
-        memory_written = False
         while True:
             last = responses[-1]
             # Lors de longues recherches, l'API peut rendre la main avant la fin (pause_turn) :
@@ -304,9 +303,8 @@ class ClaudeClient:
             # Appel à l'outil « memoire » : le bot l'exécute et renvoie le résultat. Une fois la limite
             # d'opérations atteinte, un dernier tour lui demande de répondre, puis la boucle s'arrête.
             elif last.stop_reason == "tool_use" and self.memory and operations <= self.settings.memory_max_operations:
-                results, written = await self._run_tools(last, operations)
+                results = await self._run_tools(last, operations)
                 follow_up = [{"role": "user", "content": results}]
-                memory_written = memory_written or written
                 operations += len(results)
             else:
                 break
@@ -331,14 +329,13 @@ class ClaudeClient:
         if not text:
             logger.warning("Réponse vide de Claude (stop_reason=%s).", stop_reason)
             raise ClaudeError("Je n'ai rien trouvé à répondre à ça.")
-        return Answer(text, web_used, memory_written)
+        return Answer(text, web_used)
 
-    async def _run_tools(self, response, done: int) -> tuple[list[dict], bool]:
+    async def _run_tools(self, response, done: int) -> list[dict]:
         """Exécute les appels à l'outil « memoire » d'une réponse. Renvoie leurs résultats, à envoyer
-        tous dans un même message comme l'exige l'API, et si un fichier a été modifié.
+        tous dans un même message comme l'exige l'API.
         done : opérations déjà faites pour cette réponse."""
         results = []
-        written = False
         for block in response.content:
             if block.type != "tool_use":
                 continue
@@ -350,10 +347,9 @@ class ClaudeClient:
             else:
                 result = await self.memory.run(block.input)
             done += 1
-            written = written or result.written
             results.append({"type": "tool_result", "tool_use_id": block.id, "content": result.content,
                             "is_error": result.is_error})
-        return results, written
+        return results
 
     async def write_custom_status(self, kind: str, now: str, duration: str, recent: list[str],
                                   max_length: int) -> str | None:

+ 0 - 13
bot/discord_bot.py

@@ -42,8 +42,6 @@ QUOTE_MAX_LENGTH = 500
 USER_MESSAGE_TYPES = (discord.MessageType.default, discord.MessageType.reply)
 # En passant absent ou en « Ne pas déranger », le bot prévient son salon si on y a parlé depuis moins de :
 DEPARTURE_WINDOW = timedelta(minutes=3)
-# Ajoutée au message auquel le bot répond quand il a noté quelque chose dans sa mémoire.
-NOTED_REACTION = "📝"
 # Envoyé une seule fois à chaque membre, à son premier message, avec MEMORY=on.
 WELCOME = ("{name} peut retenir des choses sur toi au fil des conversations (tes goûts, tes projets…) "
            "pour mieux te connaître. Tape /memoire sur le serveur pour voir ce qui a été retenu, le modifier "
@@ -406,8 +404,6 @@ class ChatBot(commands.Bot):
                 self.forget_waiting(*(p.message for p in batch))
             await self.history.add_exchange(channel.id, lines, short_emojis(answer.text), answer.web_used)
         await self.send_answer(last, answer.text)
-        if answer.memory_written:
-            await self.mark_noted(last)
 
     async def handle_mention(self, message: discord.Message) -> None:
         if not self.available() or message.channel.id in self._waiters:
@@ -493,8 +489,6 @@ class ChatBot(commands.Bot):
                 return
         # En retard, la réponse cite la mention : on sait à quoi elle répond.
         await self.send_answer(message, answer.text, reply=late is not None)
-        if answer.memory_written:
-            await self.mark_noted(message)
 
     # ------------------------------------------------------ réponses différées
 
@@ -831,13 +825,6 @@ class ChatBot(commands.Bot):
                 logger.error("Envoi de la réponse impossible : %s", exc)
                 return
 
-    async def mark_noted(self, message: discord.Message) -> None:
-        """Réaction 📝 sur le message auquel le bot vient de répondre : il a noté quelque chose."""
-        try:
-            await message.add_reaction(NOTED_REACTION)
-        except discord.HTTPException as exc:  # permission « Ajouter des réactions » absente, message supprimé…
-            logger.warning("Réaction %s impossible : %s", NOTED_REACTION, exc)
-
     async def safe_reply(self, message: discord.Message, text: str) -> None:
         try:
             await message.reply(text, mention_author=False)

+ 1 - 2
bot/memory.py

@@ -51,7 +51,6 @@ MEMORY_TOOL = {
 class ToolResult:
     content: str
     is_error: bool = False
-    written: bool = False  # un fichier a été modifié
 
 
 class MemoryStore:
@@ -184,7 +183,7 @@ class MemoryStore:
                 logger.error("Mémoire : impossible d'écrire %s : %s", name, exc)
                 return ToolResult("Écriture impossible pour l'instant, à cause d'une erreur du bot.", is_error=True)
         logger.info("Mémoire : %s sur %s (%s), %d → %d caractères", action, name, label, len(before), len(after))
-        return ToolResult(f"C'est fait. {name} : {len(after)}/{limit} caractères.", written=True)
+        return ToolResult(f"C'est fait. {name} : {len(after)}/{limit} caractères.")
 
     def limit(self, name: str) -> int:
         return self.server_limit if name == SERVER_FILE else self.member_limit

+ 51 - 44
changelog.txt

@@ -6,55 +6,62 @@ Version 5.10
 ------------
 
 Nouveautés
-- Mémoire des membres (MEMORY=on, en construction) : le bot tient un
-  registre des membres qu'il voit : auteurs des messages qu'il lit,
-  membres mentionnés et auteurs des messages cités, sans les bots. Pour chacun, il garde le
-  nom d'utilisateur, le nom d'affichage, le pseudo sur le serveur et les
-  5 derniers anciens pseudos, mis à jour à chaque message. Le registre
-  est dans memories/registre.json, à côté de l'historique (MEMORY_DIR
-  pour un autre dossier), et n'est réécrit que si quelque chose change.
-  Rika tient une fiche par membre (memories/membres/<id>.md) et un
-  fichier général du serveur (memories/serveur.md), avec un outil
-  « memoire » exécuté par le bot : lire, ajouter, remplacer ou réécrire,
-  dans ces seuls fichiers. Elle note en style télégraphique, une ligne
-  par fait, avec la source quand l'info vient d'un autre membre. Une
-  fiche est limitée à 3000 caractères (MEMORY_MEMBER_LIMIT) et le
-  fichier général à 5000 (MEMORY_SERVER_LIMIT) : une note de trop est
-  refusée, et Rika réécrit alors le fichier en plus court. Au plus 5
-  lectures ou écritures par réponse (MEMORY_MAX_OPERATIONS). Quand elle
-  a noté quelque chose, elle ajoute 📝 au message auquel elle répond
-  (permission « Ajouter des réactions » nécessaire). Outil maison plutôt
-  que celui d'Anthropic, qui lui ferait relire tout le dossier avant
-  chaque réponse (un appel de plus à chaque message).
-  Le bot joint lui-même à chaque message les fiches des membres
-  concernés (auteurs, membres mentionnés, auteur du message cité), sans
-  appel supplémentaire ; elles ne sont pas enregistrées dans
-  l'historique, et le cache s'arrête juste avant elles pour que
-  l'historique reste relu en cache au message suivant. Le prompt
-  contient aussi la liste des membres, avec tous leurs noms (pseudo,
-  nom d'utilisateur, surnoms de la fiche, anciens pseudos) et le chemin
-  de leur fiche, ce qui permet à Rika de noter une info sur quelqu'un
-  cité par son seul nom ou surnom ; ainsi que le fichier général. Les
-  membres qui ont désactivé la mémoire n'y figurent pas.
+- Mémoire des membres (MEMORY=on) : Rika tient une fiche sur chaque
+  membre et un fichier général du serveur (blagues récurrentes,
+  événements, habitudes). Elle décide seule de ce qui mérite d'être
+  retenu, quelle que soit la personne qui le lui apprend, et note la
+  source quand l'info vient d'un autre membre (« d'après Toto »). Elle
+  écrit en style télégraphique, une ligne par fait, sans doublon, et met
+  à jour une ligne plutôt que d'en ajouter une. Elle ne signale pas ce
+  qu'elle note : on le découvre plus tard, quand elle s'en souvient.
+- Ce que Rika voit : le bot joint lui-même à chaque message les fiches
+  des membres concernés (auteurs, membres mentionnés, auteur du message
+  cité), sans appel supplémentaire. Son prompt contient aussi le fichier
+  général et la liste des membres, avec tous leurs noms (pseudo, nom
+  d'utilisateur, surnoms notés en tête de fiche, anciens pseudos) et le
+  chemin de leur fiche : elle peut ainsi noter une info sur quelqu'un
+  cité par son seul nom ou surnom, et lire sa fiche si besoin. Les
+  fiches jointes ne sont pas enregistrées dans l'historique, et le cache
+  s'arrête juste avant elles : l'historique reste relu en cache.
+- Comment elle écrit : avec un outil « memoire » exécuté par le bot
+  (lire, ajouter, remplacer, réécrire), limité à ces seuls fichiers. Une
+  fiche fait au plus 3000 caractères (MEMORY_MEMBER_LIMIT) et le fichier
+  général 5000 (MEMORY_SERVER_LIMIT) : une note de trop est refusée, et
+  Rika réécrit alors le fichier en plus court, en fusionnant et en
+  retirant le moins important. Au plus 5 lectures ou écritures par
+  réponse (MEMORY_MAX_OPERATIONS). Outil maison plutôt que celui
+  d'Anthropic, qui lui ferait relire tout le dossier avant chaque
+  réponse, soit un appel de plus à chaque message.
 - /memoire : chaque membre voit sa fiche, visible par lui seul, et peut
   la modifier dans une fenêtre déjà remplie, tout effacer ou désactiver
-  la mémoire (avec confirmation : sa fiche est alors effacée, plus rien
-  n'est retenu sur lui ni joint à ses messages). Il peut la réactiver au
-  même endroit. Les administrateurs (permission Administrateur) voient,
+  la mémoire (avec confirmation : sa fiche est effacée, plus rien n'est
+  retenu sur lui ni joint à ses messages). Il peut la réactiver au même
+  endroit. Les administrateurs (permission Administrateur) voient,
   modifient et effacent la fiche de n'importe qui avec
   /memoire membre:@… ; seul le membre lui-même peut désactiver sa
   mémoire. /memoire-serveur, réservé aux administrateurs, fait de même
-  pour le fichier général. Avec MEMORY=off, les deux commandes le
-  signalent. MEMORY_MEMBER_LIMIT et MEMORY_SERVER_LIMIT ne peuvent pas
-  dépasser 20000 (taille maximale de la fenêtre de modification).
-- Présentation de la mémoire : au premier message d'un membre, le bot
-  lui envoie une seule fois un message privé qui explique ce qu'il peut
-  retenir et comment le voir, le modifier ou le désactiver avec
-  /memoire. Si ses messages privés sont fermés, le message est posté
-  dans le salon, avec son nom en mention mais sans notification, et
-  s'efface au bout d'une minute. Les membres déjà actifs sont prévenus
-  à leur premier message après le passage à la 5.10 ; un membre qui a
-  déjà utilisé /memoire ne l'est pas.
+  pour le fichier général.
+- Présentation : au premier message d'un membre, le bot lui envoie une
+  seule fois un message privé qui explique ce qu'il peut retenir et
+  comment le voir, le modifier ou le désactiver avec /memoire. Si ses
+  messages privés sont fermés, le message est posté dans le salon, avec
+  son nom en mention mais sans notification, et s'efface au bout d'une
+  minute. Les membres déjà actifs sont prévenus à leur premier message
+  après le passage à la 5.10 ; un membre qui a déjà utilisé /memoire
+  ne l'est pas.
+- Registre des membres : le bot garde, pour chaque membre vu, son nom
+  d'utilisateur, son nom d'affichage, son pseudo sur le serveur et ses
+  5 derniers anciens pseudos, mis à jour à chaque message, ainsi que ses
+  réglages de la mémoire. Tout est dans le dossier memories/, à côté de
+  l'historique (MEMORY_DIR pour un autre dossier) : registre.json,
+  serveur.md et membres/<id>.md. Avec Docker, il est donc sur le même
+  volume que l'historique.
+- Mise en service : MEMORY=on dans le .env (ou la ConfigMap), sans
+  nouvelle permission Discord. Avec MEMORY=off (par défaut), rien ne
+  change, et /memoire et /memoire-serveur indiquent que la mémoire est
+  coupée.
+  MEMORY_MEMBER_LIMIT et MEMORY_SERVER_LIMIT ne peuvent pas dépasser
+  20000 (taille maximale de la fenêtre de modification).
 
 Divers
 - /reset et /skip sont réservés aux membres qui ont la permission