- 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
106 lines
3.2 KiB
Go
106 lines
3.2 KiB
Go
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,
|
|
)
|
|
}
|