Skip to content

Commit b94e444

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
AI: define getNetworkActivity for Perf Insights agent
This CL allows the LLM to call the function to get the network activity for a given insight. In the design doc I explored using time ranges, but so far I am going for an approach where we query based on the active insight. I think this reduces the chance of the AI incorrectly passing invalid or incorrect times, but we can explore and experiment. Bug: 394552594 Change-Id: I2d5c054391d5802ce46f5c149ee7ce194630fa68 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6276941 Auto-Submit: Jack Franklin <[email protected]> Reviewed-by: Nikolay Vitkov <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]> Commit-Queue: Jack Franklin <[email protected]>
1 parent 40fc4f5 commit b94e444

File tree

3 files changed

+74
-7
lines changed

3 files changed

+74
-7
lines changed

front_end/panels/ai_assistance/agents/PerformanceInsightsAgent.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import type * as Common from '../../../core/common/common.js';
66
import type * as Host from '../../../core/host/host.js';
77
import * as Trace from '../../../models/trace/trace.js';
88
import {mockAidaClient} from '../../../testing/AiAssistanceHelpers.js';
9+
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
10+
import {getInsightOrError} from '../../../testing/InsightHelpers.js';
11+
import {TraceLoader} from '../../../testing/TraceLoader.js';
912
import * as TimelineUtils from '../../timeline/utils/utils.js';
1013
import {
14+
type ActionResponse,
1115
InsightContext,
1216
PerformanceInsightFormatter,
1317
PerformanceInsightsAgent,
1418
ResponseType,
19+
TraceEventFormatter,
1520
} from '../ai_assistance.js';
1621

