iop/apps/edge/internal/openai/text_tool_literals.go

433 lines
7.8 KiB
Go

package openai
import (
"encoding/json"
"strconv"
"strings"
)
func parseMustacheToolCallArguments(raw string) (map[string]any, bool) {
if args, ok := parseMustacheToolCallArgumentsStrict(raw); ok {
return args, true
}
// Some Cline/provider outputs include one extra "}" before the closing
// template parens, e.g. `...True}]})}}`. Recover only after strict parsing.
trimmed := strings.TrimSpace(raw)
if strings.HasSuffix(trimmed, "}") {
return parseMustacheToolCallArgumentsStrict(strings.TrimSpace(trimmed[:len(trimmed)-1]))
}
return nil, false
}
func parseMustacheToolCallArgumentsStrict(raw string) (map[string]any, bool) {
trimmed := strings.TrimSpace(raw)
args := map[string]any{}
if trimmed == "" {
return args, true
}
for _, part := range splitTopLevel(trimmed, ',') {
part = strings.TrimSpace(part)
if part == "" {
continue
}
eq := indexTopLevel(part, '=')
if eq < 0 {
return nil, false
}
key := strings.TrimSpace(part[:eq])
if !isTextToolIdentifier(key) {
return nil, false
}
value, ok := parseTextToolLiteral(part[eq+1:])
if !ok {
return nil, false
}
args[key] = value
}
return args, true
}
func parseTextToolLiteral(raw string) (any, bool) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "", true
}
var decoded any
if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil {
return decoded, true
}
parser := pythonishLiteralParser{s: trimmed}
value, ok := parser.parseValue()
if !ok {
return nil, false
}
parser.skipSpaces()
if parser.pos != len(parser.s) {
return nil, false
}
return value, true
}
func splitTopLevel(s string, sep byte) []string {
parts := []string{}
start := 0
depth := 0
var quote byte
escaped := false
for i := 0; i < len(s); i++ {
c := s[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 sep:
if depth == 0 {
parts = append(parts, s[start:i])
start = i + 1
}
}
}
parts = append(parts, s[start:])
return parts
}
func indexTopLevel(s string, target byte) int {
depth := 0
var quote byte
escaped := false
for i := 0; i < len(s); i++ {
c := s[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--
}
default:
if c == target && depth == 0 {
return i
}
}
}
return -1
}
func skipASCIISpaces(s string, pos int) int {
for pos < len(s) {
switch s[pos] {
case ' ', '\n', '\r', '\t':
pos++
default:
return pos
}
}
return pos
}
type pythonishLiteralParser struct {
s string
pos int
}
func (p *pythonishLiteralParser) parseValue() (any, bool) {
p.skipSpaces()
if p.pos >= len(p.s) {
return nil, false
}
switch p.s[p.pos] {
case '\'', '"':
return p.parseString()
case '[':
return p.parseArray()
case '{':
return p.parseObject()
}
if p.consumeWord("True") || p.consumeWord("true") {
return true, true
}
if p.consumeWord("False") || p.consumeWord("false") {
return false, true
}
if p.consumeWord("None") || p.consumeWord("null") {
return nil, true
}
return p.parseNumberOrBare()
}
func (p *pythonishLiteralParser) parseString() (string, bool) {
if p.pos >= len(p.s) {
return "", false
}
quote := p.s[p.pos]
p.pos++
var b strings.Builder
for p.pos < len(p.s) {
c := p.s[p.pos]
p.pos++
if c == quote {
return b.String(), true
}
if c != '\\' {
b.WriteByte(c)
continue
}
if p.pos >= len(p.s) {
return "", false
}
esc := p.s[p.pos]
p.pos++
switch esc {
case '\\', '\'', '"':
b.WriteByte(esc)
case 'n':
b.WriteByte('\n')
case 'r':
b.WriteByte('\r')
case 't':
b.WriteByte('\t')
case 'b':
b.WriteByte('\b')
case 'f':
b.WriteByte('\f')
case 'u':
if p.pos+4 > len(p.s) {
return "", false
}
r, err := strconv.ParseInt(p.s[p.pos:p.pos+4], 16, 32)
if err != nil {
return "", false
}
b.WriteRune(rune(r))
p.pos += 4
default:
b.WriteByte(esc)
}
}
return "", false
}
func (p *pythonishLiteralParser) parseArray() ([]any, bool) {
p.pos++
items := []any{}
p.skipSpaces()
if p.consumeByte(']') {
return items, true
}
for {
value, ok := p.parseValue()
if !ok {
return nil, false
}
items = append(items, value)
p.skipSpaces()
if p.consumeByte(']') {
return items, true
}
if !p.consumeByte(',') {
return nil, false
}
p.skipSpaces()
if p.consumeByte(']') {
return items, true
}
}
}
func (p *pythonishLiteralParser) parseObject() (map[string]any, bool) {
p.pos++
obj := map[string]any{}
p.skipSpaces()
if p.consumeByte('}') {
return obj, true
}
for {
key, ok := p.parseObjectKey()
if !ok {
return nil, false
}
p.skipSpaces()
if !p.consumeByte(':') {
return nil, false
}
value, ok := p.parseValue()
if !ok {
return nil, false
}
obj[key] = value
p.skipSpaces()
if p.consumeByte('}') {
return obj, true
}
if !p.consumeByte(',') {
return nil, false
}
p.skipSpaces()
if p.consumeByte('}') {
return obj, true
}
}
}
func (p *pythonishLiteralParser) parseObjectKey() (string, bool) {
p.skipSpaces()
if p.pos >= len(p.s) {
return "", false
}
if p.s[p.pos] == '\'' || p.s[p.pos] == '"' {
return p.parseString()
}
start := p.pos
for p.pos < len(p.s) && isTextToolIdentifierChar(p.s[p.pos], p.pos == start) {
p.pos++
}
if start == p.pos {
return "", false
}
return p.s[start:p.pos], true
}
func (p *pythonishLiteralParser) parseNumberOrBare() (any, bool) {
start := p.pos
for p.pos < len(p.s) && !isLiteralDelimiter(p.s[p.pos]) {
p.pos++
}
if start == p.pos {
return nil, false
}
token := p.s[start:p.pos]
if value, ok := parseNumberToken(token); ok {
return value, true
}
if isTextToolIdentifier(token) {
return token, true
}
return nil, false
}
func (p *pythonishLiteralParser) skipSpaces() {
for p.pos < len(p.s) {
switch p.s[p.pos] {
case ' ', '\n', '\r', '\t':
p.pos++
default:
return
}
}
}
func (p *pythonishLiteralParser) consumeByte(b byte) bool {
if p.pos < len(p.s) && p.s[p.pos] == b {
p.pos++
return true
}
return false
}
func (p *pythonishLiteralParser) consumeWord(word string) bool {
if !strings.HasPrefix(p.s[p.pos:], word) {
return false
}
end := p.pos + len(word)
if end < len(p.s) && isBareLiteralChar(p.s[end]) {
return false
}
p.pos = end
return true
}
func parseNumberToken(token string) (any, bool) {
if token == "" {
return nil, false
}
if strings.ContainsAny(token, ".eE") {
value, err := strconv.ParseFloat(token, 64)
if err != nil {
return nil, false
}
return value, true
}
value, err := strconv.ParseInt(token, 10, 64)
if err != nil {
return nil, false
}
return value, true
}
func isTextToolIdentifier(s string) bool {
if s == "" {
return false
}
for i := 0; i < len(s); i++ {
if !isTextToolIdentifierChar(s[i], i == 0) {
return false
}
}
return true
}
func isTextToolIdentifierChar(c byte, first bool) bool {
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' {
return true
}
if !first && (c >= '0' && c <= '9' || c == '.' || c == ':' || c == '-') {
return true
}
return false
}
func isBareLiteralChar(c byte) bool {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_'
}
func isLiteralDelimiter(c byte) bool {
switch c {
case ',', ']', '}', ' ', '\n', '\r', '\t':
return true
default:
return false
}
}
func mapKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}