use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfidentStep {
pub action: Value,
pub confidence: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub alternatives: Vec<Alternative>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification: Option<Verification>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl ConfidentStep {
pub fn new(action: Value, confidence: f64) -> Self {
Self {
action,
confidence: confidence.clamp(0.0, 1.0),
alternatives: Vec::new(),
verification: None,
description: None,
}
}
pub fn with_alternative(mut self, alt: Alternative) -> Self {
self.alternatives.push(alt);
self
}
pub fn with_alternatives(mut self, alts: Vec<Alternative>) -> Self {
self.alternatives.extend(alts);
self
}
pub fn with_verification(mut self, verification: Verification) -> Self {
self.verification = Some(verification);
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn is_confident(&self, threshold: f64) -> bool {
self.confidence >= threshold
}
pub fn has_alternatives(&self) -> bool {
!self.alternatives.is_empty()
}
pub fn best_alternative(&self) -> Option<&Alternative> {
self.alternatives.first()
}
pub fn sorted_alternatives(&self) -> Vec<&Alternative> {
let mut alts: Vec<_> = self.alternatives.iter().collect();
alts.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
alts
}
pub fn from_json(value: &Value) -> Option<Self> {
let action = value.get("action")?.clone();
let confidence = value
.get("confidence")
.and_then(|v| v.as_f64())
.unwrap_or(0.5);
let alternatives = value
.get("alternatives")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(Alternative::from_json).collect())
.unwrap_or_default();
let verification = value.get("verification").and_then(Verification::from_json);
let description = value
.get("description")
.and_then(|v| v.as_str())
.map(String::from);
Some(Self {
action,
confidence: confidence.clamp(0.0, 1.0),
alternatives,
verification,
description,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alternative {
pub action: Value,
pub confidence: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Alternative {
pub fn new(action: Value, confidence: f64) -> Self {
Self {
action,
confidence: confidence.clamp(0.0, 1.0),
description: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn from_json(value: &Value) -> Option<Self> {
let action = value.get("action")?.clone();
let confidence = value
.get("confidence")
.and_then(|v| v.as_f64())
.unwrap_or(0.3);
let description = value
.get("description")
.and_then(|v| v.as_str())
.map(String::from);
Some(Self {
action,
confidence: confidence.clamp(0.0, 1.0),
description,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Verification {
pub verification_type: VerificationType,
pub expected: String,
#[serde(default = "default_true")]
pub retry_on_failure: bool,
}
fn default_true() -> bool {
true
}
impl Verification {
pub fn url_contains(pattern: impl Into<String>) -> Self {
Self {
verification_type: VerificationType::UrlContains,
expected: pattern.into(),
retry_on_failure: true,
}
}
pub fn element_exists(selector: impl Into<String>) -> Self {
Self {
verification_type: VerificationType::ElementExists,
expected: selector.into(),
retry_on_failure: true,
}
}
pub fn text_contains(text: impl Into<String>) -> Self {
Self {
verification_type: VerificationType::TextContains,
expected: text.into(),
retry_on_failure: true,
}
}
pub fn js_condition(condition: impl Into<String>) -> Self {
Self {
verification_type: VerificationType::JsCondition,
expected: condition.into(),
retry_on_failure: true,
}
}
pub fn from_json(value: &Value) -> Option<Self> {
let type_str = value.get("type").and_then(|v| v.as_str())?;
let expected = value.get("expected").and_then(|v| v.as_str())?.to_string();
let retry_on_failure = value
.get("retry_on_failure")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let verification_type = match type_str {
"url_contains" => VerificationType::UrlContains,
"element_exists" => VerificationType::ElementExists,
"text_contains" => VerificationType::TextContains,
"js_condition" => VerificationType::JsCondition,
_ => return None,
};
Some(Self {
verification_type,
expected,
retry_on_failure,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerificationType {
UrlContains,
ElementExists,
TextContains,
JsCondition,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConfidenceRetryStrategy {
pub confidence_threshold: f64,
pub use_alternatives: bool,
pub max_alternatives: usize,
pub adaptive_threshold: bool,
pub threshold_decay: f64,
}
impl Default for ConfidenceRetryStrategy {
fn default() -> Self {
Self {
confidence_threshold: 0.7,
use_alternatives: true,
max_alternatives: 3,
adaptive_threshold: true,
threshold_decay: 0.1,
}
}
}
impl ConfidenceRetryStrategy {
pub fn new(threshold: f64) -> Self {
Self {
confidence_threshold: threshold.clamp(0.0, 1.0),
..Default::default()
}
}
pub fn high() -> Self {
Self::new(0.8)
}
pub fn medium() -> Self {
Self::new(0.6)
}
pub fn low() -> Self {
Self::new(0.4)
}
pub fn with_alternatives(mut self, use_alts: bool) -> Self {
self.use_alternatives = use_alts;
self
}
pub fn with_max_alternatives(mut self, max: usize) -> Self {
self.max_alternatives = max;
self
}
pub fn with_adaptive(mut self, adaptive: bool) -> Self {
self.adaptive_threshold = adaptive;
self
}
pub fn threshold_for_attempt(&self, attempt: usize) -> f64 {
if self.adaptive_threshold {
let decay = self.threshold_decay * attempt as f64;
(self.confidence_threshold - decay).max(0.2)
} else {
self.confidence_threshold
}
}
pub fn should_accept(&self, step: &ConfidentStep, attempt: usize) -> bool {
step.confidence >= self.threshold_for_attempt(attempt)
}
pub fn should_try_alternatives(&self, step: &ConfidentStep, attempt: usize) -> bool {
self.use_alternatives && step.has_alternatives() && attempt < self.max_alternatives
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConfidenceTracker {
step_confidences: Vec<(String, f64)>,
sum: f64,
min: f64,
max: f64,
low_confidence_count: usize,
low_threshold: f64,
}
impl ConfidenceTracker {
pub fn new() -> Self {
Self {
step_confidences: Vec::new(),
sum: 0.0,
min: 1.0,
max: 0.0,
low_confidence_count: 0,
low_threshold: 0.5,
}
}
pub fn with_low_threshold(threshold: f64) -> Self {
Self {
low_threshold: threshold.clamp(0.0, 1.0),
..Self::new()
}
}
pub fn record(&mut self, step_id: impl Into<String>, confidence: f64) {
let conf = confidence.clamp(0.0, 1.0);
self.step_confidences.push((step_id.into(), conf));
self.sum += conf;
self.min = self.min.min(conf);
self.max = self.max.max(conf);
if conf < self.low_threshold {
self.low_confidence_count += 1;
}
}
pub fn record_step(&mut self, step_id: impl Into<String>, step: &ConfidentStep) {
self.record(step_id, step.confidence);
}
pub fn average(&self) -> f64 {
if self.step_confidences.is_empty() {
0.0
} else {
self.sum / self.step_confidences.len() as f64
}
}
pub fn min(&self) -> f64 {
if self.step_confidences.is_empty() {
0.0
} else {
self.min
}
}
pub fn max(&self) -> f64 {
self.max
}
pub fn count(&self) -> usize {
self.step_confidences.len()
}
pub fn low_confidence_count(&self) -> usize {
self.low_confidence_count
}
pub fn low_confidence_ratio(&self) -> f64 {
if self.step_confidences.is_empty() {
0.0
} else {
self.low_confidence_count as f64 / self.step_confidences.len() as f64
}
}
pub fn confidences(&self) -> &[(String, f64)] {
&self.step_confidences
}
pub fn is_healthy(&self) -> bool {
self.average() >= 0.6 && self.low_confidence_ratio() < 0.3
}
pub fn summary(&self) -> ConfidenceSummary {
ConfidenceSummary {
count: self.count(),
average: self.average(),
min: self.min(),
max: self.max(),
low_confidence_count: self.low_confidence_count,
low_confidence_ratio: self.low_confidence_ratio(),
is_healthy: self.is_healthy(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfidenceSummary {
pub count: usize,
pub average: f64,
pub min: f64,
pub max: f64,
pub low_confidence_count: usize,
pub low_confidence_ratio: f64,
pub is_healthy: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confident_step_creation() {
let step = ConfidentStep::new(serde_json::json!({"Click": "button"}), 0.85)
.with_description("Click the button")
.with_alternative(Alternative::new(serde_json::json!({"Click": ".btn"}), 0.6));
assert_eq!(step.confidence, 0.85);
assert!(step.is_confident(0.8));
assert!(!step.is_confident(0.9));
assert!(step.has_alternatives());
}
#[test]
fn test_confidence_clamping() {
let step = ConfidentStep::new(serde_json::json!({}), 1.5);
assert_eq!(step.confidence, 1.0);
let step = ConfidentStep::new(serde_json::json!({}), -0.5);
assert_eq!(step.confidence, 0.0);
}
#[test]
fn test_sorted_alternatives() {
let step = ConfidentStep::new(serde_json::json!({}), 0.9)
.with_alternative(Alternative::new(serde_json::json!({"a": 1}), 0.3))
.with_alternative(Alternative::new(serde_json::json!({"b": 2}), 0.7))
.with_alternative(Alternative::new(serde_json::json!({"c": 3}), 0.5));
let sorted = step.sorted_alternatives();
assert_eq!(sorted.len(), 3);
assert_eq!(sorted[0].confidence, 0.7);
assert_eq!(sorted[1].confidence, 0.5);
assert_eq!(sorted[2].confidence, 0.3);
}
#[test]
fn test_verification() {
let v = Verification::url_contains("/dashboard");
assert_eq!(v.verification_type, VerificationType::UrlContains);
assert_eq!(v.expected, "/dashboard");
assert!(v.retry_on_failure);
}
#[test]
fn test_retry_strategy_threshold() {
let strategy = ConfidenceRetryStrategy::new(0.8).with_adaptive(true);
assert!((strategy.threshold_for_attempt(0) - 0.8).abs() < 0.001);
assert!((strategy.threshold_for_attempt(1) - 0.7).abs() < 0.001);
assert!((strategy.threshold_for_attempt(2) - 0.6).abs() < 0.001);
assert!((strategy.threshold_for_attempt(10) - 0.2).abs() < 0.001);
}
#[test]
fn test_retry_strategy_acceptance() {
let strategy = ConfidenceRetryStrategy::new(0.7);
let high_conf = ConfidentStep::new(serde_json::json!({}), 0.8);
let low_conf = ConfidentStep::new(serde_json::json!({}), 0.5);
assert!(strategy.should_accept(&high_conf, 0));
assert!(!strategy.should_accept(&low_conf, 0));
let adaptive = strategy.with_adaptive(true);
assert!(adaptive.should_accept(&low_conf, 3)); }
#[test]
fn test_confidence_tracker() {
let mut tracker = ConfidenceTracker::new();
tracker.record("step1", 0.9);
tracker.record("step2", 0.7);
tracker.record("step3", 0.4);
assert_eq!(tracker.count(), 3);
assert!((tracker.average() - 0.666).abs() < 0.01);
assert_eq!(tracker.min(), 0.4);
assert_eq!(tracker.max(), 0.9);
assert_eq!(tracker.low_confidence_count(), 1);
}
#[test]
fn test_confidence_tracker_health() {
let mut healthy = ConfidenceTracker::new();
healthy.record("s1", 0.8);
healthy.record("s2", 0.9);
healthy.record("s3", 0.7);
assert!(healthy.is_healthy());
let mut unhealthy = ConfidenceTracker::new();
unhealthy.record("s1", 0.3);
unhealthy.record("s2", 0.4);
unhealthy.record("s3", 0.2);
assert!(!unhealthy.is_healthy());
}
#[test]
fn test_parse_from_json() {
let json = serde_json::json!({
"action": { "Click": "button" },
"confidence": 0.85,
"alternatives": [
{ "action": { "Click": ".btn" }, "confidence": 0.6 }
],
"verification": {
"type": "element_exists",
"expected": ".success"
}
});
let step = ConfidentStep::from_json(&json).expect("valid JSON");
assert_eq!(step.confidence, 0.85);
assert_eq!(step.alternatives.len(), 1);
assert!(step.verification.is_some());
}
#[test]
fn test_confidence_summary() {
let mut tracker = ConfidenceTracker::new();
tracker.record("a", 0.8);
tracker.record("b", 0.6);
let summary = tracker.summary();
assert_eq!(summary.count, 2);
assert_eq!(summary.average, 0.7);
assert_eq!(summary.min, 0.6);
assert_eq!(summary.max, 0.8);
}
}