247 lines
5.9 KiB
Go
247 lines
5.9 KiB
Go
package openai
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func scanXMLTextToolCallBlocks(content string) ([]xmlTextToolCallBlock, bool) {
|
|
if !strings.Contains(strings.ToLower(content), textToolCallOpeningHint) {
|
|
return nil, false
|
|
}
|
|
foundAny := false
|
|
var blocks []xmlTextToolCallBlock
|
|
for pos := 0; pos < len(content); {
|
|
openStart, openEnd, ok := findXMLOpenTag(content, pos, len(content), "tool_call")
|
|
if !ok {
|
|
break
|
|
}
|
|
foundAny = true
|
|
closeStart, closeEnd, ok := findXMLCloseTag(content, openEnd, len(content), "tool_call")
|
|
if !ok {
|
|
blocks = append(blocks, xmlTextToolCallBlock{
|
|
start: openStart,
|
|
end: len(content),
|
|
err: fmt.Errorf("malformed tool call: unclosed tool_call block"),
|
|
})
|
|
break
|
|
}
|
|
block := parseXMLTextToolCallBlock(content, openStart, openEnd, closeStart, closeEnd)
|
|
blocks = append(blocks, block)
|
|
pos = closeEnd
|
|
}
|
|
return blocks, foundAny
|
|
}
|
|
|
|
func parseXMLTextToolCallBlock(content string, blockStart, bodyStart, bodyEnd, blockEnd int) xmlTextToolCallBlock {
|
|
_, fnOpenEnd, name, ok := findXMLNamedOpenTag(content, bodyStart, bodyEnd, "function")
|
|
if !ok {
|
|
return xmlTextToolCallBlock{
|
|
start: blockStart,
|
|
end: blockEnd,
|
|
err: fmt.Errorf("malformed tool call: missing function block or definition inside tool_call"),
|
|
}
|
|
}
|
|
fnCloseStart, _, ok := findXMLCloseTag(content, fnOpenEnd, bodyEnd, "function")
|
|
if !ok {
|
|
return xmlTextToolCallBlock{
|
|
start: blockStart,
|
|
end: blockEnd,
|
|
err: fmt.Errorf("malformed tool call: missing function block or definition inside tool_call"),
|
|
}
|
|
}
|
|
params := map[string]string{}
|
|
for pos := fnOpenEnd; pos < fnCloseStart; {
|
|
paramStart, paramOpenEnd, key, ok := findXMLNamedOpenTag(content, pos, fnCloseStart, "parameter")
|
|
if !ok {
|
|
break
|
|
}
|
|
paramCloseStart, paramCloseEnd, ok := findXMLCloseTag(content, paramOpenEnd, fnCloseStart, "parameter")
|
|
if !ok {
|
|
break
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
if key != "" {
|
|
params[key] = content[paramOpenEnd:paramCloseStart]
|
|
}
|
|
pos = paramCloseEnd
|
|
if pos <= paramStart {
|
|
break
|
|
}
|
|
}
|
|
return xmlTextToolCallBlock{
|
|
start: blockStart,
|
|
end: blockEnd,
|
|
name: strings.TrimSpace(name),
|
|
params: params,
|
|
err: nil,
|
|
}
|
|
}
|
|
|
|
func findXMLOpenTag(content string, start, limit int, tag string) (int, int, bool) {
|
|
for pos := start; pos < limit; {
|
|
idx := strings.IndexByte(content[pos:limit], '<')
|
|
if idx < 0 {
|
|
return 0, 0, false
|
|
}
|
|
tagStart := pos + idx
|
|
next := skipASCIISpaces(content, tagStart+1)
|
|
if next < limit && content[next] == '/' {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
if !matchASCIIFoldAt(content, next, tag) {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
afterTag := next + len(tag)
|
|
if afterTag < limit && isTextToolIdentifierChar(content[afterTag], false) {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
closeIdx := strings.IndexByte(content[afterTag:limit], '>')
|
|
if closeIdx < 0 {
|
|
return 0, 0, false
|
|
}
|
|
return tagStart, afterTag + closeIdx + 1, true
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func findXMLNamedOpenTag(content string, start, limit int, tag string) (int, int, string, bool) {
|
|
for pos := start; pos < limit; {
|
|
idx := strings.IndexByte(content[pos:limit], '<')
|
|
if idx < 0 {
|
|
return 0, 0, "", false
|
|
}
|
|
tagStart := pos + idx
|
|
next := skipASCIISpaces(content, tagStart+1)
|
|
if next < limit && content[next] == '/' {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
if !matchASCIIFoldAt(content, next, tag) {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
cursor := skipASCIISpaces(content, next+len(tag))
|
|
if cursor >= limit || content[cursor] != '=' {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
cursor = skipASCIISpaces(content, cursor+1)
|
|
nameStart := cursor
|
|
for cursor < limit && isTextToolIdentifierChar(content[cursor], cursor == nameStart) {
|
|
cursor++
|
|
}
|
|
if cursor == nameStart {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
name := content[nameStart:cursor]
|
|
cursor = skipASCIISpaces(content, cursor)
|
|
if cursor >= limit || content[cursor] != '>' {
|
|
pos = tagStart + 1
|
|
continue
|
|
}
|
|
return tagStart, cursor + 1, name, true
|
|
}
|
|
return 0, 0, "", false
|
|
}
|
|
|
|
func findXMLCloseTag(content string, start, limit int, tag string) (int, int, bool) {
|
|
for pos := start; pos < limit; {
|
|
idx := strings.Index(content[pos:limit], "</")
|
|
if idx < 0 {
|
|
return 0, 0, false
|
|
}
|
|
tagStart := pos + idx
|
|
cursor := skipASCIISpaces(content, tagStart+2)
|
|
if !matchASCIIFoldAt(content, cursor, tag) {
|
|
pos = tagStart + 2
|
|
continue
|
|
}
|
|
cursor = skipASCIISpaces(content, cursor+len(tag))
|
|
if cursor < limit && content[cursor] == '=' {
|
|
cursor = skipASCIISpaces(content, cursor+1)
|
|
nameStart := cursor
|
|
for cursor < limit && isTextToolIdentifierChar(content[cursor], cursor == nameStart) {
|
|
cursor++
|
|
}
|
|
if cursor == nameStart {
|
|
pos = tagStart + 2
|
|
continue
|
|
}
|
|
cursor = skipASCIISpaces(content, cursor)
|
|
}
|
|
if cursor >= limit || content[cursor] != '>' {
|
|
pos = tagStart + 2
|
|
continue
|
|
}
|
|
return tagStart, cursor + 1, true
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func matchASCIIFoldAt(s string, pos int, want string) bool {
|
|
if pos < 0 || pos+len(want) > len(s) {
|
|
return false
|
|
}
|
|
for i := 0; i < len(want); i++ {
|
|
a := s[pos+i]
|
|
b := want[i]
|
|
if a >= 'A' && a <= 'Z' {
|
|
a += 'a' - 'A'
|
|
}
|
|
if b >= 'A' && b <= 'Z' {
|
|
b += 'a' - 'A'
|
|
}
|
|
if a != b {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func findMustacheToolCallEnd(content string, argsStart int) (int, int, bool) {
|
|
depth := 0
|
|
var quote byte
|
|
escaped := false
|
|
for i := argsStart; i < len(content); i++ {
|
|
c := content[i]
|
|
if quote != 0 {
|
|
if escaped {
|
|
escaped = false
|
|
continue
|
|
}
|
|
if c == '\\' {
|
|
escaped = true
|
|
continue
|
|
}
|
|
if c == quote {
|
|
quote = 0
|
|
}
|
|
continue
|
|
}
|
|
switch c {
|
|
case '\'', '"':
|
|
quote = c
|
|
case '[', '{', '(':
|
|
depth++
|
|
case ']', '}':
|
|
if depth > 0 {
|
|
depth--
|
|
}
|
|
case ')':
|
|
if depth > 0 {
|
|
depth--
|
|
continue
|
|
}
|
|
closeStart := skipASCIISpaces(content, i+1)
|
|
if closeStart+1 < len(content) && content[closeStart] == '}' && content[closeStart+1] == '}' {
|
|
return i, closeStart + 2, true
|
|
}
|
|
}
|
|
}
|
|
return 0, 0, false
|
|
}
|