@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,526 @@
|
|
|
1
|
+
import type { AdminUser, AudioAdapter, IAdminForth } from "adminforth";
|
|
2
|
+
import { logger } from "adminforth";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { HumanMessage, SystemMessage } from "langchain";
|
|
5
|
+
import type { BaseCheckpointSaver } from "@langchain/langgraph";
|
|
6
|
+
import { createAgentChatModel, callAgent } from "./agent/simpleAgent.js";
|
|
7
|
+
import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
|
|
8
|
+
import { detectUserLanguage, type PreviousUserMessage } from "./agent/languageDetect.js";
|
|
9
|
+
import { prepareApiBasedTools as buildApiBasedTools } from "./apiBasedTools.js";
|
|
10
|
+
import type { AgentEventEmitter } from "./agentEvents.js";
|
|
11
|
+
import { buildAgentTurnSystemPrompt } from "./agent/systemPrompt.js";
|
|
12
|
+
import type { CurrentPageContext } from "./agent/tools/getUserLocation.js";
|
|
13
|
+
import { isAbortError, getErrorMessage } from "./errors.js";
|
|
14
|
+
import { sanitizeSpeechText } from "./sanitizeSpeechText.js";
|
|
15
|
+
import type { AgentSessionStore } from "./sessionStore.js";
|
|
16
|
+
import type { PluginOptions } from "./types.js";
|
|
17
|
+
|
|
18
|
+
type AgentTurnRunInput = {
|
|
19
|
+
prompt: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
turnId: string;
|
|
22
|
+
previousUserMessages: PreviousUserMessage[];
|
|
23
|
+
modeName?: string | null;
|
|
24
|
+
userTimeZone: string;
|
|
25
|
+
currentPage?: CurrentPageContext;
|
|
26
|
+
abortSignal?: AbortSignal;
|
|
27
|
+
adminUser: AdminUser;
|
|
28
|
+
sequenceDebugCollector: ReturnType<typeof createSequenceDebugCollector>;
|
|
29
|
+
emit?: AgentEventEmitter;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type RunAndPersistAgentResponseInput = {
|
|
33
|
+
prompt: string;
|
|
34
|
+
sessionId: string;
|
|
35
|
+
modeName?: string | null;
|
|
36
|
+
userTimeZone: string;
|
|
37
|
+
currentPage?: CurrentPageContext;
|
|
38
|
+
abortSignal?: AbortSignal;
|
|
39
|
+
adminUser: AdminUser;
|
|
40
|
+
emit?: AgentEventEmitter;
|
|
41
|
+
failureLogMessage: string;
|
|
42
|
+
abortLogMessage: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type RunAndPersistAgentResponseResult = {
|
|
46
|
+
text: string;
|
|
47
|
+
turnId: string;
|
|
48
|
+
aborted: boolean;
|
|
49
|
+
failed: boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type HandleTurnInput = Omit<RunAndPersistAgentResponseInput, "failureLogMessage" | "abortLogMessage"> & {
|
|
53
|
+
emit: AgentEventEmitter;
|
|
54
|
+
failureLogMessage?: string;
|
|
55
|
+
abortLogMessage?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type HandleSpeechTurnInput = Omit<HandleTurnInput, "prompt"> & {
|
|
59
|
+
audioAdapter: AudioAdapter;
|
|
60
|
+
audio: {
|
|
61
|
+
buffer: Buffer;
|
|
62
|
+
filename: string;
|
|
63
|
+
mimeType: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type AgentTurnServiceOptions = {
|
|
68
|
+
getAdminforth: () => IAdminForth;
|
|
69
|
+
getPluginInstanceId: () => string;
|
|
70
|
+
options: PluginOptions;
|
|
71
|
+
sessionStore: AgentSessionStore;
|
|
72
|
+
getCheckpointer: () => BaseCheckpointSaver;
|
|
73
|
+
getInternalAgentResourceIds: () => string[];
|
|
74
|
+
getAgentSystemPrompt: () => Promise<string>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const VEGA_LITE_FENCE_START = "```vega-lite";
|
|
78
|
+
const COMPLETE_VEGA_LITE_BLOCK_RE = /```vega-lite[\s\S]*?```/;
|
|
79
|
+
|
|
80
|
+
export class AgentTurnService {
|
|
81
|
+
constructor(private serviceOptions: AgentTurnServiceOptions) {}
|
|
82
|
+
|
|
83
|
+
private async runAgentTurn(input: AgentTurnRunInput) {
|
|
84
|
+
const adminforth = this.serviceOptions.getAdminforth();
|
|
85
|
+
const options = this.serviceOptions.options;
|
|
86
|
+
let fullResponse = "";
|
|
87
|
+
let bufferedTextDelta = "";
|
|
88
|
+
let isRenderingVegaLite = false;
|
|
89
|
+
const maxTokens = options.maxTokens ?? 1000;
|
|
90
|
+
const selectedMode = options.modes.find((mode) => mode.name === input.modeName) ?? options.modes[0];
|
|
91
|
+
const [primaryModelSpec, summaryModelSpec] = await Promise.all([
|
|
92
|
+
createAgentChatModel({
|
|
93
|
+
adapter: selectedMode.completionAdapter,
|
|
94
|
+
maxTokens,
|
|
95
|
+
purpose: "primary",
|
|
96
|
+
}),
|
|
97
|
+
createAgentChatModel({
|
|
98
|
+
adapter: selectedMode.completionAdapter,
|
|
99
|
+
maxTokens,
|
|
100
|
+
purpose: "summary",
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
const model = primaryModelSpec.model;
|
|
104
|
+
const summaryModel = summaryModelSpec.model;
|
|
105
|
+
const modelMiddleware = primaryModelSpec.middleware;
|
|
106
|
+
|
|
107
|
+
const userLanguage = await detectUserLanguage(selectedMode.completionAdapter, input.prompt, input.previousUserMessages)
|
|
108
|
+
.catch((error) => {
|
|
109
|
+
if (input.abortSignal?.aborted || isAbortError(error)) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logger.warn(`Failed to detect user language: ${getErrorMessage(error)}`);
|
|
114
|
+
return null;
|
|
115
|
+
});
|
|
116
|
+
const systemPrompt = buildAgentTurnSystemPrompt({
|
|
117
|
+
agentSystemPrompt: await this.serviceOptions.getAgentSystemPrompt(),
|
|
118
|
+
adminUser: input.adminUser,
|
|
119
|
+
usernameField: adminforth.config.auth!.usernameField,
|
|
120
|
+
userLanguage,
|
|
121
|
+
});
|
|
122
|
+
const apiBasedTools = buildApiBasedTools(
|
|
123
|
+
adminforth,
|
|
124
|
+
this.serviceOptions.getInternalAgentResourceIds(),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const stream = await callAgent({
|
|
128
|
+
name: `adminforth-agent-${this.serviceOptions.getPluginInstanceId()}`,
|
|
129
|
+
model,
|
|
130
|
+
summaryModel,
|
|
131
|
+
modelMiddleware,
|
|
132
|
+
checkpointer: this.serviceOptions.getCheckpointer(),
|
|
133
|
+
messages: [
|
|
134
|
+
new SystemMessage(systemPrompt),
|
|
135
|
+
new HumanMessage(input.prompt),
|
|
136
|
+
],
|
|
137
|
+
adminUser: input.adminUser,
|
|
138
|
+
adminforth,
|
|
139
|
+
apiBasedTools,
|
|
140
|
+
customComponentsDir: adminforth.config.customization.customComponentsDir ?? "custom",
|
|
141
|
+
sessionId: input.sessionId,
|
|
142
|
+
turnId: input.turnId,
|
|
143
|
+
currentPage: input.currentPage,
|
|
144
|
+
userTimeZone: input.userTimeZone,
|
|
145
|
+
abortSignal: input.abortSignal,
|
|
146
|
+
emitToolCallEvent: (event) => {
|
|
147
|
+
input.sequenceDebugCollector.handleToolCallEvent(event);
|
|
148
|
+
void input.emit?.({
|
|
149
|
+
type: "tool-call",
|
|
150
|
+
data: event,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
sequenceDebugSink: input.sequenceDebugCollector,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
for await (const rawChunk of stream as AsyncIterable<[any, any]>) {
|
|
157
|
+
if (input.abortSignal?.aborted) {
|
|
158
|
+
throw new DOMException("This operation was aborted", "AbortError");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const [token, metadata] = rawChunk;
|
|
162
|
+
|
|
163
|
+
const nodeName =
|
|
164
|
+
typeof metadata?.langgraph_node === "string"
|
|
165
|
+
? metadata.langgraph_node
|
|
166
|
+
: "";
|
|
167
|
+
|
|
168
|
+
if (nodeName && !["model", "model_request"].includes(nodeName)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const blocks = Array.isArray(token?.contentBlocks)
|
|
173
|
+
? token.contentBlocks
|
|
174
|
+
: Array.isArray(token?.content)
|
|
175
|
+
? token.content
|
|
176
|
+
: [];
|
|
177
|
+
const reasoningDelta = blocks
|
|
178
|
+
.filter((b: any) => b?.type === "reasoning")
|
|
179
|
+
.map((b: any) => String(b.reasoning ?? ""))
|
|
180
|
+
.join("");
|
|
181
|
+
|
|
182
|
+
const textDelta = blocks
|
|
183
|
+
.filter((b: any) => b?.type === "text")
|
|
184
|
+
.map((b: any) => String(b.text ?? ""))
|
|
185
|
+
.join("");
|
|
186
|
+
|
|
187
|
+
if (reasoningDelta) {
|
|
188
|
+
await input.emit?.({
|
|
189
|
+
type: "reasoning-delta",
|
|
190
|
+
delta: reasoningDelta,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (textDelta) {
|
|
195
|
+
fullResponse += textDelta;
|
|
196
|
+
bufferedTextDelta += textDelta;
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
bufferedTextDelta.includes(VEGA_LITE_FENCE_START) &&
|
|
200
|
+
!COMPLETE_VEGA_LITE_BLOCK_RE.test(bufferedTextDelta)
|
|
201
|
+
) {
|
|
202
|
+
if (!isRenderingVegaLite) {
|
|
203
|
+
isRenderingVegaLite = true;
|
|
204
|
+
await input.emit?.({
|
|
205
|
+
type: "rendering",
|
|
206
|
+
phase: "start",
|
|
207
|
+
label: "Rendering...",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isRenderingVegaLite) {
|
|
214
|
+
isRenderingVegaLite = false;
|
|
215
|
+
await input.emit?.({
|
|
216
|
+
type: "rendering",
|
|
217
|
+
phase: "end",
|
|
218
|
+
label: "Rendering...",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const streamableLength = bufferedTextDelta.includes(VEGA_LITE_FENCE_START)
|
|
223
|
+
? bufferedTextDelta.length
|
|
224
|
+
: bufferedTextDelta.length - getPartialVegaLiteFenceStartLength(bufferedTextDelta);
|
|
225
|
+
|
|
226
|
+
if (!streamableLength) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await input.emit?.({
|
|
231
|
+
type: "text-delta",
|
|
232
|
+
delta: bufferedTextDelta.slice(0, streamableLength),
|
|
233
|
+
});
|
|
234
|
+
bufferedTextDelta = bufferedTextDelta.slice(streamableLength);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (isRenderingVegaLite) {
|
|
239
|
+
await input.emit?.({
|
|
240
|
+
type: "rendering",
|
|
241
|
+
phase: "end",
|
|
242
|
+
label: "Rendering...",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (bufferedTextDelta) {
|
|
247
|
+
await input.emit?.({
|
|
248
|
+
type: "text-delta",
|
|
249
|
+
delta: bufferedTextDelta,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
text: fullResponse,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput) {
|
|
259
|
+
const adminforth = this.serviceOptions.getAdminforth();
|
|
260
|
+
const options = this.serviceOptions.options;
|
|
261
|
+
const previousUserMessages = await this.serviceOptions.sessionStore.getPreviousUserMessages(input.sessionId);
|
|
262
|
+
const turnId = await this.serviceOptions.sessionStore.createNewTurn(input.sessionId, input.prompt);
|
|
263
|
+
await adminforth.resource(options.sessionResource.resourceId).update(input.sessionId, {
|
|
264
|
+
[options.sessionResource.createdAtField]: new Date().toISOString(),
|
|
265
|
+
});
|
|
266
|
+
const sequenceDebugCollector = createSequenceDebugCollector();
|
|
267
|
+
let fullResponse = "";
|
|
268
|
+
let aborted = false;
|
|
269
|
+
let failed = false;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const agentResponse = await this.runAgentTurn({
|
|
273
|
+
prompt: input.prompt,
|
|
274
|
+
sessionId: input.sessionId,
|
|
275
|
+
turnId,
|
|
276
|
+
previousUserMessages,
|
|
277
|
+
modeName: input.modeName,
|
|
278
|
+
userTimeZone: input.userTimeZone,
|
|
279
|
+
currentPage: input.currentPage,
|
|
280
|
+
abortSignal: input.abortSignal,
|
|
281
|
+
adminUser: input.adminUser,
|
|
282
|
+
sequenceDebugCollector,
|
|
283
|
+
emit: input.emit,
|
|
284
|
+
});
|
|
285
|
+
fullResponse = agentResponse.text;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (input.abortSignal?.aborted || isAbortError(error)) {
|
|
288
|
+
aborted = true;
|
|
289
|
+
logger.info(input.abortLogMessage);
|
|
290
|
+
} else {
|
|
291
|
+
failed = true;
|
|
292
|
+
fullResponse = getErrorMessage(error);
|
|
293
|
+
logger.error(`${input.failureLogMessage}:\n${fullResponse}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
sequenceDebugCollector.flush();
|
|
298
|
+
const turnUpdates: Record<string, unknown> = {
|
|
299
|
+
[options.turnResource.responseField]: fullResponse,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (options.turnResource.debugField) {
|
|
303
|
+
turnUpdates[options.turnResource.debugField] = sequenceDebugCollector.getHistory();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await adminforth.resource(options.turnResource.resourceId).update(turnId, turnUpdates);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
text: fullResponse,
|
|
310
|
+
turnId,
|
|
311
|
+
aborted,
|
|
312
|
+
failed,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async handleTurn(input: HandleTurnInput) {
|
|
317
|
+
await input.emit({
|
|
318
|
+
type: "turn-started",
|
|
319
|
+
messageId: randomUUID(),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const agentResponse = await this.runAndPersistAgentResponse({
|
|
323
|
+
prompt: input.prompt,
|
|
324
|
+
sessionId: input.sessionId,
|
|
325
|
+
modeName: input.modeName,
|
|
326
|
+
userTimeZone: input.userTimeZone,
|
|
327
|
+
currentPage: input.currentPage,
|
|
328
|
+
abortSignal: input.abortSignal,
|
|
329
|
+
adminUser: input.adminUser,
|
|
330
|
+
emit: input.emit,
|
|
331
|
+
failureLogMessage: input.failureLogMessage ?? "Agent response failed",
|
|
332
|
+
abortLogMessage: input.abortLogMessage ?? "Agent response aborted",
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (agentResponse.failed) {
|
|
336
|
+
await input.emit({
|
|
337
|
+
type: "error",
|
|
338
|
+
error: agentResponse.text,
|
|
339
|
+
});
|
|
340
|
+
} else if (!agentResponse.aborted) {
|
|
341
|
+
await input.emit({
|
|
342
|
+
type: "response",
|
|
343
|
+
text: agentResponse.text,
|
|
344
|
+
sessionId: input.sessionId,
|
|
345
|
+
turnId: agentResponse.turnId,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await input.emit({
|
|
350
|
+
type: "finish",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return agentResponse;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async handleSpeechTurn(input: HandleSpeechTurnInput) {
|
|
357
|
+
let transcription;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
transcription = await input.audioAdapter.transcribe({
|
|
361
|
+
buffer: input.audio.buffer,
|
|
362
|
+
filename: input.audio.filename,
|
|
363
|
+
mimeType: input.audio.mimeType,
|
|
364
|
+
language: "auto",
|
|
365
|
+
abortSignal: input.abortSignal,
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (input.abortSignal?.aborted || isAbortError(error)) {
|
|
369
|
+
logger.info("Agent speech transcription aborted by the client");
|
|
370
|
+
await input.emit({ type: "finish" });
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
logger.error(`Agent speech transcription failed:\n${getErrorMessage(error)}`);
|
|
375
|
+
await input.emit({
|
|
376
|
+
type: "error",
|
|
377
|
+
error: "Speech transcription failed. Check server logs for details.",
|
|
378
|
+
});
|
|
379
|
+
await input.emit({ type: "finish" });
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (input.abortSignal?.aborted) {
|
|
384
|
+
await input.emit({ type: "finish" });
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const prompt = transcription.text;
|
|
389
|
+
if (!prompt) {
|
|
390
|
+
await input.emit({
|
|
391
|
+
type: "error",
|
|
392
|
+
error: "Speech transcription is empty",
|
|
393
|
+
});
|
|
394
|
+
await input.emit({ type: "finish" });
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await input.emit({
|
|
399
|
+
type: "transcript",
|
|
400
|
+
text: transcription.text,
|
|
401
|
+
language: transcription.language,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const agentResponse = await this.runAndPersistAgentResponse({
|
|
405
|
+
prompt,
|
|
406
|
+
sessionId: input.sessionId,
|
|
407
|
+
modeName: input.modeName,
|
|
408
|
+
userTimeZone: input.userTimeZone,
|
|
409
|
+
currentPage: input.currentPage,
|
|
410
|
+
abortSignal: input.abortSignal,
|
|
411
|
+
adminUser: input.adminUser,
|
|
412
|
+
emit: async (event) => {
|
|
413
|
+
if (event.type === "tool-call") {
|
|
414
|
+
await input.emit(event);
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
failureLogMessage: input.failureLogMessage ?? "Agent speech response failed",
|
|
418
|
+
abortLogMessage: input.abortLogMessage ?? "Agent speech response aborted by the client",
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (agentResponse.aborted) {
|
|
422
|
+
await input.emit({ type: "finish" });
|
|
423
|
+
return agentResponse;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (agentResponse.failed) {
|
|
427
|
+
await input.emit({
|
|
428
|
+
type: "error",
|
|
429
|
+
error: agentResponse.text,
|
|
430
|
+
});
|
|
431
|
+
await input.emit({ type: "finish" });
|
|
432
|
+
return agentResponse;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await input.emit({
|
|
437
|
+
type: "speech-response",
|
|
438
|
+
transcript: {
|
|
439
|
+
text: transcription.text,
|
|
440
|
+
language: transcription.language,
|
|
441
|
+
},
|
|
442
|
+
response: {
|
|
443
|
+
text: agentResponse.text,
|
|
444
|
+
},
|
|
445
|
+
sessionId: input.sessionId,
|
|
446
|
+
turnId: agentResponse.turnId,
|
|
447
|
+
});
|
|
448
|
+
const speech = await input.audioAdapter.synthesize({
|
|
449
|
+
text: sanitizeSpeechText(agentResponse.text),
|
|
450
|
+
stream: true,
|
|
451
|
+
streamFormat: "audio",
|
|
452
|
+
format: "pcm",
|
|
453
|
+
abortSignal: input.abortSignal,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await input.emit({
|
|
457
|
+
type: "audio-start",
|
|
458
|
+
mimeType: speech.mimeType,
|
|
459
|
+
format: speech.format,
|
|
460
|
+
sampleRate: 24000,
|
|
461
|
+
channelCount: 1,
|
|
462
|
+
bitsPerSample: 16,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const reader = speech.audioStream.getReader();
|
|
466
|
+
const cancelAudioStream = () => {
|
|
467
|
+
void reader.cancel().catch(() => undefined);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
input.abortSignal?.addEventListener("abort", cancelAudioStream, { once: true });
|
|
472
|
+
|
|
473
|
+
while (true) {
|
|
474
|
+
if (input.abortSignal?.aborted) {
|
|
475
|
+
await reader.cancel().catch(() => undefined);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const { value, done } = await reader.read();
|
|
480
|
+
|
|
481
|
+
if (done) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (input.abortSignal?.aborted) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
await input.emit({
|
|
490
|
+
type: "audio-delta",
|
|
491
|
+
value,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
} finally {
|
|
495
|
+
input.abortSignal?.removeEventListener("abort", cancelAudioStream);
|
|
496
|
+
reader.releaseLock();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await input.emit({ type: "audio-done" });
|
|
500
|
+
await input.emit({ type: "finish" });
|
|
501
|
+
return agentResponse;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (input.abortSignal?.aborted || isAbortError(error)) {
|
|
504
|
+
logger.info("Agent speech audio streaming aborted by the client");
|
|
505
|
+
} else {
|
|
506
|
+
logger.error(`Agent speech audio streaming failed:\n${getErrorMessage(error)}`);
|
|
507
|
+
await input.emit({
|
|
508
|
+
type: "error",
|
|
509
|
+
error: getErrorMessage(error),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
await input.emit({ type: "finish" });
|
|
513
|
+
return agentResponse;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function getPartialVegaLiteFenceStartLength(text: string): number {
|
|
519
|
+
for (let length = Math.min(text.length, VEGA_LITE_FENCE_START.length - 1); length > 0; length -= 1) {
|
|
520
|
+
if (VEGA_LITE_FENCE_START.startsWith(text.slice(-length))) {
|
|
521
|
+
return length;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
package/build.log
CHANGED
|
@@ -63,5 +63,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
|
|
|
63
63
|
custom/speech_recognition_frontend/types/
|
|
64
64
|
custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
|
|
65
65
|
|
|
66
|
-
sent 1,671,
|
|
66
|
+
sent 1,671,664 bytes received 936 bytes 3,345,200.00 bytes/sec
|
|
67
67
|
total size is 1,667,436 speedup is 1.00
|