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