Skip to content

Commit 2144d7e

Browse files
wolfibDevtools-frontend LUCI CQ
authored andcommitted
Allow external requests to return intermediate steps by using generators
Bug: 425498943 Change-Id: Ib93fba4277b0856dedb6fb815ad4e74d546ca190 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6778206 Reviewed-by: Jack Franklin <[email protected]> Commit-Queue: Wolfgang Beyer <[email protected]>
1 parent ba5893b commit 2144d7e

File tree

7 files changed

+253
-138
lines changed

7 files changed

+253
-138
lines changed

front_end/entrypoints/main/MainImpl.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,28 @@ describeWithMockConnection('MainMenuItem', () => {
6161
});
6262

6363
describe('handleExternalRequest', () => {
64-
const {handleExternalRequest} = Main.MainImpl;
64+
const {handleExternalRequestGenerator} = Main.MainImpl;
6565

6666
it('calls into the AiAssistance Panel for LIVE_STYLE_DEBUGGER', async () => {
6767
const panel = sinon.createStubInstance(AiAssistance.AiAssistancePanel);
6868
sinon.stub(AiAssistance.AiAssistancePanel, 'instance').callsFake(() => Promise.resolve(panel));
6969

70-
await handleExternalRequest({kind: 'LIVE_STYLE_DEBUGGER', args: {prompt: 'test', selector: '#test'}});
70+
await handleExternalRequestGenerator({kind: 'LIVE_STYLE_DEBUGGER', args: {prompt: 'test', selector: '#test'}});
7171
sinon.assert.calledWith(
7272
panel.handleExternalRequest,
7373
{prompt: 'test', conversationType: AiAssistanceModel.ConversationType.STYLING, selector: '#test'});
7474
});
7575

76-
it('throws an error for file assistance requests', async () => {
76+
it('returns an error for file assistance requests', async () => {
7777
const panel = sinon.createStubInstance(AiAssistance.AiAssistancePanel);
7878
sinon.stub(AiAssistance.AiAssistancePanel, 'instance').callsFake(() => Promise.resolve(panel));
79-
try {
80-
// @ts-expect-error
81-
await handleExternalRequest({kind: 'FILE_DEBUGGER', args: {prompt: 'test'}});
82-
assert.fail('Expected `handleExternalRequest` to throw');
83-
} catch (err) {
84-
assert.strictEqual(err.message, 'Debugging with an agent of type \'FILE_DEBUGGER\' is not implemented yet.');
85-
}
79+
80+
// @ts-expect-error
81+
const generator = await handleExternalRequestGenerator({kind: 'FILE_DEBUGGER', args: {prompt: 'test'}});
82+
const iteratorResponse = await generator.next();
83+
assert.strictEqual(iteratorResponse.value.type, 'error');
84+
assert.strictEqual(
85+
iteratorResponse.value.message, 'Debugging with an agent of type \'FILE_DEBUGGER\' is not implemented yet.');
8686
});
8787
});
8888
});

front_end/entrypoints/main/MainImpl.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import * as Platform from '../../core/platform/platform.js';
4141
import * as ProtocolClient from '../../core/protocol_client/protocol_client.js';
4242
import * as Root from '../../core/root/root.js';
4343
import * as SDK from '../../core/sdk/sdk.js';
44+
import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js';
4445
import * as AutofillManager from '../../models/autofill_manager/autofill_manager.js';
4546
import * as Bindings from '../../models/bindings/bindings.js';
4647
import * as Breakpoints from '../../models/breakpoints/breakpoints.js';
@@ -1043,22 +1044,44 @@ type ExternalRequestInput = {
10431044
args: {requestUrl: string, prompt: string},
10441045
};
10451046

