@firebase/ai 2.8.0 → 2.9.0-canary.78384d32c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { Content, GenerateContentResult, GenerateContentStreamResult, Part, RequestOptions, SingleRequestOptions, StartChatParams } from '../types';
17
+ import { Content, FunctionCall, FunctionResponsePart, GenerateContentRequest, GenerateContentResponse, GenerateContentResult, GenerateContentStreamResult, Part, RequestOptions, SingleRequestOptions, StartChatParams } from '../types';
18
18
  import { ApiSettings } from '../types/internal';
19
19
  import { ChromeAdapter } from '../types/chrome-adapter';
20
20
  /**
@@ -42,6 +42,12 @@ export declare class ChatSession {
42
42
  * to history.
43
43
  */
44
44
  getHistory(): Promise<Content[]>;
45
+ /**
46
+ * Format Content into a request for generateContent or
47
+ * generateContentStream.
48
+ * @internal
49
+ */
50
+ _formatRequest(incomingContent: Content, tempHistory: Content[]): GenerateContentRequest;
45
51
  /**
46
52
  * Sends a chat message and receives a non-streaming
47
53
  * {@link GenerateContentResult}
@@ -53,4 +59,19 @@ export declare class ChatSession {
53
59
  * and a response promise.
54
60
  */
55
61
  sendMessageStream(request: string | Array<string | Part>, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentStreamResult>;
62
+ /**
63
+ * Get function calls that the SDK has references to actually call.
64
+ * This is all-or-nothing. If the model is requesting multiple
65
+ * function calls, all of them must have references in order for
66
+ * automatic function calling to work.
67
+ *
68
+ * @internal
69
+ */
70
+ _getCallableFunctionCalls(response?: GenerateContentResponse): FunctionCall[] | undefined;
71
+ /**
72
+ * Call user-defined functions if requested by the model, and return
73
+ * the response that should be sent to the model.
74
+ * @internal
75
+ */
76
+ _callFunctionsAsNeeded(functionCalls: FunctionCall[]): Promise<FunctionResponsePart[]>;
56
77
  }
@@ -14,10 +14,12 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { GenerateContentRequest, GenerateContentResult, GenerateContentStreamResult, SingleRequestOptions } from '../types';
17
+ import { GenerateContentRequest, GenerateContentResponse, GenerateContentResult, GenerateContentStreamResult, SingleRequestOptions } from '../types';
18
18
  import { ApiSettings } from '../types/internal';
19
19
  import { ChromeAdapter } from '../types/chrome-adapter';
