Skip to main content

vv_agent/tools/
outputs.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7use crate::types::{ToolDirective, ToolExecutionResult, ToolResultStatus};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ToolOutput {
12    Text {
13        text: String,
14        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
15        metadata: BTreeMap<String, Value>,
16    },
17    Json {
18        data: Value,
19        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
20        metadata: BTreeMap<String, Value>,
21    },
22    Image {
23        url: Option<String>,
24        path: Option<PathBuf>,
25        mime_type: Option<String>,
26        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
27        metadata: BTreeMap<String, Value>,
28    },
29    File {
30        path: PathBuf,
31        mime_type: Option<String>,
32        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33        metadata: BTreeMap<String, Value>,
34    },
35    Error {
36        message: String,
37        error_code: Option<String>,
38        retryable: bool,
39        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
40        metadata: BTreeMap<String, Value>,
41    },
42}
43
44impl ToolOutput {
45    pub fn text(text: impl Into<String>) -> Self {
46        Self::Text {
47            text: text.into(),
48            metadata: BTreeMap::new(),
49        }
50    }
51
52    pub fn json(data: Value) -> Self {
53        Self::Json {
54            data,
55            metadata: BTreeMap::new(),
56        }
57    }
58
59    pub fn error(message: impl Into<String>) -> Self {
60        Self::Error {
61            message: message.into(),
62            error_code: None,
63            retryable: false,
64            metadata: BTreeMap::new(),
65        }
66    }
67
68    pub fn with_code(mut self, code: impl Into<String>) -> Self {
69        if let Self::Error {
70            error_code: field, ..
71        } = &mut self
72        {
73            *field = Some(code.into());
74        }
75        self
76    }
77
78    pub fn retryable(mut self, retryable: bool) -> Self {
79        if let Self::Error {
80            retryable: field, ..
81        } = &mut self
82        {
83            *field = retryable;
84        }
85        self
86    }
87
88    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
89        match &mut self {
90            Self::Text { metadata, .. }
91            | Self::Json { metadata, .. }
92            | Self::Image { metadata, .. }
93            | Self::File { metadata, .. }
94            | Self::Error { metadata, .. } => {
95                metadata.insert(key.into(), value);
96            }
97        }
98        self
99    }
100
101    pub fn to_result(&self, tool_call_id: impl Into<String>) -> ToolExecutionResult {
102        let tool_call_id = tool_call_id.into();
103        match self {
104            Self::Text { text, metadata } => {
105                let mut result = ToolExecutionResult::success(tool_call_id, text.clone());
106                result.metadata.extend(metadata.clone());
107                result
108            }
109            Self::Json { data, metadata } => {
110                let mut result = ToolExecutionResult::success(tool_call_id, data.to_string());
111                result
112                    .metadata
113                    .insert("output_type".to_string(), json!("json"));
114                result.metadata.extend(metadata.clone());
115                result
116            }
117            Self::Image {
118                url,
119                path,
120                mime_type,
121                metadata,
122            } => {
123                let mut result = ToolExecutionResult::success(
124                    tool_call_id,
125                    json!({
126                        "url": url,
127                        "path": path,
128                        "mime_type": mime_type,
129                    })
130                    .to_string(),
131                );
132                result
133                    .metadata
134                    .insert("output_type".to_string(), json!("image"));
135                result.image_url = url.clone();
136                result.image_path = path.as_ref().map(|path| path.display().to_string());
137                result.metadata.extend(metadata.clone());
138                result
139            }
140            Self::File {
141                path,
142                mime_type,
143                metadata,
144            } => {
145                let mut result = ToolExecutionResult::success(
146                    tool_call_id,
147                    json!({
148                        "path": path,
149                        "mime_type": mime_type,
150                    })
151                    .to_string(),
152                );
153                result
154                    .metadata
155                    .insert("output_type".to_string(), json!("file"));
156                result.metadata.extend(metadata.clone());
157                result
158            }
159            Self::Error {
160                message,
161                error_code,
162                retryable,
163                metadata,
164            } => ToolExecutionResult {
165                tool_call_id,
166                content: json!({
167                    "ok": false,
168                    "error": message,
169                    "error_code": error_code,
170                    "retryable": retryable,
171                })
172                .to_string(),
173                status: ToolResultStatus::Error,
174                directive: ToolDirective::Continue,
175                error_code: error_code.clone(),
176                metadata: [
177                    ("output_type".to_string(), json!("error")),
178                    ("retryable".to_string(), json!(retryable)),
179                ]
180                .into_iter()
181                .chain(metadata.clone())
182                .collect(),
183                image_url: None,
184                image_path: None,
185            },
186        }
187    }
188}