@memberjunction/server 5.40.2 → 5.42.0
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/agentSessions/HostInstance.d.ts +19 -0
- package/dist/agentSessions/HostInstance.d.ts.map +1 -0
- package/dist/agentSessions/HostInstance.js +48 -0
- package/dist/agentSessions/HostInstance.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +97 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -0
- package/dist/agentSessions/SessionJanitor.js +222 -0
- package/dist/agentSessions/SessionJanitor.js.map +1 -0
- package/dist/agentSessions/SessionManager.d.ts +142 -0
- package/dist/agentSessions/SessionManager.d.ts.map +1 -0
- package/dist/agentSessions/SessionManager.js +308 -0
- package/dist/agentSessions/SessionManager.js.map +1 -0
- package/dist/agentSessions/index.d.ts +5 -0
- package/dist/agentSessions/index.d.ts.map +1 -0
- package/dist/agentSessions/index.js +31 -0
- package/dist/agentSessions/index.js.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +102 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js +164 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts +66 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js +86 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js.map +1 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +4 -2
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -2
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +41 -7
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +1296 -49
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +50343 -43421
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.js +1 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.js +9 -10
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +127 -54
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +108 -0
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -0
- package/dist/integration/CustomColumnPromoter.js +508 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -0
- package/dist/logging/StartupLogger.d.ts +177 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -0
- package/dist/logging/StartupLogger.js +354 -0
- package/dist/logging/StartupLogger.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +21 -2
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts +42 -0
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -0
- package/dist/resolvers/AgentSessionResolver.js +152 -0
- package/dist/resolvers/AgentSessionResolver.js.map +1 -0
- package/dist/resolvers/EntityPermissionResolver.d.ts +16 -0
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -0
- package/dist/resolvers/EntityPermissionResolver.js +95 -0
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +27 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +136 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +101 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +544 -42
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +121 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.js +533 -0
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +688 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js +1774 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +432 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.js +1108 -0
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +0 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +5 -3
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +6 -5
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/package.json +83 -74
- package/src/__tests__/RealtimeBridgeResolver.test.ts +100 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +2605 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +181 -0
- package/src/__tests__/RemoteBrowserGoalEngine.test.ts +144 -0
- package/src/__tests__/SessionJanitor.test.ts +234 -0
- package/src/__tests__/SessionManager.test.ts +465 -0
- package/src/__tests__/remoteBrowserGoalRegistry.test.ts +81 -0
- package/src/__tests__/subscriptionRedaction.test.ts +5 -0
- package/src/agentSessions/HostInstance.ts +53 -0
- package/src/agentSessions/SessionJanitor.ts +267 -0
- package/src/agentSessions/SessionManager.ts +446 -0
- package/src/agentSessions/index.ts +32 -0
- package/src/agentSessions/remoteBrowserGoalEngine.ts +191 -0
- package/src/agentSessions/remoteBrowserGoalRegistry.ts +122 -0
- package/src/auth/initializeProviders.ts +4 -2
- package/src/config.ts +13 -2
- package/src/context.ts +42 -7
- package/src/generated/generated.ts +33808 -29025
- package/src/generic/ResolverBase.ts +1 -1
- package/src/generic/RunViewResolver.ts +9 -9
- package/src/index.ts +128 -54
- package/src/integration/CustomColumnPromoter.ts +597 -0
- package/src/logging/StartupLogger.ts +441 -0
- package/src/logging/variablesLoggingMiddleware.ts +25 -5
- package/src/resolvers/AgentSessionResolver.ts +138 -0
- package/src/resolvers/EntityPermissionResolver.ts +73 -0
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +108 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +396 -37
- package/src/resolvers/RealtimeBridgeResolver.ts +450 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +2162 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +1122 -0
- package/src/resolvers/RunAIAgentResolver.ts +1 -9
- package/src/rest/setupRESTEndpoints.ts +5 -3
- package/src/services/ScheduledJobsService.ts +6 -5
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, InputType, Field } from 'type-graphql';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { LogError, UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
4
|
+
import { LiveKitTokenService, LiveKitAgentRoomCoordinator, LiveKitEgressService } from '@memberjunction/livekit-room-server';
|
|
5
|
+
import { AppContext } from '../types.js';
|
|
6
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
|
+
import { GetReadWriteProvider } from '../util.js';
|
|
8
|
+
import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink } from '@memberjunction/ai-agents';
|
|
9
|
+
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
10
|
+
import { SessionManager } from '../agentSessions/SessionManager.js';
|
|
11
|
+
import { NotificationEngine } from '@memberjunction/notifications';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Binds the agent realtime-session factory onto the LiveKit room coordinator's model-session creation seam.
|
|
15
|
+
* Module-load side effect — it runs when MJServer builds its GraphQL schema (which imports this resolver),
|
|
16
|
+
* so `StartLiveKitAgentRoomSession` can open a real model session. `@memberjunction/server` is the natural
|
|
17
|
+
* home for the binding: it is the one package that depends on BOTH `@memberjunction/ai-agents` (the factory)
|
|
18
|
+
* and `@memberjunction/livekit-room-server` (the coordinator), keeping each of those decoupled from the
|
|
19
|
+
* other. Idempotent (latest-wins).
|
|
20
|
+
*/
|
|
21
|
+
LiveKitAgentRoomCoordinator.Instance.SetSessionFactory(CreateBridgeRealtimeSession);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Binds the co-agent run finalizer onto the bridge engine (same module-load rationale as the factory above).
|
|
25
|
+
* Lets the engine finalize a session's dangling co-agent observability run when it reaps a bridge WITHOUT a
|
|
26
|
+
* live in-memory session (a prior-boot orphan / cross-host reap) — the one teardown path the agent layer's
|
|
27
|
+
* `Close()`-wrapped finalizer can't reach. Idempotent for clean same-process teardowns.
|
|
28
|
+
*/
|
|
29
|
+
AIBridgeEngine.Instance.SetSessionRunFinalizer(FinalizeBridgeCoAgentRuns);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Binds the unified room-transcript sink onto the bridge engine (same module-load rationale). The engine
|
|
33
|
+
* elects one scribe per LiveKit room and feeds its final transcript lines here; the sink persists them as a
|
|
34
|
+
* single `MJ: Conversations` of `Type='Meeting Room'`, scoped `Application` so it stays OUT of the normal
|
|
35
|
+
* chat list (it surfaces in the Meet app's own view), with one `MJ: Conversation Detail` per utterance. The
|
|
36
|
+
* "Meeting Room"/scope choices live HERE (the Meet composition layer), keeping the engine generic.
|
|
37
|
+
*/
|
|
38
|
+
AIBridgeEngine.Instance.SetTranscriptSink(
|
|
39
|
+
CreateBridgeRoomTranscriptSink({ ConversationType: 'Meeting Room', ApplicationScope: 'Application' }),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* GraphQL surface for the MJ-native LiveKit room: mints scoped client access tokens and starts an
|
|
44
|
+
* agent's presence in a room. The thin resolver delegates to `@memberjunction/livekit-room-server`
|
|
45
|
+
* (token service + session-start coordinator) per the Transport-Layer Architecture — no LiveKit logic
|
|
46
|
+
* lives here.
|
|
47
|
+
*
|
|
48
|
+
* `MintLiveKitClientToken` is fully functional given LiveKit credentials. `StartLiveKitAgentRoomSession`
|
|
49
|
+
* opens a real agent model session via the realtime-session factory bound above (the agent must have an
|
|
50
|
+
* Active `Realtime` model + a vendor with a resolvable API key), then bridges it into the room through the
|
|
51
|
+
* coordinator. The native room media client (`@memberjunction/ai-bridge-livekit-native` /
|
|
52
|
+
* `@livekit/rtc-node`) must be installed on the agent host for the bot's audio to flow.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
@InputType()
|
|
56
|
+
export class MintLiveKitClientTokenInput {
|
|
57
|
+
@Field(() => String)
|
|
58
|
+
RoomName: string;
|
|
59
|
+
|
|
60
|
+
@Field(() => String, { nullable: true })
|
|
61
|
+
DisplayName?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@ObjectType()
|
|
65
|
+
export class LiveKitClientTokenResult {
|
|
66
|
+
@Field(() => Boolean)
|
|
67
|
+
Success: boolean;
|
|
68
|
+
|
|
69
|
+
@Field(() => String, { nullable: true })
|
|
70
|
+
ErrorMessage?: string;
|
|
71
|
+
|
|
72
|
+
@Field(() => String)
|
|
73
|
+
ServerUrl: string;
|
|
74
|
+
|
|
75
|
+
@Field(() => String)
|
|
76
|
+
Token: string;
|
|
77
|
+
|
|
78
|
+
@Field(() => String)
|
|
79
|
+
Identity: string;
|
|
80
|
+
|
|
81
|
+
@Field(() => String)
|
|
82
|
+
RoomName: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@InputType()
|
|
86
|
+
export class StartLiveKitAgentRoomSessionInput {
|
|
87
|
+
@Field(() => String, { nullable: true })
|
|
88
|
+
AgentID?: string;
|
|
89
|
+
|
|
90
|
+
@Field(() => String, { nullable: true })
|
|
91
|
+
AgentName?: string;
|
|
92
|
+
|
|
93
|
+
/** The TARGET agent the co-agent voices (the one being "called") — the Realtime Co-Agent delegates to it. */
|
|
94
|
+
@Field(() => String, { nullable: true })
|
|
95
|
+
TargetAgentID?: string;
|
|
96
|
+
|
|
97
|
+
/** Optional per-session Realtime MODEL override (Name or ID) — a dev choosing the model for this agent. */
|
|
98
|
+
@Field(() => String, { nullable: true })
|
|
99
|
+
RealtimeModelID?: string;
|
|
100
|
+
|
|
101
|
+
/** Optional per-session VOICE override (provider-native voice id) — gives this agent a distinct voice. */
|
|
102
|
+
@Field(() => String, { nullable: true })
|
|
103
|
+
RealtimeVoice?: string;
|
|
104
|
+
|
|
105
|
+
@Field(() => String, { nullable: true })
|
|
106
|
+
RoomName?: string;
|
|
107
|
+
|
|
108
|
+
@Field(() => String, { nullable: true })
|
|
109
|
+
AgentSessionID?: string;
|
|
110
|
+
|
|
111
|
+
@Field(() => String, { nullable: true })
|
|
112
|
+
TurnMode?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@ObjectType()
|
|
116
|
+
export class LiveKitAgentRoomSessionResult {
|
|
117
|
+
@Field(() => Boolean)
|
|
118
|
+
Success: boolean;
|
|
119
|
+
|
|
120
|
+
@Field(() => String, { nullable: true })
|
|
121
|
+
ErrorMessage?: string;
|
|
122
|
+
|
|
123
|
+
@Field(() => String)
|
|
124
|
+
SessionBridgeID: string;
|
|
125
|
+
|
|
126
|
+
@Field(() => String)
|
|
127
|
+
RoomName: string;
|
|
128
|
+
|
|
129
|
+
@Field(() => String)
|
|
130
|
+
ServerUrl: string;
|
|
131
|
+
|
|
132
|
+
@Field(() => String)
|
|
133
|
+
ClientToken: string;
|
|
134
|
+
|
|
135
|
+
@Field(() => String)
|
|
136
|
+
Identity: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@InputType()
|
|
140
|
+
export class LiveKitRecordingInput {
|
|
141
|
+
@Field(() => String)
|
|
142
|
+
RoomName: string;
|
|
143
|
+
|
|
144
|
+
@Field(() => String, { nullable: true })
|
|
145
|
+
Layout?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@ObjectType()
|
|
149
|
+
export class LiveKitRecordingResult {
|
|
150
|
+
@Field(() => Boolean)
|
|
151
|
+
Success: boolean;
|
|
152
|
+
|
|
153
|
+
@Field(() => String, { nullable: true })
|
|
154
|
+
ErrorMessage?: string;
|
|
155
|
+
|
|
156
|
+
@Field(() => String)
|
|
157
|
+
EgressID: string;
|
|
158
|
+
|
|
159
|
+
@Field(() => String)
|
|
160
|
+
Status: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A selectable provider-native voice for the dev voice picker. */
|
|
164
|
+
@ObjectType()
|
|
165
|
+
export class RealtimeVoiceOptionResult {
|
|
166
|
+
@Field(() => String)
|
|
167
|
+
ID: string;
|
|
168
|
+
|
|
169
|
+
@Field(() => String)
|
|
170
|
+
Name: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** An active Realtime model with the voices its driver supports — feeds the dev model/voice picker. */
|
|
174
|
+
@ObjectType()
|
|
175
|
+
export class RealtimeModelVoicesResult {
|
|
176
|
+
@Field(() => String)
|
|
177
|
+
ModelID: string;
|
|
178
|
+
|
|
179
|
+
@Field(() => String)
|
|
180
|
+
ModelName: string;
|
|
181
|
+
|
|
182
|
+
@Field(() => [RealtimeVoiceOptionResult])
|
|
183
|
+
Voices: RealtimeVoiceOptionResult[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Resolver()
|
|
187
|
+
export class RealtimeBridgeResolver extends ResolverBase {
|
|
188
|
+
/** Durable `AIAgentSession` record manager — creates the session row the bridge FK-references. */
|
|
189
|
+
private readonly sessionManager = new SessionManager();
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Mints a scoped LiveKit access token for the current user to join the given room. The participant
|
|
193
|
+
* identity is derived server-side from the authenticated user (never trusted from the client).
|
|
194
|
+
*/
|
|
195
|
+
@Mutation(() => LiveKitClientTokenResult)
|
|
196
|
+
async MintLiveKitClientToken(
|
|
197
|
+
@Arg('input', () => MintLiveKitClientTokenInput) input: MintLiveKitClientTokenInput,
|
|
198
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
199
|
+
): Promise<LiveKitClientTokenResult> {
|
|
200
|
+
const failure = (msg: string): LiveKitClientTokenResult => ({
|
|
201
|
+
Success: false,
|
|
202
|
+
ErrorMessage: msg,
|
|
203
|
+
ServerUrl: '',
|
|
204
|
+
Token: '',
|
|
205
|
+
Identity: '',
|
|
206
|
+
RoomName: input.RoomName,
|
|
207
|
+
});
|
|
208
|
+
try {
|
|
209
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
210
|
+
if (!user) {
|
|
211
|
+
return failure('Unable to determine current user.');
|
|
212
|
+
}
|
|
213
|
+
const tokenService = new LiveKitTokenService();
|
|
214
|
+
const minted = await tokenService.MintClientToken(input.RoomName, this.participantIdentity(user), input.DisplayName ?? user.Name ?? user.Email);
|
|
215
|
+
return { Success: true, ...minted };
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
218
|
+
LogError(`MintLiveKitClientToken failed: ${msg}`);
|
|
219
|
+
return failure(msg);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Starts (or reuses) an agent's presence in a LiveKit room and returns a client token so the calling
|
|
225
|
+
* user can immediately join the same room.
|
|
226
|
+
*/
|
|
227
|
+
@Mutation(() => LiveKitAgentRoomSessionResult)
|
|
228
|
+
async StartLiveKitAgentRoomSession(
|
|
229
|
+
@Arg('input', () => StartLiveKitAgentRoomSessionInput) input: StartLiveKitAgentRoomSessionInput,
|
|
230
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
231
|
+
): Promise<LiveKitAgentRoomSessionResult> {
|
|
232
|
+
const failure = (msg: string, roomName = ''): LiveKitAgentRoomSessionResult => ({
|
|
233
|
+
Success: false,
|
|
234
|
+
ErrorMessage: msg,
|
|
235
|
+
SessionBridgeID: '',
|
|
236
|
+
RoomName: roomName,
|
|
237
|
+
ServerUrl: '',
|
|
238
|
+
ClientToken: '',
|
|
239
|
+
Identity: '',
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
243
|
+
if (!user) {
|
|
244
|
+
return failure('Unable to determine current user.');
|
|
245
|
+
}
|
|
246
|
+
const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
|
|
247
|
+
const roomName = input.RoomName?.trim() || `mj-${randomUUID()}`;
|
|
248
|
+
|
|
249
|
+
// Resolve the AIAgentSession the bridge will reference. The bridge row FK-references
|
|
250
|
+
// AIAgentSession(ID), so we must use an EXISTING session — either one the caller supplied, or a
|
|
251
|
+
// freshly-created one. Previously this minted a bare random UUID with no backing row, so the
|
|
252
|
+
// bridge INSERT failed the FK_AIAgentSessionBridge_Session constraint.
|
|
253
|
+
let agentSessionID = input.AgentSessionID?.trim();
|
|
254
|
+
if (!agentSessionID) {
|
|
255
|
+
if (!input.AgentID?.trim()) {
|
|
256
|
+
return failure('An AgentID is required to start an agent room session.', roomName);
|
|
257
|
+
}
|
|
258
|
+
const createdSession = await this.sessionManager.CreateSession(
|
|
259
|
+
{ agentID: input.AgentID.trim(), userID: user.ID },
|
|
260
|
+
user,
|
|
261
|
+
provider,
|
|
262
|
+
);
|
|
263
|
+
agentSessionID = createdSession.ID;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const session = await LiveKitAgentRoomCoordinator.Instance.StartAgentRoomSession({
|
|
267
|
+
AgentSessionID: agentSessionID,
|
|
268
|
+
RoomName: roomName,
|
|
269
|
+
AgentID: input.AgentID,
|
|
270
|
+
AgentName: input.AgentName,
|
|
271
|
+
TargetAgentID: input.TargetAgentID,
|
|
272
|
+
RealtimeModelID: input.RealtimeModelID,
|
|
273
|
+
RealtimeVoice: input.RealtimeVoice,
|
|
274
|
+
TurnMode: this.normalizeTurnMode(input.TurnMode),
|
|
275
|
+
ContextUser: user,
|
|
276
|
+
MetadataProvider: provider,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const tokenService = new LiveKitTokenService();
|
|
280
|
+
const clientToken = await tokenService.MintClientToken(roomName, this.participantIdentity(user), user.Name ?? user.Email);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
Success: true,
|
|
284
|
+
SessionBridgeID: session.SessionBridgeID,
|
|
285
|
+
RoomName: session.RoomName,
|
|
286
|
+
ServerUrl: session.ServerUrl,
|
|
287
|
+
ClientToken: clientToken.Token,
|
|
288
|
+
Identity: clientToken.Identity,
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
292
|
+
LogError(`StartLiveKitAgentRoomSession failed: ${msg}`);
|
|
293
|
+
return failure(msg, input.RoomName ?? '');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Stops one agent's presence in a room (the bot leaves) — the remove half of in-room agent management.
|
|
299
|
+
* Identified by the `SessionBridgeID` returned from {@link StartLiveKitAgentRoomSession}. Returns `true`
|
|
300
|
+
* when the bridge was stopped. Best-effort: a missing/already-stopped bridge or any error resolves `false`.
|
|
301
|
+
*
|
|
302
|
+
* @param sessionBridgeID The `MJ: AI Agent Session Bridges` row id of the agent to remove.
|
|
303
|
+
*/
|
|
304
|
+
@Mutation(() => Boolean)
|
|
305
|
+
async StopLiveKitAgentRoomSession(
|
|
306
|
+
@Arg('sessionBridgeID', () => String) sessionBridgeID: string,
|
|
307
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
308
|
+
): Promise<boolean> {
|
|
309
|
+
try {
|
|
310
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
311
|
+
if (!user) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
|
|
315
|
+
return await LiveKitAgentRoomCoordinator.Instance.StopAgentRoomSession(sessionBridgeID, 'Explicit', user, provider);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
LogError(`StopLiveKitAgentRoomSession failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Lists active Realtime models with the voices each driver supports — the source for the dev model/voice
|
|
324
|
+
* picker (gated client-side by the `Realtime: Advanced Session Controls` authorization). Read-only; returns
|
|
325
|
+
* an empty list on any error so the picker degrades gracefully to "no overrides".
|
|
326
|
+
*/
|
|
327
|
+
@Query(() => [RealtimeModelVoicesResult])
|
|
328
|
+
async GetRealtimeModelVoices(
|
|
329
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
330
|
+
): Promise<RealtimeModelVoicesResult[]> {
|
|
331
|
+
try {
|
|
332
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
333
|
+
if (!user) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
|
|
337
|
+
return await GetRealtimeModelVoices(user, provider);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
LogError(`GetRealtimeModelVoices failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Invites MJ users to a live room: for each user, sends a **"Live Room Invite"** notification via the
|
|
346
|
+
* unified {@link NotificationEngine} — which writes the in-app notification (clickable → joins the room
|
|
347
|
+
* via the `meet-room` ResourceConfiguration) and ALSO delivers over MJ Comms (email/SMS) when the type's
|
|
348
|
+
* channels + a provider are configured. Best-effort: Comms not being set up never blocks the in-app
|
|
349
|
+
* notification, and a missing "Live Room Invite" type (seed not yet pushed) is caught and returns false.
|
|
350
|
+
*
|
|
351
|
+
* @param roomName The LiveKit room the invitees should join.
|
|
352
|
+
* @param userIDs The `MJ: Users` ids to invite.
|
|
353
|
+
* @returns `true` when at least one invite was delivered.
|
|
354
|
+
*/
|
|
355
|
+
@Mutation(() => Boolean)
|
|
356
|
+
async InviteUsersToLiveKitRoom(
|
|
357
|
+
@Arg('roomName', () => String) roomName: string,
|
|
358
|
+
@Arg('userIDs', () => [String]) userIDs: string[],
|
|
359
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
360
|
+
): Promise<boolean> {
|
|
361
|
+
try {
|
|
362
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
363
|
+
if (!user) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
|
|
367
|
+
await NotificationEngine.Instance.Config(false, user, provider);
|
|
368
|
+
|
|
369
|
+
const inviter = user.Name?.trim() || user.Email || 'Someone';
|
|
370
|
+
let anyDelivered = false;
|
|
371
|
+
for (const userId of userIDs ?? []) {
|
|
372
|
+
if (!userId?.trim()) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const result = await NotificationEngine.Instance.SendNotification(
|
|
376
|
+
{
|
|
377
|
+
userId,
|
|
378
|
+
typeNameOrId: 'Live Room Invite',
|
|
379
|
+
title: `${inviter} invited you to a live room`,
|
|
380
|
+
message: `${inviter} is inviting you to join a live Meet room. Open this notification to join.`,
|
|
381
|
+
resourceConfiguration: { type: 'meet-room', room: roomName },
|
|
382
|
+
},
|
|
383
|
+
user,
|
|
384
|
+
);
|
|
385
|
+
anyDelivered = anyDelivered || result.success;
|
|
386
|
+
}
|
|
387
|
+
return anyDelivered;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
LogError(`InviteUsersToLiveKitRoom failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Starts recording (composite egress) of a room. Server-authorized — the browser never holds egress
|
|
396
|
+
* credentials.
|
|
397
|
+
*/
|
|
398
|
+
@Mutation(() => LiveKitRecordingResult)
|
|
399
|
+
async StartLiveKitRecording(
|
|
400
|
+
@Arg('input', () => LiveKitRecordingInput) input: LiveKitRecordingInput,
|
|
401
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
402
|
+
): Promise<LiveKitRecordingResult> {
|
|
403
|
+
try {
|
|
404
|
+
if (!this.GetUserFromPayload(context.userPayload)) {
|
|
405
|
+
return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: '', Status: '' };
|
|
406
|
+
}
|
|
407
|
+
const info = await new LiveKitEgressService().StartRoomRecording({ RoomName: input.RoomName, Layout: input.Layout });
|
|
408
|
+
return { Success: true, EgressID: info.EgressID, Status: info.Status };
|
|
409
|
+
} catch (error) {
|
|
410
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
411
|
+
LogError(`StartLiveKitRecording failed: ${msg}`);
|
|
412
|
+
return { Success: false, ErrorMessage: msg, EgressID: '', Status: '' };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Stops a recording by egress id. */
|
|
417
|
+
@Mutation(() => LiveKitRecordingResult)
|
|
418
|
+
async StopLiveKitRecording(@Arg('egressID', () => String) egressID: string, @Ctx() context: AppContext = {} as AppContext): Promise<LiveKitRecordingResult> {
|
|
419
|
+
try {
|
|
420
|
+
if (!this.GetUserFromPayload(context.userPayload)) {
|
|
421
|
+
return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: egressID, Status: '' };
|
|
422
|
+
}
|
|
423
|
+
const info = await new LiveKitEgressService().StopRecording(egressID);
|
|
424
|
+
return { Success: true, EgressID: info.EgressID, Status: info.Status };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
427
|
+
LogError(`StopLiveKitRecording failed: ${msg}`);
|
|
428
|
+
return { Success: false, ErrorMessage: msg, EgressID: egressID, Status: '' };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Builds a stable, lowercased participant identity from the authenticated user. */
|
|
433
|
+
private participantIdentity(user: UserInfo): string {
|
|
434
|
+
return `user-${user.ID}`.toLowerCase();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Normalizes a turn-mode string to the bridge's accepted values. */
|
|
438
|
+
private normalizeTurnMode(mode?: string): 'Passive' | 'Active' | 'Hybrid' | undefined {
|
|
439
|
+
switch ((mode ?? '').toLowerCase()) {
|
|
440
|
+
case 'active':
|
|
441
|
+
return 'Active';
|
|
442
|
+
case 'hybrid':
|
|
443
|
+
return 'Hybrid';
|
|
444
|
+
case 'passive':
|
|
445
|
+
return 'Passive';
|
|
446
|
+
default:
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|