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
4878 "assistant_settings",4878 "assistant_settings",
4879 "assistant_tool",4879 "assistant_tool",
4880 "assistant_tools",4880 "assistant_tools",
4881 "async-watch",
4882 "chrono",
4883 "clap",
4881 "client",4884 "client",
4882 "context_server",4885 "context_server",
4883 "dap",4886 "dap",
4884 "env_logger 0.11.8",4887 "env_logger 0.11.8",
4888 "extension",
4885 "fs",4889 "fs",
4886 "futures 0.3.31",4890 "futures 0.3.31",
4887 "gpui",4891 "gpui",
4888 "gpui_tokio",4892 "gpui_tokio",
4893 "handlebars 4.5.0",
4889 "language",4894 "language",
4895 "language_extension",
4890 "language_model",4896 "language_model",
4891 "language_models",4897 "language_models",
4898 "languages",
4892 "node_runtime",4899 "node_runtime",
4900 "paths",
4893 "project",4901 "project",
4894 "prompt_store",4902 "prompt_store",
4895 "release_channel",4903 "release_channel",
4896 "reqwest_client",4904 "reqwest_client",
4897 "serde",4905 "serde",
4898 "settings",4906 "settings",
4907 "shellexpand 2.1.2",
4899 "toml 0.8.20",4908 "toml 0.8.20",
4909 "unindent",
4910 "util",
4900 "workspace-hack",4911 "workspace-hack",
4901]4912]
4902 4913
crates/agent/src/thread.rs+10−6
@@ -827,7 +827,7 @@ impl Thread { · 826 unmodified lines
827 })827 })
828 .collect(),828 .collect(),
829 initial_project_snapshot,829 initial_project_snapshot,
830 cumulative_token_usage: this.cumulative_token_usage.clone(),830 cumulative_token_usage: this.cumulative_token_usage,
831 detailed_summary_state: this.detailed_summary_state.clone(),831 detailed_summary_state: this.detailed_summary_state.clone(),
832 exceeded_window_error: this.exceeded_window_error.clone(),832 exceeded_window_error: this.exceeded_window_error.clone(),
833 })833 })
@@ -1016,7 +1016,7 @@ impl Thread { · 182 unmodified lines
1016 let task = cx.spawn(async move |thread, cx| {1016 let task = cx.spawn(async move |thread, cx| {
1017 let stream = model.stream_completion(request, &cx);1017 let stream = model.stream_completion(request, &cx);
1018 let initial_token_usage =1018 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);
1020 let stream_completion = async {1020 let stream_completion = async {
1021 let mut events = stream.await?;1021 let mut events = stream.await?;
1022 let mut stop_reason = StopReason::EndTurn;1022 let mut stop_reason = StopReason::EndTurn;
@@ -1038,9 +1038,9 @@ impl Thread { · 15 unmodified lines
1038 stop_reason = reason;1038 stop_reason = reason;
1039 }1039 }
1040 LanguageModelCompletionEvent::UsageUpdate(token_usage) => {1040 LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
1041 thread.cumulative_token_usage =1041 thread.cumulative_token_usage = thread.cumulative_token_usage
1042 thread.cumulative_token_usage.clone() + token_usage.clone()1042 + token_usage
1043 - current_token_usage.clone();1043 - current_token_usage;
1044 current_token_usage = token_usage;1044 current_token_usage = token_usage;
1045 }1045 }
1046 LanguageModelCompletionEvent::Text(chunk) => {1046 LanguageModelCompletionEvent::Text(chunk) => {
@@ -1183,7 +1183,7 @@ impl Thread { · 136 unmodified lines
1183 thread.auto_capture_telemetry(cx);1183 thread.auto_capture_telemetry(cx);
1184 1184
1185 if let Ok(initial_usage) = initial_token_usage {1185 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;
1187 1187
1188 telemetry::event!(1188 telemetry::event!(
1189 "Assistant Thread Completion",1189 "Assistant Thread Completion",
@@ -1862,6 +1862,10 @@ impl Thread { · 672 unmodified lines
1862 .detach();1862 .detach();
1863 }1863 }
1864 1864
1865 pub fn cumulative_token_usage(&self) -> TokenUsage {
1866 self.cumulative_token_usage
1867 }
1868
1865 pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {1869 pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
1866 let model_registry = LanguageModelRegistry::read_global(cx);1870 let model_registry = LanguageModelRegistry::read_global(cx);
1867 let Some(model) = model_registry.default_model() else {1871 let Some(model) = model_registry.default_model() else {
crates/eval/.gitignore+3−0
@@ -0,0 +1,3 @@
1repos/
2worktrees/
3runs/
crates/eval/Cargo.toml+12−1
@@ -7,28 +7,39 @@ edition.workspace = true · 6 unmodified lines
7[dependencies]7[dependencies]
8agent.workspace = true8agent.workspace = true
9anyhow.workspace = true9anyhow.workspace = true
10async-watch.workspace = true
11assistant_settings.workspace = true
10assistant_tool.workspace = true12assistant_tool.workspace = true
11assistant_tools.workspace = true13assistant_tools.workspace = true
12assistant_settings.workspace = true14chrono.workspace = true
15clap.workspace = true
13client.workspace = true16client.workspace = true
14context_server.workspace = true17context_server.workspace = true
15dap.workspace = true18dap.workspace = true
16env_logger.workspace = true19env_logger.workspace = true
20extension.workspace = true
17fs.workspace = true21fs.workspace = true
18futures.workspace = true22futures.workspace = true
19gpui.workspace = true23gpui.workspace = true
20gpui_tokio.workspace = true24gpui_tokio.workspace = true
25handlebars.workspace = true
21language.workspace = true26language.workspace = true
27language_extension.workspace = true
22language_model.workspace = true28language_model.workspace = true
23language_models.workspace = true29language_models.workspace = true
30languages.workspace = true
24node_runtime.workspace = true31node_runtime.workspace = true
32paths.workspace = true
25project.workspace = true33project.workspace = true
26prompt_store.workspace = true34prompt_store.workspace = true
27release_channel.workspace = true35release_channel.workspace = true
28reqwest_client.workspace = true36reqwest_client.workspace = true
29serde.workspace = true37serde.workspace = true
30settings.workspace = true38settings.workspace = true
39shellexpand.workspace = true
31toml.workspace = true40toml.workspace = true
41unindent.workspace = true
42util.workspace = true
32workspace-hack.workspace = true43workspace-hack.workspace = true
33 44
34[[bin]]45[[bin]]
crates/eval/examples/find_and_replace_diff_card/base.toml+2−1
@@ -1,2 +1,3 @@
1path = "../zed_worktree"1url = "https://github.com/zed-industries/zed.git"
2revision = "38fcadf9481d018543c65f36ac3bafeba190179b"2revision = "38fcadf9481d018543c65f36ac3bafeba190179b"
3language_extension = "rs"
crates/eval/examples/find_and_replace_diff_card/criteria.md+2−0
@@ -0,0 +1,2 @@
11. 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
22. 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 @@
1mod example;1mod example;
2 2
3use assistant_settings::AssistantSettings;3use assistant_settings::AssistantSettings;
4use client::{Client, UserStore};4use client::{Client, ProxySettings, UserStore};
5pub(crate) use example::*;5pub(crate) use example::*;
6 6
7use ::fs::RealFs;7use ::fs::RealFs;
8use anyhow::anyhow;8use anyhow::{Result, anyhow};
9use gpui::{App, AppContext, Application, Entity, SemanticVersion, Task};9use clap::Parser;
10use extension::ExtensionHostProxy;
11use futures::future;
12use gpui::http_client::{Uri, read_proxy_from_env};
13use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, Task};
14use gpui_tokio::Tokio;
10use language::LanguageRegistry;15use language::LanguageRegistry;
11use language_model::{16use language_model::{
12 AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,17 AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
13};18};
14use node_runtime::NodeRuntime;19use node_runtime::{NodeBinaryOptions, NodeRuntime};
15use project::Project;20use project::Project;
21use project::project_settings::ProjectSettings;
16use prompt_store::PromptBuilder;22use prompt_store::PromptBuilder;
23use release_channel::AppVersion;
17use reqwest_client::ReqwestClient;24use reqwest_client::ReqwestClient;
18use settings::{Settings, SettingsStore};25use settings::{Settings, SettingsStore};
26use std::collections::HashSet;
27use std::path::{Path, PathBuf};
19use std::sync::Arc;28use std::sync::Arc;
29use util::ResultExt as _;
30
31pub const RUNS_DIR: &str = "./crates/eval/runs";
32
33#[derive(Parser, Debug)]
34#[command(name = "eval", disable_version_flag = true)]
35struct 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}
20 43
21fn main() {44fn main() {
22 env_logger::init();45 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
23 let http_client = Arc::new(ReqwestClient::new());66 let http_client = Arc::new(ReqwestClient::new());
24 let app = Application::headless().with_http_client(http_client.clone());67 let app = Application::headless().with_http_client(http_client.clone());
25 68
26 app.run(move |cx| {69 app.run(move |cx| {
27 let app_state = init(cx);70 let app_state = init(cx);
28 71
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();
30 73
31 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {74 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
32 registry.set_default_model(Some(model.clone()), cx);75 registry.set_default_model(Some(model.clone()), cx);
@@ -39,17 +82,142 @@ fn main() { · 6 unmodified lines
39 cx.spawn(async move |cx| {82 cx.spawn(async move |cx| {
40 authenticate.await.unwrap();83 authenticate.await.unwrap();
41 84
42 let example =85 std::fs::create_dir_all(REPOS_DIR)?;
43 Example::load_from_directory("./crates/eval/examples/find_and_replace_diff_card")?;86 std::fs::create_dir_all(WORKTREES_DIR)?;
44 example.setup()?;87
45 cx.update(|cx| example.run(model, app_state, cx))?.await?;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!("");
46 157
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())
48 })189 })
49 .detach_and_log_err(cx);190 .detach_and_log_err(cx);
50 });191 });
51}192}
52 193
194async 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
207fn 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
53/// Subset of `workspace::AppState` needed by `HeadlessAssistant`, with additional fields.221/// Subset of `workspace::AppState` needed by `HeadlessAssistant`, with additional fields.
54pub struct AgentAppState {222pub struct AgentAppState {
55 pub languages: Arc<LanguageRegistry>,223 pub languages: Arc<LanguageRegistry>,
@@ -72,6 +240,27 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { · 16 unmodified lines
72 .unwrap();240 .unwrap();
73 cx.set_global(settings_store);241 cx.set_global(settings_store);
74 client::init_settings(cx);242 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
75 Project::init_settings(cx);264 Project::init_settings(cx);
76 265
77 let client = Client::production(cx);266 let client = Client::production(cx);
@@ -83,13 +272,47 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { · 5 unmodified lines
83 cx.background_executor().clone(),272 cx.background_executor().clone(),
84 ));273 ));
85 274
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);
87 278
88 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));279 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
89 280
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
90 language::init(cx);311 language::init(cx);
312 language_extension::init(extension_host_proxy.clone(), languages.clone());
91 language_model::init(client.clone(), cx);313 language_model::init(client.clone(), cx);
92 language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);314 language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
315 languages::init(languages.clone(), node_runtime.clone(), cx);
93 assistant_tools::init(client.http_client().clone(), cx);316 assistant_tools::init(client.http_client().clone(), cx);
94 context_server::init(cx);317 context_server::init(cx);
95 let stdout_is_a_pty = false;318 let stdout_is_a_pty = false;
@@ -109,7 +332,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { · 13 unmodified lines
109 client,332 client,
110 user_store,333 user_store,
111 fs,334 fs,
112 node_runtime: NodeRuntime::unavailable(),335 node_runtime,
113 prompt_builder,336 prompt_builder,
114 })337 })
115}338}
crates/eval/src/example.rs+537−63
@@ -1,83 +1,161 @@
1use agent::{RequestKind, ThreadEvent, ThreadStore};1use agent::{RequestKind, ThreadEvent, ThreadStore};
2use anyhow::{Result, anyhow};2use anyhow::{Context as _, Result, anyhow};
3use assistant_tool::ToolWorkingSet;3use assistant_tool::ToolWorkingSet;
4use client::proto::LspWorkProgress;
4use dap::DapRegistry;5use dap::DapRegistry;
5use futures::channel::oneshot;6use futures::channel::{mpsc, oneshot};
6use gpui::{App, Task};7use futures::{FutureExt, StreamExt as _};
7use language_model::{LanguageModel, StopReason};8use gpui::{App, AsyncApp, Entity, Task};
8use project::Project;9use handlebars::Handlebars;
9use serde::Deserialize;10use language::{DiagnosticSeverity, OffsetRangeExt};
10use std::process::Command;11use language_model::{
11use std::sync::Arc;12 LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
13 StopReason, TokenUsage,
14};
15use project::{LspStore, Project, ProjectPath};
16use serde::{Deserialize, Serialize};
17use std::fmt::Write as _;
18use std::fs::File;
19use std::io::Write as _;
20use std::sync::{Arc, Mutex};
21use std::time::Duration;
12use std::{22use std::{
13 fs,23 fs,
14 path::{Path, PathBuf},24 path::{Path, PathBuf},
15};25};
26use unindent::Unindent as _;
27use util::ResultExt as _;
28use util::command::new_smol_command;
29use util::serde::default_true;
16 30
17use crate::AgentAppState;31use crate::AgentAppState;
18 32
19#[derive(Debug, Deserialize)]33pub const EXAMPLES_DIR: &str = "./crates/eval/examples";
34pub const REPOS_DIR: &str = "./crates/eval/repos";
35pub const WORKTREES_DIR: &str = "./crates/eval/worktrees";
36
37#[derive(Clone, Debug, Deserialize)]
20pub struct ExampleBase {38pub struct ExampleBase {
21 pub path: PathBuf,39 pub url: String,
22 pub revision: String,40 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,
23}45}
24 46
25#[derive(Debug)]47#[derive(Clone, Debug)]
26pub struct Example {48pub struct Example {
49 pub name: String,
50 /// Content of `base.toml`
27 pub base: ExampleBase,51 pub base: ExampleBase,
28 52 /// Content of `prompt.md`
29 /// Content of the prompt.md file
30 pub prompt: String,53 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)]
61pub struct RunOutput {
62 pub repository_diff: String,
63 pub diagnostics: String,
64 pub response_count: usize,
65 pub token_usage: TokenUsage,
66}
31 67
32 /// Content of the rubric.md file68#[derive(Debug, Clone, Serialize, Deserialize)]
33 pub _rubric: String,69pub struct JudgeInput {
70 pub repository_diff: String,
71 pub criteria: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct JudgeOutput {
76 pub analysis: String,
77 pub score: u32,
34}78}
35 79
36impl Example {80impl Example {
37 /// Load an example from a directory containing base.toml, prompt.md, and rubric.md81 /// Load an example from a directory containing base.toml, prompt.md, and criteria.md
38 pub fn load_from_directory<P: AsRef<Path>>(dir_path: P) -> Result<Self> {82 pub fn load_from_directory(dir_path: &Path, run_dir: &Path) -> Result<Self> {
39 let base_path = dir_path.as_ref().join("base.toml");83 let name = dir_path.file_name().unwrap().to_string_lossy().to_string();
40 let prompt_path = dir_path.as_ref().join("prompt.md");84 let base_path = dir_path.join("base.toml");
41 let rubric_path = dir_path.as_ref().join("rubric.md");85 let prompt_path = dir_path.join("prompt.md");
86 let criteria_path = dir_path.join("criteria.md");
42 87
43 let mut base: ExampleBase = toml::from_str(&fs::read_to_string(&base_path)?)?;88 let log_file_path = run_dir.join(format!(
44 base.path = base.path.canonicalize()?;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);
45 94
46 Ok(Example {95 Ok(Example {
47 base,96 name,
48 prompt: fs::read_to_string(prompt_path)?,97 base: toml::from_str(&fs::read_to_string(&base_path)?)?,
49 _rubric: fs::read_to_string(rubric_path)?,98 prompt: fs::read_to_string(prompt_path.clone())?,
99 criteria: fs::read_to_string(criteria_path.clone())?,
100 log_file,
50 })101 })
51 }102 }
52 103
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
53 /// Set up the example by checking out the specified Git revision112 /// Set up the example by checking out the specified Git revision
54 pub fn setup(&self) -> Result<()> {113 pub async fn setup(&self) -> Result<()> {
55 // Check if the directory exists114 let repo_path = repo_path_for_url(&self.base.url);
56 let path = Path::new(&self.base.path);115
57 anyhow::ensure!(path.exists(), "Path does not exist: {:?}", self.base.path);116 run_git(
58 117 &repo_path,
59 // Change to the project directory and checkout the specified revision118 &["fetch", "--depth", "1", "origin", &self.base.revision],
60 let output = Command::new("git")119 )
61 .current_dir(&self.base.path)120 .await?;
62 .arg("checkout")121
63 .arg(&self.base.revision)122 let worktree_path = self.worktree_path();
64 .output()?;123
65 anyhow::ensure!(124 if worktree_path.is_dir() {
66 output.status.success(),125 println!("{}> Resetting existing worktree", self.name);
67 "Failed to checkout revision {}: {}",126
68 self.base.revision,127 // TODO: consider including "-x" to remove ignored files. The downside of this is that
69 String::from_utf8_lossy(&output.stderr),128 // it will also remove build artifacts, and so prevent incremental reuse there.
70 );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 }
71 149
72 Ok(())150 Ok(())
73 }151 }
74 152
75 pub fn run(153 pub fn run(
76 self,154 &self,
77 model: Arc<dyn LanguageModel>,155 model: Arc<dyn LanguageModel>,
78 app_state: Arc<AgentAppState>,156 app_state: Arc<AgentAppState>,
79 cx: &mut App,157 cx: &mut App,
80 ) -> Task<Result<()>> {158 ) -> Task<Result<RunOutput>> {
81 let project = Project::local(159 let project = Project::local(
82 app_state.client.clone(),160 app_state.client.clone(),
83 app_state.node_runtime.clone(),161 app_state.node_runtime.clone(),
@@ -89,30 +167,119 @@ impl Example { · 5 unmodified lines
89 cx,167 cx,
90 );168 );
91 169
170 let worktree_path = self.worktree_path();
92 let worktree = project.update(cx, |project, cx| {171 let worktree = project.update(cx, |project, cx| {
93 project.create_worktree(self.base.path, true, cx)172 project.create_worktree(&worktree_path, true, cx)
94 });173 });
95 174
96 let tools = Arc::new(ToolWorkingSet::default());175 let tools = Arc::new(ToolWorkingSet::default());
97 let thread_store =176 let thread_store =
98 ThreadStore::load(project.clone(), tools, app_state.prompt_builder.clone(), cx);177 ThreadStore::load(project.clone(), tools, app_state.prompt_builder.clone(), cx);
178 let this = self.clone();
99 179
100 println!("USER:");
101 println!("{}", self.prompt);
102 println!("ASSISTANT:");
103 cx.spawn(async move |cx| {180 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
105 let thread_store = thread_store.await;261 let thread_store = thread_store.await;
106 let thread =262 let thread =
107 thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;263 thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;
108 264
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
109 let (tx, rx) = oneshot::channel();273 let (tx, rx) = oneshot::channel();
110 let mut tx = Some(tx);274 let mut tx = Some(tx);
111 275
112 let _subscription =276 let _subscription = cx.subscribe(&thread, {
113 cx.subscribe(277 let log_file = this.log_file.clone();
114 &thread,278 let name = this.name.clone();
115 move |thread, event: &ThreadEvent, cx| match event {279 move |thread, event: &ThreadEvent, cx| {
280 let mut log_file = log_file.lock().unwrap();
281
282 match event {
116 ThreadEvent::Stopped(reason) => match reason {283 ThreadEvent::Stopped(reason) => match reason {
117 Ok(StopReason::EndTurn) => {284 Ok(StopReason::EndTurn) => {
118 if let Some(tx) = tx.take() {285 if let Some(tx) = tx.take() {
@@ -137,15 +304,16 @@ impl Example { · 18 unmodified lines
137 }304 }
138 }305 }
139 ThreadEvent::StreamedAssistantText(_, chunk) => {306 ThreadEvent::StreamedAssistantText(_, chunk) => {
140 print!("{}", chunk);307 write!(&mut log_file, "{}", chunk).log_err();
141 }308 }
142 ThreadEvent::StreamedAssistantThinking(_, chunk) => {309 ThreadEvent::StreamedAssistantThinking(_, chunk) => {
143 print!("{}", chunk);310 write!(&mut log_file, "{}", chunk).log_err();
144 }311 }
145 ThreadEvent::UsePendingTools { tool_uses } => {312 ThreadEvent::UsePendingTools { tool_uses } => {
146 println!("\n\nUSING TOOLS:");313 writeln!(&mut log_file, "\n\nUSING TOOLS:").log_err();
147 for tool_use in tool_uses {314 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();
149 }317 }
150 }318 }
151 ThreadEvent::ToolFinished {319 ThreadEvent::ToolFinished {
@@ -154,25 +322,331 @@ impl Example { · 2 unmodified lines
154 ..322 ..
155 } => {323 } => {
156 if let Some(tool_use) = pending_tool_use {324 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();
158 }328 }
159 if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {329 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();
161 }332 }
162 }333 }
163 _ => {}334 _ => {}
164 },335 }
165 )?;336
337 log_file.flush().log_err();
338 }
339 })?;
166 340
167 thread.update(cx, |thread, cx| {341 thread.update(cx, |thread, cx| {
168 let context = vec![];342 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);
170 thread.send_to_model(model, RequestKind::Chat, cx);344 thread.send_to_model(model, RequestKind::Chat, cx);
171 })?;345 })?;
172 346
173 rx.await??;347 rx.await??;
174 348
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 })
176 })374 })
177 }375 }
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
426fn 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
483fn 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
490async 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
527fn 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
536fn 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
555pub 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
566pub 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
587pub 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)]
617mod 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 }
178}652}
crates/eval/src/judge_prompt.hbs+25−0
@@ -0,0 +1,25 @@
1You are an expert software developer tasked with evaluating the following changes to a codebase:
2
3<changes>
4{{repository_diff}}
5</changes>
6
7Use the following criteria to score the above changes.
8
9<criteria>
10{{criteria}}
11</criteria>
12
13Based 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
18Be suspicious of the changes because they were generated by an LLM.
19Sometimes the LLM decides to change random code, so if the changes are not mentioned in the criteria, penalize the score.
20Analyze 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
83 ToolUse,83 ToolUse,
84}84}
85 85
86#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]86#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Default)]
87pub struct TokenUsage {87pub struct TokenUsage {
88 #[serde(default, skip_serializing_if = "is_default")]88 #[serde(default, skip_serializing_if = "is_default")]
89 pub input_tokens: u32,89 pub input_tokens: u32,