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>
This commit is contained in:
188
v2/internal/schema/schema.go
Normal file
188
v2/internal/schema/schema.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user