feat(tools/basic): 内置基础工具(calculator(注入安全)/datetime/random/text/json/markdown)+ 批量注册
This commit is contained in:
1 parent
b3c61aa1ff
commit
08223c06a5
19 files changed
+2074
No files matched your search
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user