-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidator.go
More file actions
178 lines (157 loc) · 5.76 KB
/
Copy pathvalidator.go
File metadata and controls
178 lines (157 loc) · 5.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package agentic
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
// structValidator is a package-level validator instance (goroutine-safe).
// It uses JSON field names in error messages so the LLM sees the same names
// it used when generating the output.
var structValidator *validator.Validate
func init() {
structValidator = validator.New()
// Use json tag names in error messages so they match what the LLM sees.
structValidator.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" || name == "" {
return fld.Name
}
return name
})
}
// ValidateStruct validates a struct using `validate` struct tags.
// Returns a *ValidationError with human-readable messages if validation fails,
// or nil if validation passes (or the value has no validate tags).
//
// This is used automatically by ToolOutputSpec when parsing structured output,
// but can also be called directly.
func ValidateStruct(v any) error {
if v == nil {
return nil
}
t := reflect.TypeOf(v)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil
}
err := structValidator.Struct(v)
if err == nil {
return nil
}
var validationErrors validator.ValidationErrors
if !errors.As(err, &validationErrors) {
return err
}
return &ValidationError{
Message: formatValidationErrors(validationErrors),
}
}
// formatValidationErrors converts validator.ValidationErrors into a clear,
// actionable message for the LLM.
func formatValidationErrors(errs validator.ValidationErrors) string {
messages := make([]string, 0, len(errs))
for _, fe := range errs {
messages = append(messages, formatFieldError(fe))
}
return "Validation failed:\n" + strings.Join(messages, "\n")
}
// formatFieldError produces a human-readable message for a single field error.
func formatFieldError(fe validator.FieldError) string {
field := fe.Field()
switch fe.Tag() {
case "required":
return fmt.Sprintf("- %s is required", field)
case "min":
if fe.Kind() == reflect.String {
return fmt.Sprintf("- %s must be at least %s characters long", field, fe.Param())
}
return fmt.Sprintf("- %s must be at least %s", field, fe.Param())
case "max":
if fe.Kind() == reflect.String {
return fmt.Sprintf("- %s must be at most %s characters long", field, fe.Param())
}
return fmt.Sprintf("- %s must be at most %s", field, fe.Param())
case "gt":
return fmt.Sprintf("- %s must be greater than %s", field, fe.Param())
case "gte":
return fmt.Sprintf("- %s must be greater than or equal to %s", field, fe.Param())
case "lt":
return fmt.Sprintf("- %s must be less than %s", field, fe.Param())
case "lte":
return fmt.Sprintf("- %s must be less than or equal to %s", field, fe.Param())
case "oneof":
return fmt.Sprintf("- %s must be one of [%s]", field, fe.Param())
case "len":
return fmt.Sprintf("- %s must have exactly %s elements", field, fe.Param())
case "email":
return fmt.Sprintf("- %s must be a valid email address", field)
case "url":
return fmt.Sprintf("- %s must be a valid URL", field)
case "contains":
return fmt.Sprintf("- %s must contain '%s'", field, fe.Param())
default:
return fmt.Sprintf("- %s failed '%s' validation", field, fe.Tag())
}
}
// OutputValidator validates agent text output and can request retries.
// When validation fails, the error message is sent back to the model
// and the agent re-enters the loop (up to maxValidationRetries).
//
// For structured output, prefer using `validate` struct tags instead —
// they are applied automatically. Use OutputValidator for custom logic
// on the raw text response.
type OutputValidator[DepsT any] interface {
Validate(ctx RunContext[DepsT], output string) error
}
// OutputValidatorFunc is a function adapter for OutputValidator.
//
// Example:
//
// agentic.WithOutputValidatorFunc[NoDeps](func(ctx agentic.RunContext[NoDeps], output string) error {
// if !strings.Contains(output, "ANSWER:") {
// return agentic.NewValidationError("Response must contain 'ANSWER:'")
// }
// return nil
// })
type OutputValidatorFunc[DepsT any] func(ctx RunContext[DepsT], output string) error
// Validate implements OutputValidator.
func (f OutputValidatorFunc[DepsT]) Validate(ctx RunContext[DepsT], output string) error {
return f(ctx, output)
}
// TypedOutputValidator validates typed structured output.
// Use this for programmatic validation that goes beyond what struct tags can express.
type TypedOutputValidator[DepsT any, OutputT any] interface {
ValidateTyped(ctx RunContext[DepsT], output OutputT) error
}
// TypedOutputValidatorFunc is a function adapter for TypedOutputValidator.
type TypedOutputValidatorFunc[DepsT any, OutputT any] func(ctx RunContext[DepsT], output OutputT) error
// ValidateTyped implements TypedOutputValidator.
func (f TypedOutputValidatorFunc[DepsT, OutputT]) ValidateTyped(ctx RunContext[DepsT], output OutputT) error {
return f(ctx, output)
}
// ValidationError signals output validation failed and the model should retry.
// Return this from a validator to have the agent send the message back to the
// model and request a new response.
type ValidationError struct {
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
// NewValidationError creates a ValidationError with the given message.
func NewValidationError(msg string) *ValidationError {
return &ValidationError{Message: msg}
}
// NewValidationErrorf creates a ValidationError with a formatted message.
func NewValidationErrorf(format string, args ...interface{}) *ValidationError {
return &ValidationError{Message: fmt.Sprintf(format, args...)}
}
// IsValidationError checks if an error is a ValidationError.
func IsValidationError(err error) bool {
_, ok := err.(*ValidationError)
return ok
}