Skip to content

Commit 947f3e9

Browse files
wolfibDevtools-frontend LUCI CQ
authored andcommitted
[Refactoring] Add ConversationHandler
Step 1 of multiple The logic for handling external requests will be moved from the AiAssistancePanel to the ConversationHandler in follow-up CLs. In this CL, only a few helpers and the handling of the conversation history are migrated to the ConversationHandler. Bug: 427407133 Change-Id: I643b7edc5c27a2a0e838ce0a59d5a1d3425c2c53 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6822155 Reviewed-by: Jack Franklin <[email protected]> Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Wolfgang Beyer <[email protected]>
1 parent 90135b8 commit 947f3e9

File tree

9 files changed

+195
-128
lines changed

9 files changed

+195
-128
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,7 @@ grd_files_unbundled_sources = [
10011001
"front_end/models/ai_assistance/AiHistoryStorage.js",
10021002
"front_end/models/ai_assistance/AiUtils.js",
10031003
"front_end/models/ai_assistance/ChangeManager.js",
1004+
"front_end/models/ai_assistance/ConversationHandler.js",
10041005
"front_end/models/ai_assistance/EvaluateAction.js",
10051006
"front_end/models/ai_assistance/ExtensionScope.js",
10061007
"front_end/models/ai_assistance/agents/AiAgent.js",

front_end/core/common/Settings.test.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,13 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import {MockStore} from '../../testing/MockSettingStorage.js';
6+
57
import * as Common from './common.js';
68

79
const SettingsStorage = Common.Settings.SettingsStorage;
810
const VersionController = Common.Settings.VersionController;
911

10-
class MockStore implements Common.Settings.SettingsBackingStore {
11-
#store = new Map();
12-
register() {
13-
}
14-
set(key: string, value: string) {
15-
this.#store.set(key, value);
16-
}
17-
get(key: string) {
18-
return this.#store.get(key);
19-
}
20-
remove(key: string) {
21-
this.#store.delete(key);
22-
}
23-
clear() {
24-
this.#store.clear();
25-
}
26-
}
27-
2812
describe('SettingsStorage class', () => {
2913
it('is able to set a name', () => {
3014
const settingsStorage = new SettingsStorage({});

front_end/models/ai_assistance/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ devtools_module("ai_assistance") {
1313
"AiHistoryStorage.ts",
1414
"AiUtils.ts",
1515
"ChangeManager.ts",
16+
"ConversationHandler.ts",
1617
"EvaluateAction.ts",
1718
"ExtensionScope.ts",
1819
"agents/AiAgent.ts",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
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 type * as Host from '../../core/host/host.js';
6+
import * as Root from '../../core/root/root.js';
7+
8+
import {type AiAgent, type ResponseData, ResponseType} from './agents/AiAgent.js';
9+
import {FileAgent} from './agents/FileAgent.js';
10+
import {NetworkAgent} from './agents/NetworkAgent.js';
11+
import {PerformanceAgent} from './agents/PerformanceAgent.js';
12+
import {StylingAgent, StylingAgentWithFunctionCalling} from './agents/StylingAgent.js';
13+
import {
14+
type Conversation,
15+
ConversationType,
16+
} from './AiHistoryStorage.js';
17+
import type {ChangeManager} from './ChangeManager.js';
18+
19+
export interface ExternalStylingRequestParameters {
20+
conversationType: ConversationType.STYLING;
21+
prompt: string;
22+
selector?: string;
23+
}
24+
25+
export interface ExternalNetworkRequestParameters {
26+
conversationType: ConversationType.NETWORK;
27+
prompt: string;
28+
requestUrl: string;
29+
}
30+
31+
export interface ExternalPerformanceInsightsRequestParameters {
32+
conversationType: ConversationType.PERFORMANCE_INSIGHT;
33+
prompt: string;
34+
insightTitle: string;
35+
}
36+
37+
function isAiAssistanceStylingWithFunctionCallingEnabled(): boolean {
38+
return Boolean(Root.Runtime.hostConfig.devToolsFreestyler?.functionCalling);
39+
}
40+
41+
function isAiAssistanceServerSideLoggingEnabled(): boolean {
42+
return !Root.Runtime.hostConfig.aidaAvailability?.disallowLogging;
43+
}
44+
45+
let conversationHandlerInstance: ConversationHandler|undefined;
46+
47+
export class ConversationHandler {
48+
#aidaClient: Host.AidaClient.AidaClient;
49+
50+
private constructor(
51+
aidaClient: Host.AidaClient.AidaClient, _aidaAvailability: Host.AidaClient.AidaAccessPreconditions) {
52+
this.#aidaClient = aidaClient;
53+
}
54+
55+
static instance(opts: {
56+
aidaClient: Host.AidaClient.AidaClient,
57+
aidaAvailability: Host.AidaClient.AidaAccessPreconditions,
58+
forceNew?: boolean,
59+
}): ConversationHandler {
60+
if (opts.forceNew || conversationHandlerInstance === undefined) {
61+
conversationHandlerInstance = new ConversationHandler(opts.aidaClient, opts.aidaAvailability);
62+
}
63+
return conversationHandlerInstance;
64+
}
65+
66+
static removeInstance(): void {
67+
conversationHandlerInstance = undefined;
68+
}
69+
70+
async *
71+
handleConversationWithHistory(
72+
items: AsyncIterable<ResponseData, void, void>, conversation: Conversation|undefined):
73+
AsyncGenerator<ResponseData, void, void> {
74+
for await (const data of items) {
75+
// We don't want to save partial responses to the conversation history.
76+
if (data.type !== ResponseType.ANSWER || data.complete) {
77+
void conversation?.addHistoryItem(data);
78+
}
79+
yield data;
80+
}
81+
}
82+
83+
createAgent(conversationType: ConversationType, changeManager?: ChangeManager): AiAgent<unknown> {
84+
const options = {
85+
aidaClient: this.#aidaClient,
86+
serverSideLoggingEnabled: isAiAssistanceServerSideLoggingEnabled(),
87+
};
88+
let agent: AiAgent<unknown>;
89+
switch (conversationType) {
90+
case ConversationType.STYLING: {
91+
agent = new StylingAgent({
92+
...options,
93+
changeManager,
94+
});
95+
if (isAiAssistanceStylingWithFunctionCallingEnabled()) {
96+
agent = new StylingAgentWithFunctionCalling({
97+
...options,
98+
changeManager,
99+
});
100+
}
101+
102+
break;
103+
}
104+
case ConversationType.NETWORK: {
105+
agent = new NetworkAgent(options);
106+
break;
107+
}
108+
case ConversationType.FILE: {
109+
agent = new FileAgent(options);
110+
break;
111+
}
112+
case ConversationType.PERFORMANCE_INSIGHT:
113+
case ConversationType.PERFORMANCE: {
114+
agent = new PerformanceAgent(options, conversationType);
115+
break;
116+
}
117+
}
118+
return agent;
119+
}
120+
}

front_end/models/ai_assistance/ai_assistance.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from './ExtensionScope.js';
1919
export * from './data_formatters/FileFormatter.js';
2020
export * from './data_formatters/NetworkRequestFormatter.js';
2121
export * from './data_formatters/PerformanceInsightFormatter.js';
22+
export * from './ConversationHandler.js';

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from '../../testing/EnvironmentHelpers.js';
2626
import {expectCall} from '../../testing/ExpectStubCall.js';
2727
import {describeWithMockConnection} from '../../testing/MockConnection.js';
28+
import {MockStore} from '../../testing/MockSettingStorage.js';
2829
import {createNetworkPanelForMockConnection} from '../../testing/NetworkHelpers.js';
2930
import {TraceLoader} from '../../testing/TraceLoader.js';
3031
import * as Snackbars from '../../ui/components/snackbars/snackbars.js';
@@ -42,6 +43,7 @@ describeWithMockConnection('AI Assistance Panel', () => {
4243
let viewManagerIsViewVisibleStub: sinon.SinonStub<[viewId: string], boolean>;
4344
beforeEach(() => {
4445
viewManagerIsViewVisibleStub = sinon.stub(UI.ViewManager.ViewManager.instance(), 'isViewVisible');
46+
AiAssistanceModel.ConversationHandler.removeInstance();
4547
registerNoopActions([
4648
'elements.toggle-element-search', 'timeline.record-reload', 'timeline.toggle-recording', 'timeline.show-history',
4749
'components.collect-garbage'
@@ -52,6 +54,15 @@ describeWithMockConnection('AI Assistance Panel', () => {
5254
UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, null);
5355
UI.Context.Context.instance().setFlavor(TimelineUtils.AIContext.AgentFocus, null);
5456
UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, null);
57+
58+
const mockStore = new MockStore();
59+
const settingsStorage = new Common.Settings.SettingsStorage({}, mockStore);
60+
Common.Settings.Settings.instance({
61+
forceNew: true,
62+
syncedStorage: settingsStorage,
63+
globalStorage: settingsStorage,
64+
localStorage: settingsStorage,
65+
});
5566
});
5667

5768
afterEach(() => {
@@ -716,7 +727,7 @@ describeWithMockConnection('AI Assistance Panel', () => {
716727
},
717728
});
718729
const aiHistoryStorage = AiAssistanceModel.AiHistoryStorage.instance({forceNew: true});
719-
const deleteHistoryEntryStub = sinon.stub(aiHistoryStorage, 'deleteHistoryEntry');
730+
const deleteHistoryEntrySpy = sinon.spy(aiHistoryStorage, 'deleteHistoryEntry');
720731
const {panel, view} = await createAiAssistancePanel(
721732
{
722733
aidaClient: mockAidaClient(
@@ -742,8 +753,8 @@ describeWithMockConnection('AI Assistance Panel', () => {
742753
view.input.onDeleteClick();
743754

744755
assert.deepEqual((await view.nextInput).messages, []);
745-
sinon.assert.callCount(deleteHistoryEntryStub, 1);
746-
assert.isString(deleteHistoryEntryStub.lastCall.args[0]);
756+
sinon.assert.callCount(deleteHistoryEntrySpy, 1);
757+
assert.isString(deleteHistoryEntrySpy.lastCall.args[0]);
747758

748759
const menuAfterDelete = openHistoryContextMenu(view.input, 'User question to Freestyler?');
749760
assert.isUndefined(menuAfterDelete.id);

0 commit comments

Comments
 (0)