433 lines
7.8 KiB
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
|
|
}
|