- Introduced a new `workspace` field across various robot-related structures, including `CreateRobotRequest`, `UpdateRobotRequest`, and `RobotResponse`, allowing for better organization and management of robots within specific workspaces. - Updated database queries and response mappings to accommodate the new workspace field, ensuring seamless integration with existing functionalities. - Enhanced agent execution context to include workspace information, improving the contextual awareness of agents during operations. - Added tests to validate the creation and updating of robots with workspace data, ensuring robust functionality and backward compatibility.
721 lines
22 KiB
Go
721 lines
22 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"
|
|
)
|
|
|
|
// ValidatorConfig configures validation behavior (decoupled from RunConfig)
|
|
type ValidatorConfig struct {
|
|
// ValidationThreshold is the minimum score to pass validation (default: 0.6)
|
|
ValidationThreshold float64
|
|
}
|
|
|
|
// DefaultValidatorConfig returns the default validator configuration
|
|
func DefaultValidatorConfig() *ValidatorConfig {
|
|
return &ValidatorConfig{
|
|
ValidationThreshold: 0.6,
|
|
}
|
|
}
|
|
|
|
// 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 *ValidatorConfig
|
|
asserter *assert.Asserter
|
|
}
|
|
|
|
// NewValidator creates a new task validator
|
|
func NewValidator(ctx *robottypes.Context, robot *robottypes.Robot, config *ValidatorConfig) *Validator {
|
|
if config == nil {
|
|
config = DefaultValidatorConfig()
|
|
}
|
|
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 (without multi-turn context)
|
|
// Equivalent to ValidateWithContext(task, output, nil)
|
|
// Use ValidateWithContext when you have a CallResult for better multi-turn support
|
|
func (v *Validator) Validate(task *robottypes.Task, output interface{}) *robottypes.ValidationResult {
|
|
return v.ValidateWithContext(task, output, nil)
|
|
}
|
|
|
|
// ValidateWithContext validates task output and determines execution state for multi-turn conversation.
|
|
// It extends basic validation with:
|
|
// - Complete: whether expected result is obtained
|
|
// - NeedReply: whether to continue conversation
|
|
// - ReplyContent: content for next turn
|
|
//
|
|
// Parameters:
|
|
// - task: the task being executed
|
|
// - output: the output from assistant/mcp/process
|
|
// - callResult: the full call result (for detecting assistant's need for more info)
|
|
func (v *Validator) ValidateWithContext(task *robottypes.Task, output interface{}, callResult *CallResult) *robottypes.ValidationResult {
|
|
// If no validation rules and no expected output, return passed and complete
|
|
if task.ExpectedOutput == "" && len(task.ValidationRules) == 0 {
|
|
return &robottypes.ValidationResult{
|
|
Passed: true,
|
|
Score: 1.0,
|
|
Complete: v.hasValidOutput(output),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// Rule validation failed - check if we should retry with feedback
|
|
ruleResult.Complete = false
|
|
ruleResult.NeedReply, ruleResult.ReplyContent = v.checkNeedReplyOnFailure(task, ruleResult)
|
|
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)
|
|
}
|
|
|
|
// Determine execution state
|
|
result.Complete = v.isComplete(task, output, result)
|
|
result.NeedReply, result.ReplyContent = v.checkNeedReply(task, output, callResult, result)
|
|
|
|
return result
|
|
}
|
|
|
|
// hasValidOutput checks if output is non-empty and valid
|
|
func (v *Validator) hasValidOutput(output interface{}) bool {
|
|
if output == nil {
|
|
return false
|
|
}
|
|
switch o := output.(type) {
|
|
case string:
|
|
return strings.TrimSpace(o) != ""
|
|
case []interface{}:
|
|
return len(o) > 0
|
|
case map[string]interface{}:
|
|
return len(o) > 0
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// isComplete determines if the expected result has been obtained
|
|
func (v *Validator) isComplete(task *robottypes.Task, output interface{}, result *robottypes.ValidationResult) bool {
|
|
// If validation failed, not complete
|
|
if !result.Passed {
|
|
return false
|
|
}
|
|
|
|
// Must have valid output
|
|
if !v.hasValidOutput(output) {
|
|
return false
|
|
}
|
|
|
|
// If score is below threshold, consider incomplete
|
|
if result.Score < v.config.ValidationThreshold {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// checkNeedReply determines if conversation should continue and generates reply content
|
|
func (v *Validator) checkNeedReply(task *robottypes.Task, output interface{}, callResult *CallResult, result *robottypes.ValidationResult) (bool, string) {
|
|
// If already complete, no need to reply
|
|
if result.Complete {
|
|
return false, ""
|
|
}
|
|
|
|
// Scenario 1: Assistant explicitly asks for more information
|
|
if callResult != nil {
|
|
text := callResult.GetText()
|
|
if v.detectNeedMoreInfo(text) {
|
|
return true, v.generateClarificationReply(task, text)
|
|
}
|
|
}
|
|
|
|
// Scenario 2: Validation passed but output is incomplete/empty
|
|
if result.Passed && !v.hasValidOutput(output) {
|
|
return true, "Please continue and provide the complete result as specified in the task."
|
|
}
|
|
|
|
// Scenario 3: Validation failed with suggestions - can retry with feedback
|
|
if !result.Passed && len(result.Suggestions) > 0 {
|
|
return true, v.generateFeedbackReply(result)
|
|
}
|
|
|
|
// Scenario 4: Low confidence score - ask for improvement
|
|
if result.Passed && result.Score < v.config.ValidationThreshold {
|
|
return true, fmt.Sprintf("The result is partially correct (score: %.2f), but needs improvement. Please refine your response to better match the expected output: %s", result.Score, task.ExpectedOutput)
|
|
}
|
|
|
|
// No need to continue
|
|
return false, ""
|
|
}
|
|
|
|
// checkNeedReplyOnFailure handles the case when rule validation fails
|
|
func (v *Validator) checkNeedReplyOnFailure(task *robottypes.Task, result *robottypes.ValidationResult) (bool, string) {
|
|
// If there are suggestions, we can try to fix
|
|
if len(result.Suggestions) > 0 {
|
|
return true, v.generateFeedbackReply(result)
|
|
}
|
|
|
|
// If there are issues, provide feedback
|
|
if len(result.Issues) > 0 {
|
|
var sb strings.Builder
|
|
sb.WriteString("Your response did not pass validation. Please fix the following issues:\n\n")
|
|
for _, issue := range result.Issues {
|
|
sb.WriteString(fmt.Sprintf("- %s\n", issue))
|
|
}
|
|
sb.WriteString(fmt.Sprintf("\nExpected output: %s", task.ExpectedOutput))
|
|
return true, sb.String()
|
|
}
|
|
|
|
return false, ""
|
|
}
|
|
|
|
// detectNeedMoreInfo checks if assistant's response indicates need for more information
|
|
func (v *Validator) detectNeedMoreInfo(text string) bool {
|
|
if text == "" {
|
|
return false
|
|
}
|
|
|
|
textLower := strings.ToLower(text)
|
|
keywords := []string{
|
|
"need more information",
|
|
"please clarify",
|
|
"could you provide",
|
|
"can you specify",
|
|
"what is the",
|
|
"which one",
|
|
"please provide",
|
|
"i need to know",
|
|
"could you tell me",
|
|
"what do you mean",
|
|
}
|
|
|
|
for _, kw := range keywords {
|
|
if strings.Contains(textLower, kw) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check for question marks at the end (likely asking for clarification)
|
|
// Note: We require 2+ question marks to avoid false positives from rhetorical questions
|
|
// or questions that are part of the output (e.g., "How can I help you?")
|
|
// Single questions are often just conversational and don't need clarification
|
|
trimmed := strings.TrimSpace(text)
|
|
if strings.HasSuffix(trimmed, "?") {
|
|
if strings.Count(text, "?") >= 2 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// generateClarificationReply generates a reply when assistant asks for clarification
|
|
func (v *Validator) generateClarificationReply(task *robottypes.Task, assistantText string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("Please proceed with the task based on the available information.\n\n")
|
|
|
|
if task.ExpectedOutput != "" {
|
|
sb.WriteString(fmt.Sprintf("**Expected Output**: %s\n\n", task.ExpectedOutput))
|
|
}
|
|
|
|
sb.WriteString("If you need to make assumptions, please state them clearly and proceed with the most reasonable interpretation.")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// generateFeedbackReply generates a reply with validation feedback
|
|
func (v *Validator) generateFeedbackReply(result *robottypes.ValidationResult) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("## Validation Feedback\n\n")
|
|
sb.WriteString("Your previous response needs improvement. Please address the following:\n\n")
|
|
|
|
if len(result.Issues) > 0 {
|
|
sb.WriteString("### Issues\n")
|
|
for _, issue := range result.Issues {
|
|
sb.WriteString(fmt.Sprintf("- %s\n", issue))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
if len(result.Suggestions) > 0 {
|
|
sb.WriteString("### Suggestions\n")
|
|
for _, suggestion := range result.Suggestions {
|
|
sb.WriteString(fmt.Sprintf("- %s\n", suggestion))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("Please provide an improved response that addresses these points.")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// 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 (per-robot config > global Uses > empty)
|
|
validationAgentID := robottypes.ResolvePhaseAgent(v.robot.Config, "validation")
|
|
if validationAgentID == "" {
|
|
return &robottypes.ValidationResult{
|
|
Passed: false,
|
|
Score: 0,
|
|
Issues: []string{"no Validation Agent configured (set uses.validation in agent.yml or resources.phases in robot config)"},
|
|
}
|
|
}
|
|
|
|
// Build validation prompt
|
|
validationPrompt := v.BuildSemanticPrompt(task, output)
|
|
|
|
// Call validation agent
|
|
caller := NewAgentCaller()
|
|
caller.Connector = v.robot.LanguageModel
|
|
caller.Workspace = v.robot.Workspace
|
|
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
|
|
// Format matches the Validation Agent's expected input structure:
|
|
// 1. Task: task definition with expected_output and validation_rules
|
|
// 2. Result: actual output from task execution
|
|
// 3. Success Criteria: overall criteria (optional)
|
|
func (v *Validator) BuildSemanticPrompt(task *robottypes.Task, output interface{}) string {
|
|
var sb strings.Builder
|
|
|
|
// Section 1: Task (matches Agent's expected "Task" input)
|
|
sb.WriteString("## Task\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 (instructions)
|
|
if len(task.Messages) > 0 {
|
|
sb.WriteString("**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))
|
|
}
|
|
|
|
// Validation rules
|
|
semanticRules := v.getSemanticRules(task.ValidationRules)
|
|
if len(semanticRules) > 0 {
|
|
sb.WriteString("**validation_rules**:\n")
|
|
for _, rule := range semanticRules {
|
|
sb.WriteString(fmt.Sprintf("- %s\n", rule))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Section 2: Result (matches Agent's expected "Result" input)
|
|
sb.WriteString("## Result\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")
|
|
}
|
|
|
|
// Section 3: Success Criteria (optional, from goals if available)
|
|
// Note: This could be extended to include criteria from exec.Goals if needed
|
|
sb.WriteString("\n## Success Criteria\n\n")
|
|
if task.ExpectedOutput != "" {
|
|
sb.WriteString(fmt.Sprintf("The task should produce: %s\n", task.ExpectedOutput))
|
|
} else {
|
|
sb.WriteString("Complete the task successfully with valid output.\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()
|
|
caller.Connector = av.v.robot.LanguageModel
|
|
caller.Workspace = av.v.robot.Workspace
|
|
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
|
|
}
|
|
}
|