a6d4266c93
The agent loop previously kept running when the LLM emitted final_answer with malformed JSON arguments: observe.analyzeResponse strictly json.Unmarshal'd the raw args and returned isDone=false on failure, while act.runToolCall separately repaired the JSON and executed the tool. The mismatch made final_answer non-terminal, so the LLM saw its own answer in the next round's tool result and re-emitted final_answer, surfacing near-identical answers multiple times to the user (issue #1008). Fix by unifying parsing in a new tools.ParseFinalAnswerArgs helper with three fallbacks (strict -> RepairJSON -> regex extraction) and making final_answer always terminal in analyzeResponse. When no answer can be recovered, emit a user-visible fallback message and a Done=true event so the UI still resolves. Also route final_answer.Execute through the same helper so tool execution and loop termination stay consistent. Adds regression tests in observe_test.go and final_answer_test.go covering valid args, RepairJSON recovery, unrecoverable args, and garbage input. Refs: Tencent/WeKnora#1008
62 lines
1.9 KiB
Go
62 lines
1.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestParseFinalAnswerArgs covers the three-tier recovery path used by both
|
|
// the final_answer tool and the ReAct loop's terminal detection (issue #1008).
|
|
func TestParseFinalAnswerArgs(t *testing.T) {
|
|
t.Run("strict JSON is parsed as-is", func(t *testing.T) {
|
|
got, ok := ParseFinalAnswerArgs(`{"answer": "Hello, world."}`)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "Hello, world.", got)
|
|
})
|
|
|
|
t.Run("trailing comma is recovered via RepairJSON", func(t *testing.T) {
|
|
got, ok := ParseFinalAnswerArgs(`{"answer": "Hi",}`)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "Hi", got)
|
|
})
|
|
|
|
t.Run("missing closing brace is recovered via RepairJSON", func(t *testing.T) {
|
|
got, ok := ParseFinalAnswerArgs(`{"answer": "Hi"`)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "Hi", got)
|
|
})
|
|
|
|
t.Run("invalid backslash escape is recovered via RepairJSON", func(t *testing.T) {
|
|
// LLM forgot to double-escape the regex metachar — RepairJSON should
|
|
// rewrite "\+" to "\\+" so Unmarshal succeeds.
|
|
got, ok := ParseFinalAnswerArgs(`{"answer": "C\+\+"}`)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, `C\+\+`, got)
|
|
})
|
|
|
|
t.Run("unescaped inner quote is recovered via regex fallback", func(t *testing.T) {
|
|
// Neither strict parse nor RepairJSON can recover this one; the regex
|
|
// still captures the well-formed prefix up to the rogue quote.
|
|
raw := `{"answer": "She said "hello" to me"}`
|
|
got, ok := ParseFinalAnswerArgs(raw)
|
|
assert.True(t, ok)
|
|
assert.NotEmpty(t, got)
|
|
})
|
|
|
|
t.Run("missing answer field returns not ok", func(t *testing.T) {
|
|
_, ok := ParseFinalAnswerArgs(`{"other": "value"}`)
|
|
assert.False(t, ok)
|
|
})
|
|
|
|
t.Run("empty answer returns not ok", func(t *testing.T) {
|
|
_, ok := ParseFinalAnswerArgs(`{"answer": ""}`)
|
|
assert.False(t, ok)
|
|
})
|
|
|
|
t.Run("completely garbled input returns not ok", func(t *testing.T) {
|
|
_, ok := ParseFinalAnswerArgs(`not json at all`)
|
|
assert.False(t, ok)
|
|
})
|
|
}
|