Skip to content

Commit 3af2a18

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[AI] Persist conversation for external PERFORMANCE_ANALYZE
Persists the conversation data for the new external performance request type. The TimelinePanel stores the state, and invalidates it whenever a new trace is recorded. This enables this flow: * MCP client records a trace * MCP client asks a performance question via performance tool, AIDA agent perhaps asks a follow up question * Developer responds (ex: "let's look at LCP") * MCP client calls the performance tool again, just passing the next prompt (ex: "let's look at LCP"), and the same AIDA conversation agent is used (so all the context is still there) * etc... Developer can continue asking performance questions about the trace, and the MCP client will continue calling the performance tool to communicate with the same AIDA agent Bug: 425270067 Change-Id: I6376f61c9d17f1e6f47947c4aca821dde9f76b05 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6885880 Auto-Submit: Connor Clark <cjamcl@chromium.org> Commit-Queue: Connor Clark <cjamcl@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Wolfgang Beyer <wolfi@chromium.org>
1 parent c73aee1 commit 3af2a18

File tree

5 files changed

+94
-65
lines changed

5 files changed

+94
-65
lines changed

front_end/entrypoints/main/MainImpl.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,15 +1109,8 @@ export async function handleExternalRequestGenerator(input: ExternalRequestInput
11091109
});
11101110
}
11111111
case 'PERFORMANCE_ANALYZE': {
1112-
const AiAssistanceModel = await import('../../models/ai_assistance/ai_assistance.js');
11131112
const TimelinePanel = await import('../../panels/timeline/timeline.js');
1114-
const traceModel = TimelinePanel.TimelinePanel.TimelinePanel.instance().model;
1115-
const conversationHandler = AiAssistanceModel.ConversationHandler.instance();
1116-
return await conversationHandler.handleExternalRequest({
1117-
conversationType: AiAssistanceModel.ConversationType.PERFORMANCE_FULL,
1118-
prompt: input.args.prompt,
1119-
traceModel,
1120-
});
1113+
return await TimelinePanel.TimelinePanel.TimelinePanel.handleExternalAnalyzeRequest(input.args.prompt);
11211114
}
11221115
case 'NETWORK_DEBUGGER': {
11231116
const AiAssistanceModel = await import('../../models/ai_assistance/ai_assistance.js');

front_end/models/ai_assistance/ConversationHandler.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,17 @@ export interface ExternalPerformanceInsightsRequestParameters {
5050
traceModel: Trace.TraceModel.Model;
5151
}
5252

53+
export interface ExternalPerformanceAIConversationData {
54+
conversationHandler: ConversationHandler;
55+
conversation: Conversation;
56+
agent: AiAgent<unknown>;
57+
selected: PerformanceTraceContext;
58+
}
59+
5360
export interface ExternalPerformanceRequestParameters {
5461
conversationType: ConversationType.PERFORMANCE_FULL;
5562
prompt: string;
56-
traceModel: Trace.TraceModel.Model;
63+
data: ExternalPerformanceAIConversationData;
5764
}
5865

5966
const UIStrings = {
@@ -210,7 +217,7 @@ export class ConversationHandler {
210217
return await this.#handleExternalPerformanceInsightsConversation(
211218
parameters.prompt, parameters.insightTitle, parameters.traceModel);
212219
case ConversationType.PERFORMANCE_FULL:
213-
return await this.#handleExternalPerformanceConversation(parameters.prompt, parameters.traceModel);
220+
return await this.#handleExternalPerformanceConversation(parameters.prompt, parameters.data);
214221
case ConversationType.NETWORK:
215222
if (!parameters.requestUrl) {
216223
return this.#generateErrorResponse('The url is required for debugging a network request.');
@@ -235,22 +242,32 @@ export class ConversationHandler {
235242
}
236243
}
237244

238-
async * #doExternalConversation(opts: {
245+
async * #createAndDoExternalConversation(opts: {
239246
conversationType: ConversationType,
240247
aiAgent: AiAgent<unknown>,
241248
prompt: string,
242249
selected: NodeContext|PerformanceTraceContext|RequestContext|null,
243250
}): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> {
244251
const {conversationType, aiAgent, prompt, selected} = opts;
245-
const externalConversation = new Conversation(
252+
const conversation = new Conversation(
246253
conversationType,
247254
[],
248255
aiAgent.id,
249256
/* isReadOnly */ true,
250257
/* isExternal */ true,
251258
);
259+
return yield* this.#doExternalConversation({conversation, aiAgent, prompt, selected});
260+
}
261+
262+
async * #doExternalConversation(opts: {
263+
conversation: Conversation,
264+
aiAgent: AiAgent<unknown>,
265+
prompt: string,
266+
selected: NodeContext|PerformanceTraceContext|RequestContext|null,
267+
}): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> {
268+
const {conversation, aiAgent, prompt, selected} = opts;
252269
const generator = aiAgent.run(prompt, {selected});
253-
const generatorWithHistory = this.handleConversationWithHistory(generator, externalConversation);
270+
const generatorWithHistory = this.handleConversationWithHistory(generator, conversation);
254271
const devToolsLogs: object[] = [];
255272
for await (const data of generatorWithHistory) {
256273
if (data.type !== ResponseType.ANSWER || data.complete) {
@@ -287,7 +304,7 @@ export class ConversationHandler {
287304
await node.setAsInspectedNode();
288305
}
289306
const selected = node ? new NodeContext(node) : null;
290-
return this.#doExternalConversation({
307+
return this.#createAndDoExternalConversation({
291308
conversationType: ConversationType.STYLING,
292309
aiAgent: stylingAgent,
293310
prompt,
@@ -306,28 +323,21 @@ export class ConversationHandler {
306323
if ('error' in focusOrError) {
307324
return this.#generateErrorResponse(focusOrError.error);
308325
}
309-
return this.#doExternalConversation({
326+
return this.#createAndDoExternalConversation({
310327
conversationType: ConversationType.PERFORMANCE_INSIGHT,
311328
aiAgent: insightsAgent,
312329
prompt,
313330
selected: new PerformanceTraceContext(focusOrError.focus),
314331
});
315332
}
316333

317-
async #handleExternalPerformanceConversation(prompt: string, traceModel: Trace.TraceModel.Model):
334+
async #handleExternalPerformanceConversation(prompt: string, data: ExternalPerformanceAIConversationData):
318335
Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> {
319-
const agent = this.createAgent(ConversationType.PERFORMANCE_FULL);
320-
const focusOrError = await Tracing.ExternalRequests.getPerformanceAgentFocusToDebug(
321-
traceModel,
322-
);
323-
if ('error' in focusOrError) {
324-
return this.#generateErrorResponse(focusOrError.error);
325-
}
326336
return this.#doExternalConversation({
327-
conversationType: ConversationType.PERFORMANCE_FULL,
328-
aiAgent: agent,
337+
conversation: data.conversation,
338+
aiAgent: data.agent,
329339
prompt,
330-
selected: new PerformanceTraceContext(focusOrError.focus),
340+
selected: data.selected,
331341
});
332342
}
333343

