@adminforth/agent 1.45.1 → 1.47.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/build.log +2 -3
- package/chatSurfaceService.ts +215 -92
- package/dist/chatSurfaceService.d.ts +8 -19
- package/dist/chatSurfaceService.js +160 -64
- package/dist/endpoints/context.d.ts +2 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -19
- package/dist/types.d.ts +11 -2
- package/endpoints/context.ts +0 -8
- package/index.ts +1 -18
- package/package.json +1 -1
- package/types.ts +11 -2
- package/custom/ChatSurfaceSettings.vue +0 -125
- package/dist/custom/ChatSurfaceSettings.vue +0 -125
- package/dist/endpoints/chatSurfaces.d.ts +0 -3
- package/dist/endpoints/chatSurfaces.js +0 -91
- package/endpoints/chatSurfaces.ts +0 -93
package/build.log
CHANGED
|
@@ -7,7 +7,6 @@ custom/
|
|
|
7
7
|
custom/ChatFooter.vue
|
|
8
8
|
custom/ChatHeader.vue
|
|
9
9
|
custom/ChatSurface.vue
|
|
10
|
-
custom/ChatSurfaceSettings.vue
|
|
11
10
|
custom/CustomAutoScrollContainer.vue
|
|
12
11
|
custom/SessionsHistory.vue
|
|
13
12
|
custom/chat.ts
|
|
@@ -63,5 +62,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
|
|
|
63
62
|
custom/speech_recognition_frontend/types/
|
|
64
63
|
custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
|
|
65
64
|
|
|
66
|
-
sent 1,
|
|
67
|
-
total size is 1,
|
|
65
|
+
sent 1,667,665 bytes received 921 bytes 3,337,172.00 bytes/sec
|
|
66
|
+
total size is 1,663,512 speedup is 1.00
|
package/chatSurfaceService.ts
CHANGED
|
@@ -5,78 +5,46 @@ import type {
|
|
|
5
5
|
ChatSurfaceIncomingMessage,
|
|
6
6
|
IAdminForth,
|
|
7
7
|
} from "adminforth";
|
|
8
|
-
import { Filters } from "adminforth";
|
|
9
|
-
import { randomUUID } from "crypto";
|
|
8
|
+
import { Filters, logger } from "adminforth";
|
|
10
9
|
import type { AgentEventEmitter } from "./agentEvents.js";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
10
|
+
import type {
|
|
11
|
+
HandleTurnInput,
|
|
12
|
+
RunAndPersistAgentResponseInput,
|
|
13
|
+
RunAndPersistAgentResponseResult,
|
|
14
|
+
} from "./agentTurnService.js";
|
|
15
|
+
import { getErrorMessage, isAbortError } from "./errors.js";
|
|
13
16
|
import type { AgentSessionStore } from "./sessionStore.js";
|
|
17
|
+
import { sanitizeSpeechText } from "./sanitizeSpeechText.js";
|
|
18
|
+
import type { PluginOptions } from "./types.js";
|
|
14
19
|
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
|
|
22
|
-
createConnectAction?(input: {
|
|
23
|
-
token: string;
|
|
24
|
-
}): ChatSurfaceConnectAction | Promise<ChatSurfaceConnectAction>;
|
|
20
|
+
type ChatSurfaceIncomingMessageWithAudio = ChatSurfaceIncomingMessage & {
|
|
21
|
+
audio?: {
|
|
22
|
+
buffer: Buffer;
|
|
23
|
+
filename: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
};
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
type ChatSurfaceEventSinkWithAudio = ChatSurfaceEventSink & {
|
|
29
|
+
emit(event: Parameters<ChatSurfaceEventSink["emit"]>[0] | {
|
|
30
|
+
type: "audio";
|
|
31
|
+
audio: Buffer;
|
|
32
|
+
filename: string;
|
|
33
|
+
mimeType: string;
|
|
34
|
+
}): void | Promise<void>;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
|
|
34
|
-
const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
|
|
35
|
-
|
|
36
37
|
export class ChatSurfaceService {
|
|
37
|
-
private linkTokens = new Map<string, ChatSurfaceLinkTokenPayload>();
|
|
38
|
-
|
|
39
38
|
constructor(
|
|
40
39
|
private getAdminforth: () => IAdminForth,
|
|
41
40
|
private options: PluginOptions,
|
|
42
41
|
private sessionStore: AgentSessionStore,
|
|
43
42
|
private handleTurn: (input: HandleTurnInput) => Promise<unknown>,
|
|
43
|
+
private runAndPersistAgentResponse: (
|
|
44
|
+
input: RunAndPersistAgentResponseInput,
|
|
45
|
+
) => Promise<RunAndPersistAgentResponseResult>,
|
|
44
46
|
) {}
|
|
45
47
|
|
|
46
|
-
getConnectActionAdapters() {
|
|
47
|
-
return (this.options.chatSurfaceAdapters ?? [])
|
|
48
|
-
.map((adapter) => adapter as ChatSurfaceAdapterWithConnectAction)
|
|
49
|
-
.filter((adapter) => adapter.createConnectAction);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
createLinkToken(surface: string, adminUser: AdminUser) {
|
|
53
|
-
for (const [token, payload] of this.linkTokens) {
|
|
54
|
-
if (payload.expiresAt <= Date.now()) {
|
|
55
|
-
this.linkTokens.delete(token);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const token = randomUUID();
|
|
60
|
-
this.linkTokens.set(token, {
|
|
61
|
-
surface,
|
|
62
|
-
adminUserId: adminUser.pk,
|
|
63
|
-
expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return token;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private consumeLinkToken(surface: string, token: string) {
|
|
70
|
-
const payload = this.linkTokens.get(token);
|
|
71
|
-
this.linkTokens.delete(token);
|
|
72
|
-
|
|
73
|
-
if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return payload;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
48
|
private createEventEmitter(sink: ChatSurfaceEventSink): AgentEventEmitter {
|
|
81
49
|
return async (event) => {
|
|
82
50
|
if (event.type === "text-delta") {
|
|
@@ -108,39 +76,200 @@ export class ChatSurfaceService {
|
|
|
108
76
|
incoming: ChatSurfaceIncomingMessage,
|
|
109
77
|
sink: ChatSurfaceEventSink,
|
|
110
78
|
) {
|
|
111
|
-
if (
|
|
79
|
+
if (incoming.metadata?.isStartCommand !== true) {
|
|
112
80
|
return false;
|
|
113
81
|
}
|
|
114
82
|
|
|
115
|
-
|
|
116
|
-
|
|
83
|
+
await sink.emit({
|
|
84
|
+
type: "done",
|
|
85
|
+
text: `Open AdminForth and connect your ${incoming.surface} account from Connected Accounts settings.`,
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async handleAudioMessage(
|
|
91
|
+
incoming: ChatSurfaceIncomingMessageWithAudio,
|
|
92
|
+
sink: ChatSurfaceEventSinkWithAudio,
|
|
93
|
+
adminUser: AdminUser,
|
|
94
|
+
) {
|
|
95
|
+
const audioAdapter = this.options.audioAdapter;
|
|
96
|
+
if (!audioAdapter) {
|
|
117
97
|
await sink.emit({
|
|
118
98
|
type: "error",
|
|
119
|
-
message: "
|
|
99
|
+
message: "Audio adapter is not configured for AdminForth Agent.",
|
|
120
100
|
});
|
|
121
|
-
return
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let transcription;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
transcription = await audioAdapter.transcribe({
|
|
108
|
+
buffer: incoming.audio!.buffer,
|
|
109
|
+
filename: incoming.audio!.filename,
|
|
110
|
+
mimeType: incoming.audio!.mimeType,
|
|
111
|
+
language: "auto",
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isAbortError(error)) {
|
|
115
|
+
logger.info(`Agent ${incoming.surface} surface speech transcription aborted`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
logger.error(`Agent ${incoming.surface} surface speech transcription failed:\n${getErrorMessage(error)}`);
|
|
120
|
+
await sink.emit({
|
|
121
|
+
type: "error",
|
|
122
|
+
message: "Speech transcription failed. Check server logs for details.",
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!transcription.text) {
|
|
128
|
+
await sink.emit({
|
|
129
|
+
type: "error",
|
|
130
|
+
message: "Speech transcription is empty",
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const agentResponse = await this.handleAgentSurfaceResponse(
|
|
136
|
+
incoming,
|
|
137
|
+
sink,
|
|
138
|
+
adminUser,
|
|
139
|
+
transcription.text,
|
|
140
|
+
{ emitDone: false },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (!agentResponse || agentResponse.aborted || agentResponse.failed) {
|
|
144
|
+
return;
|
|
122
145
|
}
|
|
123
|
-
const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
124
|
-
const adminforth = this.getAdminforth();
|
|
125
|
-
const authResourceId = adminforth.config.auth!.usersResourceId!;
|
|
126
|
-
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
127
|
-
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
128
|
-
const adminUserRecord = await adminforth.resource(authResourceId).get([
|
|
129
|
-
Filters.EQ(primaryKeyField, payload.adminUserId),
|
|
130
|
-
]);
|
|
131
146
|
|
|
132
|
-
await adminforth.resource(authResourceId).update(payload.adminUserId, {
|
|
133
|
-
[externalUserIdField]: {
|
|
134
|
-
...(adminUserRecord[externalUserIdField] ?? {}),
|
|
135
|
-
[incoming.surface]: incoming.externalUserId,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
147
|
await sink.emit({
|
|
139
148
|
type: "done",
|
|
140
|
-
text:
|
|
149
|
+
text: agentResponse.text,
|
|
141
150
|
});
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
try {
|
|
153
|
+
const speech = await audioAdapter.synthesize({
|
|
154
|
+
text: sanitizeSpeechText(agentResponse.text),
|
|
155
|
+
stream: false,
|
|
156
|
+
format: "opus",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await sink.emit({
|
|
160
|
+
type: "audio",
|
|
161
|
+
audio: speech.audio,
|
|
162
|
+
filename: "agent-response.ogg",
|
|
163
|
+
mimeType: speech.mimeType,
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (isAbortError(error)) {
|
|
167
|
+
logger.info(`Agent ${incoming.surface} surface speech synthesis aborted`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
logger.error(`Agent ${incoming.surface} surface speech synthesis failed:\n${getErrorMessage(error)}`);
|
|
172
|
+
await sink.emit({
|
|
173
|
+
type: "error",
|
|
174
|
+
message: getErrorMessage(error),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async handleAgentSurfaceResponse(
|
|
180
|
+
incoming: ChatSurfaceIncomingMessage,
|
|
181
|
+
sink: ChatSurfaceEventSink,
|
|
182
|
+
adminUser: AdminUser,
|
|
183
|
+
prompt: string,
|
|
184
|
+
options?: { emitDone?: boolean },
|
|
185
|
+
) {
|
|
186
|
+
const emitDone = options?.emitDone ?? true;
|
|
187
|
+
const sessionId = await this.sessionStore.getOrCreateChatSurfaceSession(
|
|
188
|
+
{ ...incoming, prompt },
|
|
189
|
+
adminUser,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (emitDone) {
|
|
193
|
+
await this.handleTurn({
|
|
194
|
+
prompt,
|
|
195
|
+
sessionId,
|
|
196
|
+
modeName: incoming.modeName,
|
|
197
|
+
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
198
|
+
adminUser,
|
|
199
|
+
emit: this.createEventEmitter(sink),
|
|
200
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
201
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
202
|
+
});
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const agentResponse = await this.runAndPersistAgentResponse({
|
|
207
|
+
prompt,
|
|
208
|
+
sessionId,
|
|
209
|
+
modeName: incoming.modeName,
|
|
210
|
+
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
211
|
+
adminUser,
|
|
212
|
+
emit: this.createEventEmitter(sink),
|
|
213
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
214
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (agentResponse.failed) {
|
|
218
|
+
await sink.emit({
|
|
219
|
+
type: "error",
|
|
220
|
+
message: agentResponse.text,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return agentResponse;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private async getAdminUserRecordForChatSurface(
|
|
228
|
+
adapter: ChatSurfaceAdapter,
|
|
229
|
+
incoming: ChatSurfaceIncomingMessage,
|
|
230
|
+
) {
|
|
231
|
+
const adminforth = this.getAdminforth();
|
|
232
|
+
const authResourceId = adminforth.config.auth!.usersResourceId!;
|
|
233
|
+
const externalIdentityResource = this.options.chatExternalIdentityResource;
|
|
234
|
+
if (!externalIdentityResource) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const surfaceIdentityConfig = externalIdentityResource.surfaces[adapter.name];
|
|
239
|
+
if (!surfaceIdentityConfig) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const providerField = externalIdentityResource.providerField ?? 'provider';
|
|
244
|
+
const subjectField = externalIdentityResource.subjectField ?? 'subject';
|
|
245
|
+
const adminUserIdField = externalIdentityResource.adminUserIdField ?? 'adminUserId';
|
|
246
|
+
const externalUserIdField = externalIdentityResource.externalUserIdField ?? 'externalUserId';
|
|
247
|
+
const identityFilters = [
|
|
248
|
+
Filters.EQ(providerField, surfaceIdentityConfig.provider),
|
|
249
|
+
Filters.EQ(externalUserIdField, incoming.externalUserId),
|
|
250
|
+
];
|
|
251
|
+
const identities = await adminforth.resource(externalIdentityResource.resourceId).list(identityFilters);
|
|
252
|
+
const identity = identities.find((identity) => {
|
|
253
|
+
if (String(identity[externalUserIdField]) === incoming.externalUserId) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (String(identity[subjectField]) === incoming.externalUserId) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (!identity) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
269
|
+
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
270
|
+
return adminforth.resource(authResourceId).get([
|
|
271
|
+
Filters.EQ(primaryKeyField, identity[adminUserIdField]),
|
|
272
|
+
]);
|
|
144
273
|
}
|
|
145
274
|
|
|
146
275
|
async handleMessage(
|
|
@@ -156,10 +285,7 @@ export class ChatSurfaceService {
|
|
|
156
285
|
const authResourceId = adminforth.config.auth!.usersResourceId!;
|
|
157
286
|
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
158
287
|
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
159
|
-
const
|
|
160
|
-
const adminUserRecord = (
|
|
161
|
-
await adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))
|
|
162
|
-
).find((user) => user[externalUserIdField]?.[adapter.name] === incoming.externalUserId);
|
|
288
|
+
const adminUserRecord = await this.getAdminUserRecordForChatSurface(adapter, incoming);
|
|
163
289
|
|
|
164
290
|
if (!adminUserRecord) {
|
|
165
291
|
await sink.emit({
|
|
@@ -175,15 +301,12 @@ export class ChatSurfaceService {
|
|
|
175
301
|
dbUser: adminUserRecord,
|
|
176
302
|
};
|
|
177
303
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
186
|
-
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
187
|
-
});
|
|
304
|
+
const incomingWithAudio = incoming as ChatSurfaceIncomingMessageWithAudio;
|
|
305
|
+
if (incomingWithAudio.audio) {
|
|
306
|
+
await this.handleAudioMessage(incomingWithAudio, sink as ChatSurfaceEventSinkWithAudio, adminUser);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await this.handleAgentSurfaceResponse(incoming, sink, adminUser, incoming.prompt);
|
|
188
311
|
}
|
|
189
312
|
}
|
|
@@ -1,29 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { HandleTurnInput } from "./agentTurnService.js";
|
|
3
|
-
import type { PluginOptions } from "./types.js";
|
|
1
|
+
import type { ChatSurfaceAdapter, ChatSurfaceEventSink, ChatSurfaceIncomingMessage, IAdminForth } from "adminforth";
|
|
2
|
+
import type { HandleTurnInput, RunAndPersistAgentResponseInput, RunAndPersistAgentResponseResult } from "./agentTurnService.js";
|
|
4
3
|
import type { AgentSessionStore } from "./sessionStore.js";
|
|
5
|
-
type
|
|
6
|
-
type: "url";
|
|
7
|
-
label: string;
|
|
8
|
-
url: string;
|
|
9
|
-
};
|
|
10
|
-
export type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
|
|
11
|
-
createConnectAction?(input: {
|
|
12
|
-
token: string;
|
|
13
|
-
}): ChatSurfaceConnectAction | Promise<ChatSurfaceConnectAction>;
|
|
14
|
-
};
|
|
4
|
+
import type { PluginOptions } from "./types.js";
|
|
15
5
|
export declare class ChatSurfaceService {
|
|
16
6
|
private getAdminforth;
|
|
17
7
|
private options;
|
|
18
8
|
private sessionStore;
|
|
19
9
|
private handleTurn;
|
|
20
|
-
private
|
|
21
|
-
constructor(getAdminforth: () => IAdminForth, options: PluginOptions, sessionStore: AgentSessionStore, handleTurn: (input: HandleTurnInput) => Promise<unknown>);
|
|
22
|
-
getConnectActionAdapters(): ChatSurfaceAdapterWithConnectAction[];
|
|
23
|
-
createLinkToken(surface: string, adminUser: AdminUser): `${string}-${string}-${string}-${string}-${string}`;
|
|
24
|
-
private consumeLinkToken;
|
|
10
|
+
private runAndPersistAgentResponse;
|
|
11
|
+
constructor(getAdminforth: () => IAdminForth, options: PluginOptions, sessionStore: AgentSessionStore, handleTurn: (input: HandleTurnInput) => Promise<unknown>, runAndPersistAgentResponse: (input: RunAndPersistAgentResponseInput) => Promise<RunAndPersistAgentResponseResult>);
|
|
25
12
|
private createEventEmitter;
|
|
26
13
|
private handleLink;
|
|
14
|
+
private handleAudioMessage;
|
|
15
|
+
private handleAgentSurfaceResponse;
|
|
16
|
+
private getAdminUserRecordForChatSurface;
|
|
27
17
|
handleMessage(adapter: ChatSurfaceAdapter, incoming: ChatSurfaceIncomingMessage, sink: ChatSurfaceEventSink): Promise<void>;
|
|
28
18
|
}
|
|
29
|
-
export {};
|
|
@@ -7,45 +7,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { Filters } from "adminforth";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
|
|
10
|
+
import { Filters, logger } from "adminforth";
|
|
11
|
+
import { getErrorMessage, isAbortError } from "./errors.js";
|
|
12
|
+
import { sanitizeSpeechText } from "./sanitizeSpeechText.js";
|
|
14
13
|
export class ChatSurfaceService {
|
|
15
|
-
constructor(getAdminforth, options, sessionStore, handleTurn) {
|
|
14
|
+
constructor(getAdminforth, options, sessionStore, handleTurn, runAndPersistAgentResponse) {
|
|
16
15
|
this.getAdminforth = getAdminforth;
|
|
17
16
|
this.options = options;
|
|
18
17
|
this.sessionStore = sessionStore;
|
|
19
18
|
this.handleTurn = handleTurn;
|
|
20
|
-
this.
|
|
21
|
-
}
|
|
22
|
-
getConnectActionAdapters() {
|
|
23
|
-
var _a;
|
|
24
|
-
return ((_a = this.options.chatSurfaceAdapters) !== null && _a !== void 0 ? _a : [])
|
|
25
|
-
.map((adapter) => adapter)
|
|
26
|
-
.filter((adapter) => adapter.createConnectAction);
|
|
27
|
-
}
|
|
28
|
-
createLinkToken(surface, adminUser) {
|
|
29
|
-
for (const [token, payload] of this.linkTokens) {
|
|
30
|
-
if (payload.expiresAt <= Date.now()) {
|
|
31
|
-
this.linkTokens.delete(token);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const token = randomUUID();
|
|
35
|
-
this.linkTokens.set(token, {
|
|
36
|
-
surface,
|
|
37
|
-
adminUserId: adminUser.pk,
|
|
38
|
-
expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
|
|
39
|
-
});
|
|
40
|
-
return token;
|
|
41
|
-
}
|
|
42
|
-
consumeLinkToken(surface, token) {
|
|
43
|
-
const payload = this.linkTokens.get(token);
|
|
44
|
-
this.linkTokens.delete(token);
|
|
45
|
-
if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return payload;
|
|
19
|
+
this.runAndPersistAgentResponse = runAndPersistAgentResponse;
|
|
49
20
|
}
|
|
50
21
|
createEventEmitter(sink) {
|
|
51
22
|
return (event) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -73,39 +44,169 @@ export class ChatSurfaceService {
|
|
|
73
44
|
}
|
|
74
45
|
handleLink(incoming, sink) {
|
|
75
46
|
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
-
var _a
|
|
77
|
-
if (
|
|
47
|
+
var _a;
|
|
48
|
+
if (((_a = incoming.metadata) === null || _a === void 0 ? void 0 : _a.isStartCommand) !== true) {
|
|
78
49
|
return false;
|
|
79
50
|
}
|
|
80
|
-
|
|
81
|
-
|
|
51
|
+
yield sink.emit({
|
|
52
|
+
type: "done",
|
|
53
|
+
text: `Open AdminForth and connect your ${incoming.surface} account from Connected Accounts settings.`,
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
handleAudioMessage(incoming, sink, adminUser) {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
const audioAdapter = this.options.audioAdapter;
|
|
61
|
+
if (!audioAdapter) {
|
|
62
|
+
yield sink.emit({
|
|
63
|
+
type: "error",
|
|
64
|
+
message: "Audio adapter is not configured for AdminForth Agent.",
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let transcription;
|
|
69
|
+
try {
|
|
70
|
+
transcription = yield audioAdapter.transcribe({
|
|
71
|
+
buffer: incoming.audio.buffer,
|
|
72
|
+
filename: incoming.audio.filename,
|
|
73
|
+
mimeType: incoming.audio.mimeType,
|
|
74
|
+
language: "auto",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (isAbortError(error)) {
|
|
79
|
+
logger.info(`Agent ${incoming.surface} surface speech transcription aborted`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
logger.error(`Agent ${incoming.surface} surface speech transcription failed:\n${getErrorMessage(error)}`);
|
|
83
|
+
yield sink.emit({
|
|
84
|
+
type: "error",
|
|
85
|
+
message: "Speech transcription failed. Check server logs for details.",
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!transcription.text) {
|
|
90
|
+
yield sink.emit({
|
|
91
|
+
type: "error",
|
|
92
|
+
message: "Speech transcription is empty",
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const agentResponse = yield this.handleAgentSurfaceResponse(incoming, sink, adminUser, transcription.text, { emitDone: false });
|
|
97
|
+
if (!agentResponse || agentResponse.aborted || agentResponse.failed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
yield sink.emit({
|
|
101
|
+
type: "done",
|
|
102
|
+
text: agentResponse.text,
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
const speech = yield audioAdapter.synthesize({
|
|
106
|
+
text: sanitizeSpeechText(agentResponse.text),
|
|
107
|
+
stream: false,
|
|
108
|
+
format: "opus",
|
|
109
|
+
});
|
|
110
|
+
yield sink.emit({
|
|
111
|
+
type: "audio",
|
|
112
|
+
audio: speech.audio,
|
|
113
|
+
filename: "agent-response.ogg",
|
|
114
|
+
mimeType: speech.mimeType,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (isAbortError(error)) {
|
|
119
|
+
logger.info(`Agent ${incoming.surface} surface speech synthesis aborted`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
logger.error(`Agent ${incoming.surface} surface speech synthesis failed:\n${getErrorMessage(error)}`);
|
|
82
123
|
yield sink.emit({
|
|
83
124
|
type: "error",
|
|
84
|
-
message:
|
|
125
|
+
message: getErrorMessage(error),
|
|
85
126
|
});
|
|
86
|
-
return true;
|
|
87
127
|
}
|
|
88
|
-
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
handleAgentSurfaceResponse(incoming, sink, adminUser, prompt, options) {
|
|
131
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
+
var _a, _b, _c;
|
|
133
|
+
const emitDone = (_a = options === null || options === void 0 ? void 0 : options.emitDone) !== null && _a !== void 0 ? _a : true;
|
|
134
|
+
const sessionId = yield this.sessionStore.getOrCreateChatSurfaceSession(Object.assign(Object.assign({}, incoming), { prompt }), adminUser);
|
|
135
|
+
if (emitDone) {
|
|
136
|
+
yield this.handleTurn({
|
|
137
|
+
prompt,
|
|
138
|
+
sessionId,
|
|
139
|
+
modeName: incoming.modeName,
|
|
140
|
+
userTimeZone: (_b = incoming.userTimeZone) !== null && _b !== void 0 ? _b : "UTC",
|
|
141
|
+
adminUser,
|
|
142
|
+
emit: this.createEventEmitter(sink),
|
|
143
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
144
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const agentResponse = yield this.runAndPersistAgentResponse({
|
|
149
|
+
prompt,
|
|
150
|
+
sessionId,
|
|
151
|
+
modeName: incoming.modeName,
|
|
152
|
+
userTimeZone: (_c = incoming.userTimeZone) !== null && _c !== void 0 ? _c : "UTC",
|
|
153
|
+
adminUser,
|
|
154
|
+
emit: this.createEventEmitter(sink),
|
|
155
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
156
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
157
|
+
});
|
|
158
|
+
if (agentResponse.failed) {
|
|
159
|
+
yield sink.emit({
|
|
160
|
+
type: "error",
|
|
161
|
+
message: agentResponse.text,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return agentResponse;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
getAdminUserRecordForChatSurface(adapter, incoming) {
|
|
168
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
var _a, _b, _c, _d;
|
|
89
170
|
const adminforth = this.getAdminforth();
|
|
90
171
|
const authResourceId = adminforth.config.auth.usersResourceId;
|
|
172
|
+
const externalIdentityResource = this.options.chatExternalIdentityResource;
|
|
173
|
+
if (!externalIdentityResource) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const surfaceIdentityConfig = externalIdentityResource.surfaces[adapter.name];
|
|
177
|
+
if (!surfaceIdentityConfig) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const providerField = (_a = externalIdentityResource.providerField) !== null && _a !== void 0 ? _a : 'provider';
|
|
181
|
+
const subjectField = (_b = externalIdentityResource.subjectField) !== null && _b !== void 0 ? _b : 'subject';
|
|
182
|
+
const adminUserIdField = (_c = externalIdentityResource.adminUserIdField) !== null && _c !== void 0 ? _c : 'adminUserId';
|
|
183
|
+
const externalUserIdField = (_d = externalIdentityResource.externalUserIdField) !== null && _d !== void 0 ? _d : 'externalUserId';
|
|
184
|
+
const identityFilters = [
|
|
185
|
+
Filters.EQ(providerField, surfaceIdentityConfig.provider),
|
|
186
|
+
Filters.EQ(externalUserIdField, incoming.externalUserId),
|
|
187
|
+
];
|
|
188
|
+
const identities = yield adminforth.resource(externalIdentityResource.resourceId).list(identityFilters);
|
|
189
|
+
const identity = identities.find((identity) => {
|
|
190
|
+
if (String(identity[externalUserIdField]) === incoming.externalUserId) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if (String(identity[subjectField]) === incoming.externalUserId) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
});
|
|
198
|
+
if (!identity) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
91
201
|
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId);
|
|
92
202
|
const primaryKeyField = authResource.columns.find((column) => column.primaryKey).name;
|
|
93
|
-
|
|
94
|
-
Filters.EQ(primaryKeyField,
|
|
203
|
+
return adminforth.resource(authResourceId).get([
|
|
204
|
+
Filters.EQ(primaryKeyField, identity[adminUserIdField]),
|
|
95
205
|
]);
|
|
96
|
-
yield adminforth.resource(authResourceId).update(payload.adminUserId, {
|
|
97
|
-
[externalUserIdField]: Object.assign(Object.assign({}, ((_c = adminUserRecord[externalUserIdField]) !== null && _c !== void 0 ? _c : {})), { [incoming.surface]: incoming.externalUserId }),
|
|
98
|
-
});
|
|
99
|
-
yield sink.emit({
|
|
100
|
-
type: "done",
|
|
101
|
-
text: `${incoming.surface} account connected to AdminForth.`,
|
|
102
|
-
});
|
|
103
|
-
return true;
|
|
104
206
|
});
|
|
105
207
|
}
|
|
106
208
|
handleMessage(adapter, incoming, sink) {
|
|
107
209
|
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
-
var _a, _b;
|
|
109
210
|
if (yield this.handleLink(incoming, sink)) {
|
|
110
211
|
return;
|
|
111
212
|
}
|
|
@@ -113,8 +214,7 @@ export class ChatSurfaceService {
|
|
|
113
214
|
const authResourceId = adminforth.config.auth.usersResourceId;
|
|
114
215
|
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId);
|
|
115
216
|
const primaryKeyField = authResource.columns.find((column) => column.primaryKey).name;
|
|
116
|
-
const
|
|
117
|
-
const adminUserRecord = (yield adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))).find((user) => { var _a; return ((_a = user[externalUserIdField]) === null || _a === void 0 ? void 0 : _a[adapter.name]) === incoming.externalUserId; });
|
|
217
|
+
const adminUserRecord = yield this.getAdminUserRecordForChatSurface(adapter, incoming);
|
|
118
218
|
if (!adminUserRecord) {
|
|
119
219
|
yield sink.emit({
|
|
120
220
|
type: "error",
|
|
@@ -127,16 +227,12 @@ export class ChatSurfaceService {
|
|
|
127
227
|
username: adminUserRecord[adminforth.config.auth.usernameField],
|
|
128
228
|
dbUser: adminUserRecord,
|
|
129
229
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
emit: this.createEventEmitter(sink),
|
|
137
|
-
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
138
|
-
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
139
|
-
});
|
|
230
|
+
const incomingWithAudio = incoming;
|
|
231
|
+
if (incomingWithAudio.audio) {
|
|
232
|
+
yield this.handleAudioMessage(incomingWithAudio, sink, adminUser);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
yield this.handleAgentSurfaceResponse(incoming, sink, adminUser, incoming.prompt);
|
|
140
236
|
});
|
|
141
237
|
}
|
|
142
238
|
}
|