compaction de contexte, filtre/pagination/suppression des conversations

- compaction automatique du contexte LLM quand la taille atteint le seuil
  (contexte max - taille de réponse max), par résumé de l'historique ancien
- filtre textuel (?q=) et pagination (20/page) de l'historique des conversations
- correctif ORM : offset de pagination en base 0
- suppression d'une conversation : messages associés supprimés, session en
  cours annulée, popup de confirmation (ConfirmDialog)
- titres de conversation nettoyés du markdown
- styles des listes markdown, variante danger du bouton
- go.mod/go.sum : dépendances go-rod et stealth manquantes du commit précédent
This commit is contained in:
fabien committed 2026-10-05 23:46:12 +02:00
1 parent a970bfa991
commit 51bbb45b32
86 files changed
+770 -113

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
package chat
import (
"context"
"fmt"
"log/slog"
"strings"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
const (
// charsPerToken : estimation heuristique, ~4 caractères par token pour du
// texte européen. Suffisant pour décider d'une compaction.
charsPerToken = 4
// defaultContextMaxTokens : taille de contexte du modèle quand la
// configuration ne la précise pas.
defaultContextMaxTokens = 32000
// defaultMaxResponseTokens : réserve à ménager pour la réponse du modèle.
defaultMaxResponseTokens = 4096
)
// estimateTokens estime le nombre de tokens d'un texte.
func estimateTokens(text string) int {
if text == "" {
return 0
}
tokens := len(text) / charsPerToken
if len(text)%charsPerToken > 0 {
tokens++
}
return tokens
}
// contextTokens estime la taille en tokens du prompt complet envoyé au LLM.
func (s *ChatSession) contextTokens(systemPrompt string) int {
total := estimateTokens(systemPrompt)
for _, m := range s.messages {
total += estimateTokens(m.Content) + estimateTokens(m.ToolCallsJson)
}
return total
}
// shouldCompact indique si la taille du contexte a atteint la limite au-delà
// de laquelle il ne reste plus assez de place pour une réponse complète :
// contexte max diminué de la taille de réponse max.
func (s *ChatSession) shouldCompact(systemPrompt string) bool {
return s.contextTokens(systemPrompt) >= s.contextMaxTokens-s.maxResponseTokens
}
// compactContext remplace l'historique ancien par un résumé généré par le LLM.
// Les échanges récents (à partir du dernier message utilisateur) sont conservés
// intacts : un point de coupure sur un message utilisateur ne peut jamais
// séparer un message assistant porteur d'appels d'outils de ses réponses tool,
// ce qui garderait un historique incohérent pour l'API.
// La compaction ne porte que sur la session en mémoire : l'historique complet
// reste intact en base.
func (s *ChatSession) compactContext(ctx context.Context) {
cut := -1
for i := len(s.messages) - 1; i >= 0; i-- {
if s.messages[i].Role == string(domain.RoleUser) {
cut = i
break
}
}
// rien à compacter : l'historique commence déjà au dernier message utilisateur
if cut <= 0 {
return
}
head := s.messages[:cut]
tail := s.messages[cut:]
var sb strings.Builder
for _, m := range head {
fmt.Fprintf(&sb, "%s: %s\n", m.Role, m.Content)
}
summary, err := s.llmEngine.Generate(ctx, s.provider, s.modelID, []*domain.Message{
{
Role: string(domain.RoleUser),
Content: "Résume de façon concise et factuelle la conversation suivante. " +
"Conserve les informations essentielles : demandes, décisions, résultats, fichiers ou liens mentionnés. " +
"Réponds en texte brut, sans markdown.\n\n" + sb.String(),
},
})
if err != nil {
slog.Error("compactContext.Generate", "error", err)
return
}
summaryMessage := &domain.Message{
Role: string(domain.RoleUser),
Content: "Résumé de la conversation précédente (les messages plus anciens ont été compactés) :\n" +
summary,
}
s.messages = append([]*domain.Message{summaryMessage}, tail...)
slog.Info("context compacted",
"removedMessages", len(head),
"keptMessages", len(tail)+1,
)
}