@@ -338,7 +348,7 @@ export class ConversationHandler {
338348
if (!request) {
339349
return this.#generateErrorResponse(`Can't find request with the given selector ${requestUrl}`);
340350
}
341-
return this.#doExternalConversation({
351+
return this.#createAndDoExternalConversation({
342352
conversationType: ConversationType.NETWORK,
343353
aiAgent: networkAgent,
344354
prompt,

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export class TimelinePanel extends Common.ObjectWrapper.eventMixin<EventTypes, t
392392
private traceLoadStart!: Trace.Types.Timing.Milli|null;
393393

394394
#traceEngineModel: Trace.TraceModel.Model;
395+
#externalAIConversationData: AiAssistanceModel.ExternalPerformanceAIConversationData|null = null;
395396
#sourceMapsResolver: Utils.SourceMapsResolver.SourceMapsResolver|null = null;
396397
#entityMapper: Utils.EntityMapper.EntityMapper|null = null;
397398
#onSourceMapsNodeNamesResolvedBound = this.#onSourceMapsNodeNamesResolved.bind(this);
@@ -929,6 +930,40 @@ export class TimelinePanel extends Common.ObjectWrapper.eventMixin<EventTypes, t
929930
return this.#traceEngineModel;
930931
}
931932

933+
getOrCreateExternalAIConversationData(): AiAssistanceModel.ExternalPerformanceAIConversationData {
934+
if (!this.#externalAIConversationData) {
935+
const conversationHandler = AiAssistanceModel.ConversationHandler.instance();
936+
const focus = Utils.AIContext.getPerformanceAgentFocusFromModel(this.model);
937+
if (!focus) {
938+
throw new Error('could not create performance agent focus');
939+
}
940+
941+
const agent = conversationHandler.createAgent(AiAssistanceModel.ConversationType.PERFORMANCE_FULL);
942+
const conversation = new AiAssistanceModel.Conversation(
943+
AiAssistanceModel.ConversationType.PERFORMANCE_FULL,
944+
[],
945+
agent.id,
946+
/* isReadOnly */ true,
947+
/* isExternal */ true,
948+
);
949+
950+
const selected = new AiAssistanceModel.PerformanceTraceContext(focus);
951+
952+
this.#externalAIConversationData = {
953+
conversationHandler,
954+
conversation,
955+
agent,
956+
selected,
957+
};
958+
}
959+
960+
return this.#externalAIConversationData;
961+
}
962+
963+
invalidateExternalAIConversationData(): void {
964+
this.#externalAIConversationData = null;
965+
}
966+
932967
/**
933968
* NOTE: this method only exists to enable some layout tests to be migrated to the new engine.
934969
* DO NOT use this method within DevTools. It is marked as deprecated so
@@ -1142,33 +1177,17 @@ export class TimelinePanel extends Common.ObjectWrapper.eventMixin<EventTypes, t
11421177

11431178
// Currently for debugging purposes only.
11441179
#onClickAskAIButton(): void {
1145-
const traceIndex = this.#activeTraceIndex();
1146-
if (traceIndex === null) {
1147-
return;
1148-
}
1149-
1150-
const parsedTrace = this.#traceEngineModel.parsedTrace(traceIndex);
1151-
if (parsedTrace === null) {
1152-
return;
1153-
}
1154-
1155-
const insights = this.#traceEngineModel.traceInsights(traceIndex);
1156-
if (insights === null) {
1157-
return;
1158-
}
1159-
1160-
const traceMetadata = this.#traceEngineModel.metadata(traceIndex);
1161-
if (traceMetadata === null) {
1180+
const actionId = 'drjones.performance-panel-full-context';
1181+
if (!UI.ActionRegistry.ActionRegistry.instance().hasAction(actionId)) {
11621182
return;
11631183
}
11641184

1165-
const actionId = 'drjones.performance-panel-full-context';
1166-
if (!UI.ActionRegistry.ActionRegistry.instance().hasAction(actionId)) {
1185+
const focus = Utils.AIContext.getPerformanceAgentFocusFromModel(this.#traceEngineModel);
1186+
if (!focus) {
11671187
return;
11681188
}
11691189

1170-
const context = Utils.AIContext.AgentFocus.full(parsedTrace, insights, traceMetadata);
1171-
UI.Context.Context.instance().setFlavor(Utils.AIContext.AgentFocus, context);
1190+
UI.Context.Context.instance().setFlavor(Utils.AIContext.AgentFocus, focus);
11721191

11731192
// Trigger the AI Assistance panel to open.
11741193
const action = UI.ActionRegistry.ActionRegistry.instance().getAction(actionId);
@@ -3000,6 +3019,7 @@ export class TimelinePanel extends Common.ObjectWrapper.eventMixin<EventTypes, t
30003019
type: AiAssistanceModel.ExternalRequestResponseType.NOTIFICATION,
30013020
message: 'Recording performance trace',
30023021
};
3022+
TimelinePanel.instance().invalidateExternalAIConversationData();
30033023
void VisualLogging.logFunctionCall('timeline.record-reload', 'external');
30043024
Snackbars.Snackbar.Snackbar.show({message: i18nString(UIStrings.externalRequestReceived)});
30053025

@@ -3096,6 +3116,16 @@ ${responseTextForPassedInsights}`;
30963116
panelInstance.recordReload();
30973117
});
30983118
}
3119+
3120+
static async handleExternalAnalyzeRequest(prompt: string):
3121+
Promise<AsyncGenerator<AiAssistanceModel.ExternalRequestResponse, AiAssistanceModel.ExternalRequestResponse>> {
3122+
const data = TimelinePanel.instance().getOrCreateExternalAIConversationData();
3123+
return await data.conversationHandler.handleExternalRequest({
3124+
conversationType: AiAssistanceModel.ConversationType.PERFORMANCE_FULL,
3125+
prompt,
3126+
data,
3127+
});
3128+
}
30993129
}
31003130

31013131
export const enum State {

front_end/panels/timeline/utils/AIContext.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,14 @@ export class AgentFocus {
6767
return this.#data;
6868
}
6969
}
70+
71+
export function getPerformanceAgentFocusFromModel(model: Trace.TraceModel.Model): AgentFocus|null {
72+
const parsedTrace = model.parsedTrace();
73+
const insights = model.traceInsights();
74+
const traceMetadata = model.metadata();
75+
if (!insights || !parsedTrace || !traceMetadata) {
76+
return null;
77+
}
78+
79+
return AgentFocus.full(parsedTrace, insights, traceMetadata);
80+
}

front_end/services/tracing/ExternalRequests.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import * as Trace from '../../models/trace/trace.js';
66
import * as TimelineUtils from '../../panels/timeline/utils/utils.js';
77

8-
type InsightResponse = {
8+
type PerformanceAgentResponse = {
99
focus: TimelineUtils.AIContext.AgentFocus,
1010
}|{error: string};
1111

@@ -18,7 +18,7 @@ type InsightResponse = {
1818
* some extra input data to figure it out.
1919
*/
2020
export async function getInsightAgentFocusToDebug(
21-
model: Trace.TraceModel.Model, insightTitle: string): Promise<InsightResponse> {
21+
model: Trace.TraceModel.Model, insightTitle: string): Promise<PerformanceAgentResponse> {
2222
const parsedTrace = model.parsedTrace();
2323
const latestInsights = model.traceInsights();
2424
if (!latestInsights || !parsedTrace) {
@@ -54,18 +54,3 @@ export async function getInsightAgentFocusToDebug(
5454
const focus = TimelineUtils.AIContext.AgentFocus.fromInsight(parsedTrace, insight, insights.bounds);
5555
return {focus};
5656
}
57-
58-
export async function getPerformanceAgentFocusToDebug(model: Trace.TraceModel.Model): Promise<InsightResponse> {
59-
const parsedTrace = model.parsedTrace();
60-
const insights = model.traceInsights();
61-
const traceMetadata = model.metadata();
62-
if (!insights || !parsedTrace || !traceMetadata) {
63-
return {
64-
error:
65-
'No trace has been recorded, so we cannot analyze the performance. Must run the devtools_performance_run_trace tool first.',
66-
};
67-
}
68-
69-
const focus = TimelineUtils.AIContext.AgentFocus.full(parsedTrace, insights, traceMetadata);
70-
return {focus};
71-
}

0 commit comments

Comments
 (0)