feat: filter and drop empty assistant messages in Kimi executor
- Added `filterKimiEmptyAssistantMessages` to identify and remove empty assistant messages with no content, tool links, or reasoning. - Integrated logging to track the number of dropped messages. - Updated tests to validate the filtering logic for both empty and valid assistant messages. Fixed: #1730
This commit is contained in:
@@ -322,7 +322,17 @@ func normalizeKimiToolMessageLinks(body []byte) ([]byte, error) {
|
|||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
out := body
|
msgs := messages.Array()
|
||||||
|
out, dropped, err := filterKimiEmptyAssistantMessages(body, msgs)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
if dropped > 0 {
|
||||||
|
log.WithField("dropped_assistant_messages", dropped).Debug("kimi executor: dropped empty assistant messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = gjson.GetBytes(out, "messages")
|
||||||
|
msgs = messages.Array()
|
||||||
pending := make([]string, 0)
|
pending := make([]string, 0)
|
||||||
patched := 0
|
patched := 0
|
||||||
patchedReasoning := 0
|
patchedReasoning := 0
|
||||||
@@ -340,7 +350,6 @@ func normalizeKimiToolMessageLinks(body []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msgs := messages.Array()
|
|
||||||
for msgIdx := range msgs {
|
for msgIdx := range msgs {
|
||||||
msg := msgs[msgIdx]
|
msg := msgs[msgIdx]
|
||||||
role := strings.TrimSpace(msg.Get("role").String())
|
role := strings.TrimSpace(msg.Get("role").String())
|
||||||
@@ -428,6 +437,96 @@ func normalizeKimiToolMessageLinks(body []byte) ([]byte, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterKimiEmptyAssistantMessages(body []byte, msgs []gjson.Result) ([]byte, int, error) {
|
||||||
|
kept := make([]string, 0, len(msgs))
|
||||||
|
dropped := 0
|
||||||
|
for _, msg := range msgs {
|
||||||
|
if shouldDropKimiAssistantMessage(msg) {
|
||||||
|
dropped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, msg.Raw)
|
||||||
|
}
|
||||||
|
if dropped == 0 {
|
||||||
|
return body, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMessages := []byte("[" + strings.Join(kept, ",") + "]")
|
||||||
|
out, err := sjson.SetRawBytes(body, "messages", rawMessages)
|
||||||
|
if err != nil {
|
||||||
|
return body, 0, fmt.Errorf("kimi executor: failed to drop empty assistant messages: %w", err)
|
||||||
|
}
|
||||||
|
return out, dropped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldDropKimiAssistantMessage(msg gjson.Result) bool {
|
||||||
|
if strings.TrimSpace(msg.Get("role").String()) != "assistant" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if hasKimiToolCalls(msg) || hasKimiLegacyFunctionCall(msg) || hasKimiAssistantReasoning(msg) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isKimiAssistantContentEmpty(msg.Get("content"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasKimiToolCalls(msg gjson.Result) bool {
|
||||||
|
toolCalls := msg.Get("tool_calls")
|
||||||
|
return toolCalls.Exists() && toolCalls.IsArray() && len(toolCalls.Array()) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasKimiLegacyFunctionCall(msg gjson.Result) bool {
|
||||||
|
functionCall := msg.Get("function_call")
|
||||||
|
if !functionCall.Exists() || functionCall.Type == gjson.Null {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if functionCall.IsObject() && strings.TrimSpace(functionCall.Raw) == "{}" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(functionCall.Raw) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasKimiAssistantReasoning(msg gjson.Result) bool {
|
||||||
|
reasoning := msg.Get("reasoning_content")
|
||||||
|
return reasoning.Exists() && strings.TrimSpace(reasoning.String()) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKimiAssistantContentEmpty(content gjson.Result) bool {
|
||||||
|
if !content.Exists() || content.Type == gjson.Null {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if content.Type == gjson.String {
|
||||||
|
return strings.TrimSpace(content.String()) == ""
|
||||||
|
}
|
||||||
|
if !content.IsArray() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, part := range content.Array() {
|
||||||
|
if !isKimiAssistantContentPartEmpty(part) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKimiAssistantContentPartEmpty(part gjson.Result) bool {
|
||||||
|
if !part.Exists() || part.Type == gjson.Null {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if part.Type == gjson.String {
|
||||||
|
return strings.TrimSpace(part.String()) == ""
|
||||||
|
}
|
||||||
|
if !part.IsObject() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if text := part.Get("text"); text.Exists() {
|
||||||
|
return strings.TrimSpace(text.String()) == ""
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(part.Get("type").String()) == "text" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(part.Raw) == "{}"
|
||||||
|
}
|
||||||
|
|
||||||
func fallbackAssistantReasoning(msg gjson.Result, hasLatest bool, latest string) string {
|
func fallbackAssistantReasoning(msg gjson.Result, hasLatest bool, latest string) string {
|
||||||
if hasLatest && strings.TrimSpace(latest) != "" {
|
if hasLatest && strings.TrimSpace(latest) != "" {
|
||||||
return latest
|
return latest
|
||||||
|
|||||||
@@ -203,3 +203,70 @@ func TestNormalizeKimiToolMessageLinks_RepairsIDsAndReasoningTogether(t *testing
|
|||||||
t.Fatalf("messages.2.reasoning_content = %q, want %q", got, "r1")
|
t.Fatalf("messages.2.reasoning_content = %q, want %q", got, "r1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeKimiToolMessageLinks_DropsEmptyAssistantWithoutToolLink(t *testing.T) {
|
||||||
|
body := []byte(`{
|
||||||
|
"messages":[
|
||||||
|
{"role":"user","content":"start"},
|
||||||
|
{"role":"assistant","content":""},
|
||||||
|
{"role":"assistant","content":" "},
|
||||||
|
{"role":"assistant","content":"","tool_calls":null},
|
||||||
|
{"role":"assistant","content":[{"type":"text","text":" "}]},
|
||||||
|
{"role":"assistant"},
|
||||||
|
{"role":"assistant","content":"keep"},
|
||||||
|
{"role":"user","content":"next"}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
out, err := normalizeKimiToolMessageLinks(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalizeKimiToolMessageLinks() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := gjson.GetBytes(out, "messages").Array()
|
||||||
|
if len(messages) != 3 {
|
||||||
|
t.Fatalf("messages length = %d, want 3, raw = %s", len(messages), gjson.GetBytes(out, "messages").Raw)
|
||||||
|
}
|
||||||
|
if got := messages[0].Get("content").String(); got != "start" {
|
||||||
|
t.Fatalf("messages.0.content = %q, want %q", got, "start")
|
||||||
|
}
|
||||||
|
if got := messages[1].Get("content").String(); got != "keep" {
|
||||||
|
t.Fatalf("messages.1.content = %q, want %q", got, "keep")
|
||||||
|
}
|
||||||
|
if got := messages[2].Get("content").String(); got != "next" {
|
||||||
|
t.Fatalf("messages.2.content = %q, want %q", got, "next")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeKimiToolMessageLinks_PreservesAssistantWithToolLinkOrReasoning(t *testing.T) {
|
||||||
|
body := []byte(`{
|
||||||
|
"messages":[
|
||||||
|
{"role":"assistant","content":"","tool_calls":[{"id":"call_1","type":"function","function":{"name":"list_directory","arguments":"{}"}}]},
|
||||||
|
{"role":"assistant","content":"","function_call":{"name":"legacy_call","arguments":"{}"}},
|
||||||
|
{"role":"assistant","content":"","reasoning_content":"thought"},
|
||||||
|
{"role":"assistant","content":[{"type":"text","text":" visible "}]}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
out, err := normalizeKimiToolMessageLinks(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalizeKimiToolMessageLinks() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := gjson.GetBytes(out, "messages").Array()
|
||||||
|
if len(messages) != 4 {
|
||||||
|
t.Fatalf("messages length = %d, want 4, raw = %s", len(messages), gjson.GetBytes(out, "messages").Raw)
|
||||||
|
}
|
||||||
|
if !messages[0].Get("tool_calls").Exists() {
|
||||||
|
t.Fatalf("messages.0.tool_calls should exist")
|
||||||
|
}
|
||||||
|
if !messages[1].Get("function_call").Exists() {
|
||||||
|
t.Fatalf("messages.1.function_call should exist")
|
||||||
|
}
|
||||||
|
if got := messages[2].Get("reasoning_content").String(); got != "thought" {
|
||||||
|
t.Fatalf("messages.2.reasoning_content = %q, want %q", got, "thought")
|
||||||
|
}
|
||||||
|
if got := messages[3].Get("content.0.text").String(); got != " visible " {
|
||||||
|
t.Fatalf("messages.3.content.0.text = %q, want %q", got, " visible ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user