v2 is a new Go module (v2/) with a dramatically simpler API: - Unified Message type (no more Input marker interface) - Define[T] for ergonomic tool creation with standard context.Context - Chat session with automatic tool-call loop (agent loop) - Streaming via pull-based StreamReader - MCP one-call connect (MCPStdioServer, MCPHTTPServer, MCPSSEServer) - Middleware support (logging, retry, timeout, usage tracking) - Decoupled JSON Schema (map[string]any, no provider coupling) - Sample tools: WebSearch, Browser, Exec, ReadFile, WriteFile, HTTP - Providers: OpenAI, Anthropic, Google (all with streaming) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
4.5 KiB
Go
189 lines
4.5 KiB
Go
// Package schema provides JSON Schema generation from Go structs.
|
|
// It produces standard JSON Schema as map[string]any, with no provider-specific types.
|
|
package schema
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// FromStruct generates a JSON Schema object from a Go struct.
|
|
// Struct tags used:
|
|
// - `json:"name"` — sets the property name (standard Go JSON convention)
|
|
// - `description:"..."` — sets the property description
|
|
// - `enum:"a,b,c"` — restricts string values to the given set
|
|
//
|
|
// Pointer fields are treated as optional; non-pointer fields are required.
|
|
// Anonymous (embedded) struct fields are flattened into the parent.
|
|
func FromStruct(v any) map[string]any {
|
|
t := reflect.TypeOf(v)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
panic("schema.FromStruct expects a struct or pointer to struct")
|
|
}
|
|
return objectSchema(t)
|
|
}
|
|
|
|
func objectSchema(t reflect.Type) map[string]any {
|
|
properties := map[string]any{}
|
|
var required []string
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
|
|
// Skip unexported fields
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
// Flatten anonymous (embedded) structs
|
|
if field.Anonymous {
|
|
ft := field.Type
|
|
if ft.Kind() == reflect.Ptr {
|
|
ft = ft.Elem()
|
|
}
|
|
if ft.Kind() == reflect.Struct {
|
|
embedded := objectSchema(ft)
|
|
if props, ok := embedded["properties"].(map[string]any); ok {
|
|
for k, v := range props {
|
|
properties[k] = v
|
|
}
|
|
}
|
|
if req, ok := embedded["required"].([]string); ok {
|
|
required = append(required, req...)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
name := fieldName(field)
|
|
isRequired := true
|
|
ft := field.Type
|
|
|
|
if ft.Kind() == reflect.Ptr {
|
|
ft = ft.Elem()
|
|
isRequired = false
|
|
}
|
|
|
|
prop := fieldSchema(field, ft)
|
|
properties[name] = prop
|
|
|
|
if isRequired {
|
|
required = append(required, name)
|
|
}
|
|
}
|
|
|
|
result := map[string]any{
|
|
"type": "object",
|
|
"properties": properties,
|
|
}
|
|
if len(required) > 0 {
|
|
result["required"] = required
|
|
}
|
|
return result
|
|
}
|
|
|
|
func fieldSchema(field reflect.StructField, ft reflect.Type) map[string]any {
|
|
prop := map[string]any{}
|
|
|
|
// Check for enum tag first
|
|
if enumTag, ok := field.Tag.Lookup("enum"); ok {
|
|
vals := parseEnum(enumTag)
|
|
prop["type"] = "string"
|
|
prop["enum"] = vals
|
|
if desc, ok := field.Tag.Lookup("description"); ok {
|
|
prop["description"] = desc
|
|
}
|
|
return prop
|
|
}
|
|
|
|
switch ft.Kind() {
|
|
case reflect.String:
|
|
prop["type"] = "string"
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
prop["type"] = "integer"
|
|
case reflect.Float32, reflect.Float64:
|
|
prop["type"] = "number"
|
|
case reflect.Bool:
|
|
prop["type"] = "boolean"
|
|
case reflect.Struct:
|
|
return objectSchema(ft)
|
|
case reflect.Slice:
|
|
prop["type"] = "array"
|
|
elemType := ft.Elem()
|
|
if elemType.Kind() == reflect.Ptr {
|
|
elemType = elemType.Elem()
|
|
}
|
|
prop["items"] = typeSchema(elemType)
|
|
case reflect.Map:
|
|
prop["type"] = "object"
|
|
if ft.Key().Kind() == reflect.String {
|
|
valType := ft.Elem()
|
|
if valType.Kind() == reflect.Ptr {
|
|
valType = valType.Elem()
|
|
}
|
|
prop["additionalProperties"] = typeSchema(valType)
|
|
}
|
|
default:
|
|
prop["type"] = "string" // fallback
|
|
}
|
|
|
|
if desc, ok := field.Tag.Lookup("description"); ok {
|
|
prop["description"] = desc
|
|
}
|
|
|
|
return prop
|
|
}
|
|
|
|
func typeSchema(t reflect.Type) map[string]any {
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
return map[string]any{"type": "string"}
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return map[string]any{"type": "integer"}
|
|
case reflect.Float32, reflect.Float64:
|
|
return map[string]any{"type": "number"}
|
|
case reflect.Bool:
|
|
return map[string]any{"type": "boolean"}
|
|
case reflect.Struct:
|
|
return objectSchema(t)
|
|
case reflect.Slice:
|
|
elemType := t.Elem()
|
|
if elemType.Kind() == reflect.Ptr {
|
|
elemType = elemType.Elem()
|
|
}
|
|
return map[string]any{
|
|
"type": "array",
|
|
"items": typeSchema(elemType),
|
|
}
|
|
default:
|
|
return map[string]any{"type": "string"}
|
|
}
|
|
}
|
|
|
|
func fieldName(f reflect.StructField) string {
|
|
if tag, ok := f.Tag.Lookup("json"); ok {
|
|
parts := strings.SplitN(tag, ",", 2)
|
|
if parts[0] != "" && parts[0] != "-" {
|
|
return parts[0]
|
|
}
|
|
}
|
|
return f.Name
|
|
}
|
|
|
|
func parseEnum(tag string) []string {
|
|
parts := strings.Split(tag, ",")
|
|
var vals []string
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
vals = append(vals, p)
|
|
}
|
|
}
|
|
return vals
|
|
}
|