iop/apps/edge/internal/openai/text_tool_markup.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
}