systemprompt_models/artifacts/
cli.rs1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6use std::collections::HashMap;
7use thiserror::Error;
8
9use super::list::ListItem;
10use super::table::Column;
11use super::types::ColumnType;
12use super::{CopyPasteTextArtifact, DashboardArtifact, ListArtifact, TableArtifact, TextArtifact};
13use crate::execution::context::RequestContext;
14
15#[derive(Debug, Error)]
16pub enum ConversionError {
17 #[error("Missing columns hint for table artifact")]
18 MissingColumns,
19
20 #[error("No array found in data for table/list conversion")]
21 NoArrayFound,
22
23 #[error("JSON serialization error: {0}")]
24 Json(#[from] serde_json::Error),
25
26 #[error("Unsupported artifact type: {0}")]
27 UnsupportedType(String),
28}
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
31#[serde(rename_all = "snake_case")]
32pub enum CliArtifactType {
33 Table,
34 List,
35 PresentationCard,
36 Text,
37 CopyPasteText,
38 Chart,
39 Form,
40 Dashboard,
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
44pub struct RenderingHints {
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub columns: Option<Vec<String>>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub chart_type: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub theme: Option<String>,
51 #[serde(flatten)]
52 pub extra: HashMap<String, JsonValue>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
56pub struct CommandResultRaw {
57 pub data: JsonValue,
58 pub artifact_type: CliArtifactType,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub title: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub hints: Option<RenderingHints>,
63}
64
65impl CommandResultRaw {
66 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
67 serde_json::from_str(json)
68 }
69
70 pub fn from_value(value: JsonValue) -> Result<Self, serde_json::Error> {
71 serde_json::from_value(value)
72 }
73
74 pub fn to_cli_artifact(&self, ctx: &RequestContext) -> Result<CliArtifact, ConversionError> {
75 match self.artifact_type {
76 CliArtifactType::Table => self.convert_table(ctx),
77 CliArtifactType::List => self.convert_list(ctx),
78 CliArtifactType::CopyPasteText => Ok(self.convert_copy_paste_text(ctx)),
79 CliArtifactType::Text
80 | CliArtifactType::PresentationCard
81 | CliArtifactType::Dashboard
82 | CliArtifactType::Chart
83 | CliArtifactType::Form => Ok(self.convert_text(ctx)),
84 }
85 }
86
87 fn convert_table(&self, ctx: &RequestContext) -> Result<CliArtifact, ConversionError> {
88 let column_names = self
89 .hints
90 .as_ref()
91 .and_then(|h| h.columns.as_ref())
92 .ok_or(ConversionError::MissingColumns)?;
93
94 let items = extract_array_from_value(&self.data)?;
95
96 let columns: Vec<Column> = column_names
97 .iter()
98 .map(|name| Column::new(name, ColumnType::String))
99 .collect();
100
101 let artifact = TableArtifact::new(columns, ctx).with_rows(items);
102
103 Ok(CliArtifact::Table { artifact })
104 }
105
106 fn convert_list(&self, ctx: &RequestContext) -> Result<CliArtifact, ConversionError> {
107 let items = extract_array_from_value(&self.data)?;
108
109 let list_items: Vec<ListItem> = items
110 .iter()
111 .filter_map(|item| {
112 let title = item
113 .get("title")
114 .or_else(|| item.get("name"))
115 .and_then(|v| v.as_str())?;
116
117 let summary = item
118 .get("summary")
119 .or_else(|| item.get("description"))
120 .and_then(|v| v.as_str())
121 .unwrap_or("");
122
123 let link = item
124 .get("link")
125 .or_else(|| item.get("url"))
126 .or_else(|| item.get("id"))
127 .and_then(|v| v.as_str())
128 .unwrap_or("");
129
130 Some(ListItem::new(title, summary, link))
131 })
132 .collect();
133
134 let artifact = ListArtifact::new(ctx).with_items(list_items);
135
136 Ok(CliArtifact::List { artifact })
137 }
138
139 fn convert_text(&self, ctx: &RequestContext) -> CliArtifact {
140 let content = self
141 .data
142 .get("message")
143 .and_then(|v| v.as_str())
144 .map_or_else(
145 || {
146 serde_json::to_string_pretty(&self.data)
147 .unwrap_or_else(|_| self.data.to_string())
148 },
149 String::from,
150 );
151
152 let mut artifact = TextArtifact::new(&content, ctx);
153
154 if let Some(title) = &self.title {
155 artifact = artifact.with_title(title);
156 }
157
158 CliArtifact::Text { artifact }
159 }
160
161 fn convert_copy_paste_text(&self, ctx: &RequestContext) -> CliArtifact {
162 let content = self
163 .data
164 .get("content")
165 .or_else(|| self.data.get("message"))
166 .and_then(|v| v.as_str())
167 .map_or_else(
168 || {
169 serde_json::to_string_pretty(&self.data)
170 .unwrap_or_else(|_| self.data.to_string())
171 },
172 String::from,
173 );
174
175 let mut artifact = CopyPasteTextArtifact::new(&content, ctx);
176
177 if let Some(title) = &self.title {
178 artifact = artifact.with_title(title);
179 }
180
181 CliArtifact::CopyPasteText { artifact }
182 }
183}
184
185fn extract_array_from_value(value: &JsonValue) -> Result<Vec<JsonValue>, ConversionError> {
186 if let Some(arr) = value.as_array() {
187 return Ok(arr.clone());
188 }
189
190 if let Some(obj) = value.as_object() {
191 for v in obj.values() {
192 if let Some(arr) = v.as_array() {
193 return Ok(arr.clone());
194 }
195 }
196 }
197
198 Err(ConversionError::NoArrayFound)
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
202#[serde(tag = "artifact_type", rename_all = "snake_case")]
203pub enum CliArtifact {
204 Table {
205 #[serde(flatten)]
206 artifact: TableArtifact,
207 },
208 List {
209 #[serde(flatten)]
210 artifact: ListArtifact,
211 },
212 Text {
213 #[serde(flatten)]
214 artifact: TextArtifact,
215 },
216 #[serde(rename = "copy_paste_text")]
217 CopyPasteText {
218 #[serde(flatten)]
219 artifact: CopyPasteTextArtifact,
220 },
221 Dashboard {
222 #[serde(flatten)]
223 artifact: DashboardArtifact,
224 },
225}
226
227impl CliArtifact {
228 #[must_use]
229 pub const fn artifact_type_str(&self) -> &'static str {
230 match self {
231 Self::Table { .. } => "table",
232 Self::List { .. } => "list",
233 Self::Text { .. } => "text",
234 Self::CopyPasteText { .. } => "copy_paste_text",
235 Self::Dashboard { .. } => "dashboard",
236 }
237 }
238
239 #[must_use]
240 pub const fn table(artifact: TableArtifact) -> Self {
241 Self::Table { artifact }
242 }
243
244 #[must_use]
245 pub const fn list(artifact: ListArtifact) -> Self {
246 Self::List { artifact }
247 }
248
249 #[must_use]
250 pub const fn text(artifact: TextArtifact) -> Self {
251 Self::Text { artifact }
252 }
253
254 #[must_use]
255 pub const fn copy_paste_text(artifact: CopyPasteTextArtifact) -> Self {
256 Self::CopyPasteText { artifact }
257 }
258
259 #[must_use]
260 pub const fn dashboard(artifact: DashboardArtifact) -> Self {
261 Self::Dashboard { artifact }
262 }
263}