1046-
interface ExternalRequestResponse {
1047-
response: string;
1048-
devToolsLogs: object[];
1047+
// For backwards-compatibility we iterate over the generator and drop the
1048+
// intermediate results. The final response is transformed to its legacy type.
1049+
// Instead of sending responses of type error, errors are throws.
1050+
export async function handleExternalRequest(input: ExternalRequestInput):
1051+
Promise<{response: string, devToolsLogs: object[]}> {
1052+
const generator = await handleExternalRequestGenerator(input);
1053+
let result: IteratorResult<AiAssistanceModel.ExternalRequestResponse, AiAssistanceModel.ExternalRequestResponse>;
1054+
do {
1055+
result = await generator.next();
1056+
} while (!result.done);
1057+
const response = result.value;
1058+
if (response.type === AiAssistanceModel.ExternalRequestResponseType.ERROR) {
1059+
throw new Error(response.message);
1060+
}
1061+
if (response.type === AiAssistanceModel.ExternalRequestResponseType.ANSWER) {
1062+
return {
1063+
response: response.message,
1064+
devToolsLogs: response.devToolsLogs,
1065+
};
1066+
}
1067+
throw new Error('Received no response of type answer or type error');
10491068
}
10501069

1051-
export async function handleExternalRequest(input: ExternalRequestInput): Promise<ExternalRequestResponse> {
1070+
// @ts-expect-error
1071+
globalThis.handleExternalRequest = handleExternalRequest;
1072+
1073+
export async function handleExternalRequestGenerator(input: ExternalRequestInput):
1074+
Promise<AsyncGenerator<AiAssistanceModel.ExternalRequestResponse, AiAssistanceModel.ExternalRequestResponse>> {
10521075
switch (input.kind) {
10531076
case 'PERFORMANCE_RELOAD_GATHER_INSIGHTS': {
10541077
const TimelinePanel = await import('../../panels/timeline/timeline.js');
1055-
return await TimelinePanel.TimelinePanel.TimelinePanel.handleExternalRecordRequest();
1078+
return TimelinePanel.TimelinePanel.TimelinePanel.handleExternalRecordRequest();
10561079
}
10571080
case 'PERFORMANCE_ANALYZE_INSIGHT': {
10581081
const AiAssistance = await import('../../panels/ai_assistance/ai_assistance.js');
10591082
const AiAssistanceModel = await import('../../models/ai_assistance/ai_assistance.js');
10601083
const panelInstance = await AiAssistance.AiAssistancePanel.instance();
1061-
return await panelInstance.handleExternalRequest({
1084+
return panelInstance.handleExternalRequest({
10621085
conversationType: AiAssistanceModel.ConversationType.PERFORMANCE_INSIGHT,
10631086
prompt: input.args.prompt,
10641087
insightTitle: input.args.insightTitle,
@@ -1068,7 +1091,7 @@ export async function handleExternalRequest(input: ExternalRequestInput): Promis
10681091
const AiAssistance = await import('../../panels/ai_assistance/ai_assistance.js');
10691092
const AiAssistanceModel = await import('../../models/ai_assistance/ai_assistance.js');
10701093
const panelInstance = await AiAssistance.AiAssistancePanel.instance();
1071-
return await panelInstance.handleExternalRequest({
1094+
return panelInstance.handleExternalRequest({
10721095
conversationType: AiAssistanceModel.ConversationType.NETWORK,
10731096
prompt: input.args.prompt,
10741097
requestUrl: input.args.requestUrl,
@@ -1078,16 +1101,23 @@ export async function handleExternalRequest(input: ExternalRequestInput): Promis
10781101
const AiAssistance = await import('../../panels/ai_assistance/ai_assistance.js');
10791102
const AiAssistanceModel = await import('../../models/ai_assistance/ai_assistance.js');
10801103
const panelInstance = await AiAssistance.AiAssistancePanel.instance();
1081-
return await panelInstance.handleExternalRequest({
1104+
return panelInstance.handleExternalRequest({
10821105
conversationType: AiAssistanceModel.ConversationType.STYLING,
10831106
prompt: input.args.prompt,
10841107
selector: input.args.selector,
10851108
});
10861109
}
10871110
}
1088-
// @ts-expect-error
1089-
throw new Error(`Debugging with an agent of type '${input.kind}' is not implemented yet.`);
1111+
// eslint-disable-next-line require-yield
1112+
return (async function*
1113+
(): AsyncGenerator<AiAssistanceModel.ExternalRequestResponse, AiAssistanceModel.ExternalRequestResponse> {
1114+
return {
1115+
type: AiAssistanceModel.ExternalRequestResponseType.ERROR,
1116+
// @ts-expect-error
1117+
message: `Debugging with an agent of type '${input.kind}' is not implemented yet.`,
1118+
};
1119+
})();
10901120
}
10911121

10921122
// @ts-expect-error
1093-
globalThis.handleExternalRequest = handleExternalRequest;
1123+
globalThis.handleExternalRequestGenerator = handleExternalRequestGenerator;

front_end/models/ai_assistance/agents/AiAgent.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,30 @@ export interface ConversationSuggestion {
145145
jslogContext?: string;
146146
}
147147

148+
export const enum ExternalRequestResponseType {
149+
ANSWER = 'answer',
150+
NOTIFICATION = 'notification',
151+
ERROR = 'error',
152+
}
153+
154+
export interface ExternalRequestAnswer {
155+
type: ExternalRequestResponseType.ANSWER;
156+
message: string;
157+
devToolsLogs: object[];
158+
}
159+
160+
export interface ExternalRequestNotification {
161+
type: ExternalRequestResponseType.NOTIFICATION;
162+
message: string;
163+
}
164+
165+
export interface ExternalRequestError {
166+
type: ExternalRequestResponseType.ERROR;
167+
message: string;
168+
}
169+
170+
export type ExternalRequestResponse = ExternalRequestAnswer|ExternalRequestNotification|ExternalRequestError;
171+
148172
export abstract class ConversationContext<T> {
149173
abstract getOrigin(): string;
150174
abstract getItem(): T;

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,44 +1604,36 @@ describeWithMockConnection('AI Assistance Panel', () => {
16041604
});
16051605

16061606
describe('can be blocked', () => {
1607-
beforeEach(() => {
1608-
// These tests log the error to the console.
1609-
// we don't need that noise in the test output.
1610-
sinon.stub(console, 'error');
1611-
});
1612-
16131607
it('by a setting', async () => {
16141608
Common.Settings.moduleSetting('ai-assistance-enabled').set(false);
16151609
const {panel} = await createAiAssistancePanel({
16161610
aidaClient: mockAidaClient([[{explanation}]]),
16171611
});
1618-
try {
1619-
await panel.handleExternalRequest({
1620-
prompt: 'Please help me debug this problem',
1621-
conversationType: AiAssistanceModel.ConversationType.STYLING
1622-
});
1623-
assert.fail('Expected `handleExternalRequest` to throw');
1624-
} catch (err) {
1625-
assert.strictEqual(
1626-
err.message, 'For AI features to be available, you need to enable AI assistance in DevTools settings.');
1627-
}
1612+
const generator = await panel.handleExternalRequest({
1613+
prompt: 'Please help me debug this problem',
1614+
conversationType: AiAssistanceModel.ConversationType.STYLING
1615+
});
1616+
const response = await generator.next();
1617+
assert.strictEqual(response.value.type, 'error');
1618+
assert.strictEqual(
1619+
response.value.message,
1620+
'For AI features to be available, you need to enable AI assistance in DevTools settings.');
16281621
});
16291622

16301623
it('by feature availability', async () => {
16311624
const {panel} = await createAiAssistancePanel({
16321625
aidaClient: mockAidaClient([[{explanation}]]),
16331626
aidaAvailability: Host.AidaClient.AidaAccessPreconditions.SYNC_IS_PAUSED,
16341627
});
1635-
try {
1636-
await panel.handleExternalRequest({
1637-
prompt: 'Please help me debug this problem',
1638-
conversationType: AiAssistanceModel.ConversationType.STYLING
1639-
});
1640-
assert.fail('Expected `handleExternalRequest` to throw');
1641-
} catch (err) {
1642-
assert.strictEqual(
1643-
err.message, 'This feature is only available when you sign into Chrome with your Google account.');
1644-
}
1628+
const generator = await panel.handleExternalRequest({
1629+
prompt: 'Please help me debug this problem',
1630+
conversationType: AiAssistanceModel.ConversationType.STYLING
1631+
});
1632+
const response = await generator.next();
1633+
assert.strictEqual(response.value.type, 'error');
1634+
assert.strictEqual(
1635+
response.value.message,
1636+
'This feature is only available when you sign into Chrome with your Google account.');
16451637
});
16461638

16471639
it('by user age', async () => {
@@ -1656,15 +1648,14 @@ describeWithMockConnection('AI Assistance Panel', () => {
16561648
const {panel} = await createAiAssistancePanel({
16571649
aidaClient: mockAidaClient([[{explanation}]]),
16581650
});
1659-
try {
1660-
await panel.handleExternalRequest({
1661-
prompt: 'Please help me debug this problem',
1662-
conversationType: AiAssistanceModel.ConversationType.STYLING
1663-
});
1664-
assert.fail('Expected `handleExternalRequest` to throw');
1665-
} catch (err) {
1666-
assert.strictEqual(err.message, 'This feature is only available to users who are 18 years of age or older.');
1667-
}
1651+
const generator = await panel.handleExternalRequest({
1652+
prompt: 'Please help me debug this problem',
1653+
conversationType: AiAssistanceModel.ConversationType.STYLING
1654+
});
1655+
const response = await generator.next();
1656+
assert.strictEqual(response.value.type, 'error');
1657+
assert.strictEqual(
1658+
response.value.message, 'This feature is only available to users who are 18 years of age or older.');
16681659
});
16691660
});
16701661

@@ -1673,27 +1664,29 @@ describeWithMockConnection('AI Assistance Panel', () => {
16731664
aidaClient: mockAidaClient([[{explanation}]]),
16741665
});
16751666
const snackbarShowStub = sinon.stub(Snackbars.Snackbar.Snackbar, 'show');
1676-
const response = await panel.handleExternalRequest(
1667+
const generator = await panel.handleExternalRequest(
16771668
{prompt: 'Please help me debug this problem', conversationType: AiAssistanceModel.ConversationType.STYLING});
1678-
assert.strictEqual(response.response, explanation);
1669+
const response = await generator.next();
1670+
assert.strictEqual(response.value.message, explanation);
16791671
sinon.assert.calledOnceWithExactly(snackbarShowStub, {message: 'DevTools received an external request'});
16801672
});
16811673

16821674
it('handles styling assistance requests which contain a selector', async () => {
16831675
const {panel} = await createAiAssistancePanel({
16841676
aidaClient: mockAidaClient([[{explanation}]]),
16851677
});
1686-
const response = await panel.handleExternalRequest({
1678+
const generator = await panel.handleExternalRequest({
16871679
prompt: 'Please help me debug this problem',
16881680
conversationType: AiAssistanceModel.ConversationType.STYLING,
16891681
selector: 'h1'
16901682
});
1691-
assert.strictEqual(response.response, explanation);
1683+
const response = await generator.next();
1684+
assert.strictEqual(response.value.message, explanation);
16921685
sinon.assert.calledOnce(performSearchStub);
16931686
assert.strictEqual(performSearchStub.getCall(0).args[0].query, 'h1');
16941687
});
16951688

1696-
it('throws an error if no answer could be generated', async () => {
1689+
it('returns an error if no answer could be generated', async () => {
16971690
const {panel} = await createAiAssistancePanel({
16981691
aidaClient: mockAidaClient([
16991692
[{
@@ -1703,23 +1696,20 @@ STOP`,
17031696
}],
17041697
])
17051698
});
1706-
try {
1707-
await panel.handleExternalRequest({
1708-
prompt: 'Please help me debug this problem',
1709-
conversationType: AiAssistanceModel.ConversationType.STYLING
1710-
});
1711-
assert.fail('Expected `handleExternalRequest` to throw');
1712-
} catch (err) {
1713-
assert.strictEqual(err.message, 'Something went wrong. No answer was generated.');
1714-
}
1699+
const generator = await panel.handleExternalRequest(
1700+
{prompt: 'Please help me debug this problem', conversationType: AiAssistanceModel.ConversationType.STYLING});
1701+
const response = await generator.next();
1702+
assert.strictEqual(response.value.type, 'error');
1703+
assert.strictEqual(response.value.message, 'Something went wrong. No answer was generated.');
17151704
});
17161705

17171706
it('persists external conversations to history', async () => {
17181707
const {panel, view} = await createAiAssistancePanel({
17191708
aidaClient: mockAidaClient([[{explanation}]]),
17201709
});
1721-
await panel.handleExternalRequest(
1710+
const generator = await panel.handleExternalRequest(
17221711
{prompt: 'Please help me debug this problem', conversationType: AiAssistanceModel.ConversationType.STYLING});
1712+
await generator.next();
17231713
const {contextMenu, id} = openHistoryContextMenu(view.input, '[External] Please help me debug this problem');
17241714
assert.isDefined(id);
17251715
contextMenu.invokeHandler(id);
@@ -1799,9 +1789,10 @@ STOP`,
17991789
},
18001790
]);
18011791

1802-
const response = await panel.handleExternalRequest(
1792+
const generator = await panel.handleExternalRequest(
18031793
{prompt: 'Please help me debug this problem', conversationType: AiAssistanceModel.ConversationType.STYLING});
1804-
assert.strictEqual(response.response, 'test2');
1794+
const response = await generator.next();
1795+
assert.strictEqual(response.value.message, 'test2');
18051796

18061797
view.input.onTextSubmit('Follow-up question to DrJones?');
18071798
assert.deepEqual((await view.nextInput).messages, [
@@ -1846,12 +1837,15 @@ STOP`,
18461837
});
18471838
sinon.stub(SDK.TargetManager.TargetManager.instance(), 'models').returns([networkManager]);
18481839

1849-
const response = await panel.handleExternalRequest({
1840+
const generator = await panel.handleExternalRequest({
18501841
prompt: 'Please help me debug this problem',
18511842
conversationType: AiAssistanceModel.ConversationType.NETWORK,
18521843
requestUrl: 'https://localhost:8080/'
18531844
});
1854-
assert.strictEqual(response.response, explanation);
1845+
let response = await generator.next();
1846+
assert.strictEqual(response.value.message, 'Analyzing network data');
1847+
response = await generator.next();
1848+
assert.strictEqual(response.value.message, explanation);
18551849
sinon.assert.calledOnceWithExactly(snackbarShowStub, {message: 'DevTools received an external request'});
18561850
});
18571851

@@ -1866,32 +1860,31 @@ STOP`,
18661860
await traceModel.parse(events);
18671861
Timeline.TimelinePanel.TimelinePanel.instance({forceNew: true, isNode: false, traceModel});
18681862

1869-
const response = await panel.handleExternalRequest({
1863+
const generator = await panel.handleExternalRequest({
18701864
prompt: 'Please help me debug this problem',
18711865
conversationType: AiAssistanceModel.ConversationType.PERFORMANCE_INSIGHT,
18721866
insightTitle: 'LCP breakdown'
18731867
});
1874-
assert.strictEqual(response.response, explanation);
1868+
let response = await generator.next();
1869+
assert.strictEqual(response.value.message, 'Analyzing insight: LCP breakdown');
1870+
response = await generator.next();
1871+
assert.strictEqual(response.value.message, explanation);
18751872
});
18761873

18771874
it('errors for performance insight requests with no insightTitle', async () => {
1878-
// Suppress console noise in test output
1879-
sinon.stub(console, 'error');
1880-
18811875
const {panel} = await createAiAssistancePanel({
18821876
aidaClient: mockAidaClient([[{explanation}]]),
18831877
});
1884-
try {
1885-
await panel.handleExternalRequest(
1886-
// @ts-expect-error
1887-
{
1888-
prompt: 'Please help me debug this problem',
1889-
conversationType: AiAssistanceModel.ConversationType.PERFORMANCE_INSIGHT
1890-
});
1891-
assert.fail('Expected `handleExternalRequest` to throw');
1892-
} catch (err) {
1893-
assert.strictEqual(err.message, 'The insightTitle parameter is required for debugging a Performance Insight.');
1894-
}
1878+
const generator = await panel.handleExternalRequest(
1879+
// @ts-expect-error
1880+
{
1881+
prompt: 'Please help me debug this problem',
1882+
conversationType: AiAssistanceModel.ConversationType.PERFORMANCE_INSIGHT
1883+
});
1884+
const response = await generator.next();
1885+
assert.strictEqual(response.value.type, 'error');
1886+
assert.strictEqual(
1887+
response.value.message, 'The insightTitle parameter is required for debugging a Performance Insight.');
18951888
});
18961889
});
18971890
});

0 commit comments

Comments
 (0)