Files
Enterprise-Knowledge-Base/internal/agent/tools/final_answer_test.go
T
wizardchen a6d4266c93 fix(agent): terminate ReAct loop on final_answer regardless of arg parse success
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
2026-04-23 19:36:28 +08:00

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)
})
}