use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::ingredient::Ingredient;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AgentMode {
#[default]
Managed,
External,
#[serde(other)]
Unknown,
}
impl std::fmt::Display for AgentMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgentMode::Managed => write!(f, "managed"),
AgentMode::External => write!(f, "external"),
AgentMode::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Guardrail {
pub kind: String,
#[serde(default = "default_guardrail_stage")]
pub stage: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
}
fn default_guardrail_stage() -> String {
"pre".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agent {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
pub framework: AgentFramework,
#[serde(default)]
pub mode: AgentMode,
#[serde(default)]
pub model_provider: String,
#[serde(default)]
pub model_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backup_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backup_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_turns: Option<u32>,
#[serde(default)]
pub skills: Vec<String>,
#[serde(default)]
pub guardrails: Vec<Guardrail>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub a2a_endpoint: Option<String>,
#[serde(default)]
pub ingredients: Vec<Ingredient>,
#[serde(default)]
pub tags: Vec<String>,
pub status: AgentStatus,
#[serde(default, alias = "kitchen", skip_serializing_if = "Option::is_none")]
pub kitchen_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl Agent {
pub fn builder(name: impl Into<String>) -> AgentBuilder {
AgentBuilder::new(name)
}
pub fn qualified_name(&self) -> String {
format!("{}@{}", self.name, self.version)
}
}
#[derive(Debug)]
pub struct AgentBuilder {
name: String,
version: String,
description: String,
framework: AgentFramework,
mode: AgentMode,
model_provider: String,
model_name: String,
backup_provider: Option<String>,
backup_model: Option<String>,
system_prompt: Option<String>,
max_turns: Option<u32>,
skills: Vec<String>,
guardrails: Vec<Guardrail>,
a2a_endpoint: Option<String>,
ingredients: Vec<Ingredient>,
tags: Vec<String>,
kitchen_id: Option<String>,
metadata: Option<serde_json::Value>,
}
impl AgentBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
version: "0.1.0".into(),
description: String::new(),
framework: AgentFramework::Custom,
mode: AgentMode::default(),
model_provider: String::new(),
model_name: String::new(),
backup_provider: None,
backup_model: None,
system_prompt: None,
max_turns: None,
skills: Vec::new(),
guardrails: Vec::new(),
a2a_endpoint: None,
ingredients: Vec::new(),
tags: Vec::new(),
kitchen_id: None,
metadata: None,
}
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = version.into();
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn framework(mut self, framework: AgentFramework) -> Self {
self.framework = framework;
self
}
pub fn ingredient(mut self, ingredient: Ingredient) -> Self {
self.ingredients.push(ingredient);
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn kitchen(mut self, kitchen_id: impl Into<String>) -> Self {
self.kitchen_id = Some(kitchen_id.into());
self
}
pub fn mode(mut self, mode: AgentMode) -> Self {
self.mode = mode;
self
}
pub fn model_provider(mut self, provider: impl Into<String>) -> Self {
self.model_provider = provider.into();
self
}
pub fn model_name(mut self, name: impl Into<String>) -> Self {
self.model_name = name.into();
self
}
pub fn backup_provider(mut self, provider: impl Into<String>) -> Self {
self.backup_provider = Some(provider.into());
self
}
pub fn backup_model(mut self, model: impl Into<String>) -> Self {
self.backup_model = Some(model.into());
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn max_turns(mut self, turns: u32) -> Self {
self.max_turns = Some(turns);
self
}
pub fn skill(mut self, s: impl Into<String>) -> Self {
self.skills.push(s.into());
self
}
pub fn guardrail(mut self, g: Guardrail) -> Self {
self.guardrails.push(g);
self
}
pub fn a2a_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.a2a_endpoint = Some(endpoint.into());
self
}
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn build(self) -> Agent {
let now = Utc::now();
Agent {
id: Uuid::new_v4().to_string(),
name: self.name,
version: self.version,
description: self.description,
framework: self.framework,
mode: self.mode,
model_provider: self.model_provider,
model_name: self.model_name,
backup_provider: self.backup_provider,
backup_model: self.backup_model,
system_prompt: self.system_prompt,
max_turns: self.max_turns,
skills: self.skills,
guardrails: self.guardrails,
a2a_endpoint: self.a2a_endpoint,
ingredients: self.ingredients,
tags: self.tags,
status: AgentStatus::Draft,
kitchen_id: self.kitchen_id,
resolved_config: None,
created_by: None,
created_at: now,
updated_at: now,
metadata: self.metadata,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AgentFramework {
#[serde(alias = "lang-graph", alias = "langgraph")]
Langchain,
#[serde(alias = "crew-ai")]
Crewai,
#[serde(alias = "openai-sdk", alias = "open-ai-sdk")]
Openai,
#[serde(alias = "auto-gen")]
Autogen,
Managed,
#[default]
#[serde(other)]
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AgentStatus {
Draft,
Baking,
Ready,
Cooled,
Burnt,
Retired,
}
impl std::fmt::Display for AgentStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgentStatus::Draft => write!(f, "🟡 draft"),
AgentStatus::Baking => write!(f, "🔥 baking"),
AgentStatus::Ready => write!(f, "🟢 ready"),
AgentStatus::Cooled => write!(f, "⏸️ cooled"),
AgentStatus::Burnt => write!(f, "🔴 burnt"),
AgentStatus::Retired => write!(f, "⚫ retired"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ingredient::Ingredient;
#[test]
fn test_agent_builder() {
let agent = Agent::builder("summarizer")
.version("1.0.0")
.description("Summarizes documents")
.framework(AgentFramework::Langchain)
.ingredient(Ingredient::model("gpt-4o").provider("azure-openai").build())
.tag("nlp")
.tag("summarization")
.build();
assert_eq!(agent.name, "summarizer");
assert_eq!(agent.version, "1.0.0");
assert_eq!(agent.qualified_name(), "summarizer@1.0.0");
assert_eq!(agent.status, AgentStatus::Draft);
assert_eq!(agent.ingredients.len(), 1);
assert_eq!(agent.tags, vec!["nlp", "summarization"]);
}
}