Skip to content

Commit 6ac2bba

Browse files
Lightning00BladeDevtools-frontend LUCI CQ
authored andcommitted
[AI Assistance] Add types for Function calling
Provide the ability to create function calling agents Bug: 360751542 Change-Id: If8be4bfdb7ef0f651c6b1ff7c71496cc3d402559 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6070375 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]>
1 parent 1f24eb2 commit 6ac2bba

File tree

6 files changed

+149
-28
lines changed

6 files changed

+149
-28
lines changed

front_end/core/host/AidaClient.test.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,27 @@ describeWithEnvironment('AidaClient', () => {
154154
const provider = new Host.AidaClient.AidaClient();
155155
const results = await getAllResults(provider);
156156
assert.deepStrictEqual(results, [
157-
{explanation: 'hello ', metadata: {rpcGlobalId: 123}, completed: false},
158-
{explanation: 'hello brave ', metadata: {rpcGlobalId: 123}, completed: false},
159-
{explanation: 'hello brave new world!', metadata: {rpcGlobalId: 123}, completed: false},
160-
{explanation: 'hello brave new world!', metadata: {rpcGlobalId: 123}, completed: true},
157+
{
158+
explanation: 'hello ',
159+
metadata: {rpcGlobalId: 123},
160+
completed: false,
161+
},
162+
{
163+
explanation: 'hello brave ',
164+
metadata: {rpcGlobalId: 123},
165+
completed: false,
166+
},
167+
{
168+
explanation: 'hello brave new world!',
169+
metadata: {rpcGlobalId: 123},
170+
completed: false,
171+
},
172+
{
173+
explanation: 'hello brave new world!',
174+
metadata: {rpcGlobalId: 123},
175+
functionCall: undefined,
176+
completed: true,
177+
},
161178
]);
162179
});
163180

@@ -175,8 +192,17 @@ describeWithEnvironment('AidaClient', () => {
175192
const provider = new Host.AidaClient.AidaClient();
176193
const results = await getAllResults(provider);
177194
assert.deepStrictEqual(results, [
178-
{explanation: 'hello world', metadata: {rpcGlobalId: 123}, completed: false},
179-
{explanation: 'hello world', metadata: {rpcGlobalId: 123}, completed: true},
195+
{
196+
explanation: 'hello world',
197+
metadata: {rpcGlobalId: 123},
198+
completed: false,
199+
},
200+
{
201+
explanation: 'hello world',
202+
metadata: {rpcGlobalId: 123},
203+
functionCall: undefined,
204+
completed: true,
205+
},
180206
]);
181207
});
182208

@@ -255,6 +281,7 @@ describeWithEnvironment('AidaClient', () => {
255281
'If it were so, it was a grievous fault,\n' +
256282
'And grievously hath Caesar answer’d it.\n',
257283
metadata: {rpcGlobalId: 123},
284+
functionCall: undefined,
258285
completed: true,
259286
},
260287
]);
@@ -315,6 +342,7 @@ describeWithEnvironment('AidaClient', () => {
315342
},
316343
],
317344
},
345+
functionCall: undefined,
318346
completed: true,
319347
},
320348
]);

front_end/core/host/AidaClient.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,53 @@ export interface Content {
3131
role: Role;
3232
}
3333

