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