1722
const FAKE_LCP_MODEL = {
@@ -24,7 +29,7 @@ const FAKE_LCP_MODEL = {
2429
} as const;
2530
const FAKE_PARSED_TRACE = {} as unknown as Trace.Handlers.Types.ParsedTrace;
2631

27-
describe('PerformanceInsightsAgent', () => {
32+
describeWithEnvironment('PerformanceInsightsAgent', () => {
2833
describe('handleContextDetails', () => {
2934
it('outputs the right context for the initial query from the user', async () => {
3035
const mockInsight = new TimelineUtils.InsightAIContext.ActiveInsight(FAKE_LCP_MODEL, FAKE_PARSED_TRACE);
@@ -67,6 +72,7 @@ describe('PerformanceInsightsAgent', () => {
6772
]);
6873
});
6974
});
75+
7076
describe('enhanceQuery', () => {
7177
it('adds the context to the query from the user', async () => {
7278
const agent = new PerformanceInsightsAgent({
@@ -86,4 +92,42 @@ What is this?`;
8692
assert.strictEqual(finalQuery, expected);
8793
});
8894
});
95+
96+
describe('function calls', () => {
97+
it('calls getNetworkActivity', async function() {
98+
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'lcp-images.json.gz');
99+
assert.isOk(insights);
100+
const [firstNav] = parsedTrace.Meta.mainFrameNavigations;
101+
const lcpPhases = getInsightOrError('LCPPhases', insights, firstNav);
102+
const agent = new PerformanceInsightsAgent({
103+
aidaClient: mockAidaClient(
104+
[[{explanation: '', functionCalls: [{name: 'getNetworkActivity', args: {}}]}], [{explanation: 'done'}]])
105+
});
106+
const activeInsight = new TimelineUtils.InsightAIContext.ActiveInsight(lcpPhases, parsedTrace);
107+
const context = new InsightContext(activeInsight);
108+
109+
const responses = await Array.fromAsync(agent.run('test', {selected: context}));
110+
const action = responses.find(response => response.type === ResponseType.ACTION);
111+
112+
// Find the requests we expect the handler to have returned.
113+
const expectedRequestUrls = [
114+
'https://chromedevtools.github.io/performance-stories/lcp-large-image/index.html',
115+
'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@1,800',
116+
'https://chromedevtools.github.io/performance-stories/lcp-large-image/app.css',
117+
'https://via.placeholder.com/50.jpg', 'https://via.placeholder.com/2000.jpg'
118+
];
119+
120+
const requests = expectedRequestUrls.map(url => {
121+
const match = parsedTrace.NetworkRequests.byTime.find(r => r.args.data.url === url);
122+
assert.isOk(match, `no request found for ${url}`);
123+
return match;
124+
});
125+
126+
const expectedRequestsOutput = requests.map(r => TraceEventFormatter.networkRequest(r, parsedTrace));
127+
const expectedOutput = JSON.stringify({requests: expectedRequestsOutput});
128+
129+
assert.exists(action);
130+
assert.deepEqual(action, {type: 'action' as ActionResponse['type'], output: expectedOutput, canceled: false});
131+
});
132+
});
89133
});

front_end/panels/ai_assistance/agents/PerformanceInsightsAgent.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
import * as Host from '../../../core/host/host.js';
66
import type * as Lit from '../../../ui/lit/lit.js';
7-
import type * as TimelineUtils from '../../timeline/utils/utils.js';
7+
import * as TimelineUtils from '../../timeline/utils/utils.js';
88
import * as PanelUtils from '../../utils/utils.js';
9-
import {PerformanceInsightFormatter} from '../data_formatters/PerformanceInsightFormatter.js';
9+
import {PerformanceInsightFormatter, TraceEventFormatter} from '../data_formatters/PerformanceInsightFormatter.js';
1010

1111
import {
1212
type AgentOptions as BaseAgentOptions,
@@ -79,8 +79,6 @@ export class InsightContext extends ConversationContext<TimelineUtils.InsightAIC
7979
}
8080

8181
export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAIContext.ActiveInsight> {
82-
// TODO: make use of the Insight.
83-
// eslint-disable-next-line no-unused-private-class-members
8482
#insight: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|undefined;
8583

8684
override async *
@@ -113,7 +111,30 @@ export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAICon
113111

114112
constructor(opts: BaseAgentOptions) {
115113
super(opts);
116-
// TODO: define the set of functions for the LLM.
114+
115+
this.declareFunction<Record<never, unknown>, {
116+
requests: string[],
117+
}>('getNetworkActivity', {
118+
description: 'Returns relevant network requests for the selected insight',
119+
parameters: {
120+
type: Host.AidaClient.ParametersTypes.OBJECT,
121+
description: '',
122+
nullable: true,
123+
properties: {},
124+
},
125+
handler: async () => {
126+
if (!this.#insight) {
127+
return {error: 'No insight available'};
128+
}
129+
const activeInsight = this.#insight.getItem();
130+
const requests = TimelineUtils.InsightAIContext.AIQueries.networkRequests(
131+
activeInsight.insight,
132+
activeInsight.parsedTrace,
133+
);
134+
const formatted = requests.map(r => TraceEventFormatter.networkRequest(r, activeInsight.parsedTrace));
135+
return {result: {requests: formatted}};
136+
},
137+
});
117138
}
118139

119140
override async enhanceQuery(

front_end/panels/timeline/utils/InsightAIContext.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import * as Trace from '../../../models/trace/trace.js';
1313
*/
1414
export class ActiveInsight {
1515
#insight: Trace.Insights.Types.InsightModel<{}, {}>;
16-
// eslint-disable-next-line no-unused-private-class-members
1716
#parsedTrace: Trace.Handlers.Types.ParsedTrace;
1817

1918
constructor(insight: Trace.Insights.Types.InsightModel<{}, {}>, parsedTrace: Trace.Handlers.Types.ParsedTrace) {
@@ -24,6 +23,9 @@ export class ActiveInsight {
2423
get insight(): Readonly<Trace.Insights.Types.InsightModel<{}, {}>> {
2524
return this.#insight;
2625
}
26+
get parsedTrace(): Trace.Handlers.Types.ParsedTrace {
27+
return this.#parsedTrace;
28+
}
2729

2830
title(): string {
2931
return this.#insight.title;

0 commit comments

Comments
 (0)