Skip to content

Commit e91015c

Browse files
committed
Initial commit: Adding support for new API to send files
1. adds new RTVI types 2. adds new `sendFile()` method 3. adds better error handling for sending messages that are too large 4. Currently, only sending images as bytes is tested and working
1 parent 0822c74 commit e91015c

3 files changed

Lines changed: 130 additions & 10 deletions

File tree

client-js/client/client.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
PipecatMetricsData,
2424
RTVIEvent,
2525
RTVIEvents,
26+
RTVIFile,
2627
RTVIMessage,
2728
RTVIMessageType,
2829
SendTextOptions,
@@ -182,9 +183,17 @@ export class PipecatClient extends RTVIEventEmitter {
182183
options?.callbacks?.onError?.(message);
183184
try {
184185
this.emit(RTVIEvent.Error, message);
185-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
186186
} catch (e) {
187-
logger.debug("Could not emit error", message);
187+
if (e instanceof Error && e.message.includes("Unhandled error")) {
188+
if (!options?.callbacks?.onError) {
189+
logger.debug(
190+
"No onError callback registered to handle error",
191+
message
192+
);
193+
}
194+
} else {
195+
logger.debug("Could not emit error", message, e);
196+
}
188197
}
189198
const data = message.data as ErrorData;
190199
if (data?.fatal) {
@@ -489,10 +498,30 @@ export class PipecatClient extends RTVIEventEmitter {
489498

490499
// Create a new message dispatch queue for async message handling
491500
this._messageDispatcher = new MessageDispatcher(
492-
this._transport.sendMessage.bind(this._transport)
501+
this._sendMessage.bind(this)
493502
);
494503
}
495504

505+
/**
506+
* Internal wrapper around the transport's sendMessage method
507+
*/
508+
private _sendMessage(message: RTVIMessage): void {
509+
try {
510+
this._transport.sendMessage(message);
511+
} catch (error) {
512+
if (error instanceof Error) {
513+
this._options.callbacks?.onError?.(
514+
RTVIMessage.error(error.message, false)
515+
);
516+
} else {
517+
this._options.callbacks?.onError?.(
518+
RTVIMessage.error("Unknown error sending message", false)
519+
);
520+
}
521+
throw error;
522+
}
523+
}
524+
496525
/**
497526
* Get the current state of the transport
498527
*/
@@ -588,7 +617,7 @@ export class PipecatClient extends RTVIEventEmitter {
588617
*/
589618
@transportReady
590619
public sendClientMessage(msgType: string, data?: unknown): void {
591-
this._transport.sendMessage(
620+
this._sendMessage(
592621
new RTVIMessage(RTVIMessageType.CLIENT_MESSAGE, {
593622
t: msgType,
594623
d: data,
@@ -637,7 +666,7 @@ export class PipecatClient extends RTVIEventEmitter {
637666
@transportReady
638667
public async appendToContext(context: LLMContextMessage) {
639668
logger.warn("appendToContext() is deprecated. Use sendText() instead.");
640-
await this._transport.sendMessage(
669+
await this._sendMessage(
641670
new RTVIMessage(RTVIMessageType.APPEND_TO_CONTEXT, {
642671
role: context.role,
643672
content: context.content,
@@ -649,22 +678,35 @@ export class PipecatClient extends RTVIEventEmitter {
649678

650679
@transportReady
651680
public async sendText(content: string, options: SendTextOptions = {}) {
652-
await this._transport.sendMessage(
681+
await this._sendMessage(
653682
new RTVIMessage(RTVIMessageType.SEND_TEXT, {
654683
content,
655684
options,
656685
})
657686
);
658687
}
659688

689+
@transportReady
690+
public async sendFile(
691+
file: RTVIFile,
692+
content: string,
693+
options: SendTextOptions = {}
694+
) {
695+
await this._sendMessage(
696+
new RTVIMessage(RTVIMessageType.SEND_FILE, {
697+
file,
698+
content,
699+
options,
700+
})
701+
);
702+
}
703+
660704
/**
661705
* Disconnects the bot, but keeps the session alive
662706
*/
663707
@transportReady
664708
public disconnectBot(): void {
665-
this._transport.sendMessage(
666-
new RTVIMessage(RTVIMessageType.DISCONNECT_BOT, {})
667-
);
709+
this._sendMessage(new RTVIMessage(RTVIMessageType.DISCONNECT_BOT, {}));
668710
}
669711

670712
protected handleMessage(ev: RTVIMessage): void {
@@ -772,7 +814,7 @@ export class PipecatClient extends RTVIEventEmitter {
772814
if (result == undefined) {
773815
return;
774816
}
775-
this._transport.sendMessage(
817+
this._sendMessage(
776818
new RTVIMessage(RTVIMessageType.LLM_FUNCTION_CALL_RESULT, {
777819
function_name: data.function_name,
778820
tool_call_id: data.tool_call_id,

client-js/rtvi/errors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ export class UnsupportedFeatureError extends RTVIError {
7676
}
7777
}
7878

79+
export class MessageTooLargeError extends RTVIError {
80+
constructor(message?: string | undefined) {
81+
super(
82+
message ?? "Message size exceeds the maximum allowed limit for transport."
83+
);
84+
}
85+
}
86+
7987
export type DeviceArray = Array<"cam" | "mic" | "speaker">;
8088
export type DeviceErrorType =
8189
| "in-use"

client-js/rtvi/messages.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum RTVIMessageType {
2525
// Client-to-server messages
2626
CLIENT_MESSAGE = "client-message",
2727
SEND_TEXT = "send-text",
28+
SEND_FILE = "send-file",
2829
// DEPRECATED
2930
APPEND_TO_CONTEXT = "append-to-context",
3031

@@ -187,6 +188,75 @@ export type SendTextOptions = {
187188
audio_response?: boolean;
188189
};
189190

191+
type Serializable =
192+
| string
193+
| number
194+
| boolean
195+
| null
196+
| Serializable[]
197+
| { [key: number | string]: Serializable };
198+
199+
export type RTVIImageFormat =
200+
| "png"
201+
| "jpg"
202+
| "jpeg"
203+
| "webp"
204+
| "gif"
205+
| "heic"
206+
| "hief";
207+
export type RTVIDocFormat =
208+
| "pdf"
209+
| "csv"
210+
| "txt"
211+
| "md"
212+
| "doc"
213+
| "docx"
214+
| "xls"
215+
| "xlsx"
216+
| "json"
217+
| "html"
218+
| "css"
219+
| "javascript";
220+
export type RTVIMediaFormat =
221+
| "mp3"
222+
| "wav"
223+
| "ogg"
224+
| "aac"
225+
| "mp4"
226+
| "webm"
227+
| "ogg"
228+
| "avi";
229+
export type RTVIFileFormat = RTVIImageFormat | RTVIDocFormat | RTVIMediaFormat;
230+
231+
export type FileSourceType = "bytes" | "url" | "id";
232+
233+
export type FileBytes = {
234+
type: Extract<FileSourceType, "bytes">;
235+
bytes: string;
236+
width?: number;
237+
height?: number;
238+
};
239+
export type FileUrl = {
240+
type: Extract<FileSourceType, "url">;
241+
url: string | URL;
242+
};
243+
export type FileId = {
244+
type: Extract<FileSourceType, "id">;
245+
id: string;
246+
};
247+
248+
export type RTVIFile = {
249+
format: RTVIFileFormat;
250+
source: FileBytes | FileUrl | FileId;
251+
customOpts: { [key: number | string]: Serializable }; // for things like 'detail' in openAI or 'citations' in Bedrock
252+
};
253+
254+
export type FileSupport = {
255+
formats: RTVIFileFormat[];
256+
sources: FileSourceType[];
257+
maxSize: number; // bytes
258+
};
259+
190260
/** DEPRECATED */
191261
export type LLMContextMessage = {
192262
role: "user" | "assistant";

0 commit comments

Comments
 (0)