filename:
assistant/agent/human_tool_parse.go
branch:
main
back to repo
package agent
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"unicode"
)
// ParseHumanToolInvocation parses the text after "/tool " into a tool name and JSON arguments.
// Supported forms:
// <name> → {}
// <name> { ... } → JSON object (legacy / advanced)
// <name> key=value key2="a b" → human key=value (values quoted if they contain spaces)
func ParseHumanToolInvocation(rest string) (name string, argsJSON string, err error) {
rest = strings.TrimSpace(rest)
if rest == "" {
return "", "", fmt.Errorf("missing tool name")
}
i := 0
for i < len(rest) && (unicode.IsLetter(rune(rest[i])) || unicode.IsDigit(rune(rest[i])) || rest[i] == '_') {
i++
}
if i == 0 {
return "", "", fmt.Errorf("missing tool name (use letters, digits, underscores)")
}
name = rest[:i]
after := strings.TrimSpace(rest[i:])
if after == "" {
return name, "{}", nil
}
if after[0] == '{' {
if !json.Valid([]byte(after)) {
return "", "", fmt.Errorf("invalid JSON for tool arguments")
}
return name, after, nil
}
m, err := parseHumanKeyValues(after)
if err != nil {
return "", "", err
}
b, err := json.Marshal(m)
if err != nil {
return "", "", err
}
return name, string(b), nil
}
func parseHumanKeyValues(s string) (map[string]interface{}, error) {
s = strings.TrimSpace(s)
if s == "" {
return map[string]interface{}{}, nil
}
r := kvScanner{s: s, i: 0}
out := map[string]interface{}{}
for r.i < len(r.s) {
r.skipSpace()
if r.i >= len(r.s) {
break
}
key := r.readIdent()
if key == "" {
return nil, fmt.Errorf("expected argument name (letters, digits, underscore) near %q", r.rest())
}
r.skipSpace()
if r.peek() != '=' {
return nil, fmt.Errorf("expected = after %q", key)
}
r.i++
r.skipSpace()
raw, err := r.readValue()
if err != nil {
return nil, err
}
out[key] = coerceJSONScalar(raw)
}
return out, nil
}
type kvScanner struct {
s string
i int
}
func (r *kvScanner) rest() string {
if r.i >= len(r.s) {
return ""
}
return r.s[r.i:]
}
func (r *kvScanner) peek() byte {
if r.i >= len(r.s) {
return 0
}
return r.s[r.i]
}
func (r *kvScanner) skipSpace() {
for r.i < len(r.s) && (r.s[r.i] == ' ' || r.s[r.i] == '\t' || r.s[r.i] == '\n' || r.s[r.i] == '\r') {
r.i++
}
}
func (r *kvScanner) readIdent() string {
start := r.i
for r.i < len(r.s) && (unicode.IsLetter(rune(r.s[r.i])) || unicode.IsDigit(rune(r.s[r.i])) || r.s[r.i] == '_') {
r.i++
}
return r.s[start:r.i]
}
func (r *kvScanner) readValue() (string, error) {
if r.i >= len(r.s) {
return "", nil
}
switch r.s[r.i] {
case '"':
return r.readDoubleQuoted()
case '\'':
return r.readSingleQuoted()
default:
return r.readBare(), nil
}
}
func (r *kvScanner) readDoubleQuoted() (string, error) {
r.i++ // opening "
var b strings.Builder
for r.i < len(r.s) {
c := r.s[r.i]
if c == '\\' && r.i+1 < len(r.s) {
r.i++
b.WriteByte(r.s[r.i])
r.i++
continue
}
if c == '"' {
r.i++
return b.String(), nil
}
b.WriteByte(c)
r.i++
}
return "", fmt.Errorf("unclosed \" in value")
}
func (r *kvScanner) readSingleQuoted() (string, error) {
r.i++ // opening '
var b strings.Builder
for r.i < len(r.s) {
c := r.s[r.i]
if c == '\'' {
r.i++
return b.String(), nil
}
b.WriteByte(c)
r.i++
}
return "", fmt.Errorf("unclosed ' in value")
}
func (r *kvScanner) readBare() string {
start := r.i
for r.i < len(r.s) && !unicode.IsSpace(rune(r.s[r.i])) {
r.i++
}
return r.s[start:r.i]
}
func coerceJSONScalar(s string) interface{} {
if s == "" {
return ""
}
if s == "true" {
return true
}
if s == "false" {
return false
}
if s == "null" {
return nil
}
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
return n
}
if f, err := strconv.ParseFloat(s, 64); err == nil {
return f
}
return s
}