use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use turul_a2a_proto::AgentSkill;
use crate::error::{ManifestError, RenderError, ValidationError};
use crate::schema::{compile_schema, validate_json};
use crate::template::render_template;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SkillCard {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
pub examples: Vec<String>,
pub input_modes: Vec<String>,
pub output_modes: Vec<String>,
pub security_requirements: Vec<String>,
pub input_schema: Option<Value>,
pub output_schema: Option<Value>,
pub execution_hints: Option<ExecutionHints>,
pub provider_config: Option<Value>,
pub body: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Frontmatter {
id: String,
name: String,
description: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
examples: Vec<String>,
#[serde(default)]
input_modes: Vec<String>,
#[serde(default)]
output_modes: Vec<String>,
#[serde(default)]
security_requirements: Vec<String>,
#[serde(default)]
input_schema: Option<Value>,
#[serde(default)]
output_schema: Option<Value>,
#[serde(default)]
execution_hints: Option<ExecutionHints>,
#[serde(default)]
provider_config: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[non_exhaustive]
pub struct ExecutionHints {
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub top_p: Option<f32>,
}
fn yaml_to_json(value: serde_yaml::Value) -> Result<Value, ManifestError> {
use serde_yaml::Value as Y;
Ok(match value {
Y::Null => Value::Null,
Y::Bool(b) => Value::Bool(b),
Y::Number(n) => {
if let Some(i) = n.as_i64() {
Value::from(i)
} else if let Some(u) = n.as_u64() {
Value::from(u)
} else if let Some(f) = n.as_f64() {
serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null)
} else {
Value::Null
}
}
Y::String(s) => Value::String(s),
Y::Sequence(seq) => Value::Array(
seq.into_iter()
.map(yaml_to_json)
.collect::<Result<Vec<_>, _>>()?,
),
Y::Mapping(map) => {
let mut obj = Map::new();
for (k, v) in map {
let key = match k {
Y::String(s) => s,
other => {
return Err(ManifestError::Parse {
location: "frontmatter".to_string(),
reason: format!("non-string mapping key: {other:?}"),
});
}
};
obj.insert(key, yaml_to_json(v)?);
}
Value::Object(obj)
}
Y::Tagged(tagged) => yaml_to_json(tagged.value)?,
})
}
impl SkillCard {
pub fn parse(text: &str) -> Result<Self, ManifestError> {
let (frontmatter, body) = split_frontmatter(text).ok_or(ManifestError::Parse {
location: "document".to_string(),
reason: "missing `---` frontmatter delimiters".to_string(),
})?;
let yaml: serde_yaml::Value =
serde_yaml::from_str(frontmatter).map_err(|e| ManifestError::Parse {
location: "frontmatter".to_string(),
reason: e.to_string(),
})?;
let json = yaml_to_json(yaml)?;
let fm: Frontmatter = serde_json::from_value(json).map_err(|e| ManifestError::Parse {
location: "frontmatter".to_string(),
reason: e.to_string(),
})?;
for (schema, location) in [
(&fm.input_schema, "/inputSchema"),
(&fm.output_schema, "/outputSchema"),
] {
if let Some(schema) = schema {
compile_schema(schema).map_err(|e| ManifestError::Schema {
location: location.to_string(),
reason: e.to_string(),
})?;
}
}
Ok(SkillCard {
id: fm.id,
name: fm.name,
description: fm.description,
tags: fm.tags,
examples: fm.examples,
input_modes: fm.input_modes,
output_modes: fm.output_modes,
security_requirements: fm.security_requirements,
input_schema: fm.input_schema,
output_schema: fm.output_schema,
execution_hints: fm.execution_hints,
provider_config: fm.provider_config,
body: body.to_string(),
})
}
pub fn to_agent_skill(&self) -> AgentSkill {
AgentSkill {
id: self.id.clone(),
name: self.name.clone(),
description: self.description.clone(),
tags: self.tags.clone(),
examples: self.examples.clone(),
input_modes: self.input_modes.clone(),
output_modes: self.output_modes.clone(),
security_requirements: self
.security_requirements
.iter()
.map(|scheme| {
let mut schemes = std::collections::HashMap::new();
schemes.insert(scheme.clone(), turul_a2a_proto::StringList { list: vec![] });
turul_a2a_proto::SecurityRequirement { schemes }
})
.collect(),
}
}
pub fn render_prompt(&self, params: &Value) -> Result<String, RenderError> {
render_template(&self.body, params)
}
pub fn validate_input(&self, input: &Value) -> Result<(), ValidationError> {
match &self.input_schema {
Some(schema) => validate_json(schema, input),
None => Ok(()),
}
}
pub fn validate_output(&self, output: &Value) -> Result<(), ValidationError> {
match &self.output_schema {
Some(schema) => validate_json(schema, output),
None => Ok(()),
}
}
}
fn split_frontmatter(text: &str) -> Option<(&str, &str)> {
let rest = text.strip_prefix("---")?;
let after_open_nl = rest.find('\n')?;
let body_start = &rest[after_open_nl + 1..];
let close = body_start.find("\n---")?;
let frontmatter = &body_start[..close];
let after_close = &body_start[close + 4..]; let body = after_close.strip_prefix('\n').unwrap_or(after_close);
Some((frontmatter, body))
}