Files
Loki/internal/ajean/chat_prompt_size_test.go
T
nathaninline 6807bad48d v0.8.7 : envoyer et recevoir des fichiers dans le chat
Les fichiers circulent enfin dans les deux sens.

Envoi : trombone dans le composeur, glisser-deposer sur toute la fenetre,
ou Ctrl+V. Le fichier est depose dans uploads/ du dossier de travail de
l'agent et son chemin est annonce au modele, qui en fait ce qu'il veut
avec ses outils. Rien n'est interprete au passage, donc tout type de
fichier passe. Le depot n'a lieu qu'a l'ENVOI du message : retirer un
fichier de la liste ne laisse aucune trace sur le disque. Transport en
base64 dans du JSON et non en multipart, seule forme que le tunnel E2E
dispatche : l'acces distant marche sans code specifique.

Reception : le modele ecrit un lien Markdown ordinaire vers un fichier de
son dossier de travail, et le clic telecharge. Il repondait jusqu'ici par
un chemin serveur, inutilisable depuis un navigateur. Le clic passe par
fetch + blob (la cle de pilotage voyage dans un en-tete qu'une navigation
ne porterait pas, et le chemin de base change derriere le tunnel).
Markdown refusant les espaces non echappes dans une cible de lien, ils
sont encodes avant rendu : sans ca [x](mon rapport.pdf) ne produisait
rien du tout.

Perimetre du telechargement strictement limite au dossier de travail :
'..', chemins absolus et liens symboliques qui en sortent sont refuses,
et le fichier est toujours servi en piece jointe avec nosniff — un .html
produit par le modele ne doit pas s'executer dans l'origine de l'UI, ou
il lirait la cle de pilotage.

Preambule du mode agent : ~2350 -> ~1770 tokens, sans perdre une regle.
Le poids etait dans les schemas d'outils (le double du prompt), pas dans
le prompt. Celui-ci reenumerait des outils que les schemas decrivent
deja, et la regle « write, jamais echo » y figurait trois fois. Trois
tests gardent le budget : ce prompt a deja regrossi ligne par ligne deux
fois.

Aussi : l'export mentionne les pieces jointes (un message sans texte
donnait une section vide), et le decompte de contexte perd le mot
« contexte » au profit du nom du modele.
2026-08-10 17:11:56 +02:00

66 lines
2.4 KiB
Go

package ajean
import (
"encoding/json"
"strings"
"testing"
)
// Budget du préambule envoyé À CHAQUE TOUR : prompt système + schémas des outils.
// Il est payé sur toute la conversation et sort du contexte utile.
//
// Ce test existe pour empêcher le regonflement. Le prompt a déjà été raccourci
// une fois pour une raison de comportement (un préambule verbeux fait
// sur-raisonner les modèles à reasoning, qui finissent leur tour sans appeler
// d'outil), puis une seconde fois pour le coût. Les deux fois, il avait
// regrossi ligne par ligne, chacune paraissant anodine.
//
// Approximation 1 tok ≈ 4 caractères : suffisante pour une alerte, et sans
// dépendance à un tokenizer.
const promptCharBudget = 7500 // ~1875 tokens, tout allumé
func TestSystemPromptStaysLean(t *testing.T) {
caps := Caps{Agent: true, Internet: true, Mem: MemAlways}
sp := baseSystemPrompt(caps)
tb, err := json.Marshal(EnabledTools(caps))
if err != nil {
t.Fatal(err)
}
total := len(sp) + len(tb)
t.Logf("prompt %d car (~%d tok) + outils %d car (~%d tok) = ~%d tok",
len(sp), len(sp)/4, len(tb), len(tb)/4, total/4)
if total > promptCharBudget {
t.Fatalf("préambule à %d car (~%d tok), budget %d car (~%d tok).\n"+
"Avant de relever le budget : les schémas d'outils pèsent le double du prompt,\n"+
"et une consigne écrite dans un schéma n'a pas à être répétée dans le prompt.",
total, total/4, promptCharBudget, promptCharBudget/4)
}
}
// Le prompt ne doit pas redevenir un catalogue d'outils : leurs schémas partent
// dans la MÊME requête et les décrivent déjà un par un.
func TestSystemPromptDoesNotRelistTools(t *testing.T) {
sp := baseSystemPrompt(Caps{Agent: true, Internet: true, Mem: MemAlways})
for _, line := range strings.Split(sp, "\n") {
l := strings.TrimSpace(line)
if !strings.HasPrefix(l, "- ") {
continue
}
// Une puce qui commence par un nom d'outil = un catalogue qui revient.
for _, tool := range []string{"bash ", "write ", "edit ", "mem_", "web_"} {
if strings.HasPrefix(l[2:], tool) {
t.Fatalf("le prompt réénumère un outil : %q", l)
}
}
}
}
// Sans agent ni mémoire, aucun préambule : un modèle de chat simple à qui on
// ordonne d'appeler des outils invente des appels textuels qui fuient dans la
// réponse.
func TestSystemPromptEmptyWithoutTools(t *testing.T) {
if sp := baseSystemPrompt(Caps{Mem: MemOff}); sp != "" {
t.Fatalf("préambule non vide sans outils : %q", sp)
}
}