Download internal/tools.go from bg5/zai2api3: direct link, hf CLI and curl.
- Browser
- Download file 26.5 kB
-
https://huggingface.co/spaces/bg5/zai2api3/resolve/main/internal/tools.go
- Command line
-
hf download hf://spaces/bg5/zai2api3/internal/tools.go
-
curl -L -o tools.go https://huggingface.co/spaces/bg5/zai2api3/resolve/main/internal/tools.go
26.5 kB
| package internal | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "fmt" | |
| "html" | |
| "regexp" | |
| "strings" | |
| "sync/atomic" | |
| "time" | |
| ) | |
| type Tool struct { | |
| Type string `json:"type"` | |
| Function ToolFunction `json:"function,omitempty"` | |
| } | |
| type ToolFunction struct { | |
| Name string `json:"name"` | |
| Description string `json:"description,omitempty"` | |
| Parameters json.RawMessage `json:"parameters,omitempty"` | |
| } | |
| type ToolCall struct { | |
| Index int `json:"index,omitempty"` | |
| ID string `json:"id"` | |
| Type string `json:"type"` | |
| Function ToolCallFunction `json:"function"` | |
| } | |
| type ToolCallFunction struct { | |
| Name string `json:"name"` | |
| Arguments string `json:"arguments"` | |
| } | |
| var ( | |
| toolCallFencePattern = regexp.MustCompile("(?s)```(?:json|xml)?\\s*(.*?)\\s*```") | |
| functionCallPattern = regexp.MustCompile(`(?s)调用函数\s*[::]\s*([\w\-\.]+)\s*(?:参数|arguments)[::]\s*(\{.*?\})`) | |
| functionInvokePattern = regexp.MustCompile(`(?s)\b([\w\-\.]+)\s*\(\s*(\{.*?\})\s*\)`) | |
| xmlToolCallBlockPattern = regexp.MustCompile(`(?is)<tool_calls>\s*(.*?)\s*</tool_calls>`) | |
| xmlToolCallItemPattern = regexp.MustCompile(`(?is)<tool_call(?:\s+id="([^"]+)")?>(.*?)</tool_call>`) | |
| xmlToolNamePattern = regexp.MustCompile(`(?is)<name>\s*([^<]+?)\s*</name>`) | |
| xmlToolArgumentsPattern = regexp.MustCompile(`(?is)<arguments>\s*(.*?)\s*</arguments>`) | |
| xmlToolArgumentsCDataStart = "<![CDATA[" | |
| xmlToolArgumentsCDataEnd = "]]>" | |
| callIDCounter int64 | |
| ) | |
| func GenerateToolPrompt(tools []Tool, toolChoice interface{}) string { | |
| if len(tools) == 0 { | |
| return "" | |
| } | |
| var toolDefs []string | |
| var toolNames []string | |
| for _, tool := range tools { | |
| if tool.Type != "function" { | |
| continue | |
| } | |
| fn := tool.Function | |
| toolNames = append(toolNames, fn.Name) | |
| toolInfo := fmt.Sprintf("<tool>\n<name>%s</name>", html.EscapeString(fn.Name)) | |
| if fn.Description != "" { | |
| toolInfo += fmt.Sprintf("\n<description>%s</description>", html.EscapeString(fn.Description)) | |
| } | |
| if len(fn.Parameters) > 0 { | |
| var prettyParams bytes.Buffer | |
| if err := json.Indent(&prettyParams, fn.Parameters, "", " "); err == nil { | |
| toolInfo += fmt.Sprintf("\n<parameters><![CDATA[%s]]></parameters>", prettyParams.String()) | |
| } else { | |
| toolInfo += fmt.Sprintf("\n<parameters><![CDATA[%s]]></parameters>", string(fn.Parameters)) | |
| } | |
| } | |
| toolInfo += "\n</tool>" | |
| toolDefs = append(toolDefs, toolInfo) | |
| } | |
| if len(toolDefs) == 0 { | |
| return "" | |
| } | |
| instructions := getToolChoiceInstructions(toolChoice, toolNames) | |
| examples := buildToolExamples(toolNames) | |
| return "<tool_injection>\n<available_tools>\n" + strings.Join(toolDefs, "\n") + "\n</available_tools>\n" + instructions + examples + "\n</tool_injection>" | |
| } | |
| // buildToolExamples 用真实工具名生成强示例。 | |
| // 灵感来自 CJackHwang/ds2api:用真实工具名 + 多个正负示例显著提升 GLM 类模型的工具调用率。 | |
| func buildToolExamples(toolNames []string) string { | |
| if len(toolNames) == 0 { | |
| return "" | |
| } | |
| primary := toolNames[0] | |
| // 尝试列出第一个工具的"single call"示例 | |
| example := fmt.Sprintf(` | |
| <correct_examples> | |
| <!-- 示例 1:调用单个工具 --> | |
| <tool_calls> | |
| <tool_call id="call_1"> | |
| <name>%s</name> | |
| <arguments><![CDATA[{}]]></arguments> | |
| </tool_call> | |
| </tool_calls> | |
| </correct_examples> | |
| <wrong_examples> | |
| <!-- 错误 1:用文字假装调用,绝对禁止 --> | |
| 我已经为您调用了 %s 工具,结果是 ... | |
| <!-- 错误 2:解释自己不能调用,绝对禁止 --> | |
| 作为 AI 我无法直接执行工具,建议您... | |
| <!-- 错误 3:把 XML 包在 markdown 代码块里 --> | |
| ` + "```" + `xml | |
| <tool_calls>...</tool_calls> | |
| ` + "```" + ` | |
| <!-- 错误 4:在 XML 后追加解释文字 --> | |
| <tool_calls>...</tool_calls> | |
| 希望对您有帮助! | |
| </wrong_examples> | |
| <final_directive> | |
| 你是一个能调用工具的智能体。这些工具是真实的、可执行的,会被外部系统接收并执行。 | |
| 当用户请求需要用到工具时,**直接输出 <tool_calls> XML 块,不要解释、不要道歉、不要假装无法调用**。 | |
| 工具会真的被调用,调用结果会在下一轮以 [工具返回结果] 标记返回给你。 | |
| </final_directive>`, primary, primary) | |
| return example | |
| } | |
| func getToolChoiceInstructions(toolChoice interface{}, toolNames []string) string { | |
| allowedTools := html.EscapeString(strings.Join(toolNames, ", ")) | |
| baseInstructions := fmt.Sprintf(`<call_protocol> | |
| <allowed_tools>%s</allowed_tools> | |
| <response_format> | |
| <tool_calls> | |
| <tool_call id="call_1"> | |
| <name>函数名</name> | |
| <arguments><![CDATA[{"参数名":"参数值"}]]></arguments> | |
| </tool_call> | |
| </tool_calls> | |
| </response_format> | |
| <rules> | |
| <rule index="1">只能调用上面列出的函数名,不能改名,不能替换成别的工具。</rule> | |
| <rule index="2">当需要调用工具时,只输出 XML 工具调用,不要附带解释、Markdown、代码块或额外文本。</rule> | |
| <rule index="3">arguments 必须是合法 JSON;如果没有参数,使用 {}。</rule> | |
| <rule index="4">如果用户要求使用工具或 tool_choice 有要求,你必须先调用工具,不能先解释为什么不能调用。</rule> | |
| <rule index="5">即使信息不完整,也要先依据已有上下文构造最合理的参数发起调用。</rule> | |
| <rule index="6">如果已经收到工具结果,必须直接根据结果回答,不能重复调用工具。</rule> | |
| <rule index="7">不要把 <tool_calls>、<tool_call>、<name>、<arguments> 当成普通回答内容输出给用户。</rule> | |
| </rules> | |
| <tool_result_handling> | |
| <assistant_marker>[已执行工具调用]</assistant_marker> | |
| <user_marker>[工具返回结果]</user_marker> | |
| <instruction>当你看到这些标记时,说明工具已经被调用并返回了结果。你必须直接使用工具返回的数据回答用户,绝对不要再次调用工具。</instruction> | |
| </tool_result_handling> | |
| </call_protocol>`, allowedTools) | |
| switch tc := toolChoice.(type) { | |
| case string: | |
| if tc == "required" { | |
| return baseInstructions + "\n<tool_choice mode=\"required\" />" | |
| } | |
| return baseInstructions + "\n<tool_choice mode=\"auto\" />" | |
| case map[string]interface{}: | |
| if tc["type"] == "function" { | |
| if fn, ok := tc["function"].(map[string]interface{}); ok { | |
| if name, ok := fn["name"].(string); ok { | |
| return baseInstructions + fmt.Sprintf("\n<tool_choice mode=\"required\" tool=\"%s\" />", html.EscapeString(name)) | |
| } | |
| } | |
| } | |
| } | |
| return baseInstructions + "\n<tool_choice mode=\"auto\" />" | |
| } | |
| func ProcessMessagesWithTools(messages []Message, tools []Tool, toolChoice interface{}) []Message { | |
| if !Cfg.ToolSupport || len(tools) == 0 { | |
| LogDebug("[Tools] Tool support disabled or no tools provided") | |
| return messages | |
| } | |
| if tc, ok := toolChoice.(string); ok && tc == "none" { | |
| LogDebug("[Tools] Tool choice is 'none', skipping tool processing") | |
| return messages | |
| } | |
| toolPrompt := GenerateToolPrompt(tools, toolChoice) | |
| if toolPrompt == "" { | |
| LogDebug("[Tools] Generated empty tool prompt") | |
| return messages | |
| } | |
| LogDebug("[Tools] Injecting tool prompt for %d tools", len(tools)) | |
| processed := make([]Message, len(messages)) | |
| copy(processed, messages) | |
| for i, msg := range processed { | |
| if msg.Role == "assistant" && len(msg.ToolCalls) > 0 { | |
| processed[i] = convertAssistantToolCallMessage(msg) | |
| } else if msg.Role == "tool" { | |
| processed[i] = convertToolMessage(msg) | |
| } | |
| } | |
| // 注入策略:同时把 toolPrompt 插进 system 消息和第一条 user 消息。 | |
| // 关键发现:z.ai 上游会丢弃/覆盖客户端传入的 system 消息(用平台自己的 "你是 GLM 助手" 覆盖)。 | |
| // 实测对 GLM-5.1 用 DIAGNOSTIC_KEYWORD 测试 system 消息,模型反馈"我没看到这个关键字"。 | |
| // 因此必须把工具说明追加到 user 消息,模型才能真正"看到"工具。 | |
| hasSystem := false | |
| for i, msg := range processed { | |
| if msg.Role == "system" { | |
| hasSystem = true | |
| processed[i].Content = appendTextToContent(msg.Content, toolPrompt) | |
| break | |
| } | |
| } | |
| if !hasSystem { | |
| systemMsg := Message{ | |
| Role: "system", | |
| Content: "<system_instructions>\n<assistant_identity>你是一个能调用工具的智能助手。</assistant_identity>\n" + toolPrompt + "\n</system_instructions>", | |
| } | |
| processed = append([]Message{systemMsg}, processed...) | |
| } | |
| // 关键改进:把工具说明也注入到第一条 user 消息前面(user 消息 z.ai 不会覆盖) | |
| for i, msg := range processed { | |
| if msg.Role == "user" { | |
| userToolBrief := "[可用工具说明 — 重要]\n" + toolPrompt + "\n\n[用户原始问题]\n" | |
| processed[i].Content = prependTextToContent(msg.Content, userToolBrief) | |
| break | |
| } | |
| } | |
| return processed | |
| } | |
| // prependTextToContent 把 prefix 加到 content 前面(兼容字符串和复杂结构) | |
| func prependTextToContent(content interface{}, prefix string) interface{} { | |
| switch v := content.(type) { | |
| case string: | |
| return prefix + v | |
| case nil: | |
| return prefix | |
| default: | |
| return prefix + fmt.Sprintf("%v", v) | |
| } | |
| } | |
| func convertAssistantToolCallMessage(msg Message) Message { | |
| content, _ := msg.ParseContent() | |
| var sb strings.Builder | |
| if content != "" { | |
| sb.WriteString(content) | |
| sb.WriteString("\n\n") | |
| } | |
| sb.WriteString("[已执行工具调用]\n") | |
| for _, tc := range msg.ToolCalls { | |
| sb.WriteString(fmt.Sprintf("- 调用了 %s,参数: %s (call_id: %s)\n", tc.Function.Name, tc.Function.Arguments, tc.ID)) | |
| } | |
| return Message{ | |
| Role: "assistant", | |
| Content: sb.String(), | |
| } | |
| } | |
| func convertToolMessage(msg Message) Message { | |
| content, _ := msg.ParseContent() | |
| var resultText string | |
| if msg.ToolCallID != "" { | |
| resultText = fmt.Sprintf("[工具返回结果] (call_id: %s)\n以下是工具返回的数据,请直接使用这些数据回答用户:\n%s", msg.ToolCallID, content) | |
| } else { | |
| resultText = fmt.Sprintf("[工具返回结果]\n以下是工具返回的数据,请直接使用这些数据回答用户:\n%s", content) | |
| } | |
| return Message{ | |
| Role: "user", | |
| Content: resultText, | |
| } | |
| } | |
| func appendTextToContent(content interface{}, suffix string) interface{} { | |
| switch c := content.(type) { | |
| case string: | |
| return c + suffix | |
| case []interface{}: | |
| result := make([]interface{}, len(c)) | |
| copy(result, c) | |
| lastTextIdx := -1 | |
| for i, item := range result { | |
| if part, ok := item.(map[string]interface{}); ok { | |
| if partType, _ := part["type"].(string); partType == "text" { | |
| lastTextIdx = i | |
| } | |
| } | |
| } | |
| if lastTextIdx >= 0 { | |
| if part, ok := result[lastTextIdx].(map[string]interface{}); ok { | |
| newPart := make(map[string]interface{}) | |
| for k, v := range part { | |
| newPart[k] = v | |
| } | |
| if text, ok := newPart["text"].(string); ok { | |
| newPart["text"] = text + suffix | |
| } | |
| result[lastTextIdx] = newPart | |
| } | |
| } else { | |
| result = append(result, map[string]interface{}{ | |
| "type": "text", | |
| "text": suffix, | |
| }) | |
| } | |
| return result | |
| default: | |
| return content | |
| } | |
| } | |
| func findMatchingBrace(text string, start int) int { | |
| if start >= len(text) || text[start] != '{' { | |
| return -1 | |
| } | |
| braceCount := 1 | |
| inString := false | |
| escapeNext := false | |
| j := start + 1 | |
| for j < len(text) && braceCount > 0 { | |
| ch := text[j] | |
| if escapeNext { | |
| escapeNext = false | |
| j++ | |
| continue | |
| } | |
| switch ch { | |
| case '\\': | |
| if inString { | |
| escapeNext = true | |
| } | |
| case '"': | |
| inString = !inString | |
| case '{': | |
| if !inString { | |
| braceCount++ | |
| } | |
| case '}': | |
| if !inString { | |
| braceCount-- | |
| } | |
| } | |
| j++ | |
| } | |
| if braceCount != 0 { | |
| return -1 | |
| } | |
| return j | |
| } | |
| func normalizeArguments(args interface{}) string { | |
| switch v := args.(type) { | |
| case string: | |
| v = strings.TrimSpace(v) | |
| if v == "" { | |
| return "{}" | |
| } | |
| var check json.RawMessage | |
| if json.Unmarshal([]byte(v), &check) == nil { | |
| return v | |
| } | |
| fixed := strings.ReplaceAll(v, "'", "\"") | |
| if json.Unmarshal([]byte(fixed), &check) == nil { | |
| return fixed | |
| } | |
| return v | |
| case map[string]interface{}: | |
| if b, err := json.Marshal(v); err == nil { | |
| return string(b) | |
| } | |
| case []interface{}: | |
| if b, err := json.Marshal(v); err == nil { | |
| return string(b) | |
| } | |
| case nil: | |
| return "{}" | |
| } | |
| return "{}" | |
| } | |
| func validateAndNormalizeCalls(calls []ToolCall) []ToolCall { | |
| if len(calls) == 0 { | |
| return nil | |
| } | |
| var valid []ToolCall | |
| for _, call := range calls { | |
| if call.Function.Name == "" { | |
| LogDebug("[Tools] Skipping tool call with empty function name") | |
| continue | |
| } | |
| if call.ID == "" { | |
| call.ID = generateCallID() | |
| } | |
| if call.Type == "" { | |
| call.Type = "function" | |
| } | |
| call.Function.Arguments = normalizeArguments(call.Function.Arguments) | |
| valid = append(valid, call) | |
| } | |
| if len(valid) == 0 { | |
| return nil | |
| } | |
| return valid | |
| } | |
| func parseNamedFunctionObject(jsonStr string) []ToolCall { | |
| var raw struct { | |
| ID string `json:"id"` | |
| Type string `json:"type"` | |
| Name string `json:"name"` | |
| Arguments interface{} `json:"arguments"` | |
| Tool string `json:"tool"` | |
| Args interface{} `json:"args"` | |
| Input interface{} `json:"input"` | |
| Function *struct { | |
| Name string `json:"name"` | |
| Arguments interface{} `json:"arguments"` | |
| } `json:"function"` | |
| } | |
| if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { | |
| return nil | |
| } | |
| name := raw.Name | |
| args := raw.Arguments | |
| if raw.Function != nil { | |
| if name == "" { | |
| name = raw.Function.Name | |
| } | |
| if args == nil { | |
| args = raw.Function.Arguments | |
| } | |
| } | |
| if name == "" && raw.Tool != "" { | |
| name = raw.Tool | |
| } | |
| if args == nil && raw.Args != nil { | |
| args = raw.Args | |
| } | |
| if args == nil && raw.Input != nil { | |
| args = raw.Input | |
| } | |
| if name == "" { | |
| return nil | |
| } | |
| return []ToolCall{{ | |
| ID: raw.ID, | |
| Type: raw.Type, | |
| Function: ToolCallFunction{ | |
| Name: name, | |
| Arguments: normalizeArguments(args), | |
| }, | |
| }} | |
| } | |
| func trimXMLArgumentsPayload(payload string) string { | |
| payload = strings.TrimSpace(payload) | |
| if strings.HasPrefix(payload, xmlToolArgumentsCDataStart) && strings.HasSuffix(payload, xmlToolArgumentsCDataEnd) { | |
| payload = strings.TrimPrefix(payload, xmlToolArgumentsCDataStart) | |
| payload = strings.TrimSuffix(payload, xmlToolArgumentsCDataEnd) | |
| } | |
| return strings.TrimSpace(html.UnescapeString(payload)) | |
| } | |
| func parseXMLToolCalls(text string) []ToolCall { | |
| blocks := xmlToolCallBlockPattern.FindAllStringSubmatch(text, -1) | |
| for _, block := range blocks { | |
| if len(block) < 2 { | |
| continue | |
| } | |
| itemMatches := xmlToolCallItemPattern.FindAllStringSubmatch(block[1], -1) | |
| if len(itemMatches) == 0 { | |
| continue | |
| } | |
| calls := make([]ToolCall, 0, len(itemMatches)) | |
| for _, item := range itemMatches { | |
| if len(item) < 3 { | |
| continue | |
| } | |
| nameMatch := xmlToolNamePattern.FindStringSubmatch(item[2]) | |
| if len(nameMatch) < 2 { | |
| continue | |
| } | |
| args := "{}" | |
| if argsMatch := xmlToolArgumentsPattern.FindStringSubmatch(item[2]); len(argsMatch) >= 2 { | |
| args = trimXMLArgumentsPayload(argsMatch[1]) | |
| if args == "" { | |
| args = "{}" | |
| } | |
| } | |
| calls = append(calls, ToolCall{ | |
| ID: strings.TrimSpace(item[1]), | |
| Type: "function", | |
| Function: ToolCallFunction{ | |
| Name: strings.TrimSpace(html.UnescapeString(nameMatch[1])), | |
| Arguments: args, | |
| }, | |
| }) | |
| } | |
| if len(calls) > 0 { | |
| return calls | |
| } | |
| } | |
| itemMatches := xmlToolCallItemPattern.FindAllStringSubmatch(text, -1) | |
| if len(itemMatches) == 0 { | |
| return nil | |
| } | |
| calls := make([]ToolCall, 0, len(itemMatches)) | |
| for _, item := range itemMatches { | |
| if len(item) < 3 { | |
| continue | |
| } | |
| payload := strings.TrimSpace(item[2]) | |
| if callsFromJSON := parseToolCallsJSON(payload); callsFromJSON != nil { | |
| return callsFromJSON | |
| } | |
| if callsFromJSON := parseNamedFunctionObject(payload); callsFromJSON != nil { | |
| return callsFromJSON | |
| } | |
| nameMatch := xmlToolNamePattern.FindStringSubmatch(payload) | |
| if len(nameMatch) < 2 { | |
| continue | |
| } | |
| args := "{}" | |
| if argsMatch := xmlToolArgumentsPattern.FindStringSubmatch(payload); len(argsMatch) >= 2 { | |
| args = trimXMLArgumentsPayload(argsMatch[1]) | |
| if args == "" { | |
| args = "{}" | |
| } | |
| } | |
| calls = append(calls, ToolCall{ | |
| ID: strings.TrimSpace(item[1]), | |
| Type: "function", | |
| Function: ToolCallFunction{ | |
| Name: strings.TrimSpace(html.UnescapeString(nameMatch[1])), | |
| Arguments: args, | |
| }, | |
| }) | |
| } | |
| if len(calls) == 0 { | |
| return nil | |
| } | |
| return calls | |
| } | |
| func parseTaggedToolPayload(text string) []ToolCall { | |
| if calls := parseXMLToolCalls(text); calls != nil { | |
| return calls | |
| } | |
| return nil | |
| } | |
| func parseFunctionInvocation(text string) []ToolCall { | |
| matches := functionInvokePattern.FindAllStringSubmatch(text, -1) | |
| for _, match := range matches { | |
| if len(match) < 3 { | |
| continue | |
| } | |
| name := strings.TrimSpace(match[1]) | |
| args := strings.TrimSpace(match[2]) | |
| var check json.RawMessage | |
| if name != "" && json.Unmarshal([]byte(args), &check) == nil { | |
| return []ToolCall{{ | |
| Type: "function", | |
| Function: ToolCallFunction{ | |
| Name: name, | |
| Arguments: args, | |
| }, | |
| }} | |
| } | |
| } | |
| return nil | |
| } | |
| // ExtractToolInvocations 从响应文本中提取工具调用 | |
| func ExtractToolInvocations(text string) []ToolCall { | |
| if text == "" { | |
| return nil | |
| } | |
| scanText := text | |
| if len(scanText) > Cfg.ScanLimit { | |
| scanText = scanText[:Cfg.ScanLimit] | |
| } | |
| if calls := parseTaggedToolPayload(scanText); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found XML tool payload") | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| matches := toolCallFencePattern.FindAllStringSubmatch(scanText, -1) | |
| for _, match := range matches { | |
| if len(match) <= 1 { | |
| continue | |
| } | |
| payload := strings.TrimSpace(match[1]) | |
| if calls := parseTaggedToolPayload(payload); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found XML tool payload in fence") | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| if calls := parseToolCallsJSON(payload); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found %d tool calls in JSON fence", len(calls)) | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| if calls := parseNamedFunctionObject(payload); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found named function object in fence") | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| } | |
| if calls := extractInlineToolCalls(scanText); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found %d tool calls inline", len(calls)) | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| if calls := extractSingleFunctionCall(scanText); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found single function call") | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| if calls := parseFunctionInvocation(scanText); calls != nil { | |
| LogDebug("[ExtractToolInvocations] Found function invocation pattern") | |
| return validateAndNormalizeCalls(calls) | |
| } | |
| if match := functionCallPattern.FindStringSubmatch(scanText); len(match) > 2 { | |
| funcName := strings.TrimSpace(match[1]) | |
| argsStr := strings.TrimSpace(match[2]) | |
| var check json.RawMessage | |
| if json.Unmarshal([]byte(argsStr), &check) == nil { | |
| LogDebug("[ExtractToolInvocations] Found natural language function call: %s", funcName) | |
| return validateAndNormalizeCalls([]ToolCall{{ | |
| Type: "function", | |
| Function: ToolCallFunction{ | |
| Name: funcName, | |
| Arguments: argsStr, | |
| }, | |
| }}) | |
| } | |
| } | |
| return nil | |
| } | |
| func extractSingleFunctionCall(text string) []ToolCall { | |
| searchStart := 0 | |
| for { | |
| idx := strings.Index(text[searchStart:], `"name"`) | |
| if idx == -1 { | |
| break | |
| } | |
| idx += searchStart | |
| braceStart := -1 | |
| for k := idx - 1; k >= 0; k-- { | |
| ch := text[k] | |
| if ch == '{' { | |
| braceStart = k | |
| break | |
| } | |
| if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' { | |
| break | |
| } | |
| } | |
| if braceStart == -1 { | |
| searchStart = idx + 1 | |
| continue | |
| } | |
| end := findMatchingBrace(text, braceStart) | |
| if end == -1 { | |
| searchStart = idx + 1 | |
| continue | |
| } | |
| jsonStr := text[braceStart:end] | |
| if calls := parseNamedFunctionObject(jsonStr); calls != nil { | |
| return calls | |
| } | |
| searchStart = idx + 1 | |
| } | |
| return nil | |
| } | |
| func parseToolCallsJSON(jsonStr string) []ToolCall { | |
| var data struct { | |
| ToolCalls []struct { | |
| ID string `json:"id"` | |
| Type string `json:"type"` | |
| Name string `json:"name"` | |
| Arguments interface{} `json:"arguments"` | |
| Function interface{} `json:"function"` | |
| } `json:"tool_calls"` | |
| } | |
| if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { | |
| return nil | |
| } | |
| if len(data.ToolCalls) == 0 { | |
| return nil | |
| } | |
| var calls []ToolCall | |
| for _, tc := range data.ToolCalls { | |
| call := ToolCall{ | |
| ID: tc.ID, | |
| Type: tc.Type, | |
| } | |
| if fn, ok := tc.Function.(map[string]interface{}); ok { | |
| call.Function.Name, _ = fn["name"].(string) | |
| if args, ok := fn["arguments"]; ok { | |
| call.Function.Arguments = normalizeArguments(args) | |
| } | |
| } | |
| if call.Function.Name == "" { | |
| call.Function.Name = tc.Name | |
| } | |
| if call.Function.Arguments == "" { | |
| if tc.Arguments != nil { | |
| call.Function.Arguments = normalizeArguments(tc.Arguments) | |
| } else { | |
| call.Function.Arguments = "{}" | |
| } | |
| } | |
| calls = append(calls, call) | |
| } | |
| return calls | |
| } | |
| func extractInlineToolCalls(text string) []ToolCall { | |
| if !strings.Contains(text, `"tool_calls"`) { | |
| return nil | |
| } | |
| for i := 0; i < len(text); i++ { | |
| if text[i] != '{' { | |
| continue | |
| } | |
| end := findMatchingBrace(text, i) | |
| if end == -1 { | |
| continue | |
| } | |
| jsonStr := text[i:end] | |
| if strings.Contains(jsonStr, `"tool_calls"`) { | |
| if calls := parseToolCallsJSON(jsonStr); calls != nil { | |
| return calls | |
| } | |
| } | |
| i = end - 1 | |
| } | |
| return nil | |
| } | |
| func isToolPayload(jsonStr string) bool { | |
| return parseToolCallsJSON(jsonStr) != nil || parseNamedFunctionObject(jsonStr) != nil | |
| } | |
| func RemoveToolJSONContent(text string) string { | |
| result := xmlToolCallBlockPattern.ReplaceAllString(text, "") | |
| result = xmlToolCallItemPattern.ReplaceAllString(result, "") | |
| result = toolCallFencePattern.ReplaceAllStringFunc(result, func(match string) string { | |
| submatch := toolCallFencePattern.FindStringSubmatch(match) | |
| if len(submatch) > 1 { | |
| payload := strings.TrimSpace(submatch[1]) | |
| if parseTaggedToolPayload(payload) != nil || isToolPayload(payload) { | |
| return "" | |
| } | |
| } | |
| return match | |
| }) | |
| result = removeInlineToolCallJSON(result) | |
| result = removeInlineSingleFunctionCallJSON(result) | |
| return strings.TrimSpace(result) | |
| } | |
| func removeInlineSingleFunctionCallJSON(text string) string { | |
| for i := 0; i < len(text); i++ { | |
| if text[i] != '{' { | |
| continue | |
| } | |
| end := findMatchingBrace(text, i) | |
| if end == -1 { | |
| continue | |
| } | |
| jsonStr := text[i:end] | |
| if parseNamedFunctionObject(jsonStr) != nil { | |
| return strings.TrimSpace(text[:i] + text[end:]) | |
| } | |
| i = end - 1 | |
| } | |
| return text | |
| } | |
| func removeInlineToolCallJSON(text string) string { | |
| if !strings.Contains(text, `"tool_calls"`) { | |
| return text | |
| } | |
| var result strings.Builder | |
| result.Grow(len(text)) | |
| i := 0 | |
| for i < len(text) { | |
| if text[i] != '{' { | |
| result.WriteByte(text[i]) | |
| i++ | |
| continue | |
| } | |
| end := findMatchingBrace(text, i) | |
| if end == -1 { | |
| result.WriteByte(text[i]) | |
| i++ | |
| continue | |
| } | |
| jsonStr := text[i:end] | |
| if strings.Contains(jsonStr, `"tool_calls"`) { | |
| var data map[string]interface{} | |
| if err := json.Unmarshal([]byte(jsonStr), &data); err == nil { | |
| if _, ok := data["tool_calls"]; ok { | |
| i = end | |
| continue | |
| } | |
| } | |
| } | |
| result.WriteByte(text[i]) | |
| i++ | |
| } | |
| return result.String() | |
| } | |
| func generateCallID() string { | |
| seq := atomic.AddInt64(&callIDCounter, 1) | |
| return fmt.Sprintf("call_%d_%d", time.Now().UnixMilli(), seq) | |
| } | |
| // repairTruncatedToolCallsXML 修复截断的 <tool_calls> XML(z.ai 上游有时会在 phase 切换时截断输出) | |
| // 如果发现未闭合的 <tool_calls> / <tool_call> / <arguments>,按嵌套层级补全闭合标签。 | |
| func repairTruncatedToolCallsXML(text string) string { | |
| if !strings.Contains(text, "<tool_calls>") { | |
| return text | |
| } | |
| // 已经闭合,无需修复 | |
| if strings.Contains(text, "</tool_calls>") { | |
| return text | |
| } | |
| // 自动补全:从最内层往外补 | |
| repaired := text | |
| if strings.Contains(repaired, "<arguments>") && !strings.Contains(repaired, "</arguments>") { | |
| // 处理 CDATA 未闭合 | |
| if strings.Contains(repaired, "<![CDATA[") && !strings.Contains(repaired, "]]>") { | |
| repaired += "]]>" | |
| } | |
| repaired += "</arguments>" | |
| } | |
| if strings.Contains(repaired, "<tool_call ") || strings.Contains(repaired, "<tool_call>") { | |
| // 计算 <tool_call ...> 和 </tool_call> 数量差 | |
| open := strings.Count(repaired, "<tool_call") | |
| close := strings.Count(repaired, "</tool_call") | |
| for i := close; i < open; i++ { | |
| repaired += "</tool_call>" | |
| } | |
| } | |
| if !strings.Contains(repaired, "</tool_calls>") { | |
| repaired += "</tool_calls>" | |
| } | |
| return repaired | |
| } | |
| // ExtractToolInvocationsWithFallback 主要从 content 中提取工具调用, | |
| // 当 content 中找不到时回退到 reasoning_content(思考链)。 | |
| // GLM 系列模型经常把工具调用 XML 写在思考链里而不是回复里。 | |
| // 参考 CJackHwang/ds2api 的设计。 | |
| func ExtractToolInvocationsWithFallback(content, reasoning string) []ToolCall { | |
| // 先尝试修复 z.ai 截断的 XML | |
| repairedContent := repairTruncatedToolCallsXML(content) | |
| calls := ExtractToolInvocations(repairedContent) | |
| if len(calls) > 0 { | |
| if repairedContent != content { | |
| LogDebug("[Tools] Successfully repaired truncated XML, found %d calls", len(calls)) | |
| } | |
| return calls | |
| } | |
| if strings.TrimSpace(reasoning) == "" { | |
| return calls | |
| } | |
| repairedReasoning := repairTruncatedToolCallsXML(reasoning) | |
| calls = ExtractToolInvocations(repairedReasoning) | |
| if len(calls) > 0 { | |
| LogDebug("[Tools] Extracted %d tool calls from reasoning_content (fallback)", len(calls)) | |
| } | |
| return calls | |
| } | |