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

@@ -0,0 +1,97 @@
package chat
import (
"context"
"strings"
"testing"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type fakeEngine struct{ summary string }
func (f *fakeEngine) ListProviderTypes() []domain.Item { return nil }
func (f *fakeEngine) ListModelsFromProvider(ctx context.Context, p *domain.Provider) ([]string, error) {
return nil, nil
}
func (f *fakeEngine) Stream(ctx context.Context, p *domain.Provider, m string, params *domain.LLMParams, msgs []*domain.Message) *domain.Message {
return &domain.Message{}
}
func (f *fakeEngine) Generate(ctx context.Context, p *domain.Provider, m string, msgs []*domain.Message) (string, error) {
return f.summary, nil
}
func mk(role, content string) *domain.Message {
return &domain.Message{Role: role, Content: content}
}
func TestEstimateTokens(t *testing.T) {
if got := estimateTokens(""); got != 0 {
t.Errorf("empty = %d", got)
}
if got := estimateTokens("abcd"); got != 1 {
t.Errorf("abcd = %d", got)
}
if got := estimateTokens("abcde"); got != 2 {
t.Errorf("abcde = %d", got)
}
}
func TestShouldCompactThreshold(t *testing.T) {
s := &ChatSession{
messages: []*domain.Message{mk(string(domain.RoleUser), strings.Repeat("a", 320))}, // 80 tokens
contextMaxTokens: 100,
maxResponseTokens: 20,
}
if !s.shouldCompact("system") { // 80 + 1 >= 100-20
t.Errorf("should compact at contextMax - maxResponse boundary")
}
s.maxResponseTokens = 40
s.contextMaxTokens = 130 // seuil 130-40=90 > 82 : pas de compaction
if s.shouldCompact("system") {
t.Errorf("should not compact below threshold")
}
}
func TestCompactContext(t *testing.T) {
s := &ChatSession{
provider: &domain.Provider{},
llmEngine: &fakeEngine{summary: "resume des echanges"},
contextMaxTokens: 100,
maxResponseTokens: 10,
messages: []*domain.Message{
mk(string(domain.RoleUser), "ancienne question 1"),
mk(string(domain.RoleAssistant), "ancienne reponse 1"),
mk(string(domain.RoleUser), "question courante"),
mk(string(domain.RoleAssistant), "reponse courante"),
},
}
s.compactContext(context.Background())
if len(s.messages) != 3 {
t.Fatalf("expected 3 messages after compaction, got %d", len(s.messages))
}
if !strings.Contains(s.messages[0].Content, "resume des echanges") {
t.Errorf("first message should be the summary, got %q", s.messages[0].Content)
}
if s.messages[0].Role != string(domain.RoleUser) {
t.Errorf("summary role = %s", s.messages[0].Role)
}
if s.messages[1].Content != "question courante" || s.messages[2].Content != "reponse courante" {
t.Errorf("tail not preserved: %v", s.messages)
}
}
func TestCompactContextNoUserCut(t *testing.T) {
s := &ChatSession{
messages: []*domain.Message{
mk(string(domain.RoleUser), "seule question"),
mk(string(domain.RoleAssistant), "reponse"),
},
}
s.compactContext(context.Background())
if len(s.messages) != 2 {
t.Errorf("nothing to compact, expected 2 messages, got %d", len(s.messages))
}
}