read_file_tool.rs

  1use std::sync::Arc;
  2
  3use crate::code_symbols_tool::file_outline;
  4use crate::schema::json_schema_for;
  5use anyhow::{Result, anyhow};
  6use assistant_tool::{ActionLog, Tool};
  7use gpui::{App, Entity, Task};
  8use itertools::Itertools;
  9use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 10use project::Project;
 11use schemars::JsonSchema;
 12use serde::{Deserialize, Serialize};
 13use ui::IconName;
 14use util::markdown::MarkdownString;
 15
 16/// If the model requests to read a file whose size exceeds this, then
 17/// the tool will return an error along with the model's symbol outline,
 18/// and suggest trying again using line ranges from the outline.
 19const MAX_FILE_SIZE_TO_READ: usize = 4096;
 20
 21#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 22pub struct ReadFileToolInput {
 23    /// The relative path of the file to read.
 24    ///
 25    /// This path should never be absolute, and the first component
 26    /// of the path should always be a root directory in a project.
 27    ///
 28    /// <example>
 29    /// If the project has the following root directories:
 30    ///
 31    /// - directory1
 32    /// - directory2
 33    ///
 34    /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
 35    /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
 36    /// </example>
 37    pub path: String,
 38
 39    /// Optional line number to start reading on (1-based index)
 40    #[serde(default)]
 41    pub start_line: Option<usize>,
 42
 43    /// Optional line number to end reading on (1-based index)
 44    #[serde(default)]
 45    pub end_line: Option<usize>,
 46}
 47
 48pub struct ReadFileTool;
 49
 50impl Tool for ReadFileTool {
 51    fn name(&self) -> String {
 52        "read_file".into()
 53    }
 54
 55    fn needs_confirmation(&self) -> bool {
 56        false
 57    }
 58
 59    fn description(&self) -> String {
 60        include_str!("./read_file_tool/description.md").into()
 61    }
 62
 63    fn icon(&self) -> IconName {
 64        IconName::FileSearch
 65    }
 66
 67    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
 68        json_schema_for::<ReadFileToolInput>(format)
 69    }
 70
 71    fn ui_text(&self, input: &serde_json::Value) -> String {
 72        match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
 73            Ok(input) => {
 74                let path = MarkdownString::inline_code(&input.path);
 75                match (input.start_line, input.end_line) {
 76                    (Some(start), None) => format!("Read file {path} (from line {start})"),
 77                    (Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"),
 78                    _ => format!("Read file {path}"),
 79                }
 80            }
 81            Err(_) => "Read file".to_string(),
 82        }
 83    }
 84
 85    fn run(
 86        self: Arc<Self>,
 87        input: serde_json::Value,
 88        _messages: &[LanguageModelRequestMessage],
 89        project: Entity<Project>,
 90        action_log: Entity<ActionLog>,
 91        cx: &mut App,
 92    ) -> Task<Result<String>> {
 93        let input = match serde_json::from_value::<ReadFileToolInput>(input) {
 94            Ok(input) => input,
 95            Err(err) => return Task::ready(Err(anyhow!(err))),
 96        };
 97
 98        let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
 99            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,)));
100        };
101
102        let file_path = input.path.clone();
103        cx.spawn(async move |cx| {
104            let buffer = cx
105                .update(|cx| {
106                    project.update(cx, |project, cx| project.open_buffer(project_path, cx))
107                })?
108                .await?;
109
110            // Check if specific line ranges are provided
111            if input.start_line.is_some() || input.end_line.is_some() {
112                let result = buffer.read_with(cx, |buffer, _cx| {
113                    let text = buffer.text();
114                    let start = input.start_line.unwrap_or(1);
115                    let lines = text.split('\n').skip(start - 1);
116                    if let Some(end) = input.end_line {
117                        let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
118                        Itertools::intersperse(lines.take(count), "\n").collect()
119                    } else {
120                        Itertools::intersperse(lines, "\n").collect()
121                    }
122                })?;
123
124                action_log.update(cx, |log, cx| {
125                    log.buffer_read(buffer, cx);
126                })?;
127
128                Ok(result)
129            } else {
130                // No line ranges specified, so check file size to see if it's too big.
131                let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
132
133                if file_size <= MAX_FILE_SIZE_TO_READ {
134                    // File is small enough, so return its contents.
135                    let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
136
137                    action_log.update(cx, |log, cx| {
138                        log.buffer_read(buffer, cx);
139                    })?;
140
141                    Ok(result)
142                } else {
143                    // File is too big, so return an error with the outline
144                    // and a suggestion to read again with line numbers.
145                    let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
146
147                    Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline."))
148                }
149            }
150        })
151    }
152}