Skip to main content

systemprompt_models/artifacts/list/
mod.rs

1//! List artifact.
2//!
3//! A [`ListArtifact`] is an ordered collection of [`ListItem`]s, each a
4//! title/summary/link triple plus optional addressing fields (uri, slug,
5//! source id) that let downstream tools resolve the underlying resource. It
6//! implements [`Artifact`].
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, SourceId};
19
20fn default_artifact_type() -> String {
21    "list".to_owned()
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
25pub struct ListItem {
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub id: Option<String>,
28    pub title: String,
29    pub summary: String,
30    pub link: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub uri: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub slug: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub source_id: Option<SourceId>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub category: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub description: Option<String>,
41}
42
43impl ListItem {
44    pub fn new(
45        title: impl Into<String>,
46        summary: impl Into<String>,
47        link: impl Into<String>,
48    ) -> Self {
49        Self {
50            id: None,
51            title: title.into(),
52            summary: summary.into(),
53            link: link.into(),
54            uri: None,
55            slug: None,
56            source_id: None,
57            category: None,
58            description: None,
59        }
60    }
61
62    pub fn with_id(mut self, id: impl Into<String>) -> Self {
63        self.id = Some(id.into());
64        self
65    }
66
67    pub fn with_uri(mut self, uri: impl Into<String>) -> Self {
68        self.uri = Some(uri.into());
69        self
70    }
71
72    pub fn with_slug(mut self, slug: impl Into<String>) -> Self {
73        self.slug = Some(slug.into());
74        self
75    }
76
77    pub fn with_source_id(mut self, source_id: SourceId) -> Self {
78        self.source_id = Some(source_id);
79        self
80    }
81
82    pub fn with_category(mut self, category: impl Into<String>) -> Self {
83        self.category = Some(category.into());
84        self
85    }
86
87    pub fn with_description(mut self, description: impl Into<String>) -> Self {
88        self.description = Some(description.into());
89        self
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
94pub struct ListArtifact {
95    #[serde(rename = "x-artifact-type")]
96    #[serde(default = "default_artifact_type")]
97    pub artifact_type: String,
98    pub items: Vec<ListItem>,
99    pub count: usize,
100    #[serde(skip)]
101    #[schemars(skip)]
102    metadata: ExecutionMetadata,
103}
104
105impl ListArtifact {
106    pub const ARTIFACT_TYPE_STR: &'static str = "list";
107
108    pub fn new() -> Self {
109        Self {
110            artifact_type: "list".to_owned(),
111            items: Vec::new(),
112            count: 0,
113            metadata: ExecutionMetadata::default(),
114        }
115    }
116
117    pub fn with_request(mut self, ctx: &RequestContext) -> Self {
118        self.metadata = ExecutionMetadata::with_request(ctx);
119        self
120    }
121
122    pub fn with_items(mut self, items: Vec<ListItem>) -> Self {
123        self.count = items.len();
124        self.items = items;
125        self
126    }
127
128    pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
129        self.metadata.execution_id = Some(id.into());
130        self
131    }
132
133    pub fn with_skill(
134        mut self,
135        skill_id: impl Into<SkillId>,
136        skill_name: impl Into<String>,
137    ) -> Self {
138        self.metadata.skill_id = Some(skill_id.into());
139        self.metadata.skill_name = Some(skill_name.into());
140        self
141    }
142}
143
144impl Default for ListArtifact {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl Artifact for ListArtifact {
151    fn artifact_type(&self) -> ArtifactType {
152        ArtifactType::List
153    }
154
155    fn to_schema(&self) -> JsonValue {
156        json!({
157            "type": "object",
158            "properties": {
159                "items": {
160                    "type": "array",
161                    "description": "Array of list items",
162                    "items": {
163                        "type": "object",
164                        "properties": {
165                            "title": {
166                                "type": "string",
167                                "description": "Item title"
168                            },
169                            "summary": {
170                                "type": "string",
171                                "description": "Item summary"
172                            },
173                            "link": {
174                                "type": "string",
175                                "description": "Item URL (full HTTPS URL compatible with resource_loading tool's uris parameter)"
176                            },
177                            "uri": {
178                                "type": "string",
179                                "description": "Standardized URI format (tyingshoelaces://blog/slug) for use with resource_loading tool"
180                            },
181                            "slug": {
182                                "type": "string",
183                                "description": "Content slug - can be used directly with resource_loading tool as tyingshoelaces://blog/{slug}"
184                            }
185                        },
186                        "required": ["title", "summary", "link"]
187                    }
188                },
189                "count": {
190                    "type": "integer",
191                    "description": "Total number of items"
192                },
193                "_execution_id": {
194                    "type": "string",
195                    "description": "Execution ID for tracking"
196                }
197            },
198            "required": ["items"],
199            "x-artifact-type": "list"
200        })
201    }
202}