Skip to content

Commit c90203d

Browse files
yyyu-googlecopybara-github
authored andcommitted
fix: throws instructive client side error message when bad request happens for function calling
PiperOrigin-RevId: 602531639
1 parent 22f4950 commit c90203d

File tree

2 files changed

+159
-14
lines changed

2 files changed

+159
-14
lines changed

src/index.ts

+48-14
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ export class ChatSession {
248248
async sendMessage(
249249
request: string | Array<string | Part>
250250
): Promise<GenerateContentResult> {
251-
const newContent: Content[] = formulateNewContent(request);
251+
const newContent: Content[] =
252+
formulateNewContentFromSendMessageRequest(request);
252253
const generateContentrequest: GenerateContentRequest = {
253254
contents: this.historyInternal.concat(newContent),
254255
safety_settings: this.safety_settings,
@@ -311,7 +312,8 @@ export class ChatSession {
311312
async sendMessageStream(
312313
request: string | Array<string | Part>
313314
): Promise<StreamGenerateContentResult> {
314-
const newContent: Content[] = formulateNewContent(request);
315+
const newContent: Content[] =
316+
formulateNewContentFromSendMessageRequest(request);
315317
const generateContentrequest: GenerateContentRequest = {
316318
contents: this.historyInternal.concat(newContent),
317319
safety_settings: this.safety_settings,
@@ -390,7 +392,7 @@ export class GenerativeModel {
390392
this.safety_settings
391393
);
392394

393-
validateGcsInput(request.contents);
395+
validateGenerateContentRequest(request);
394396

395397
if (request.generation_config) {
396398
request.generation_config = validateGenerationConfig(
@@ -445,7 +447,7 @@ export class GenerativeModel {
445447
this.generation_config,
446448
this.safety_settings
447449
);
448-
validateGcsInput(request.contents);
450+
validateGenerateContentRequest(request);
449451

450452
if (request.generation_config) {
451453
request.generation_config = validateGenerationConfig(
@@ -521,7 +523,7 @@ export class GenerativeModel {
521523
}
522524
}
523525

524-
function formulateNewContent(
526+
function formulateNewContentFromSendMessageRequest(
525527
request: string | Array<string | Part>
526528
): Content[] {
527529
let newParts: Part[] = [];
@@ -538,7 +540,7 @@ function formulateNewContent(
538540
}
539541
}
540542

541-
return formatPartsByRole(newParts);
543+
return assignRoleToPartsAndValidateSendMessageRequest(newParts);
542544
}
543545

544546
/**
@@ -549,27 +551,38 @@ function formulateNewContent(
549551
* @param {Array<Part>} parts Array of parts to pass to the model
550552
* @return {Content[]} Array of content items
551553
*/
552-
function formatPartsByRole(parts: Array<Part>): Content[] {
553-
const partsByRole: Content[] = [];
554+
function assignRoleToPartsAndValidateSendMessageRequest(
555+
parts: Array<Part>
556+
): Content[] {
554557
const userContent: Content = {role: constants.USER_ROLE, parts: []};
555558
const functionContent: Content = {role: constants.FUNCTION_ROLE, parts: []};
556-
559+
let hasUserContent = false;
560+
let hasFunctionContent = false;
557561
for (const part of parts) {
558562
if ('functionResponse' in part) {
559563
functionContent.parts.push(part);
564+
hasFunctionContent = true;
560565
} else {
561566
userContent.parts.push(part);
567+
hasUserContent = true;
562568
}
563569
}
564570

565-
if (userContent.parts.length > 0) {
566-
partsByRole.push(userContent);
571+
if (hasUserContent && hasFunctionContent) {
572+
throw new ClientError(
573+
'Within a single message, FunctionResponse cannot be mixed with other type of part in the request for sending chat message.'
574+
);
575+
}
576+
577+
if (!hasUserContent && !hasFunctionContent) {
578+
throw new ClientError('No content is provided for sending chat message.');
567579
}
568-
if (functionContent.parts.length > 0) {
569-
partsByRole.push(functionContent);
580+
581+
if (hasUserContent) {
582+
return [userContent];
570583
}
571584

572-
return partsByRole;
585+
return [functionContent];
573586
}
574587

575588
function throwErrorIfNotOK(response: Response | undefined) {
@@ -603,6 +616,27 @@ function validateGcsInput(contents: Content[]) {
603616
}
604617
}
605618

619+
function validateFunctionResponseRequest(contents: Content[]) {
620+
const lastestContentPart = contents[contents.length - 1].parts[0];
621+
if (!('functionResponse' in lastestContentPart)) {
622+
return;
623+
}
624+
const errorMessage =
625+
'Please ensure that function response turn comes immediately after a function call turn.';
626+
if (contents.length < 2) {
627+
throw new ClientError(errorMessage);
628+
}
629+
const secondLastestContentPart = contents[contents.length - 2].parts[0];
630+
if (!('functionCall' in secondLastestContentPart)) {
631+
throw new ClientError(errorMessage);
632+
}
633+
}
634+
635+
function validateGenerateContentRequest(request: GenerateContentRequest) {
636+
validateGcsInput(request.contents);
637+
validateFunctionResponseRequest(request.contents);
638+
}
639+
606640
function validateGenerationConfig(
607641
generation_config: GenerationConfig
608642
): GenerationConfig {

test/index_test.ts

+111
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,42 @@ describe('VertexAI', () => {
562562
const resp = await model.generateContent(req);
563563
expect(resp).toEqual(expectedResult);
564564
});
565+
566+
it('throws ClientError when functionResponse is not immedidately following functionCall case1', async () => {
567+
const req: GenerateContentRequest = {
568+
contents: [
569+
{role: 'user', parts: [{text: 'What is the weater like in Boston?'}]},
570+
{
571+
role: 'function',
572+
parts: TEST_FUNCTION_RESPONSE_PART,
573+
},
574+
],
575+
tools: TEST_TOOLS_WITH_FUNCTION_DECLARATION,
576+
};
577+
const expectedErrorMessage =
578+
'[VertexAI.ClientError]: Please ensure that function response turn comes immediately after a function call turn.';
579+
await model.generateContent(req).catch(e => {
580+
expect(e.message).toEqual(expectedErrorMessage);
581+
});
582+
});
583+
584+
it('throws ClientError when functionResponse is not immedidately following functionCall case2', async () => {
585+
const req: GenerateContentRequest = {
586+
contents: [
587+
{role: 'user', parts: [{text: 'What is the weater like in Boston?'}]},
588+
{
589+
role: 'function',
590+
parts: TEST_FUNCTION_RESPONSE_PART,
591+
},
592+
],
593+
tools: TEST_TOOLS_WITH_FUNCTION_DECLARATION,
594+
};
595+
const expectedErrorMessage =
596+
'[VertexAI.ClientError]: Please ensure that function response turn comes immediately after a function call turn.';
597+
await model.generateContent(req).catch(e => {
598+
expect(e.message).toEqual(expectedErrorMessage);
599+
});
600+
});
565601
});
566602
describe('generateContentStream', () => {
567603
it('returns a GenerateContentResponse when passed text content', async () => {
@@ -629,6 +665,41 @@ describe('VertexAI', () => {
629665
const resp = await model.generateContentStream(req);
630666
expect(resp).toEqual(expectedStreamResult);
631667
});
668+
it('throws ClientError when functionResponse is not immedidately following functionCall case1', async () => {
669+
const req: GenerateContentRequest = {
670+
contents: [
671+
{role: 'user', parts: [{text: 'What is the weater like in Boston?'}]},
672+
{
673+
role: 'function',
674+
parts: TEST_FUNCTION_RESPONSE_PART,
675+
},
676+
],
677+
tools: TEST_TOOLS_WITH_FUNCTION_DECLARATION,
678+
};
679+
const expectedErrorMessage =
680+
'[VertexAI.ClientError]: Please ensure that function response turn comes immediately after a function call turn.';
681+
await model.generateContentStream(req).catch(e => {
682+
expect(e.message).toEqual(expectedErrorMessage);
683+
});
684+
});
685+
686+
it('throws ClientError when functionResponse is not immedidately following functionCall case2', async () => {
687+
const req: GenerateContentRequest = {
688+
contents: [
689+
{role: 'user', parts: [{text: 'What is the weater like in Boston?'}]},
690+
{
691+
role: 'function',
692+
parts: TEST_FUNCTION_RESPONSE_PART,
693+
},
694+
],
695+
tools: TEST_TOOLS_WITH_FUNCTION_DECLARATION,
696+
};
697+
const expectedErrorMessage =
698+
'[VertexAI.ClientError]: Please ensure that function response turn comes immediately after a function call turn.';
699+
await model.generateContentStream(req).catch(e => {
700+
expect(e.message).toEqual(expectedErrorMessage);
701+
});
702+
});
632703
});
633704

634705
describe('startChat', () => {
@@ -805,6 +876,26 @@ describe('ChatSession', () => {
805876
expect(response2).toEqual(expectedFollowUpResponse);
806877
expect(chatSessionWithFunctionCall.history.length).toEqual(4);
807878
});
879+
880+
it('throw ClientError when request has no content', async () => {
881+
const expectedErrorMessage =
882+
'[VertexAI.ClientError]: No content is provided for sending chat message.';
883+
await chatSessionWithNoArgs.sendMessage([]).catch(e => {
884+
expect(e.message).toEqual(expectedErrorMessage);
885+
});
886+
});
887+
888+
it('throw ClientError when request mix functionCall part with other types of part', async () => {
889+
const chatRequest = [
890+
'what is the weather like in LA',
891+
TEST_FUNCTION_RESPONSE_PART[0],
892+
];
893+
const expectedErrorMessage =
894+
'[VertexAI.ClientError]: Within a single message, FunctionResponse cannot be mixed with other type of part in the request for sending chat message.';
895+
await chatSessionWithNoArgs.sendMessage(chatRequest).catch(e => {
896+
expect(e.message).toEqual(expectedErrorMessage);
897+
});
898+
});
808899
});
809900

810901
describe('sendMessageStream', () => {
@@ -889,6 +980,26 @@ describe('ChatSession', () => {
889980
expect(response2).toEqual(expectedFollowUpStreamResult);
890981
expect(chatSessionWithFunctionCall.history.length).toEqual(4);
891982
});
983+
984+
it('throw ClientError when request has no content', async () => {
985+
const expectedErrorMessage =
986+
'[VertexAI.ClientError]: No content is provided for sending chat message.';
987+
await chatSessionWithNoArgs.sendMessageStream([]).catch(e => {
988+
expect(e.message).toEqual(expectedErrorMessage);
989+
});
990+
});
991+
992+
it('throw ClientError when request mix functionCall part with other types of part', async () => {
993+
const chatRequest = [
994+
'what is the weather like in LA',
995+
TEST_FUNCTION_RESPONSE_PART[0],
996+
];
997+
const expectedErrorMessage =
998+
'[VertexAI.ClientError]: Within a single message, FunctionResponse cannot be mixed with other type of part in the request for sending chat message.';
999+
await chatSessionWithNoArgs.sendMessageStream(chatRequest).catch(e => {
1000+
expect(e.message).toEqual(expectedErrorMessage);
1001+
});
1002+
});
8921003
});
8931004
});
8941005

0 commit comments

Comments
 (0)