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    // JSON: Table rows are the tool's own row objects.
33    pub items: Vec<JsonValue>,
34    pub count: usize,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub execution_id: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    #[schemars(with = "Option<JsonValue>")]
39    // JSON: Free-form `_meta.hints` object the tool emitted; see `hints.rs`.
40    pub hints: Option<JsonValue>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
44pub struct TableArtifact {
45    #[serde(rename = "x-artifact-type")]
46    #[serde(default = "default_artifact_type")]
47    pub artifact_type: String,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub title: Option<String>,
50    pub columns: Vec<Column>,
51    // JSON: Table rows are the tool's own row objects.
52    pub items: Vec<JsonValue>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[schemars(with = "Option<JsonValue>")]
55    // JSON: Free-form `_meta.hints` object the tool emitted; see `hints.rs`.
56    pub hints: Option<JsonValue>,
57    #[serde(skip)]
58    #[schemars(skip)]
59    hints_builder: TableHints,
60    #[serde(skip)]
61    #[schemars(skip)]
62    metadata: ExecutionMetadata,
63}
64
65fn default_artifact_type() -> String {
66    "table".to_owned()
67}
68
69impl TableArtifact {
70    pub const ARTIFACT_TYPE_STR: &'static str = "table";
71
72    pub fn new(columns: Vec<Column>) -> Self {
73        Self {
74            artifact_type: "table".to_owned(),
75            title: None,
76            columns,
77            items: Vec::new(),
78            hints: None,
79            hints_builder: TableHints::default(),
80            metadata: ExecutionMetadata::default(),
81        }
82    }
83
84    pub fn with_request(mut self, ctx: &RequestContext) -> Self {
85        self.metadata = ExecutionMetadata::with_request(ctx);
86        self
87    }
88
89    #[must_use]
90    pub fn with_title(mut self, title: impl Into<String>) -> Self {
91        self.title = Some(title.into());
92        self
93    }
94
95    // JSON: Table rows are the tool's own row objects.
96    pub fn with_rows(mut self, items: Vec<JsonValue>) -> Self {
97        self.items = items;
98        self
99    }
100
101    pub fn with_hints(mut self, hints: TableHints) -> Self {
102        use crate::artifacts::traits::ArtifactSchema;
103        self.hints = Some(hints.generate_schema());
104        self.hints_builder = hints;
105        self
106    }
107
108    pub fn with_metadata(mut self, metadata: ExecutionMetadata) -> Self {
109        self.metadata = metadata;
110        self
111    }
112
113    pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
114        self.metadata.execution_id = Some(id.into());
115        self
116    }
117
118    pub fn with_skill(
119        mut self,
120        skill_id: impl Into<SkillId>,
121        skill_name: impl Into<String>,
122    ) -> Self {
123        self.metadata.skill_id = Some(skill_id.into());
124        self.metadata.skill_name = Some(skill_name.into());
125        self
126    }
127
128    // JSON: A2A `DataPart.data` payload.
129    pub fn to_response(&self) -> JsonValue {
130        use crate::artifacts::traits::ArtifactSchema;
131
132        let response = TableResponse {
133            artifact_type: "table".to_owned(),
134            title: self.title.clone(),
135            columns: self.columns.clone(),
136            items: self.items.clone(),
137            count: self.items.len(),
138            execution_id: self.metadata.execution_id.clone(),
139            hints: Some(self.hints_builder.generate_schema()),
140        };
141        match serde_json::to_value(response) {
142            Ok(v) => v,
143            Err(e) => {
144                tracing::error!(error = %e, "Failed to serialize table response");
145                JsonValue::Null
146            },
147        }
148    }
149}
150
151impl Artifact for TableArtifact {
152    fn artifact_type(&self) -> ArtifactType {
153        ArtifactType::Table
154    }
155
156    // JSON: JSON Schema document describing the artifact for the model.
157    fn to_schema(&self) -> JsonValue {
158        use crate::artifacts::traits::ArtifactSchema;
159
160        json!({
161            "type": "object",
162            "properties": {
163                "columns": {
164                    "type": "array",
165                    "description": "Column definitions"
166                },
167                "items": {
168                    "type": "array",
169                    "description": "Array of data records"
170                },
171                "count": {
172                    "type": "integer",
173                    "description": "Total number of records"
174                },
175                "_execution_id": {
176                    "type": "string",
177                    "description": "Execution ID for tracking"
178                }
179            },
180            "required": ["columns", "items"],
181            "x-artifact-type": "table",
182            "x-table-hints": self.hints_builder.generate_schema()
183        })
184    }
185}