diff --git a/internal/loki/llm_client.go b/internal/loki/llm_client.go index 66ee03f..f38a7d4 100644 --- a/internal/loki/llm_client.go +++ b/internal/loki/llm_client.go @@ -241,6 +241,26 @@ func InjectSkills(msgs []Message, caps Caps) []Message { return append([]Message{{Role: "system", Content: prefix}}, msgs...) } +// steerSystem ajoute une consigne système SANS jamais créer un second message +// système ailleurs qu'en tête : elle est fusionnée dans celui d'ouverture, ou +// posée en première position s'il n'y en a pas. +// +// Un message système ajouté À LA FIN faisait échouer le tour sur les modèles +// dont le gabarit l'interdit : gpt-oss (rôle « developer ») lève « System +// message must be at the beginning », et ce 500 du gabarit remplaçait l'erreur +// d'origine que la nouvelle tentative cherchait justement à contourner — +// l'utilisateur voyait une exception Jinja au lieu du vrai problème. +func steerSystem(msgs []Message, hint string) []Message { + if len(msgs) > 0 && msgs[0].Role == "system" { + if existing, ok := msgs[0].Content.(string); ok { + out := append([]Message(nil), msgs...) + out[0] = Message{Role: "system", Content: existing + "\n\n" + hint} + return out + } + } + return append([]Message{{Role: "system", Content: hint}}, msgs...) +} + // EnabledTools returns the tools to advertise on the next inference call. func EnabledTools(caps Caps) []Tool { tools := []Tool{} @@ -651,7 +671,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps // Nudge the model to answer in plain text from what it already // gathered, so it doesn't immediately re-emit a tool call that // llama.cpp would again fail to parse. - messages = append(messages, Message{Role: "system", Content: "N'appelle plus d'outil. Réponds maintenant directement en français à partir des informations déjà obtenues."}) + messages = steerSystem(messages, "N'appelle plus d'outil. Réponds maintenant directement en français à partir des informations déjà obtenues.") continue } err := fmt.Errorf("llama-server a renvoyé %d : %s", resp.StatusCode, msg) diff --git a/internal/loki/llm_steer_test.go b/internal/loki/llm_steer_test.go new file mode 100644 index 0000000..4563c4b --- /dev/null +++ b/internal/loki/llm_steer_test.go @@ -0,0 +1,51 @@ +package loki + +import "testing" + +// Plusieurs gabarits de conversation exigent que le message système soit le +// PREMIER — gpt-oss lève « System message must be at the beginning ». La +// consigne posée en cours de tour (repli après un 500) ne doit donc jamais +// arriver en fin de liste, sinon la tentative de récupération échoue à son tour +// et masque l'erreur d'origine. +func TestSteerSystemResteEnTete(t *testing.T) { + hint := "N'appelle plus d'outil." + + t.Run("fusionne dans le système existant", func(t *testing.T) { + msgs := []Message{ + {Role: "system", Content: "Tu es Loki."}, + {Role: "user", Content: "bonjour"}, + {Role: "assistant", Content: "salut"}, + } + out := steerSystem(msgs, hint) + if len(out) != len(msgs) { + t.Fatalf("%d messages, attendu %d : aucun message ne doit être ajouté", len(out), len(msgs)) + } + if got := out[0].Content.(string); got != "Tu es Loki.\n\n"+hint { + t.Fatalf("consigne non fusionnée en tête : %q", got) + } + for i, m := range out[1:] { + if m.Role == "system" { + t.Fatalf("message système en position %d — il doit rester unique et en tête", i+1) + } + } + }) + + t.Run("sans système existant, il est posé en tête", func(t *testing.T) { + msgs := []Message{{Role: "user", Content: "bonjour"}} + out := steerSystem(msgs, hint) + if out[0].Role != "system" || out[0].Content.(string) != hint { + t.Fatalf("premier message = %+v, attendu le système", out[0]) + } + if out[1].Role != "user" { + t.Fatalf("la conversation d'origine doit suivre, obtenu %+v", out[1]) + } + }) + + t.Run("ne modifie pas la liste d'origine", func(t *testing.T) { + msgs := []Message{{Role: "system", Content: "Tu es Loki."}, {Role: "user", Content: "bonjour"}} + _ = steerSystem(msgs, hint) + if got := msgs[0].Content.(string); got != "Tu es Loki." { + t.Fatalf("liste d'appelant modifiée : %q", got) + } + }) +}