use crate::tool_types::ToolDefinition;
use crate::tools::Tool;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "low"))]
#[serde(rename_all = "lowercase")]
pub enum RiskLevel {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlueprintModel {
Fixed(String),
Default(String),
Inherit,
}
pub struct AgentBlueprint {
pub id: &'static str,
pub name: &'static str,
pub description: &'static str,
pub model: BlueprintModel,
pub system_prompt: &'static str,
pub tools: Vec<Box<dyn Tool>>,
pub max_turns: Option<usize>,
pub config_schema: Option<serde_json::Value>,
}
impl AgentBlueprint {
pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
self.tools.iter().map(|t| t.to_definition()).collect()
}
pub fn validate_config(
&self,
config: Option<&serde_json::Value>,
) -> Result<(), BlueprintConfigError> {
let Some(schema) = self.config_schema.as_ref() else {
return match config {
Some(_) => Err(BlueprintConfigError::NotAccepted { id: self.id }),
None => Ok(()),
};
};
let Some(config) = config else {
let required = schema
.get("required")
.and_then(|r| r.as_array())
.is_some_and(|required| !required.is_empty());
return if required {
Err(BlueprintConfigError::Required { id: self.id })
} else {
Ok(())
};
};
let validator = jsonschema::validator_for(schema).map_err(|error| {
BlueprintConfigError::InvalidSchema {
id: self.id,
reason: error.to_string(),
}
})?;
let issues: Vec<String> = validator
.iter_errors(config)
.map(|error| {
let path = error.instance_path().to_string();
if path.is_empty() {
error.to_string()
} else {
format!("{path}: {error}")
}
})
.collect();
if issues.is_empty() {
Ok(())
} else {
Err(BlueprintConfigError::Invalid {
id: self.id,
issues,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BlueprintConfigError {
NotAccepted {
id: &'static str,
},
Required {
id: &'static str,
},
Invalid {
id: &'static str,
issues: Vec<String>,
},
InvalidSchema {
id: &'static str,
reason: String,
},
}
impl std::fmt::Display for BlueprintConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAccepted { id } => {
write!(f, "Blueprint \"{id}\" accepts no config.")
}
Self::Required { id } => {
write!(f, "Blueprint \"{id}\" requires config.")
}
Self::Invalid { id, issues } => {
write!(
f,
"Blueprint \"{id}\" received invalid config: {}",
issues.join("; ")
)
}
Self::InvalidSchema { id, reason } => {
write!(
f,
"Blueprint \"{id}\" has an invalid config schema: {reason}"
)
}
}
}
}
impl std::error::Error for BlueprintConfigError {}
impl std::fmt::Debug for AgentBlueprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentBlueprint")
.field("id", &self.id)
.field("name", &self.name)
.field("model", &self.model)
.field("tool_count", &self.tools.len())
.field("max_turns", &self.max_turns)
.finish()
}
}