Skip to content

Commit efb6338

Browse files
committed
don't append messages with empty content to anthropic thread
should fix edge cases that might occur because of other errors (like network issues, tool call errors, etc). references #91 Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
1 parent b6b6f50 commit efb6338

2 files changed

Lines changed: 132 additions & 5 deletions

File tree

pkg/model/provider/anthropic/client.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ func convertMessages(messages []chat.Message) []anthropic.MessageParam {
186186
msg := &messages[i]
187187
if msg.Role == chat.MessageRoleSystem {
188188
// Convert system message to user message with system prefix
189-
systemContent := "System: " + msg.Content
190-
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(strings.TrimSpace(systemContent))))
189+
if systemContent := strings.TrimSpace("System: " + msg.Content); systemContent != "System:" {
190+
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(systemContent)))
191+
}
191192
continue
192193
}
193194
if msg.Role == chat.MessageRoleUser {
@@ -196,7 +197,9 @@ func convertMessages(messages []chat.Message) []anthropic.MessageParam {
196197
contentBlocks := make([]anthropic.ContentBlockParamUnion, 0, len(msg.MultiContent))
197198
for _, part := range msg.MultiContent {
198199
if part.Type == chat.MessagePartTypeText {
199-
contentBlocks = append(contentBlocks, anthropic.NewTextBlock(strings.TrimSpace(part.Text)))
200+
if txt := strings.TrimSpace(part.Text); txt != "" {
201+
contentBlocks = append(contentBlocks, anthropic.NewTextBlock(txt))
202+
}
200203
} else if part.Type == chat.MessagePartTypeImageURL && part.ImageURL != nil {
201204
// Anthropic expects base64 image data
202205
// Extract base64 data from data URL
@@ -249,7 +252,9 @@ func convertMessages(messages []chat.Message) []anthropic.MessageParam {
249252
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(contentBlocks...))
250253
}
251254
} else {
252-
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock(strings.TrimSpace(msg.Content))))
255+
if txt := strings.TrimSpace(msg.Content); txt != "" {
256+
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock(txt)))
257+
}
253258
}
254259
continue
255260
}
@@ -281,7 +286,9 @@ func convertMessages(messages []chat.Message) []anthropic.MessageParam {
281286
}
282287
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(toolUseBlocks...))
283288
} else {
284-
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(strings.TrimSpace(msg.Content))))
289+
if txt := strings.TrimSpace(msg.Content); txt != "" {
290+
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(txt)))
291+
}
285292
}
286293
continue
287294
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package anthropic
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/docker/cagent/pkg/chat"
8+
"github.com/docker/cagent/pkg/tools"
9+
)
10+
11+
func TestConvertMessages_SkipEmptySystemText(t *testing.T) {
12+
msgs := []chat.Message{{
13+
Role: chat.MessageRoleSystem,
14+
Content: " \n\t ",
15+
}}
16+
17+
out := convertMessages(msgs)
18+
if len(out) != 0 {
19+
t.Fatalf("expected 0 messages, got %d", len(out))
20+
}
21+
}
22+
23+
func TestConvertMessages_SkipEmptyUserText_NoMultiContent(t *testing.T) {
24+
msgs := []chat.Message{{
25+
Role: chat.MessageRoleUser,
26+
Content: " \n\t ",
27+
}}
28+
29+
out := convertMessages(msgs)
30+
if len(out) != 0 {
31+
t.Fatalf("expected 0 messages, got %d", len(out))
32+
}
33+
}
34+
35+
func TestConvertMessages_UserMultiContent_SkipEmptyText_KeepImage(t *testing.T) {
36+
msgs := []chat.Message{{
37+
Role: chat.MessageRoleUser,
38+
MultiContent: []chat.MessagePart{
39+
{Type: chat.MessagePartTypeText, Text: " "},
40+
{Type: chat.MessagePartTypeImageURL, ImageURL: &chat.MessageImageURL{URL: "data:image/png;base64,AAAA"}},
41+
},
42+
}}
43+
44+
out := convertMessages(msgs)
45+
if len(out) != 1 {
46+
t.Fatalf("expected 1 message, got %d", len(out))
47+
}
48+
49+
b, err := json.Marshal(out[0])
50+
if err != nil {
51+
t.Fatalf("marshal error: %v", err)
52+
}
53+
// Basic JSON structure checks
54+
var m map[string]any
55+
if err := json.Unmarshal(b, &m); err != nil {
56+
t.Fatalf("unmarshal error: %v", err)
57+
}
58+
// role should be user
59+
if role, _ := m["role"].(string); role != "user" {
60+
t.Fatalf("expected role 'user', got %v", m["role"])
61+
}
62+
// content should contain exactly one block (the image)
63+
if content, _ := m["content"].([]any); len(content) != 1 {
64+
t.Fatalf("expected 1 content block, got %d", len(content))
65+
}
66+
// and it should be an image block
67+
if content, _ := m["content"].([]any); len(content) == 1 {
68+
cb, _ := content[0].(map[string]any)
69+
if typ, _ := cb["type"].(string); typ != "image" {
70+
t.Fatalf("expected content block type 'image', got %v", typ)
71+
}
72+
}
73+
}
74+
75+
func TestConvertMessages_SkipEmptyAssistantText_NoToolCalls(t *testing.T) {
76+
msgs := []chat.Message{{
77+
Role: chat.MessageRoleAssistant,
78+
Content: " \t\n ",
79+
}}
80+
81+
out := convertMessages(msgs)
82+
if len(out) != 0 {
83+
t.Fatalf("expected 0 messages, got %d", len(out))
84+
}
85+
}
86+
87+
func TestConvertMessages_AssistantToolCalls_NoText_IncludesToolUse(t *testing.T) {
88+
msgs := []chat.Message{{
89+
Role: chat.MessageRoleAssistant,
90+
Content: " ",
91+
ToolCalls: []tools.ToolCall{
92+
{ID: "tool-1", Function: tools.FunctionCall{Name: "do_thing", Arguments: "{\"x\":1}"}},
93+
},
94+
}}
95+
96+
out := convertMessages(msgs)
97+
if len(out) != 1 {
98+
t.Fatalf("expected 1 message, got %d", len(out))
99+
}
100+
101+
b, err := json.Marshal(out[0])
102+
if err != nil {
103+
t.Fatalf("marshal error: %v", err)
104+
}
105+
var m map[string]any
106+
if err := json.Unmarshal(b, &m); err != nil {
107+
t.Fatalf("unmarshal error: %v", err)
108+
}
109+
if role, _ := m["role"].(string); role != "assistant" {
110+
t.Fatalf("expected role 'assistant', got %v", m["role"])
111+
}
112+
content, _ := m["content"].([]any)
113+
if len(content) != 1 {
114+
t.Fatalf("expected 1 content block, got %d", len(content))
115+
}
116+
cb, _ := content[0].(map[string]any)
117+
if typ, _ := cb["type"].(string); typ != "tool_use" {
118+
t.Fatalf("expected content block type 'tool_use', got %v", typ)
119+
}
120+
}

0 commit comments

Comments
 (0)