fix(antigravity): normalize tool calls to avoid empty function names
This commit is contained in:
parent
d1655d5996
commit
caf3913347
4 changed files with 229 additions and 18 deletions
|
|
@ -605,15 +605,20 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log tool calls
|
normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
|
||||||
toolNames := make([]string, 0, len(response.ToolCalls))
|
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range response.ToolCalls {
|
||||||
|
normalizedToolCalls = append(normalizedToolCalls, normalizeProviderToolCall(tc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log tool calls
|
||||||
|
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||||
|
for _, tc := range normalizedToolCalls {
|
||||||
toolNames = append(toolNames, tc.Name)
|
toolNames = append(toolNames, tc.Name)
|
||||||
}
|
}
|
||||||
logger.InfoCF("agent", "LLM requested tool calls",
|
logger.InfoCF("agent", "LLM requested tool calls",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"tools": toolNames,
|
"tools": toolNames,
|
||||||
"count": len(response.ToolCalls),
|
"count": len(normalizedToolCalls),
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -622,7 +627,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: response.Content,
|
Content: response.Content,
|
||||||
}
|
}
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
thoughtSignature := ""
|
thoughtSignature := ""
|
||||||
if tc.Function != nil {
|
if tc.Function != nil {
|
||||||
|
|
@ -632,6 +637,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
|
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
Type: "function",
|
Type: "function",
|
||||||
|
Name: tc.Name,
|
||||||
|
Arguments: tc.Arguments,
|
||||||
Function: &providers.FunctionCall{
|
Function: &providers.FunctionCall{
|
||||||
Name: tc.Name,
|
Name: tc.Name,
|
||||||
Arguments: string(argumentsJSON),
|
Arguments: string(argumentsJSON),
|
||||||
|
|
@ -645,7 +652,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
al.sessions.AddFullMessage(opts.SessionKey, assistantMsg)
|
al.sessions.AddFullMessage(opts.SessionKey, assistantMsg)
|
||||||
|
|
||||||
// Execute tool calls
|
// Execute tool calls
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
// Log tool call with arguments preview
|
// Log tool call with arguments preview
|
||||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
argsPreview := utils.Truncate(string(argsJSON), 200)
|
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||||
|
|
@ -708,6 +715,45 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
return finalContent, iteration, nil
|
return finalContent, iteration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProviderToolCall(tc providers.ToolCall) providers.ToolCall {
|
||||||
|
normalized := tc
|
||||||
|
|
||||||
|
if normalized.Name == "" && normalized.Function != nil {
|
||||||
|
normalized.Name = normalized.Function.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized.Arguments == nil {
|
||||||
|
normalized.Arguments = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
|
normalized.Arguments = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
argsJSON, _ := json.Marshal(normalized.Arguments)
|
||||||
|
if normalized.Function == nil {
|
||||||
|
normalized.Function = &providers.FunctionCall{
|
||||||
|
Name: normalized.Name,
|
||||||
|
Arguments: string(argsJSON),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if normalized.Function.Name == "" {
|
||||||
|
normalized.Function.Name = normalized.Name
|
||||||
|
}
|
||||||
|
if normalized.Name == "" {
|
||||||
|
normalized.Name = normalized.Function.Name
|
||||||
|
}
|
||||||
|
if normalized.Function.Arguments == "" {
|
||||||
|
normalized.Function.Arguments = string(argsJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
// updateToolContexts updates the context for tools that need channel/chatID info.
|
// updateToolContexts updates the context for tools that need channel/chatID info.
|
||||||
func (al *AgentLoop) updateToolContexts(channel, chatID string) {
|
func (al *AgentLoop) updateToolContexts(channel, chatID string) {
|
||||||
// Use ContextualTool interface instead of type assertions
|
// Use ContextualTool interface instead of type assertions
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ type antigravityGenConfig struct {
|
||||||
|
|
||||||
func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest {
|
func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest {
|
||||||
req := antigravityRequest{}
|
req := antigravityRequest{}
|
||||||
|
toolCallNames := make(map[string]string)
|
||||||
|
|
||||||
// Build contents from messages
|
// Build contents from messages
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
|
|
@ -205,12 +206,13 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
}
|
}
|
||||||
case "user":
|
case "user":
|
||||||
if msg.ToolCallID != "" {
|
if msg.ToolCallID != "" {
|
||||||
|
toolName := resolveToolResponseName(msg.ToolCallID, toolCallNames)
|
||||||
// Tool result
|
// Tool result
|
||||||
req.Contents = append(req.Contents, antigravityContent{
|
req.Contents = append(req.Contents, antigravityContent{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Parts: []antigravityPart{{
|
Parts: []antigravityPart{{
|
||||||
FunctionResponse: &antigravityFunctionResponse{
|
FunctionResponse: &antigravityFunctionResponse{
|
||||||
Name: msg.ToolCallID,
|
Name: toolName,
|
||||||
Response: map[string]interface{}{
|
Response: map[string]interface{}{
|
||||||
"result": msg.Content,
|
"result": msg.Content,
|
||||||
},
|
},
|
||||||
|
|
@ -231,10 +233,20 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
content.Parts = append(content.Parts, antigravityPart{Text: msg.Content})
|
content.Parts = append(content.Parts, antigravityPart{Text: msg.Content})
|
||||||
}
|
}
|
||||||
for _, tc := range msg.ToolCalls {
|
for _, tc := range msg.ToolCalls {
|
||||||
|
toolName, toolArgs := normalizeStoredToolCall(tc)
|
||||||
|
if toolName == "" {
|
||||||
|
logger.WarnCF("provider.antigravity", "Skipping tool call with empty name in history", map[string]interface{}{
|
||||||
|
"tool_call_id": tc.ID,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tc.ID != "" {
|
||||||
|
toolCallNames[tc.ID] = toolName
|
||||||
|
}
|
||||||
content.Parts = append(content.Parts, antigravityPart{
|
content.Parts = append(content.Parts, antigravityPart{
|
||||||
FunctionCall: &antigravityFunctionCall{
|
FunctionCall: &antigravityFunctionCall{
|
||||||
Name: tc.Name,
|
Name: toolName,
|
||||||
Args: tc.Arguments,
|
Args: toolArgs,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -242,11 +254,12 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
req.Contents = append(req.Contents, content)
|
req.Contents = append(req.Contents, content)
|
||||||
}
|
}
|
||||||
case "tool":
|
case "tool":
|
||||||
|
toolName := resolveToolResponseName(msg.ToolCallID, toolCallNames)
|
||||||
req.Contents = append(req.Contents, antigravityContent{
|
req.Contents = append(req.Contents, antigravityContent{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Parts: []antigravityPart{{
|
Parts: []antigravityPart{{
|
||||||
FunctionResponse: &antigravityFunctionResponse{
|
FunctionResponse: &antigravityFunctionResponse{
|
||||||
Name: msg.ToolCallID,
|
Name: toolName,
|
||||||
Response: map[string]interface{}{
|
Response: map[string]interface{}{
|
||||||
"result": msg.Content,
|
"result": msg.Content,
|
||||||
},
|
},
|
||||||
|
|
@ -294,6 +307,56 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}) {
|
||||||
|
name := tc.Name
|
||||||
|
args := tc.Arguments
|
||||||
|
|
||||||
|
if name == "" && tc.Function != nil {
|
||||||
|
name = tc.Function.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if args == nil {
|
||||||
|
args = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" {
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
|
args = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveToolResponseName(toolCallID string, toolCallNames map[string]string) string {
|
||||||
|
if toolCallID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := toolCallNames[toolCallID]; ok && name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return inferToolNameFromCallID(toolCallID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inferToolNameFromCallID(toolCallID string) string {
|
||||||
|
if !strings.HasPrefix(toolCallID, "call_") {
|
||||||
|
return toolCallID
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := strings.TrimPrefix(toolCallID, "call_")
|
||||||
|
if idx := strings.LastIndex(rest, "_"); idx > 0 {
|
||||||
|
candidate := rest[:idx]
|
||||||
|
if candidate != "" {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolCallID
|
||||||
|
}
|
||||||
|
|
||||||
// --- Response parsing ---
|
// --- Response parsing ---
|
||||||
|
|
||||||
type antigravityJSONResponse struct {
|
type antigravityJSONResponse struct {
|
||||||
|
|
|
||||||
56
pkg/providers/antigravity_provider_test.go
Normal file
56
pkg/providers/antigravity_provider_test.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBuildRequestUsesFunctionFieldsWhenToolCallNameMissing(t *testing.T) {
|
||||||
|
p := &AntigravityProvider{}
|
||||||
|
|
||||||
|
messages := []Message{
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []ToolCall{{
|
||||||
|
ID: "call_read_file_123",
|
||||||
|
Function: &FunctionCall{
|
||||||
|
Name: "read_file",
|
||||||
|
Arguments: `{"path":"README.md"}`,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "tool",
|
||||||
|
ToolCallID: "call_read_file_123",
|
||||||
|
Content: "ok",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := p.buildRequest(messages, nil, "", nil)
|
||||||
|
if len(req.Contents) != 2 {
|
||||||
|
t.Fatalf("expected 2 contents, got %d", len(req.Contents))
|
||||||
|
}
|
||||||
|
|
||||||
|
modelPart := req.Contents[0].Parts[0]
|
||||||
|
if modelPart.FunctionCall == nil {
|
||||||
|
t.Fatal("expected functionCall in assistant message")
|
||||||
|
}
|
||||||
|
if modelPart.FunctionCall.Name != "read_file" {
|
||||||
|
t.Fatalf("expected functionCall name read_file, got %q", modelPart.FunctionCall.Name)
|
||||||
|
}
|
||||||
|
if got := modelPart.FunctionCall.Args["path"]; got != "README.md" {
|
||||||
|
t.Fatalf("expected functionCall args[path] to be README.md, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolPart := req.Contents[1].Parts[0]
|
||||||
|
if toolPart.FunctionResponse == nil {
|
||||||
|
t.Fatal("expected functionResponse in tool message")
|
||||||
|
}
|
||||||
|
if toolPart.FunctionResponse.Name != "read_file" {
|
||||||
|
t.Fatalf("expected functionResponse name read_file, got %q", toolPart.FunctionResponse.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveToolResponseNameInfersNameFromGeneratedCallID(t *testing.T) {
|
||||||
|
got := resolveToolResponseName("call_search_docs_999", map[string]string{})
|
||||||
|
if got != "search_docs" {
|
||||||
|
t.Fatalf("expected inferred tool name search_docs, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,15 +83,20 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Log tool calls
|
normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
|
||||||
toolNames := make([]string, 0, len(response.ToolCalls))
|
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range response.ToolCalls {
|
||||||
|
normalizedToolCalls = append(normalizedToolCalls, normalizeProviderToolCall(tc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Log tool calls
|
||||||
|
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||||
|
for _, tc := range normalizedToolCalls {
|
||||||
toolNames = append(toolNames, tc.Name)
|
toolNames = append(toolNames, tc.Name)
|
||||||
}
|
}
|
||||||
logger.InfoCF("toolloop", "LLM requested tool calls",
|
logger.InfoCF("toolloop", "LLM requested tool calls",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"tools": toolNames,
|
"tools": toolNames,
|
||||||
"count": len(response.ToolCalls),
|
"count": len(normalizedToolCalls),
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -100,11 +105,13 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: response.Content,
|
Content: response.Content,
|
||||||
}
|
}
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
|
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
Type: "function",
|
Type: "function",
|
||||||
|
Name: tc.Name,
|
||||||
|
Arguments: tc.Arguments,
|
||||||
Function: &providers.FunctionCall{
|
Function: &providers.FunctionCall{
|
||||||
Name: tc.Name,
|
Name: tc.Name,
|
||||||
Arguments: string(argumentsJSON),
|
Arguments: string(argumentsJSON),
|
||||||
|
|
@ -114,7 +121,7 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
|
||||||
messages = append(messages, assistantMsg)
|
messages = append(messages, assistantMsg)
|
||||||
|
|
||||||
// 7. Execute tool calls
|
// 7. Execute tool calls
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
argsPreview := utils.Truncate(string(argsJSON), 200)
|
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||||
logger.InfoCF("toolloop", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
|
logger.InfoCF("toolloop", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
|
||||||
|
|
@ -152,3 +159,42 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
|
||||||
Iterations: iteration,
|
Iterations: iteration,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProviderToolCall(tc providers.ToolCall) providers.ToolCall {
|
||||||
|
normalized := tc
|
||||||
|
|
||||||
|
if normalized.Name == "" && normalized.Function != nil {
|
||||||
|
normalized.Name = normalized.Function.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized.Arguments == nil {
|
||||||
|
normalized.Arguments = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
|
normalized.Arguments = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
argsJSON, _ := json.Marshal(normalized.Arguments)
|
||||||
|
if normalized.Function == nil {
|
||||||
|
normalized.Function = &providers.FunctionCall{
|
||||||
|
Name: normalized.Name,
|
||||||
|
Arguments: string(argsJSON),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if normalized.Function.Name == "" {
|
||||||
|
normalized.Function.Name = normalized.Name
|
||||||
|
}
|
||||||
|
if normalized.Name == "" {
|
||||||
|
normalized.Name = normalized.Function.Name
|
||||||
|
}
|
||||||
|
if normalized.Function.Arguments == "" {
|
||||||
|
normalized.Function.Arguments = string(argsJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue