use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
pub id: String,
pub name: String,
pub description: String,
pub category: SkillCategory,
pub parameters: Vec<SkillParameter>,
pub steps: Vec<SkillStep>,
pub tags: Vec<String>,
pub author: String,
pub version: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub public: bool,
pub rating: f32,
pub usage_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SkillCategory {
Navigation,
FormFilling,
DataExtraction,
Testing,
Automation,
Security,
Ecommerce,
SocialMedia,
General,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillParameter {
pub name: String,
pub description: String,
pub parameter_type: ParameterType,
pub required: bool,
pub default_value: Option<serde_json::Value>,
pub validation: Option<ParameterValidation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ParameterType {
String,
Number,
Boolean,
Array,
Object,
Url,
Selector,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterValidation {
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
pub allowed_values: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillStep {
pub id: String,
pub name: String,
pub action: StepAction,
pub parameters: HashMap<String, serde_json::Value>,
pub timeout_ms: u32,
pub retry_count: u32,
pub on_failure: StepFailureAction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StepAction {
Navigate,
Click,
Type,
Wait,
Screenshot,
ExecuteScript,
ExtractText,
ExtractAttribute,
Scroll,
WaitForElement,
Condition,
Loop,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StepFailureAction {
Stop,
Continue,
Retry,
Skip,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillExecution {
pub id: String,
pub skill_id: String,
pub agent_id: String,
pub session_id: String,
pub parameters: HashMap<String, serde_json::Value>,
pub status: ExecutionStatus,
pub started_at: chrono::DateTime<chrono::Utc>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub current_step: Option<String>,
pub step_results: HashMap<String, StepResult>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ExecutionStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepResult {
pub step_id: String,
pub success: bool,
pub data: serde_json::Value,
pub error: Option<String>,
pub execution_time_ms: u64,
pub screenshot: Option<String>,
}
pub struct SkillEngine {
skills: Arc<RwLock<HashMap<String, Skill>>>,
executions: Arc<RwLock<HashMap<String, SkillExecution>>>,
}
impl Default for SkillEngine {
fn default() -> Self {
Self::new()
}
}
impl SkillEngine {
pub fn new() -> Self {
Self {
skills: Arc::new(RwLock::new(HashMap::new())),
executions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn register_skill(&self, skill: Skill) -> Result<(), anyhow::Error> {
let mut skills = self.skills.write().await;
if skills.contains_key(&skill.id) {
return Err(anyhow::anyhow!("Skill with ID {} already exists", skill.id));
}
skills.insert(skill.id.clone(), skill.clone());
info!("Registered skill: {} (ID: {})", skill.name, skill.id);
Ok(())
}
pub async fn get_skill(&self, skill_id: &str) -> Option<Skill> {
let skills = self.skills.read().await;
skills.get(skill_id).cloned()
}
pub async fn list_skills(
&self,
category: Option<SkillCategory>,
tags: Option<Vec<String>>,
) -> Vec<Skill> {
let skills = self.skills.read().await;
skills
.values()
.filter(|skill| {
let category_match = category.as_ref().is_none_or(|c| skill.category == *c);
let tags_match = tags
.as_ref()
.is_none_or(|t| t.iter().all(|tag| skill.tags.contains(tag)));
category_match && tags_match
})
.cloned()
.collect()
}
pub async fn search_skills(&self, query: &str) -> Vec<Skill> {
let skills = self.skills.read().await;
let query_lower = query.to_lowercase();
skills
.values()
.filter(|skill| {
skill.name.to_lowercase().contains(&query_lower)
|| skill.description.to_lowercase().contains(&query_lower)
|| skill
.tags
.iter()
.any(|tag| tag.to_lowercase().contains(&query_lower))
})
.cloned()
.collect()
}
pub async fn execute_skill(
&self,
skill_id: &str,
agent_id: String,
session_id: String,
parameters: HashMap<String, serde_json::Value>,
) -> Result<String, anyhow::Error> {
let skill = {
let skills = self.skills.read().await;
skills
.get(skill_id)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Skill not found: {}", skill_id))?
};
self.validate_skill_parameters(&skill, ¶meters).await?;
let execution_id = Uuid::new_v4().to_string();
let execution = SkillExecution {
id: execution_id.clone(),
skill_id: skill_id.to_string(),
agent_id,
session_id,
parameters,
status: ExecutionStatus::Pending,
started_at: chrono::Utc::now(),
completed_at: None,
current_step: None,
step_results: HashMap::new(),
error: None,
};
{
let mut executions = self.executions.write().await;
executions.insert(execution_id.clone(), execution);
}
let skill_clone = skill.clone();
let executions_clone = Arc::clone(&self.executions);
let execution_id_for_spawn = execution_id.clone();
tokio::spawn(async move {
if let Err(e) = Self::run_skill_execution(
skill_clone,
execution_id_for_spawn.clone(),
executions_clone,
)
.await
{
error!("Skill execution {} failed: {}", execution_id_for_spawn, e);
}
});
info!("Started skill execution: {}", execution_id);
Ok(execution_id)
}
pub async fn get_execution(&self, execution_id: &str) -> Option<SkillExecution> {
let executions = self.executions.read().await;
executions.get(execution_id).cloned()
}
pub async fn cancel_execution(&self, execution_id: &str) -> Result<(), anyhow::Error> {
let mut executions = self.executions.write().await;
if let Some(execution) = executions.get_mut(execution_id) {
match execution.status {
ExecutionStatus::Pending | ExecutionStatus::Running => {
execution.status = ExecutionStatus::Cancelled;
execution.completed_at = Some(chrono::Utc::now());
info!("Cancelled skill execution: {}", execution_id);
Ok(())
}
_ => Err(anyhow::anyhow!(
"Cannot cancel execution in status: {:?}",
execution.status
)),
}
} else {
Err(anyhow::anyhow!("Execution not found: {}", execution_id))
}
}
pub async fn list_executions(&self, agent_id: Option<&str>) -> Vec<SkillExecution> {
let executions = self.executions.read().await;
executions
.values()
.filter(|execution| agent_id.is_none_or(|id| execution.agent_id == id))
.cloned()
.collect()
}
async fn validate_skill_parameters(
&self,
skill: &Skill,
parameters: &HashMap<String, serde_json::Value>,
) -> Result<(), anyhow::Error> {
for param in &skill.parameters {
let value = parameters.get(¶m.name);
if param.required && value.is_none() {
return Err(anyhow::anyhow!(
"Required parameter '{}' is missing",
param.name
));
}
if let Some(value) = value {
self.validate_parameter_value(param, value)?;
}
}
Ok(())
}
fn validate_parameter_value(
&self,
param: &SkillParameter,
value: &serde_json::Value,
) -> Result<(), anyhow::Error> {
match param.parameter_type {
ParameterType::String => {
if !value.is_string() {
return Err(anyhow::anyhow!(
"Parameter '{}' must be a string",
param.name
));
}
}
ParameterType::Number => {
if !value.is_number() {
return Err(anyhow::anyhow!(
"Parameter '{}' must be a number",
param.name
));
}
}
ParameterType::Boolean => {
if !value.is_boolean() {
return Err(anyhow::anyhow!(
"Parameter '{}' must be a boolean",
param.name
));
}
}
ParameterType::Array => {
if !value.is_array() {
return Err(anyhow::anyhow!(
"Parameter '{}' must be an array",
param.name
));
}
}
ParameterType::Object => {
if !value.is_object() {
return Err(anyhow::anyhow!(
"Parameter '{}' must be an object",
param.name
));
}
}
_ => {} }
if let Some(validation) = ¶m.validation {
if let Some(string_value) = value.as_str() {
if let Some(min_length) = validation.min_length {
if string_value.len() < min_length {
return Err(anyhow::anyhow!(
"Parameter '{}' must be at least {} characters",
param.name,
min_length
));
}
}
if let Some(max_length) = validation.max_length {
if string_value.len() > max_length {
return Err(anyhow::anyhow!(
"Parameter '{}' must be at most {} characters",
param.name,
max_length
));
}
}
if let Some(pattern) = &validation.pattern {
if !string_value.contains(pattern) {
return Err(anyhow::anyhow!(
"Parameter '{}' does not match required pattern",
param.name
));
}
}
}
}
Ok(())
}
async fn run_skill_execution(
skill: Skill,
execution_id: String,
executions: Arc<RwLock<HashMap<String, SkillExecution>>>,
) -> Result<(), anyhow::Error> {
{
let mut execs = executions.write().await;
if let Some(execution) = execs.get_mut(&execution_id) {
execution.status = ExecutionStatus::Running;
}
}
info!("Running skill execution: {}", execution_id);
for step in &skill.steps {
{
let mut execs = executions.write().await;
if let Some(execution) = execs.get_mut(&execution_id) {
execution.current_step = Some(step.id.clone());
}
}
debug!("Executing step: {}", step.name);
let step_result = Self::execute_skill_step(step).await;
{
let mut execs = executions.write().await;
if let Some(execution) = execs.get_mut(&execution_id) {
execution
.step_results
.insert(step.id.clone(), step_result.clone());
if !step_result.success {
match step.on_failure {
StepFailureAction::Stop => {
execution.status = ExecutionStatus::Failed;
execution.error = step_result.error;
execution.completed_at = Some(chrono::Utc::now());
return Err(anyhow::anyhow!(
"Skill execution failed at step: {}",
step.name
));
}
StepFailureAction::Skip => continue,
StepFailureAction::Retry => {
continue;
}
StepFailureAction::Continue => continue,
}
}
}
}
}
{
let mut execs = executions.write().await;
if let Some(execution) = execs.get_mut(&execution_id) {
execution.status = ExecutionStatus::Completed;
execution.completed_at = Some(chrono::Utc::now());
execution.current_step = None;
}
}
info!("Skill execution completed: {}", execution_id);
Ok(())
}
async fn execute_skill_step(step: &SkillStep) -> StepResult {
let start_time = std::time::Instant::now();
debug!("Executing step action: {:?}", step.action);
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let execution_time = start_time.elapsed().as_millis() as u64;
let (success, data, error) = match &step.action {
StepAction::Navigate => {
if let Some(url) = step.parameters.get("url") {
(true, url.clone(), None)
} else {
(
false,
serde_json::Value::Null,
Some("Missing URL parameter".to_string()),
)
}
}
StepAction::Click => {
if let Some(selector) = step.parameters.get("selector") {
(true, serde_json::json!({"clicked": selector}), None)
} else {
(
false,
serde_json::Value::Null,
Some("Missing selector parameter".to_string()),
)
}
}
StepAction::Type => {
if let Some(text) = step.parameters.get("text") {
(true, serde_json::json!({"typed": text}), None)
} else {
(
false,
serde_json::Value::Null,
Some("Missing text parameter".to_string()),
)
}
}
StepAction::Screenshot => (
true,
serde_json::json!({"screenshot": "base64_image_data"}),
None,
),
_ => (true, serde_json::json!({"result": "success"}), None),
};
StepResult {
step_id: step.id.clone(),
success,
data,
error,
execution_time_ms: execution_time,
screenshot: None,
}
}
pub async fn get_skill_stats(&self) -> SkillStats {
let skills = self.skills.read().await;
let executions = self.executions.read().await;
let total_skills = skills.len();
let public_skills = skills.values().filter(|s| s.public).count();
let category_counts = {
let mut counts = HashMap::new();
for skill in skills.values() {
*counts.entry(format!("{:?}", skill.category)).or_insert(0) += 1;
}
counts
};
let total_executions = executions.len();
let running_executions = executions
.values()
.filter(|e| e.status == ExecutionStatus::Running)
.count();
let completed_executions = executions
.values()
.filter(|e| e.status == ExecutionStatus::Completed)
.count();
let failed_executions = executions
.values()
.filter(|e| e.status == ExecutionStatus::Failed)
.count();
SkillStats {
total_skills,
public_skills,
category_counts,
total_executions,
running_executions,
completed_executions,
failed_executions,
}
}
pub async fn init_default_skills(&self) -> Result<(), anyhow::Error> {
let default_skills = vec![
Skill {
id: "navigate-to-url".to_string(),
name: "Navigate to URL".to_string(),
description: "Navigate to a specific URL".to_string(),
category: SkillCategory::Navigation,
parameters: vec![SkillParameter {
name: "url".to_string(),
description: "URL to navigate to".to_string(),
parameter_type: ParameterType::Url,
required: true,
default_value: None,
validation: None,
}],
steps: vec![SkillStep {
id: "step1".to_string(),
name: "Navigate to URL".to_string(),
action: StepAction::Navigate,
parameters: HashMap::from([(
"url".to_string(),
serde_json::Value::String("${url}".to_string()),
)]),
timeout_ms: 10000,
retry_count: 3,
on_failure: StepFailureAction::Stop,
}],
tags: vec!["navigation".to_string(), "basic".to_string()],
author: "Ditto Team".to_string(),
version: "1.0.0".to_string(),
created_at: chrono::Utc::now(),
public: true,
rating: 5.0,
usage_count: 0,
},
Skill {
id: "take-screenshot".to_string(),
name: "Take Screenshot".to_string(),
description: "Take a screenshot of the current page".to_string(),
category: SkillCategory::Testing,
parameters: vec![SkillParameter {
name: "filename".to_string(),
description: "Filename for the screenshot".to_string(),
parameter_type: ParameterType::String,
required: false,
default_value: Some(serde_json::Value::String("screenshot.png".to_string())),
validation: None,
}],
steps: vec![SkillStep {
id: "step1".to_string(),
name: "Take screenshot".to_string(),
action: StepAction::Screenshot,
parameters: HashMap::from([(
"filename".to_string(),
serde_json::Value::String("${filename}".to_string()),
)]),
timeout_ms: 5000,
retry_count: 2,
on_failure: StepFailureAction::Stop,
}],
tags: vec!["screenshot".to_string(), "testing".to_string()],
author: "Ditto Team".to_string(),
version: "1.0.0".to_string(),
created_at: chrono::Utc::now(),
public: true,
rating: 4.5,
usage_count: 0,
},
];
for skill in default_skills {
self.register_skill(skill).await?;
}
info!("Initialized default skills");
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillStats {
pub total_skills: usize,
pub public_skills: usize,
pub category_counts: HashMap<String, usize>,
pub total_executions: usize,
pub running_executions: usize,
pub completed_executions: usize,
pub failed_executions: usize,
}