feat(tools): implement universal tool calling and multi-turn resolution for generic Gradio spaces
This commit is contained in:
@@ -438,18 +438,33 @@ func BuildToolInstruction(tools []Tool) string {
|
||||
return ""
|
||||
}
|
||||
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
|
||||
return fmt.Sprintf("\n\n# Tool Calling Instructions\n\nYou have access to the following functions:\n<tools>\n%s\n</tools>\n\nWhen you need to call a function, respond ONLY with a <tool_call> block formatted exactly as follows:\n<tool_call>\n{\"name\": \"<function-name>\", \"arguments\": {<args-json-object>}}\n</tool_call>\n\nDo not include conversational filler before or after the tool call.", string(toolsBytes))
|
||||
return fmt.Sprintf("\n\n# Tool Calling Instructions\n\nYou have access to the following functions:\n<tools>\n%s\n</tools>\n\nWhen you need to call a function, respond ONLY with a <tool_call> block formatted exactly as follows:\n<tool_call>\n{\"name\": \"<function-name>\", \"arguments\": {<args-json-object>}}\n</tool_call>\n\nWhen you receive a <tool_response>, use the provided information to answer the user's request, or call further tools if needed.\nDo not include conversational filler before or after the tool call.", string(toolsBytes))
|
||||
}
|
||||
|
||||
func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) {
|
||||
toolInstruction = BuildToolInstruction(req.Tools)
|
||||
// 1. Build lookup map from tool_call_id to function name across all assistant messages
|
||||
toolIDToName := make(map[string]string)
|
||||
for _, msg := range req.Messages {
|
||||
for _, tc := range msg.ToolCalls {
|
||||
if tc.ID != "" && tc.Function.Name != "" {
|
||||
toolIDToName[tc.ID] = tc.Function.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toolInstruction = BuildToolInstruction(req.Tools)
|
||||
|
||||
// 2. Process and coalesce messages preserving turn parity
|
||||
var staged []ChatMessage
|
||||
for i := 0; i < len(req.Messages); i++ {
|
||||
msg := req.Messages[i]
|
||||
contentStr := msg.GetContentString()
|
||||
m := ChatMessage{Role: msg.Role, Content: contentStr}
|
||||
|
||||
switch msg.Role {
|
||||
case "system":
|
||||
hasSystem = true
|
||||
m.Content = contentStr
|
||||
staged = append(staged, ChatMessage{Role: "system", Content: contentStr})
|
||||
|
||||
case "assistant":
|
||||
var sb strings.Builder
|
||||
if contentStr != "" {
|
||||
@@ -465,40 +480,79 @@ func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, tool
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("<tool_call>\n{\"name\": %q, \"arguments\": %s}\n</tool_call>", tc.Function.Name, args))
|
||||
}
|
||||
m.Content = sb.String()
|
||||
staged = append(staged, ChatMessage{
|
||||
Role: "assistant",
|
||||
Content: sb.String(),
|
||||
ReasoningContent: msg.ReasoningContent,
|
||||
ToolCalls: msg.ToolCalls,
|
||||
})
|
||||
|
||||
case "tool", "function":
|
||||
m.Role = "user"
|
||||
toolName := msg.Name
|
||||
if toolName == "" {
|
||||
toolName = msg.ToolCallID
|
||||
// Gather consecutive tool returns into a coalesced turn
|
||||
var toolResponses []string
|
||||
j := i
|
||||
for j < len(req.Messages) && (req.Messages[j].Role == "tool" || req.Messages[j].Role == "function") {
|
||||
tMsg := req.Messages[j]
|
||||
tContent := tMsg.GetContentString()
|
||||
tName := tMsg.Name
|
||||
if tName == "" && tMsg.ToolCallID != "" {
|
||||
if mapped, ok := toolIDToName[tMsg.ToolCallID]; ok {
|
||||
tName = mapped
|
||||
} else {
|
||||
tName = tMsg.ToolCallID
|
||||
}
|
||||
}
|
||||
var contentJSON []byte
|
||||
if json.Valid([]byte(tContent)) {
|
||||
contentJSON = []byte(tContent)
|
||||
} else {
|
||||
contentJSON, _ = json.Marshal(tContent)
|
||||
}
|
||||
toolResponses = append(toolResponses, fmt.Sprintf("<tool_response>\n{\"name\": %q, \"content\": %s}\n</tool_response>", tName, string(contentJSON)))
|
||||
j++
|
||||
}
|
||||
var contentJSON []byte
|
||||
if json.Valid([]byte(contentStr)) {
|
||||
contentJSON = []byte(contentStr)
|
||||
} else {
|
||||
contentJSON, _ = json.Marshal(contentStr)
|
||||
i = j - 1 // advance loop
|
||||
|
||||
promptSuffix := "Please answer the user's request based on the tool result."
|
||||
if len(toolResponses) > 1 {
|
||||
promptSuffix = "Please answer the user's request based on the tool results."
|
||||
}
|
||||
m.Content = fmt.Sprintf("<tool_response>\n{\"name\": %q, \"content\": %s}\n</tool_response>", toolName, string(contentJSON))
|
||||
coalesced := strings.Join(toolResponses, "\n") + "\n\n" + promptSuffix
|
||||
staged = append(staged, ChatMessage{
|
||||
Role: "user",
|
||||
Content: coalesced,
|
||||
})
|
||||
|
||||
default: // "user" or other roles
|
||||
staged = append(staged, ChatMessage{Role: msg.Role, Content: contentStr})
|
||||
}
|
||||
processed = append(processed, m)
|
||||
}
|
||||
|
||||
// 3. Inject tool instructions into system prompt
|
||||
if toolInstruction != "" {
|
||||
if hasSystem {
|
||||
for i, m := range processed {
|
||||
systemInjected := false
|
||||
for i, m := range staged {
|
||||
if m.Role == "system" {
|
||||
processed[i].Content = m.GetContentString() + "\n" + strings.TrimSpace(toolInstruction)
|
||||
staged[i].Content = m.GetContentString() + "\n\n" + strings.TrimSpace(toolInstruction)
|
||||
systemInjected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !systemInjected {
|
||||
staged = append([]ChatMessage{
|
||||
{Role: "system", Content: strings.TrimSpace(toolInstruction)},
|
||||
}, staged...)
|
||||
}
|
||||
} else {
|
||||
processed = append([]ChatMessage{
|
||||
{Role: "user", Content: strings.TrimSpace(toolInstruction)},
|
||||
}, processed...)
|
||||
staged = append([]ChatMessage{
|
||||
{Role: "system", Content: strings.TrimSpace(toolInstruction)},
|
||||
}, staged...)
|
||||
hasSystem = true
|
||||
}
|
||||
}
|
||||
|
||||
return processed, toolInstruction, hasSystem
|
||||
return staged, toolInstruction, hasSystem
|
||||
}
|
||||
|
||||
func cleanJSONBlock(input string) string {
|
||||
@@ -538,6 +592,35 @@ func sanitizeJSONValue(v interface{}) interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
type ToolTagPair struct {
|
||||
Start string
|
||||
End string
|
||||
}
|
||||
|
||||
var ToolTagPairs = []ToolTagPair{
|
||||
{Start: "<tool_call>", End: "</tool_call>"},
|
||||
{Start: "<tool_calls>", End: "</tool_calls>"},
|
||||
{Start: "<function_call>", End: "</function_call>"},
|
||||
{Start: "[TOOL_CALLS]", End: "[/TOOL_CALLS]"},
|
||||
}
|
||||
|
||||
func getToolStartPrefixes() []string {
|
||||
seen := make(map[string]bool)
|
||||
var prefixes []string
|
||||
for _, pair := range ToolTagPairs {
|
||||
for i := 1; i <= len(pair.Start); i++ {
|
||||
pref := pair.Start[:i]
|
||||
if !seen[pref] {
|
||||
seen[pref] = true
|
||||
prefixes = append(prefixes, pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
var toolStartPrefixes = getToolStartPrefixes()
|
||||
|
||||
func repairToolCallJSON(input string) (ToolCall, bool) {
|
||||
s := strings.TrimSpace(input)
|
||||
reName := regexp.MustCompile(`"(?:name|function|action|call)"\s*:\s*"([^"]+)"`)
|
||||
@@ -547,8 +630,8 @@ func repairToolCallJSON(input string) (ToolCall, bool) {
|
||||
}
|
||||
fnName := matches[1]
|
||||
|
||||
reArgs := regexp.MustCompile(`"(?:arguments|parameters|args|input)"\s*:\s*(\{[\s\S]*\})`)
|
||||
argMatches := reArgs.FindStringSubmatch(s)
|
||||
reArgsObj := regexp.MustCompile(`"(?:arguments|parameters|args|input)"\s*:\s*(\{[\s\S]*\})`)
|
||||
argMatches := reArgsObj.FindStringSubmatch(s)
|
||||
argsStr := "{}"
|
||||
if len(argMatches) >= 2 {
|
||||
candidate := argMatches[1]
|
||||
@@ -556,6 +639,15 @@ func repairToolCallJSON(input string) (ToolCall, bool) {
|
||||
if json.Unmarshal([]byte(candidate), &dummy) == nil {
|
||||
argsStr = candidate
|
||||
}
|
||||
} else {
|
||||
reArgsStr := regexp.MustCompile(`"(?:arguments|parameters|args|input)"\s*:\s*"((?:\\.|[^"\\])*)"`)
|
||||
strMatches := reArgsStr.FindStringSubmatch(s)
|
||||
if len(strMatches) >= 2 {
|
||||
var unescaped string
|
||||
if json.Unmarshal([]byte(`"`+strMatches[1]+`"`), &unescaped) == nil {
|
||||
argsStr = unescaped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ToolCall{
|
||||
@@ -671,18 +763,68 @@ func parseSingleToolCall(jsonStr string) (ToolCall, bool) {
|
||||
return repairToolCallJSON(cleaned)
|
||||
}
|
||||
|
||||
func parseXMLToolCall(block string) (ToolCall, bool) {
|
||||
inner := strings.TrimSpace(block)
|
||||
if strings.HasPrefix(inner, "<tool_call>") {
|
||||
inner = strings.TrimPrefix(inner, "<tool_call>")
|
||||
func parseMultipleToolCalls(raw string) ([]ToolCall, bool) {
|
||||
cleaned := cleanJSONBlock(raw)
|
||||
if cleaned == "" {
|
||||
return nil, false
|
||||
}
|
||||
if strings.HasSuffix(inner, "</tool_call>") {
|
||||
inner = strings.TrimSuffix(inner, "</tool_call>")
|
||||
|
||||
// 1. Direct JSON array: [{"name":...}, ...]
|
||||
var rawList []interface{}
|
||||
if err := json.Unmarshal([]byte(cleaned), &rawList); err == nil {
|
||||
var calls []ToolCall
|
||||
for _, item := range rawList {
|
||||
b, err := json.Marshal(item)
|
||||
if err == nil {
|
||||
if tc, ok := parseSingleToolCall(string(b)); ok {
|
||||
calls = append(calls, tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(calls) > 0 {
|
||||
return calls, true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Wrapper object with tool_calls / calls array
|
||||
var rawMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(cleaned), &rawMap); err == nil {
|
||||
for _, listKey := range []string{"tool_calls", "calls", "functions"} {
|
||||
if subArr, ok := rawMap[listKey].([]interface{}); ok && len(subArr) > 0 {
|
||||
var calls []ToolCall
|
||||
for _, item := range subArr {
|
||||
b, err := json.Marshal(item)
|
||||
if err == nil {
|
||||
if tc, ok := parseSingleToolCall(string(b)); ok {
|
||||
calls = append(calls, tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(calls) > 0 {
|
||||
return calls, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Single tool call
|
||||
if tc, ok := parseSingleToolCall(cleaned); ok {
|
||||
return []ToolCall{tc}, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func parseXMLToolCall(block string) ([]ToolCall, bool) {
|
||||
inner := strings.TrimSpace(block)
|
||||
for _, pair := range ToolTagPairs {
|
||||
inner = strings.ReplaceAll(inner, pair.Start, "")
|
||||
inner = strings.ReplaceAll(inner, pair.End, "")
|
||||
}
|
||||
inner = cleanJSONBlock(inner)
|
||||
|
||||
if tc, ok := parseSingleToolCall(inner); ok {
|
||||
return tc, true
|
||||
if calls, ok := parseMultipleToolCalls(inner); ok && len(calls) > 0 {
|
||||
return calls, true
|
||||
}
|
||||
|
||||
var fnName string
|
||||
@@ -707,88 +849,58 @@ func parseXMLToolCall(block string) (ToolCall, bool) {
|
||||
if argsStr == "" {
|
||||
argsStr = "{}"
|
||||
}
|
||||
return ToolCall{
|
||||
return []ToolCall{{
|
||||
ID: "call_" + GenerateUUID()[:8],
|
||||
Type: "function",
|
||||
Function: ToolCallFunction{
|
||||
Name: fnName,
|
||||
Arguments: argsStr,
|
||||
},
|
||||
}, true
|
||||
}}, true
|
||||
}
|
||||
|
||||
return ToolCall{}, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func ExtractToolCallBlocks(content string) (blocks []string, remaining string) {
|
||||
s := content
|
||||
remaining = content
|
||||
|
||||
for strings.Contains(s, "<tool_call>") {
|
||||
sIdx := strings.Index(s, "<tool_call>")
|
||||
rest := s[sIdx+len("<tool_call>"):]
|
||||
for _, pair := range ToolTagPairs {
|
||||
for strings.Contains(remaining, pair.Start) {
|
||||
sIdx := strings.Index(remaining, pair.Start)
|
||||
rest := remaining[sIdx+len(pair.Start):]
|
||||
|
||||
relNextSIdx := strings.Index(rest, "<tool_call>")
|
||||
var nextSIdx int
|
||||
if relNextSIdx != -1 {
|
||||
nextSIdx = sIdx + len("<tool_call>") + relNextSIdx
|
||||
} else {
|
||||
nextSIdx = -1
|
||||
}
|
||||
relNextSIdx := strings.Index(rest, pair.Start)
|
||||
var nextSIdx int
|
||||
if relNextSIdx != -1 {
|
||||
nextSIdx = sIdx + len(pair.Start) + relNextSIdx
|
||||
} else {
|
||||
nextSIdx = -1
|
||||
}
|
||||
|
||||
relEIdx := strings.Index(rest, "</tool_call>")
|
||||
var eIdx int
|
||||
if relEIdx != -1 {
|
||||
eIdx = sIdx + len("<tool_call>") + relEIdx
|
||||
} else {
|
||||
eIdx = -1
|
||||
}
|
||||
relEIdx := strings.Index(rest, pair.End)
|
||||
var eIdx int
|
||||
if relEIdx != -1 {
|
||||
eIdx = sIdx + len(pair.Start) + relEIdx
|
||||
} else {
|
||||
eIdx = -1
|
||||
}
|
||||
|
||||
var blockText string
|
||||
var blockEndPos int
|
||||
var blockText string
|
||||
if eIdx != -1 && (nextSIdx == -1 || eIdx < nextSIdx) {
|
||||
blockEndPos := eIdx + len(pair.End)
|
||||
blockText = remaining[sIdx:blockEndPos]
|
||||
remaining = strings.TrimSpace(remaining[:sIdx] + remaining[blockEndPos:])
|
||||
} else if nextSIdx != -1 {
|
||||
blockEndPos := nextSIdx
|
||||
blockText = remaining[sIdx:blockEndPos]
|
||||
remaining = strings.TrimSpace(remaining[:sIdx] + remaining[blockEndPos:])
|
||||
} else {
|
||||
blockText = remaining[sIdx:]
|
||||
remaining = strings.TrimSpace(remaining[:sIdx])
|
||||
}
|
||||
|
||||
if eIdx != -1 && (nextSIdx == -1 || eIdx < nextSIdx) {
|
||||
blockEndPos = eIdx + len("</tool_call>")
|
||||
blockText = s[sIdx:blockEndPos]
|
||||
s = s[blockEndPos:]
|
||||
} else if nextSIdx != -1 {
|
||||
blockEndPos = nextSIdx
|
||||
blockText = s[sIdx:blockEndPos]
|
||||
s = s[blockEndPos:]
|
||||
} else {
|
||||
blockText = s[sIdx:]
|
||||
s = ""
|
||||
}
|
||||
|
||||
blocks = append(blocks, blockText)
|
||||
}
|
||||
|
||||
for strings.Contains(remaining, "<tool_call>") {
|
||||
st := strings.Index(remaining, "<tool_call>")
|
||||
rest := remaining[st+len("<tool_call>"):]
|
||||
|
||||
relNext := strings.Index(rest, "<tool_call>")
|
||||
var nextSt int
|
||||
if relNext != -1 {
|
||||
nextSt = st + len("<tool_call>") + relNext
|
||||
} else {
|
||||
nextSt = -1
|
||||
}
|
||||
|
||||
relEn := strings.Index(rest, "</tool_call>")
|
||||
var en int
|
||||
if relEn != -1 {
|
||||
en = st + len("<tool_call>") + relEn
|
||||
} else {
|
||||
en = -1
|
||||
}
|
||||
|
||||
if en != -1 && (nextSt == -1 || en < nextSt) {
|
||||
remaining = strings.TrimSpace(remaining[:st] + remaining[en+len("</tool_call>"):])
|
||||
} else if nextSt != -1 {
|
||||
remaining = strings.TrimSpace(remaining[:st] + remaining[nextSt:])
|
||||
} else {
|
||||
remaining = strings.TrimSpace(remaining[:st])
|
||||
blocks = append(blocks, blockText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,8 +912,8 @@ func DetectToolCalls(content string) ([]ToolCall, string, bool) {
|
||||
var calls []ToolCall
|
||||
|
||||
for _, block := range blocks {
|
||||
if toolCall, ok := parseXMLToolCall(block); ok {
|
||||
calls = append(calls, toolCall)
|
||||
if tcs, ok := parseXMLToolCall(block); ok {
|
||||
calls = append(calls, tcs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,8 +921,8 @@ func DetectToolCalls(content string) ([]ToolCall, string, bool) {
|
||||
return calls, remaining, true
|
||||
}
|
||||
|
||||
if tc, ok := parseSingleToolCall(strings.TrimSpace(content)); ok {
|
||||
return []ToolCall{tc}, "", true
|
||||
if tcs, ok := parseMultipleToolCalls(strings.TrimSpace(content)); ok && len(tcs) > 0 {
|
||||
return tcs, "", true
|
||||
}
|
||||
|
||||
return nil, content, false
|
||||
@@ -955,12 +1067,24 @@ func NewStreamThinkingFilter() *StreamThinkingFilter {
|
||||
}
|
||||
|
||||
func hasPrefixOf(target string, prefixes []string) int {
|
||||
maxMatch := 0
|
||||
for _, p := range prefixes {
|
||||
if strings.HasSuffix(target, p) {
|
||||
return len(p)
|
||||
if strings.HasSuffix(target, p) && len(p) > maxMatch {
|
||||
maxMatch = len(p)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
return maxMatch
|
||||
}
|
||||
|
||||
func hasSuffixPrefixOf(target string, tag string) int {
|
||||
maxMatch := 0
|
||||
for i := 1; i < len(tag); i++ {
|
||||
p := tag[:i]
|
||||
if strings.HasSuffix(target, p) && len(p) > maxMatch {
|
||||
maxMatch = len(p)
|
||||
}
|
||||
}
|
||||
return maxMatch
|
||||
}
|
||||
|
||||
func (f *StreamThinkingFilter) Feed(chunk string, onContent func(string), onReasoning func(string)) {
|
||||
@@ -1034,11 +1158,13 @@ func (f *StreamThinkingFilter) Flush(onContent func(string), onReasoning func(st
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type StreamToolCallFilter struct {
|
||||
inToolCall bool
|
||||
buf string
|
||||
toolCallBuf string
|
||||
toolIndex int
|
||||
emittedCall bool
|
||||
inToolCall bool
|
||||
buf string
|
||||
toolCallBuf string
|
||||
toolIndex int
|
||||
emittedCall bool
|
||||
activePair ToolTagPair
|
||||
activeEndTag string
|
||||
}
|
||||
|
||||
func NewStreamToolCallFilter() *StreamToolCallFilter {
|
||||
@@ -1047,22 +1173,31 @@ func NewStreamToolCallFilter() *StreamToolCallFilter {
|
||||
|
||||
func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onToolCall func(ToolCall)) {
|
||||
f.buf += chunk
|
||||
toolStartTag := "<tool_call>"
|
||||
toolEndTag := "</tool_call>"
|
||||
|
||||
startPrefixes := []string{"<", "<t", "<to", "<too", "<tool", "<tool_", "<tool_c", "<tool_ca", "<tool_cal", "<tool_call"}
|
||||
endPrefixes := []string{"<", "</", "</t", "</to", "</too", "</tool", "</tool_", "</tool_c", "</tool_ca", "</tool_cal", "</tool_call"}
|
||||
|
||||
for len(f.buf) > 0 {
|
||||
if !f.inToolCall {
|
||||
if idx := strings.Index(f.buf, toolStartTag); idx != -1 {
|
||||
before := f.buf[:idx]
|
||||
earliestIdx := -1
|
||||
var matchedPair ToolTagPair
|
||||
|
||||
for _, pair := range ToolTagPairs {
|
||||
if idx := strings.Index(f.buf, pair.Start); idx != -1 {
|
||||
if earliestIdx == -1 || idx < earliestIdx {
|
||||
earliestIdx = idx
|
||||
matchedPair = pair
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if earliestIdx != -1 {
|
||||
before := f.buf[:earliestIdx]
|
||||
if before != "" {
|
||||
onContent(before)
|
||||
}
|
||||
f.inToolCall = true
|
||||
f.buf = f.buf[idx+len(toolStartTag):]
|
||||
} else if matchLen := hasPrefixOf(f.buf, startPrefixes); matchLen > 0 {
|
||||
f.activePair = matchedPair
|
||||
f.activeEndTag = matchedPair.End
|
||||
f.buf = f.buf[earliestIdx+len(matchedPair.Start):]
|
||||
} else if matchLen := hasPrefixOf(f.buf, toolStartPrefixes); matchLen > 0 {
|
||||
safe := f.buf[:len(f.buf)-matchLen]
|
||||
if safe != "" {
|
||||
onContent(safe)
|
||||
@@ -1075,28 +1210,32 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if idx := strings.Index(f.buf, toolEndTag); idx != -1 {
|
||||
if idx := strings.Index(f.buf, f.activeEndTag); idx != -1 {
|
||||
f.toolCallBuf += f.buf[:idx]
|
||||
f.buf = f.buf[idx+len(toolEndTag):]
|
||||
f.buf = f.buf[idx+len(f.activeEndTag):]
|
||||
f.inToolCall = false
|
||||
|
||||
if tc, ok := parseSingleToolCall(f.toolCallBuf); ok {
|
||||
idxCopy := f.toolIndex
|
||||
tc.Index = &idxCopy
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc)
|
||||
} else if tc2, ok2 := parseXMLToolCall("<tool_call>" + f.toolCallBuf + "</tool_call>"); ok2 {
|
||||
idxCopy := f.toolIndex
|
||||
tc2.Index = &idxCopy
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc2)
|
||||
if tcs, ok := parseMultipleToolCalls(f.toolCallBuf); ok && len(tcs) > 0 {
|
||||
for _, tc := range tcs {
|
||||
idxCopy := f.toolIndex
|
||||
tc.Index = &idxCopy
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc)
|
||||
}
|
||||
} else if tcs2, ok2 := parseXMLToolCall(f.activePair.Start + f.toolCallBuf + f.activePair.End); ok2 && len(tcs2) > 0 {
|
||||
for _, tc := range tcs2 {
|
||||
idxCopy := f.toolIndex
|
||||
tc.Index = &idxCopy
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc)
|
||||
}
|
||||
} else {
|
||||
onContent("<tool_call>" + f.toolCallBuf + "</tool_call>")
|
||||
onContent(f.activePair.Start + f.toolCallBuf + f.activePair.End)
|
||||
}
|
||||
f.toolCallBuf = ""
|
||||
} else if matchLen := hasPrefixOf(f.buf, endPrefixes); matchLen > 0 {
|
||||
} else if matchLen := hasSuffixPrefixOf(f.buf, f.activeEndTag); matchLen > 0 {
|
||||
safe := f.buf[:len(f.buf)-matchLen]
|
||||
f.toolCallBuf += safe
|
||||
f.buf = f.buf[len(f.buf)-matchLen:]
|
||||
@@ -1112,18 +1251,24 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
|
||||
|
||||
func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(ToolCall)) {
|
||||
if f.inToolCall && len(f.toolCallBuf) > 0 {
|
||||
if tc, ok := parseSingleToolCall(f.toolCallBuf); ok {
|
||||
idxCopy := f.toolIndex
|
||||
tc.Index = &idxCopy
|
||||
f.emittedCall = true
|
||||
onToolCall(tc)
|
||||
} else if tc2, ok2 := parseXMLToolCall("<tool_call>" + f.toolCallBuf + "</tool_call>"); ok2 {
|
||||
idxCopy := f.toolIndex
|
||||
tc2.Index = &idxCopy
|
||||
f.emittedCall = true
|
||||
onToolCall(tc2)
|
||||
if tcs, ok := parseMultipleToolCalls(f.toolCallBuf); ok && len(tcs) > 0 {
|
||||
for _, tc := range tcs {
|
||||
idxCopy := f.toolIndex
|
||||
tc.Index = &idxCopy
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc)
|
||||
}
|
||||
} else if tcs2, ok2 := parseXMLToolCall(f.activePair.Start + f.toolCallBuf + f.activePair.End); ok2 && len(tcs2) > 0 {
|
||||
for _, tc := range tcs2 {
|
||||
idxCopy := f.toolIndex
|
||||
tc.Index = &idxCopy
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc)
|
||||
}
|
||||
} else {
|
||||
onContent("<tool_call>" + f.toolCallBuf)
|
||||
onContent(f.activePair.Start + f.toolCallBuf)
|
||||
}
|
||||
f.toolCallBuf = ""
|
||||
}
|
||||
@@ -1268,21 +1413,18 @@ func (d *SpaceDiscovery) GetModelList() []ModelItem {
|
||||
return items
|
||||
}
|
||||
|
||||
// InspectSpace queries Gradio's /gradio_api/info, /config, and HuggingFace Space APIs
|
||||
// to build an adaptive schema mapping for any Gradio space.
|
||||
func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscovery, error) {
|
||||
cleanURL := strings.TrimRight(rawURL, "/")
|
||||
if !strings.HasPrefix(cleanURL, "http://") && !strings.HasPrefix(cleanURL, "https://") {
|
||||
func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
|
||||
cleanURL := strings.TrimRight(spaceURL, "/")
|
||||
if cleanURL != "" && !strings.HasPrefix(cleanURL, "http://") && !strings.HasPrefix(cleanURL, "https://") {
|
||||
cleanURL = "https://" + cleanURL
|
||||
}
|
||||
|
||||
discovery := &SpaceDiscovery{
|
||||
SpaceURL: cleanURL,
|
||||
APIPrefix: "/gradio_api",
|
||||
Endpoint: "/chat_fn",
|
||||
CleanEndpoint: "chat_fn",
|
||||
Protocol: "call",
|
||||
TotalInputs: 1,
|
||||
return &SpaceDiscovery{
|
||||
SpaceURL: cleanURL,
|
||||
APIPrefix: "/gradio_api",
|
||||
Endpoint: "/chat_fn",
|
||||
CleanEndpoint: "chat_fn",
|
||||
Protocol: "call",
|
||||
TotalInputs: 1,
|
||||
HistoryIndex: -1,
|
||||
MessageIndex: 0,
|
||||
SystemIndex: -1,
|
||||
@@ -1295,6 +1437,17 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
||||
HistoryFormat: "messages",
|
||||
LastDiscovered: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// InspectSpace queries Gradio's /gradio_api/info, /config, and HuggingFace Space APIs
|
||||
// to build an adaptive schema mapping for any Gradio space.
|
||||
func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscovery, error) {
|
||||
cleanURL := strings.TrimRight(rawURL, "/")
|
||||
if !strings.HasPrefix(cleanURL, "http://") && !strings.HasPrefix(cleanURL, "https://") {
|
||||
cleanURL = "https://" + cleanURL
|
||||
}
|
||||
|
||||
discovery := NewDefaultSpaceDiscovery(cleanURL)
|
||||
|
||||
// 1. Try fetching /gradio_api/info or /info
|
||||
var infoResp GradioAPIInfoResponse
|
||||
@@ -1532,16 +1685,19 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
||||
}
|
||||
}
|
||||
|
||||
// Check parameters in bestEndpointInfo for history support
|
||||
// Check parameters in bestEndpointInfo for input indices and history support
|
||||
if bestEndpointInfo != nil {
|
||||
if discovery.TotalInputs < len(bestEndpointInfo.Parameters) {
|
||||
discovery.TotalInputs = len(bestEndpointInfo.Parameters)
|
||||
}
|
||||
for idx, p := range bestEndpointInfo.Parameters {
|
||||
pName := strings.ToLower(p.ParameterName)
|
||||
if strings.Contains(pName, "message") && discovery.MessageIndex == 0 {
|
||||
discovery.MessageIndex = idx
|
||||
} else if strings.Contains(pName, "history") {
|
||||
discovery.HistoryIndex = idx
|
||||
} else if strings.Contains(pName, "system") {
|
||||
if strings.Contains(pName, "system") {
|
||||
discovery.SystemIndex = idx
|
||||
} else if strings.Contains(pName, "history") || strings.Contains(pName, "chat") {
|
||||
discovery.HistoryIndex = idx
|
||||
} else if strings.Contains(pName, "message") || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pName, "text") {
|
||||
discovery.MessageIndex = idx
|
||||
} else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") {
|
||||
discovery.ThinkLevelIndex = idx
|
||||
} else if strings.Contains(pName, "functions") || strings.Contains(pName, "tools") {
|
||||
@@ -1660,20 +1816,10 @@ func (g *GradioGateway) GetDiscovery(spaceURL, userAgent string) *SpaceDiscovery
|
||||
}
|
||||
|
||||
// Fallback discovery
|
||||
fallback := &SpaceDiscovery{
|
||||
SpaceURL: cleanTarget,
|
||||
APIPrefix: "/gradio_api",
|
||||
Endpoint: "/chat_fn",
|
||||
CleanEndpoint: "chat_fn",
|
||||
Protocol: "call",
|
||||
TotalInputs: 2,
|
||||
HistoryIndex: -1,
|
||||
MessageIndex: 0,
|
||||
SystemIndex: -1,
|
||||
PrimaryModel: "gradio-chat",
|
||||
Models: []string{"gradio-chat"},
|
||||
LastDiscovered: time.Now(),
|
||||
}
|
||||
fallback := NewDefaultSpaceDiscovery(cleanTarget)
|
||||
fallback.TotalInputs = 2
|
||||
fallback.PrimaryModel = "gradio-chat"
|
||||
fallback.Models = []string{"gradio-chat"}
|
||||
g.discoveries[cleanTarget] = fallback
|
||||
return fallback
|
||||
}
|
||||
@@ -1703,13 +1849,27 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
||||
var nonSystem []ChatMessage
|
||||
for _, m := range transformed {
|
||||
cStr := m.GetContentString()
|
||||
if m.Role == "system" && systemPromptStr == "" {
|
||||
systemPromptStr = cStr
|
||||
if m.Role == "system" {
|
||||
if systemPromptStr == "" {
|
||||
systemPromptStr = cStr
|
||||
} else {
|
||||
systemPromptStr += "\n\n" + cStr
|
||||
}
|
||||
} else {
|
||||
nonSystem = append(nonSystem, m)
|
||||
}
|
||||
}
|
||||
|
||||
// If the space has NO native system prompt input (disc.SystemIndex == -1),
|
||||
// but we have system instructions (from system message or tool instructions):
|
||||
if disc.SystemIndex == -1 && systemPromptStr != "" && len(nonSystem) > 0 {
|
||||
// If the space supports conversation history, prepend system instructions to the first turn
|
||||
if disc.HistoryIndex != -1 {
|
||||
nonSystem[0].Content = systemPromptStr + "\n\n" + nonSystem[0].GetContentString()
|
||||
systemPromptStr = ""
|
||||
}
|
||||
}
|
||||
|
||||
if len(nonSystem) > 0 {
|
||||
for i := 0; i < len(nonSystem)-1; i++ {
|
||||
m := nonSystem[i]
|
||||
@@ -1803,7 +1963,15 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s: %s\n\n", roleLabel, m.GetContentString()))
|
||||
}
|
||||
sb.WriteString(lastUserMessage)
|
||||
lastRoleLabel := "User"
|
||||
if len(nonSystem) > 0 && nonSystem[len(nonSystem)-1].Role == "assistant" {
|
||||
lastRoleLabel = "Assistant"
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%s: %s", lastRoleLabel, lastUserMessage))
|
||||
} else {
|
||||
sb.WriteString(lastUserMessage)
|
||||
}
|
||||
promptMessageText = sb.String()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user