20
- export declare function generateContentStream(apiSettings: ApiSettings, model: string, params: GenerateContentRequest, chromeAdapter?: ChromeAdapter, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentStreamResult>;
20
+ export declare function generateContentStream(apiSettings: ApiSettings, model: string, params: GenerateContentRequest, chromeAdapter?: ChromeAdapter, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentStreamResult & {
21
+ firstValue?: GenerateContentResponse;
22
+ }>;
21
23
  export declare function templateGenerateContent(apiSettings: ApiSettings, templateId: string, templateParams: object, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentResult>;
22
24
  export declare function templateGenerateContentStream(apiSettings: ApiSettings, templateId: string, templateParams: object, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentStreamResult>;
23
25
  export declare function generateContent(apiSettings: ApiSettings, model: string, params: GenerateContentRequest, chromeAdapter?: ChromeAdapter, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentResult>;
@@ -36,7 +36,7 @@ export declare function getText(response: GenerateContentResponse, partFilter: (
36
36
  /**
37
37
  * Returns every {@link FunctionCall} associated with first candidate.
38
38
  */
39
- export declare function getFunctionCalls(response: GenerateContentResponse): FunctionCall[] | undefined;
39
+ export declare function getFunctionCalls(response?: GenerateContentResponse): FunctionCall[] | undefined;
40
40
  /**
41
41
  * Returns every {@link InlineDataPart} in the first candidate if present.
42
42
  *
@@ -25,7 +25,9 @@ import { InferenceSource } from '../public-types';
25
25
  *
26
26
  * @param response - Response from a fetch call
27
27
  */
28
- export declare function processStream(response: Response, apiSettings: ApiSettings, inferenceSource?: InferenceSource): GenerateContentStreamResult;
28
+ export declare function processStream(response: Response, apiSettings: ApiSettings, inferenceSource?: InferenceSource): Promise<GenerateContentStreamResult & {
29
+ firstValue?: GenerateContentResponse;
30
+ }>;
29
31
  /**
30
32
  * Reads a raw string stream, buffers incomplete chunks, and yields parsed JSON objects.
31
33
  */
@@ -243,6 +243,7 @@ export interface FunctionResponse {
243
243
  id?: string;
244
244
  name: string;
245
245
  response: object;
246
+ parts?: Part[];
246
247
  }
247
248
  /**
248
249
  * Interface for sending an image.
@@ -231,6 +231,15 @@ export interface RequestOptions {
231
231
  * (used regardless of your chosen Gemini API provider).
232
232
  */
233
233
  baseUrl?: string;
234
+ /**
235
+ * Limits amount of sequential function calls the SDK can make during automatic
236
+ * function calling, in order to prevent infinite loops. If not specified,
237
+ * this value defaults to 10.
238
+ *
239
+ * When it reaches this limit, it will return the last response received
240
+ * from the model, whether it is a text response or further function calls.
241
+ */
242
+ maxSequentalFunctionCalls?: number;
234
243
  }
235
244
  /**
236
245
  * Options that can be provided per-request.
@@ -304,6 +313,11 @@ export interface FunctionDeclaration {
304
313
  * case-sensitive. For a function with no parameters, this can be left unset.
305
314
  */
306
315
  parameters?: ObjectSchema | ObjectSchemaRequest;
316
+ /**
317
+ * Reference to an actual function to call. Specifying this will cause the
318
+ * function to be called automatically when requested by the model.
319
+ */
320
+ functionReference?: Function;
307
321
  }
308
322
  /**
309
323
  * A tool that allows a Gemini model to connect to Google Search to access and incorporate
package/dist/index.cjs.js CHANGED
@@ -8,7 +8,7 @@ var util = require('@firebase/util');
8
8
  var logger$1 = require('@firebase/logger');
9
9
 
10
10
  var name = "@firebase/ai";
11
- var version = "2.8.0";
11
+ var version = "2.9.0-canary.78384d32c";
12
12
 
13
13
  /**
14
14
  * @license
@@ -1780,6 +1780,9 @@ function getText(response, partFilter) {
1780
1780
  * Returns every {@link FunctionCall} associated with first candidate.
1781
1781
  */
1782
1782
  function getFunctionCalls(response) {
1783
+ if (!response) {
1784
+ return undefined;
1785
+ }
1783
1786
  const functionCalls = [];
1784
1787
  if (response.candidates?.[0].content?.parts) {
1785
1788
  for (const part of response.candidates?.[0].content?.parts) {
@@ -2075,15 +2078,44 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
2075
2078
  *
2076
2079
  * @param response - Response from a fetch call
2077
2080
  */
2078
- function processStream(response, apiSettings, inferenceSource) {
2081
+ async function processStream(response, apiSettings, inferenceSource) {
2079
2082
  const inputStream = response.body.pipeThrough(new TextDecoderStream('utf8', { fatal: true }));
2080
2083
  const responseStream = getResponseStream(inputStream);
2081
2084
  // We split the stream so the user can iterate over partial results (stream1)
2082
2085
  // while we aggregate the full result for history/final response (stream2).
2083
2086
  const [stream1, stream2] = responseStream.tee();
2087
+ const { response: internalResponse, firstValue } = await processStreamInternal(stream2, apiSettings, inferenceSource);
2084
2088
  return {
2085
2089
  stream: generateResponseSequence(stream1, apiSettings, inferenceSource),
2086
- response: getResponsePromise(stream2, apiSettings, inferenceSource)
2090
+ response: internalResponse,
2091
+ firstValue
2092
+ };
2093
+ }
2094
+ /**
2095
+ * Consumes streams teed from the input stream for internal needs.
2096
+ * The streams need to be teed because each stream can only be consumed
2097
+ * by one reader.
2098
+ *
2099
+ * "streamForPeek"
2100
+ * This tee is used to peek at the first value for relevant information
2101
+ * that we need to evaluate before returning the stream handle to the
2102
+ * client. For example, we need to check if the response is a function
2103
+ * call that may need to be handled by automatic function calling before
2104
+ * returning a response to the client.
2105
+ *
2106
+ * "streamForAggregation"
2107
+ * We iterate through this tee independently from the user and aggregate
2108
+ * it into a single response when the stream is complete. We need this
2109
+ * aggregate object to add to chat history when using ChatSession. It's
2110
+ * also provided to the user if they want it.
2111
+ */
2112
+ async function processStreamInternal(stream, apiSettings, inferenceSource) {
2113
+ const [streamForPeek, streamForAggregation] = stream.tee();
2114
+ const reader = streamForPeek.getReader();
2115
+ const { value } = await reader.read();
2116
+ return {
2117
+ firstValue: value,
2118
+ response: getResponsePromise(streamForAggregation, apiSettings, inferenceSource)
2087
2119
  };
2088
2120
  }
2089
2121
  async function getResponsePromise(stream, apiSettings, inferenceSource) {
@@ -2654,6 +2686,11 @@ function validateChatHistory(history) {
2654
2686
  * by the user, preventing duplicate console logs.
2655
2687
  */
2656
2688
  const SILENT_ERROR = 'SILENT_ERROR';
2689
+ /**
2690
+ * Prevent infinite loop if the model continues to request sequential
2691
+ * function calls during automatic function calling.
2692
+ */
2693
+ const DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS = 10;
2657
2694
  /**
2658
2695
  * ChatSession class that enables sending chat messages and stores
2659
2696
  * history of sent and received messages so far.
@@ -2688,48 +2725,89 @@ class ChatSession {
2688
2725
  return this._history;
2689
2726
  }
2690
2727
  /**
2691
- * Sends a chat message and receives a non-streaming
2692
- * {@link GenerateContentResult}
2728
+ * Format Content into a request for generateContent or
2729
+ * generateContentStream.
2730
+ * @internal
2693
2731
  */
2694
- async sendMessage(request, singleRequestOptions) {
2695
- await this._sendPromise;
2696
- const newContent = formatNewContent(request);
2697
- const generateContentRequest = {
2732
+ _formatRequest(incomingContent, tempHistory) {
2733
+ return {
2698
2734
  safetySettings: this.params?.safetySettings,
2699
2735
  generationConfig: this.params?.generationConfig,
2700
2736
  tools: this.params?.tools,
2701
2737
  toolConfig: this.params?.toolConfig,
2702
2738
  systemInstruction: this.params?.systemInstruction,
2703
- contents: [...this._history, newContent]
2739
+ contents: [...this._history, ...tempHistory, incomingContent]
2704
2740
  };
2741
+ }
2742
+ /**
2743
+ * Sends a chat message and receives a non-streaming
2744
+ * {@link GenerateContentResult}
2745
+ */
2746
+ async sendMessage(request, singleRequestOptions) {
2705
2747
  let finalResult = {};
2706
- this._sendPromise = this._sendPromise
2707
- .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, {
2708
- ...this.requestOptions,
2709
- ...singleRequestOptions
2710
- }))
2711
- .then(result => {
2712
- // TODO: Make this update atomic. If creating `responseContent` throws,
2713
- // history will contain the user message but not the response, causing
2714
- // validation errors on the next request.
2715
- if (result.response.candidates &&
2716
- result.response.candidates.length > 0) {
2717
- this._history.push(newContent);
2718
- const responseContent = {
2719
- parts: result.response.candidates?.[0].content.parts || [],
2720
- role: result.response.candidates?.[0].content.role || 'model'
2721
- };
2722
- this._history.push(responseContent);
2723
- }
2724
- else {
2725
- const blockErrorMessage = formatBlockErrorMessage(result.response);
2726
- if (blockErrorMessage) {
2727
- logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2748
+ await this._sendPromise;
2749
+ /**
2750
+ * Temporarily store multiple turns for cases like automatic function
2751
+ * calling, only writing them to official history when the entire
2752
+ * sequence has completed successfully.
2753
+ */
2754
+ const tempHistory = [];
2755
+ this._sendPromise = this._sendPromise.then(async () => {
2756
+ let functionCalls;
2757
+ let functionCallTurnCount = 0;
2758
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2759
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2760
+ // Repeats until model returns a response with no function calls
2761
+ // or until `functionCallMaxTurns` is met or exceeded.
2762
+ do {
2763
+ let formattedContent;
2764
+ if (functionCalls) {
2765
+ functionCallTurnCount++;
2766
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2767
+ formattedContent = formatNewContent(functionResponseParts);
2768
+ }
2769
+ else {
2770
+ formattedContent = formatNewContent(request);
2728
2771
  }
2772
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2773
+ tempHistory.push(formattedContent);
2774
+ const result = await generateContent(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2775
+ ...this.requestOptions,
2776
+ ...singleRequestOptions
2777
+ });
2778
+ if (result) {
2779
+ finalResult = result;
2780
+ functionCalls = this._getCallableFunctionCalls(result.response);
2781
+ if (result.response.candidates &&
2782
+ result.response.candidates.length > 0) {
2783
+ // TODO: Make this update atomic. If creating `responseContent` throws,
2784
+ // history will contain the user message but not the response, causing
2785
+ // validation errors on the next request.
2786
+ const responseContent = {
2787
+ parts: result.response.candidates?.[0].content.parts || [],
2788
+ // Response seems to come back without a role set.
2789
+ role: result.response.candidates?.[0].content.role || 'model'
2790
+ };
2791
+ tempHistory.push(responseContent);
2792
+ }
2793
+ else {
2794
+ const blockErrorMessage = formatBlockErrorMessage(result.response);
2795
+ if (blockErrorMessage) {
2796
+ logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2797
+ }
2798
+ }
2799
+ }
2800
+ else {
2801
+ functionCalls = undefined;
2802
+ }
2803
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2804
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2805
+ logger.warn(`Automatic function calling exceeded the limit of` +
2806
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2729
2807
  }
2730
- finalResult = result;
2731
2808
  });
2732
2809
  await this._sendPromise;
2810
+ this._history = this._history.concat(tempHistory);
2733
2811
  return finalResult;
2734
2812
  }
2735
2813
  /**
@@ -2739,23 +2817,62 @@ class ChatSession {
2739
2817
  */
2740
2818
  async sendMessageStream(request, singleRequestOptions) {
2741
2819
  await this._sendPromise;
2742
- const newContent = formatNewContent(request);
2743
- const generateContentRequest = {
2744
- safetySettings: this.params?.safetySettings,
2745
- generationConfig: this.params?.generationConfig,
2746
- tools: this.params?.tools,
2747
- toolConfig: this.params?.toolConfig,
2748
- systemInstruction: this.params?.systemInstruction,
2749
- contents: [...this._history, newContent]
2820
+ /**
2821
+ * Temporarily store multiple turns for cases like automatic function
2822
+ * calling, only writing them to official history when the entire
2823
+ * sequence has completed successfully.
2824
+ */
2825
+ const tempHistory = [];
2826
+ const callGenerateContentStream = async () => {
2827
+ let functionCalls;
2828
+ let functionCallTurnCount = 0;
2829
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2830
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2831
+ let result;
2832
+ // Repeats until model returns a response with no function calls
2833
+ // or until `functionCallMaxTurns` is met or exceeded.
2834
+ do {
2835
+ let formattedContent;
2836
+ if (functionCalls) {
2837
+ functionCallTurnCount++;
2838
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2839
+ formattedContent = formatNewContent(functionResponseParts);
2840
+ }
2841
+ else {
2842
+ formattedContent = formatNewContent(request);
2843
+ }
2844
+ tempHistory.push(formattedContent);
2845
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2846
+ result = await generateContentStream(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2847
+ ...this.requestOptions,
2848
+ ...singleRequestOptions
2849
+ });
2850
+ functionCalls = this._getCallableFunctionCalls(result.firstValue);
2851
+ if (functionCalls &&
2852
+ result.firstValue &&
2853
+ result.firstValue.candidates &&
2854
+ result.firstValue.candidates.length > 0) {
2855
+ const responseContent = {
2856
+ ...result.firstValue.candidates[0].content
2857
+ };
2858
+ if (!responseContent.role) {
2859
+ responseContent.role = 'model';
2860
+ }
2861
+ tempHistory.push(responseContent);
2862
+ }
2863
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2864
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2865
+ logger.warn(`Automatic function calling exceeded the limit of` +
2866
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2867
+ }
2868
+ return { stream: result.stream, response: result.response };
2750
2869
  };
2751
- const streamPromise = generateContentStream(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, {
2752
- ...this.requestOptions,
2753
- ...singleRequestOptions
2754
- });
2755
- // We hook into the chain to update history, but we don't block the
2756
- // return of `streamPromise` to the user.
2870
+ const streamPromise = callGenerateContentStream();
2871
+ // Add onto the chain.
2757
2872
  this._sendPromise = this._sendPromise
2758
- .then(() => streamPromise)
2873
+ .then(async () => streamPromise)
2874
+ // This must be handled to avoid unhandled rejection, but jump
2875
+ // to the final catch block with a label to not log this error.
2759
2876
  .catch(_ignored => {
2760
2877
  // If the initial fetch fails, the user's `streamPromise` rejects.
2761
2878
  // We swallow the error here to prevent double logging in the final catch.
@@ -2768,7 +2885,7 @@ class ChatSession {
2768
2885
  // TODO: Move response validation logic upstream to `stream-reader` so
2769
2886
  // errors propagate to the user's `result.response` promise.
2770
2887
  if (response.candidates && response.candidates.length > 0) {
2771
- this._history.push(newContent);
2888
+ this._history = this._history.concat(tempHistory);
2772
2889
  // TODO: Validate that `response.candidates[0].content` is not null.
2773
2890
  const responseContent = { ...response.candidates[0].content };
2774
2891
  if (!responseContent.role) {
@@ -2791,6 +2908,75 @@ class ChatSession {
2791
2908
  });
2792
2909
  return streamPromise;
2793
2910
  }
2911
+ /**
2912
+ * Get function calls that the SDK has references to actually call.
2913
+ * This is all-or-nothing. If the model is requesting multiple
2914
+ * function calls, all of them must have references in order for
2915
+ * automatic function calling to work.
2916
+ *
2917
+ * @internal
2918
+ */
2919
+ _getCallableFunctionCalls(response) {
2920
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2921
+ if (!functionDeclarationsTool?.functionDeclarations) {
2922
+ return;
2923
+ }
2924
+ const functionCalls = getFunctionCalls(response);
2925
+ if (!functionCalls) {
2926
+ return;
2927
+ }
2928
+ for (const functionCall of functionCalls) {
2929
+ const hasFunctionReference = functionDeclarationsTool.functionDeclarations?.some(declaration => declaration.name === functionCall.name &&
2930
+ typeof declaration.functionReference === 'function');
2931
+ if (!hasFunctionReference) {
2932
+ return;
2933
+ }
2934
+ }
2935
+ return functionCalls;
2936
+ }
2937
+ /**
2938
+ * Call user-defined functions if requested by the model, and return
2939
+ * the response that should be sent to the model.
2940
+ * @internal
2941
+ */
2942
+ async _callFunctionsAsNeeded(functionCalls) {
2943
+ const activeCallList = new Map();
2944
+ const promiseList = [];
2945
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2946
+ if (functionDeclarationsTool &&
2947
+ functionDeclarationsTool.functionDeclarations) {
2948
+ for (const functionCall of functionCalls) {
2949
+ const functionDeclaration = functionDeclarationsTool.functionDeclarations.find(declaration => declaration.name === functionCall.name);
2950
+ if (functionDeclaration?.functionReference) {
2951
+ const results = Promise.resolve(functionDeclaration.functionReference(functionCall.args)).catch(e => {
2952
+ const wrappedError = new AIError(AIErrorCode.ERROR, `Error in user-defined function "${functionDeclaration.name}": ${e.message}`);
2953
+ wrappedError.stack = e.stack;
2954
+ throw wrappedError;
2955
+ });
2956
+ activeCallList.set(functionCall.name, {
2957
+ id: functionCall.id,
2958
+ results
2959
+ });
2960
+ promiseList.push(results);
2961
+ }
2962
+ }
2963
+ // Wait for promises to finish.
2964
+ await Promise.all(promiseList);
2965
+ const functionResponseParts = [];
2966
+ for (const [name, callData] of activeCallList) {
2967
+ functionResponseParts.push({
2968
+ functionResponse: {
2969
+ name,
2970
+ response: await callData.results
2971
+ }
2972
+ });
2973
+ }
2974
+ return functionResponseParts;
2975
+ }
2976
+ else {
2977
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `No function declarations were provided in "tools".`);
2978
+ }
2979
+ }
2794
2980
  }
2795
2981
 
2796
2982
  /**
@@ -2894,7 +3080,7 @@ class GenerativeModel extends AIModel {
2894
3080
  */
2895
3081
  async generateContentStream(request, singleRequestOptions) {
2896
3082
  const formattedParams = formatGenerateContentInput(request);
2897
- return generateContentStream(this._apiSettings, this.model, {
3083
+ const { stream, response } = await generateContentStream(this._apiSettings, this.model, {
2898
3084
  generationConfig: this.generationConfig,
2899
3085
  safetySettings: this.safetySettings,
2900
3086
  tools: this.tools,
@@ -2907,6 +3093,7 @@ class GenerativeModel extends AIModel {
2907
3093
  ...this.requestOptions,
2908
3094
  ...singleRequestOptions
2909
3095
  });
3096
+ return { stream, response };
2910
3097
  }
2911
3098
  /**
2912
3099
  * Gets a new {@link ChatSession} instance which can be used for