Skip to main content

askit_std_agents/
file.rs

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// Glob Agent
20#[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// List Files Agent
72#[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// Read Text File Agent
138#[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// Write Text File Agent
197#[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        // Ensure parent directories exist
240        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}