Skip to main content

llimorse_tools/
file.rs

1//! File tools.
2
3use super::ToolGate;
4use anyhow::{Result, anyhow, bail};
5use helpers::TruncatedDisplay;
6use llimorse::{Agent, CallableTool};
7use std::fmt;
8use std::fs::{self, File};
9use std::io::{self, BufRead, BufReader, Read as _, Seek as _, Write as _};
10use std::path::PathBuf;
11
12llimorse::tool! {
13    'name: "view";
14
15    /// Read the given file, in whole or in part.
16    #[derive(Debug)]
17    'params: pub struct ViewParams {
18        /// Filename to read
19        filename: PathBuf,
20
21        /// Range of lines to read: First line and last line (inclusive). Full file if not
22        /// specified.
23        range: Option<(usize, usize)>,
24    }
25
26    /// Result of reading a range from a file.
27    #[derive(Debug)]
28    'result: pub struct ViewResult {
29        /// Filename from which data was read
30        filename: PathBuf,
31
32        /// Requested content
33        content: String,
34    }
35
36    /// Allow reading files (unrestricted), in whole or in part.
37    #[derive(Debug)]
38    'state: pub struct View<G: ToolGate> {
39        /// Gate for receiving permissions to read files
40        gate: G,
41    }
42}
43
44impl fmt::Display for ViewParams {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "filename={}", self.filename.display())?;
47
48        if let Some((start, end)) = self.range {
49            write!(f, " range=[{start}, {end}]")?
50        }
51
52        Ok(())
53    }
54}
55
56impl fmt::Display for ViewResult {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(
59            f,
60            "filename={} content={:?}",
61            self.filename.display(),
62            self.content.truncated_display(100)
63        )
64    }
65}
66
67impl<G: ToolGate> View<G> {
68    /// Allow viewing files (anywhere)
69    pub fn new(gate: G) -> Self {
70        View { gate }
71    }
72}
73
74impl<G: ToolGate> CallableTool for View<G> {
75    async fn execute(&self, _agent: &Agent, params: ViewParams) -> Result<ViewResult> {
76        self.gate
77            .permitted(&params)
78            .await
79            .map_err(|e| anyhow!("View tool call rejected: {e}"))?;
80
81        let file = File::open(&params.filename)
82            .map_err(|err| anyhow!("Failed to open {}: {err}", params.filename.display()))?;
83        let reader = BufReader::new(file);
84
85        let range = params.range.unwrap_or((1, usize::MAX));
86        let skip = range
87            .0
88            .checked_sub(1) // ranges are 1-based
89            .ok_or_else(|| anyhow!("Range {range:?}: First line is 1, not 0"))?;
90
91        let take = range
92            .1
93            .checked_sub(range.0)
94            .ok_or_else(|| {
95                anyhow!(
96                    "Range end must be greater or equal to the range start: {} >= {}",
97                    range.1,
98                    range.0
99                )
100            })?
101            .saturating_add(1); // end is inclusive, so 0 diff means read 1 line
102
103        let content = reader
104            .lines()
105            .skip(skip)
106            .take(take)
107            .try_fold(String::new(), |mut acc, line| -> Result<String> {
108                if !acc.is_empty() {
109                    acc.push('\n');
110                }
111                acc.push_str(&line?);
112                Ok(acc)
113            })
114            .map_err(|err| anyhow!("Failed to read {}: {err}", params.filename.display()))?;
115
116        Ok(ViewResult {
117            filename: params.filename,
118            content,
119        })
120    }
121}
122
123llimorse::tool! {
124    'name: "write";
125
126    /// Overwrite a file in full.
127    #[derive(Debug)]
128    'params: pub struct WriteParams {
129        /// Filename to overwrite
130        filename: PathBuf,
131
132        /// Full content to write into the file
133        content: String,
134    }
135
136    /// Result of overwriting a file in full.
137    #[derive(Debug)]
138    'result: pub struct WriteResult {
139        /// Filename into which the data was written
140        filename: PathBuf,
141
142        /// How much data was written
143        bytes_written: usize,
144    }
145
146    /// Overwrite files (unrestricted) in whole
147    #[derive(Debug)]
148    'state: pub struct Write<G: ToolGate> {
149        /// Gate for receiving permissions to overwrite files
150        gate: G,
151    }
152}
153
154impl fmt::Display for WriteParams {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(
157            f,
158            "filename={} content={:?}",
159            self.filename.display(),
160            self.content.truncated_display(100)
161        )
162    }
163}
164
165impl fmt::Display for WriteResult {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(
168            f,
169            "filename={} bytes_written={}",
170            self.filename.display(),
171            self.bytes_written
172        )
173    }
174}
175
176impl<G: ToolGate> Write<G> {
177    /// Overwrite files (anywhere) in whole
178    pub fn new(gate: G) -> Self {
179        Write { gate }
180    }
181}
182
183impl<G: ToolGate> CallableTool for Write<G> {
184    async fn execute(&self, _agent: &Agent, params: WriteParams) -> Result<WriteResult> {
185        self.gate
186            .permitted(&params)
187            .await
188            .map_err(|e| anyhow!("Write tool call rejected: {e}"))?;
189
190        let mut file = fs::OpenOptions::new()
191            .write(true)
192            .create(true)
193            .truncate(true)
194            .open(&params.filename)
195            .map_err(|err| anyhow!("Failed to open {}: {err}", params.filename.display()))?;
196
197        let len = params.content.len(); // byte length
198        file.write_all(params.content.as_bytes())
199            .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
200
201        Ok(WriteResult {
202            filename: params.filename,
203            bytes_written: len,
204        })
205    }
206}
207
208llimorse::tool! {
209    'name: "edit";
210
211    /// Substitute a string in a file
212    #[derive(Debug)]
213    'params: pub struct EditParams {
214        /// File to edit
215        filename: PathBuf,
216
217        /// Old string, the one to replace
218        old_content: String,
219
220        /// New string to replace the old one
221        new_content: String,
222    }
223
224    /// Result of editing a file
225    #[derive(Debug)]
226    'result: pub struct EditResult {
227        /// Filename into which the data was written
228        filename: PathBuf,
229
230        /// Size difference from before and after
231        byte_size_changed: isize,
232    }
233
234    /// Substitute strings in files (unrestricted)
235    #[derive(Debug)]
236    'state: pub struct Edit<G: ToolGate> {
237        /// Gate for receiving permissions to edit files
238        gate: G,
239    }
240}
241
242impl fmt::Display for EditParams {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        write!(
245            f,
246            "filename={} {:?} -> {:?}",
247            self.filename.display(),
248            self.old_content.truncated_display(100),
249            self.new_content.truncated_display(100)
250        )
251    }
252}
253
254impl fmt::Display for EditResult {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        write!(
257            f,
258            "filename={} byte_size_changed={}",
259            self.filename.display(),
260            self.byte_size_changed
261        )
262    }
263}
264
265impl<G: ToolGate> Edit<G> {
266    /// Substitute strings in files (unrestricted)
267    pub fn new(gate: G) -> Self {
268        Edit { gate }
269    }
270}
271
272impl<G: ToolGate> CallableTool for Edit<G> {
273    async fn execute(&self, _agent: &Agent, params: EditParams) -> Result<EditResult> {
274        self.gate
275            .permitted(&params)
276            .await
277            .map_err(|e| anyhow!("Edit tool call rejected: {e}"))?;
278
279        let mut file = fs::OpenOptions::new()
280            .read(true)
281            .write(true)
282            .open(&params.filename)
283            .map_err(|err| anyhow!("Failed to open {}: {err}", params.filename.display()))?;
284
285        let mut content = String::new();
286        file.read_to_string(&mut content)
287            .map_err(|err| anyhow!("Failed to read from {}: {err}", params.filename.display()))?;
288
289        let first_match = content.find(&params.old_content).ok_or_else(|| {
290            anyhow!(
291                "Did not find old_content string in {}",
292                params.filename.display()
293            )
294        })?;
295
296        let end_match = first_match + params.old_content.len();
297        if content[end_match..].find(&params.old_content).is_some() {
298            bail!(
299                "Multiple old_content matches in {}",
300                params.filename.display()
301            )
302        }
303
304        file.set_len(first_match as u64)
305            .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
306        file.seek(io::SeekFrom::End(0))
307            .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
308        file.write_all(params.new_content.as_bytes())
309            .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
310        file.write_all(&content.as_bytes()[end_match..])
311            .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
312
313        let byte_size_changed =
314            params.new_content.len() as isize - params.old_content.len() as isize;
315        Ok(EditResult {
316            filename: params.filename,
317            byte_size_changed,
318        })
319    }
320}