Skip to content

Commit 475cfb7

Browse files
Lightning00BladeDevtools-frontend LUCI CQ
authored andcommitted
[AI Assistance] Simplify agent.run
Moves the history handling logic outside the run itself. As we are storing all the yields. Refactor Error yields creation to insure we collect metrics correctly. Bug: 360751542 Change-Id: I4aa3afd4430725d8cc14c876b1196f5b4a708244 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6235030 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]>
1 parent 0645c18 commit 475cfb7

File tree

1 file changed

+100
-135
lines changed

1 file changed

+100
-135
lines changed

front_end/panels/ai_assistance/agents/AiAgent.ts

Lines changed: 100 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,66 @@ export abstract class AiAgent<T> {
333333
get isHistoryEntry(): boolean {
334334
return this.#generatedFromHistory;
335335
}
336-
337336
async * run(initialQuery: string, options: {
338337
signal?: AbortSignal, selected: ConversationContext<T>|null,
338+
}): AsyncGenerator<ResponseData, void, void> {
339+
for await (const response of this.#run(initialQuery, options)) {
340+
this.#addHistory(response);
341+
yield response;
342+
}
343+
}
344+
345+
async * runFromHistory(): AsyncGenerator<ResponseData, void, void> {
346+
if (this.isEmpty) {
347+
return;
348+
}
349+
350+
this.#generatedFromHistory = true;
351+
for (const entry of this.#history) {
352+
yield entry;
353+
}
354+
}
355+
356+
parseResponse(response: Host.AidaClient.AidaResponse): ParsedResponse {
357+
if (response.functionCalls && response.completed) {
358+
throw new Error('Function calling not supported yet');
359+
}
360+
return {
361+
answer: response.explanation,
362+
};
363+
}
364+
365+
/**
366+
* Declare a function that the AI model can call.
367+
* @param name - The name of the function
368+
* @param declaration - the function declaration. Currently functions must:
369+
* 1. Return an object of serializable key/value pairs. You cannot return
370+
* anything other than a plain JavaScript object that can be serialized.
371+
* 2. Take one parameter which is an object that can have
372+
* multiple keys and values. For example, rather than a function being called
373+
* with two args, `foo` and `bar`, you should instead have the function be
374+
* called with one object with `foo` and `bar` keys.
375+
*/
376+
protected declareFunction<Args extends Record<string, unknown>, ReturnType = unknown>(
377+
name: string, declaration: FunctionDeclaration<Args, ReturnType>): void {
378+
if (this.#functionDeclarations.has(name)) {
379+
throw new Error(`Duplicate function declaration ${name}`);
380+
}
381+
this.#functionDeclarations.set(name, declaration as FunctionDeclaration<Record<string, unknown>, ReturnType>);
382+
}
383+
384+
protected formatParsedAnswer({answer}: ParsedAnswer): string {
385+
return answer;
386+
}
387+
388+
protected handleAction(action: string, options?: {signal?: AbortSignal}):
389+
AsyncGenerator<SideEffectResponse, ActionResponse, void>;
390+
protected handleAction(): never {
391+
throw new Error('Unexpected action found');
392+
}
393+
394+
async * #run(initialQuery: string, options: {
395+
signal?: AbortSignal, selected: ConversationContext<T>|null,
339396
}): AsyncGenerator<ResponseData, void, void> {
340397
if (this.#generatedFromHistory) {
341398
throw new Error('History entries are read-only.');
@@ -360,24 +417,17 @@ export abstract class AiAgent<T> {
360417
// Request is built here to capture history up to this point.
361418
let request = this.buildRequest(query, Host.AidaClient.Role.USER);
362419

363-
const response = {
420+
yield {
364421
type: ResponseType.USER_QUERY,
365422
query: initialQuery,
366-
} as const;
367-
this.#addHistory(response);
368-
yield response;
423+
};
369424

370-
for await (const response of this.handleContextDetails(options.selected)) {
371-
this.#addHistory(response);
372-
yield response;
373-
}
425+
yield* this.handleContextDetails(options.selected);
374426

375427
for (let i = 0; i < MAX_STEPS; i++) {
376-
const queryResponse = {
428+
yield {
377429
type: ResponseType.QUERYING,
378-
} as const;
379-
this.#addHistory(queryResponse);
380-
yield queryResponse;
430+
};
381431

382432
let rpcId: Host.AidaClient.RpcGlobalId|undefined;
383433
let parsedResponse: ParsedResponse|undefined = undefined;
@@ -400,44 +450,33 @@ export abstract class AiAgent<T> {
400450
} catch (err) {
401451
debugLog('Error calling the AIDA API', err);
402452

403-
this.#removeLastRunParts();
453+
let error = ErrorType.UNKNOWN;
404454
if (err instanceof Host.AidaClient.AidaAbortError) {
405-
const response = this.#createAbortResponse();
406-
this.#addHistory(response);
407-
yield response;
408-
break;
455+
error = ErrorType.ABORT;
456+
} else if (err instanceof Host.AidaClient.AidaBlockError) {
457+
error = ErrorType.BLOCK;
409458
}
410-
411-
const error = (err instanceof Host.AidaClient.AidaBlockError) ? ErrorType.BLOCK : ErrorType.UNKNOWN;
412-
const response = {
413-
type: ResponseType.ERROR,
414-
error,
415-
} as const;
416-
this.#addHistory(response);
417-
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceError);
418-
yield response;
459+
yield this.#createErrorResponse(error);
419460

420461
break;
421462
}
422463

423464
this.#partsHistory.push(request.current_message);
424465

425466
if (parsedResponse && 'answer' in parsedResponse && Boolean(parsedResponse.answer)) {
426-
const response = {
427-
type: ResponseType.ANSWER,
428-
text: parsedResponse.answer,
429-
suggestions: parsedResponse.suggestions,
430-
rpcId,
431-
} as const;
432467
this.#partsHistory.push({
433468
parts: [{
434469
text: this.formatParsedAnswer(parsedResponse),
435470
}],
436471
role: Host.AidaClient.Role.MODEL,
437472
});
438473
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceAnswerReceived);
439-
this.#addHistory(response);
440-
yield response;
474+
yield {
475+
type: ResponseType.ANSWER,
476+
text: parsedResponse.answer,
477+
suggestions: parsedResponse.suggestions,
478+
rpcId,
479+
};
441480
break;
442481
} else if (parsedResponse && !('answer' in parsedResponse)) {
443482
const {
@@ -447,23 +486,19 @@ export abstract class AiAgent<T> {
447486
} = parsedResponse;
448487

449488
if (title) {
450-
const response = {
489+
yield {
451490
type: ResponseType.TITLE,
452491
title,
453492
rpcId,
454-
} as const;
455-
this.#addHistory(response);
456-
yield response;
493+
};
457494
}
458495

459496
if (thought) {
460-
const response = {
497+
yield {
461498
type: ResponseType.THOUGHT,
462499
thought,
463500
rpcId,
464-
} as const;
465-
this.#addHistory(response);
466-
yield response;
501+
};
467502
}
468503

469504
this.#partsHistory.push({
@@ -476,14 +511,9 @@ export abstract class AiAgent<T> {
476511
if (action) {
477512
const result = yield* this.handleAction(action, {signal: options.signal});
478513
if (options?.signal?.aborted) {
479-
this.#removeLastRunParts();
480-
const response = this.#createAbortResponse();
481-
this.#addHistory(response);
482-
483-
yield response;
514+
yield this.#createErrorResponse(ErrorType.ABORT);
484515
break;
485516
}
486-
this.#addHistory(result);
487517
query = {text: `${OBSERVATION_PREFIX} ${result.output}`};
488518
// Capture history state for the next iteration query.
489519
request = this.buildRequest(query, Host.AidaClient.Role.USER);
@@ -494,12 +524,11 @@ export abstract class AiAgent<T> {
494524
const result = yield* this.#callFunction(functionCall.name, functionCall.args);
495525

496526
if (result.result) {
497-
const response = {
527+
yield {
498528
type: ResponseType.ACTION,
499529
output: JSON.stringify(result.result),
500530
canceled: false,
501-
} as const;
502-
yield response;
531+
};
503532
}
504533

505534
query = {
@@ -510,23 +539,11 @@ export abstract class AiAgent<T> {
510539
};
511540
request = this.buildRequest(query, Host.AidaClient.Role.ROLE_UNSPECIFIED);
512541
} catch {
513-
this.#removeLastRunParts();
514-
const response = {
515-
type: ResponseType.ERROR,
516-
error: ErrorType.UNKNOWN,
517-
} as const;
518-
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceError);
519-
yield response;
542+
yield this.#createErrorResponse(ErrorType.UNKNOWN);
520543
break;
521544
}
522545
} else {
523-
this.#removeLastRunParts();
524-
const response = {
525-
type: ResponseType.ERROR,
526-
error: i - 1 === MAX_STEPS ? ErrorType.MAX_STEPS : ErrorType.UNKNOWN,
527-
} as const;
528-
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceError);
529-
yield response;
546+
yield this.#createErrorResponse(i - 1 === MAX_STEPS ? ErrorType.MAX_STEPS : ErrorType.UNKNOWN);
530547
break;
531548
}
532549
}
@@ -536,55 +553,6 @@ export abstract class AiAgent<T> {
536553
}
537554
}
538555

