@adminforth/agent 1.45.0 → 1.45.1
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/agentTurnService.ts +526 -0
- package/build.log +1 -1
- package/chatSurfaceService.ts +189 -0
- package/dist/agentTurnService.d.ts +70 -0
- package/dist/agentTurnService.js +453 -0
- package/dist/chatSurfaceService.d.ts +29 -0
- package/dist/chatSurfaceService.js +142 -0
- package/dist/endpoints/chatSurfaces.d.ts +3 -0
- package/dist/endpoints/chatSurfaces.js +91 -0
- package/dist/endpoints/context.d.ts +30 -0
- package/dist/endpoints/context.js +1 -0
- package/dist/endpoints/core.d.ts +3 -0
- package/dist/endpoints/core.js +106 -0
- package/dist/endpoints/sessions.d.ts +3 -0
- package/dist/endpoints/sessions.js +177 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +4 -47
- package/dist/index.js +37 -917
- package/dist/sessionStore.d.ts +19 -0
- package/dist/sessionStore.js +83 -0
- package/endpoints/chatSurfaces.ts +93 -0
- package/endpoints/context.ts +66 -0
- package/endpoints/core.ts +113 -0
- package/endpoints/sessions.ts +183 -0
- package/errors.ts +10 -0
- package/index.ts +47 -1053
- package/package.json +1 -1
- package/sessionStore.ts +94 -0
- package/agentResponseEvents.ts +0 -1
- package/dist/agentResponseEvents.d.ts +0 -1
- package/dist/agentResponseEvents.js +0 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
11
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
12
|
+
var m = o[Symbol.asyncIterator], i;
|
|
13
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
14
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
15
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
16
|
+
};
|
|
17
|
+
import { logger } from "adminforth";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
import { HumanMessage, SystemMessage } from "langchain";
|
|
20
|
+
import { createAgentChatModel, callAgent } from "./agent/simpleAgent.js";
|
|
21
|
+
import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
|
|
22
|
+
import { detectUserLanguage } from "./agent/languageDetect.js";
|
|
23
|
+
import { prepareApiBasedTools as buildApiBasedTools } from "./apiBasedTools.js";
|
|
24
|
+
import { buildAgentTurnSystemPrompt } from "./agent/systemPrompt.js";
|
|
25
|
+
import { isAbortError, getErrorMessage } from "./errors.js";
|
|
26
|
+
import { sanitizeSpeechText } from "./sanitizeSpeechText.js";
|
|
27
|
+
const VEGA_LITE_FENCE_START = "```vega-lite";
|
|
28
|
+
const COMPLETE_VEGA_LITE_BLOCK_RE = /```vega-lite[\s\S]*?```/;
|
|
29
|
+
export class AgentTurnService {
|
|
30
|
+
constructor(serviceOptions) {
|
|
31
|
+
this.serviceOptions = serviceOptions;
|
|
32
|
+
}
|
|
33
|
+
runAgentTurn(input) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
var _a, e_1, _b, _c;
|
|
36
|
+
var _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
37
|
+
const adminforth = this.serviceOptions.getAdminforth();
|
|
38
|
+
const options = this.serviceOptions.options;
|
|
39
|
+
let fullResponse = "";
|
|
40
|
+
let bufferedTextDelta = "";
|
|
41
|
+
let isRenderingVegaLite = false;
|
|
42
|
+
const maxTokens = (_d = options.maxTokens) !== null && _d !== void 0 ? _d : 1000;
|
|
43
|
+
const selectedMode = (_e = options.modes.find((mode) => mode.name === input.modeName)) !== null && _e !== void 0 ? _e : options.modes[0];
|
|
44
|
+
const [primaryModelSpec, summaryModelSpec] = yield Promise.all([
|
|
45
|
+
createAgentChatModel({
|
|
46
|
+
adapter: selectedMode.completionAdapter,
|
|
47
|
+
maxTokens,
|
|
48
|
+
purpose: "primary",
|
|
49
|
+
}),
|
|
50
|
+
createAgentChatModel({
|
|
51
|
+
adapter: selectedMode.completionAdapter,
|
|
52
|
+
maxTokens,
|
|
53
|
+
purpose: "summary",
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
const model = primaryModelSpec.model;
|
|
57
|
+
const summaryModel = summaryModelSpec.model;
|
|
58
|
+
const modelMiddleware = primaryModelSpec.middleware;
|
|
59
|
+
const userLanguage = yield detectUserLanguage(selectedMode.completionAdapter, input.prompt, input.previousUserMessages)
|
|
60
|
+
.catch((error) => {
|
|
61
|
+
var _a;
|
|
62
|
+
if (((_a = input.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) || isAbortError(error)) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
logger.warn(`Failed to detect user language: ${getErrorMessage(error)}`);
|
|
66
|
+
return null;
|
|
67
|
+
});
|
|
68
|
+
const systemPrompt = buildAgentTurnSystemPrompt({
|
|
69
|
+
agentSystemPrompt: yield this.serviceOptions.getAgentSystemPrompt(),
|
|
70
|
+
adminUser: input.adminUser,
|
|
71
|
+
usernameField: adminforth.config.auth.usernameField,
|
|
72
|
+
userLanguage,
|
|
73
|
+
});
|
|
74
|
+
const apiBasedTools = buildApiBasedTools(adminforth, this.serviceOptions.getInternalAgentResourceIds());
|
|
75
|
+
const stream = yield callAgent({
|
|
76
|
+
name: `adminforth-agent-${this.serviceOptions.getPluginInstanceId()}`,
|
|
77
|
+
model,
|
|
78
|
+
summaryModel,
|
|
79
|
+
modelMiddleware,
|
|
80
|
+
checkpointer: this.serviceOptions.getCheckpointer(),
|
|
81
|
+
messages: [
|
|
82
|
+
new SystemMessage(systemPrompt),
|
|
83
|
+
new HumanMessage(input.prompt),
|
|
84
|
+
],
|
|
85
|
+
adminUser: input.adminUser,
|
|
86
|
+
adminforth,
|
|
87
|
+
apiBasedTools,
|
|
88
|
+
customComponentsDir: (_f = adminforth.config.customization.customComponentsDir) !== null && _f !== void 0 ? _f : "custom",
|
|
89
|
+
sessionId: input.sessionId,
|
|
90
|
+
turnId: input.turnId,
|
|
91
|
+
currentPage: input.currentPage,
|
|
92
|
+
userTimeZone: input.userTimeZone,
|
|
93
|
+
abortSignal: input.abortSignal,
|
|
94
|
+
emitToolCallEvent: (event) => {
|
|
95
|
+
var _a;
|
|
96
|
+
input.sequenceDebugCollector.handleToolCallEvent(event);
|
|
97
|
+
void ((_a = input.emit) === null || _a === void 0 ? void 0 : _a.call(input, {
|
|
98
|
+
type: "tool-call",
|
|
99
|
+
data: event,
|
|
100
|
+
}));
|
|
101
|
+
},
|
|
102
|
+
sequenceDebugSink: input.sequenceDebugCollector,
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
for (var _p = true, _q = __asyncValues(stream), _r; _r = yield _q.next(), _a = _r.done, !_a; _p = true) {
|
|
106
|
+
_c = _r.value;
|
|
107
|
+
_p = false;
|
|
108
|
+
const rawChunk = _c;
|
|
109
|
+
if ((_g = input.abortSignal) === null || _g === void 0 ? void 0 : _g.aborted) {
|
|
110
|
+
throw new DOMException("This operation was aborted", "AbortError");
|
|
111
|
+
}
|
|
112
|
+
const [token, metadata] = rawChunk;
|
|
113
|
+
const nodeName = typeof (metadata === null || metadata === void 0 ? void 0 : metadata.langgraph_node) === "string"
|
|
114
|
+
? metadata.langgraph_node
|
|
115
|
+
: "";
|
|
116
|
+
if (nodeName && !["model", "model_request"].includes(nodeName)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const blocks = Array.isArray(token === null || token === void 0 ? void 0 : token.contentBlocks)
|
|
120
|
+
? token.contentBlocks
|
|
121
|
+
: Array.isArray(token === null || token === void 0 ? void 0 : token.content)
|
|
122
|
+
? token.content
|
|
123
|
+
: [];
|
|
124
|
+
const reasoningDelta = blocks
|
|
125
|
+
.filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "reasoning")
|
|
126
|
+
.map((b) => { var _a; return String((_a = b.reasoning) !== null && _a !== void 0 ? _a : ""); })
|
|
127
|
+
.join("");
|
|
128
|
+
const textDelta = blocks
|
|
129
|
+
.filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "text")
|
|
130
|
+
.map((b) => { var _a; return String((_a = b.text) !== null && _a !== void 0 ? _a : ""); })
|
|
131
|
+
.join("");
|
|
132
|
+
if (reasoningDelta) {
|
|
133
|
+
yield ((_h = input.emit) === null || _h === void 0 ? void 0 : _h.call(input, {
|
|
134
|
+
type: "reasoning-delta",
|
|
135
|
+
delta: reasoningDelta,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
if (textDelta) {
|
|
139
|
+
fullResponse += textDelta;
|
|
140
|
+
bufferedTextDelta += textDelta;
|
|
141
|
+
if (bufferedTextDelta.includes(VEGA_LITE_FENCE_START) &&
|
|
142
|
+
!COMPLETE_VEGA_LITE_BLOCK_RE.test(bufferedTextDelta)) {
|
|
143
|
+
if (!isRenderingVegaLite) {
|
|
144
|
+
isRenderingVegaLite = true;
|
|
145
|
+
yield ((_j = input.emit) === null || _j === void 0 ? void 0 : _j.call(input, {
|
|
146
|
+
type: "rendering",
|
|
147
|
+
phase: "start",
|
|
148
|
+
label: "Rendering...",
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (isRenderingVegaLite) {
|
|
154
|
+
isRenderingVegaLite = false;
|
|
155
|
+
yield ((_k = input.emit) === null || _k === void 0 ? void 0 : _k.call(input, {
|
|
156
|
+
type: "rendering",
|
|
157
|
+
phase: "end",
|
|
158
|
+
label: "Rendering...",
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
const streamableLength = bufferedTextDelta.includes(VEGA_LITE_FENCE_START)
|
|
162
|
+
? bufferedTextDelta.length
|
|
163
|
+
: bufferedTextDelta.length - getPartialVegaLiteFenceStartLength(bufferedTextDelta);
|
|
164
|
+
if (!streamableLength) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
yield ((_l = input.emit) === null || _l === void 0 ? void 0 : _l.call(input, {
|
|
168
|
+
type: "text-delta",
|
|
169
|
+
delta: bufferedTextDelta.slice(0, streamableLength),
|
|
170
|
+
}));
|
|
171
|
+
bufferedTextDelta = bufferedTextDelta.slice(streamableLength);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
176
|
+
finally {
|
|
177
|
+
try {
|
|
178
|
+
if (!_p && !_a && (_b = _q.return)) yield _b.call(_q);
|
|
179
|
+
}
|
|
180
|
+
finally { if (e_1) throw e_1.error; }
|
|
181
|
+
}
|
|
182
|
+
if (isRenderingVegaLite) {
|
|
183
|
+
yield ((_m = input.emit) === null || _m === void 0 ? void 0 : _m.call(input, {
|
|
184
|
+
type: "rendering",
|
|
185
|
+
phase: "end",
|
|
186
|
+
label: "Rendering...",
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
if (bufferedTextDelta) {
|
|
190
|
+
yield ((_o = input.emit) === null || _o === void 0 ? void 0 : _o.call(input, {
|
|
191
|
+
type: "text-delta",
|
|
192
|
+
delta: bufferedTextDelta,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
text: fullResponse,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
runAndPersistAgentResponse(input) {
|
|
201
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
202
|
+
var _a;
|
|
203
|
+
const adminforth = this.serviceOptions.getAdminforth();
|
|
204
|
+
const options = this.serviceOptions.options;
|
|
205
|
+
const previousUserMessages = yield this.serviceOptions.sessionStore.getPreviousUserMessages(input.sessionId);
|
|
206
|
+
const turnId = yield this.serviceOptions.sessionStore.createNewTurn(input.sessionId, input.prompt);
|
|
207
|
+
yield adminforth.resource(options.sessionResource.resourceId).update(input.sessionId, {
|
|
208
|
+
[options.sessionResource.createdAtField]: new Date().toISOString(),
|
|
209
|
+
});
|
|
210
|
+
const sequenceDebugCollector = createSequenceDebugCollector();
|
|
211
|
+
let fullResponse = "";
|
|
212
|
+
let aborted = false;
|
|
213
|
+
let failed = false;
|
|
214
|
+
try {
|
|
215
|
+
const agentResponse = yield this.runAgentTurn({
|
|
216
|
+
prompt: input.prompt,
|
|
217
|
+
sessionId: input.sessionId,
|
|
218
|
+
turnId,
|
|
219
|
+
previousUserMessages,
|
|
220
|
+
modeName: input.modeName,
|
|
221
|
+
userTimeZone: input.userTimeZone,
|
|
222
|
+
currentPage: input.currentPage,
|
|
223
|
+
abortSignal: input.abortSignal,
|
|
224
|
+
adminUser: input.adminUser,
|
|
225
|
+
sequenceDebugCollector,
|
|
226
|
+
emit: input.emit,
|
|
227
|
+
});
|
|
228
|
+
fullResponse = agentResponse.text;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (((_a = input.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) || isAbortError(error)) {
|
|
232
|
+
aborted = true;
|
|
233
|
+
logger.info(input.abortLogMessage);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
failed = true;
|
|
237
|
+
fullResponse = getErrorMessage(error);
|
|
238
|
+
logger.error(`${input.failureLogMessage}:\n${fullResponse}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
sequenceDebugCollector.flush();
|
|
242
|
+
const turnUpdates = {
|
|
243
|
+
[options.turnResource.responseField]: fullResponse,
|
|
244
|
+
};
|
|
245
|
+
if (options.turnResource.debugField) {
|
|
246
|
+
turnUpdates[options.turnResource.debugField] = sequenceDebugCollector.getHistory();
|
|
247
|
+
}
|
|
248
|
+
yield adminforth.resource(options.turnResource.resourceId).update(turnId, turnUpdates);
|
|
249
|
+
return {
|
|
250
|
+
text: fullResponse,
|
|
251
|
+
turnId,
|
|
252
|
+
aborted,
|
|
253
|
+
failed,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
handleTurn(input) {
|
|
258
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
259
|
+
var _a, _b;
|
|
260
|
+
yield input.emit({
|
|
261
|
+
type: "turn-started",
|
|
262
|
+
messageId: randomUUID(),
|
|
263
|
+
});
|
|
264
|
+
const agentResponse = yield this.runAndPersistAgentResponse({
|
|
265
|
+
prompt: input.prompt,
|
|
266
|
+
sessionId: input.sessionId,
|
|
267
|
+
modeName: input.modeName,
|
|
268
|
+
userTimeZone: input.userTimeZone,
|
|
269
|
+
currentPage: input.currentPage,
|
|
270
|
+
abortSignal: input.abortSignal,
|
|
271
|
+
adminUser: input.adminUser,
|
|
272
|
+
emit: input.emit,
|
|
273
|
+
failureLogMessage: (_a = input.failureLogMessage) !== null && _a !== void 0 ? _a : "Agent response failed",
|
|
274
|
+
abortLogMessage: (_b = input.abortLogMessage) !== null && _b !== void 0 ? _b : "Agent response aborted",
|
|
275
|
+
});
|
|
276
|
+
if (agentResponse.failed) {
|
|
277
|
+
yield input.emit({
|
|
278
|
+
type: "error",
|
|
279
|
+
error: agentResponse.text,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
else if (!agentResponse.aborted) {
|
|
283
|
+
yield input.emit({
|
|
284
|
+
type: "response",
|
|
285
|
+
text: agentResponse.text,
|
|
286
|
+
sessionId: input.sessionId,
|
|
287
|
+
turnId: agentResponse.turnId,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
yield input.emit({
|
|
291
|
+
type: "finish",
|
|
292
|
+
});
|
|
293
|
+
return agentResponse;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
handleSpeechTurn(input) {
|
|
297
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
298
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
299
|
+
let transcription;
|
|
300
|
+
try {
|
|
301
|
+
transcription = yield input.audioAdapter.transcribe({
|
|
302
|
+
buffer: input.audio.buffer,
|
|
303
|
+
filename: input.audio.filename,
|
|
304
|
+
mimeType: input.audio.mimeType,
|
|
305
|
+
language: "auto",
|
|
306
|
+
abortSignal: input.abortSignal,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (((_a = input.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) || isAbortError(error)) {
|
|
311
|
+
logger.info("Agent speech transcription aborted by the client");
|
|
312
|
+
yield input.emit({ type: "finish" });
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
logger.error(`Agent speech transcription failed:\n${getErrorMessage(error)}`);
|
|
316
|
+
yield input.emit({
|
|
317
|
+
type: "error",
|
|
318
|
+
error: "Speech transcription failed. Check server logs for details.",
|
|
319
|
+
});
|
|
320
|
+
yield input.emit({ type: "finish" });
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
if ((_b = input.abortSignal) === null || _b === void 0 ? void 0 : _b.aborted) {
|
|
324
|
+
yield input.emit({ type: "finish" });
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const prompt = transcription.text;
|
|
328
|
+
if (!prompt) {
|
|
329
|
+
yield input.emit({
|
|
330
|
+
type: "error",
|
|
331
|
+
error: "Speech transcription is empty",
|
|
332
|
+
});
|
|
333
|
+
yield input.emit({ type: "finish" });
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
yield input.emit({
|
|
337
|
+
type: "transcript",
|
|
338
|
+
text: transcription.text,
|
|
339
|
+
language: transcription.language,
|
|
340
|
+
});
|
|
341
|
+
const agentResponse = yield this.runAndPersistAgentResponse({
|
|
342
|
+
prompt,
|
|
343
|
+
sessionId: input.sessionId,
|
|
344
|
+
modeName: input.modeName,
|
|
345
|
+
userTimeZone: input.userTimeZone,
|
|
346
|
+
currentPage: input.currentPage,
|
|
347
|
+
abortSignal: input.abortSignal,
|
|
348
|
+
adminUser: input.adminUser,
|
|
349
|
+
emit: (event) => __awaiter(this, void 0, void 0, function* () {
|
|
350
|
+
if (event.type === "tool-call") {
|
|
351
|
+
yield input.emit(event);
|
|
352
|
+
}
|
|
353
|
+
}),
|
|
354
|
+
failureLogMessage: (_c = input.failureLogMessage) !== null && _c !== void 0 ? _c : "Agent speech response failed",
|
|
355
|
+
abortLogMessage: (_d = input.abortLogMessage) !== null && _d !== void 0 ? _d : "Agent speech response aborted by the client",
|
|
356
|
+
});
|
|
357
|
+
if (agentResponse.aborted) {
|
|
358
|
+
yield input.emit({ type: "finish" });
|
|
359
|
+
return agentResponse;
|
|
360
|
+
}
|
|
361
|
+
if (agentResponse.failed) {
|
|
362
|
+
yield input.emit({
|
|
363
|
+
type: "error",
|
|
364
|
+
error: agentResponse.text,
|
|
365
|
+
});
|
|
366
|
+
yield input.emit({ type: "finish" });
|
|
367
|
+
return agentResponse;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
yield input.emit({
|
|
371
|
+
type: "speech-response",
|
|
372
|
+
transcript: {
|
|
373
|
+
text: transcription.text,
|
|
374
|
+
language: transcription.language,
|
|
375
|
+
},
|
|
376
|
+
response: {
|
|
377
|
+
text: agentResponse.text,
|
|
378
|
+
},
|
|
379
|
+
sessionId: input.sessionId,
|
|
380
|
+
turnId: agentResponse.turnId,
|
|
381
|
+
});
|
|
382
|
+
const speech = yield input.audioAdapter.synthesize({
|
|
383
|
+
text: sanitizeSpeechText(agentResponse.text),
|
|
384
|
+
stream: true,
|
|
385
|
+
streamFormat: "audio",
|
|
386
|
+
format: "pcm",
|
|
387
|
+
abortSignal: input.abortSignal,
|
|
388
|
+
});
|
|
389
|
+
yield input.emit({
|
|
390
|
+
type: "audio-start",
|
|
391
|
+
mimeType: speech.mimeType,
|
|
392
|
+
format: speech.format,
|
|
393
|
+
sampleRate: 24000,
|
|
394
|
+
channelCount: 1,
|
|
395
|
+
bitsPerSample: 16,
|
|
396
|
+
});
|
|
397
|
+
const reader = speech.audioStream.getReader();
|
|
398
|
+
const cancelAudioStream = () => {
|
|
399
|
+
void reader.cancel().catch(() => undefined);
|
|
400
|
+
};
|
|
401
|
+
try {
|
|
402
|
+
(_e = input.abortSignal) === null || _e === void 0 ? void 0 : _e.addEventListener("abort", cancelAudioStream, { once: true });
|
|
403
|
+
while (true) {
|
|
404
|
+
if ((_f = input.abortSignal) === null || _f === void 0 ? void 0 : _f.aborted) {
|
|
405
|
+
yield reader.cancel().catch(() => undefined);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
const { value, done } = yield reader.read();
|
|
409
|
+
if (done) {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if ((_g = input.abortSignal) === null || _g === void 0 ? void 0 : _g.aborted) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
yield input.emit({
|
|
416
|
+
type: "audio-delta",
|
|
417
|
+
value,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
(_h = input.abortSignal) === null || _h === void 0 ? void 0 : _h.removeEventListener("abort", cancelAudioStream);
|
|
423
|
+
reader.releaseLock();
|
|
424
|
+
}
|
|
425
|
+
yield input.emit({ type: "audio-done" });
|
|
426
|
+
yield input.emit({ type: "finish" });
|
|
427
|
+
return agentResponse;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
if (((_j = input.abortSignal) === null || _j === void 0 ? void 0 : _j.aborted) || isAbortError(error)) {
|
|
431
|
+
logger.info("Agent speech audio streaming aborted by the client");
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
logger.error(`Agent speech audio streaming failed:\n${getErrorMessage(error)}`);
|
|
435
|
+
yield input.emit({
|
|
436
|
+
type: "error",
|
|
437
|
+
error: getErrorMessage(error),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
yield input.emit({ type: "finish" });
|
|
441
|
+
return agentResponse;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function getPartialVegaLiteFenceStartLength(text) {
|
|
447
|
+
for (let length = Math.min(text.length, VEGA_LITE_FENCE_START.length - 1); length > 0; length -= 1) {
|
|
448
|
+
if (VEGA_LITE_FENCE_START.startsWith(text.slice(-length))) {
|
|
449
|
+
return length;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AdminUser, ChatSurfaceAdapter, ChatSurfaceEventSink, ChatSurfaceIncomingMessage, IAdminForth } from "adminforth";
|
|
2
|
+
import type { HandleTurnInput } from "./agentTurnService.js";
|
|
3
|
+
import type { PluginOptions } from "./types.js";
|
|
4
|
+
import type { AgentSessionStore } from "./sessionStore.js";
|
|
5
|
+
type ChatSurfaceConnectAction = {
|
|
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
|
+
};
|
|
15
|
+
export declare class ChatSurfaceService {
|
|
16
|
+
private getAdminforth;
|
|
17
|
+
private options;
|
|
18
|
+
private sessionStore;
|
|
19
|
+
private handleTurn;
|
|
20
|
+
private linkTokens;
|
|
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;
|
|
25
|
+
private createEventEmitter;
|
|
26
|
+
private handleLink;
|
|
27
|
+
handleMessage(adapter: ChatSurfaceAdapter, incoming: ChatSurfaceIncomingMessage, sink: ChatSurfaceEventSink): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { Filters } from "adminforth";
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
|
|
13
|
+
const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
|
|
14
|
+
export class ChatSurfaceService {
|
|
15
|
+
constructor(getAdminforth, options, sessionStore, handleTurn) {
|
|
16
|
+
this.getAdminforth = getAdminforth;
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.sessionStore = sessionStore;
|
|
19
|
+
this.handleTurn = handleTurn;
|
|
20
|
+
this.linkTokens = new Map();
|
|
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;
|
|
49
|
+
}
|
|
50
|
+
createEventEmitter(sink) {
|
|
51
|
+
return (event) => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
if (event.type === "text-delta") {
|
|
53
|
+
yield sink.emit({
|
|
54
|
+
type: "text_delta",
|
|
55
|
+
delta: event.delta,
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === "response") {
|
|
60
|
+
yield sink.emit({
|
|
61
|
+
type: "done",
|
|
62
|
+
text: event.text,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (event.type === "error") {
|
|
67
|
+
yield sink.emit({
|
|
68
|
+
type: "error",
|
|
69
|
+
message: event.error,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
handleLink(incoming, sink) {
|
|
75
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
var _a, _b, _c;
|
|
77
|
+
if (typeof ((_a = incoming.metadata) === null || _a === void 0 ? void 0 : _a.startPayload) !== "string") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const payload = this.consumeLinkToken(incoming.surface, incoming.metadata.startPayload);
|
|
81
|
+
if (!payload) {
|
|
82
|
+
yield sink.emit({
|
|
83
|
+
type: "error",
|
|
84
|
+
message: "This chat surface link is expired or invalid. Please start linking again from AdminForth.",
|
|
85
|
+
});
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const externalUserIdField = (_b = this.options.chatExternalIdsField) !== null && _b !== void 0 ? _b : DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
89
|
+
const adminforth = this.getAdminforth();
|
|
90
|
+
const authResourceId = adminforth.config.auth.usersResourceId;
|
|
91
|
+
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId);
|
|
92
|
+
const primaryKeyField = authResource.columns.find((column) => column.primaryKey).name;
|
|
93
|
+
const adminUserRecord = yield adminforth.resource(authResourceId).get([
|
|
94
|
+
Filters.EQ(primaryKeyField, payload.adminUserId),
|
|
95
|
+
]);
|
|
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
|
+
});
|
|
105
|
+
}
|
|
106
|
+
handleMessage(adapter, incoming, sink) {
|
|
107
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
if (yield this.handleLink(incoming, sink)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const adminforth = this.getAdminforth();
|
|
113
|
+
const authResourceId = adminforth.config.auth.usersResourceId;
|
|
114
|
+
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId);
|
|
115
|
+
const primaryKeyField = authResource.columns.find((column) => column.primaryKey).name;
|
|
116
|
+
const externalUserIdField = (_a = this.options.chatExternalIdsField) !== null && _a !== void 0 ? _a : DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
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; });
|
|
118
|
+
if (!adminUserRecord) {
|
|
119
|
+
yield sink.emit({
|
|
120
|
+
type: "error",
|
|
121
|
+
message: "This chat account is not authorized to use AdminForth Agent.",
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const adminUser = {
|
|
126
|
+
pk: adminUserRecord[primaryKeyField],
|
|
127
|
+
username: adminUserRecord[adminforth.config.auth.usernameField],
|
|
128
|
+
dbUser: adminUserRecord,
|
|
129
|
+
};
|
|
130
|
+
yield this.handleTurn({
|
|
131
|
+
prompt: incoming.prompt,
|
|
132
|
+
sessionId: yield this.sessionStore.getOrCreateChatSurfaceSession(incoming, adminUser),
|
|
133
|
+
modeName: incoming.modeName,
|
|
134
|
+
userTimeZone: (_b = incoming.userTimeZone) !== null && _b !== void 0 ? _b : "UTC",
|
|
135
|
+
adminUser,
|
|
136
|
+
emit: this.createEventEmitter(sink),
|
|
137
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
138
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|