Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@webex/internal-plugin-llm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as WebexCore from '@webex/webex-core';
import LLMChannel, {config} from './llm';
import {DataChannelTokenType} from './llm.types';

export type {RegisterAndConnectTiming} from './llm.types';

WebexCore.registerInternalPlugin('llm', LLMChannel, {
config,
});
Expand Down
30 changes: 26 additions & 4 deletions packages/@webex/internal-plugin-llm/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM,
LLM_DEFAULT_SESSION,
} from './constants';
import {ILLMChannel, DataChannelTokenType} from './llm.types';
import {ILLMChannel, DataChannelTokenType, RegisterAndConnectTiming} from './llm.types';

export const config = {
llm: {
Expand Down Expand Up @@ -124,10 +124,14 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
datachannelUrl: string,
datachannelToken?: string,
sessionId: string = LLM_DEFAULT_SESSION
): Promise<void> =>
this.register(datachannelUrl, datachannelToken, sessionId).then(async () => {
): Promise<RegisterAndConnectTiming | undefined> => {
const registerStart = performance.now();

return this.register(datachannelUrl, datachannelToken, sessionId).then(async () => {
if (!locusUrl || !datachannelUrl) return undefined;

const clientLLMDatachannelResponseTime = Math.round(performance.now() - registerStart);

// Get or create connection data
const sessionData = this.connections.get(sessionId) || {};
sessionData.locusUrl = locusUrl;
Expand All @@ -141,8 +145,15 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
? LLMChannel.buildUrlWithAwareSubchannels(sessionData.webSocketUrl, AWARE_DATA_CHANNEL)
: sessionData.webSocketUrl;

return this.connect(connectUrl, sessionId);
const connectStart = performance.now();

await this.connect(connectUrl, sessionId);

const clientLLMWebSocketConnectTime = Math.round(performance.now() - connectStart);

return {clientLLMDatachannelResponseTime, clientLLMWebSocketConnectTime};
});
};

/**
* Tells if LLM socket is connected
Expand Down Expand Up @@ -188,6 +199,17 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
return sessionData?.datachannelUrl;
};

/**
* Get WebSocket URL for the connection
* @param {string} sessionId - Connection identifier
* @returns {string | undefined} WebSocket URL
*/
public getWebSocketUrl = (sessionId = LLM_DEFAULT_SESSION): string | undefined => {
const sessionData = this.connections.get(sessionId);

return sessionData?.webSocketUrl;
};

/**
* Set the owner meeting ID for a given LLM session. Used by the meetings
* plugin to tag which Meeting instance currently owns the (default) LLM
Expand Down
17 changes: 15 additions & 2 deletions packages/@webex/internal-plugin-llm/src/llm.types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
/**
* Latencies (in milliseconds) captured during the LLM register + websocket
* connect flow. Returned by `registerAndConnect` so callers can include them
* in diagnostic events.
*/
type RegisterAndConnectTiming = {
/** Time taken by the LLM datachannel HTTP request to return the websocket URL. */
clientLLMDatachannelResponseTime?: number;
/** Time taken to establish the LLM websocket connection (connect request -> response). */
clientLLMWebSocketConnectTime?: number;
};

interface ILLMChannel {
registerAndConnect: (
locusUrl: string,
datachannelUrl: string,
datachannelToken?: string,
sessionId?: string
) => Promise<void>;
) => Promise<RegisterAndConnectTiming | undefined>;
isConnected: (sessionId?: string) => boolean;
getBinding: (sessionId?: string) => string;
getLocusUrl: (sessionId?: string) => string;
getDatachannelUrl: (sessionId?: string) => string;
getWebSocketUrl: (sessionId?: string) => string | undefined;
disconnectLLM: (options: {code: number; reason: string}, sessionId?: string) => Promise<void>;
disconnectAllLLM: (options?: {code: number; reason: string}) => Promise<void>;
setOwnerMeetingId: (ownerMeetingId: string | undefined, sessionId?: string) => void;
Expand All @@ -32,4 +45,4 @@ export enum DataChannelTokenType {
}

// eslint-disable-next-line import/prefer-default-export
export type {ILLMChannel};
export type {ILLMChannel, RegisterAndConnectTiming};
53 changes: 53 additions & 0 deletions packages/@webex/internal-plugin-llm/test/unit/spec/llm.js
Original file line number Diff line number Diff line change
Expand Up @@ -502,5 +502,58 @@ describe('plugin-llm', () => {
});
});


