Skip to content

Commit 10a2fef

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
Eval: use the GCP bucket and store examples there
This CL uses the newly created GCP bucket in two ways: 1. `gclient sync` will now fetch the contents of the bucket. 2. The `upload_to_gcp.ts` script will upload the latest contents of the directory to GCP, and will then update the `DEPs` file with the latest revision. It first generates a hash of all the files in the directory to ensure that we do not end up uploading a folder that is identical to the existing one. [email protected] Bug: 436223818 Change-Id: I28641b7eee64945c78a5fbdb3378f3633fc6a0ea Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6862825 Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Alex Rudenko <[email protected]>
1 parent c6f27ce commit 10a2fef

File tree

6 files changed

+177
-7
lines changed

6 files changed

+177
-7
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ npm-debug.log
2828
/scripts/ai_assistance/auto-run/data
2929
/scripts/ai_assistance/performance-trace-downloads
3030
/scripts/ai_assistance/auto-run/performance-trace-downloads
31-
/scripts/ai_assistance/suite/outputs/**/*.json
31+
# This folder is populated from the GCP bucket and managed by gclient sync.
32+
/scripts/ai_assistance/suite/outputs
3233

3334
/build
3435
/buildtools

DEPS

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,19 @@ deps = {
216216
},
217217
],
218218
},
219+
"scripts/ai_assistance/suite/outputs": {
220+
"dep_type": "gcs",
221+
'condition': 'checkout_ai_evals == True',
222+
"bucket": "chrome-devtools-ai-evals",
223+
"objects": [
224+
{
225+
"object_name": "f0e8e7b99dc61f7a943bfdf284552982c63bdf8d6217091f5260bc8ebd84ca9f",
226+
"sha256sum": "af579f30f2384089e1bece67db9afb71b902aa6ff99cb9749d4694ce53783670",
227+
"size_bytes": 3582,
228+
"generation": 1755705853621054
229+
}
230+
]
231+
},
219232
'third_party/node/win': {
220233
'dep_type': 'gcs',
221234
'condition': 'host_os == "win" and build_with_chromium == False and non_git_source',
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# DevTools AI Evals Suite
2+
3+
This folder contains the scripts required to execute the DevTools eval suite.
4+
5+
At this time, this is being heavily iterated on and may change rapidly. Chat to jacktfranklin@ if you have any questions or feedback.
6+
7+
## Getting started
8+
9+
### 1: get the outputs from GCP
10+
11+
The actual output files you need to run the suite are hosted in a GCP bucket. The contents are fetched for you by `gclient sync` but only if you set the `checkout_ai_evals` arg in your `.gclient` config:
12+
13+
```
14+
solutions = [
15+
{
16+
"name": "devtools-frontend",
17+
"url": "https://chromium.googlesource.com/devtools/devtools-frontend.git",
18+
"deps_file": "DEPS",
19+
"managed": False,
20+
"custom_deps": {},
21+
"custom_vars": {
22+
"checkout_ai_evals": True,
23+
}
24+
},
25+
]
26+
```
27+
28+
Note that you have to be a Google employee to access this.
29+
30+
### 2: set your Gemini API key.
31+
32+
Evaluations that engage an LLM will expect to find a `GEMINI_API_KEY` environment variable defined.
33+
34+
## Running the suite
35+
36+
Run `cd scripts/ai_assistance && npm run eval-suite` to execute the suite.
37+
38+
## Adding new outputs
39+
40+
Once you have new outputs you want to put into the set, move them into the right place in the `suite/outputs/outputs` folder.:
41+
42+
The structure of files in this folder is like so: `outputs/type/YYYY-MM-DD/label-XYZ.json`.
43+
44+
- Type here should map roughly to an agent, e.g. "performance".
45+
- For actual outputs, store them as JSON files within a `YYYY-MM-DD` folder.
46+
- The file name must start with a label - e.g. `lcp-breakdown`. After that, it can have any other content you like. You can have one JSON file with multiple outputs, or you can have multiple files with outputs. If there are multiple files, they will be merged accordingly when the eval suite is run.
47+
48+
Then, run (from the DevTools root directory in this case, but it doesn't matter):
49+
50+
```
51+
node scripts/ai_assistance/suite/upload_to_gcp.ts
52+
```
53+
54+
This will upload the changes to the GCP bucket and update the `DEPS` file for you, which you should ensure you commit in a CL.
55+
56+
If you get any authorisation errors, run `gsutil.py config` to refresh your authentication status.
57+

scripts/ai_assistance/suite/helpers/outputs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as path from 'node:path';
88

99
import type {Conversation, EvalFileOutput} from '../types';
1010

11-
const BASE_DIR = path.join(path.dirname(import.meta.filename), '..', 'outputs');
11+
const BASE_DIR = path.join(path.dirname(import.meta.filename), '..', 'outputs', 'outputs');
1212

1313
export function getMarkdownConversation(example: Conversation): string {
1414
let markdown = '';

scripts/ai_assistance/suite/outputs/README.md

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {execSync} from 'node:child_process';
6+
import crypto from 'node:crypto';
7+
import * as fs from 'node:fs/promises';
8+
import * as path from 'node:path';
9+
import {fileURLToPath} from 'node:url';
10+
11+
const filename = fileURLToPath(import.meta.url);
12+
const dirname = path.dirname(filename);
13+
14+
// the upload_to_google_storage_first_class.py script only lets you upload
15+
// folders in the immediate directory, so we run it from the `outputs`
16+
// directory. Remember that the outputs directory is nested and looks like
17+
// this:
18+
// scripts/ai_assistance/suite/outputs/outputs
19+
// We need to run the script from the first outputs directory, uploading the second.
20+
const CWD_TO_UPLOAD_FROM = path.join(dirname, 'outputs');
21+
22+
const DEVTOOLS_ROOT = path.join(dirname, '..', '..', '..');
23+
24+
const hash = await getDirectoryHash(path.join(CWD_TO_UPLOAD_FROM, 'outputs'));
25+
26+
const output = execSync(
27+
`upload_to_google_storage_first_class.py --bucket chrome-devtools-ai-evals --object-name ${hash} outputs`,
28+
{cwd: CWD_TO_UPLOAD_FROM, encoding: 'utf8'});
29+
30+
// The output contains some logs and then a JSON object that we want to use to
31+
// update our DEPS with the newest data.
32+
const jsonRegex = /{.*}/s;
33+
const jsonMatch = output.match(jsonRegex);
34+
if (!jsonMatch) {
35+
console.error('ERROR: could not parse DEPs JSON out of upload response.');
36+
process.exit(1);
37+
}
38+
39+
console.log('Data uploaded to GCP bucket.');
40+
41+
const data = JSON.parse(jsonMatch[0]) as {
42+
path: {
43+
// eslint-disable-next-line @typescript-eslint/naming-convention
44+
dep_type: 'gcs',
45+
bucket: 'chrome-devtools-ai-evals',
46+
objects: [{
47+
// eslint-disable-next-line @typescript-eslint/naming-convention
48+
object_name: string,
49+
sha256sum: string,
50+
// eslint-disable-next-line @typescript-eslint/naming-convention
51+
size_bytes: number,
52+
generation: number,
53+
}],
54+
},
55+
};
56+
57+
// We now need to update the DEPs entry. We can use `gclient setdeps` which
58+
// expects a particular format (taken from the --help output):
59+
// If it is a GCS dependency, dep must be of the form path@object_name,sha256sum,size_bytes,generation
60+
const DEPS_PATH = 'scripts/ai_assistance/suite/outputs';
61+
const setDepInput = [...Object.values(data.path.objects[0])].join(',');
62+
63+
execSync(`gclient setdep -r ${DEPS_PATH}@${setDepInput}`, {cwd: DEVTOOLS_ROOT, encoding: 'utf8', stdio: 'ignore'});
64+
65+
console.log('DEPS file updated. You should commit this change.');
66+
67+
/**
68+
* Walks a directory and creates a hash of it based on the contents of all the
69+
* files. Used to generate the name of the TAR file that gets put onto GCP. We
70+
* do it like this to ensure we don't upload the same contents again.
71+
*/
72+
async function getDirectoryHash(directoryPath: string): Promise<string> {
73+
const files: string[] = [];
74+
75+
async function walk(currentPath: string) {
76+
const items = await fs.readdir(currentPath, {withFileTypes: true});
77+
78+
for (const item of items) {
79+
const fullPath = path.join(currentPath, item.name);
80+
if (item.isDirectory()) {
81+
await walk(fullPath);
82+
} else if (item.name.endsWith('.json')) {
83+
files.push(fullPath);
84+
}
85+
}
86+
}
87+
88+
await walk(directoryPath);
89+
// Sort paths for a consistent hash
90+
files.sort();
91+
92+
const hasher = crypto.createHash('sha256');
93+
for (const filePath of files) {
94+
// Get a hash of the file's content
95+
const fileContent = await fs.readFile(filePath);
96+
const fileHash = crypto.createHash('sha256').update(fileContent).digest('hex');
97+
98+
// Combine the relative path and file hash
99+
const relativePath = path.relative(directoryPath, filePath);
100+
hasher.update(`${relativePath}:${fileHash}`);
101+
}
102+
103+
return hasher.digest('hex');
104+
}

0 commit comments

Comments
 (0)