Skip to content

Commit 546c5dd

Browse files
Lightning00BladeDevtools-frontend LUCI CQ
authored andcommitted
[Freestyler] Extract run code
Unify how we do run in AI Assistance to streamline creation of new Agents Bug: 369303799 Change-Id: I4aca9f997caf22062e0e812b94a62f955688fdee Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5904625 Commit-Queue: Nikolay Vitkov <[email protected]> Reviewed-by: Alex Rudenko <[email protected]> Reviewed-by: Ergün Erdoğmuş <[email protected]>
1 parent 2348235 commit 546c5dd

15 files changed

+331
-402
lines changed

front_end/panels/freestyler/AiAgent.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as Freestyler from './freestyler.js';
1111

1212
const {AiAgent} = Freestyler;
1313

14-
class AiAgentMock extends AiAgent {
14+
class AiAgentMock extends AiAgent<unknown> {
1515
override preamble = 'preamble';
1616

1717
clientFeature: Host.AidaClient.ClientFeature = 0;

front_end/panels/freestyler/AiAgent.ts

Lines changed: 196 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface AnswerResponse {
2525
type: ResponseType.ANSWER;
2626
text: string;
2727
rpcId?: number;
28-
suggestions?: string[];
28+
suggestions?: [string, ...string[]];
2929
}
3030

3131
export interface ErrorResponse {
@@ -99,14 +99,34 @@ type AgentOptions = {
9999
serverSideLoggingEnabled?: boolean,
100100
};
101101

102-
export abstract class AiAgent {
102+
interface ParsedResponseAnswer {
103+
answer: string;
104+
suggestions?: [string, ...string[]];
105+
}
106+
107+
interface ParsedResponseStep {
108+
thought?: string;
109+
title?: string;
110+
action?: string;
111+
}
112+
113+
export type ParsedResponse = ParsedResponseAnswer|ParsedResponseStep;
114+
115+
const MAX_STEP = 10;
116+
117+
export abstract class AiAgent<T> {
118+
static validTemperature(temperature: number|undefined): number|undefined {
119+
return typeof temperature === 'number' && temperature >= 0 ? temperature : undefined;
120+
}
121+
103122
readonly #sessionId: string = crypto.randomUUID();
104123
#aidaClient: Host.AidaClient.AidaClient;
105124
#serverSideLoggingEnabled: boolean;
106125
abstract readonly preamble: string;
107126
abstract readonly options: AidaRequestOptions;
108127
abstract readonly clientFeature: Host.AidaClient.ClientFeature;
109128
abstract readonly userTier: string|undefined;
129+
abstract handleContextDetails(select: T|null): AsyncGenerator<ContextResponse, void, void>;
110130

111131
/**
112132
* Mapping between the unique request id and
@@ -135,7 +155,49 @@ export abstract class AiAgent {
135155
this.#chatHistory.delete(id);
136156
}
137157

138-
addToHistory({
158+
addToHistory(options: {
159+
id: number,
160+
query: string,
161+
response: ParsedResponse,
162+
}): void {
163+
const response = options.response;
164+
if ('answer' in response) {
165+
this.#storeHistoryEntries({
166+
id: options.id,
167+
query: options.query,
168+
output: response.answer,
169+
});
170+
return;
171+
}
172+
173+
const {
174+
title,
175+
thought,
176+
action,
177+
} = response;
178+
179+
if (thought) {
180+
this.#storeHistoryEntries({
181+
id: options.id,
182+
query: options.query,
183+
output: `THOUGHT: ${thought}
184+
TITLE: ${title}
185+
ACTION
186+
${action}
187+
STOP`,
188+
});
189+
} else {
190+
this.#storeHistoryEntries({
191+
id: options.id,
192+
query: options.query,
193+
output: `ACTION
194+
${action}
195+
STOP`,
196+
});
197+
}
198+
}
199+
200+
#storeHistoryEntries({
139201
id,
140202
query,
141203
output,
@@ -169,15 +231,15 @@ export abstract class AiAgent {
169231
options?: {signal?: AbortSignal},
170232
): Promise<{
171233
response: string,
172-
rpcId: number|undefined,
234+
rpcId?: number,
173235
}> {
174236
const request = this.buildRequest({
175237
input,
176238
});
177239

178240
let rawResponse: Host.AidaClient.AidaResponse|undefined = undefined;
179241
let response = '';
180-
let rpcId;
242+
let rpcId: number|undefined;
181243
for await (rawResponse of this.#aidaClient.fetch(request, options)) {
182244
response = rawResponse.explanation;
183245
rpcId = rawResponse.metadata.rpcGlobalId ?? rpcId;
@@ -223,8 +285,135 @@ export abstract class AiAgent {
223285
return request;
224286
}
225287

226-
static validTemperature(temperature: number|undefined): number|undefined {
227-
return typeof temperature === 'number' && temperature >= 0 ? temperature : undefined;
288+
handleAction(_action: string, _rpcId?: number): AsyncGenerator<SideEffectResponse, ActionResponse, void> {
289+
throw new Error('Unexpected action found');
290+
}
291+
292+
async enhanceQuery(query: string, selected: T|null): Promise<string>;
293+
async enhanceQuery(query: string): Promise<string> {
294+
return query;
295+
}
296+
297+
parseResponse(response: string): ParsedResponse {
298+
return {
299+
answer: response,
300+
};
301+
}
302+
303+
#runId = 0;
304+
async * run(query: string, options: {
305+
signal?: AbortSignal, selected: T|null,
306+
}): AsyncGenerator<ResponseData, void, void> {
307+
yield* this.handleContextDetails(options.selected);
308+
309+
query = await this.enhanceQuery(query, options.selected);
310+
const currentRunId = ++this.#runId;
311+
312+
for (let i = 0; i < MAX_STEP; i++) {
313+
yield {
314+
type: ResponseType.QUERYING,
315+
};
316+
317+
let response: string;
318+
let rpcId: number|undefined;
319+
try {
320+
const fetchResult = await this.aidaFetch(
321+
query,
322+
{signal: options.signal},
323+
);
324+
response = fetchResult.response;
325+
rpcId = fetchResult.rpcId;
326+
} catch (err) {
327+
debugLog('Error calling the AIDA API', err);
328+
329+
if (err instanceof Host.AidaClient.AidaAbortError) {
330+
this.removeHistoryRun(currentRunId);
331+
yield {
332+
type: ResponseType.ERROR,
333+
error: ErrorType.ABORT,
334+
rpcId,
335+
};
336+
break;
337+
}
338+
339+
yield {
340+
type: ResponseType.ERROR,
341+
error: ErrorType.UNKNOWN,
342+
rpcId,
343+
};
344+
break;
345+
}
346+
347+
const parsedResponse = this.parseResponse(response);
348+
349+
this.addToHistory({
350+
id: currentRunId,
351+
query,
352+
response: parsedResponse,
353+
});
354+
if ('answer' in parsedResponse) {
355+
const {
356+
answer,
357+
suggestions,
358+
} = parsedResponse;
359+
if (answer) {
360+
yield {
361+
type: ResponseType.ANSWER,
362+
text: answer,
363+
rpcId,
364+
suggestions,
365+
};
366+
} else {
367+
this.removeHistoryRun(currentRunId);
368+
yield {
369+
type: ResponseType.ERROR,
370+
error: ErrorType.UNKNOWN,
371+
rpcId,
372+
};
373+
}
374+
375+
break;
376+
}
377+
378+
const {
379+
title,
380+
thought,
381+
action,
382+
} = parsedResponse;
383+
384+
if (title) {
385+
yield {
386+
type: ResponseType.TITLE,
387+
title,
388+
rpcId,
389+
};
390+
}
391+
392+
if (thought) {
393+
yield {
394+
type: ResponseType.THOUGHT,
395+
thought,
396+
rpcId,
397+
};
398+
}
399+
400+
if (action) {
401+
const result = yield* this.handleAction(action, rpcId);
402+
yield result;
403+
query = `OBSERVATION: ${result.output}`;
404+
}
405+
406+
if (i === MAX_STEP - 1) {
407+
yield {
408+
type: ResponseType.ERROR,
409+
error: ErrorType.MAX_STEPS,
410+
};
411+
break;
412+
}
413+
}
414+
if (isDebugMode()) {
415+
window.dispatchEvent(new CustomEvent('freestylerdone'));
416+
}
228417
}
229418
}
230419

front_end/panels/freestyler/DrJonesFileAgent.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describeWithEnvironment('DrJonesFileAgent', () => {
157157
});
158158

159159
const uiSourceCode = project.uiSourceCodeForURL(url);
160-
const responses = await Array.fromAsync(agent.run('test', {selectedFile: uiSourceCode}));
160+
const responses = await Array.fromAsync(agent.run('test', {selected: uiSourceCode}));
161161

162162
assert.deepStrictEqual(responses, [
163163
{
@@ -173,9 +173,13 @@ File Content:
173173
},
174174
],
175175
},
176+
{
177+
type: ResponseType.QUERYING,
178+
},
176179
{
177180
type: ResponseType.ANSWER,
178181
text: 'This is the answer',
182+
suggestions: undefined,
179183
rpcId: 123,
180184
},
181185
]);

front_end/panels/freestyler/DrJonesFileAgent.ts

Lines changed: 8 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ import {
1212
type AidaRequestOptions,
1313
type ContextDetail,
1414
type ContextResponse,
15-
debugLog,
16-
ErrorType,
17-
isDebugMode,
18-
type ResponseData,
15+
type ParsedResponse,
1916
ResponseType,
2017
} from './AiAgent.js';
2118

@@ -70,7 +67,7 @@ const MAX_FILE_SIZE = 50000;
7067
* One agent instance handles one conversation. Create a new agent
7168
* instance for a new conversation.
7269
*/
73-
export class DrJonesFileAgent extends AiAgent {
70+
export class DrJonesFileAgent extends AiAgent<Workspace.UISourceCode.UISourceCode> {
7471
readonly preamble = preamble;
7572
readonly clientFeature = Host.AidaClient.ClientFeature.CHROME_DRJONES_FILE_AGENT;
7673
get userTier(): string|undefined {
@@ -88,9 +85,9 @@ export class DrJonesFileAgent extends AiAgent {
8885
};
8986
}
9087

91-
*
88+
async *
9289
handleContextDetails(selectedFile: Workspace.UISourceCode.UISourceCode|null):
93-
Generator<ContextResponse, void, void> {
90+
AsyncGenerator<ContextResponse, void, void> {
9491
if (!selectedFile) {
9592
return;
9693
}
@@ -102,61 +99,16 @@ export class DrJonesFileAgent extends AiAgent {
10299
};
103100
}
104101

105-
async enhanceQuery(query: string, selectedFile: Workspace.UISourceCode.UISourceCode|null): Promise<string> {
102+
override async enhanceQuery(query: string, selectedFile: Workspace.UISourceCode.UISourceCode|null): Promise<string> {
106103
const fileEnchantmentQuery =
107104
selectedFile ? `# Selected file\n${formatFile(selectedFile)}\n\n# User request\n\n` : '';
108105
return `${fileEnchantmentQuery}${query}`;
109106
}
110107

111-
#runId = 0;
112-
async * run(query: string, options: {
113-
signal?: AbortSignal, selectedFile: Workspace.UISourceCode.UISourceCode|null,
114-
}): AsyncGenerator<ResponseData, void, void> {
115-
yield* this.handleContextDetails(options.selectedFile);
116-
117-
query = await this.enhanceQuery(query, options.selectedFile);
118-
const currentRunId = ++this.#runId;
119-
120-
let response: string;
121-
let rpcId: number|undefined;
122-
try {
123-
const fetchResult = await this.aidaFetch(query, {signal: options.signal});
124-
response = fetchResult.response;
125-
rpcId = fetchResult.rpcId;
126-
} catch (err) {
127-
debugLog('Error calling the AIDA API', err);
128-
if (err instanceof Host.AidaClient.AidaAbortError) {
129-
this.removeHistoryRun(currentRunId);
130-
yield {
131-
type: ResponseType.ERROR,
132-
error: ErrorType.ABORT,
133-
rpcId,
134-
};
135-
return;
136-
}
137-
138-
yield {
139-
type: ResponseType.ERROR,
140-
error: ErrorType.UNKNOWN,
141-
rpcId,
142-
};
143-
return;
144-
}
145-
146-
this.addToHistory({
147-
id: currentRunId,
148-
query,
149-
output: response,
150-
});
151-
152-
yield {
153-
type: ResponseType.ANSWER,
154-
text: response,
155-
rpcId,
108+
override parseResponse(response: string): ParsedResponse {
109+
return {
110+
answer: response,
156111
};
157-
if (isDebugMode()) {
158-
window.dispatchEvent(new CustomEvent('freestylerdone'));
159-
}
160112
}
161113
}
162114

0 commit comments

Comments
 (0)