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:
1 parent
a970bfa991
commit
51bbb45b32
86 files changed
+770
-113
No files matched your search
@@ -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,
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user