RPP: create new annotations AI agent

Required mostly for metrics so we can track its usage accurately rather
than bundled in with the existing "Ask AI" integration.

(Googlers: see the attached bug for a link to the backend CL, this needs
to land first before this can land).

Bug: 406795908
Change-Id: Ibf0fff042f72a6e622e9d9d9e87769a71feb23af
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6407494
Reviewed-by: Nikolay Vitkov <nvitkov@chromium.org>
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
This commit is contained in:
Jack Franklin
2025-03-28 03:57:43 -07:00
committed by Devtools-frontend LUCI CQ
parent 5f7e033b32
commit 9995b96203
10 changed files with 88 additions and 53 deletions
+1
View File
@@ -989,6 +989,7 @@ grd_files_debug_sources = [
"front_end/models/ai_assistance/agents/NetworkAgent.js",
"front_end/models/ai_assistance/agents/PatchAgent.js",
"front_end/models/ai_assistance/agents/PerformanceAgent.js",
"front_end/models/ai_assistance/agents/PerformanceAnnotationsAgent.js",
"front_end/models/ai_assistance/agents/PerformanceInsightsAgent.js",
"front_end/models/ai_assistance/agents/StylingAgent.js",
"front_end/models/ai_assistance/data_formatters/FileFormatter.js",
+2
View File
@@ -124,6 +124,8 @@ export enum ClientFeature {
CHROME_NETWORK_AGENT = 7,
// Chrome AI Assistance Performance Agent.
CHROME_PERFORMANCE_AGENT = 8,
// Chrome AI Annotations Performance Agent
CHROME_PERFORMANCE_ANNOTATIONS_AGENT = 20,
// Chrome AI Assistance File Agent.
CHROME_FILE_AGENT = 9,
// Chrome AI Patch Agent.
+2
View File
@@ -19,6 +19,7 @@ devtools_module("ai_assistance") {
"agents/NetworkAgent.ts",
"agents/PatchAgent.ts",
"agents/PerformanceAgent.ts",
"agents/PerformanceAnnotationsAgent.ts",
"agents/PerformanceInsightsAgent.ts",
"agents/StylingAgent.ts",
"data_formatters/FileFormatter.ts",
@@ -76,6 +77,7 @@ ts_library("unittests") {
"agents/NetworkAgent.test.ts",
"agents/PatchAgent.test.ts",
"agents/PerformanceAgent.test.ts",
"agents/PerformanceAnnotationsAgent.test.ts",
"agents/PerformanceInsightsAgent.test.ts",
"agents/StylingAgent.test.ts",
"data_formatters/FileFormatter.test.ts",
@@ -226,22 +226,4 @@ self: 3
assert.isFalse(enhancedQuery3.includes(mockAiCallTree.serialize()));
});
});
describe('generating an AI entry label', () => {
it('generates a label from the final answer and trims newlines', async function() {
const agent = new PerformanceAgent({
aidaClient: mockAidaClient([[{
explanation: 'hello world\n',
}]]),
});
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const evalScriptEvent = parsedTrace.Renderer.allTraceEntries.find(
event => event.name === Trace.Types.Events.Name.EVALUATE_SCRIPT && event.ts === 122411195649);
assert.exists(evalScriptEvent);
const aiCallTree = TimelineUtils.AICallTree.AICallTree.fromEvent(evalScriptEvent, parsedTrace);
assert.isOk(aiCallTree);
const label = await agent.generateAIEntryLabel(aiCallTree);
assert.strictEqual(label, 'hello world');
});
});
});
@@ -201,7 +201,11 @@ export class CallTreeContext extends ConversationContext<TimelineUtils.AICallTre
export class PerformanceAgent extends AiAgent<TimelineUtils.AICallTree.AICallTree> {
override readonly type = AgentType.PERFORMANCE;
readonly preamble = preamble;
readonly clientFeature = Host.AidaClient.ClientFeature.CHROME_PERFORMANCE_AGENT;
// We have to set the type of clientFeature here to be the entire enum
// because in PerformanceAnnotationsAgent.ts we override it.
// TODO(b/406961576): split the agents apart rather than have one extend the other.
readonly clientFeature: Host.AidaClient.ClientFeature = Host.AidaClient.ClientFeature.CHROME_PERFORMANCE_AGENT;
get userTier(): string|undefined {
return Root.Runtime.hostConfig.devToolsAiAssistancePerformanceAgent?.userTier;
}
@@ -251,33 +255,4 @@ export class PerformanceAgent extends AiAgent<TimelineUtils.AICallTree.AICallTre
const perfEnhancementQuery = treeStr ? `${treeStr}\n\n# User request\n\n` : '';
return `${perfEnhancementQuery}${query}`;
}
/**
* Used in the Performance panel to automatically generate a label for a selected entry.
*/
async generateAIEntryLabel(callTree: TimelineUtils.AICallTree.AICallTree): Promise<string> {
const context = new CallTreeContext(callTree);
const response = await Array.fromAsync(this.run(AI_LABEL_GENERATION_PROMPT, {selected: context}));
const lastResponse = response.at(-1);
if (lastResponse && lastResponse.type === ResponseType.ANSWER && lastResponse.complete === true) {
return lastResponse.text.trim();
}
throw new Error('Failed to generate AI entry label');
}
}
const AI_LABEL_GENERATION_PROMPT = `## Instruction:
Generate a concise label (max 60 chars, single line) describing the *user-visible effect* of the selected call tree's activity, based solely on the provided call tree data.
## Strict Constraints:
- Output must be a single line of text.
- Maximum 60 characters.
- No full stops.
- Focus on user impact, not internal operations.
- Do not include the name of the selected event.
- Do not make assumptions about when the activity happened.
- Base the description only on the information present within the call tree data.
- Prioritize brevity.
- Only include third-party script names if their identification is highly confident.
- Always use "responsiveness" rather than "user interaction responsiveness".
`;
@@ -0,0 +1,30 @@
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as TimelineUtils from '../../../panels/timeline/utils/utils.js';
import {mockAidaClient} from '../../../testing/AiAssistanceHelpers.js';
import {
describeWithEnvironment,
} from '../../../testing/EnvironmentHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../../trace/trace.js';
import {PerformanceAnnotationsAgent} from '../ai_assistance.js';
describeWithEnvironment('PerformanceAnnotationsAgent', () => {
it('generates a label from the response', async function() {
const agent = new PerformanceAnnotationsAgent({
aidaClient: mockAidaClient([[{
explanation: 'hello world\n',
}]]),
});
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const evalScriptEvent = parsedTrace.Renderer.allTraceEntries.find(
event => event.name === Trace.Types.Events.Name.EVALUATE_SCRIPT && event.ts === 122411195649);
assert.exists(evalScriptEvent);
const aiCallTree = TimelineUtils.AICallTree.AICallTree.fromEvent(evalScriptEvent, parsedTrace);
assert.isOk(aiCallTree);
const label = await agent.generateAIEntryLabel(aiCallTree);
assert.strictEqual(label, 'hello world');
});
});
@@ -0,0 +1,42 @@
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Host from '../../../core/host/host.js';
import type * as TimelineUtils from '../../../panels/timeline/utils/utils.js';
import {ResponseType} from './AiAgent.js';
import {CallTreeContext, PerformanceAgent} from './PerformanceAgent.js';
export class PerformanceAnnotationsAgent extends PerformanceAgent {
override readonly clientFeature = Host.AidaClient.ClientFeature.CHROME_PERFORMANCE_ANNOTATIONS_AGENT;
/**
* Used in the Performance panel to automatically generate a label for a selected entry.
*/
async generateAIEntryLabel(callTree: TimelineUtils.AICallTree.AICallTree): Promise<string> {
const context = new CallTreeContext(callTree);
const response = await Array.fromAsync(this.run(AI_LABEL_GENERATION_PROMPT, {selected: context}));
const lastResponse = response.at(-1);
if (lastResponse && lastResponse.type === ResponseType.ANSWER && lastResponse.complete === true) {
return lastResponse.text.trim();
}
throw new Error('Failed to generate AI entry label');
}
}
const AI_LABEL_GENERATION_PROMPT = `## Instruction:
Generate a concise label (max 60 chars, single line) describing the *user-visible effect* of the selected call tree's activity, based solely on the provided call tree data.
## Strict Constraints:
- Output must be a single line of text.
- Maximum 60 characters.
- No full stops.
- Focus on user impact, not internal operations.
- Do not include the name of the selected event.
- Do not make assumptions about when the activity happened.
- Base the description only on the information present within the call tree data.
- Prioritize brevity.
- Only include third-party script names if their identification is highly confident.
- Always use "responsiveness" rather than "user interaction responsiveness".
`;
@@ -8,6 +8,7 @@ export * from './agents/AiAgent.js';
export * from './agents/FileAgent.js';
export * from './agents/NetworkAgent.js';
export * from './agents/PerformanceAgent.js';
export * from './agents/PerformanceAnnotationsAgent.js';
export * from './agents/PerformanceInsightsAgent.js';
export * from './agents/StylingAgent.js';
export * from './agents/PatchAgent.js';
@@ -590,7 +590,7 @@ describeWithEnvironment('Overlays', () => {
const generateButton = elementsWrapper.querySelector<HTMLElement>('.ai-label-button');
assert.isOk(generateButton, 'could not find "Generate label" button');
assert.isTrue(generateButton.classList.contains('enabled'));
const agent = new AiAssistanceModels.PerformanceAgent({
const agent = new AiAssistanceModels.PerformanceAnnotationsAgent({
aidaClient: mockAidaClient([[{
explanation: 'This is an interesting entry',
metadata: {
@@ -189,7 +189,7 @@ export class EntryLabelOverlay extends HTMLElement {
#callTree: Utils.AICallTree.AICallTree|null = null;
// Creates or gets the setting if it exists.
#aiAnnotationsEnabledSetting = Common.Settings.Settings.instance().createSetting('ai-annotations-enabled', false);
#performanceAgent = new AiAssistanceModels.PerformanceAgent({
#agent = new AiAssistanceModels.PerformanceAnnotationsAgent({
aidaClient: new Host.AidaClient.AidaClient(),
serverSideLoggingEnabled: isAiAssistanceServerSideLoggingEnabled(),
});
@@ -249,8 +249,8 @@ export class EntryLabelOverlay extends HTMLElement {
/**
* So we can provide a mocked agent in tests. Do not call this method outside of a test!
*/
overrideAIAgentForTest(agent: AiAssistanceModels.PerformanceAgent): void {
this.#performanceAgent = agent;
overrideAIAgentForTest(agent: AiAssistanceModels.PerformanceAnnotationsAgent): void {
this.#agent = agent;
}
connectedCallback(): void {
@@ -521,7 +521,7 @@ export class EntryLabelOverlay extends HTMLElement {
this.#focusInputBox();
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
this.#label = await this.#performanceAgent.generateAIEntryLabel(this.#callTree);
this.#label = await this.#agent.generateAIEntryLabel(this.#callTree);
this.dispatchEvent(new EntryLabelChangeEvent(this.#label));
this.#inputField.innerText = this.#label;