- 流式输出: SSE 逐 token 接收, \\n\n\ 段落缓冲后 mdprint 彩色渲染 - 日志系统: charmbracelet/log v2 双写(stderr + log.yml), yunshu log 命令 - 会议室架构: dialog(main) + weather/profile/note(sub) 多 Agent 编排 - 泛型工具注册: NewTool[T] 反射推导 JSON Schema, 类型安全 - 安全加固: safeMemoryPath 三段校验(EvalSymlinks+Rel), maxToolCalls=2 - 性能优化: sync.Once 延迟加载, note 一步完成, obs/summary 合并 - Prompt 适配: 流式输出原则(先调工具不说话), 单 Agent 查询跳过 obs+summary - 文档: AGENTS.md + architecture.md + changelog.md 全部同步至 v2.3.0
134 lines
2.8 KiB
Go
134 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
func structToSchema(t reflect.Type) Schema {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
return typeToSchema(t)
|
|
}
|
|
|
|
schema := Schema{
|
|
"type": "object",
|
|
"properties": map[string]any{},
|
|
}
|
|
|
|
properties := schema["properties"].(map[string]any)
|
|
var required []any
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
|
|
if !f.IsExported() {
|
|
continue
|
|
}
|
|
|
|
jsonTag := f.Tag.Get("json")
|
|
if jsonTag == "-" {
|
|
continue
|
|
}
|
|
|
|
name := strings.Split(jsonTag, ",")[0]
|
|
if name == "" {
|
|
name = strings.ToLower(f.Name[:1]) + f.Name[1:]
|
|
}
|
|
|
|
if !strings.Contains(jsonTag, "omitempty") {
|
|
required = append(required, name)
|
|
}
|
|
|
|
fieldSchema := typeToSchema(f.Type)
|
|
|
|
if desc := f.Tag.Get("description"); desc != "" {
|
|
fieldSchema["description"] = desc
|
|
}
|
|
|
|
if enum := f.Tag.Get("enum"); enum != "" {
|
|
vals := strings.Split(enum, ",")
|
|
enumVals := make([]any, len(vals))
|
|
for i, v := range vals {
|
|
enumVals[i] = strings.TrimSpace(v)
|
|
}
|
|
fieldSchema["enum"] = enumVals
|
|
}
|
|
|
|
properties[name] = fieldSchema
|
|
}
|
|
|
|
if len(required) > 0 {
|
|
schema["required"] = required
|
|
}
|
|
|
|
return schema
|
|
}
|
|
|
|
func typeToSchema(t reflect.Type) Schema {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
return Schema{"type": "string"}
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return Schema{"type": "integer"}
|
|
case reflect.Float32, reflect.Float64:
|
|
return Schema{"type": "number"}
|
|
case reflect.Bool:
|
|
return Schema{"type": "boolean"}
|
|
case reflect.Slice, reflect.Array:
|
|
items := typeToSchema(t.Elem())
|
|
return Schema{"type": "array", "items": items}
|
|
case reflect.Interface:
|
|
return Schema{}
|
|
case reflect.Map:
|
|
m := Schema{"type": "object"}
|
|
if t.Elem().Kind() != reflect.Interface {
|
|
m["additionalProperties"] = typeToSchema(t.Elem())
|
|
}
|
|
return m
|
|
case reflect.Struct:
|
|
return structToSchema(t)
|
|
default:
|
|
return Schema{"type": "string"}
|
|
}
|
|
}
|
|
|
|
func NewTool[T any](name, description string, fn func(T) (string, error)) *ToolDef {
|
|
var zero T
|
|
t := reflect.TypeOf(zero)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
panic(fmt.Sprintf("NewTool: %T 不是结构体类型", zero))
|
|
}
|
|
|
|
schema := structToSchema(t)
|
|
|
|
return &ToolDef{
|
|
Name: name,
|
|
Description: description,
|
|
Parameters: schema,
|
|
Execute: func(args map[string]any) (string, error) {
|
|
data, err := json.Marshal(args)
|
|
if err != nil {
|
|
return "", fmt.Errorf("序列化参数失败: %w", err)
|
|
}
|
|
var typed T
|
|
if err := json.Unmarshal(data, &typed); err != nil {
|
|
return "", fmt.Errorf("参数解析失败: %w", err)
|
|
}
|
|
return fn(typed)
|
|
},
|
|
}
|
|
}
|