539-
async * runFromHistory(): AsyncGenerator<ResponseData, void, void> {
540-
if (this.isEmpty) {
541-
return;
542-
}
543-
544-
this.#generatedFromHistory = true;
545-
for (const entry of this.#history) {
546-
yield entry;
547-
}
548-
}
549-
550-
parseResponse(response: Host.AidaClient.AidaResponse): ParsedResponse {
551-
if (response.functionCalls && response.completed) {
552-
throw new Error('Function calling not supported yet');
553-
}
554-
return {
555-
answer: response.explanation,
556-
};
557-
}
558-
559-
/**
560-
* Declare a function that the AI model can call.
561-
* @param name - The name of the function
562-
* @param declaration - the function declaration. Currently functions must:
563-
* 1. Return an object of serializable key/value pairs. You cannot return
564-
* anything other than a plain JavaScript object that can be serialized.
565-
* 2. Take one parameter which is an object that can have
566-
* multiple keys and values. For example, rather than a function being called
567-
* with two args, `foo` and `bar`, you should instead have the function be
568-
* called with one object with `foo` and `bar` keys.
569-
*/
570-
protected declareFunction<Args extends Record<string, unknown>, ReturnType = unknown>(
571-
name: string, declaration: FunctionDeclaration<Args, ReturnType>): void {
572-
if (this.#functionDeclarations.has(name)) {
573-
throw new Error(`Duplicate function declaration ${name}`);
574-
}
575-
this.#functionDeclarations.set(name, declaration as FunctionDeclaration<Record<string, unknown>, ReturnType>);
576-
}
577-
578-
protected formatParsedAnswer({answer}: ParsedAnswer): string {
579-
return answer;
580-
}
581-
582-
protected handleAction(action: string, options?: {signal?: AbortSignal}):
583-
AsyncGenerator<SideEffectResponse, ActionResponse, void>;
584-
protected handleAction(): never {
585-
throw new Error('Unexpected action found');
586-
}
587-
588556
async * #callFunction(name: string, args: Record<string, unknown>, options?: {
589557
signal?: AbortSignal,
590558
approved?: boolean,
@@ -606,39 +574,32 @@ export abstract class AiAgent<T> {
606574
if (call.displayInfoFromArgs) {
607575
const {title, thought, code, suggestions} = call.displayInfoFromArgs(args);
608576
if (title) {
609-
const response = {
577+
yield {
610578
type: ResponseType.TITLE,
611579
title,
612-
} as const;
613-
this.#addHistory(response);
614-
yield response;
580+
};
615581
}
616582

617583
if (thought) {
618-
const response = {
584+
yield {
619585
type: ResponseType.THOUGHT,
620586
thought,
621-
} as const;
622-
this.#addHistory(response);
623-
yield response;
587+
};
624588
}
625589

626590
if (code) {
627-
const response = {
591+
yield {
628592
type: ResponseType.ACTION,
629593
code,
630594
canceled: false,
631-
} as const;
632-
this.#addHistory(response);
633-
yield response;
595+
};
634596
}
635597

636598
if (suggestions) {
637-
const response = {
599+
yield {
638600
type: ResponseType.SUGGESTIONS,
639601
suggestions,
640-
} as const;
641-
yield response;
602+
};
642603
}
643604
}
644605

@@ -671,12 +632,11 @@ export abstract class AiAgent<T> {
671632

672633
const approvedRun = await sideEffectConfirmationPromiseWithResolvers.promise;
673634
if (!approvedRun) {
674-
const response = {
635+
yield {
675636
type: ResponseType.ACTION,
676637
code: '',
677638
canceled: true,
678-
} as const;
679-
yield response;
639+
};
680640
return {
681641
result: 'Error: User denied code execution with side effects.',
682642
};
@@ -775,10 +735,15 @@ STOP`;
775735
}));
776736
}
777737

778-
#createAbortResponse(): ResponseData {
738+
#createErrorResponse(error: ErrorType): ResponseData {
739+
this.#removeLastRunParts();
740+
if (error !== ErrorType.ABORT) {
741+
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceError);
742+
}
743+
779744
return {
780745
type: ResponseType.ERROR,
781-
error: ErrorType.ABORT,
746+
error,
782747
};
783748
}
784749
}

0 commit comments

Comments
 (0)