@firebase/ai 2.8.0 → 2.9.0-20260224183151
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.
- package/dist/ai-public.d.ts +18 -0
- package/dist/ai.d.ts +36 -0
- package/dist/esm/index.esm.js +237 -50
- package/dist/esm/index.esm.js.map +1 -1
- package/dist/esm/src/methods/chat-session.d.ts +22 -1
- package/dist/esm/src/methods/generate-content.d.ts +4 -2
- package/dist/esm/src/requests/response-helpers.d.ts +1 -1
- package/dist/esm/src/requests/stream-reader.d.ts +3 -1
- package/dist/esm/src/types/content.d.ts +1 -0
- package/dist/esm/src/types/requests.d.ts +14 -0
- package/dist/index.cjs.js +237 -50
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.node.cjs.js +237 -50
- package/dist/index.node.cjs.js.map +1 -1
- package/dist/index.node.mjs +237 -50
- package/dist/index.node.mjs.map +1 -1
- package/dist/src/methods/chat-session.d.ts +22 -1
- package/dist/src/methods/generate-content.d.ts +4 -2
- package/dist/src/requests/response-helpers.d.ts +1 -1
- package/dist/src/requests/stream-reader.d.ts +3 -1
- package/dist/src/types/content.d.ts +1 -0
- package/dist/src/types/requests.d.ts +14 -0
- package/package.json +5 -5
package/dist/index.node.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { FirebaseError, Deferred, getModularInstance } from '@firebase/util';
|
|
|
4
4
|
import { Logger } from '@firebase/logger';
|
|
5
5
|
|
|
6
6
|
var name = "@firebase/ai";
|
|
7
|
-
var version = "2.
|
|
7
|
+
var version = "2.9.0-20260224183151";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @license
|
|
@@ -1468,6 +1468,9 @@ function getText(response, partFilter) {
|
|
|
1468
1468
|
* Returns every {@link FunctionCall} associated with first candidate.
|
|
1469
1469
|
*/
|
|
1470
1470
|
function getFunctionCalls(response) {
|
|
1471
|
+
if (!response) {
|
|
1472
|
+
return undefined;
|
|
1473
|
+
}
|
|
1471
1474
|
const functionCalls = [];
|
|
1472
1475
|
if (response.candidates?.[0].content?.parts) {
|
|
1473
1476
|
for (const part of response.candidates?.[0].content?.parts) {
|
|
@@ -1763,15 +1766,44 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
|
|
|
1763
1766
|
*
|
|
1764
1767
|
* @param response - Response from a fetch call
|
|
1765
1768
|
*/
|
|
1766
|
-
function processStream(response, apiSettings, inferenceSource) {
|
|
1769
|
+
async function processStream(response, apiSettings, inferenceSource) {
|
|
1767
1770
|
const inputStream = response.body.pipeThrough(new TextDecoderStream('utf8', { fatal: true }));
|
|
1768
1771
|
const responseStream = getResponseStream(inputStream);
|
|
1769
1772
|
// We split the stream so the user can iterate over partial results (stream1)
|
|
1770
1773
|
// while we aggregate the full result for history/final response (stream2).
|
|
1771
1774
|
const [stream1, stream2] = responseStream.tee();
|
|
1775
|
+
const { response: internalResponse, firstValue } = await processStreamInternal(stream2, apiSettings, inferenceSource);
|
|
1772
1776
|
return {
|
|
1773
1777
|
stream: generateResponseSequence(stream1, apiSettings, inferenceSource),
|
|
1774
|
-
response:
|
|
1778
|
+
response: internalResponse,
|
|
1779
|
+
firstValue
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Consumes streams teed from the input stream for internal needs.
|
|
1784
|
+
* The streams need to be teed because each stream can only be consumed
|
|
1785
|
+
* by one reader.
|
|
1786
|
+
*
|
|
1787
|
+
* "streamForPeek"
|
|
1788
|
+
* This tee is used to peek at the first value for relevant information
|
|
1789
|
+
* that we need to evaluate before returning the stream handle to the
|
|
1790
|
+
* client. For example, we need to check if the response is a function
|
|
1791
|
+
* call that may need to be handled by automatic function calling before
|
|
1792
|
+
* returning a response to the client.
|
|
1793
|
+
*
|
|
1794
|
+
* "streamForAggregation"
|
|
1795
|
+
* We iterate through this tee independently from the user and aggregate
|
|
1796
|
+
* it into a single response when the stream is complete. We need this
|
|
1797
|
+
* aggregate object to add to chat history when using ChatSession. It's
|
|
1798
|
+
* also provided to the user if they want it.
|
|
1799
|
+
*/
|
|
1800
|
+
async function processStreamInternal(stream, apiSettings, inferenceSource) {
|
|
1801
|
+
const [streamForPeek, streamForAggregation] = stream.tee();
|
|
1802
|
+
const reader = streamForPeek.getReader();
|
|
1803
|
+
const { value } = await reader.read();
|
|
1804
|
+
return {
|
|
1805
|
+
firstValue: value,
|
|
1806
|
+
response: getResponsePromise(streamForAggregation, apiSettings, inferenceSource)
|
|
1775
1807
|
};
|
|
1776
1808
|
}
|
|
1777
1809
|
async function getResponsePromise(stream, apiSettings, inferenceSource) {
|
|
@@ -2342,6 +2374,11 @@ function validateChatHistory(history) {
|
|
|
2342
2374
|
* by the user, preventing duplicate console logs.
|
|
2343
2375
|
*/
|
|
2344
2376
|
const SILENT_ERROR = 'SILENT_ERROR';
|
|
2377
|
+
/**
|
|
2378
|
+
* Prevent infinite loop if the model continues to request sequential
|
|
2379
|
+
* function calls during automatic function calling.
|
|
2380
|
+
*/
|
|
2381
|
+
const DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS = 10;
|
|
2345
2382
|
/**
|
|
2346
2383
|
* ChatSession class that enables sending chat messages and stores
|
|
2347
2384
|
* history of sent and received messages so far.
|
|
@@ -2376,48 +2413,89 @@ class ChatSession {
|
|
|
2376
2413
|
return this._history;
|
|
2377
2414
|
}
|
|
2378
2415
|
/**
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2416
|
+
* Format Content into a request for generateContent or
|
|
2417
|
+
* generateContentStream.
|
|
2418
|
+
* @internal
|
|
2381
2419
|
*/
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
const newContent = formatNewContent(request);
|
|
2385
|
-
const generateContentRequest = {
|
|
2420
|
+
_formatRequest(incomingContent, tempHistory) {
|
|
2421
|
+
return {
|
|
2386
2422
|
safetySettings: this.params?.safetySettings,
|
|
2387
2423
|
generationConfig: this.params?.generationConfig,
|
|
2388
2424
|
tools: this.params?.tools,
|
|
2389
2425
|
toolConfig: this.params?.toolConfig,
|
|
2390
2426
|
systemInstruction: this.params?.systemInstruction,
|
|
2391
|
-
contents: [...this._history,
|
|
2427
|
+
contents: [...this._history, ...tempHistory, incomingContent]
|
|
2392
2428
|
};
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Sends a chat message and receives a non-streaming
|
|
2432
|
+
* {@link GenerateContentResult}
|
|
2433
|
+
*/
|
|
2434
|
+
async sendMessage(request, singleRequestOptions) {
|
|
2393
2435
|
let finalResult = {};
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
if (blockErrorMessage) {
|
|
2415
|
-
logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
|
|
2436
|
+
await this._sendPromise;
|
|
2437
|
+
/**
|
|
2438
|
+
* Temporarily store multiple turns for cases like automatic function
|
|
2439
|
+
* calling, only writing them to official history when the entire
|
|
2440
|
+
* sequence has completed successfully.
|
|
2441
|
+
*/
|
|
2442
|
+
const tempHistory = [];
|
|
2443
|
+
this._sendPromise = this._sendPromise.then(async () => {
|
|
2444
|
+
let functionCalls;
|
|
2445
|
+
let functionCallTurnCount = 0;
|
|
2446
|
+
const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
|
|
2447
|
+
DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
|
|
2448
|
+
// Repeats until model returns a response with no function calls
|
|
2449
|
+
// or until `functionCallMaxTurns` is met or exceeded.
|
|
2450
|
+
do {
|
|
2451
|
+
let formattedContent;
|
|
2452
|
+
if (functionCalls) {
|
|
2453
|
+
functionCallTurnCount++;
|
|
2454
|
+
const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
|
|
2455
|
+
formattedContent = formatNewContent(functionResponseParts);
|
|
2416
2456
|
}
|
|
2457
|
+
else {
|
|
2458
|
+
formattedContent = formatNewContent(request);
|
|
2459
|
+
}
|
|
2460
|
+
const formattedRequest = this._formatRequest(formattedContent, tempHistory);
|
|
2461
|
+
tempHistory.push(formattedContent);
|
|
2462
|
+
const result = await generateContent(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
|
|
2463
|
+
...this.requestOptions,
|
|
2464
|
+
...singleRequestOptions
|
|
2465
|
+
});
|
|
2466
|
+
if (result) {
|
|
2467
|
+
finalResult = result;
|
|
2468
|
+
functionCalls = this._getCallableFunctionCalls(result.response);
|
|
2469
|
+
if (result.response.candidates &&
|
|
2470
|
+
result.response.candidates.length > 0) {
|
|
2471
|
+
// TODO: Make this update atomic. If creating `responseContent` throws,
|
|
2472
|
+
// history will contain the user message but not the response, causing
|
|
2473
|
+
// validation errors on the next request.
|
|
2474
|
+
const responseContent = {
|
|
2475
|
+
parts: result.response.candidates?.[0].content.parts || [],
|
|
2476
|
+
// Response seems to come back without a role set.
|
|
2477
|
+
role: result.response.candidates?.[0].content.role || 'model'
|
|
2478
|
+
};
|
|
2479
|
+
tempHistory.push(responseContent);
|
|
2480
|
+
}
|
|
2481
|
+
else {
|
|
2482
|
+
const blockErrorMessage = formatBlockErrorMessage(result.response);
|
|
2483
|
+
if (blockErrorMessage) {
|
|
2484
|
+
logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
else {
|
|
2489
|
+
functionCalls = undefined;
|
|
2490
|
+
}
|
|
2491
|
+
} while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
|
|
2492
|
+
if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
|
|
2493
|
+
logger.warn(`Automatic function calling exceeded the limit of` +
|
|
2494
|
+
` ${functionCallMaxTurns} function calls. Returning last model response.`);
|
|
2417
2495
|
}
|
|
2418
|
-
finalResult = result;
|
|
2419
2496
|
});
|
|
2420
2497
|
await this._sendPromise;
|
|
2498
|
+
this._history = this._history.concat(tempHistory);
|
|
2421
2499
|
return finalResult;
|
|
2422
2500
|
}
|
|
2423
2501
|
/**
|
|
@@ -2427,23 +2505,62 @@ class ChatSession {
|
|
|
2427
2505
|
*/
|
|
2428
2506
|
async sendMessageStream(request, singleRequestOptions) {
|
|
2429
2507
|
await this._sendPromise;
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2508
|
+
/**
|
|
2509
|
+
* Temporarily store multiple turns for cases like automatic function
|
|
2510
|
+
* calling, only writing them to official history when the entire
|
|
2511
|
+
* sequence has completed successfully.
|
|
2512
|
+
*/
|
|
2513
|
+
const tempHistory = [];
|
|
2514
|
+
const callGenerateContentStream = async () => {
|
|
2515
|
+
let functionCalls;
|
|
2516
|
+
let functionCallTurnCount = 0;
|
|
2517
|
+
const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
|
|
2518
|
+
DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
|
|
2519
|
+
let result;
|
|
2520
|
+
// Repeats until model returns a response with no function calls
|
|
2521
|
+
// or until `functionCallMaxTurns` is met or exceeded.
|
|
2522
|
+
do {
|
|
2523
|
+
let formattedContent;
|
|
2524
|
+
if (functionCalls) {
|
|
2525
|
+
functionCallTurnCount++;
|
|
2526
|
+
const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
|
|
2527
|
+
formattedContent = formatNewContent(functionResponseParts);
|
|
2528
|
+
}
|
|
2529
|
+
else {
|
|
2530
|
+
formattedContent = formatNewContent(request);
|
|
2531
|
+
}
|
|
2532
|
+
tempHistory.push(formattedContent);
|
|
2533
|
+
const formattedRequest = this._formatRequest(formattedContent, tempHistory);
|
|
2534
|
+
result = await generateContentStream(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
|
|
2535
|
+
...this.requestOptions,
|
|
2536
|
+
...singleRequestOptions
|
|
2537
|
+
});
|
|
2538
|
+
functionCalls = this._getCallableFunctionCalls(result.firstValue);
|
|
2539
|
+
if (functionCalls &&
|
|
2540
|
+
result.firstValue &&
|
|
2541
|
+
result.firstValue.candidates &&
|
|
2542
|
+
result.firstValue.candidates.length > 0) {
|
|
2543
|
+
const responseContent = {
|
|
2544
|
+
...result.firstValue.candidates[0].content
|
|
2545
|
+
};
|
|
2546
|
+
if (!responseContent.role) {
|
|
2547
|
+
responseContent.role = 'model';
|
|
2548
|
+
}
|
|
2549
|
+
tempHistory.push(responseContent);
|
|
2550
|
+
}
|
|
2551
|
+
} while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
|
|
2552
|
+
if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
|
|
2553
|
+
logger.warn(`Automatic function calling exceeded the limit of` +
|
|
2554
|
+
` ${functionCallMaxTurns} function calls. Returning last model response.`);
|
|
2555
|
+
}
|
|
2556
|
+
return { stream: result.stream, response: result.response };
|
|
2438
2557
|
};
|
|
2439
|
-
const streamPromise =
|
|
2440
|
-
|
|
2441
|
-
...singleRequestOptions
|
|
2442
|
-
});
|
|
2443
|
-
// We hook into the chain to update history, but we don't block the
|
|
2444
|
-
// return of `streamPromise` to the user.
|
|
2558
|
+
const streamPromise = callGenerateContentStream();
|
|
2559
|
+
// Add onto the chain.
|
|
2445
2560
|
this._sendPromise = this._sendPromise
|
|
2446
|
-
.then(() => streamPromise)
|
|
2561
|
+
.then(async () => streamPromise)
|
|
2562
|
+
// This must be handled to avoid unhandled rejection, but jump
|
|
2563
|
+
// to the final catch block with a label to not log this error.
|
|
2447
2564
|
.catch(_ignored => {
|
|
2448
2565
|
// If the initial fetch fails, the user's `streamPromise` rejects.
|
|
2449
2566
|
// We swallow the error here to prevent double logging in the final catch.
|
|
@@ -2456,7 +2573,7 @@ class ChatSession {
|
|
|
2456
2573
|
// TODO: Move response validation logic upstream to `stream-reader` so
|
|
2457
2574
|
// errors propagate to the user's `result.response` promise.
|
|
2458
2575
|
if (response.candidates && response.candidates.length > 0) {
|
|
2459
|
-
this._history.
|
|
2576
|
+
this._history = this._history.concat(tempHistory);
|
|
2460
2577
|
// TODO: Validate that `response.candidates[0].content` is not null.
|
|
2461
2578
|
const responseContent = { ...response.candidates[0].content };
|
|
2462
2579
|
if (!responseContent.role) {
|
|
@@ -2479,6 +2596,75 @@ class ChatSession {
|
|
|
2479
2596
|
});
|
|
2480
2597
|
return streamPromise;
|
|
2481
2598
|
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Get function calls that the SDK has references to actually call.
|
|
2601
|
+
* This is all-or-nothing. If the model is requesting multiple
|
|
2602
|
+
* function calls, all of them must have references in order for
|
|
2603
|
+
* automatic function calling to work.
|
|
2604
|
+
*
|
|
2605
|
+
* @internal
|
|
2606
|
+
*/
|
|
2607
|
+
_getCallableFunctionCalls(response) {
|
|
2608
|
+
const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
|
|
2609
|
+
if (!functionDeclarationsTool?.functionDeclarations) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const functionCalls = getFunctionCalls(response);
|
|
2613
|
+
if (!functionCalls) {
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
for (const functionCall of functionCalls) {
|
|
2617
|
+
const hasFunctionReference = functionDeclarationsTool.functionDeclarations?.some(declaration => declaration.name === functionCall.name &&
|
|
2618
|
+
typeof declaration.functionReference === 'function');
|
|
2619
|
+
if (!hasFunctionReference) {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return functionCalls;
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Call user-defined functions if requested by the model, and return
|
|
2627
|
+
* the response that should be sent to the model.
|
|
2628
|
+
* @internal
|
|
2629
|
+
*/
|
|
2630
|
+
async _callFunctionsAsNeeded(functionCalls) {
|
|
2631
|
+
const activeCallList = new Map();
|
|
2632
|
+
const promiseList = [];
|
|
2633
|
+
const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
|
|
2634
|
+
if (functionDeclarationsTool &&
|
|
2635
|
+
functionDeclarationsTool.functionDeclarations) {
|
|
2636
|
+
for (const functionCall of functionCalls) {
|
|
2637
|
+
const functionDeclaration = functionDeclarationsTool.functionDeclarations.find(declaration => declaration.name === functionCall.name);
|
|
2638
|
+
if (functionDeclaration?.functionReference) {
|
|
2639
|
+
const results = Promise.resolve(functionDeclaration.functionReference(functionCall.args)).catch(e => {
|
|
2640
|
+
const wrappedError = new AIError(AIErrorCode.ERROR, `Error in user-defined function "${functionDeclaration.name}": ${e.message}`);
|
|
2641
|
+
wrappedError.stack = e.stack;
|
|
2642
|
+
throw wrappedError;
|
|
2643
|
+
});
|
|
2644
|
+
activeCallList.set(functionCall.name, {
|
|
2645
|
+
id: functionCall.id,
|
|
2646
|
+
results
|
|
2647
|
+
});
|
|
2648
|
+
promiseList.push(results);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
// Wait for promises to finish.
|
|
2652
|
+
await Promise.all(promiseList);
|
|
2653
|
+
const functionResponseParts = [];
|
|
2654
|
+
for (const [name, callData] of activeCallList) {
|
|
2655
|
+
functionResponseParts.push({
|
|
2656
|
+
functionResponse: {
|
|
2657
|
+
name,
|
|
2658
|
+
response: await callData.results
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
return functionResponseParts;
|
|
2663
|
+
}
|
|
2664
|
+
else {
|
|
2665
|
+
throw new AIError(AIErrorCode.REQUEST_ERROR, `No function declarations were provided in "tools".`);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2482
2668
|
}
|
|
2483
2669
|
|
|
2484
2670
|
/**
|
|
@@ -2582,7 +2768,7 @@ class GenerativeModel extends AIModel {
|
|
|
2582
2768
|
*/
|
|
2583
2769
|
async generateContentStream(request, singleRequestOptions) {
|
|
2584
2770
|
const formattedParams = formatGenerateContentInput(request);
|
|
2585
|
-
|
|
2771
|
+
const { stream, response } = await generateContentStream(this._apiSettings, this.model, {
|
|
2586
2772
|
generationConfig: this.generationConfig,
|
|
2587
2773
|
safetySettings: this.safetySettings,
|
|
2588
2774
|
tools: this.tools,
|
|
@@ -2595,6 +2781,7 @@ class GenerativeModel extends AIModel {
|
|
|
2595
2781
|
...this.requestOptions,
|
|
2596
2782
|
...singleRequestOptions
|
|
2597
2783
|
});
|
|
2784
|
+
return { stream, response };
|
|
2598
2785
|
}
|
|
2599
2786
|
/**
|
|
2600
2787
|
* Gets a new {@link ChatSession} instance which can be used for
|