describe('#registerAndConnect timing', () => {
it('returns timing data on successful connection', async () => {
llmService.register = sinon.stub().callsFake(async () => {
llmService.binding = 'binding';
const sessionData = llmService.connections.get('llm-default-session') || {};
sessionData.webSocketUrl = 'wss://example.com/socket';
sessionData.binding = 'binding';
llmService.connections.set('llm-default-session', sessionData);
});

const result = await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined);

assert.isDefined(result);
assert.isNumber(result.clientLLMDatachannelResponseTime);
assert.isNumber(result.clientLLMWebSocketConnectTime);
assert.isAtLeast(result.clientLLMDatachannelResponseTime, 0);
assert.isAtLeast(result.clientLLMWebSocketConnectTime, 0);
});

it('returns undefined when locusUrl is empty', async () => {
llmService.register = sinon.stub().resolves();

const result = await llmService.registerAndConnect('', datachannelUrl, undefined);

assert.isUndefined(result);
});
});

describe('#getWebSocketUrl', () => {
it('returns the websocket URL for the default session', () => {
llmService.connections.set('llm-default-session', {
webSocketUrl: 'wss://test.example.com/ws',
});

assert.equal(llmService.getWebSocketUrl(), 'wss://test.example.com/ws');
});

it('returns undefined when no connection exists', () => {
llmService.connections.clear();

assert.isUndefined(llmService.getWebSocketUrl());
});

it('returns the websocket URL for a specific session', () => {
llmService.connections.set('custom-session', {
webSocketUrl: 'wss://custom.example.com/ws',
});

assert.equal(llmService.getWebSocketUrl('custom-session'), 'wss://custom.example.com/ws');
});
});

});
});
25 changes: 18 additions & 7 deletions packages/@webex/plugin-meetings/src/breakouts/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,30 @@ const breakoutEvent: {
if (!eventInfo?.breakoutMoveId || !eventInfo?.meeting) {
return;
}
if (event === 'client.breakout-session.join.response' && !eventInfo?.llmLatency) {
return;
}
if (!eventInfo.meeting.meetingInfo?.enableConvergedArchitecture) {
return;
}

const payload: any = {
...(eventInfo?.llmLatency && {llmLatency: eventInfo.llmLatency}),
identifiers: {
breakoutMoveId: eventInfo.breakoutMoveId,
breakoutSessionId: eventInfo?.currentSession?.sessionId,
breakoutGroupId: eventInfo?.currentSession?.groupId,
...(eventInfo?.llmWebsocketUrl && {llmWebsocketUrl: eventInfo.llmWebsocketUrl}),
},
};

submitClientEvent({
name: event,
payload: {
identifiers: {
breakoutMoveId: eventInfo.breakoutMoveId,
breakoutSessionId: eventInfo?.currentSession?.sessionId,
breakoutGroupId: eventInfo?.currentSession?.groupId,
},
payload,
options: {
meetingId: eventInfo.meeting.id,
...(eventInfo?.error && {rawError: eventInfo.error}),
},
options: {meetingId: eventInfo.meeting.id},
});
},
};
Expand Down
2 changes: 2 additions & 0 deletions packages/@webex/plugin-meetings/src/breakouts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const Breakouts = WebexPlugin.extend({
name: 'string', // only present when in a breakout session
sessionId: 'string', // appears from the moment you enable breakouts
sessionType: 'string', // appears from the moment you enable breakouts
breakoutMoveId: 'string', // id used to correlate breakout move/join call analyzer events
startTime: 'string', // appears once breakouts start
status: 'string', // only present when in a breakout session
url: 'string', // appears from the moment you enable breakouts
Expand Down Expand Up @@ -328,6 +329,7 @@ const Breakouts = WebexPlugin.extend({
*/
updateBreakout(params) {
this.set(params);
this.set('breakoutMoveId', params.breakoutMoveId);
// These values are set manually so they are unset when they are not included in params
this.set('groups', params.groups);
this.set('startTime', params.startTime);
Expand Down
Loading
Loading