Skip to main content

systemprompt_models/artifacts/card/
mod.rs

1//! Presentation-card artifact.
2//!
3//! A [`PresentationCardArtifact`] renders a titled card composed of
4//! [`CardSection`]s and optional [`CardCta`] action buttons under a named
5//! theme. [`PresentationCardResponse`] is the matching deserialization shape
6//! for tool output.
7
8use crate::artifacts::metadata::ExecutionMetadata;
9use crate::artifacts::traits::Artifact;
10use crate::artifacts::types::ArtifactType;
11use crate::execution::context::RequestContext;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_json::{Value as JsonValue, json};
15use systemprompt_identifiers::SkillId;
16
17#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
18pub struct PresentationCardResponse {
19    #[serde(rename = "x-artifact-type")]
20    pub artifact_type: String,
21    pub title: String,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub subtitle: Option<String>,
24    pub sections: Vec<CardSection>,
25    #[serde(skip_serializing_if = "Vec::is_empty", default)]
26    pub ctas: Vec<CardCta>,
27    pub theme: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub execution_id: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub skill_id: Option<SkillId>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub skill_name: Option<String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37pub struct CardSection {
38    pub heading: String,
39    pub content: String,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub icon: Option<String>,
42}
43
44impl CardSection {
45    pub fn new(heading: impl Into<String>, content: impl Into<String>) -> Self {
46        Self {
47            heading: heading.into(),
48            content: content.into(),
49            icon: None,
50        }
51    }
52
53    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
54        self.icon = Some(icon.into());
55        self
56    }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60pub struct CardCta {
61    pub id: String,
62    pub label: String,
63    pub message: String,
64    pub variant: String,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub icon: Option<String>,
67}
68
69impl CardCta {
70    pub fn new(
71        id: impl Into<String>,
72        label: impl Into<String>,
73        message: impl Into<String>,
74        variant: impl Into<String>,
75    ) -> Self {
76        Self {
77            id: id.into(),
78            label: label.into(),
79            message: message.into(),
80            variant: variant.into(),
81            icon: None,
82        }
83    }
84
85    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
86        self.icon = Some(icon.into());
87        self
88    }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
92pub struct PresentationCardArtifact {
93    #[serde(rename = "x-artifact-type")]
94    #[serde(default = "default_card_artifact_type")]
95    pub artifact_type: String,
96    pub title: String,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub subtitle: Option<String>,
99    pub sections: Vec<CardSection>,
100    #[serde(default, skip_serializing_if = "Vec::is_empty")]
101    pub ctas: Vec<CardCta>,
102    #[serde(default = "default_theme")]
103    pub theme: String,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub execution_id: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub skill_id: Option<SkillId>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub skill_name: Option<String>,
110    #[serde(skip)]
111    #[schemars(skip)]
112    metadata: ExecutionMetadata,
113}
114
115fn default_theme() -> String {
116    "gradient".to_owned()
117}
118
119fn default_card_artifact_type() -> String {
120    "presentation_card".to_owned()
121}
122
123impl PresentationCardArtifact {
124    pub const ARTIFACT_TYPE_STR: &'static str = "presentation_card";
125
126    pub fn new(title: impl Into<String>, ctx: &RequestContext) -> Self {
127        Self {
128            artifact_type: "presentation_card".to_owned(),
129            title: title.into(),
130            subtitle: None,
131            sections: Vec::new(),
132            ctas: Vec::new(),
133            theme: default_theme(),
134            execution_id: None,
135            skill_id: None,
136            skill_name: None,
137            metadata: ExecutionMetadata::with_request(ctx),
138        }
139    }
140
141    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
142        self.subtitle = Some(subtitle.into());
143        self
144    }
145
146    pub fn with_sections(mut self, sections: Vec<CardSection>) -> Self {
147        self.sections = sections;
148        self
149    }
150
151    pub fn add_section(mut self, section: CardSection) -> Self {
152        self.sections.push(section);
153        self
154    }
155
156    pub fn with_ctas(mut self, ctas: Vec<CardCta>) -> Self {
157        self.ctas = ctas;
158        self
159    }
160
161    pub fn add_cta(mut self, cta: CardCta) -> Self {
162        self.ctas.push(cta);
163        self
164    }
165
166    pub fn with_theme(mut self, theme: impl Into<String>) -> Self {
167        self.theme = theme.into();
168        self
169    }
170
171    pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
172        let id_str = id.into();
173        self.execution_id = Some(id_str.clone());
174        self.metadata.execution_id = Some(id_str);
175        self
176    }
177
178    pub fn with_skill(
179        mut self,
180        skill_id: impl Into<SkillId>,
181        skill_name: impl Into<String>,
182    ) -> Self {
183        let id = skill_id.into();
184        self.skill_id = Some(id.clone());
185        self.skill_name = Some(skill_name.into());
186        self.metadata.skill_id = Some(id);
187        self
188    }
189}
190
191impl Artifact for PresentationCardArtifact {
192    fn artifact_type(&self) -> ArtifactType {
193        ArtifactType::PresentationCard
194    }
195
196    fn to_schema(&self) -> JsonValue {
197        json!({
198            "type": "object",
199            "properties": {
200                "title": {
201                    "type": "string",
202                    "description": "Card title"
203                },
204                "subtitle": {
205                    "type": "string",
206                    "description": "Card subtitle"
207                },
208                "sections": {
209                    "type": "array",
210                    "description": "Content sections",
211                    "items": {
212                        "type": "object",
213                        "properties": {
214                            "heading": {"type": "string"},
215                            "content": {"type": "string"},
216                            "icon": {"type": "string"}
217                        },
218                        "required": ["heading", "content"]
219                    }
220                },
221                "ctas": {
222                    "type": "array",
223                    "description": "Call-to-action buttons",
224                    "items": {
225                        "type": "object",
226                        "properties": {
227                            "id": {"type": "string"},
228                            "label": {"type": "string"},
229                            "message": {"type": "string"},
230                            "variant": {"type": "string"},
231                            "icon": {"type": "string"}
232                        },
233                        "required": ["id", "label", "message", "variant"]
234                    }
235                },
236                "theme": {
237                    "type": "string",
238                    "description": "Card theme",
239                    "default": "gradient"
240                },
241                "_execution_id": {
242                    "type": "string",
243                    "description": "Execution ID for tracking"
244                }
245            },
246            "required": ["title", "sections"],
247            "x-artifact-type": "presentation_card",
248            "x-presentation-hints": {
249                "theme": self.theme
250            }
251        })
252    }
253}