systemprompt_models/artifacts/card/
mod.rs1use 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: JsonValue,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub icon: Option<String>,
47}
48
49impl CardSection {
50 pub fn new(heading: impl Into<String>, content: impl Into<String>) -> Self {
51 Self {
52 heading: heading.into(),
53 content: JsonValue::String(content.into()),
54 icon: None,
55 }
56 }
57
58 #[must_use]
59 pub fn value(heading: impl Into<String>, content: JsonValue) -> Self {
60 Self {
61 heading: heading.into(),
62 content,
63 icon: None,
64 }
65 }
66
67 #[must_use]
70 pub fn content_display(&self) -> String {
71 match &self.content {
72 JsonValue::String(s) => s.clone(),
73 JsonValue::Null => String::new(),
74 other => other.to_string(),
75 }
76 }
77
78 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
79 self.icon = Some(icon.into());
80 self
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
85pub struct CardCta {
86 pub id: String,
87 pub label: String,
88 pub message: String,
89 pub variant: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub icon: Option<String>,
92}
93
94impl CardCta {
95 pub fn new(
96 id: impl Into<String>,
97 label: impl Into<String>,
98 message: impl Into<String>,
99 variant: impl Into<String>,
100 ) -> Self {
101 Self {
102 id: id.into(),
103 label: label.into(),
104 message: message.into(),
105 variant: variant.into(),
106 icon: None,
107 }
108 }
109
110 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
111 self.icon = Some(icon.into());
112 self
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
117pub struct PresentationCardArtifact {
118 #[serde(rename = "x-artifact-type")]
119 #[serde(default = "default_card_artifact_type")]
120 pub artifact_type: String,
121 pub title: String,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub subtitle: Option<String>,
124 pub sections: Vec<CardSection>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub ctas: Vec<CardCta>,
127 #[serde(default = "default_theme")]
128 pub theme: String,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub execution_id: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub skill_id: Option<SkillId>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub skill_name: Option<String>,
135 #[serde(skip)]
136 #[schemars(skip)]
137 metadata: ExecutionMetadata,
138}
139
140fn default_theme() -> String {
141 "gradient".to_owned()
142}
143
144fn default_card_artifact_type() -> String {
145 "presentation_card".to_owned()
146}
147
148impl PresentationCardArtifact {
149 pub const ARTIFACT_TYPE_STR: &'static str = "presentation_card";
150
151 pub fn new(title: impl Into<String>) -> Self {
152 Self {
153 artifact_type: "presentation_card".to_owned(),
154 title: title.into(),
155 subtitle: None,
156 sections: Vec::new(),
157 ctas: Vec::new(),
158 theme: default_theme(),
159 execution_id: None,
160 skill_id: None,
161 skill_name: None,
162 metadata: ExecutionMetadata::default(),
163 }
164 }
165
166 pub fn with_request(mut self, ctx: &RequestContext) -> Self {
167 self.metadata = ExecutionMetadata::with_request(ctx);
168 self
169 }
170
171 pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
172 self.subtitle = Some(subtitle.into());
173 self
174 }
175
176 pub fn with_sections(mut self, sections: Vec<CardSection>) -> Self {
177 self.sections = sections;
178 self
179 }
180
181 pub fn add_section(mut self, section: CardSection) -> Self {
182 self.sections.push(section);
183 self
184 }
185
186 pub fn with_ctas(mut self, ctas: Vec<CardCta>) -> Self {
187 self.ctas = ctas;
188 self
189 }
190
191 pub fn add_cta(mut self, cta: CardCta) -> Self {
192 self.ctas.push(cta);
193 self
194 }
195
196 pub fn with_theme(mut self, theme: impl Into<String>) -> Self {
197 self.theme = theme.into();
198 self
199 }
200
201 pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
202 let id_str = id.into();
203 self.execution_id = Some(id_str.clone());
204 self.metadata.execution_id = Some(id_str);
205 self
206 }
207
208 pub fn with_skill(
209 mut self,
210 skill_id: impl Into<SkillId>,
211 skill_name: impl Into<String>,
212 ) -> Self {
213 let id = skill_id.into();
214 self.skill_id = Some(id.clone());
215 self.skill_name = Some(skill_name.into());
216 self.metadata.skill_id = Some(id);
217 self
218 }
219}
220
221impl Artifact for PresentationCardArtifact {
222 fn artifact_type(&self) -> ArtifactType {
223 ArtifactType::PresentationCard
224 }
225
226 fn to_schema(&self) -> JsonValue {
227 json!({
228 "type": "object",
229 "properties": {
230 "title": {
231 "type": "string",
232 "description": "Card title"
233 },
234 "subtitle": {
235 "type": "string",
236 "description": "Card subtitle"
237 },
238 "sections": {
239 "type": "array",
240 "description": "Content sections",
241 "items": {
242 "type": "object",
243 "properties": {
244 "heading": {"type": "string"},
245 "content": {"description": "Section content: plain string or structured JSON"},
246 "icon": {"type": "string"}
247 },
248 "required": ["heading", "content"]
249 }
250 },
251 "ctas": {
252 "type": "array",
253 "description": "Call-to-action buttons",
254 "items": {
255 "type": "object",
256 "properties": {
257 "id": {"type": "string"},
258 "label": {"type": "string"},
259 "message": {"type": "string"},
260 "variant": {"type": "string"},
261 "icon": {"type": "string"}
262 },
263 "required": ["id", "label", "message", "variant"]
264 }
265 },
266 "theme": {
267 "type": "string",
268 "description": "Card theme",
269 "default": "gradient"
270 },
271 "_execution_id": {
272 "type": "string",
273 "description": "Execution ID for tracking"
274 }
275 },
276 "required": ["title", "sections"],
277 "x-artifact-type": "presentation_card",
278 "x-presentation-hints": {
279 "theme": self.theme
280 }
281 })
282 }
283}