Skip to main content

systemprompt_models/artifacts/cli/
mod.rs

1//! CLI artifact envelope and raw-result conversion.
2//!
3//! [`CliArtifact`] is the tagged union of every renderable artifact a CLI
4//! command can emit (table, list, text, dashboard, chart, media, card).
5//! [`CommandResultRaw`] is the untyped intake shape — data plus a declared
6//! [`CliArtifactType`] and [`RenderingHints`] — converted into a
7//! [`CliArtifact`] by the [`conversion`] submodule. [`ConversionError`] covers
8//! the failure modes.
9
10pub mod conversion;
11
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_json::Value as JsonValue;
15use std::collections::HashMap;
16use thiserror::Error;
17
18use super::{
19    AudioArtifact, ChartArtifact, CopyPasteTextArtifact, DashboardArtifact, ImageArtifact,
20    ListArtifact, PresentationCardArtifact, TableArtifact, TextArtifact, VideoArtifact,
21};
22
23#[derive(Debug, Error)]
24pub enum ConversionError {
25    #[error("Missing columns hint for table artifact")]
26    MissingColumns,
27
28    #[error("No array found in data for table/list conversion")]
29    NoArrayFound,
30
31    #[error("JSON serialization error: {0}")]
32    Json(#[from] serde_json::Error),
33
34    #[error("Unsupported artifact type: {0}")]
35    UnsupportedType(String),
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
39#[serde(rename_all = "snake_case")]
40pub enum CliArtifactType {
41    Table,
42    List,
43    PresentationCard,
44    Text,
45    CopyPasteText,
46    Chart,
47    Form,
48    Dashboard,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
52pub struct RenderingHints {
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub columns: Option<Vec<String>>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub chart_type: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub theme: Option<String>,
59    #[serde(flatten)]
60    pub extra: HashMap<String, JsonValue>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
64pub struct CommandResultRaw {
65    pub data: JsonValue,
66    pub artifact_type: CliArtifactType,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub title: Option<String>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub hints: Option<RenderingHints>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
74#[serde(tag = "artifact_type", rename_all = "snake_case")]
75pub enum CliArtifact {
76    Table {
77        #[serde(flatten)]
78        artifact: TableArtifact,
79    },
80    List {
81        #[serde(flatten)]
82        artifact: ListArtifact,
83    },
84    Text {
85        #[serde(flatten)]
86        artifact: TextArtifact,
87    },
88    #[serde(rename = "copy_paste_text")]
89    CopyPasteText {
90        #[serde(flatten)]
91        artifact: CopyPasteTextArtifact,
92    },
93    Dashboard {
94        #[serde(flatten)]
95        artifact: DashboardArtifact,
96    },
97    Chart {
98        #[serde(flatten)]
99        artifact: ChartArtifact,
100    },
101    Audio {
102        #[serde(flatten)]
103        artifact: AudioArtifact,
104    },
105    Image {
106        #[serde(flatten)]
107        artifact: ImageArtifact,
108    },
109    Video {
110        #[serde(flatten)]
111        artifact: VideoArtifact,
112    },
113    #[serde(rename = "presentation_card")]
114    PresentationCard {
115        #[serde(flatten)]
116        artifact: PresentationCardArtifact,
117    },
118}
119
120impl CliArtifact {
121    #[must_use]
122    pub const fn artifact_type_str(&self) -> &'static str {
123        match self {
124            Self::Table { .. } => TableArtifact::ARTIFACT_TYPE_STR,
125            Self::List { .. } => ListArtifact::ARTIFACT_TYPE_STR,
126            Self::Text { .. } => TextArtifact::ARTIFACT_TYPE_STR,
127            Self::CopyPasteText { .. } => CopyPasteTextArtifact::ARTIFACT_TYPE_STR,
128            Self::Dashboard { .. } => DashboardArtifact::ARTIFACT_TYPE_STR,
129            Self::Chart { .. } => ChartArtifact::ARTIFACT_TYPE_STR,
130            Self::Audio { .. } => AudioArtifact::ARTIFACT_TYPE_STR,
131            Self::Image { .. } => ImageArtifact::ARTIFACT_TYPE_STR,
132            Self::Video { .. } => VideoArtifact::ARTIFACT_TYPE_STR,
133            Self::PresentationCard { .. } => PresentationCardArtifact::ARTIFACT_TYPE_STR,
134        }
135    }
136
137    #[must_use]
138    pub fn title(&self) -> Option<String> {
139        match self {
140            Self::Text { artifact } => artifact.title.clone(),
141            Self::CopyPasteText { artifact } => artifact.title.clone(),
142            Self::Dashboard { artifact } => Some(artifact.title.clone()),
143            Self::Audio { artifact } => artifact.title.clone(),
144            Self::PresentationCard { artifact } => Some(artifact.title.clone()),
145            Self::Table { .. }
146            | Self::List { .. }
147            | Self::Chart { .. }
148            | Self::Image { .. }
149            | Self::Video { .. } => None,
150        }
151    }
152
153    #[must_use]
154    pub const fn table(artifact: TableArtifact) -> Self {
155        Self::Table { artifact }
156    }
157
158    #[must_use]
159    pub const fn list(artifact: ListArtifact) -> Self {
160        Self::List { artifact }
161    }
162
163    #[must_use]
164    pub const fn text(artifact: TextArtifact) -> Self {
165        Self::Text { artifact }
166    }
167
168    #[must_use]
169    pub const fn copy_paste_text(artifact: CopyPasteTextArtifact) -> Self {
170        Self::CopyPasteText { artifact }
171    }
172
173    #[must_use]
174    pub const fn dashboard(artifact: DashboardArtifact) -> Self {
175        Self::Dashboard { artifact }
176    }
177
178    #[must_use]
179    pub const fn chart(artifact: ChartArtifact) -> Self {
180        Self::Chart { artifact }
181    }
182
183    #[must_use]
184    pub const fn audio(artifact: AudioArtifact) -> Self {
185        Self::Audio { artifact }
186    }
187
188    #[must_use]
189    pub const fn image(artifact: ImageArtifact) -> Self {
190        Self::Image { artifact }
191    }
192
193    #[must_use]
194    pub const fn video(artifact: VideoArtifact) -> Self {
195        Self::Video { artifact }
196    }
197
198    #[must_use]
199    pub const fn presentation_card(artifact: PresentationCardArtifact) -> Self {
200        Self::PresentationCard { artifact }
201    }
202}