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