filename:
assistant/agent/human_tool_list.go
branch:
main
back to repo
package agent
import (
"encoding/json"
"fmt"
"sort"
"strings"
"unicode/utf8"
"assistant/tools"
)
const (
maxToolDescRunes = 160
maxPropDescRunes = 72
)
// formatToolListHuman renders tools as compact Markdown for the chat UI.
func formatToolListHuman(reg tools.Registry) string {
if len(reg) == 0 {
return "No tools are registered."
}
names := make([]string, 0, len(reg))
for n := range reg {
names = append(names, n)
}
sort.Strings(names)
var b strings.Builder
for _, n := range names {
b.WriteString(formatOneToolMarkdown(reg[n]))
b.WriteString("\n\n")
}
b.WriteString(toolListFooterMarkdown())
return strings.TrimSpace(b.String())
}
func toolListFooterMarkdown() string {
return strings.Join([]string{
"### How to run",
"- `/tool <name> key=value …` - separate pairs with spaces",
"- Quote text: `content=\"likes dark mode\"`",
"- Numbers/booleans unquoted: `limit=20` `action=create`",
"- Optional JSON: `/tool calendar_list_range {\"from\":\"2025-01-01\",\"to\":\"2025-02-01\"}`",
}, "\n")
}
func formatOneToolMarkdown(t tools.Tool) string {
var b strings.Builder
b.WriteString("### ")
b.WriteString(t.Name)
b.WriteString("\n\n")
desc := shortenPlain(cleanForMarkdown(strings.TrimSpace(t.Description)), maxToolDescRunes)
if desc != "" {
b.WriteString("*")
b.WriteString(desc)
b.WriteString("*\n\n")
}
lines, example := schemaToMarkdownLines(t.Parameters, t.Name)
for _, line := range lines {
b.WriteString("- ")
b.WriteString(line)
b.WriteString("\n")
}
b.WriteString("- Example: `")
b.WriteString(example)
b.WriteString("`")
return b.String()
}
func schemaToMarkdownLines(params json.RawMessage, toolName string) (lines []string, example string) {
if len(params) == 0 {
return []string{"*No arguments.*"}, fmt.Sprintf("/tool %s", toolName)
}
var root map[string]interface{}
if err := json.Unmarshal(params, &root); err != nil {
return []string{fmt.Sprintf("Schema error: %v", err)}, fmt.Sprintf("/tool %s", toolName)
}
props, _ := root["properties"].(map[string]interface{})
if len(props) == 0 {
return []string{"*No arguments.*"}, fmt.Sprintf("/tool %s", toolName)
}
reqSet := map[string]struct{}{}
if req, ok := root["required"].([]interface{}); ok {
for _, r := range req {
if s, ok := r.(string); ok {
reqSet[s] = struct{}{}
}
}
}
names := make([]string, 0, len(props))
for k := range props {
names = append(names, k)
}
sort.Strings(names)
for _, key := range names {
p, _ := props[key].(map[string]interface{})
lines = append(lines, describePropMarkdown(key, p, reqSet))
}
ex := buildExampleLine(toolName, names, props, reqSet)
return lines, ex
}
func describePropMarkdown(name string, p map[string]interface{}, req map[string]struct{}) string {
_, required := req[name]
reqLabel := "opt"
if required {
reqLabel = "req"
}
typ := typeString(p)
desc := ""
if d, ok := p["description"].(string); ok {
desc = shortenPlain(cleanForMarkdown(strings.TrimSpace(d)), maxPropDescRunes)
}
var sb strings.Builder
sb.WriteString("`")
sb.WriteString(name)
sb.WriteString("` ")
sb.WriteString(typ)
sb.WriteString(" · ")
sb.WriteString(reqLabel)
if desc != "" {
sb.WriteString(" - ")
sb.WriteString(desc)
}
if raw, ok := p["enum"]; ok {
sb.WriteString(" · ")
sb.WriteString(shortEnum(raw))
}
return sb.String()
}
func shortEnum(raw interface{}) string {
switch v := raw.(type) {
case []interface{}:
parts := make([]string, 0, len(v))
for _, x := range v {
parts = append(parts, fmt.Sprint(x))
}
s := strings.Join(parts, ", ")
if utf8.RuneCountInString(s) > 48 {
return "enum: " + shortenPlain(s, 45)
}
return "enum: " + s
default:
return ""
}
}
func cleanForMarkdown(s string) string {
s = strings.ReplaceAll(s, "\n", " ")
s = strings.ReplaceAll(s, "\r", "")
s = strings.ReplaceAll(s, "*", "")
s = strings.ReplaceAll(s, "_", " ")
for strings.Contains(s, " ") {
s = strings.ReplaceAll(s, " ", " ")
}
return strings.TrimSpace(s)
}
// shortenPlain trims to max runes, preferring end of first sentence.
func shortenPlain(s string, max int) string {
if max <= 0 || s == "" {
return s
}
if utf8.RuneCountInString(s) <= max {
return s
}
for _, sep := range []string{". ", "? ", "! ", "; "} {
if idx := strings.Index(s, sep); idx != -1 && idx < max+20 {
frag := s[:idx+1]
if utf8.RuneCountInString(frag) <= max {
return frag
}
}
}
rs := []rune(s)
if len(rs) <= max {
return s
}
cut := max - 1
for cut > 0 && cut < len(rs) && rs[cut] != ' ' {
cut--
}
if cut < max/2 {
cut = max - 1
}
return string(rs[:cut]) + "…"
}
func typeString(p map[string]interface{}) string {
t, ok := p["type"].(string)
if !ok {
return "any"
}
return t
}
func buildExampleLine(toolName string, keys []string, props map[string]interface{}, req map[string]struct{}) string {
var parts []string
parts = append(parts, "/tool", toolName)
for _, k := range keys {
if _, isReq := req[k]; !isReq {
continue
}
p, _ := props[k].(map[string]interface{})
parts = append(parts, examplePair(k, p))
}
if len(parts) == 2 {
for _, k := range keys {
if _, isReq := req[k]; isReq {
continue
}
p, _ := props[k].(map[string]interface{})
parts = append(parts, examplePair(k, p))
break
}
}
return strings.Join(parts, " ")
}
func examplePair(key string, p map[string]interface{}) string {
if enums, ok := p["enum"].([]interface{}); ok && len(enums) > 0 {
if s, ok := enums[0].(string); ok {
return fmt.Sprintf("%s=%s", key, s)
}
}
switch typeString(p) {
case "integer", "number":
return fmt.Sprintf("%s=1", key)
case "boolean":
return fmt.Sprintf("%s=true", key)
default:
if key == "content" || key == "query" || key == "title" || key == "notes" {
return fmt.Sprintf(`%s="…"`, key)
}
if key == "from" || key == "due_at" {
return fmt.Sprintf("%s=2025-01-01", key)
}
if key == "to" {
return "to=2025-02-01"
}
if key == "url" {
return `url="https://…"`
}
return fmt.Sprintf(`%s="…"`, key)
}
}