1use std::fs;
2use std::path::Path;
3
4use agent_stream_kit::{
5 ASKit, AgentContext, AgentData, AgentError, AgentOutput, AgentSpec, AgentValue, AsAgent,
6 askit_agent, async_trait,
7};
8use glob::glob;
9use im::hashmap;
10
11const CATEGORY: &str = "Std/File";
12
13const PIN_DATA: &str = "data";
14const PIN_DOC: &str = "doc";
15const PIN_FILES: &str = "files";
16const PIN_PATH: &str = "path";
17const PIN_STRING: &str = "string";
18
19#[askit_agent(
21 title = "Glob",
22 category = CATEGORY,
23 inputs = [PIN_PATH],
24 outputs = [PIN_FILES]
25)]
26struct GlobAgent {
27 data: AgentData,
28}
29
30#[async_trait]
31impl AsAgent for GlobAgent {
32 fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
33 Ok(Self {
34 data: AgentData::new(askit, id, spec),
35 })
36 }
37
38 async fn process(
39 &mut self,
40 ctx: AgentContext,
41 _pin: String,
42 value: AgentValue,
43 ) -> Result<(), AgentError> {
44 let pat = value
45 .as_str()
46 .ok_or_else(|| AgentError::InvalidValue("not a string".to_string()))?;
47
48 let mut files = Vec::new();
49
50 for entry in glob(pat).map_err(|e| {
51 AgentError::InvalidValue(format!("Failed to read glob pattern {}: {}", pat, e))
52 })? {
53 match entry {
54 Ok(path) => {
55 files.push(path.to_string_lossy().to_string().into());
56 }
57 Err(e) => {
58 return Err(AgentError::InvalidValue(format!(
59 "Failed to read glob entry: {}",
60 e
61 )));
62 }
63 }
64 }
65
66 let out_value = AgentValue::array(files.into());
67 self.output(ctx, PIN_FILES, out_value).await
68 }
69}
70
71#[askit_agent(
73 title = "List Files",
74 category = CATEGORY,
75 inputs = [PIN_PATH],
76 outputs = [PIN_FILES]
77)]
78struct ListFilesAgent {
79 data: AgentData,
80}
81
82#[async_trait]
83impl AsAgent for ListFilesAgent {
84 fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
85 Ok(Self {
86 data: AgentData::new(askit, id, spec),
87 })
88 }
89
90 async fn process(
91 &mut self,
92 ctx: AgentContext,
93 _pin: String,
94 value: AgentValue,
95 ) -> Result<(), AgentError> {
96 let path = value
97 .as_str()
98 .ok_or_else(|| AgentError::InvalidValue("path is not a string".to_string()))?;
99 let path = Path::new(path);
100
101 if !path.exists() {
102 return Err(AgentError::InvalidValue(format!(
103 "Path does not exist: {}",
104 path.display()
105 )));
106 }
107
108 if !path.is_dir() {
109 return Err(AgentError::InvalidValue(format!(
110 "Path is not a directory: {}",
111 path.display()
112 )));
113 }
114
115 let mut files = Vec::new();
116 let entries = fs::read_dir(path).map_err(|e| {
117 AgentError::InvalidValue(format!(
118 "Failed to read directory {}: {}",
119 path.display(),
120 e
121 ))
122 })?;
123
124 for entry in entries {
125 let entry = entry.map_err(|e| {
126 AgentError::InvalidValue(format!("Failed to read directory entry: {}", e))
127 })?;
128 let file_name = entry.file_name().to_string_lossy().to_string();
129 files.push(file_name.into());
130 }
131
132 let out_value = AgentValue::array(files.into());
133 self.output(ctx, PIN_FILES, out_value).await
134 }
135}
136
137#[askit_agent(
139 title = "Read Text File",
140 category = CATEGORY,
141 inputs = [PIN_PATH],
142 outputs = [PIN_STRING, PIN_DOC]
143)]
144struct ReadTextFileAgent {
145 data: AgentData,
146}
147
148#[async_trait]
149impl AsAgent for ReadTextFileAgent {
150 fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
151 Ok(Self {
152 data: AgentData::new(askit, id, spec),
153 })
154 }
155
156 async fn process(
157 &mut self,
158 ctx: AgentContext,
159 _pin: String,
160 value: AgentValue,
161 ) -> Result<(), AgentError> {
162 let path = value
163 .as_str()
164 .ok_or_else(|| AgentError::InvalidValue("path is not a string".into()))?;
165 let path = Path::new(path);
166
167 if !path.exists() {
168 return Err(AgentError::InvalidValue(format!(
169 "Path does not exist: {}",
170 path.display()
171 )));
172 }
173
174 if !path.is_file() {
175 return Err(AgentError::InvalidValue(format!(
176 "Path is not a file: {}",
177 path.display()
178 )));
179 }
180
181 let content = fs::read_to_string(path).map_err(|e| {
182 AgentError::InvalidValue(format!("Failed to read file {}: {}", path.display(), e))
183 })?;
184
185 let text = AgentValue::string(content);
186 self.output(ctx.clone(), PIN_STRING, text.clone()).await?;
187
188 let out_doc = AgentValue::object(hashmap! {
189 "path".into() => AgentValue::string(path.to_string_lossy().to_string()),
190 "text".into() => text,
191 });
192 self.output(ctx, PIN_DOC, out_doc).await
193 }
194}
195
196#[askit_agent(
198 title = "Write Text File",
199 category = CATEGORY,
200 inputs = [PIN_DATA],
201 outputs = [PIN_DATA]
202)]
203struct WriteTextFileAgent {
204 data: AgentData,
205}
206
207#[async_trait]
208impl AsAgent for WriteTextFileAgent {
209 fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
210 Ok(Self {
211 data: AgentData::new(askit, id, spec),
212 })
213 }
214
215 async fn process(
216 &mut self,
217 ctx: AgentContext,
218 _pin: String,
219 value: AgentValue,
220 ) -> Result<(), AgentError> {
221 let input = value
222 .as_object()
223 .ok_or_else(|| AgentError::InvalidValue("Input is not an object".into()))?;
224
225 let path = input
226 .get("path")
227 .ok_or_else(|| AgentError::InvalidValue("Missing 'path' in input".into()))?
228 .as_str()
229 .ok_or_else(|| AgentError::InvalidValue("'path' is not a string".into()))?;
230
231 let text = input
232 .get("text")
233 .ok_or_else(|| AgentError::InvalidValue("Missing 'text' in input".into()))?
234 .as_str()
235 .ok_or_else(|| AgentError::InvalidValue("'text' is not a string".into()))?;
236
237 let path = Path::new(path);
238
239 if let Some(parent) = path.parent() {
241 if !parent.exists() {
242 fs::create_dir_all(parent).map_err(|e| {
243 AgentError::InvalidValue(format!("Failed to create parent directories: {}", e))
244 })?
245 }
246 }
247
248 fs::write(path, text).map_err(|e| {
249 AgentError::InvalidValue(format!("Failed to write file {}: {}", path.display(), e))
250 })?;
251
252 self.output(ctx, PIN_DATA, value).await
253 }
254}