mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Repris d'OpenFox (stream-pure, agent-loop, 2.0.15x). - Repéré PENDANT le flux (hors réflexion en ligne) : la génération est coupée tout de suite au lieu de laisser le modèle dérouler un faux appel — parfois un fichier entier — qui ne serait jamais exécuté. - La consigne corrective cite l'extrait fautif : le modèle voit ce qu'il a mal écrit. - Jusqu'à 3 relances consécutives au lieu d'une par tour ; le compteur repart à zéro après chaque appel émis par le protocole. Un long tour d'agent pouvait rater la syntaxe deux fois et finir sur un pseudo-appel. - Nouveaux motifs : le format XML de Qwen3-Coder (<function=…>, <parameter=…>) quand le gabarit du moteur ne l'a pas converti. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package loki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Les appels d'outils écrits en texte doivent être détectés…
|
|
func TestAppelOutilTextuelDetecte(t *testing.T) {
|
|
for _, s := range []string{
|
|
"<tool_call>{\"name\":\"bash\"}</tool_call>",
|
|
"Je vais lancer default_api:bash pour vérifier.",
|
|
"default_api.run_shell(command='ls')",
|
|
"functions.bash({\"command\": \"ls\"})",
|
|
"```tool_code\nprint('x')\n```",
|
|
"{\"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}",
|
|
} {
|
|
if !textualToolCall(s) {
|
|
t.Errorf("non détecté : %q", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// … sans jamais relancer un tour normal (code cité, JSON quelconque).
|
|
func TestReponseNormaleNonRelancee(t *testing.T) {
|
|
for _, s := range []string{
|
|
"Voici la réponse : le fichier contient trois fonctions.",
|
|
"```go\nfunc main() {}\n```",
|
|
"Le JSON de config : {\"name\": \"loki\", \"port\": 8090}",
|
|
"La fonction bash() de ce script accepte un paramètre.",
|
|
"",
|
|
} {
|
|
if textualToolCall(s) {
|
|
t.Errorf("relance à tort : %q", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format XML de Qwen3-Coder resté en texte : détecté, et l'extrait est cité.
|
|
func TestAppelOutilQwenXMLDetecte(t *testing.T) {
|
|
s := "Je regarde.\n<function=bash>\n<parameter=command>ls -la</parameter>\n</function>"
|
|
snip := textualToolCallSnippet(s)
|
|
if snip == "" || !strings.HasPrefix(snip, "<function=bash>") {
|
|
t.Fatalf("extrait = %q", snip)
|
|
}
|
|
if textualToolCall("Un <div class=\"x\"> et un <param name=\"a\"> en HTML.") {
|
|
t.Fatal("HTML ordinaire pris pour un appel d'outil")
|
|
}
|
|
}
|
|
|
|
// Le faux appel est repéré PENDANT le flux : la génération est coupée (la suite
|
|
// n'arrive jamais à l'écran) et la relance cite l'extrait fautif.
|
|
func TestAppelTextuelCoupeLeFlux(t *testing.T) {
|
|
testHome(t)
|
|
var n int32
|
|
var second string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.WriteHeader(200)
|
|
if atomic.AddInt32(&n, 1) == 1 {
|
|
_, _ = w.Write([]byte(sseChunk("Je lance <tool_call>")))
|
|
w.(http.Flusher).Flush()
|
|
time.Sleep(300 * time.Millisecond)
|
|
_, _ = w.Write([]byte(sseChunk("FIN-INUTILE")))
|
|
_, _ = w.Write([]byte(`data: {"choices":[{"delta":{},"finish_reason":"stop"}]}` + "\n\n"))
|
|
return
|
|
}
|
|
var body struct {
|
|
Messages []Message `json:"messages"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
second = msgText(body.Messages[len(body.Messages)-1])
|
|
_, _ = w.Write([]byte(sseChunk("ok")))
|
|
_, _ = w.Write([]byte(`data: {"choices":[{"delta":{},"finish_reason":"stop"}]}` + "\n\ndata: [DONE]\n\n"))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
u, _ := url.Parse(srv.URL)
|
|
if err := SetConfigKey("PORT", u.Port()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var content strings.Builder
|
|
if _, err := runChat(context.Background(), []Message{{Role: "user", Content: "liste"}}, 0.7, Caps{Agent: true}, func(ev StreamEvent) bool {
|
|
content.WriteString(ev.Content)
|
|
return true
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(content.String(), "FIN-INUTILE") {
|
|
t.Fatal("le flux n'a pas été coupé sur l'appel écrit en texte")
|
|
}
|
|
if !strings.Contains(second, "<tool_call>") || !strings.Contains(second, "WRITTEN AS TEXT") {
|
|
t.Fatalf("relance sans l'extrait fautif : %q", second)
|
|
}
|
|
}
|