1414 * limitations under the License.
1515 */
1616
17- import { GenkitError , z } from 'genkit' ;
17+ import { GenkitError , ToolRequest , z } from 'genkit' ;
1818import {
1919 CandidateData ,
2020 MessageData ,
@@ -23,12 +23,14 @@ import {
2323 TextPart ,
2424 ToolDefinition ,
2525} from 'genkit/model' ;
26+ import { JSONPath } from 'jsonpath-plus' ;
2627import {
2728 FunctionCallingMode ,
2829 FunctionDeclaration ,
2930 GenerateContentCandidate as GeminiCandidate ,
3031 Content as GeminiContent ,
3132 Part as GeminiPart ,
33+ PartialArg ,
3234 Schema ,
3335 SchemaType ,
3436 VideoMetadata ,
@@ -139,38 +141,41 @@ function toGeminiToolRequest(part: Part): GeminiPart {
139141 if ( ! part . toolRequest ?. input ) {
140142 throw Error ( 'Invalid ToolRequestPart: input was missing.' ) ;
141143 }
142- return maybeAddGeminiThoughtSignature ( part , {
143- functionCall : {
144- name : part . toolRequest . name ,
145- args : part . toolRequest . input ,
146- } ,
147- } ) ;
144+ const functionCall : GeminiPart [ 'functionCall' ] = {
145+ name : part . toolRequest . name ,
146+ args : part . toolRequest . input ,
147+ } ;
148+ if ( part . toolRequest . ref ) {
149+ functionCall . id = part . toolRequest . ref ;
150+ }
151+ return maybeAddGeminiThoughtSignature ( part , { functionCall } ) ;
148152}
149153
150154function toGeminiToolResponse ( part : Part ) : GeminiPart {
151155 if ( ! part . toolResponse ?. output ) {
152156 throw Error ( 'Invalid ToolResponsePart: output was missing.' ) ;
153157 }
154- return maybeAddGeminiThoughtSignature ( part , {
155- functionResponse : {
158+ const functionResponse : GeminiPart [ 'functionResponse' ] = {
159+ name : part . toolResponse . name ,
160+ response : {
156161 name : part . toolResponse . name ,
157- response : {
158- name : part . toolResponse . name ,
159- content : part . toolResponse . output ,
160- } ,
162+ content : part . toolResponse . output ,
161163 } ,
164+ } ;
165+ if ( part . toolResponse . ref ) {
166+ functionResponse . id = part . toolResponse . ref ;
167+ }
168+ return maybeAddGeminiThoughtSignature ( part , {
169+ functionResponse,
162170 } ) ;
163171}
164172
165173function toGeminiReasoning ( part : Part ) : GeminiPart {
166174 const out : GeminiPart = { thought : true } ;
167- if ( typeof part . metadata ?. thoughtSignature === 'string' ) {
168- out . thoughtSignature = part . metadata . thoughtSignature ;
169- }
170175 if ( part . reasoning ?. length ) {
171176 out . text = part . reasoning ;
172177 }
173- return out ;
178+ return maybeAddGeminiThoughtSignature ( part , out ) ;
174179}
175180
176181function toGeminiCustom ( part : Part ) : GeminiPart {
@@ -354,10 +359,9 @@ function maybeAddThoughtSignature(geminiPart: GeminiPart, part: Part): Part {
354359}
355360
356361function fromGeminiThought ( part : GeminiPart ) : Part {
357- return {
362+ return maybeAddThoughtSignature ( part , {
358363 reasoning : part . text || '' ,
359- metadata : { thoughtSignature : part . thoughtSignature } ,
360- } ;
364+ } ) ;
361365}
362366
363367function fromGeminiInlineData ( part : GeminiPart ) : Part {
@@ -400,34 +404,153 @@ function fromGeminiFileData(part: GeminiPart): Part {
400404 } ) ;
401405}
402406
403- function fromGeminiFunctionCall ( part : GeminiPart , ref : string ) : Part {
407+ /**
408+ * Applies Gemini partial args to the target object.
409+ *
410+ * https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/Content#PartialArg
411+ */
412+ export function applyGeminiPartialArgs (
413+ target : object ,
414+ partialArgs : PartialArg [ ]
415+ ) {
416+ for ( const partialArg of partialArgs ) {
417+ if ( ! partialArg . jsonPath ) {
418+ continue ;
419+ }
420+ let value : boolean | string | number | null | undefined ;
421+ if ( partialArg . boolValue !== undefined ) {
422+ value = partialArg . boolValue ;
423+ } else if ( partialArg . nullValue !== undefined ) {
424+ value = null ;
425+ } else if ( partialArg . numberValue !== undefined ) {
426+ value = partialArg . numberValue ;
427+ } else if ( partialArg . stringValue !== undefined ) {
428+ value = partialArg . stringValue ;
429+ }
430+ if ( value === undefined ) {
431+ continue ;
432+ }
433+
434+ let current : any = target ;
435+ const path = JSONPath . toPathArray ( partialArg . jsonPath ) ;
436+ // ex: for path '$.data[0][0]' toPathArray returns: ['$', 'data', '0', '0']
437+ // we skip the first (root) reference and dereference the rest.
438+ for ( let i = 1 ; i < path . length - 1 ; i ++ ) {
439+ const key = path [ i ] ;
440+ const nextKey = path [ i + 1 ] ;
441+ if ( current [ key ] === undefined ) {
442+ if ( ! isNaN ( parseInt ( nextKey , 10 ) ) ) {
443+ current [ key ] = [ ] ;
444+ } else {
445+ current [ key ] = { } ;
446+ }
447+ }
448+ current = current [ key ] ;
449+ }
450+
451+ const finalKey = path [ path . length - 1 ] ;
452+ if (
453+ partialArg . stringValue !== undefined &&
454+ typeof current [ finalKey ] === 'string'
455+ ) {
456+ current [ finalKey ] += partialArg . stringValue ;
457+ } else {
458+ current [ finalKey ] = value as any ;
459+ }
460+ }
461+ }
462+
463+ function fromGeminiFunctionCall (
464+ part : GeminiPart ,
465+ previousChunks ?: CandidateData [ ]
466+ ) : Part {
404467 if ( ! part . functionCall ) {
405468 throw Error (
406469 'Invalid Gemini Function Call Part: missing function call data'
407470 ) ;
408471 }
409- return maybeAddThoughtSignature ( part , {
410- toolRequest : {
411- name : part . functionCall . name ,
412- input : part . functionCall . args ,
413- ref,
414- } ,
415- } ) ;
472+ const req : Partial < ToolRequest > = {
473+ name : part . functionCall . name ,
474+ input : part . functionCall . args ,
475+ } ;
476+
477+ if ( part . functionCall . id ) {
478+ req . ref = part . functionCall . id ;
479+ }
480+
481+ if ( part . functionCall . willContinue ) {
482+ req . partial = true ;
483+ }
484+
485+ handleFunctionCallPartials ( req , part , previousChunks ) ;
486+
487+ const toolRequest : Part = { toolRequest : req as ToolRequest } ;
488+
489+ return maybeAddThoughtSignature ( part , toolRequest ) ;
490+ }
491+
492+ function handleFunctionCallPartials (
493+ req : Partial < ToolRequest > ,
494+ part : GeminiPart ,
495+ previousChunks ?: CandidateData [ ]
496+ ) {
497+ if ( ! part . functionCall ) {
498+ throw Error (
499+ 'Invalid Gemini Function Call Part: missing function call data'
500+ ) ;
501+ }
502+
503+ // we try to find if there's a previous partial tool request part.
504+ const prevPart = previousChunks ?. at ( - 1 ) ?. message . content ?. at ( - 1 ) ;
505+ const prevPartialToolRequestPart =
506+ prevPart ?. toolRequest && prevPart ?. toolRequest . partial
507+ ? prevPart
508+ : undefined ;
509+
510+ // if the current functionCall has partialArgs, we try to apply the diff to the
511+ // potentially including the previous partial part.
512+ if ( part . functionCall . partialArgs ) {
513+ const newInput = prevPartialToolRequestPart ?. toolRequest ?. input
514+ ? JSON . parse ( JSON . stringify ( prevPartialToolRequestPart . toolRequest . input ) )
515+ : { } ;
516+ applyGeminiPartialArgs ( newInput , part . functionCall . partialArgs ) ;
517+ req . input = newInput ;
518+ }
519+
520+ // If there's a previous partial part, we copy some fields over, because the
521+ // API will not return these.
522+ if ( prevPartialToolRequestPart ) {
523+ if ( ! req . name ) {
524+ req . name = prevPartialToolRequestPart . toolRequest . name ;
525+ }
526+ if ( ! req . ref ) {
527+ req . ref = prevPartialToolRequestPart . toolRequest . ref ;
528+ }
529+ // This is a special case for the final partial function call chunk from the API,
530+ // it will have nothing... so we need to make sure to copy the input
531+ // from the previous.
532+ if ( req . input === undefined ) {
533+ req . input = prevPartialToolRequestPart . toolRequest . input ;
534+ }
535+ }
416536}
417537
418- function fromGeminiFunctionResponse ( part : GeminiPart , ref ?: string ) : Part {
538+ function fromGeminiFunctionResponse ( part : GeminiPart ) : Part {
419539 if ( ! part . functionResponse ) {
420540 throw new Error (
421541 'Invalid Gemini Function Call Part: missing function call data'
422542 ) ;
423543 }
424- return maybeAddThoughtSignature ( part , {
544+ const toolResponse : Part = {
425545 toolResponse : {
426546 name : part . functionResponse . name . replace ( / _ _ / g, '/' ) , // restore slashes
427547 output : part . functionResponse . response ,
428- ref,
429548 } ,
430- } ) ;
549+ } ;
550+ if ( part . functionResponse . id ) {
551+ toolResponse . toolResponse . ref = part . functionResponse . id ;
552+ }
553+ return maybeAddThoughtSignature ( part , toolResponse ) ;
431554}
432555
433556function fromExecutableCode ( part : GeminiPart ) : Part {
@@ -462,20 +585,26 @@ function fromGeminiText(part: GeminiPart): Part {
462585 return maybeAddThoughtSignature ( part , { text : part . text } as TextPart ) ;
463586}
464587
465- function fromGeminiPart ( part : GeminiPart , ref : string ) : Part {
588+ function fromGeminiPart (
589+ part : GeminiPart ,
590+ previousChunks ?: CandidateData [ ]
591+ ) : Part {
466592 if ( part . thought ) return fromGeminiThought ( part as any ) ;
467593 if ( typeof part . text === 'string' ) return fromGeminiText ( part ) ;
468594 if ( part . inlineData ) return fromGeminiInlineData ( part ) ;
469595 if ( part . fileData ) return fromGeminiFileData ( part ) ;
470- if ( part . functionCall ) return fromGeminiFunctionCall ( part , ref ) ;
471- if ( part . functionResponse ) return fromGeminiFunctionResponse ( part , ref ) ;
596+ if ( part . functionCall ) return fromGeminiFunctionCall ( part , previousChunks ) ;
597+ if ( part . functionResponse ) return fromGeminiFunctionResponse ( part ) ;
472598 if ( part . executableCode ) return fromExecutableCode ( part ) ;
473599 if ( part . codeExecutionResult ) return fromCodeExecutionResult ( part ) ;
474600
475601 throw new Error ( 'Unsupported GeminiPart type ' + JSON . stringify ( part ) ) ;
476602}
477603
478- export function fromGeminiCandidate ( candidate : GeminiCandidate ) : CandidateData {
604+ export function fromGeminiCandidate (
605+ candidate : GeminiCandidate ,
606+ previousChunks ?: CandidateData [ ]
607+ ) : CandidateData {
479608 const parts = candidate . content ?. parts || [ ] ;
480609 const genkitCandidate : CandidateData = {
481610 index : candidate . index || 0 ,
@@ -484,7 +613,7 @@ export function fromGeminiCandidate(candidate: GeminiCandidate): CandidateData {
484613 content : parts
485614 // the model sometimes returns empty parts, ignore those.
486615 . filter ( ( p ) => Object . keys ( p ) . length > 0 )
487- . map ( ( part , index ) => fromGeminiPart ( part , index . toString ( ) ) ) ,
616+ . map ( ( part ) => fromGeminiPart ( part , previousChunks ) ) ,
488617 } ,
489618 finishReason : fromGeminiFinishReason ( candidate . finishReason ) ,
490619 finishMessage : candidate . finishMessage ,
0 commit comments