@firebase/ai 2.3.0-canary.7a7634f79 → 2.3.0-canary.ccbf7ba36

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.
@@ -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.3.0-canary.7a7634f79";
7
+ var version = "2.3.0-canary.ccbf7ba36";
8
8
 
9
9
  /**
10
10
  * @license
@@ -1592,7 +1592,7 @@ function mapGenerateContentCandidates(candidates) {
1592
1592
  // videoMetadata is not supported.
1593
1593
  // Throw early since developers may send a long video as input and only expect to pay
1594
1594
  // for inference on a small portion of the video.
1595
- if (candidate.content?.parts.some(part => part?.videoMetadata)) {
1595
+ if (candidate.content?.parts?.some(part => part?.videoMetadata)) {
1596
1596
  throw new AIError(AIErrorCode.UNSUPPORTED, 'Part.videoMetadata is not supported in the Gemini Developer API. Please remove this property.');
1597
1597
  }
1598
1598
  const mappedCandidate = {
@@ -1694,6 +1694,14 @@ async function* generateResponseSequence(stream, apiSettings) {
1694
1694
  else {
1695
1695
  enhancedResponse = createEnhancedContentResponse(value);
1696
1696
  }
1697
+ const firstCandidate = enhancedResponse.candidates?.[0];
1698
+ // Don't yield a response with no useful data for the developer.
1699
+ if (!firstCandidate?.content?.parts &&
1700
+ !firstCandidate?.finishReason &&
1701
+ !firstCandidate?.citationMetadata &&
1702
+ !firstCandidate?.urlContextMetadata) {
1703
+ continue;
1704
+ }
1697
1705
  yield enhancedResponse;
1698
1706
  }
1699
1707
  }
@@ -1788,32 +1796,28 @@ function aggregateResponses(responses) {
1788
1796
  * Candidates should always have content and parts, but this handles
1789
1797
  * possible malformed responses.
1790
1798
  */
1791
- if (candidate.content && candidate.content.parts) {
1799
+ if (candidate.content) {
1800
+ // Skip a candidate without parts.
1801
+ if (!candidate.content.parts) {
1802
+ continue;
1803
+ }
1792
1804
  if (!aggregatedResponse.candidates[i].content) {
1793
1805
  aggregatedResponse.candidates[i].content = {
1794
1806
  role: candidate.content.role || 'user',
1795
1807
  parts: []
1796
1808
  };
1797
1809
  }
1798
- const newPart = {};
1799
1810
  for (const part of candidate.content.parts) {
1800
- if (part.text !== undefined) {
1801
- // The backend can send empty text parts. If these are sent back
1802
- // (e.g. in chat history), the backend will respond with an error.
1803
- // To prevent this, ignore empty text parts.
1804
- if (part.text === '') {
1805
- continue;
1806
- }
1807
- newPart.text = part.text;
1808
- }
1809
- if (part.functionCall) {
1810
- newPart.functionCall = part.functionCall;
1811
+ const newPart = { ...part };
1812
+ // The backend can send empty text parts. If these are sent back
1813
+ // (e.g. in chat history), the backend will respond with an error.
1814
+ // To prevent this, ignore empty text parts.
1815
+ if (part.text === '') {
1816
+ continue;
1811
1817
  }
1812
- if (Object.keys(newPart).length === 0) {
1813
- throw new AIError(AIErrorCode.INVALID_CONTENT, 'Part should have at least one property, but there are none. This is likely caused ' +
1814
- 'by a malformed response from the backend.');
1818
+ if (Object.keys(newPart).length > 0) {
1819
+ aggregatedResponse.candidates[i].content.parts.push(newPart);
1815
1820
  }
1816
- aggregatedResponse.candidates[i].content.parts.push(newPart);
1817
1821
  }
1818
1822
  }
1819
1823
  }
@@ -2519,6 +2523,25 @@ class LiveSession {
2519
2523
  this.webSocketHandler.send(JSON.stringify(message));
2520
2524
  });
2521
2525
  }
2526
+ /**
2527
+ * Sends function responses to the server.
2528
+ *
2529
+ * @param functionResponses - The function responses to send.
2530
+ * @throws If this session has been closed.
2531
+ *
2532
+ * @beta
2533
+ */
2534
+ async sendFunctionResponses(functionResponses) {
2535
+ if (this.isClosed) {
2536
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2537
+ }
2538
+ const message = {
2539
+ toolResponse: {
2540
+ functionResponses
2541
+ }
2542
+ };
2543
+ this.webSocketHandler.send(JSON.stringify(message));
2544
+ }
2522
2545
  /**
2523
2546
  * Sends a stream of {@link GenerativeContentBlob}.
2524
2547
  *
@@ -3500,9 +3523,9 @@ class AudioConversationRunner {
3500
3523
  }
3501
3524
  else {
3502
3525
  try {
3503
- const resultPart = await this.options.functionCallingHandler(message.functionCalls);
3526
+ const functionResponse = await this.options.functionCallingHandler(message.functionCalls);
3504
3527
  if (!this.isStopped) {
3505
- void this.liveSession.send([resultPart]);
3528
+ void this.liveSession.sendFunctionResponses([functionResponse]);
3506
3529
  }
3507
3530
  }
3508
3531
  catch (e) {