mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Agent : read_file, str_replace, old_string… traduits vers les vrais outils
Généralisation du transformSubAgentAliases d'OpenFox. Les petits modèles
ont appris d'autres agents : read_file, str_replace, run_command, ou path /
old_string au lieu de file / old. L'appel tombait sur « outil inconnu »
ou sur un argument manquant, et le tour se perdait en allers-retours.
Le nom et les arguments sont traduits vers l'outil réel quand la cible est
disponible dans ce tour, avant que l'appel soit rangé dans l'historique
(le modèle y relit l'appel tel qu'exécuté). Un sous-agent appelé comme un
outil (« explorer », « code_reviewer ») devient subagent{role, task}. Un
appel déjà correct, ou dont la cible n'est pas offerte, reste intact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad87f2fc9f
commit
a745d2399e
3 files changed
+163
No files matched your search
@@ -0,0 +1,102 @@
|
||||
package loki
|
||||
|
||||
// code_alias.go — réparation des noms d'outils et d'arguments hallucinés.
|
||||
//
|
||||
// Les petits modèles ont appris d'autres agents : ils appellent read_file,
|
||||
// str_replace, run_command, ou passent path / old_string au lieu de file /
|
||||
// old. L'appel tombait sur « outil inconnu » ou sur un argument manquant, et
|
||||
// le tour se perdait en allers-retours. On traduit vers l'outil RÉEL quand la
|
||||
// cible existe dans ce tour (généralisation du transformSubAgentAliases
|
||||
// d'OpenFox : un outil nommé d'après un sous-agent devient subagent{role}).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// toolNameAliases : nom halluciné → outil de Loki.
|
||||
var toolNameAliases = map[string]string{
|
||||
"read_file": "read", "view": "read", "cat": "read", "open_file": "read",
|
||||
"write_file": "write", "create_file": "write",
|
||||
"edit_file": "edit", "str_replace": "edit", "replace_in_file": "edit", "str_replace_editor": "edit",
|
||||
"run_command": "bash", "shell": "bash", "run_shell": "bash", "execute_command": "bash", "terminal": "bash",
|
||||
"search": "grep", "grep_search": "grep", "search_files": "grep",
|
||||
"find_files": "glob", "list_files": "glob", "file_search": "glob",
|
||||
}
|
||||
|
||||
// toolArgAliases : par outil, argument halluciné → argument attendu.
|
||||
var toolArgAliases = map[string]map[string]string{
|
||||
"read": {"path": "file", "file_path": "file", "filename": "file", "filepath": "file"},
|
||||
"write": {"path": "file", "file_path": "file", "filename": "file", "filepath": "file", "contents": "content", "text": "content"},
|
||||
"edit": {"path": "file", "file_path": "file", "filename": "file", "filepath": "file",
|
||||
"old_string": "old", "old_str": "old", "old_text": "old", "search": "old",
|
||||
"new_string": "new", "new_str": "new", "new_text": "new", "replace": "new"},
|
||||
"bash": {"cmd": "command"},
|
||||
"bash_bg": {"cmd": "command"},
|
||||
"grep": {"query": "pattern", "regex": "pattern"},
|
||||
"glob": {"query": "pattern"},
|
||||
}
|
||||
|
||||
// repairToolCall corrige en place le nom et les arguments d'un appel quand ils
|
||||
// visent, sous un autre nom, un outil disponible dans ce tour. Renvoie true si
|
||||
// l'appel a été modifié. Les arguments doivent déjà être du JSON valide.
|
||||
func repairToolCall(tc *ToolCall, tools []Tool) bool {
|
||||
have := make(map[string]bool, len(tools))
|
||||
for _, t := range tools {
|
||||
have[t.Function.Name] = true
|
||||
}
|
||||
name := tc.Function.Name
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil || args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
changed := false
|
||||
if !have[name] {
|
||||
lower := strings.ToLower(strings.TrimSpace(name))
|
||||
if t := toolNameAliases[lower]; t != "" && have[t] {
|
||||
name, changed = t, true
|
||||
} else if have["subagent"] {
|
||||
// Un sous-agent appelé comme un outil (« explorer », « code_reviewer »).
|
||||
role := strings.ReplaceAll(lower, "_", "-")
|
||||
for _, r := range subagentRoles {
|
||||
if role == r {
|
||||
if _, ok := args["role"]; !ok {
|
||||
args["role"] = r
|
||||
}
|
||||
if _, ok := args["task"]; !ok {
|
||||
for _, k := range []string{"prompt", "query", "question", "description"} {
|
||||
if v, ok := args[k].(string); ok && v != "" {
|
||||
args["task"] = v
|
||||
delete(args, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
name, changed = "subagent", true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for from, to := range toolArgAliases[name] {
|
||||
v, ok := args[from]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, exists := args[to]; !exists {
|
||||
args[to] = v
|
||||
changed = true
|
||||
}
|
||||
delete(args, from)
|
||||
}
|
||||
if !changed {
|
||||
return false
|
||||
}
|
||||
b, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
tc.Function.Name = name
|
||||
tc.Function.Arguments = string(b)
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func outils(noms ...string) []Tool {
|
||||
var ts []Tool
|
||||
for _, n := range noms {
|
||||
ts = append(ts, Tool{Type: "function", Function: ToolFunction{Name: n}})
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
// Noms et arguments appris d'autres agents : traduits vers les outils de Loki.
|
||||
func TestReparationDesAppelsHallucines(t *testing.T) {
|
||||
tools := outils("read", "edit", "bash", "subagent")
|
||||
cas := []struct {
|
||||
name, args, wantName string
|
||||
wantArgs map[string]any
|
||||
}{
|
||||
{"read_file", `{"path":"a.go"}`, "read", map[string]any{"file": "a.go"}},
|
||||
{"str_replace", `{"file_path":"a.go","old_string":"x","new_string":"y"}`, "edit", map[string]any{"file": "a.go", "old": "x", "new": "y"}},
|
||||
{"run_command", `{"cmd":"ls"}`, "bash", map[string]any{"command": "ls"}},
|
||||
{"edit", `{"file":"a.go","old_str":"x","new":"y"}`, "edit", map[string]any{"file": "a.go", "old": "x", "new": "y"}},
|
||||
{"code_reviewer", `{"prompt":"relis"}`, "subagent", map[string]any{"role": "code-reviewer", "task": "relis"}},
|
||||
}
|
||||
for _, c := range cas {
|
||||
tc := ToolCall{Function: ToolCallFunc{Name: c.name, Arguments: c.args}}
|
||||
if !repairToolCall(&tc, tools) {
|
||||
t.Errorf("%s : non réparé", c.name)
|
||||
continue
|
||||
}
|
||||
var got map[string]any
|
||||
_ = json.Unmarshal([]byte(tc.Function.Arguments), &got)
|
||||
if tc.Function.Name != c.wantName || len(got) != len(c.wantArgs) {
|
||||
t.Errorf("%s → %s %v", c.name, tc.Function.Name, got)
|
||||
continue
|
||||
}
|
||||
for k, v := range c.wantArgs {
|
||||
if got[k] != v {
|
||||
t.Errorf("%s : %s=%v, attendu %v", c.name, k, got[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Outil cible absent de ce tour, ou appel déjà correct : intact.
|
||||
for _, tc := range []ToolCall{
|
||||
{Function: ToolCallFunc{Name: "write_file", Arguments: `{"path":"a"}`}},
|
||||
{Function: ToolCallFunc{Name: "read", Arguments: `{"file":"a"}`}},
|
||||
} {
|
||||
if repairToolCall(&tc, tools) {
|
||||
t.Errorf("modifié à tort : %+v", tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1530,6 +1530,11 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
badArgs[tc.ID] = true
|
||||
}
|
||||
tc.Function.Arguments = "{}"
|
||||
} else {
|
||||
// Nom ou argument appris d'un autre agent (read_file, old_string…) :
|
||||
// traduit vers l'outil réel, AVANT de ranger l'appel dans
|
||||
// l'historique — le modèle y relit l'appel tel qu'exécuté.
|
||||
repairToolCall(&tc, tools)
|
||||
}
|
||||
tcs = append(tcs, tc)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user