Skip to content

Commit b62483a

Browse files
fix: aggregate text & functionCall(s) correctly (#497)
* fix: aggregate text & functionCall(s) correctly content parts should have either text or functionCall set, but never both. When aggregating, combine the text (as before), but add functionCall(s) as new parts. Then when done, if the text from part[0] is empty, that part can be removed. fixes #494 * fix: aggregate text & functionCall(s) correctly don't delete the text part, because it could just be added back again when working with multiple responses (and the end up as `"undefined\n"` for example). Leave the text part, and always push functions as new parts. fixes #494 * fix: aggregate text & functionCall(s) correctly the AI API gives an error for empty text, so the empty text has to be deleted after aggregating all responses fixes #494 * chore: fix style using script --------- Co-authored-by: Yvonne Yu <150068659+yyyu-google@users.noreply.github.com>
1 parent 46f655b commit b62483a

File tree

3 files changed

+100
-6
lines changed

3 files changed

+100
-6
lines changed

src/functions/post_fetch_processing.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,16 +249,15 @@ export function aggregateResponses(
249249
response.candidates[i].content.parts &&
250250
response.candidates[i].content.parts.length > 0
251251
) {
252+
const {parts} = aggregatedResponse.candidates[i].content;
252253
for (const part of response.candidates[i].content.parts) {
254+
// NOTE: cannot have text and functionCall both in the same part.
255+
// add functionCall(s) to new parts.
253256
if (part.text) {
254-
aggregatedResponse.candidates[i].content.parts[0].text += part.text;
257+
parts[0].text += part.text;
255258
}
256259
if (part.functionCall) {
257-
aggregatedResponse.candidates[i].content.parts[0].functionCall =
258-
part.functionCall;
259-
// the empty 'text' key should be removed if functionCall is in the
260-
// response
261-
delete aggregatedResponse.candidates[i].content.parts[0].text;
260+
parts.push({functionCall: part.functionCall});
262261
}
263262
}
264263
}
@@ -273,6 +272,16 @@ export function aggregateResponses(
273272
}
274273
}
275274
}
275+
if (aggregatedResponse.candidates?.length) {
276+
aggregatedResponse.candidates.forEach(candidate => {
277+
if (
278+
candidate.content.parts.length > 1 &&
279+
candidate.content.parts[0].text === ''
280+
) {
281+
candidate.content.parts.shift(); // remove empty text parameter
282+
}
283+
});
284+
}
276285
return aggregatedResponse;
277286
}
278287

src/functions/test/post_fetch_processing_test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ import {
2020
AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_2,
2121
AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_3,
2222
AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_4,
23+
AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_5,
24+
AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_6,
2325
COUNT_TOKENS_RESPONSE_1,
2426
STREAM_RESPONSE_CHUNKS_1,
2527
STREAM_RESPONSE_CHUNKS_2,
2628
STREAM_RESPONSE_CHUNKS_3,
2729
STREAM_RESPONSE_CHUNKS_4,
30+
STREAM_RESPONSE_CHUNKS_5,
31+
STREAM_RESPONSE_CHUNKS_6,
2832
UNARY_RESPONSE_1,
2933
UNARY_RESPONSE_MISSING_ROLE_INDEX,
3034
} from './test_data';
@@ -84,6 +88,22 @@ describe('aggregateResponses', () => {
8488
JSON.stringify(AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_4)
8589
);
8690
});
91+
92+
it('should aggregate text and functionCalls to separate parts', () => {
93+
const actualResult = aggregateResponses(STREAM_RESPONSE_CHUNKS_5);
94+
95+
expect(JSON.stringify(actualResult)).toEqual(
96+
JSON.stringify(AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_5)
97+
);
98+
});
99+
100+
it('should aggregate functionCalls with the empty text removed', () => {
101+
const actualResult = aggregateResponses(STREAM_RESPONSE_CHUNKS_6);
102+
103+
expect(JSON.stringify(actualResult)).toEqual(
104+
JSON.stringify(AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_6)
105+
);
106+
});
87107
});
88108

89109
describe('processUnary', () => {

src/functions/test/test_data.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,71 @@ export const AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_4: GenerateContentRespon
905905
],
906906
} as GenerateContentResponse;
907907

908+
export const STREAM_RESPONSE_CHUNKS_5: GenerateContentResponse[] = [
909+
{
910+
candidates: [
911+
{
912+
content: {
913+
parts: [
914+
{functionCall: {name: 'fn', args: {a: 1}}},
915+
{text: 'chunk1Text1'},
916+
],
917+
},
918+
},
919+
],
920+
},
921+
{
922+
candidates: [
923+
{
924+
content: {
925+
parts: [{text: 'chunk2Text1'}],
926+
},
927+
},
928+
],
929+
},
930+
] as GenerateContentResponse[];
931+
932+
export const AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_5: GenerateContentResponse =
933+
{
934+
candidates: [
935+
{
936+
index: 0,
937+
content: {
938+
role: 'model',
939+
parts: [
940+
{text: 'chunk1Text1chunk2Text1'},
941+
{functionCall: {name: 'fn', args: {a: 1}}},
942+
],
943+
},
944+
},
945+
],
946+
} as GenerateContentResponse;
947+
948+
export const STREAM_RESPONSE_CHUNKS_6: GenerateContentResponse[] = [
949+
{
950+
candidates: [
951+
{
952+
content: {
953+
parts: [{functionCall: {name: 'fn', args: {a: 1}}}],
954+
},
955+
},
956+
],
957+
},
958+
] as GenerateContentResponse[];
959+
960+
export const AGGREGATED_RESPONSE_STREAM_RESPONSE_CHUNKS_6: GenerateContentResponse =
961+
{
962+
candidates: [
963+
{
964+
index: 0,
965+
content: {
966+
role: 'model',
967+
parts: [{functionCall: {name: 'fn', args: {a: 1}}}],
968+
},
969+
},
970+
],
971+
} as GenerateContentResponse;
972+
908973
export const UNARY_RESPONSE_1: GenerateContentResponse = {
909974
candidates: [
910975
{

0 commit comments

Comments
 (0)