History

Add judge to new eval + provide LSP diagnostics (#28713)

Release Notes: - N/A ---------

Michael Sloan committed 1 year ago

Commit 6b80eb51 parent 2603f3611 files · +838 −84Browse files

Unified · Split

11 files
  1. Cargo.lock +11 −0
  2. crates/agent/src/thread.rs +10 −6
  3. crates/eval/.gitignore +3 −0
  4. crates/eval/Cargo.toml +12 −1
  5. crates/eval/examples/find_and_replace_diff_card/base.toml +2 −1
  6. crates/eval/examples/find_and_replace_diff_card/criteria.md +2 −0
  7. crates/eval/examples/find_and_replace_diff_card/rubric.md +0 −0
  8. crates/eval/src/eval.rs +235 −12
  9. crates/eval/src/example.rs +537 −63
  10. crates/eval/src/judge_prompt.hbs +25 −0
  11. crates/language_model/src/language_model.rs +1 −1
Cargo.lock+11−0
@@ -4878,25 +4878,36 @@ dependencies = [ · 4877 unmodified lines
48784878 "assistant_settings",
48794879 "assistant_tool",
48804880 "assistant_tools",
4881+ "async-watch",
4882+ "chrono",
4883+ "clap",
48814884 "client",
48824885 "context_server",
48834886 "dap",
48844887 "env_logger 0.11.8",
4888+ "extension",
48854889 "fs",
48864890 "futures 0.3.31",
48874891 "gpui",
48884892 "gpui_tokio",
4893+ "handlebars 4.5.0",
48894894 "language",
4895+ "language_extension",
48904896 "language_model",
48914897 "language_models",
4898+ "languages",
48924899 "node_runtime",
4900+ "paths",
48934901 "project",
48944902 "prompt_store",
48954903 "release_channel",
48964904 "reqwest_client",
48974905 "serde",
48984906 "settings",
4907+ "shellexpand 2.1.2",
48994908 "toml 0.8.20",
4909+ "unindent",
4910+ "util",
49004911 "workspace-hack",
49014912 ]
49024913
crates/agent/src/thread.rs+10−6
@@ -827,7 +827,7 @@ impl Thread { · 826 unmodified lines
827827 })
828828 .collect(),
829829 initial_project_snapshot,
830− cumulative_token_usage: this.cumulative_token_usage.clone(),
830+ cumulative_token_usage: this.cumulative_token_usage,
831831 detailed_summary_state: this.detailed_summary_state.clone(),
832832 exceeded_window_error: this.exceeded_window_error.clone(),
833833 })
@@ -1016,7 +1016,7 @@ impl Thread { · 182 unmodified lines
10161016 let task = cx.spawn(async move |thread, cx| {
10171017 let stream = model.stream_completion(request, &cx);
10181018 let initial_token_usage =
1019− thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage.clone());
1019+ thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
10201020 let stream_completion = async {
10211021 let mut events = stream.await?;
10221022 let mut stop_reason = StopReason::EndTurn;
@@ -1038,9 +1038,9 @@ impl Thread { · 15 unmodified lines
10381038 stop_reason = reason;
10391039 }
10401040 LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
1041− thread.cumulative_token_usage =
1042− thread.cumulative_token_usage.clone() + token_usage.clone()
1043− - current_token_usage.clone();
1041+ thread.cumulative_token_usage = thread.cumulative_token_usage
1042+ + token_usage
1043+ - current_token_usage;
10441044 current_token_usage = token_usage;
10451045 }
10461046 LanguageModelCompletionEvent::Text(chunk) => {
@@ -1183,7 +1183,7 @@ impl Thread { · 136 unmodified lines
11831183 thread.auto_capture_telemetry(cx);
11841184
11851185 if let Ok(initial_usage) = initial_token_usage {
1186− let usage = thread.cumulative_token_usage.clone() - initial_usage;
1186+ let usage = thread.cumulative_token_usage - initial_usage;
11871187
11881188 telemetry::event!(
11891189 "Assistant Thread Completion",
@@ -1862,6 +1862,10 @@ impl Thread { · 672 unmodified lines
18621862 .detach();
18631863 }
18641864
1865+ pub fn cumulative_token_usage(&self) -> TokenUsage {
1866+ self.cumulative_token_usage
1867+ }
1868+
18651869 pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
18661870 let model_registry = LanguageModelRegistry::read_global(cx);
18671871 let Some(model) = model_registry.default_model() else {
crates/eval/.gitignore+3−0
@@ -0,0 +1,3 @@
1+repos/
2+worktrees/
3+runs/
crates/eval/Cargo.toml+12−1
@@ -7,28 +7,39 @@ edition.workspace = true · 6 unmodified lines
77 [dependencies]
88 agent.workspace = true
99 anyhow.workspace = true
10+async-watch.workspace = true
11+assistant_settings.workspace = true
1012 assistant_tool.workspace = true
1113 assistant_tools.workspace = true
12−assistant_settings.workspace = true
14+chrono.workspace = true
15+clap.workspace = true
1316 client.workspace = true
1417 context_server.workspace = true
1518 dap.workspace = true
1619 env_logger.workspace = true
20+extension.workspace = true
1721 fs.workspace = true
1822 futures.workspace = true
1923 gpui.workspace = true
2024 gpui_tokio.workspace = true
25+handlebars.workspace = true
2126 language.workspace = true
27+language_extension.workspace = true
2228 language_model.workspace = true
2329 language_models.workspace = true
30+languages.workspace = true
2431 node_runtime.workspace = true
32+paths.workspace = true
2533 project.workspace = true
2634 prompt_store.workspace = true
2735 release_channel.workspace = true
2836 reqwest_client.workspace = true
2937 serde.workspace = true
3038 settings.workspace = true
39+shellexpand.workspace = true
3140 toml.workspace = true
41+unindent.workspace = true
42+util.workspace = true
3243 workspace-hack.workspace = true
3344
3445 [[bin]]
crates/eval/examples/find_and_replace_diff_card/base.toml+2−1
@@ -1,2 +1,3 @@
1−path = "../zed_worktree"
1+url = "https://github.com/zed-industries/zed.git"
22 revision = "38fcadf9481d018543c65f36ac3bafeba190179b"
3+language_extension = "rs"
crates/eval/examples/find_and_replace_diff_card/criteria.md+2−0
@@ -0,0 +1,2 @@
1+1. The changes must replace the previous output returned by `FindReplaceFileTool` with the new `ToolResult` struct. The struct should contain an `output` field that is the same as the string we were returning before, and a new `card` field that contains a view for the card
2+2. The card should be a view that displays a diff. Each line in the diff should be colored according to whether it was added, removed or unchanged.
crates/eval/examples/find_and_replace_diff_card/rubric.md+0−0

Empty file deleted.

crates/eval/src/eval.rs+235−12
@@ -1,32 +1,75 @@
11 mod example;
22
33 use assistant_settings::AssistantSettings;
4−use client::{Client, UserStore};
4+use client::{Client, ProxySettings, UserStore};
55 pub(crate) use example::*;
66
77 use ::fs::RealFs;
8−use anyhow::anyhow;
9−use gpui::{App, AppContext, Application, Entity, SemanticVersion, Task};
8+use anyhow::{Result, anyhow};
9+use clap::Parser;
10+use extension::ExtensionHostProxy;
11+use futures::future;
12+use gpui::http_client::{Uri, read_proxy_from_env};
13+use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, Task};
14+use gpui_tokio::Tokio;
1015 use language::LanguageRegistry;
1116 use language_model::{
1217 AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
1318 };
14−use node_runtime::NodeRuntime;
19+use node_runtime::{NodeBinaryOptions, NodeRuntime};
1520 use project::Project;
21+use project::project_settings::ProjectSettings;
1622 use prompt_store::PromptBuilder;
23+use release_channel::AppVersion;
1724 use reqwest_client::ReqwestClient;
1825 use settings::{Settings, SettingsStore};
26+use std::collections::HashSet;
27+use std::path::{Path, PathBuf};
1928 use std::sync::Arc;
29+use util::ResultExt as _;
30+
31+pub const RUNS_DIR: &str = "./crates/eval/runs";
32+
33+#[derive(Parser, Debug)]
34+#[command(name = "eval", disable_version_flag = true)]
35+struct Args {
36+ /// Runs all examples that contain these substrings. If unspecified, all examples are run.
37+ #[arg(value_name = "EXAMPLE_SUBSTRING")]
38+ examples: Vec<String>,
39+ /// Model to use (default: "claude-3-7-sonnet-latest")
40+ #[arg(long, default_value = "claude-3-7-sonnet-latest")]
41+ model: String,
42+}
2043
2144 fn main() {
2245 env_logger::init();
46+
47+ let args = Args::parse();
48+ let all_available_examples = list_all_examples().unwrap();
49+ let example_paths = all_available_examples
50+ .iter()
51+ .filter_map(|example_path| {
52+ let name = example_path.file_name()?.to_string_lossy();
53+ if args.examples.is_empty()
54+ || args
55+ .examples
56+ .iter()
57+ .any(|name_substring| name.contains(name_substring))
58+ {
59+ Some(example_path.clone())
60+ } else {
61+ None
62+ }
63+ })
64+ .collect::<Vec<_>>();
65+
2366 let http_client = Arc::new(ReqwestClient::new());
2467 let app = Application::headless().with_http_client(http_client.clone());
2568
2669 app.run(move |cx| {
2770 let app_state = init(cx);
2871
29− let model = find_model("claude-3-7-sonnet-thinking-latest", cx).unwrap();
72+ let model = find_model("claude-3-7-sonnet-latest", cx).unwrap();
3073
3174 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
3275 registry.set_default_model(Some(model.clone()), cx);
@@ -39,17 +82,142 @@ fn main() { · 6 unmodified lines
3982 cx.spawn(async move |cx| {
4083 authenticate.await.unwrap();
4184
42− let example =
43− Example::load_from_directory("./crates/eval/examples/find_and_replace_diff_card")?;
44− example.setup()?;
45− cx.update(|cx| example.run(model, app_state, cx))?.await?;
85+ std::fs::create_dir_all(REPOS_DIR)?;
86+ std::fs::create_dir_all(WORKTREES_DIR)?;
87+
88+ let run_dir = Path::new(RUNS_DIR).join(format!(
89+ "{}",
90+ chrono::Local::now().format("%Y-%m-%d_%H-%M-%S")
91+ ));
92+ std::fs::create_dir_all(&run_dir)?;
93+
94+ let mut examples = Vec::new();
95+ for example_path in example_paths {
96+ let example = Example::load_from_directory(&example_path, &run_dir)?;
97+ examples.push((example_path, example));
98+ }
99+ let mut repo_urls = HashSet::new();
100+
101+ let mut clone_tasks = Vec::new();
102+
103+ for (_, example) in examples.iter() {
104+ let repo_url = example.base.url.clone();
105+ if repo_urls.insert(repo_url.clone()) {
106+ let repo_path = repo_path_for_url(&repo_url);
107+
108+ if !repo_path.join(".git").is_dir() {
109+ println!("Cloning: {}", repo_url);
110+
111+ let git_task = cx.spawn(async move |_cx| {
112+ std::fs::create_dir_all(&repo_path)?;
113+ run_git(&repo_path, &["init"]).await?;
114+ run_git(&repo_path, &["remote", "add", "origin", &repo_url]).await
115+ });
116+
117+ clone_tasks.push(git_task);
118+ } else {
119+ println!("Already cloned: {}", repo_url);
120+
121+ let actual_origin =
122+ run_git(&repo_path, &["remote", "get-url", "origin"]).await?;
123+ if actual_origin != repo_url {
124+ return Err(anyhow!(
125+ "remote origin {} does not match expected origin {}",
126+ actual_origin,
127+ repo_url,
128+ ));
129+ }
130+ }
131+ }
132+ }
133+
134+ future::join_all(clone_tasks).await;
135+
136+ let tasks = examples
137+ .into_iter()
138+ .map(|(example_path, example)| {
139+ let app_state = app_state.clone();
140+ let model = model.clone();
141+ cx.spawn(async move |cx| {
142+ (
143+ example_path,
144+ run_example(example, model, app_state, cx).await,
145+ )
146+ })
147+ })
148+ .collect::<Vec<_>>();
149+
150+ let results: Vec<(PathBuf, Result<JudgeOutput>)> = future::join_all(tasks).await;
151+
152+ println!("\n\n");
153+ println!("========================================");
154+ println!(" EVAL RESULTS ");
155+ println!("========================================");
156+ println!("");
46157
47− anyhow::Ok(())
158+ let mut judge_scores = Vec::new();
159+
160+ for (example_path, result) in results {
161+ let example_name = example_path.file_name().unwrap().to_string_lossy();
162+ match result {
163+ Err(err) => {
164+ println!("💥 {:<30}: {:?}", example_name, err);
165+ }
166+ Ok(judge_output) => {
167+ const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
168+
169+ println!(
170+ "{} {:<30}: {}",
171+ SCORES[judge_output.score.min(5) as usize],
172+ example_name,
173+ judge_output.score,
174+ );
175+ judge_scores.push(judge_output.score);
176+ }
177+ }
178+ }
179+
180+ let score_count = judge_scores.len();
181+ let average_score = judge_scores
182+ .into_iter()
183+ .map(|score| score as f32)
184+ .sum::<f32>()
185+ / (score_count as f32);
186+ println!("\nAverage score: {average_score}");
187+
188+ cx.update(|cx| cx.quit())
48189 })
49190 .detach_and_log_err(cx);
50191 });
51192 }
52193
194+async fn run_example(
195+ mut example: Example,
196+ model: Arc<dyn LanguageModel>,
197+ app_state: Arc<AgentAppState>,
198+ cx: &mut AsyncApp,
199+) -> Result<JudgeOutput> {
200+ example.setup().await?;
201+ cx.update(|cx| example.run(model.clone(), app_state, cx))?
202+ .await?;
203+ let diff = example.repository_diff().await?;
204+ example.judge(model, diff, cx).await
205+}
206+
207+fn list_all_examples() -> Result<Vec<PathBuf>> {
208+ let path = std::fs::canonicalize(EXAMPLES_DIR).unwrap();
209+ let entries = std::fs::read_dir(path).unwrap();
210+ let mut result_paths = Vec::new();
211+ for entry in entries {
212+ let entry = entry?;
213+ let path = entry.path();
214+ if path.is_dir() {
215+ result_paths.push(path);
216+ }
217+ }
218+ Ok(result_paths)
219+}
220+
53221 /// Subset of `workspace::AppState` needed by `HeadlessAssistant`, with additional fields.
54222 pub struct AgentAppState {
55223 pub languages: Arc<LanguageRegistry>,
@@ -72,6 +240,27 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { · 16 unmodified lines
72240 .unwrap();
73241 cx.set_global(settings_store);
74242 client::init_settings(cx);
243+
244+ // Set User-Agent so we can download language servers from GitHub
245+ let user_agent = format!(
246+ "Zed/{} ({}; {})",
247+ AppVersion::global(cx),
248+ std::env::consts::OS,
249+ std::env::consts::ARCH
250+ );
251+ let proxy_str = ProxySettings::get_global(cx).proxy.to_owned();
252+ let proxy_url = proxy_str
253+ .as_ref()
254+ .and_then(|input| input.parse::<Uri>().ok())
255+ .or_else(read_proxy_from_env);
256+ let http = {
257+ let _guard = Tokio::handle(cx).enter();
258+
259+ ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent)
260+ .expect("could not start HTTP client")
261+ };
262+ cx.set_http_client(Arc::new(http));
263+
75264 Project::init_settings(cx);
76265
77266 let client = Client::production(cx);
@@ -83,13 +272,47 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { · 5 unmodified lines
83272 cx.background_executor().clone(),
84273 ));
85274
86− let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
275+ let mut languages = LanguageRegistry::new(cx.background_executor().clone());
276+ languages.set_language_server_download_dir(paths::languages_dir().clone());
277+ let languages = Arc::new(languages);
87278
88279 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
89280
281+ extension::init(cx);
282+
283+ let (tx, rx) = async_watch::channel(None);
284+ cx.observe_global::<SettingsStore>(move |cx| {
285+ let settings = &ProjectSettings::get_global(cx).node;
286+ let options = NodeBinaryOptions {
287+ allow_path_lookup: !settings.ignore_system_version.unwrap_or_default(),
288+ allow_binary_download: true,
289+ use_paths: settings.path.as_ref().map(|node_path| {
290+ let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
291+ let npm_path = settings
292+ .npm_path
293+ .as_ref()
294+ .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref()));
295+ (
296+ node_path.clone(),
297+ npm_path.unwrap_or_else(|| {
298+ let base_path = PathBuf::new();
299+ node_path.parent().unwrap_or(&base_path).join("npm")
300+ }),
301+ )
302+ }),
303+ };
304+ tx.send(Some(options)).log_err();
305+ })
306+ .detach();
307+ let node_runtime = NodeRuntime::new(client.http_client().clone(), rx);
308+
309+ let extension_host_proxy = ExtensionHostProxy::global(cx);
310+
90311 language::init(cx);
312+ language_extension::init(extension_host_proxy.clone(), languages.clone());
91313 language_model::init(client.clone(), cx);
92314 language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
315+ languages::init(languages.clone(), node_runtime.clone(), cx);
93316 assistant_tools::init(client.http_client().clone(), cx);
94317 context_server::init(cx);
95318 let stdout_is_a_pty = false;
@@ -109,7 +332,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { · 13 unmodified lines
109332 client,
110333 user_store,
111334 fs,
112− node_runtime: NodeRuntime::unavailable(),
335+ node_runtime,
113336 prompt_builder,
114337 })
115338 }
crates/eval/src/example.rs+537−63
@@ -1,83 +1,161 @@
11 use agent::{RequestKind, ThreadEvent, ThreadStore};
2−use anyhow::{Result, anyhow};
2+use anyhow::{Context as _, Result, anyhow};
33 use assistant_tool::ToolWorkingSet;
4+use client::proto::LspWorkProgress;
45 use dap::DapRegistry;
5−use futures::channel::oneshot;
6−use gpui::{App, Task};
7−use language_model::{LanguageModel, StopReason};
8−use project::Project;
9−use serde::Deserialize;
10−use std::process::Command;
11−use std::sync::Arc;
6+use futures::channel::{mpsc, oneshot};
7+use futures::{FutureExt, StreamExt as _};
8+use gpui::{App, AsyncApp, Entity, Task};
9+use handlebars::Handlebars;
10+use language::{DiagnosticSeverity, OffsetRangeExt};
11+use language_model::{
12+ LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
13+ StopReason, TokenUsage,
14+};
15+use project::{LspStore, Project, ProjectPath};
16+use serde::{Deserialize, Serialize};
17+use std::fmt::Write as _;
18+use std::fs::File;
19+use std::io::Write as _;
20+use std::sync::{Arc, Mutex};
21+use std::time::Duration;
1222 use std::{
1323 fs,
1424 path::{Path, PathBuf},
1525 };
26+use unindent::Unindent as _;
27+use util::ResultExt as _;
28+use util::command::new_smol_command;
29+use util::serde::default_true;
1630
1731 use crate::AgentAppState;
1832
19−#[derive(Debug, Deserialize)]
33+pub const EXAMPLES_DIR: &str = "./crates/eval/examples";
34+pub const REPOS_DIR: &str = "./crates/eval/repos";
35+pub const WORKTREES_DIR: &str = "./crates/eval/worktrees";
36+
37+#[derive(Clone, Debug, Deserialize)]
2038 pub struct ExampleBase {
21− pub path: PathBuf,
39+ pub url: String,
2240 pub revision: String,
41+ pub language_extension: Option<String>,
42+ pub insert_id: Option<String>,
43+ #[serde(default = "default_true")]
44+ pub require_lsp: bool,
2345 }
2446
25−#[derive(Debug)]
47+#[derive(Clone, Debug)]
2648 pub struct Example {
49+ pub name: String,
50+ /// Content of `base.toml`
2751 pub base: ExampleBase,
28−
29− /// Content of the prompt.md file
52+ /// Content of `prompt.md`
3053 pub prompt: String,
54+ /// Content of `criteria.md`
55+ pub criteria: String,
56+ /// Markdown log file to append to
57+ pub log_file: Arc<Mutex<File>>,
58+}
59+
60+#[derive(Debug, Serialize, Deserialize, Clone)]
61+pub struct RunOutput {
62+ pub repository_diff: String,
63+ pub diagnostics: String,
64+ pub response_count: usize,
65+ pub token_usage: TokenUsage,
66+}
3167
32− /// Content of the rubric.md file
33− pub _rubric: String,
68+#[derive(Debug, Clone, Serialize, Deserialize)]
69+pub struct JudgeInput {
70+ pub repository_diff: String,
71+ pub criteria: String,
72+}
73+
74+#[derive(Debug, Clone, Serialize, Deserialize)]
75+pub struct JudgeOutput {
76+ pub analysis: String,
77+ pub score: u32,
3478 }
3579
3680 impl Example {
37− /// Load an example from a directory containing base.toml, prompt.md, and rubric.md
38− pub fn load_from_directory<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
39− let base_path = dir_path.as_ref().join("base.toml");
40− let prompt_path = dir_path.as_ref().join("prompt.md");
41− let rubric_path = dir_path.as_ref().join("rubric.md");
81+ /// Load an example from a directory containing base.toml, prompt.md, and criteria.md
82+ pub fn load_from_directory(dir_path: &Path, run_dir: &Path) -> Result<Self> {
83+ let name = dir_path.file_name().unwrap().to_string_lossy().to_string();
84+ let base_path = dir_path.join("base.toml");
85+ let prompt_path = dir_path.join("prompt.md");
86+ let criteria_path = dir_path.join("criteria.md");
4287
43− let mut base: ExampleBase = toml::from_str(&fs::read_to_string(&base_path)?)?;
44− base.path = base.path.canonicalize()?;
88+ let log_file_path = run_dir.join(format!(
89+ "{}.md",
90+ dir_path.file_name().unwrap().to_str().unwrap()
91+ ));
92+ let log_file = Arc::new(Mutex::new(File::create(&log_file_path).unwrap()));
93+ println!("{}> Logging to {:?}", name, log_file_path);
4594
4695 Ok(Example {
47− base,
48− prompt: fs::read_to_string(prompt_path)?,
49− _rubric: fs::read_to_string(rubric_path)?,
96+ name,
97+ base: toml::from_str(&fs::read_to_string(&base_path)?)?,
98+ prompt: fs::read_to_string(prompt_path.clone())?,
99+ criteria: fs::read_to_string(criteria_path.clone())?,
100+ log_file,
50101 })
51102 }
52103
104+ pub fn worktree_path(&self) -> PathBuf {
105+ Path::new(WORKTREES_DIR)
106+ .canonicalize()
107+ .context(format!("No such directory {WORKTREES_DIR}"))
108+ .unwrap()
109+ .join(&self.name)
110+ }
111+
53112 /// Set up the example by checking out the specified Git revision
54− pub fn setup(&self) -> Result<()> {
55− // Check if the directory exists
56− let path = Path::new(&self.base.path);
57− anyhow::ensure!(path.exists(), "Path does not exist: {:?}", self.base.path);
58−
59− // Change to the project directory and checkout the specified revision
60− let output = Command::new("git")
61− .current_dir(&self.base.path)
62− .arg("checkout")
63− .arg(&self.base.revision)
64− .output()?;
65− anyhow::ensure!(
66− output.status.success(),
67− "Failed to checkout revision {}: {}",
68− self.base.revision,
69− String::from_utf8_lossy(&output.stderr),
70− );
113+ pub async fn setup(&self) -> Result<()> {
114+ let repo_path = repo_path_for_url(&self.base.url);
115+
116+ run_git(
117+ &repo_path,
118+ &["fetch", "--depth", "1", "origin", &self.base.revision],
119+ )
120+ .await?;
121+
122+ let worktree_path = self.worktree_path();
123+
124+ if worktree_path.is_dir() {
125+ println!("{}> Resetting existing worktree", self.name);
126+
127+ // TODO: consider including "-x" to remove ignored files. The downside of this is that
128+ // it will also remove build artifacts, and so prevent incremental reuse there.
129+ run_git(&worktree_path, &["clean", "--force", "-d"]).await?;
130+ run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?;
131+ run_git(&worktree_path, &["checkout", &self.base.revision]).await?;
132+ } else {
133+ println!("{}> Creating worktree", self.name);
134+
135+ let worktree_path_string = worktree_path.to_string_lossy().to_string();
136+
137+ run_git(
138+ &repo_path,
139+ &[
140+ "worktree",
141+ "add",
142+ "-f",
143+ &worktree_path_string,
144+ &self.base.revision,
145+ ],
146+ )
147+ .await?;
148+ }
71149
72150 Ok(())
73151 }
74152
75153 pub fn run(
76− self,
154+ &self,
77155 model: Arc<dyn LanguageModel>,
78156 app_state: Arc<AgentAppState>,
79157 cx: &mut App,
80− ) -> Task<Result<()>> {
158+ ) -> Task<Result<RunOutput>> {
81159 let project = Project::local(
82160 app_state.client.clone(),
83161 app_state.node_runtime.clone(),
@@ -89,30 +167,119 @@ impl Example { · 5 unmodified lines
89167 cx,
90168 );
91169
170+ let worktree_path = self.worktree_path();
92171 let worktree = project.update(cx, |project, cx| {
93− project.create_worktree(self.base.path, true, cx)
172+ project.create_worktree(&worktree_path, true, cx)
94173 });
95174
96175 let tools = Arc::new(ToolWorkingSet::default());
97176 let thread_store =
98177 ThreadStore::load(project.clone(), tools, app_state.prompt_builder.clone(), cx);
178+ let this = self.clone();
99179
100− println!("USER:");
101− println!("{}", self.prompt);
102− println!("ASSISTANT:");
103180 cx.spawn(async move |cx| {
104− worktree.await?;
181+ let worktree = worktree.await?;
182+
183+ // Wait for worktree scan to finish before choosing a file to open.
184+ worktree
185+ .update(cx, |worktree, _cx| {
186+ worktree.as_local().unwrap().scan_complete()
187+ })?
188+ .await;
189+
190+ let lsp_open_handle_and_store = if this.base.require_lsp {
191+ let language_extension = this.base.language_extension.as_deref().context(
192+ "language_extension field is required in base.toml when `require_lsp == true`",
193+ )?;
194+
195+ // Open a file that matches the language to cause LSP to start.
196+ let language_file = worktree.read_with(cx, |worktree, _cx| {
197+ worktree
198+ .files(false, 0)
199+ .find_map(|e| {
200+ if e.path.clone().extension().and_then(|ext| ext.to_str())
201+ == Some(language_extension)
202+ {
203+ Some(ProjectPath {
204+ worktree_id: worktree.id(),
205+ path: e.path.clone(),
206+ })
207+ } else {
208+ None
209+ }
210+ })
211+ .context("Failed to find a file for example language")
212+ })??;
213+
214+ let open_language_file_buffer_task = project.update(cx, |project, cx| {
215+ project.open_buffer(language_file.clone(), cx)
216+ })?;
217+
218+ let language_file_buffer = open_language_file_buffer_task.await?;
219+
220+ let (lsp_open_handle, lsp_store) = project.update(cx, |project, cx| {
221+ (
222+ project.register_buffer_with_language_servers(&language_file_buffer, cx),
223+ project.lsp_store().clone(),
224+ )
225+ })?;
226+
227+ // TODO: remove this once the diagnostics tool waits for new diagnostics
228+ cx.background_executor().timer(Duration::new(5, 0)).await;
229+ wait_for_lang_server(&lsp_store, this.name.clone(), cx).await?;
230+
231+ lsp_store.update(cx, |lsp_store, cx| {
232+ lsp_open_handle.update(cx, |buffer, cx| {
233+ buffer.update(cx, |buffer, cx| {
234+ let has_language_server = lsp_store
235+ .language_servers_for_local_buffer(buffer, cx)
236+ .next()
237+ .is_some();
238+ if has_language_server {
239+ Ok(())
240+ } else {
241+ Err(anyhow!(
242+ "`{:?}` was opened to cause the language server to start, \
243+ but no language servers are registered for its buffer. \
244+ Set `require_lsp = false` in `base.toml` to skip this.",
245+ language_file
246+ ))
247+ }
248+ })
249+ })
250+ })??;
251+
252+ Some((lsp_open_handle, lsp_store))
253+ } else {
254+ None
255+ };
256+
257+ if std::env::var("ZED_EVAL_SETUP_ONLY").is_ok() {
258+ return Err(anyhow!("Setup only mode"));
259+ }
260+
105261 let thread_store = thread_store.await;
106262 let thread =
107263 thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;
108264
265+ {
266+ let mut log_file = this.log_file.lock().unwrap();
267+ writeln!(&mut log_file, "👤 USER:").log_err();
268+ writeln!(&mut log_file, "{}", this.prompt).log_err();
269+ writeln!(&mut log_file, "🤖 ASSISTANT:").log_err();
270+ log_file.flush().log_err();
271+ }
272+
109273 let (tx, rx) = oneshot::channel();
110274 let mut tx = Some(tx);
111275
112− let _subscription =
113− cx.subscribe(
114− &thread,
115− move |thread, event: &ThreadEvent, cx| match event {
276+ let _subscription = cx.subscribe(&thread, {
277+ let log_file = this.log_file.clone();
278+ let name = this.name.clone();
279+ move |thread, event: &ThreadEvent, cx| {
280+ let mut log_file = log_file.lock().unwrap();
281+
282+ match event {
116283 ThreadEvent::Stopped(reason) => match reason {
117284 Ok(StopReason::EndTurn) => {
118285 if let Some(tx) = tx.take() {
@@ -137,15 +304,16 @@ impl Example { · 18 unmodified lines
137304 }
138305 }
139306 ThreadEvent::StreamedAssistantText(_, chunk) => {
140− print!("{}", chunk);
307+ write!(&mut log_file, "{}", chunk).log_err();
141308 }
142309 ThreadEvent::StreamedAssistantThinking(_, chunk) => {
143− print!("{}", chunk);
310+ write!(&mut log_file, "{}", chunk).log_err();
144311 }
145312 ThreadEvent::UsePendingTools { tool_uses } => {
146− println!("\n\nUSING TOOLS:");
313+ writeln!(&mut log_file, "\n\nUSING TOOLS:").log_err();
147314 for tool_use in tool_uses {
148− println!("{}: {}", tool_use.name, tool_use.input);
315+ writeln!(&mut log_file, "{}: {}", tool_use.name, tool_use.input)
316+ .log_err();
149317 }
150318 }
151319 ThreadEvent::ToolFinished {
@@ -154,25 +322,331 @@ impl Example { · 2 unmodified lines
154322 ..
155323 } => {
156324 if let Some(tool_use) = pending_tool_use {
157− println!("\nTOOL FINISHED: {}", tool_use.name);
325+ let message = format!("TOOL FINISHED: {}", tool_use.name);
326+ println!("{name}> {message}");
327+ writeln!(&mut log_file, "\n{}", message).log_err();
158328 }
159329 if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
160− println!("\n{}\n", tool_result.content);
330+ let message = format!("\n{}\n", tool_result.content);
331+ writeln!(&mut log_file, "{}", message).log_err();
161332 }
162333 }
163334 _ => {}
164− },
165− )?;
335+ }
336+
337+ log_file.flush().log_err();
338+ }
339+ })?;
166340
167341 thread.update(cx, |thread, cx| {
168342 let context = vec![];
169− thread.insert_user_message(self.prompt.clone(), context, None, cx);
343+ thread.insert_user_message(this.prompt.clone(), context, None, cx);
170344 thread.send_to_model(model, RequestKind::Chat, cx);
171345 })?;
172346
173347 rx.await??;
174348
175− Ok(())
349+ if let Some((_, lsp_store)) = lsp_open_handle_and_store.as_ref() {
350+ wait_for_lang_server(lsp_store, this.name.clone(), cx).await?;
351+ }
352+
353+ let repository_diff = this.repository_diff().await?;
354+ let diagnostics = cx
355+ .update(move |cx| {
356+ cx.spawn(async move |cx| query_lsp_diagnostics(project, cx).await)
357+ })?
358+ .await?;
359+
360+ drop(lsp_open_handle_and_store);
361+
362+ thread.update(cx, |thread, _cx| {
363+ let response_count = thread
364+ .messages()
365+ .filter(|message| message.role == language_model::Role::Assistant)
366+ .count();
367+ RunOutput {
368+ repository_diff,
369+ diagnostics,
370+ response_count,
371+ token_usage: thread.cumulative_token_usage(),
372+ }
373+ })
176374 })
177375 }
376+
377+ pub async fn judge(
378+ &mut self,
379+ model: Arc<dyn LanguageModel>,
380+ repository_diff: String,
381+ cx: &AsyncApp,
382+ ) -> Result<JudgeOutput> {
383+ let judge_prompt = include_str!("judge_prompt.hbs");
384+ let judge_prompt_name = "judge_prompt";
385+ let mut handlebars = Handlebars::new();
386+ handlebars.register_template_string(judge_prompt_name, judge_prompt)?;
387+ let prompt = handlebars.render(
388+ judge_prompt_name,
389+ &JudgeInput {
390+ repository_diff,
391+ criteria: self.criteria.clone(),
392+ },
393+ )?;
394+
395+ let request = LanguageModelRequest {
396+ messages: vec![LanguageModelRequestMessage {
397+ role: Role::User,
398+ content: vec![MessageContent::Text(prompt)],
399+ cache: false,
400+ }],
401+ temperature: None,
402+ tools: Vec::new(),
403+ stop: Vec::new(),
404+ };
405+
406+ let response = send_language_model_request(model, request, cx).await?;
407+
408+ let mut log_file = self.log_file.lock().unwrap();
409+
410+ writeln!(&mut log_file, "\n\n").log_err();
411+ writeln!(&mut log_file, "========================================").log_err();
412+ writeln!(&mut log_file, " JUDGE OUTPUT ").log_err();
413+ writeln!(&mut log_file, "========================================").log_err();
414+ writeln!(&mut log_file, "\n{}", &response).log_err();
415+
416+ parse_judge_output(&response)
417+ }
418+
419+ pub async fn repository_diff(&self) -> Result<String> {
420+ let worktree_path = self.worktree_path();
421+ run_git(&worktree_path, &["add", "-N"]).await?;
422+ run_git(&worktree_path, &["diff"]).await
423+ }
424+}
425+
426+fn wait_for_lang_server(
427+ lsp_store: &Entity<LspStore>,
428+ name: String,
429+ cx: &mut AsyncApp,
430+) -> Task<Result<()>> {
431+ if cx
432+ .update(|cx| !has_pending_lang_server_work(lsp_store, cx))
433+ .unwrap()
434+ || std::env::var("ZED_EVAL_SKIP_LS_WAIT").is_ok()
435+ {
436+ return Task::ready(anyhow::Ok(()));
437+ }
438+
439+ println!("{}> ⏵ Waiting for language server", name);
440+
441+ let (mut tx, mut rx) = mpsc::channel(1);
442+
443+ let subscription =
444+ cx.subscribe(&lsp_store, {
445+ let name = name.clone();
446+ move |lsp_store, event, cx| {
447+ match event {
448+ project::LspStoreEvent::LanguageServerUpdate {
449+ message:
450+ client::proto::update_language_server::Variant::WorkProgress(
451+ LspWorkProgress {
452+ message: Some(message),
453+ ..
454+ },
455+ ),
456+ ..
457+ } => println!("{name}> ⟲ {message}"),
458+ _ => {}
459+ }
460+
461+ if !has_pending_lang_server_work(&lsp_store, cx) {
462+ tx.try_send(()).ok();
463+ }
464+ }
465+ });
466+
467+ cx.spawn(async move |cx| {
468+ let timeout = cx.background_executor().timer(Duration::new(60 * 5, 0));
469+ let result = futures::select! {
470+ _ = rx.next() => {
471+ println!("{}> ⚑ Language server idle", name);
472+ anyhow::Ok(())
473+ },
474+ _ = timeout.fuse() => {
475+ Err(anyhow!("LSP wait timed out after 5 minutes"))
476+ }
477+ };
478+ drop(subscription);
479+ result
480+ })
481+}
482+
483+fn has_pending_lang_server_work(lsp_store: &Entity<LspStore>, cx: &App) -> bool {
484+ lsp_store
485+ .read(cx)
486+ .language_server_statuses()
487+ .any(|(_, status)| !status.pending_work.is_empty())
488+}
489+
490+async fn query_lsp_diagnostics(project: Entity<Project>, cx: &mut AsyncApp) -> Result<String> {
491+ let paths_with_diagnostics = project.update(cx, |project, cx| {
492+ project
493+ .diagnostic_summaries(true, cx)
494+ .filter(|(_, _, summary)| summary.error_count > 0 || summary.warning_count > 0)
495+ .map(|(project_path, _, _)| project_path)
496+ .collect::<Vec<_>>()
497+ })?;
498+
499+ let mut output = String::new();
500+ for project_path in paths_with_diagnostics {
501+ let buffer = project
502+ .update(cx, |project, cx| project.open_buffer(project_path, cx))?
503+ .await?;
504+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
505+
506+ for (_, group) in snapshot.diagnostic_groups(None) {
507+ let entry = &group.entries[group.primary_ix];
508+ let range = entry.range.to_point(&snapshot);
509+ let severity = match entry.diagnostic.severity {
510+ DiagnosticSeverity::ERROR => "error",
511+ DiagnosticSeverity::WARNING => "warning",
512+ _ => continue,
513+ };
514+
515+ writeln!(
516+ output,
517+ "{} at line {}: {}",
518+ severity,
519+ range.start.row + 1,
520+ entry.diagnostic.message
521+ )?;
522+ }
523+ }
524+ anyhow::Ok(output)
525+}
526+
527+fn parse_judge_output(response: &str) -> Result<JudgeOutput> {
528+ let analysis = get_tag("analysis", response)?.to_string();
529+ let score = get_tag("score", response)?
530+ .parse()
531+ .context("error parsing score")?;
532+
533+ Ok(JudgeOutput { analysis, score })
534+}
535+
536+fn get_tag(name: &'static str, response: &str) -> Result<String> {
537+ let start_tag = format!("<{}>", name);
538+ let end_tag = format!("</{}>", name);
539+
540+ let start_ix = response
541+ .find(&start_tag)
542+ .context(format!("{} start tag not found", name))?;
543+ let content_start_ix = start_ix + start_tag.len();
544+
545+ let end_ix = content_start_ix
546+ + response[content_start_ix..]
547+ .find(&end_tag)
548+ .context(format!("{} end tag not found", name))?;
549+
550+ let content = response[content_start_ix..end_ix].trim().unindent();
551+
552+ anyhow::Ok(content)
553+}
554+
555+pub fn repo_path_for_url(repo_url: &str) -> PathBuf {
556+ let repo_name = repo_url
557+ .trim_start_matches("https://")
558+ .replace(|c: char| !c.is_alphanumeric(), "-");
559+ Path::new(REPOS_DIR)
560+ .canonicalize()
561+ .context(format!("No such directory {REPOS_DIR}"))
562+ .unwrap()
563+ .join(repo_name)
564+}
565+
566+pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
567+ let output = new_smol_command("git")
568+ .current_dir(repo_path)
569+ .args(args)
570+ .output()
571+ .await?;
572+
573+ if output.status.success() {
574+ Ok(String::from_utf8(output.stdout)?.trim().to_string())
575+ } else {
576+ Err(anyhow!(
577+ "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}",
578+ args.join(" "),
579+ repo_path.display(),
580+ output.status,
581+ String::from_utf8_lossy(&output.stderr),
582+ String::from_utf8_lossy(&output.stdout),
583+ ))
584+ }
585+}
586+
587+pub async fn send_language_model_request(
588+ model: Arc<dyn LanguageModel>,
589+ request: LanguageModelRequest,
590+ cx: &AsyncApp,
591+) -> anyhow::Result<String> {
592+ match model.stream_completion_text(request, &cx).await {
593+ Ok(mut stream) => {
594+ let mut full_response = String::new();
595+ while let Some(chunk_result) = stream.stream.next().await {
596+ match chunk_result {
597+ Ok(chunk_str) => {
598+ print!("{}", &chunk_str);
599+ full_response.push_str(&chunk_str);
600+ }
601+ Err(err) => {
602+ return Err(anyhow!(
603+ "Error receiving response from language model: {err}"
604+ ));
605+ }
606+ }
607+ }
608+ Ok(full_response)
609+ }
610+ Err(err) => Err(anyhow!(
611+ "Failed to get response from language model. Error was: {err}"
612+ )),
613+ }
614+}
615+
616+#[cfg(test)]
617+mod test {
618+ use super::*;
619+
620+ #[test]
621+ fn test_parse_judge_output() {
622+ let response = r#"
623+ <analysis>The model did a good job but there were still compilations errors.</analysis>
624+ <score>3</score>
625+ "#
626+ .unindent();
627+
628+ let output = parse_judge_output(&response).unwrap();
629+ assert_eq!(
630+ output.analysis,
631+ "The model did a good job but there were still compilations errors."
632+ );
633+ assert_eq!(output.score, 3);
634+
635+ let response = r#"
636+ Text around ignored
637+
638+ <analysis>
639+ Failed to compile:
640+ - Error 1
641+ - Error 2
642+ </analysis>
643+
644+ <score>1</score>
645+ "#
646+ .unindent();
647+
648+ let output = parse_judge_output(&response).unwrap();
649+ assert_eq!(output.analysis, "Failed to compile:\n- Error 1\n- Error 2");
650+ assert_eq!(output.score, 1);
651+ }
178652 }
crates/eval/src/judge_prompt.hbs+25−0
@@ -0,0 +1,25 @@
1+You are an expert software developer tasked with evaluating the following changes to a codebase:
2+
3+<changes>
4+{{repository_diff}}
5+</changes>
6+
7+Use the following criteria to score the above changes.
8+
9+<criteria>
10+{{criteria}}
11+</criteria>
12+
13+Based on these criteria, give the test output a score between 0 and 5.
14+
15+- 5 means: changes meet all criteria
16+- 0 means: changes don't meet any criteria
17+
18+Be suspicious of the changes because they were generated by an LLM.
19+Sometimes the LLM decides to change random code, so if the changes are not mentioned in the criteria, penalize the score.
20+Analyze the diff hunk by hunk and describe how each change meets or fails to meet the criteria.
21+
22+```
23+<analysis>{YOUR ANALYSIS HERE}</analysis>
24+<score>{YOUR SCORE HERE}</score>
25+```
crates/language_model/src/language_model.rs+1−1
@@ -83,7 +83,7 @@ pub enum StopReason { · 82 unmodified lines
8383 ToolUse,
8484 }
8585
86−#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
86+#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Default)]
8787 pub struct TokenUsage {
8888 #[serde(default, skip_serializing_if = "is_default")]
8989 pub input_tokens: u32,