@absolutejs/voice 0.0.22-beta.17 → 0.0.22-beta.18
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/assistantHealth.d.ts +79 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +299 -192
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { type VoiceAssistantRunsSummary } from './assistant';
|
|
3
|
+
import { type VoiceProviderHealthSummary } from './providerHealth';
|
|
4
|
+
import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
|
|
5
|
+
export type VoiceAssistantHealthFailure = {
|
|
6
|
+
at: number;
|
|
7
|
+
assistantId?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
rateLimited?: boolean;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
turnId?: string;
|
|
14
|
+
type: StoredVoiceTraceEvent['type'];
|
|
15
|
+
};
|
|
16
|
+
export type VoiceAssistantHealthSummary<TProvider extends string = string> = {
|
|
17
|
+
assistantRuns: VoiceAssistantRunsSummary;
|
|
18
|
+
providerHealth: VoiceProviderHealthSummary<TProvider>[];
|
|
19
|
+
recentFailures: VoiceAssistantHealthFailure[];
|
|
20
|
+
};
|
|
21
|
+
export type VoiceAssistantHealthSummaryOptions<TProvider extends string = string> = {
|
|
22
|
+
events?: StoredVoiceTraceEvent[];
|
|
23
|
+
maxFailures?: number;
|
|
24
|
+
providers?: readonly TProvider[];
|
|
25
|
+
store?: VoiceTraceEventStore;
|
|
26
|
+
};
|
|
27
|
+
export type VoiceAssistantHealthHTMLHandlerOptions<TProvider extends string = string> = VoiceAssistantHealthSummaryOptions<TProvider> & {
|
|
28
|
+
headers?: HeadersInit;
|
|
29
|
+
render?: (summary: VoiceAssistantHealthSummary<TProvider>) => string | Promise<string>;
|
|
30
|
+
};
|
|
31
|
+
export type VoiceAssistantHealthRoutesOptions<TProvider extends string = string> = VoiceAssistantHealthHTMLHandlerOptions<TProvider> & {
|
|
32
|
+
htmlPath?: false | string;
|
|
33
|
+
name?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
};
|
|
36
|
+
export declare const summarizeVoiceAssistantHealth: <TProvider extends string = string>(options: VoiceAssistantHealthSummaryOptions<TProvider>) => Promise<VoiceAssistantHealthSummary<TProvider>>;
|
|
37
|
+
export declare const renderVoiceAssistantHealthHTML: <TProvider extends string = string>(summary: VoiceAssistantHealthSummary<TProvider>) => string;
|
|
38
|
+
export declare const createVoiceAssistantHealthJSONHandler: <TProvider extends string = string>(options: VoiceAssistantHealthSummaryOptions<TProvider>) => () => Promise<VoiceAssistantHealthSummary<TProvider>>;
|
|
39
|
+
export declare const createVoiceAssistantHealthHTMLHandler: <TProvider extends string = string>(options: VoiceAssistantHealthHTMLHandlerOptions<TProvider>) => () => Promise<Response>;
|
|
40
|
+
export declare const createVoiceAssistantHealthRoutes: <TProvider extends string = string>(options: VoiceAssistantHealthRoutesOptions<TProvider>) => Elysia<"", {
|
|
41
|
+
decorator: {};
|
|
42
|
+
store: {};
|
|
43
|
+
derive: {};
|
|
44
|
+
resolve: {};
|
|
45
|
+
}, {
|
|
46
|
+
typebox: {};
|
|
47
|
+
error: {};
|
|
48
|
+
}, {
|
|
49
|
+
schema: {};
|
|
50
|
+
standaloneSchema: {};
|
|
51
|
+
macro: {};
|
|
52
|
+
macroFn: {};
|
|
53
|
+
parser: {};
|
|
54
|
+
response: {};
|
|
55
|
+
}, {
|
|
56
|
+
[x: string]: {
|
|
57
|
+
get: {
|
|
58
|
+
body: unknown;
|
|
59
|
+
params: {};
|
|
60
|
+
query: unknown;
|
|
61
|
+
headers: unknown;
|
|
62
|
+
response: {
|
|
63
|
+
200: VoiceAssistantHealthSummary<TProvider>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}, {
|
|
68
|
+
derive: {};
|
|
69
|
+
resolve: {};
|
|
70
|
+
schema: {};
|
|
71
|
+
standaloneSchema: {};
|
|
72
|
+
response: {};
|
|
73
|
+
}, {
|
|
74
|
+
derive: {};
|
|
75
|
+
resolve: {};
|
|
76
|
+
schema: {};
|
|
77
|
+
standaloneSchema: {};
|
|
78
|
+
response: {};
|
|
79
|
+
}>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { voice } from './plugin';
|
|
2
2
|
export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
|
|
3
|
+
export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
|
|
3
4
|
export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
|
|
4
5
|
export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
|
|
5
6
|
export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
|
|
@@ -26,6 +27,7 @@ export { resolveVoiceRuntimePreset } from './presets';
|
|
|
26
27
|
export { resolveTurnDetectionConfig, TURN_PROFILE_DEFAULTS } from './turnProfiles';
|
|
27
28
|
export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewRecorder, renderVoiceCallReviewHTML, renderVoiceCallReviewMarkdown } from './testing/review';
|
|
28
29
|
export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
|
|
30
|
+
export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
|
|
29
31
|
export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
|
|
30
32
|
export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
|
|
31
33
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
package/dist/index.js
CHANGED
|
@@ -6101,6 +6101,292 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
6101
6101
|
totalRuns: assistantRuns.length
|
|
6102
6102
|
};
|
|
6103
6103
|
};
|
|
6104
|
+
// src/assistantHealth.ts
|
|
6105
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
6106
|
+
|
|
6107
|
+
// src/providerHealth.ts
|
|
6108
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
6109
|
+
var getString = (value) => typeof value === "string" ? value : undefined;
|
|
6110
|
+
var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
6111
|
+
var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
|
|
6112
|
+
var summarizeVoiceProviderHealth = async (input) => {
|
|
6113
|
+
const options = Array.isArray(input) ? { events: input } : input;
|
|
6114
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6115
|
+
const providers = options.providers ?? [];
|
|
6116
|
+
const providerSet = new Set(providers);
|
|
6117
|
+
const now = options.now ?? Date.now();
|
|
6118
|
+
const entries = new Map;
|
|
6119
|
+
const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
|
|
6120
|
+
const getEntry = (provider) => {
|
|
6121
|
+
const existing = entries.get(provider);
|
|
6122
|
+
if (existing) {
|
|
6123
|
+
return existing;
|
|
6124
|
+
}
|
|
6125
|
+
const entry = {
|
|
6126
|
+
elapsedCount: 0,
|
|
6127
|
+
elapsedTotal: 0,
|
|
6128
|
+
errorCount: 0,
|
|
6129
|
+
fallbackCount: 0,
|
|
6130
|
+
provider,
|
|
6131
|
+
rateLimited: false,
|
|
6132
|
+
recommended: false,
|
|
6133
|
+
runCount: 0,
|
|
6134
|
+
status: "idle"
|
|
6135
|
+
};
|
|
6136
|
+
entries.set(provider, entry);
|
|
6137
|
+
return entry;
|
|
6138
|
+
};
|
|
6139
|
+
for (const provider of providers) {
|
|
6140
|
+
getEntry(provider);
|
|
6141
|
+
}
|
|
6142
|
+
const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
|
|
6143
|
+
for (const event of events) {
|
|
6144
|
+
if (event.type === "assistant.run") {
|
|
6145
|
+
if (hasProviderRouterEvents) {
|
|
6146
|
+
continue;
|
|
6147
|
+
}
|
|
6148
|
+
const provider2 = event.payload.variantId;
|
|
6149
|
+
if (!isAllowedProvider(provider2)) {
|
|
6150
|
+
continue;
|
|
6151
|
+
}
|
|
6152
|
+
const entry2 = getEntry(provider2);
|
|
6153
|
+
entry2.runCount += 1;
|
|
6154
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6155
|
+
if (elapsedMs !== undefined) {
|
|
6156
|
+
entry2.elapsedCount += 1;
|
|
6157
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6158
|
+
}
|
|
6159
|
+
continue;
|
|
6160
|
+
}
|
|
6161
|
+
if (event.type !== "session.error") {
|
|
6162
|
+
continue;
|
|
6163
|
+
}
|
|
6164
|
+
const provider = event.payload.provider;
|
|
6165
|
+
if (!isAllowedProvider(provider)) {
|
|
6166
|
+
continue;
|
|
6167
|
+
}
|
|
6168
|
+
const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
6169
|
+
const applyProviderHealth = () => {
|
|
6170
|
+
const entry2 = getEntry(provider);
|
|
6171
|
+
const providerHealth = event.payload.providerHealth;
|
|
6172
|
+
if (providerHealth && typeof providerHealth === "object") {
|
|
6173
|
+
const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
|
|
6174
|
+
if (suppressedUntil2 !== undefined) {
|
|
6175
|
+
entry2.suppressedUntil = suppressedUntil2;
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
const suppressedUntil = getNumber(event.payload.suppressedUntil);
|
|
6179
|
+
if (suppressedUntil !== undefined) {
|
|
6180
|
+
entry2.suppressedUntil = suppressedUntil;
|
|
6181
|
+
}
|
|
6182
|
+
const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
|
|
6183
|
+
if (suppressionRemainingMs !== undefined) {
|
|
6184
|
+
entry2.suppressionRemainingMs = suppressionRemainingMs;
|
|
6185
|
+
}
|
|
6186
|
+
return entry2;
|
|
6187
|
+
};
|
|
6188
|
+
if (providerStatus === "success" || providerStatus === "fallback") {
|
|
6189
|
+
const entry2 = applyProviderHealth();
|
|
6190
|
+
entry2.runCount += 1;
|
|
6191
|
+
entry2.lastSuccessAt = event.at;
|
|
6192
|
+
if (providerStatus === "success") {
|
|
6193
|
+
entry2.lastError = undefined;
|
|
6194
|
+
entry2.rateLimited = false;
|
|
6195
|
+
entry2.suppressedUntil = undefined;
|
|
6196
|
+
entry2.suppressionRemainingMs = undefined;
|
|
6197
|
+
}
|
|
6198
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6199
|
+
if (elapsedMs !== undefined) {
|
|
6200
|
+
entry2.elapsedCount += 1;
|
|
6201
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6202
|
+
}
|
|
6203
|
+
const selectedProvider = event.payload.selectedProvider;
|
|
6204
|
+
if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
|
|
6205
|
+
getEntry(selectedProvider).fallbackCount += 1;
|
|
6206
|
+
}
|
|
6207
|
+
continue;
|
|
6208
|
+
}
|
|
6209
|
+
const entry = applyProviderHealth();
|
|
6210
|
+
entry.errorCount += 1;
|
|
6211
|
+
entry.lastError = getString(event.payload.error);
|
|
6212
|
+
entry.lastErrorAt = event.at;
|
|
6213
|
+
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
6214
|
+
}
|
|
6215
|
+
const summaries = [...entries.values()].map((entry) => {
|
|
6216
|
+
const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
|
|
6217
|
+
const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
|
|
6218
|
+
const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
|
|
6219
|
+
const recoverable = hadSuppression && !activeSuppression;
|
|
6220
|
+
const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
|
|
6221
|
+
const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
|
|
6222
|
+
return {
|
|
6223
|
+
averageElapsedMs,
|
|
6224
|
+
errorCount: entry.errorCount,
|
|
6225
|
+
fallbackCount: entry.fallbackCount,
|
|
6226
|
+
lastError: entry.lastError,
|
|
6227
|
+
lastErrorAt: entry.lastErrorAt,
|
|
6228
|
+
lastSuccessAt: entry.lastSuccessAt,
|
|
6229
|
+
provider: entry.provider,
|
|
6230
|
+
rateLimited: entry.rateLimited,
|
|
6231
|
+
recommended: false,
|
|
6232
|
+
runCount: entry.runCount,
|
|
6233
|
+
status,
|
|
6234
|
+
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
6235
|
+
suppressedUntil: entry.suppressedUntil
|
|
6236
|
+
};
|
|
6237
|
+
});
|
|
6238
|
+
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
6239
|
+
if (recommended) {
|
|
6240
|
+
recommended.recommended = true;
|
|
6241
|
+
}
|
|
6242
|
+
return summaries;
|
|
6243
|
+
};
|
|
6244
|
+
var escapeHtml3 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6245
|
+
var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
|
|
6246
|
+
'<div class="voice-provider-health">',
|
|
6247
|
+
...providers.map((provider) => {
|
|
6248
|
+
const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
|
|
6249
|
+
return [
|
|
6250
|
+
`<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
|
|
6251
|
+
'<div class="voice-provider-card-header">',
|
|
6252
|
+
`<strong>${escapeHtml3(provider.provider)}</strong>`,
|
|
6253
|
+
`<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
|
|
6254
|
+
"</div>",
|
|
6255
|
+
"<dl>",
|
|
6256
|
+
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
6257
|
+
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
6258
|
+
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
6259
|
+
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
6260
|
+
"</dl>",
|
|
6261
|
+
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
6262
|
+
provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
|
|
6263
|
+
"</article>"
|
|
6264
|
+
].join("");
|
|
6265
|
+
}),
|
|
6266
|
+
"</div>"
|
|
6267
|
+
].join("");
|
|
6268
|
+
var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
|
|
6269
|
+
var createVoiceProviderHealthHTMLHandler = (options) => async () => {
|
|
6270
|
+
const providers = await summarizeVoiceProviderHealth(options);
|
|
6271
|
+
const render = options.render ?? renderVoiceProviderHealthHTML;
|
|
6272
|
+
const body = await render(providers);
|
|
6273
|
+
return new Response(body, {
|
|
6274
|
+
headers: {
|
|
6275
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6276
|
+
...options.headers
|
|
6277
|
+
}
|
|
6278
|
+
});
|
|
6279
|
+
};
|
|
6280
|
+
var createVoiceProviderHealthRoutes = (options) => {
|
|
6281
|
+
const path = options.path ?? "/api/provider-status";
|
|
6282
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6283
|
+
const routes = new Elysia2({
|
|
6284
|
+
name: options.name ?? "absolutejs-voice-provider-health"
|
|
6285
|
+
}).get(path, createVoiceProviderHealthJSONHandler(options));
|
|
6286
|
+
if (htmlPath) {
|
|
6287
|
+
routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
|
|
6288
|
+
}
|
|
6289
|
+
return routes;
|
|
6290
|
+
};
|
|
6291
|
+
|
|
6292
|
+
// src/assistantHealth.ts
|
|
6293
|
+
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6294
|
+
var renderCountMap = (values) => {
|
|
6295
|
+
const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
|
|
6296
|
+
if (entries.length === 0) {
|
|
6297
|
+
return '<p class="voice-assistant-health-empty">No data yet.</p>';
|
|
6298
|
+
}
|
|
6299
|
+
return [
|
|
6300
|
+
'<div class="voice-assistant-health-metrics">',
|
|
6301
|
+
...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
|
|
6302
|
+
"</div>"
|
|
6303
|
+
].join("");
|
|
6304
|
+
};
|
|
6305
|
+
var getString2 = (value) => typeof value === "string" ? value : undefined;
|
|
6306
|
+
var getRecentFailures = (events, maxFailures) => events.filter((event) => event.type === "session.error" || event.type === "assistant.guardrail" && event.payload.action === "blocked").toReversed().slice(0, maxFailures).map((event) => ({
|
|
6307
|
+
at: event.at,
|
|
6308
|
+
assistantId: getString2(event.payload.assistantId),
|
|
6309
|
+
error: getString2(event.payload.error),
|
|
6310
|
+
provider: getString2(event.payload.provider),
|
|
6311
|
+
rateLimited: event.payload.rateLimited === true ? true : undefined,
|
|
6312
|
+
sessionId: event.sessionId,
|
|
6313
|
+
status: getString2(event.payload.providerStatus),
|
|
6314
|
+
turnId: event.turnId,
|
|
6315
|
+
type: event.type
|
|
6316
|
+
}));
|
|
6317
|
+
var summarizeVoiceAssistantHealth = async (options) => {
|
|
6318
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6319
|
+
return {
|
|
6320
|
+
assistantRuns: await summarizeVoiceAssistantRuns({ events }),
|
|
6321
|
+
providerHealth: await summarizeVoiceProviderHealth({
|
|
6322
|
+
events,
|
|
6323
|
+
providers: options.providers
|
|
6324
|
+
}),
|
|
6325
|
+
recentFailures: getRecentFailures(events, options.maxFailures ?? 8)
|
|
6326
|
+
};
|
|
6327
|
+
};
|
|
6328
|
+
var renderVoiceAssistantHealthHTML = (summary) => {
|
|
6329
|
+
const assistant = summary.assistantRuns.assistants[0];
|
|
6330
|
+
const failures = summary.recentFailures;
|
|
6331
|
+
return [
|
|
6332
|
+
'<div class="voice-assistant-health">',
|
|
6333
|
+
'<section class="voice-assistant-health-grid">',
|
|
6334
|
+
`<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
|
|
6335
|
+
`<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
|
|
6336
|
+
`<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
|
|
6337
|
+
`<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
|
|
6338
|
+
"</section>",
|
|
6339
|
+
"<section>",
|
|
6340
|
+
"<h3>Provider Health</h3>",
|
|
6341
|
+
renderVoiceProviderHealthHTML(summary.providerHealth),
|
|
6342
|
+
"</section>",
|
|
6343
|
+
'<section class="voice-assistant-health-columns">',
|
|
6344
|
+
`<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
|
|
6345
|
+
`<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
|
|
6346
|
+
`<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
|
|
6347
|
+
`<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
|
|
6348
|
+
"</section>",
|
|
6349
|
+
"<section>",
|
|
6350
|
+
"<h3>Recent Failures</h3>",
|
|
6351
|
+
failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
|
|
6352
|
+
'<div class="voice-assistant-health-failures">',
|
|
6353
|
+
...failures.map((failure) => [
|
|
6354
|
+
"<article>",
|
|
6355
|
+
`<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
|
|
6356
|
+
`<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
|
|
6357
|
+
failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
|
|
6358
|
+
`<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
|
|
6359
|
+
"</article>"
|
|
6360
|
+
].join("")),
|
|
6361
|
+
"</div>"
|
|
6362
|
+
].join(""),
|
|
6363
|
+
"</section>",
|
|
6364
|
+
"</div>"
|
|
6365
|
+
].join("");
|
|
6366
|
+
};
|
|
6367
|
+
var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
|
|
6368
|
+
var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
|
|
6369
|
+
const summary = await summarizeVoiceAssistantHealth(options);
|
|
6370
|
+
const render = options.render ?? renderVoiceAssistantHealthHTML;
|
|
6371
|
+
const body = await render(summary);
|
|
6372
|
+
return new Response(body, {
|
|
6373
|
+
headers: {
|
|
6374
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6375
|
+
...options.headers
|
|
6376
|
+
}
|
|
6377
|
+
});
|
|
6378
|
+
};
|
|
6379
|
+
var createVoiceAssistantHealthRoutes = (options) => {
|
|
6380
|
+
const path = options.path ?? "/api/assistant-health";
|
|
6381
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6382
|
+
const routes = new Elysia3({
|
|
6383
|
+
name: options.name ?? "absolutejs-voice-assistant-health"
|
|
6384
|
+
}).get(path, createVoiceAssistantHealthJSONHandler(options));
|
|
6385
|
+
if (htmlPath) {
|
|
6386
|
+
routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
|
|
6387
|
+
}
|
|
6388
|
+
return routes;
|
|
6389
|
+
};
|
|
6104
6390
|
// src/fileStore.ts
|
|
6105
6391
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
6106
6392
|
import { join } from "path";
|
|
@@ -6437,7 +6723,7 @@ var exportVoiceTrace = async (input) => {
|
|
|
6437
6723
|
};
|
|
6438
6724
|
};
|
|
6439
6725
|
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
6440
|
-
var
|
|
6726
|
+
var escapeHtml5 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6441
6727
|
var formatTraceValue = (value) => {
|
|
6442
6728
|
if (value === undefined || value === null) {
|
|
6443
6729
|
return "";
|
|
@@ -6715,10 +7001,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6715
7001
|
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
6716
7002
|
return [
|
|
6717
7003
|
"<tr>",
|
|
6718
|
-
`<td>${
|
|
6719
|
-
`<td>${
|
|
6720
|
-
`<td>${
|
|
6721
|
-
`<td><code>${
|
|
7004
|
+
`<td>${escapeHtml5(String(offset))}</td>`,
|
|
7005
|
+
`<td>${escapeHtml5(event.type)}</td>`,
|
|
7006
|
+
`<td>${escapeHtml5(event.turnId ?? "")}</td>`,
|
|
7007
|
+
`<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
|
|
6722
7008
|
"</tr>"
|
|
6723
7009
|
].join("");
|
|
6724
7010
|
}).join(`
|
|
@@ -6729,7 +7015,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6729
7015
|
"<head>",
|
|
6730
7016
|
'<meta charset="utf-8" />',
|
|
6731
7017
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
6732
|
-
`<title>${
|
|
7018
|
+
`<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
|
|
6733
7019
|
"<style>",
|
|
6734
7020
|
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
6735
7021
|
"main{max-width:1100px;margin:auto}",
|
|
@@ -6743,7 +7029,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6743
7029
|
"</style>",
|
|
6744
7030
|
"</head>",
|
|
6745
7031
|
"<body><main>",
|
|
6746
|
-
`<h1>${
|
|
7032
|
+
`<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
6747
7033
|
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
6748
7034
|
'<section class="summary">',
|
|
6749
7035
|
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
@@ -6757,7 +7043,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6757
7043
|
eventRows,
|
|
6758
7044
|
"</tbody></table>",
|
|
6759
7045
|
"<h2>Markdown Export</h2>",
|
|
6760
|
-
`<pre>${
|
|
7046
|
+
`<pre>${escapeHtml5(markdown)}</pre>`,
|
|
6761
7047
|
"</main></body></html>"
|
|
6762
7048
|
].join(`
|
|
6763
7049
|
`);
|
|
@@ -7855,190 +8141,6 @@ var createGeminiVoiceAssistantModel = (options) => {
|
|
|
7855
8141
|
}
|
|
7856
8142
|
};
|
|
7857
8143
|
};
|
|
7858
|
-
// src/providerHealth.ts
|
|
7859
|
-
import { Elysia as Elysia2 } from "elysia";
|
|
7860
|
-
var getString = (value) => typeof value === "string" ? value : undefined;
|
|
7861
|
-
var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
7862
|
-
var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
|
|
7863
|
-
var summarizeVoiceProviderHealth = async (input) => {
|
|
7864
|
-
const options = Array.isArray(input) ? { events: input } : input;
|
|
7865
|
-
const events = options.events ?? await options.store?.list() ?? [];
|
|
7866
|
-
const providers = options.providers ?? [];
|
|
7867
|
-
const providerSet = new Set(providers);
|
|
7868
|
-
const now = options.now ?? Date.now();
|
|
7869
|
-
const entries = new Map;
|
|
7870
|
-
const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
|
|
7871
|
-
const getEntry = (provider) => {
|
|
7872
|
-
const existing = entries.get(provider);
|
|
7873
|
-
if (existing) {
|
|
7874
|
-
return existing;
|
|
7875
|
-
}
|
|
7876
|
-
const entry = {
|
|
7877
|
-
elapsedCount: 0,
|
|
7878
|
-
elapsedTotal: 0,
|
|
7879
|
-
errorCount: 0,
|
|
7880
|
-
fallbackCount: 0,
|
|
7881
|
-
provider,
|
|
7882
|
-
rateLimited: false,
|
|
7883
|
-
recommended: false,
|
|
7884
|
-
runCount: 0,
|
|
7885
|
-
status: "idle"
|
|
7886
|
-
};
|
|
7887
|
-
entries.set(provider, entry);
|
|
7888
|
-
return entry;
|
|
7889
|
-
};
|
|
7890
|
-
for (const provider of providers) {
|
|
7891
|
-
getEntry(provider);
|
|
7892
|
-
}
|
|
7893
|
-
const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
|
|
7894
|
-
for (const event of events) {
|
|
7895
|
-
if (event.type === "assistant.run") {
|
|
7896
|
-
if (hasProviderRouterEvents) {
|
|
7897
|
-
continue;
|
|
7898
|
-
}
|
|
7899
|
-
const provider2 = event.payload.variantId;
|
|
7900
|
-
if (!isAllowedProvider(provider2)) {
|
|
7901
|
-
continue;
|
|
7902
|
-
}
|
|
7903
|
-
const entry2 = getEntry(provider2);
|
|
7904
|
-
entry2.runCount += 1;
|
|
7905
|
-
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
7906
|
-
if (elapsedMs !== undefined) {
|
|
7907
|
-
entry2.elapsedCount += 1;
|
|
7908
|
-
entry2.elapsedTotal += elapsedMs;
|
|
7909
|
-
}
|
|
7910
|
-
continue;
|
|
7911
|
-
}
|
|
7912
|
-
if (event.type !== "session.error") {
|
|
7913
|
-
continue;
|
|
7914
|
-
}
|
|
7915
|
-
const provider = event.payload.provider;
|
|
7916
|
-
if (!isAllowedProvider(provider)) {
|
|
7917
|
-
continue;
|
|
7918
|
-
}
|
|
7919
|
-
const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
7920
|
-
const applyProviderHealth = () => {
|
|
7921
|
-
const entry2 = getEntry(provider);
|
|
7922
|
-
const providerHealth = event.payload.providerHealth;
|
|
7923
|
-
if (providerHealth && typeof providerHealth === "object") {
|
|
7924
|
-
const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
|
|
7925
|
-
if (suppressedUntil2 !== undefined) {
|
|
7926
|
-
entry2.suppressedUntil = suppressedUntil2;
|
|
7927
|
-
}
|
|
7928
|
-
}
|
|
7929
|
-
const suppressedUntil = getNumber(event.payload.suppressedUntil);
|
|
7930
|
-
if (suppressedUntil !== undefined) {
|
|
7931
|
-
entry2.suppressedUntil = suppressedUntil;
|
|
7932
|
-
}
|
|
7933
|
-
const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
|
|
7934
|
-
if (suppressionRemainingMs !== undefined) {
|
|
7935
|
-
entry2.suppressionRemainingMs = suppressionRemainingMs;
|
|
7936
|
-
}
|
|
7937
|
-
return entry2;
|
|
7938
|
-
};
|
|
7939
|
-
if (providerStatus === "success" || providerStatus === "fallback") {
|
|
7940
|
-
const entry2 = applyProviderHealth();
|
|
7941
|
-
entry2.runCount += 1;
|
|
7942
|
-
entry2.lastSuccessAt = event.at;
|
|
7943
|
-
if (providerStatus === "success") {
|
|
7944
|
-
entry2.lastError = undefined;
|
|
7945
|
-
entry2.rateLimited = false;
|
|
7946
|
-
entry2.suppressedUntil = undefined;
|
|
7947
|
-
entry2.suppressionRemainingMs = undefined;
|
|
7948
|
-
}
|
|
7949
|
-
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
7950
|
-
if (elapsedMs !== undefined) {
|
|
7951
|
-
entry2.elapsedCount += 1;
|
|
7952
|
-
entry2.elapsedTotal += elapsedMs;
|
|
7953
|
-
}
|
|
7954
|
-
const selectedProvider = event.payload.selectedProvider;
|
|
7955
|
-
if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
|
|
7956
|
-
getEntry(selectedProvider).fallbackCount += 1;
|
|
7957
|
-
}
|
|
7958
|
-
continue;
|
|
7959
|
-
}
|
|
7960
|
-
const entry = applyProviderHealth();
|
|
7961
|
-
entry.errorCount += 1;
|
|
7962
|
-
entry.lastError = getString(event.payload.error);
|
|
7963
|
-
entry.lastErrorAt = event.at;
|
|
7964
|
-
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
7965
|
-
}
|
|
7966
|
-
const summaries = [...entries.values()].map((entry) => {
|
|
7967
|
-
const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
|
|
7968
|
-
const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
|
|
7969
|
-
const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
|
|
7970
|
-
const recoverable = hadSuppression && !activeSuppression;
|
|
7971
|
-
const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
|
|
7972
|
-
const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
|
|
7973
|
-
return {
|
|
7974
|
-
averageElapsedMs,
|
|
7975
|
-
errorCount: entry.errorCount,
|
|
7976
|
-
fallbackCount: entry.fallbackCount,
|
|
7977
|
-
lastError: entry.lastError,
|
|
7978
|
-
lastErrorAt: entry.lastErrorAt,
|
|
7979
|
-
lastSuccessAt: entry.lastSuccessAt,
|
|
7980
|
-
provider: entry.provider,
|
|
7981
|
-
rateLimited: entry.rateLimited,
|
|
7982
|
-
recommended: false,
|
|
7983
|
-
runCount: entry.runCount,
|
|
7984
|
-
status,
|
|
7985
|
-
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
7986
|
-
suppressedUntil: entry.suppressedUntil
|
|
7987
|
-
};
|
|
7988
|
-
});
|
|
7989
|
-
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
7990
|
-
if (recommended) {
|
|
7991
|
-
recommended.recommended = true;
|
|
7992
|
-
}
|
|
7993
|
-
return summaries;
|
|
7994
|
-
};
|
|
7995
|
-
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7996
|
-
var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
|
|
7997
|
-
'<div class="voice-provider-health">',
|
|
7998
|
-
...providers.map((provider) => {
|
|
7999
|
-
const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
|
|
8000
|
-
return [
|
|
8001
|
-
`<article class="voice-provider-card ${escapeHtml4(provider.status)}">`,
|
|
8002
|
-
'<div class="voice-provider-card-header">',
|
|
8003
|
-
`<strong>${escapeHtml4(provider.provider)}</strong>`,
|
|
8004
|
-
`<span>${escapeHtml4(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
|
|
8005
|
-
"</div>",
|
|
8006
|
-
"<dl>",
|
|
8007
|
-
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
8008
|
-
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
8009
|
-
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
8010
|
-
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
8011
|
-
"</dl>",
|
|
8012
|
-
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
8013
|
-
provider.lastError ? `<p>${escapeHtml4(provider.lastError)}</p>` : "",
|
|
8014
|
-
"</article>"
|
|
8015
|
-
].join("");
|
|
8016
|
-
}),
|
|
8017
|
-
"</div>"
|
|
8018
|
-
].join("");
|
|
8019
|
-
var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
|
|
8020
|
-
var createVoiceProviderHealthHTMLHandler = (options) => async () => {
|
|
8021
|
-
const providers = await summarizeVoiceProviderHealth(options);
|
|
8022
|
-
const render = options.render ?? renderVoiceProviderHealthHTML;
|
|
8023
|
-
const body = await render(providers);
|
|
8024
|
-
return new Response(body, {
|
|
8025
|
-
headers: {
|
|
8026
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
8027
|
-
...options.headers
|
|
8028
|
-
}
|
|
8029
|
-
});
|
|
8030
|
-
};
|
|
8031
|
-
var createVoiceProviderHealthRoutes = (options) => {
|
|
8032
|
-
const path = options.path ?? "/api/provider-status";
|
|
8033
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
8034
|
-
const routes = new Elysia2({
|
|
8035
|
-
name: options.name ?? "absolutejs-voice-provider-health"
|
|
8036
|
-
}).get(path, createVoiceProviderHealthJSONHandler(options));
|
|
8037
|
-
if (htmlPath) {
|
|
8038
|
-
routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
|
|
8039
|
-
}
|
|
8040
|
-
return routes;
|
|
8041
|
-
};
|
|
8042
8144
|
// src/sqliteStore.ts
|
|
8043
8145
|
import { Database } from "bun:sqlite";
|
|
8044
8146
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
@@ -10495,6 +10597,7 @@ export {
|
|
|
10495
10597
|
summarizeVoiceOpsTaskAnalytics,
|
|
10496
10598
|
summarizeVoiceIntegrationEvents,
|
|
10497
10599
|
summarizeVoiceAssistantRuns,
|
|
10600
|
+
summarizeVoiceAssistantHealth,
|
|
10498
10601
|
startVoiceOpsTask,
|
|
10499
10602
|
shapeTelephonyAssistantText,
|
|
10500
10603
|
selectVoiceTraceEventsForPrune,
|
|
@@ -10516,6 +10619,7 @@ export {
|
|
|
10516
10619
|
renderVoiceProviderHealthHTML,
|
|
10517
10620
|
renderVoiceCallReviewMarkdown,
|
|
10518
10621
|
renderVoiceCallReviewHTML,
|
|
10622
|
+
renderVoiceAssistantHealthHTML,
|
|
10519
10623
|
redactVoiceTraceText,
|
|
10520
10624
|
redactVoiceTraceEvents,
|
|
10521
10625
|
redactVoiceTraceEvent,
|
|
@@ -10618,6 +10722,9 @@ export {
|
|
|
10618
10722
|
createVoiceCRMActivitySink,
|
|
10619
10723
|
createVoiceAssistantMemoryRecord,
|
|
10620
10724
|
createVoiceAssistantMemoryHandle,
|
|
10725
|
+
createVoiceAssistantHealthRoutes,
|
|
10726
|
+
createVoiceAssistantHealthJSONHandler,
|
|
10727
|
+
createVoiceAssistantHealthHTMLHandler,
|
|
10621
10728
|
createVoiceAssistant,
|
|
10622
10729
|
createVoiceAgentTool,
|
|
10623
10730
|
createVoiceAgentSquad,
|