162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
package basic
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// text exposes common string transformations: case folding, trimming, length,
|
|
// splitting, replacement and template placeholder substitution.
|
|
type text struct{}
|
|
|
|
// Name returns the tool identifier.
|
|
func (text) Name() string { return "text" }
|
|
|
|
// Description explains what the tool does for the model.
|
|
func (text) Description() string {
|
|
return "Transform or measure text: upper, lower, title, trim, length, split, replace and template placeholder substitution."
|
|
}
|
|
|
|
// Parameters returns the tool input JSON Schema. The operation-specific inputs
|
|
// (delimiter, old/new for replace, values for template) are validated by Execute
|
|
// because they only apply to their operation.
|
|
func (text) Parameters() (map[string]any, error) {
|
|
return map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"operation": map[string]any{
|
|
"type": "string",
|
|
"enum": []any{"upper", "lower", "title", "trim", "length", "split", "replace", "template"},
|
|
"description": "the transformation to apply",
|
|
},
|
|
"input": map[string]any{"type": "string", "description": "the input string"},
|
|
"delimiter": map[string]any{"type": "string", "description": "split delimiter"},
|
|
"old": map[string]any{"type": "string", "description": "substring to replace"},
|
|
"new": map[string]any{"type": "string", "description": "replacement for old"},
|
|
"values": map[string]any{"type": "object", "description": "template placeholder substitutions"},
|
|
},
|
|
"required": []any{"operation", "input"},
|
|
}, nil
|
|
}
|
|
|
|
// Execute applies the requested operation.
|
|
func (text) Execute(ctx context.Context, args any) (any, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
m, err := asMap(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
op, err := reqString(m, "operation")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
in, err := reqString(m, "input")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(op)) {
|
|
case "upper":
|
|
return map[string]any{"result": strings.ToUpper(in)}, nil
|
|
case "lower":
|
|
return map[string]any{"result": strings.ToLower(in)}, nil
|
|
case "title":
|
|
return map[string]any{"result": title(in)}, nil
|
|
case "trim":
|
|
return map[string]any{"result": strings.TrimSpace(in)}, nil
|
|
case "length":
|
|
return map[string]any{"result": utf8.RuneCountInString(in)}, nil
|
|
case "split":
|
|
delim, err := reqString(m, "delimiter")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts := strings.Split(in, delim)
|
|
out := make([]any, len(parts))
|
|
for i, p := range parts {
|
|
out[i] = p
|
|
}
|
|
return map[string]any{"result": out}, nil
|
|
case "replace":
|
|
old, err := reqString(m, "old")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repl, err := reqString(m, "new")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"result": strings.ReplaceAll(in, old, repl)}, nil
|
|
case "template":
|
|
vals, err := reqStringMap(m, "values")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := renderTemplate(in, vals)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"result": out}, nil
|
|
default:
|
|
return nil, fmt.Errorf("basic: unknown text operation %q: %w", op, ErrUnsupported)
|
|
}
|
|
}
|
|
|
|
// title returns the title case of s: the first letter of each whitespace-delimited
|
|
// word upper-cased and the rest lower-cased, preserving the original layout.
|
|
func title(s string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(s))
|
|
atStart := true
|
|
for _, r := range s {
|
|
if unicode.IsLetter(r) {
|
|
if atStart {
|
|
b.WriteRune(unicode.ToUpper(r))
|
|
} else {
|
|
b.WriteRune(unicode.ToLower(r))
|
|
}
|
|
atStart = false
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
if unicode.IsSpace(r) {
|
|
atStart = true
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// renderTemplate substitutes {key} placeholders in tpl using vals, leaving any
|
|
// placeholder whose key is absent unchanged. It scans once so a value that itself
|
|
// contains a placeholder is not re-substituted.
|
|
func renderTemplate(tpl string, vals map[string]string) (string, error) {
|
|
var b strings.Builder
|
|
b.Grow(len(tpl))
|
|
for i := 0; i < len(tpl); {
|
|
open := strings.IndexByte(tpl[i:], '{')
|
|
if open < 0 {
|
|
b.WriteString(tpl[i:])
|
|
break
|
|
}
|
|
b.WriteString(tpl[i : i+open])
|
|
close := strings.IndexByte(tpl[i+open:], '}')
|
|
if close < 0 {
|
|
return "", fmt.Errorf("basic: unbalanced placeholder in template: %w", ErrInvalidArgument)
|
|
}
|
|
start := i + open + 1
|
|
end := i + open + close
|
|
key := tpl[start:end]
|
|
if val, ok := vals[key]; ok {
|
|
b.WriteString(val)
|
|
} else {
|
|
b.WriteString(tpl[i+open : end+1])
|
|
}
|
|
i = end + 1
|
|
}
|
|
return b.String(), nil
|
|
}
|