Skip to content

Commit 11c8822

Browse files
fix: update fs_write tool to try to accomodate hallucinations, other UX changes for fs_read (#730)
Co-authored-by: Chay Nabors <[email protected]>
1 parent 983362b commit 11c8822

File tree

6 files changed

+83
-29
lines changed

6 files changed

+83
-29
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ serde = { version = "1.0.216", features = ["derive", "rc"] }
128128
serde_json = "1.0.115"
129129
sha2 = "0.10.6"
130130
shlex = "1.3.0"
131+
similar = "2.6.0"
131132
strum = { version = "0.26.3", features = ["derive"] }
132133
sysinfo = "0.32.0"
133134
thiserror = "2.0.3"

crates/q_cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ semver.workspace = true
6868
serde.workspace = true
6969
serde_json.workspace = true
7070
shlex.workspace = true
71+
similar.workspace = true
7172
spinners = "4.1.0"
7273
sysinfo.workspace = true
7374
tempfile.workspace = true

crates/q_cli/src/cli/chat/tools/fs_read.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use super::{
2626
OutputKind,
2727
format_path,
2828
sanitize_path_tool_arg,
29-
stylize_output_if_able,
3029
};
3130

3231
#[derive(Debug, Clone, Deserialize)]
@@ -52,6 +51,7 @@ impl FsRead {
5251
pub async fn invoke(&self, ctx: &Context, updates: &mut impl Write) -> Result<InvokeOutput> {
5352
let path = sanitize_path_tool_arg(ctx, &self.path);
5453
let is_file = ctx.fs().symlink_metadata(&path).await?.is_file();
54+
let relative_path = format_path(ctx.env().current_dir()?, &path);
5555
debug!(?path, is_file, "Reading");
5656

5757
if is_file {
@@ -84,8 +84,22 @@ impl FsRead {
8484
.collect::<Vec<_>>()
8585
.join("\n");
8686

87-
let formatted = stylize_output_if_able(ctx, &path, file_contents.as_str(), Some(start + 1), None);
88-
queue!(updates, style::Print(formatted), style::ResetColor, style::Print("\n"))?;
87+
queue!(
88+
updates,
89+
style::Print("Reading: "),
90+
style::SetForegroundColor(Color::Green),
91+
style::Print(relative_path),
92+
style::ResetColor,
93+
style::Print(", lines "),
94+
style::SetForegroundColor(Color::Green),
95+
style::Print(start + 1),
96+
style::ResetColor,
97+
style::Print("-"),
98+
style::SetForegroundColor(Color::Green),
99+
style::Print(end + 1),
100+
style::ResetColor,
101+
style::Print("\n"),
102+
)?;
89103

90104
// TODO: We copy and paste this below so the control flow needs work in this tool
91105
let byte_count = file_contents.len();
@@ -108,13 +122,11 @@ impl FsRead {
108122
);
109123
}
110124

111-
let file_text = stylize_output_if_able(ctx, path, file.as_str(), None, None);
112125
queue!(
113126
updates,
127+
style::Print("Reading: "),
114128
style::SetForegroundColor(Color::Green),
115-
style::Print("Reading:\n"),
116-
style::ResetColor,
117-
style::Print(file_text),
129+
style::Print(relative_path),
118130
style::ResetColor,
119131
style::Print("\n"),
120132
)?;
@@ -135,8 +147,9 @@ impl FsRead {
135147
let relative_path = format_path(&cwd, &path);
136148
queue!(
137149
updates,
150+
style::Print("Reading: "),
138151
style::SetForegroundColor(Color::Green),
139-
style::Print(format!("Reading directory {}", &relative_path)),
152+
style::Print(&relative_path),
140153
style::ResetColor,
141154
style::Print("\n"),
142155
)?;

crates/q_cli/src/cli/chat/tools/fs_write.rs

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use eyre::{
1313
};
1414
use fig_os_shim::Context;
1515
use serde::Deserialize;
16+
use tracing::warn;
1617

1718
use super::{
1819
InvokeOutput,
@@ -24,8 +25,14 @@ use super::{
2425
#[derive(Debug, Clone, Deserialize)]
2526
#[serde(tag = "command")]
2627
pub enum FsWrite {
28+
/// The tool spec should only require `file_text`, but the model sometimes doesn't want to
29+
/// provide it. Thus, including `new_str` as a fallback check, if it's available.
2730
#[serde(rename = "create")]
28-
Create { path: String, file_text: String },
31+
Create {
32+
path: String,
33+
file_text: Option<String>,
34+
new_str: Option<String>,
35+
},
2936
#[serde(rename = "str_replace")]
3037
StrReplace {
3138
path: String,
@@ -45,18 +52,15 @@ impl FsWrite {
4552
let fs = ctx.fs();
4653
let cwd = ctx.env().current_dir()?;
4754
match self {
48-
FsWrite::Create { path, file_text } => {
55+
FsWrite::Create { path, .. } => {
56+
let file_text = self.canonical_create_command_text();
4957
let path = sanitize_path_tool_arg(ctx, path);
50-
let relative_path = format_path(cwd, fs.chroot_path(&path));
51-
let invoke_description = if fs.exists(&path) {
52-
"Replacing the current file contents at"
53-
} else {
54-
"Creating a new file at"
55-
};
58+
let invoke_description = if fs.exists(&path) { "Replacing: " } else { "Creating: " };
5659
queue!(
5760
updates,
61+
style::Print(invoke_description),
5862
style::SetForegroundColor(Color::Green),
59-
style::Print(format!("{} {}", invoke_description, relative_path)),
63+
style::Print(format_path(cwd, &path)),
6064
style::ResetColor,
6165
style::Print("\n"),
6266
)?;
@@ -69,8 +73,9 @@ impl FsWrite {
6973
let matches = file.match_indices(old_str).collect::<Vec<_>>();
7074
queue!(
7175
updates,
76+
style::Print("Updating: "),
7277
style::SetForegroundColor(Color::Green),
73-
style::Print(format!("Updating {}", format_path(cwd, &path))),
78+
style::Print(format_path(cwd, &path)),
7479
style::ResetColor,
7580
style::Print("\n"),
7681
)?;
@@ -89,19 +94,16 @@ impl FsWrite {
8994
insert_line,
9095
new_str,
9196
} => {
97+
let path = sanitize_path_tool_arg(ctx, path);
98+
let mut file = fs.read_to_string(&path).await?;
9299
queue!(
93100
updates,
101+
style::Print("Updating: "),
94102
style::SetForegroundColor(Color::Green),
95-
style::Print(format!(
96-
"Inserting at line {} in {}",
97-
insert_line,
98-
format_path(cwd, path)
99-
)),
103+
style::Print(format_path(cwd, &path)),
100104
style::ResetColor,
101105
style::Print("\n"),
102106
)?;
103-
let path = fs.chroot_path_str(path);
104-
let mut file = fs.read_to_string(&path).await?;
105107

106108
// Get the index of the start of the line to insert at.
107109
let num_lines = file.lines().enumerate().map(|(i, _)| i + 1).last().unwrap_or(1);
@@ -121,7 +123,8 @@ impl FsWrite {
121123
pub fn queue_description(&self, ctx: &Context, updates: &mut impl Write) -> Result<()> {
122124
let cwd = ctx.env().current_dir()?;
123125
match self {
124-
FsWrite::Create { path, file_text } => {
126+
FsWrite::Create { path, .. } => {
127+
let file_text = self.canonical_create_command_text();
125128
let relative_path = format_path(cwd, path);
126129
queue!(
127130
updates,
@@ -134,7 +137,7 @@ impl FsWrite {
134137
if ctx.fs().exists(path) {
135138
let prev = ctx.fs().read_to_string_sync(path)?;
136139
let prev = stylize_output_if_able(ctx, &relative_path, prev.as_str(), None, Some("-"));
137-
let new = stylize_output_if_able(ctx, &relative_path, file_text, None, Some("+"));
140+
let new = stylize_output_if_able(ctx, &relative_path, &file_text, None, Some("+"));
138141
queue!(
139142
updates,
140143
style::Print("Replacing:\n"),
@@ -147,7 +150,7 @@ impl FsWrite {
147150
style::Print("\n\n")
148151
)?;
149152
} else {
150-
let file = stylize_output_if_able(ctx, &relative_path, file_text, None, None);
153+
let file = stylize_output_if_able(ctx, &relative_path, &file_text, None, None);
151154
queue!(
152155
updates,
153156
style::Print("Contents:\n"),
@@ -223,6 +226,25 @@ impl FsWrite {
223226

224227
Ok(())
225228
}
229+
230+
/// Returns the text to use for the [FsWrite::Create] command. This is required since we can't
231+
/// rely on the model always providing `file_text`.
232+
fn canonical_create_command_text(&self) -> String {
233+
match self {
234+
FsWrite::Create { file_text, new_str, .. } => match (file_text, new_str) {
235+
(Some(file_text), _) => file_text.clone(),
236+
(None, Some(new_str)) => {
237+
warn!("required field `file_text` is missing, using the provided `new_str` instead");
238+
new_str.clone()
239+
},
240+
_ => {
241+
warn!("no content provided for the create command");
242+
String::new()
243+
},
244+
},
245+
_ => String::new(),
246+
}
247+
}
226248
}
227249

228250
/// Limits the passed str to `max_len`.
@@ -358,6 +380,20 @@ mod tests {
358380
.unwrap();
359381

360382
assert_eq!(ctx.fs().read_to_string("/my-file").await.unwrap(), file_text);
383+
384+
let file_text = "This is a new string";
385+
let v = serde_json::json!({
386+
"path": "/my-file",
387+
"command": "create",
388+
"new_str": file_text
389+
});
390+
serde_json::from_value::<FsWrite>(v)
391+
.unwrap()
392+
.invoke(&ctx, &mut stdout)
393+
.await
394+
.unwrap();
395+
396+
assert_eq!(ctx.fs().read_to_string("/my-file").await.unwrap(), file_text);
361397
}
362398

363399
#[tokio::test]

crates/q_cli/src/cli/chat/tools/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,9 @@ fn sanitize_path_tool_arg(ctx: &Context, path: impl AsRef<Path>) -> PathBuf {
224224
Some(p) => res.push(p),
225225
None => return res,
226226
}
227-
res.push(path);
227+
for p in path {
228+
res.push(p);
229+
}
228230
// For testing scenarios, we need to make sure paths are appropriately handled in chroot test
229231
// file systems since they are passed directly from the model.
230232
ctx.fs().chroot_path(res)

0 commit comments

Comments
 (0)