yao/agent/robot/executor/standard/validator.go
Max 0c9bdb8000 Implement P3 Run Phase with Enhanced Validation and Execution Logic
- Completed the implementation of the P3 Run phase, integrating task execution and validation mechanisms.
- Introduced a new `RunConfig` struct to manage execution parameters such as retries and validation thresholds.
- Developed a two-layer validation system using the new `yao/assert` package, supporting both natural language and structured JSON rules.
- Enhanced the `RunExecution` method to execute tasks sequentially with progress tracking and a retry mechanism for validation failures.
- Updated task structures to include comprehensive validation rules and expected outputs, ensuring robust task management.
- Added unit tests for the new execution and validation features, achieving high test coverage across the implementation.
- Revised documentation to reflect changes in the architecture and functionality of the P3 phase.
2026-01-17 12:22:23 +08:00

495 lines
14 KiB
Go

package standard
import (
"encoding/json"
"fmt"
"strings"
"github.com/yaoapp/gou/process"
robottypes "github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/assert"
)
// Validator handles task result validation using a two-layer approach:
// 1. Rule-based validation: Uses yao/assert for deterministic rules (type, contains, regex, json_path)
// 2. Semantic validation: Calls Validation Agent for semantic understanding (ExpectedOutput)
type Validator struct {
ctx *robottypes.Context
robot *robottypes.Robot
config *RunConfig
asserter *assert.Asserter
}
// NewValidator creates a new task validator
func NewValidator(ctx *robottypes.Context, robot *robottypes.Robot, config *RunConfig) *Validator {
v := &Validator{
ctx: ctx,
robot: robot,
config: config,
asserter: assert.New(),
}
// Configure asserter with robot-specific implementations
v.asserter.WithAgentValidator(&robotAgentValidator{v: v})
v.asserter.WithScriptRunner(&robotScriptRunner{ctx: ctx})
return v
}
// Validate validates task output using two-layer validation:
// 1. First, run rule-based assertions (fast, deterministic)
// 2. Then, if ExpectedOutput is set, run semantic validation via Agent
func (v *Validator) Validate(task *robottypes.Task, output interface{}) *robottypes.ValidationResult {
// If no validation rules and no expected output, return passed
if task.ExpectedOutput == "" && len(task.ValidationRules) == 0 {
return &robottypes.ValidationResult{
Passed: true,
Score: 1.0,
}
}
result := &robottypes.ValidationResult{
Passed: true,
Score: 1.0,
}
// Layer 1: Rule-based validation (using yao/assert)
if len(task.ValidationRules) > 0 {
ruleResult := v.validateRules(task.ValidationRules, output)
if !ruleResult.Passed {
return ruleResult
}
// Merge rule validation results
result.Issues = append(result.Issues, ruleResult.Issues...)
result.Suggestions = append(result.Suggestions, ruleResult.Suggestions...)
}
// Layer 2: Semantic validation (using Validation Agent)
// Only run if ExpectedOutput is set or there are agent-type rules
if task.ExpectedOutput != "" || v.hasAgentRules(task.ValidationRules) {
semanticResult := v.validateSemantic(task, output)
result = v.mergeResults(result, semanticResult)
}
return result
}
// validateRules validates output against rule-based assertions
func (v *Validator) validateRules(rules []string, output interface{}) *robottypes.ValidationResult {
result := &robottypes.ValidationResult{
Passed: true,
Score: 1.0,
}
// Parse rules into assertions
assertions := v.parseRules(rules)
if len(assertions) == 0 {
return result
}
// Run assertions
passed, message := v.asserter.Validate(assertions, output)
if !passed {
result.Passed = false
result.Score = 0
result.Issues = append(result.Issues, message)
}
return result
}
// parseRules converts validation rules (strings or JSON) to assertions
// Supports:
// - Simple string rules: "output must be valid JSON" (converted to type check)
// - JSON assertion objects: {"type": "contains", "value": "success"}
func (v *Validator) parseRules(rules []string) []*assert.Assertion {
var assertions []*assert.Assertion
for _, rule := range rules {
// Try to parse as JSON assertion
if strings.HasPrefix(rule, "{") {
var assertionMap map[string]interface{}
if err := json.Unmarshal([]byte(rule), &assertionMap); err == nil {
parsed := assert.ParseAssertions(assertionMap)
assertions = append(assertions, parsed...)
continue
}
}
// Convert common string rules to assertions
assertion := v.convertStringRule(rule)
if assertion != nil {
assertions = append(assertions, assertion)
}
}
return assertions
}
// convertStringRule converts a human-readable rule string to an assertion
// Examples:
// - "output must be valid JSON" -> {"type": "type", "value": "object"}
// - "must contain 'success'" -> {"type": "contains", "value": "success"}
// - "count > 0" -> (passed to semantic validation)
func (v *Validator) convertStringRule(rule string) *assert.Assertion {
ruleLower := strings.ToLower(rule)
// JSON type check
if strings.Contains(ruleLower, "valid json") || strings.Contains(ruleLower, "json object") {
return &assert.Assertion{
Type: "type",
Value: "object",
Message: rule,
}
}
// Array type check
if strings.Contains(ruleLower, "json array") || strings.Contains(ruleLower, "must be array") {
return &assert.Assertion{
Type: "type",
Value: "array",
Message: rule,
}
}
// Contains check
if strings.Contains(ruleLower, "contain") {
// Extract the value in quotes
if start := strings.Index(rule, "'"); start != -1 {
if end := strings.Index(rule[start+1:], "'"); end != -1 {
value := rule[start+1 : start+1+end]
return &assert.Assertion{
Type: "contains",
Value: value,
Message: rule,
}
}
}
if start := strings.Index(rule, "\""); start != -1 {
if end := strings.Index(rule[start+1:], "\""); end != -1 {
value := rule[start+1 : start+1+end]
return &assert.Assertion{
Type: "contains",
Value: value,
Message: rule,
}
}
}
}
// Not empty check - use regex to match at least one character
if strings.Contains(ruleLower, "not empty") || strings.Contains(ruleLower, "non-empty") {
return &assert.Assertion{
Type: "regex",
Value: ".+",
Message: rule,
}
}
// For other rules, return nil (will be handled by semantic validation)
return nil
}
// hasAgentRules checks if any rule requires agent-based validation
func (v *Validator) hasAgentRules(rules []string) bool {
for _, rule := range rules {
if strings.HasPrefix(rule, "{") {
var assertionMap map[string]interface{}
if err := json.Unmarshal([]byte(rule), &assertionMap); err == nil {
if assertionMap["type"] == "agent" {
return true
}
}
}
}
return false
}
// validateSemantic performs semantic validation using the Validation Agent
func (v *Validator) validateSemantic(task *robottypes.Task, output interface{}) *robottypes.ValidationResult {
// Get validation agent ID
validationAgentID := "__yao.validation" // default
if v.robot.Config != nil && v.robot.Config.Resources != nil {
if customID, ok := v.robot.Config.Resources.Phases["validation"]; ok && customID != "" {
validationAgentID = customID
}
}
// Build validation prompt
validationPrompt := v.BuildSemanticPrompt(task, output)
// Call validation agent
caller := NewAgentCaller()
result, err := caller.CallWithMessages(v.ctx, validationAgentID, validationPrompt)
if err != nil {
return &robottypes.ValidationResult{
Passed: false,
Score: 0,
Issues: []string{fmt.Sprintf("Validation agent error: %s", err.Error())},
}
}
return v.ParseAgentResult(result)
}
// BuildSemanticPrompt builds the prompt for semantic validation
func (v *Validator) BuildSemanticPrompt(task *robottypes.Task, output interface{}) string {
var sb strings.Builder
sb.WriteString("## Task Definition\n\n")
sb.WriteString(fmt.Sprintf("**Task ID**: %s\n", task.ID))
sb.WriteString(fmt.Sprintf("**Executor**: %s (%s)\n\n", task.ExecutorID, task.ExecutorType))
// Task description
if len(task.Messages) > 0 {
sb.WriteString("**Task Instructions**:\n")
for _, msg := range task.Messages {
if content, ok := msg.Content.(string); ok {
sb.WriteString(content + "\n")
}
}
sb.WriteString("\n")
}
// Expected output (primary criterion for semantic validation)
if task.ExpectedOutput != "" {
sb.WriteString(fmt.Sprintf("**Expected Output**: %s\n\n", task.ExpectedOutput))
}
// Semantic validation rules (rules that couldn't be converted to assertions)
semanticRules := v.getSemanticRules(task.ValidationRules)
if len(semanticRules) > 0 {
sb.WriteString("**Validation Criteria**:\n")
for _, rule := range semanticRules {
sb.WriteString(fmt.Sprintf("- %s\n", rule))
}
sb.WriteString("\n")
}
// Actual output
sb.WriteString("## Actual Output\n\n")
if output != nil {
outputJSON, err := json.MarshalIndent(output, "", " ")
if err == nil {
sb.WriteString(fmt.Sprintf("```json\n%s\n```\n", string(outputJSON)))
} else {
sb.WriteString(fmt.Sprintf("%v\n", output))
}
} else {
sb.WriteString("(no output)\n")
}
sb.WriteString("\n## Validation Request\n\n")
sb.WriteString("Please validate the actual output against the expected output and validation criteria. ")
sb.WriteString("Focus on semantic correctness and completeness. ")
sb.WriteString("Return a JSON object with: passed (bool), score (0-1), issues (array), suggestions (array), details (markdown report).\n")
return sb.String()
}
// getSemanticRules returns rules that need semantic validation (not convertible to assertions)
func (v *Validator) getSemanticRules(rules []string) []string {
var semanticRules []string
for _, rule := range rules {
// Skip JSON assertions (already handled)
if strings.HasPrefix(rule, "{") {
continue
}
// Skip rules that were converted to assertions
if v.convertStringRule(rule) == nil {
semanticRules = append(semanticRules, rule)
}
}
return semanticRules
}
// ParseAgentResult parses the validation agent's response
func (v *Validator) ParseAgentResult(result *CallResult) *robottypes.ValidationResult {
validation := &robottypes.ValidationResult{
Passed: false,
Score: 0,
}
// Try to parse as JSON
data, err := result.GetJSON()
if err != nil {
// If not JSON, try to interpret the text response
text := result.GetText()
if text != "" {
validation.Details = text
// Simple heuristic: check for positive keywords
textLower := strings.ToLower(text)
positiveKeywords := []string{"passed", "valid", "correct", "success"}
for _, keyword := range positiveKeywords {
if strings.Contains(textLower, keyword) {
validation.Passed = true
validation.Score = 0.8
break
}
}
}
return validation
}
// Parse JSON fields
if passed, ok := data["passed"].(bool); ok {
validation.Passed = passed
}
if score, ok := data["score"].(float64); ok {
validation.Score = score
}
if issues, ok := data["issues"].([]interface{}); ok {
for _, issue := range issues {
if s, ok := issue.(string); ok {
validation.Issues = append(validation.Issues, s)
}
}
}
if suggestions, ok := data["suggestions"].([]interface{}); ok {
for _, suggestion := range suggestions {
if s, ok := suggestion.(string); ok {
validation.Suggestions = append(validation.Suggestions, s)
}
}
}
if details, ok := data["details"].(string); ok {
validation.Details = details
}
return validation
}
// mergeResults merges rule-based and semantic validation results
func (v *Validator) mergeResults(ruleResult, semanticResult *robottypes.ValidationResult) *robottypes.ValidationResult {
// If either failed, the overall result is failed
if !ruleResult.Passed || !semanticResult.Passed {
return &robottypes.ValidationResult{
Passed: false,
Score: min(ruleResult.Score, semanticResult.Score),
Issues: append(ruleResult.Issues, semanticResult.Issues...),
Suggestions: append(ruleResult.Suggestions, semanticResult.Suggestions...),
Details: semanticResult.Details,
}
}
// Both passed
return &robottypes.ValidationResult{
Passed: true,
Score: (ruleResult.Score + semanticResult.Score) / 2,
Issues: append(ruleResult.Issues, semanticResult.Issues...),
Suggestions: append(ruleResult.Suggestions, semanticResult.Suggestions...),
Details: semanticResult.Details,
}
}
// ============================================================================
// Robot-specific implementations of assert interfaces
// ============================================================================
// robotAgentValidator implements assert.AgentValidator for robot package
type robotAgentValidator struct {
v *Validator
}
// Validate validates output using an agent
func (av *robotAgentValidator) Validate(agentID string, output, input, criteria interface{}, options *assert.AssertionOptions) *assert.Result {
result := &assert.Result{}
// Build validation request
validationInput := map[string]interface{}{
"output": output,
"input": input,
}
if criteria != nil {
validationInput["criteria"] = criteria
}
inputJSON, err := json.Marshal(validationInput)
if err != nil {
result.Passed = false
result.Message = fmt.Sprintf("failed to marshal validation input: %s", err.Error())
return result
}
// Call agent
caller := NewAgentCaller()
callResult, err := caller.CallWithMessages(av.v.ctx, agentID, string(inputJSON))
if err != nil {
result.Passed = false
result.Message = fmt.Sprintf("agent validation error: %s", err.Error())
return result
}
// Parse response
data, err := callResult.GetJSON()
if err != nil {
result.Passed = false
result.Message = "agent returned invalid response format"
return result
}
if passed, ok := data["passed"].(bool); ok {
result.Passed = passed
}
if reason, ok := data["reason"].(string); ok {
result.Message = reason
}
result.Expected = data
return result
}
// robotScriptRunner implements assert.ScriptRunner for robot package
type robotScriptRunner struct {
ctx *robottypes.Context
}
// Run runs an assertion script using Yao process
func (r *robotScriptRunner) Run(scriptName string, output, input, expected interface{}) (bool, string, error) {
// Build script arguments
args := []interface{}{output, input, expected}
// Create and run the process
proc, err := process.Of(scriptName, args...)
if err != nil {
return false, "", fmt.Errorf("failed to create process: %w", err)
}
// Set context for timeout and cancellation support
if r.ctx != nil {
proc.Context = r.ctx.Context
}
if err := proc.Execute(); err != nil {
return false, "", fmt.Errorf("script execution failed: %w", err)
}
defer proc.Release()
// Parse result - expected format: bool or { "pass": bool, "message": string }
res := proc.Value()
switch v := res.(type) {
case bool:
if v {
return true, "script assertion passed", nil
}
return false, "script assertion failed", nil
case map[string]interface{}:
passed := false
message := ""
if pass, ok := v["pass"].(bool); ok {
passed = pass
}
if msg, ok := v["message"].(string); ok {
message = msg
}
return passed, message, nil
default:
return false, fmt.Sprintf("script returned unexpected type: %T", res), nil
}
}