Skip to content

Commit 0bb2fe6

Browse files
authored
feat(ai): Add method to send function responses in a live session (#9272)
1 parent ea85128 commit 0bb2fe6

File tree

9 files changed

+141
-19
lines changed

9 files changed

+141
-19
lines changed

.changeset/lazy-donuts-agree.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'firebase': minor
3+
'@firebase/ai': minor
4+
---
5+
6+
Added a `sendFunctionResponses` method to `LiveSession`, allowing function responses to be sent during realtime sessions.
7+
Fixed an issue where function responses during audio conversations caused the WebSocket connection to close. See [GitHub Issue #9264](https://github.com/firebase/firebase-js-sdk/issues/9264).
8+
- **Breaking Change**: Changed the `functionCallingHandler` property in `StartAudioConversationOptions` so that it now must return a `Promise<FunctionResponse>`.
9+
This breaking change is allowed in a minor release since the Live API is in Public Preview.

common/api-review/ai.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,7 @@ export class LiveSession {
994994
isClosed: boolean;
995995
receive(): AsyncGenerator<LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation>;
996996
send(request: string | Array<string | Part>, turnComplete?: boolean): Promise<void>;
997+
sendFunctionResponses(functionResponses: FunctionResponse[]): Promise<void>;
997998
sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise<void>;
998999
sendMediaStream(mediaChunkStream: ReadableStream<GenerativeContentBlob>): Promise<void>;
9991000
}
@@ -1263,7 +1264,7 @@ export function startAudioConversation(liveSession: LiveSession, options?: Start
12631264

12641265
// @beta
12651266
export interface StartAudioConversationOptions {
1266-
functionCallingHandler?: (functionCalls: LiveServerToolCall['functionCalls']) => Promise<Part>;
1267+
functionCallingHandler?: (functionCalls: FunctionCall[]) => Promise<FunctionResponse>;
12671268
}
12681269

12691270
// @public

docs-devsite/ai.livesession.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export declare class LiveSession
3939
| [close()](./ai.livesession.md#livesessionclose) | | <b><i>(Public Preview)</i></b> Closes this session. All methods on this session will throw an error once this resolves. |
4040
| [receive()](./ai.livesession.md#livesessionreceive) | | <b><i>(Public Preview)</i></b> Yields messages received from the server. This can only be used by one consumer at a time. |
4141
| [send(request, turnComplete)](./ai.livesession.md#livesessionsend) | | <b><i>(Public Preview)</i></b> Sends content to the server. |
42+
| [sendFunctionResponses(functionResponses)](./ai.livesession.md#livesessionsendfunctionresponses) | | <b><i>(Public Preview)</i></b> Sends function responses to the server. |
4243
| [sendMediaChunks(mediaChunks)](./ai.livesession.md#livesessionsendmediachunks) | | <b><i>(Public Preview)</i></b> Sends realtime input to the server. |
4344
| [sendMediaStream(mediaChunkStream)](./ai.livesession.md#livesessionsendmediastream) | | <b><i>(Public Preview)</i></b> Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface)<!-- -->. |
4445

@@ -134,6 +135,33 @@ Promise&lt;void&gt;
134135

135136
If this session has been closed.
136137

138+
## LiveSession.sendFunctionResponses()
139+
140+
> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.
141+
>
142+
143+
Sends function responses to the server.
144+
145+
<b>Signature:</b>
146+
147+
```typescript
148+
sendFunctionResponses(functionResponses: FunctionResponse[]): Promise<void>;
149+
```
150+
151+
#### Parameters
152+
153+
| Parameter | Type | Description |
154+
| --- | --- | --- |
155+
| functionResponses | [FunctionResponse](./ai.functionresponse.md#functionresponse_interface)<!-- -->\[\] | The function responses to send. |
156+
157+
<b>Returns:</b>
158+
159+
Promise&lt;void&gt;
160+
161+
#### Exceptions
162+
163+
If this session has been closed.
164+
137165
## LiveSession.sendMediaChunks()
138166

139167
> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.

docs-devsite/ai.startaudioconversationoptions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface StartAudioConversationOptions
2525

2626
| Property | Type | Description |
2727
| --- | --- | --- |
28-
| [functionCallingHandler](./ai.startaudioconversationoptions.md#startaudioconversationoptionsfunctioncallinghandler) | (functionCalls: [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface)<!-- -->\['functionCalls'\]) =&gt; Promise&lt;[Part](./ai.md#part)<!-- -->&gt; | <b><i>(Public Preview)</i></b> An async handler that is called when the model requests a function to be executed. The handler should perform the function call and return the result as a <code>Part</code>, which will then be sent back to the model. |
28+
| [functionCallingHandler](./ai.startaudioconversationoptions.md#startaudioconversationoptionsfunctioncallinghandler) | (functionCalls: [FunctionCall](./ai.functioncall.md#functioncall_interface)<!-- -->\[\]) =&gt; Promise&lt;[FunctionResponse](./ai.functionresponse.md#functionresponse_interface)<!-- -->&gt; | <b><i>(Public Preview)</i></b> An async handler that is called when the model requests a function to be executed. The handler should perform the function call and return the result as a <code>Part</code>, which will then be sent back to the model. |
2929

3030
## StartAudioConversationOptions.functionCallingHandler
3131

@@ -37,5 +37,5 @@ An async handler that is called when the model requests a function to be execute
3737
<b>Signature:</b>
3838

3939
```typescript
40-
functionCallingHandler?: (functionCalls: LiveServerToolCall['functionCalls']) => Promise<Part>;
40+
functionCallingHandler?: (functionCalls: FunctionCall[]) => Promise<FunctionResponse>;
4141
```

packages/ai/src/methods/live-session-helpers.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import sinonChai from 'sinon-chai';
2121
import chaiAsPromised from 'chai-as-promised';
2222
import { AIError } from '../errors';
2323
import { startAudioConversation } from './live-session-helpers';
24-
import { LiveServerContent, LiveServerToolCall, Part } from '../types';
24+
import {
25+
FunctionResponse,
26+
LiveServerContent,
27+
LiveServerToolCall
28+
} from '../types';
2529
import { logger } from '../logger';
2630
import { isNode } from '@firebase/util';
2731

@@ -62,6 +66,7 @@ class MockLiveSession {
6266
inConversation = false;
6367
send = sinon.stub();
6468
sendMediaChunks = sinon.stub();
69+
sendFunctionResponses = sinon.stub();
6570
messageGenerator = new MockMessageGenerator();
6671
receive = (): MockMessageGenerator => this.messageGenerator;
6772
}
@@ -249,16 +254,21 @@ describe('Audio Conversation Helpers', () => {
249254
});
250255

251256
it('should call function handler and send result on toolCall message.', async () => {
252-
const handlerStub = sinon.stub().resolves({
253-
functionResponse: { name: 'get_weather', response: { temp: '72F' } }
254-
} as Part);
257+
const functionResponse: FunctionResponse = {
258+
id: '1',
259+
name: 'get_weather',
260+
response: { temp: '72F' }
261+
};
262+
const handlerStub = sinon.stub().resolves(functionResponse);
255263
const controller = await startAudioConversation(liveSession as any, {
256264
functionCallingHandler: handlerStub
257265
});
258266

259267
const toolCallMessage: LiveServerToolCall = {
260268
type: 'toolCall',
261-
functionCalls: [{ name: 'get_weather', args: { location: 'LA' } }]
269+
functionCalls: [
270+
{ id: '1', name: 'get_weather', args: { location: 'LA' } }
271+
]
262272
};
263273

264274
liveSession.messageGenerator.simulateMessage(toolCallMessage);
@@ -267,8 +277,8 @@ describe('Audio Conversation Helpers', () => {
267277
expect(handlerStub).to.have.been.calledOnceWith(
268278
toolCallMessage.functionCalls
269279
);
270-
expect(liveSession.send).to.have.been.calledOnceWith([
271-
{ functionResponse: { name: 'get_weather', response: { temp: '72F' } } }
280+
expect(liveSession.sendFunctionResponses).to.have.been.calledOnceWith([
281+
functionResponse
272282
]);
273283
await controller.stop();
274284
});

packages/ai/src/methods/live-session-helpers.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import { AIError } from '../errors';
1919
import { logger } from '../logger';
2020
import {
2121
AIErrorCode,
22+
FunctionCall,
23+
FunctionResponse,
2224
GenerativeContentBlob,
23-
LiveServerContent,
24-
LiveServerToolCall,
25-
Part
25+
LiveServerContent
2626
} from '../types';
2727
import { LiveSession } from './live-session';
2828
import { Deferred } from '@firebase/util';
@@ -115,8 +115,8 @@ export interface StartAudioConversationOptions {
115115
* which will then be sent back to the model.
116116
*/
117117
functionCallingHandler?: (
118-
functionCalls: LiveServerToolCall['functionCalls']
119-
) => Promise<Part>;
118+
functionCalls: FunctionCall[]
119+
) => Promise<FunctionResponse>;
120120
}
121121

122122
/**
@@ -338,11 +338,11 @@ export class AudioConversationRunner {
338338
);
339339
} else {
340340
try {
341-
const resultPart = await this.options.functionCallingHandler(
341+
const functionResponse = await this.options.functionCallingHandler(
342342
message.functionCalls
343343
);
344344
if (!this.isStopped) {
345-
void this.liveSession.send([resultPart]);
345+
void this.liveSession.sendFunctionResponses([functionResponse]);
346346
}
347347
} catch (e) {
348348
throw new AIError(

packages/ai/src/methods/live-session.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { spy, stub } from 'sinon';
2020
import sinonChai from 'sinon-chai';
2121
import chaiAsPromised from 'chai-as-promised';
2222
import {
23+
FunctionResponse,
2324
LiveResponseType,
2425
LiveServerContent,
2526
LiveServerToolCall,
@@ -153,6 +154,35 @@ describe('LiveSession', () => {
153154
});
154155
});
155156

157+
describe('sendFunctionResponses()', () => {
158+
it('should send all function responses', async () => {
159+
const functionResponses: FunctionResponse[] = [
160+
{
161+
id: 'function-call-1',
162+
name: 'function-name',
163+
response: {
164+
result: 'foo'
165+
}
166+
},
167+
{
168+
id: 'function-call-2',
169+
name: 'function-name-2',
170+
response: {
171+
result: 'bar'
172+
}
173+
}
174+
];
175+
await session.sendFunctionResponses(functionResponses);
176+
expect(mockHandler.send).to.have.been.calledOnce;
177+
const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]);
178+
expect(sentData).to.deep.equal({
179+
toolResponse: {
180+
functionResponses
181+
}
182+
});
183+
});
184+
});
185+
156186
describe('receive()', () => {
157187
it('should correctly parse and transform all server message types', async () => {
158188
const receivePromise = (async () => {

packages/ai/src/methods/live-session.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import {
1919
AIErrorCode,
20+
FunctionResponse,
2021
GenerativeContentBlob,
2122
LiveResponseType,
2223
LiveServerContent,
@@ -30,7 +31,8 @@ import { WebSocketHandler } from '../websocket';
3031
import { logger } from '../logger';
3132
import {
3233
_LiveClientContent,
33-
_LiveClientRealtimeInput
34+
_LiveClientRealtimeInput,
35+
_LiveClientToolResponse
3436
} from '../types/live-responses';
3537

3638
/**
@@ -119,6 +121,32 @@ export class LiveSession {
119121
});
120122
}
121123

124+
/**
125+
* Sends function responses to the server.
126+
*
127+
* @param functionResponses - The function responses to send.
128+
* @throws If this session has been closed.
129+
*
130+
* @beta
131+
*/
132+
async sendFunctionResponses(
133+
functionResponses: FunctionResponse[]
134+
): Promise<void> {
135+
if (this.isClosed) {
136+
throw new AIError(
137+
AIErrorCode.REQUEST_ERROR,
138+
'This LiveSession has been closed and cannot be used.'
139+
);
140+
}
141+
142+
const message: _LiveClientToolResponse = {
143+
toolResponse: {
144+
functionResponses
145+
}
146+
};
147+
this.webSocketHandler.send(JSON.stringify(message));
148+
}
149+
122150
/**
123151
* Sends a stream of {@link GenerativeContentBlob}.
124152
*

packages/ai/src/types/live-responses.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Content, GenerativeContentBlob, Part } from './content';
18+
import {
19+
Content,
20+
FunctionResponse,
21+
GenerativeContentBlob,
22+
Part
23+
} from './content';
1924
import { LiveGenerationConfig, Tool, ToolConfig } from './requests';
2025

2126
/**
@@ -42,6 +47,17 @@ export interface _LiveClientRealtimeInput {
4247
mediaChunks: GenerativeContentBlob[];
4348
};
4449
}
50+
51+
/**
52+
* Function responses that are sent to the model in real time.
53+
*/
54+
// eslint-disable-next-line @typescript-eslint/naming-convention
55+
export interface _LiveClientToolResponse {
56+
toolResponse: {
57+
functionResponses: FunctionResponse[];
58+
};
59+
}
60+
4561
/**
4662
* The first message in a Live session, used to configure generation options.
4763
*

0 commit comments

Comments
 (0)