use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SkillParameter {
pub name: String,
#[serde(rename = "type")]
pub param_type: String,
pub description: String,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub default: Option<Value>,
#[serde(default)]
pub example: Option<Value>,
#[serde(default)]
pub enum_values: Option<Vec<String>>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SkillMetadata {
pub name: String,
pub description: String,
pub usage_hint: String,
pub parameters: Vec<SkillParameter>,
pub example_call: serde_json::Value,
pub example_output: String,
pub category: String,
}
#[async_trait::async_trait]
pub trait Skill: Send + Sync + Debug {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn usage_hint(&self) -> &str {
"No usage hint provided"
}
fn parameters(&self) -> Vec<SkillParameter> {
vec![]
}
fn example_call(&self) -> serde_json::Value {
serde_json::json!({})
}
fn example_output(&self) -> String {
String::new()
}
fn category(&self) -> &str {
"general"
}
async fn execute(&self, parameters: &HashMap<String, Value>) -> Result<String>;
fn validate(&self, parameters: &HashMap<String, Value>) -> Result<()> {
let param_defs = self.parameters();
for def in param_defs {
let param_name = &def.name;
let has_value = parameters.contains_key(param_name);
if def.required && !has_value {
anyhow::bail!("Required parameter '{}' is missing", param_name);
}
if let Some(value) = parameters.get(param_name) {
let type_matches = match def.param_type.as_str() {
"string" => value.is_string(),
"integer" => value.is_i64() || value.is_u64(),
"boolean" => value.is_boolean(),
"array" => value.is_array(),
"object" => value.is_object(),
_ => true, };
if !type_matches {
anyhow::bail!(
"Parameter '{}' expects type '{}' but got {:?}",
param_name,
def.param_type,
value
);
}
if let Some(enum_vals) = &def.enum_values {
if let Some(str_val) = value.as_str() {
if !enum_vals.contains(&str_val.to_string()) {
anyhow::bail!(
"Parameter '{}' value '{}' is not in allowed values: {:?}",
param_name,
str_val,
enum_vals
);
}
}
}
}
}
Ok(())
}
fn get_metadata(&self) -> SkillMetadata {
SkillMetadata {
name: self.name().to_string(),
description: self.description().to_string(),
usage_hint: self.usage_hint().to_string(),
parameters: self.parameters(),
example_call: self.example_call(),
example_output: self.example_output(),
category: self.category().to_string(),
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct SkillCall {
pub action: String,
#[serde(default)]
pub parameters: HashMap<String, Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[derive(Debug)]
struct TestSkill {
name: String,
description: String,
}
#[async_trait::async_trait]
impl Skill for TestSkill {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
async fn execute(&self, params: &HashMap<String, Value>) -> Result<String> {
let result = params
.get("input")
.and_then(|v| v.as_str())
.unwrap_or("no input");
Ok(format!("Executed {} with: {}", self.name, result))
}
fn parameters(&self) -> Vec<SkillParameter> {
vec![
SkillParameter {
name: "input".to_string(),
param_type: "string".to_string(),
description: "Input string".to_string(),
required: true,
default: None,
example: Some(json!("test")),
enum_values: None,
},
SkillParameter {
name: "count".to_string(),
param_type: "integer".to_string(),
description: "Count value".to_string(),
required: false,
default: Some(json!(1)),
example: Some(json!(5)),
enum_values: None,
},
]
}
fn usage_hint(&self) -> &str {
"Use this skill to test functionality"
}
fn category(&self) -> &str {
"test"
}
}
#[derive(Debug)]
struct ValidatingSkill;
#[async_trait::async_trait]
impl Skill for ValidatingSkill {
fn name(&self) -> &str {
"validator"
}
fn description(&self) -> &str {
"Validates parameters"
}
async fn execute(&self, params: &HashMap<String, Value>) -> Result<String> {
Ok(format!("Validated: {:?}", params))
}
fn parameters(&self) -> Vec<SkillParameter> {
vec![
SkillParameter {
name: "color".to_string(),
param_type: "string".to_string(),
description: "Color name".to_string(),
required: true,
default: None,
example: Some(json!("red")),
enum_values: Some(vec![
"red".to_string(),
"green".to_string(),
"blue".to_string(),
]),
},
SkillParameter {
name: "value".to_string(),
param_type: "integer".to_string(),
description: "Numeric value".to_string(),
required: false,
default: Some(json!(0)),
example: Some(json!(42)),
enum_values: None,
},
]
}
}
#[tokio::test]
async fn test_skill_metadata_creation() {
let skill = TestSkill {
name: "test_skill".to_string(),
description: "A test skill".to_string(),
};
let metadata = skill.get_metadata();
assert_eq!(metadata.name, "test_skill");
assert_eq!(metadata.description, "A test skill");
assert_eq!(metadata.usage_hint, "Use this skill to test functionality");
assert_eq!(metadata.category, "test");
assert_eq!(metadata.parameters.len(), 2);
assert_eq!(metadata.parameters[0].name, "input");
assert_eq!(metadata.parameters[0].required, true);
assert_eq!(metadata.parameters[1].name, "count");
assert_eq!(metadata.parameters[1].required, false);
}
#[tokio::test]
async fn test_skill_execution_with_parameters() {
let skill = TestSkill {
name: "echo_skill".to_string(),
description: "Echoes input".to_string(),
};
let mut params = HashMap::new();
params.insert("input".to_string(), json!("Hello, World!"));
let result = skill.execute(¶ms).await.unwrap();
assert_eq!(result, "Executed echo_skill with: Hello, World!");
}
#[tokio::test]
async fn test_skill_validation() {
let skill = ValidatingSkill;
let mut valid_params = HashMap::new();
valid_params.insert("color".to_string(), json!("red"));
valid_params.insert("value".to_string(), json!(42));
let result = skill.validate(&valid_params);
assert!(result.is_ok());
let mut missing_required = HashMap::new();
missing_required.insert("value".to_string(), json!(42));
let result = skill.validate(&missing_required);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Required parameter 'color'")
);
let mut invalid_enum = HashMap::new();
invalid_enum.insert("color".to_string(), json!("yellow"));
let result = skill.validate(&invalid_enum);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not in allowed values")
);
let mut wrong_type = HashMap::new();
wrong_type.insert("color".to_string(), json!("red"));
wrong_type.insert("value".to_string(), json!("not an integer"));
let result = skill.validate(&wrong_type);
assert!(result.is_err());
}
#[test]
fn test_skill_call_deserialization() {
let json_data = json!({
"action": "read_file",
"parameters": {
"path": "./config.json",
"encoding": "utf-8"
}
});
let call: SkillCall = serde_json::from_value(json_data).unwrap();
assert_eq!(call.action, "read_file");
assert_eq!(call.parameters.len(), 2);
assert_eq!(
call.parameters.get("path").unwrap().as_str(),
Some("./config.json")
);
assert_eq!(
call.parameters.get("encoding").unwrap().as_str(),
Some("utf-8")
);
}
#[test]
fn test_skill_call_without_parameters() {
let json_data = json!({
"action": "list_skills"
});
let call: SkillCall = serde_json::from_value(json_data).unwrap();
assert_eq!(call.action, "list_skills");
assert!(call.parameters.is_empty());
}
#[test]
fn test_skill_parameter_serialization() {
let param = SkillParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Timeout in seconds".to_string(),
required: true,
default: Some(json!(30)),
example: Some(json!(60)),
enum_values: None,
};
let serialized = serde_json::to_string(¶m).unwrap();
let deserialized: SkillParameter = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.name, param.name);
assert_eq!(deserialized.param_type, param.param_type);
assert_eq!(deserialized.required, param.required);
assert_eq!(deserialized.default, param.default);
}
#[test]
fn test_skill_parameter_with_enum_values() {
let param = SkillParameter {
name: "mode".to_string(),
param_type: "string".to_string(),
description: "Operation mode".to_string(),
required: true,
default: None,
example: Some(json!("fast")),
enum_values: Some(vec![
"fast".to_string(),
"slow".to_string(),
"balanced".to_string(),
]),
};
assert_eq!(param.enum_values.as_ref().unwrap().len(), 3);
assert!(
param
.enum_values
.as_ref()
.unwrap()
.contains(&"fast".to_string())
);
}
}