mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
v0.2.8 : l'IA garde la trace de ses appels d'outils entre les tours
Les tours d'outils (lecture de skill, commandes shell) ne survivaient qu'a l'interieur de la boucle agentique d'un seul tour : seul le texte final etait persiste. Resultat : a chaque nouveau message le modele n'avait aucune trace d'avoir deja lu un skill, donc il le re-appelait en boucle et confabulait des chemins/resultats disparus du contexte. runChat retourne desormais les messages intermediaires (assistant tool_calls + resultats tool). Le CLI les insere dans l'historique avant la reponse finale ; le serveur web les emet en fin de flux SSE pour que le client (stateless) les stocke. Couvre aussi ajean.link via E2E (handleE2EChat -> runChatStream).
This commit is contained in:
1 parent
a453229642
commit
f632b7b42c
7 files changed
+54
-19
No files matched your search
@@ -244,7 +244,14 @@ type streamChunk struct {
|
||||
// and call /v1/chat/completions again — up to 8 iterations as a safety cap.
|
||||
const thinkClose = "</think>"
|
||||
|
||||
func runChat(ctx context.Context, messages []Message, temperature float64, caps Caps, cb ChatCallback) error {
|
||||
// runChat returns `extra`: the tool-turn messages (assistant-with-tool_calls and
|
||||
// their tool results) it appended during the agentic loop. The caller persists
|
||||
// these into the durable history BEFORE the final assistant text, so the model
|
||||
// keeps the trace of what it already read/ran across user turns — otherwise it
|
||||
// re-invokes the same skill/command every turn (it has no memory of having done
|
||||
// it) and can confabulate paths/results it can no longer see.
|
||||
func runChat(ctx context.Context, messages []Message, temperature float64, caps Caps, cb ChatCallback) ([]Message, error) {
|
||||
var extra []Message
|
||||
tools := EnabledTools(caps)
|
||||
// Some backends (vanilla llama.cpp builds) don't populate `reasoning_content`
|
||||
// in streaming mode: the model's <think> block (opened by the chat template)
|
||||
@@ -280,14 +287,14 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
url := fmt.Sprintf("http://localhost:%d/v1/chat/completions", LLMPort())
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
return extra, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
authHeader(req)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
cb(StreamEvent{Err: err})
|
||||
return err
|
||||
return extra, err
|
||||
}
|
||||
// A non-200 here (e.g. context window exceeded after several large tool
|
||||
// outputs) is NOT valid SSE: without this check we'd scan an empty/HTML
|
||||
@@ -313,7 +320,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
}
|
||||
err := fmt.Errorf("llama-server a renvoyé %d : %s", resp.StatusCode, msg)
|
||||
cb(StreamEvent{Err: err})
|
||||
return err
|
||||
return extra, err
|
||||
}
|
||||
toolCalls := map[int]*ToolCall{}
|
||||
assistantContent := strings.Builder{}
|
||||
@@ -445,7 +452,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
}
|
||||
resp.Body.Close()
|
||||
if aborted {
|
||||
return nil
|
||||
return extra, nil
|
||||
}
|
||||
|
||||
// Treat any accumulated tool calls as a tool turn even if the backend set
|
||||
@@ -474,6 +481,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
assistant.Content = s
|
||||
}
|
||||
messages = append(messages, assistant)
|
||||
extra = append(extra, assistant)
|
||||
// Loop guard: same exact call(s) as last turn? Count it; on the 3rd
|
||||
// identical turn, stop so we don't churn the same command forever.
|
||||
sig := strings.Builder{}
|
||||
@@ -487,7 +495,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
repeatSig++
|
||||
if repeatSig >= 2 {
|
||||
cb(StreamEvent{Content: "\n\n[stop: appel d'outil répété en boucle — " + tcs[0].Function.Name + "]"})
|
||||
return nil
|
||||
return extra, nil
|
||||
}
|
||||
} else {
|
||||
lastSig = s
|
||||
@@ -559,7 +567,9 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
shown = string(r[:4000]) + "\n…[tronqué]"
|
||||
}
|
||||
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shown, Done: true}})
|
||||
messages = append(messages, Message{Role: "tool", ToolCallID: tc.ID, Content: result})
|
||||
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
||||
messages = append(messages, toolMsg)
|
||||
extra = append(extra, toolMsg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -569,10 +579,10 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
if strings.TrimSpace(assistantContent.String()) == "" {
|
||||
cb(StreamEvent{Content: "_(le modèle n'a pas produit de réponse — finish: " + finishReason + ")_"})
|
||||
}
|
||||
return nil
|
||||
return extra, nil
|
||||
}
|
||||
cb(StreamEvent{Content: "\n\n[stop: trop d'appels d'outils]"})
|
||||
return nil
|
||||
return extra, nil
|
||||
}
|
||||
|
||||
// healthCheck pings llama.cpp's /health endpoint.
|
||||
|
||||
Reference in new issue
Block a user