Skip to main content

systemprompt_models/artifacts/table/
mod.rs

1//! Tabular artifact: columnar data records returned by skills and tools.
2//!
3//! [`TableArtifact`] is the builder-style producer; [`TableResponse`] is the
4//! serialized wire shape. Column definitions live in [`mod@column`] and display
5//! hints in [`hints`].
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10pub mod column;
11pub mod hints;
12
13pub use column::Column;
14pub use hints::TableHints;
15
16use crate::artifacts::metadata::ExecutionMetadata;
17use crate::artifacts::traits::Artifact;
18use crate::artifacts::types::ArtifactType;
19use crate::execution::context::RequestContext;
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use serde_json::{Value as JsonValue, json};
23use systemprompt_identifiers::SkillId;
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26pub struct TableResponse {
27    #[serde(rename = "x-artifact-type")]
28    pub artifact_type: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub title: Option<String>,
31    pub columns: Vec<Column>,
32    pub items: Vec<JsonValue>,
33    pub count: usize,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub execution_id: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    #[schemars(with = "Option<JsonValue>")]
38    pub hints: Option<JsonValue>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
42pub struct TableArtifact {
43    #[serde(rename = "x-artifact-type")]
44    #[serde(default = "default_artifact_type")]
45    pub artifact_type: String,
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub title: Option<String>,
48    pub columns: Vec<Column>,
49    pub items: Vec<JsonValue>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[schemars(with = "Option<JsonValue>")]
52    pub hints: Option<JsonValue>,
53    #[serde(skip)]
54    #[schemars(skip)]
55    hints_builder: TableHints,
56    #[serde(skip)]
57    #[schemars(skip)]
58    metadata: ExecutionMetadata,
59}
60
61fn default_artifact_type() -> String {
62    "table".to_owned()
63}
64
65impl TableArtifact {
66    pub const ARTIFACT_TYPE_STR: &'static str = "table";
67
68    pub fn new(columns: Vec<Column>) -> Self {
69        Self {
70            artifact_type: "table".to_owned(),
71            title: None,
72            columns,
73            items: Vec::new(),
74            hints: None,
75            hints_builder: TableHints::default(),
76            metadata: ExecutionMetadata::default(),
77        }
78    }
79
80    pub fn with_request(mut self, ctx: &RequestContext) -> Self {
81        self.metadata = ExecutionMetadata::with_request(ctx);
82        self
83    }
84
85    #[must_use]
86    pub fn with_title(mut self, title: impl Into<String>) -> Self {
87        self.title = Some(title.into());
88        self
89    }
90
91    pub fn with_rows(mut self, items: Vec<JsonValue>) -> Self {
92        self.items = items;
93        self
94    }
95
96    pub fn with_hints(mut self, hints: TableHints) -> Self {
97        use crate::artifacts::traits::ArtifactSchema;
98        self.hints = Some(hints.generate_schema());
99        self.hints_builder = hints;
100        self
101    }
102
103    pub fn with_metadata(mut self, metadata: ExecutionMetadata) -> Self {
104        self.metadata = metadata;
105        self
106    }
107
108    pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
109        self.metadata.execution_id = Some(id.into());
110        self
111    }
112
113    pub fn with_skill(
114        mut self,
115        skill_id: impl Into<SkillId>,
116        skill_name: impl Into<String>,
117    ) -> Self {
118        self.metadata.skill_id = Some(skill_id.into());
119        self.metadata.skill_name = Some(skill_name.into());
120        self
121    }
122
123    pub fn to_response(&self) -> JsonValue {
124        use crate::artifacts::traits::ArtifactSchema;
125
126        let response = TableResponse {
127            artifact_type: "table".to_owned(),
128            title: self.title.clone(),
129            columns: self.columns.clone(),
130            items: self.items.clone(),
131            count: self.items.len(),
132            execution_id: self.metadata.execution_id.clone(),
133            hints: Some(self.hints_builder.generate_schema()),
134        };
135        match serde_json::to_value(response) {
136            Ok(v) => v,
137            Err(e) => {
138                tracing::error!(error = %e, "Failed to serialize table response");
139                JsonValue::Null
140            },
141        }
142    }
143}
144
145impl Artifact for TableArtifact {
146    fn artifact_type(&self) -> ArtifactType {
147        ArtifactType::Table
148    }
149
150    fn to_schema(&self) -> JsonValue {
151        use crate::artifacts::traits::ArtifactSchema;
152
153        json!({
154            "type": "object",
155            "properties": {
156                "columns": {
157                    "type": "array",
158                    "description": "Column definitions"
159                },
160                "items": {
161                    "type": "array",
162                    "description": "Array of data records"
163                },
164                "count": {
165                    "type": "integer",
166                    "description": "Total number of records"
167                },
168                "_execution_id": {
169                    "type": "string",
170                    "description": "Execution ID for tracking"
171                }
172            },
173            "required": ["columns", "items"],
174            "x-artifact-type": "table",
175            "x-table-hints": self.hints_builder.generate_schema()
176        })
177    }
178}