use anyhow::{Context, Result};
use async_trait::async_trait;
use std::sync::Arc;
use crate::providers::{ChatRequest, ContentBlock, Message, MessageContent, Provider};
use crate::workflow::context::WorkflowContext;
use crate::workflow::def::NodeDef;
use crate::workflow::rule_engine::{Rule, RuleEngine, ValidationResult};
use crate::workflow::template::TemplateRenderer;
use super::node_executor::NodeExecutor;
#[derive(Debug, Clone)]
pub struct ValidateExecutorConfig {
pub enable_ai_validation: bool,
pub ai_validation_prompt: String,
pub abort_on_ai_failure: bool,
}
impl Default for ValidateExecutorConfig {
fn default() -> Self {
Self {
enable_ai_validation: false,
ai_validation_prompt: String::new(),
abort_on_ai_failure: true,
}
}
}
pub struct ValidateExecutor {
provider: Option<Arc<dyn Provider>>,
config: ValidateExecutorConfig,
template_renderer: TemplateRenderer,
}
impl ValidateExecutor {
pub fn new() -> Self {
Self {
provider: None,
config: ValidateExecutorConfig::default(),
template_renderer: TemplateRenderer::new(),
}
}
pub fn with_ai(provider: Arc<dyn Provider>, config: ValidateExecutorConfig) -> Self {
Self {
provider: Some(provider),
config,
template_renderer: TemplateRenderer::new(),
}
}
async fn validate_with_ai(
&self,
data: &serde_json::Value,
context: &WorkflowContext,
) -> Result<ValidationResult> {
if let Some(provider) = &self.provider {
let prompt = if self.config.ai_validation_prompt.is_empty() {
format!(
"Please validate the following data and return a JSON object with 'passed' (boolean) and 'errors' (array of strings):\n{}",
serde_json::to_string_pretty(data)?
)
} else {
self.template_renderer.render(&self.config.ai_validation_prompt, &context.variables)?
};
let messages = vec![Message {
role: crate::providers::Role::User,
content: MessageContent::Text(prompt),
}];
let request = ChatRequest {
messages,
tools: Vec::new(),
system: Some("You are a data validator. Return JSON with 'passed' and 'errors' fields.".to_string()),
think: false,
max_tokens: 1024,
server_tools: Vec::new(),
enable_caching: false,
};
let response = provider.chat(request).await?;
for block in &response.content {
if let ContentBlock::Text { text } = block
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(text) {
let passed = json.get("passed")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let errors = json.get("errors")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect())
.unwrap_or_default();
return Ok(ValidationResult {
passed,
errors,
});
}
}
Ok(ValidationResult::failure("Failed to parse AI validation response".to_string()))
} else {
Ok(ValidationResult::success())
}
}
}
impl Default for ValidateExecutor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl NodeExecutor for ValidateExecutor {
async fn execute(
&self,
node: &NodeDef,
context: &mut WorkflowContext,
) -> Result<serde_json::Value> {
let rules_json = node.params.get("rules")
.ok_or_else(|| anyhow::anyhow!("Validate executor requires 'rules' parameter"))?;
let rules: Vec<Rule> = serde_json::from_value(rules_json.clone())
.with_context(|| "Failed to parse validation rules")?;
let mut rule_engine = RuleEngine::new();
let mut result = ValidationResult::success();
for rule in &rules {
result = result.merge(rule_engine.validate(rule, &context.variables)?);
}
if result.passed && self.config.enable_ai_validation && self.provider.is_some() {
let context_vars: serde_json::Map<String, serde_json::Value> = context
.variables
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let data_to_validate = node.params.get("data")
.cloned()
.unwrap_or(serde_json::Value::Object(context_vars));
let ai_result = self.validate_with_ai(&data_to_validate, context).await?;
result = result.merge(ai_result);
}
let output = serde_json::json!({
"passed": result.passed,
"errors": result.errors,
"node_id": node.id,
});
if !result.passed && self.config.abort_on_ai_failure {
return Err(anyhow::anyhow!("Validation failed: {}", result.errors.join("; ")));
}
Ok(output)
}
fn name(&self) -> &str {
"validate_executor"
}
}