Files
Cyrene_v2/plugins/tools/basic/text.go
T

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
}