package toolcall import ( "strings" ) type ParsedToolCall struct { Name string `json:"name"` Input map[string]any `json:"input"` } type ToolCallParseResult struct { Calls []ParsedToolCall SawToolCallSyntax bool RejectedByPolicy bool RejectedToolNames []string } func ParseToolCalls(text string, availableToolNames []string) []ParsedToolCall { return ParseToolCallsDetailed(text, availableToolNames).Calls } func ParseToolCallsDetailed(text string, availableToolNames []string) ToolCallParseResult { return parseToolCallsDetailedXMLOnly(text) } func ParseStandaloneToolCalls(text string, availableToolNames []string) []ParsedToolCall { return ParseStandaloneToolCallsDetailed(text, availableToolNames).Calls } func ParseStandaloneToolCallsDetailed(text string, availableToolNames []string) ToolCallParseResult { return parseToolCallsDetailedXMLOnly(text) } func ParseAssistantToolCallsDetailed(text, thinking string, availableToolNames []string) ToolCallParseResult { textParsed := ParseStandaloneToolCallsDetailed(text, availableToolNames) if len(textParsed.Calls) > 0 { return textParsed } if strings.TrimSpace(text) != "" { return textParsed } thinkingParsed := ParseStandaloneToolCallsDetailed(thinking, availableToolNames) if len(thinkingParsed.Calls) > 0 { return thinkingParsed } return textParsed } func parseToolCallsDetailedXMLOnly(text string) ToolCallParseResult { result := ToolCallParseResult{} trimmed := strings.TrimSpace(text) if trimmed == "" { return result } result.SawToolCallSyntax = looksLikeToolCallSyntax(trimmed) trimmed = stripFencedCodeBlocks(trimmed) trimmed = strings.TrimSpace(trimmed) if trimmed == "" { return result } normalized, ok := normalizeDSMLToolCallMarkup(trimmed) if !ok { return result } parsed := parseXMLToolCalls(normalized) if len(parsed) == 0 && strings.Contains(strings.ToLower(normalized), " 0 && len(calls) == 0 return result } func filterToolCallsDetailed(parsed []ParsedToolCall) ([]ParsedToolCall, []string) { out := make([]ParsedToolCall, 0, len(parsed)) for _, tc := range parsed { if tc.Name == "" { continue } if tc.Input == nil { tc.Input = map[string]any{} } if len(tc.Input) > 0 && !toolCallInputHasMeaningfulValue(tc.Input) { continue } out = append(out, tc) } return out, nil } func toolCallInputHasMeaningfulValue(v any) bool { switch x := v.(type) { case nil: return false case string: return strings.TrimSpace(x) != "" case map[string]any: if len(x) == 0 { return false } for _, child := range x { if toolCallInputHasMeaningfulValue(child) { return true } } return false case []any: if len(x) == 0 { return false } for _, child := range x { if toolCallInputHasMeaningfulValue(child) { return true } } return false default: return true } } func looksLikeToolCallSyntax(text string) bool { hasDSML, hasCanonical := ContainsToolCallWrapperSyntaxOutsideIgnored(text) return hasDSML || hasCanonical } func stripFencedCodeBlocks(text string) string { if text == "" { return "" } var b strings.Builder b.Grow(len(text)) lines := strings.SplitAfter(text, "\n") inFence := false fenceMarker := "" inCDATA := false cdataFenceMarker := "" // Track builder length when a fence opens so we can preserve content // collected before the unclosed fence. beforeFenceLen := 0 for _, line := range lines { if inCDATA || cdataStartsBeforeFence(line) { b.WriteString(line) inCDATA, cdataFenceMarker = updateCDATAStateForStrip(inCDATA, cdataFenceMarker, line) continue } trimmed := strings.TrimLeft(line, " \t") if !inFence { if marker, ok := parseFenceOpen(trimmed); ok { inFence = true fenceMarker = marker beforeFenceLen = b.Len() continue } b.WriteString(line) continue } if isFenceClose(trimmed, fenceMarker) { inFence = false fenceMarker = "" } } if inFence { // Unclosed fence: preserve content that was collected before the // fence started rather than dropping everything. result := b.String() if beforeFenceLen > 0 && beforeFenceLen <= len(result) { return result[:beforeFenceLen] } return "" } return b.String() } func cdataStartsBeforeFence(line string) bool { cdataIdx := strings.Index(strings.ToLower(line), "") if end < 0 { return true, fenceMarker } endPos := pos + end pos = endPos + len("]]>") if fenceMarker != "" { continue } if cdataEndLooksStructural(lower, pos) || strings.TrimSpace(lower[pos:]) == "" { state = false for pos < len(lower) { start := strings.Index(lower[pos:], "