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: 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}