Skip to main content

systemprompt_models/artifacts/cli/
conversion.rs

1//! Conversion from a raw command result into a typed CLI artifact.
2//!
3//! These impls turn a loosely-typed [`CommandResultRaw`] (data plus a declared
4//! [`CliArtifactType`] and rendering hints) into a concrete [`CliArtifact`],
5//! mapping each artifact type to its builder and reporting shape mismatches as
6//! [`ConversionError`].
7
8use serde_json::Value as JsonValue;
9
10use super::{CliArtifact, CliArtifactType, CommandResultRaw, ConversionError};
11use crate::artifacts::list::ListItem;
12use crate::artifacts::table::Column;
13use crate::artifacts::types::ColumnType;
14use crate::artifacts::{
15    CopyPasteTextArtifact, ListArtifact, PresentationCardArtifact, TableArtifact, TextArtifact,
16};
17use crate::execution::context::RequestContext;
18
19impl CommandResultRaw {
20    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
21        serde_json::from_str(json)
22    }
23
24    pub fn from_value(value: JsonValue) -> Result<Self, serde_json::Error> {
25        serde_json::from_value(value)
26    }
27
28    pub fn to_cli_artifact(&self, ctx: &RequestContext) -> Result<CliArtifact, ConversionError> {
29        match self.artifact_type {
30            CliArtifactType::Table => self.convert_table(ctx),
31            CliArtifactType::List => self.convert_list(ctx),
32            CliArtifactType::CopyPasteText => Ok(self.convert_copy_paste_text(ctx)),
33            CliArtifactType::PresentationCard => self.convert_presentation_card(),
34            CliArtifactType::Text
35            | CliArtifactType::Dashboard
36            | CliArtifactType::Chart
37            | CliArtifactType::Form => Ok(self.convert_text(ctx)),
38        }
39    }
40
41    fn convert_table(&self, ctx: &RequestContext) -> Result<CliArtifact, ConversionError> {
42        let column_names = self
43            .hints
44            .as_ref()
45            .and_then(|h| h.columns.as_ref())
46            .ok_or(ConversionError::MissingColumns)?;
47
48        let items = extract_array_from_value(&self.data)?;
49
50        let columns: Vec<Column> = column_names
51            .iter()
52            .map(|name| Column::new(name, ColumnType::String))
53            .collect();
54
55        let artifact = TableArtifact::new(columns, ctx).with_rows(items);
56
57        Ok(CliArtifact::Table { artifact })
58    }
59
60    fn convert_list(&self, ctx: &RequestContext) -> Result<CliArtifact, ConversionError> {
61        let items = extract_array_from_value(&self.data)?;
62
63        let list_items: Vec<ListItem> = items
64            .iter()
65            .filter_map(|item| {
66                let title = item
67                    .get("title")
68                    .or_else(|| item.get("name"))
69                    .and_then(|v| v.as_str())?;
70
71                let summary = item
72                    .get("summary")
73                    .or_else(|| item.get("description"))
74                    .and_then(|v| v.as_str())
75                    .unwrap_or("");
76
77                let link = item
78                    .get("link")
79                    .or_else(|| item.get("url"))
80                    .or_else(|| item.get("id"))
81                    .and_then(|v| v.as_str())
82                    .unwrap_or("");
83
84                Some(ListItem::new(title, summary, link))
85            })
86            .collect();
87
88        let artifact = ListArtifact::new(ctx).with_items(list_items);
89
90        Ok(CliArtifact::List { artifact })
91    }
92
93    fn convert_text(&self, ctx: &RequestContext) -> CliArtifact {
94        let content = self
95            .data
96            .get("message")
97            .and_then(|v| v.as_str())
98            .map_or_else(
99                || {
100                    serde_json::to_string_pretty(&self.data)
101                        .unwrap_or_else(|_| self.data.to_string())
102                },
103                String::from,
104            );
105
106        let mut artifact = TextArtifact::new(&content, ctx);
107
108        if let Some(title) = &self.title {
109            artifact = artifact.with_title(title);
110        }
111
112        CliArtifact::Text { artifact }
113    }
114
115    fn convert_copy_paste_text(&self, ctx: &RequestContext) -> CliArtifact {
116        let content = self
117            .data
118            .get("content")
119            .or_else(|| self.data.get("message"))
120            .and_then(|v| v.as_str())
121            .map_or_else(
122                || {
123                    serde_json::to_string_pretty(&self.data)
124                        .unwrap_or_else(|_| self.data.to_string())
125                },
126                String::from,
127            );
128
129        let mut artifact = CopyPasteTextArtifact::new(&content, ctx);
130
131        if let Some(title) = &self.title {
132            artifact = artifact.with_title(title);
133        }
134
135        CliArtifact::CopyPasteText { artifact }
136    }
137
138    fn convert_presentation_card(&self) -> Result<CliArtifact, ConversionError> {
139        let artifact: PresentationCardArtifact =
140            serde_json::from_value(self.data.clone()).map_err(ConversionError::Json)?;
141        Ok(CliArtifact::PresentationCard { artifact })
142    }
143}
144
145fn extract_array_from_value(value: &JsonValue) -> Result<Vec<JsonValue>, ConversionError> {
146    if let Some(arr) = value.as_array() {
147        return Ok(arr.clone());
148    }
149
150    if let Some(obj) = value.as_object() {
151        for v in obj.values() {
152            if let Some(arr) = v.as_array() {
153                return Ok(arr.clone());
154            }
155        }
156    }
157
158    Err(ConversionError::NoArrayFound)
159}