Files
go-llm/v2/internal/schema/schema.go
Steve Dudenhoeffer a4cb4baab5 Add go-llm v2: redesigned API for simpler LLM abstraction
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>
2026-02-07 20:00:08 -05:00

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
}