34-
export interface Part {
35-
text?: string;
34+
export type Part = {
35+
text: string,
36+
}|{
37+
functionResponse: {
38+
name: string,
39+
response: Record<string, unknown>,
40+
},
41+
}|{
3642
// Inline media bytes.
37-
inlineData?: MediaBlob;
43+
inlineData: MediaBlob,
44+
};
45+
46+
export const enum ParametersTypes {
47+
STRING = 1,
48+
NUMBER = 2,
49+
INTEGER = 3,
50+
BOOLEAN = 4,
51+
ARRAY = 5,
52+
OBJECT = 6,
53+
}
54+
55+
interface BaseFunctionParam {
56+
description: string;
57+
nullable?: boolean;
58+
}
59+
60+
interface FunctionPrimitiveParams extends BaseFunctionParam {
61+
type: ParametersTypes.BOOLEAN|ParametersTypes.INTEGER|ParametersTypes.STRING|ParametersTypes.BOOLEAN;
62+
}
63+
interface FunctionObjectParam extends BaseFunctionParam {
64+
type: ParametersTypes.OBJECT;
65+
// TODO: this can be also be ObjectParams
66+
properties: {[Key in string]: FunctionPrimitiveParams};
67+
}
68+
// TODO: Add FunctionArrayParam
69+
70+
/**
71+
* More about function declaration can be read at
72+
* https://ai.google.dev/gemini-api/docs/function-calling
73+
*/
74+
export interface FunctionDeclaration {
75+
name: string;
76+
/**
77+
* A description for the LLM to understand what the specific function will do once called.
78+
*/
79+
description: string;
80+
parameters: FunctionObjectParam|FunctionPrimitiveParams;
3881
}
3982

4083
// Raw media bytes.
@@ -83,12 +126,14 @@ export enum UserTier {
83126
}
84127

85128
export interface AidaRequest {
129+
client: string;
86130
// eslint-disable-next-line @typescript-eslint/naming-convention
87131
current_message?: Content;
88132
preamble?: string;
89133
// eslint-disable-next-line @typescript-eslint/naming-convention
90134
historical_contexts?: Content[];
91-
client: string;
135+
// eslint-disable-next-line @typescript-eslint/naming-convention
136+
function_declarations?: FunctionDeclaration[];
92137
options?: {
93138
temperature?: number,
94139
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -145,6 +190,11 @@ export interface AttributionMetadata {
145190
citations: Citation[];
146191
}
147192

193+
export interface AidaFunctionCallResponse {
194+
name: string;
195+
args: Record<string, unknown>;
196+
}
197+
148198
export interface AidaResponseMetadata {
149199
rpcGlobalId?: number;
150200
attributionMetadata?: AttributionMetadata[];
@@ -153,6 +203,7 @@ export interface AidaResponseMetadata {
153203
export interface AidaResponse {
154204
explanation: string;
155205
metadata: AidaResponseMetadata;
206+
functionCall?: AidaFunctionCallResponse;
156207
completed: boolean;
157208
}
158209

@@ -261,6 +312,7 @@ export class AidaClient {
261312
let chunk;
262313
const text = [];
263314
let inCodeChunk = false;
315+
let functionCall: AidaFunctionCallResponse|undefined = undefined;
264316
const metadata: AidaResponseMetadata = {rpcGlobalId: 0};
265317
while ((chunk = await stream.read())) {
266318
let textUpdated = false;
@@ -315,6 +367,11 @@ export class AidaClient {
315367
}
316368
text.push(result.codeChunk.code);
317369
textUpdated = true;
370+
} else if ('functionCallChunk' in result) {
371+
functionCall = {
372+
name: result.functionCallChunk.functionCall.name,
373+
args: result.functionCallChunk.functionCall.args,
374+
};
318375
} else if ('error' in result) {
319376
throw new Error(`Server responded: ${JSON.stringify(result)}`);
320377
} else {
@@ -332,6 +389,7 @@ export class AidaClient {
332389
yield {
333390
explanation: text.join('') + (inCodeChunk ? CODE_CHUNK_SEPARATOR : ''),
334391
metadata,
392+
functionCall,
335393
completed: true,
336394
};
337395
}

front_end/panels/freestyler/AiAgent.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describeWithEnvironment('AiAgent', () => {
132132
serverSideLoggingEnabled: false,
133133
});
134134
const request = agent.buildRequest({text: 'test input'});
135-
assert.strictEqual(request.current_message?.parts[0].text, 'test input');
135+
assert.deepStrictEqual(request.current_message?.parts[0], {text: 'test input'});
136136
assert.strictEqual(request.historical_contexts, undefined);
137137
});
138138

@@ -149,7 +149,7 @@ describeWithEnvironment('AiAgent', () => {
149149
aidaClient: {} as Host.AidaClient.AidaClient,
150150
});
151151
const request = agent.buildRequest({text: 'test input'});
152-
assert.strictEqual(request.current_message?.parts[0].text, 'test input');
152+
assert.deepStrictEqual(request.current_message?.parts[0], {text: 'test input'});
153153
assert.strictEqual(request.preamble, 'preamble');
154154
assert.strictEqual(request.historical_contexts, undefined);
155155
});
@@ -191,7 +191,7 @@ describeWithEnvironment('AiAgent', () => {
191191
},
192192
];
193193
const request = agent.buildRequest({text: 'test input'});
194-
assert.strictEqual(request.current_message?.parts[0].text, 'test input');
194+
assert.deepStrictEqual(request.current_message?.parts[0], {text: 'test input'});
195195
assert.deepStrictEqual(request.historical_contexts, [
196196
{
197197
parts: [{text: 'test'}],
@@ -239,7 +239,7 @@ describeWithEnvironment('AiAgent', () => {
239239
},
240240
];
241241
const request = agent.buildRequest({text: 'test input'});
242-
assert.strictEqual(request.current_message?.parts[0].text, 'test input');
242+
assert.deepStrictEqual(request.current_message?.parts[0], {text: 'test input'});
243243
assert.deepStrictEqual(request.historical_contexts, undefined);
244244
});
245245

@@ -300,7 +300,7 @@ describeWithEnvironment('AiAgent', () => {
300300
},
301301
];
302302
const request = agent.buildRequest({text: 'test input'});
303-
assert.strictEqual(request.current_message?.parts[0].text, 'test input');
303+
assert.deepStrictEqual(request.current_message?.parts[0], {text: 'test input'});
304304
assert.deepStrictEqual(request.historical_contexts, [
305305
{
306306
parts: [{text: 'test2'}],

front_end/panels/freestyler/AiAgent.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export abstract class ConversationContext<T> {
156156
}
157157
}
158158

159+
interface AgentFunctionDefinition extends Host.AidaClient.FunctionDeclaration {
160+
method: (...args: any[]) => Record<string, unknown>;
161+
}
162+
159163
export abstract class AiAgent<T> {
160164
static validTemperature(temperature: number|undefined): number|undefined {
161165
return typeof temperature === 'number' && temperature >= 0 ? temperature : undefined;
@@ -171,6 +175,7 @@ export abstract class AiAgent<T> {
171175
abstract readonly clientFeature: Host.AidaClient.ClientFeature;
172176
abstract readonly userTier: string|undefined;
173177
abstract handleContextDetails(select: ConversationContext<T>|null): AsyncGenerator<ContextResponse, void, void>;
178+
functionDefinitions: AgentFunctionDefinition[]|undefined;
174179
#generatedFromHistory = false;
175180

176181
/**
@@ -231,6 +236,16 @@ export abstract class AiAgent<T> {
231236
return this.#generatedFromHistory;
232237
}
233238

239+
get functionDeclarations(): Host.AidaClient.FunctionDeclaration[]|undefined {
240+
return this.functionDefinitions?.map(call => {
241+
return {
242+
name: call.name,
243+
description: call.description,
244+
parameters: call.parameters,
245+
} satisfies Host.AidaClient.FunctionDeclaration;
246+
});
247+
}
248+
234249
serialized(): SerializedAgent {
235250
return {
236251
id: this.id,
@@ -256,8 +271,17 @@ export abstract class AiAgent<T> {
256271
for await (rawResponse of this.#aidaClient.fetch(request, options)) {
257272
response = rawResponse.explanation;
258273
rpcId = rawResponse.metadata.rpcGlobalId ?? rpcId;
274+
275+
if (rawResponse.functionCall) {
276+
throw new Error('Function calling not supported yet');
277+
}
278+
259279
const parsedResponse = this.parseResponse(response);
260-
yield {rpcId, parsedResponse, completed: rawResponse.completed};
280+
yield {
281+
rpcId,
282+
parsedResponse,
283+
completed: rawResponse.completed,
284+
};
261285
}
262286

263287
debugLog({
@@ -273,16 +297,22 @@ export abstract class AiAgent<T> {
273297
localStorage.setItem('freestylerStructuredLog', JSON.stringify(this.#structuredLog));
274298
}
275299

276-
buildRequest(opts: BuildRequestOptions): Host.AidaClient.AidaRequest {
277-
const currentMessage = {parts: [{text: opts.text}], role: Host.AidaClient.Role.USER};
300+
buildRequest(part: Host.AidaClient.Part): Host.AidaClient.AidaRequest {
301+
const currentMessage: Host.AidaClient.Content = {
302+
parts: [part],
303+
role: Host.AidaClient.Role.USER,
304+
};
278305
const history = this.#chatHistoryForAida;
306+
const declarations = this.functionDeclarations;
279307
const request: Host.AidaClient.AidaRequest = {
308+
client: Host.AidaClient.CLIENT_NAME,
280309
// eslint-disable-next-line @typescript-eslint/naming-convention
281310
current_message: currentMessage,
282311
preamble: this.preamble,
283312
// eslint-disable-next-line @typescript-eslint/naming-convention
284313
historical_contexts: history.length ? history : undefined,
285-
client: Host.AidaClient.CLIENT_NAME,
314+
// eslint-disable-next-line @typescript-eslint/naming-convention
315+
...(declarations ? {function_declarations: declarations} : {}),
286316
options: {
287317
temperature: AiAgent.validTemperature(this.options.temperature),
288318
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -364,14 +394,18 @@ STOP`;
364394
flushCurrentStep();
365395
history.push({
366396
role: Host.AidaClient.Role.USER,
367-
parts: [{text: data.query}],
397+
parts: [{
398+
text: data.query,
399+
}],
368400
});
369401
break;
370402
}
371403
case ResponseType.ANSWER:
372404
history.push({
373405
role: Host.AidaClient.Role.MODEL,
374-
parts: [{text: this.formatParsedAnswer({answer: data.text})}],
406+
parts: [{
407+
text: this.formatParsedAnswer({answer: data.text}),
408+
}],
375409
});
376410
break;
377411
case ResponseType.TITLE:

front_end/panels/freestyler/FreestylerAgent.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -839,9 +839,9 @@ STOP`,
839839
assert.isUndefined(requests[0].historical_contexts, 'Unexpected historical contexts in the initial request');
840840
assert.exists(requests[0].current_message);
841841
assert.lengthOf(requests[0].current_message.parts, 1);
842-
assert.strictEqual(
843-
requests[0].current_message.parts[0].text,
844-
'# Inspected element\n\n* Its selector is `undefined`\n\n# User request\n\nQUERY: test',
842+
assert.deepStrictEqual(
843+
requests[0].current_message.parts[0],
844+
{text: '# Inspected element\n\n* Its selector is `undefined`\n\n# User request\n\nQUERY: test'},
845845
'Unexpected input text in the initial request');
846846
assert.strictEqual(requests[0].current_message.role, Host.AidaClient.Role.USER);
847847
assert.deepStrictEqual(
@@ -862,8 +862,8 @@ STOP`,
862862
'Unexpected historical contexts in the follow-up request');
863863
assert.exists(requests[1].current_message);
864864
assert.lengthOf(requests[1].current_message.parts, 1);
865-
assert.strictEqual(
866-
requests[1].current_message.parts[0].text, 'OBSERVATION: test data',
865+
assert.deepStrictEqual(
866+
requests[1].current_message.parts[0], {text: 'OBSERVATION: test data'},
867867
'Unexpected input in the follow-up request');
868868
});
869869

test/e2e/freestyler/freestyler_test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ STOP`,
307307
iframeId: 'iframe',
308308
});
309309

310-
assert.deepStrictEqual(
311-
result.at(-1)!.request.current_message.parts[0].text, 'OBSERVATION: {"title":"I have a title"}');
310+
assert.deepStrictEqual(result.at(-1)!.request.current_message.parts[0], {
311+
text: 'OBSERVATION: {"title":"I have a title"}',
312+
});
312313
});
313314
});

0 commit comments

Comments
 (0)