systemprompt-models 0.2.1

Foundation data models for systemprompt.io AI governance infrastructure. Shared DTOs, config, and domain types consumed by every layer of the MCP governance pipeline.
Documentation
use crate::artifacts::metadata::ExecutionMetadata;
use crate::artifacts::traits::Artifact;
use crate::artifacts::types::ArtifactType;
use crate::execution::context::RequestContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use systemprompt_identifiers::SkillId;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct PresentationCardResponse {
    #[serde(rename = "x-artifact-type")]
    pub artifact_type: String,
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subtitle: Option<String>,
    pub sections: Vec<CardSection>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub ctas: Vec<CardCta>,
    pub theme: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub execution_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub skill_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub skill_name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CardSection {
    pub heading: String,
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icon: Option<String>,
}

impl CardSection {
    pub fn new(heading: impl Into<String>, content: impl Into<String>) -> Self {
        Self {
            heading: heading.into(),
            content: content.into(),
            icon: None,
        }
    }

    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
        self.icon = Some(icon.into());
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CardCta {
    // JSON: CTA button action identifier, not an entity ID
    pub id: String,
    pub label: String,
    pub message: String,
    pub variant: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icon: Option<String>,
}

impl CardCta {
    pub fn new(
        id: impl Into<String>,
        label: impl Into<String>,
        message: impl Into<String>,
        variant: impl Into<String>,
    ) -> Self {
        Self {
            id: id.into(),
            label: label.into(),
            message: message.into(),
            variant: variant.into(),
            icon: None,
        }
    }

    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
        self.icon = Some(icon.into());
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PresentationCardArtifact {
    #[serde(rename = "x-artifact-type")]
    #[serde(default = "default_card_artifact_type")]
    pub artifact_type: String,
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subtitle: Option<String>,
    pub sections: Vec<CardSection>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub ctas: Vec<CardCta>,
    #[serde(default = "default_theme")]
    pub theme: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub execution_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub skill_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub skill_name: Option<String>,
    #[serde(skip)]
    #[schemars(skip)]
    metadata: ExecutionMetadata,
}

fn default_theme() -> String {
    "gradient".to_string()
}

fn default_card_artifact_type() -> String {
    "presentation_card".to_string()
}

impl PresentationCardArtifact {
    pub const ARTIFACT_TYPE_STR: &'static str = "presentation_card";

    pub fn new(title: impl Into<String>, ctx: &RequestContext) -> Self {
        Self {
            artifact_type: "presentation_card".to_string(),
            title: title.into(),
            subtitle: None,
            sections: Vec::new(),
            ctas: Vec::new(),
            theme: default_theme(),
            execution_id: None,
            skill_id: None,
            skill_name: None,
            metadata: ExecutionMetadata::with_request(ctx),
        }
    }

    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
        self.subtitle = Some(subtitle.into());
        self
    }

    pub fn with_sections(mut self, sections: Vec<CardSection>) -> Self {
        self.sections = sections;
        self
    }

    pub fn add_section(mut self, section: CardSection) -> Self {
        self.sections.push(section);
        self
    }

    pub fn with_ctas(mut self, ctas: Vec<CardCta>) -> Self {
        self.ctas = ctas;
        self
    }

    pub fn add_cta(mut self, cta: CardCta) -> Self {
        self.ctas.push(cta);
        self
    }

    pub fn with_theme(mut self, theme: impl Into<String>) -> Self {
        self.theme = theme.into();
        self
    }

    pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
        let id_str = id.into();
        self.execution_id = Some(id_str.clone());
        self.metadata.execution_id = Some(id_str);
        self
    }

    pub fn with_skill(
        mut self,
        skill_id: impl Into<SkillId>,
        skill_name: impl Into<String>,
    ) -> Self {
        let id = skill_id.into();
        self.skill_id = Some(id.to_string());
        self.skill_name = Some(skill_name.into());
        self.metadata.skill_id = Some(id);
        self
    }
}

impl Artifact for PresentationCardArtifact {
    fn artifact_type(&self) -> ArtifactType {
        ArtifactType::PresentationCard
    }

    fn to_schema(&self) -> JsonValue {
        json!({
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "Card title"
                },
                "subtitle": {
                    "type": "string",
                    "description": "Card subtitle"
                },
                "sections": {
                    "type": "array",
                    "description": "Content sections",
                    "items": {
                        "type": "object",
                        "properties": {
                            "heading": {"type": "string"},
                            "content": {"type": "string"},
                            "icon": {"type": "string"}
                        },
                        "required": ["heading", "content"]
                    }
                },
                "ctas": {
                    "type": "array",
                    "description": "Call-to-action buttons",
                    "items": {
                        "type": "object",
                        "properties": {
                            "id": {"type": "string"},
                            "label": {"type": "string"},
                            "message": {"type": "string"},
                            "variant": {"type": "string"},
                            "icon": {"type": "string"}
                        },
                        "required": ["id", "label", "message", "variant"]
                    }
                },
                "theme": {
                    "type": "string",
                    "description": "Card theme",
                    "default": "gradient"
                },
                "_execution_id": {
                    "type": "string",
                    "description": "Execution ID for tracking"
                }
            },
            "required": ["title", "sections"],
            "x-artifact-type": "presentation_card",
            "x-presentation-hints": {
                "theme": self.theme
            }
        })
    }
}