@absolutejs/voice 0.0.22-beta.2 → 0.0.22-beta.21
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/angular/index.d.ts +1 -0
- package/dist/angular/index.js +124 -0
- package/dist/angular/voice-provider-status.service.d.ts +12 -0
- package/dist/assistant.d.ts +44 -0
- package/dist/assistantHealth.d.ts +81 -0
- package/dist/assistantMemory.d.ts +63 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +81 -0
- package/dist/client/providerStatus.d.ts +19 -0
- package/dist/fileStore.d.ts +5 -2
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1626 -28
- package/dist/modelAdapters.d.ts +93 -0
- package/dist/providerHealth.d.ts +78 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +100 -0
- package/dist/react/useVoiceProviderStatus.d.ts +8 -0
- package/dist/sessionReplay.d.ts +94 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +8 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +83 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +878 -0
- package/dist/testing/providerSimulator.d.ts +36 -0
- package/dist/trace.d.ts +1 -1
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +113 -0
- package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5150,6 +5150,17 @@ var createVoiceAgent = (options) => {
|
|
|
5150
5150
|
if (output.assistantText?.trim()) {
|
|
5151
5151
|
messages.push({
|
|
5152
5152
|
content: output.assistantText,
|
|
5153
|
+
metadata: output.toolCalls?.length ? {
|
|
5154
|
+
toolCalls: output.toolCalls
|
|
5155
|
+
} : undefined,
|
|
5156
|
+
role: "assistant"
|
|
5157
|
+
});
|
|
5158
|
+
} else if (output.toolCalls?.length) {
|
|
5159
|
+
messages.push({
|
|
5160
|
+
content: "",
|
|
5161
|
+
metadata: {
|
|
5162
|
+
toolCalls: output.toolCalls
|
|
5163
|
+
},
|
|
5153
5164
|
role: "assistant"
|
|
5154
5165
|
});
|
|
5155
5166
|
}
|
|
@@ -5594,6 +5605,112 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
|
|
|
5594
5605
|
};
|
|
5595
5606
|
};
|
|
5596
5607
|
|
|
5608
|
+
// src/assistantMemory.ts
|
|
5609
|
+
var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
5610
|
+
var createVoiceAssistantMemoryRecord = (input) => {
|
|
5611
|
+
const now = Date.now();
|
|
5612
|
+
return {
|
|
5613
|
+
...input,
|
|
5614
|
+
createdAt: input.createdAt ?? input.updatedAt ?? now,
|
|
5615
|
+
updatedAt: input.updatedAt ?? now
|
|
5616
|
+
};
|
|
5617
|
+
};
|
|
5618
|
+
var createVoiceMemoryAssistantMemoryStore = () => {
|
|
5619
|
+
const records = new Map;
|
|
5620
|
+
return {
|
|
5621
|
+
delete: async (input) => {
|
|
5622
|
+
records.delete(createMemoryId(input));
|
|
5623
|
+
},
|
|
5624
|
+
get: async (input) => records.get(createMemoryId(input)),
|
|
5625
|
+
list: async (input) => [...records.values()].filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt),
|
|
5626
|
+
set: async (input) => {
|
|
5627
|
+
const id = createMemoryId(input);
|
|
5628
|
+
const existing = records.get(id);
|
|
5629
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
5630
|
+
...input,
|
|
5631
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
5632
|
+
updatedAt: input.updatedAt
|
|
5633
|
+
});
|
|
5634
|
+
records.set(id, record);
|
|
5635
|
+
return record;
|
|
5636
|
+
}
|
|
5637
|
+
};
|
|
5638
|
+
};
|
|
5639
|
+
var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
|
|
5640
|
+
var createVoiceAssistantMemoryHandle = async (input) => {
|
|
5641
|
+
const namespace = await resolveVoiceAssistantMemoryNamespace({
|
|
5642
|
+
assistantId: input.assistantId,
|
|
5643
|
+
context: input.context,
|
|
5644
|
+
memory: input.memory,
|
|
5645
|
+
session: input.session
|
|
5646
|
+
});
|
|
5647
|
+
const trace = async (event) => {
|
|
5648
|
+
await input.trace?.append({
|
|
5649
|
+
at: Date.now(),
|
|
5650
|
+
payload: {
|
|
5651
|
+
assistantId: input.assistantId,
|
|
5652
|
+
namespace,
|
|
5653
|
+
...event
|
|
5654
|
+
},
|
|
5655
|
+
scenarioId: input.session.scenarioId,
|
|
5656
|
+
sessionId: input.session.id,
|
|
5657
|
+
type: "assistant.memory"
|
|
5658
|
+
});
|
|
5659
|
+
};
|
|
5660
|
+
return {
|
|
5661
|
+
delete: async (key) => {
|
|
5662
|
+
await input.memory.store.delete({
|
|
5663
|
+
assistantId: input.assistantId,
|
|
5664
|
+
key,
|
|
5665
|
+
namespace
|
|
5666
|
+
});
|
|
5667
|
+
await trace({
|
|
5668
|
+
action: "delete",
|
|
5669
|
+
key
|
|
5670
|
+
});
|
|
5671
|
+
},
|
|
5672
|
+
get: async (key) => {
|
|
5673
|
+
const record = await input.memory.store.get({
|
|
5674
|
+
assistantId: input.assistantId,
|
|
5675
|
+
key,
|
|
5676
|
+
namespace
|
|
5677
|
+
});
|
|
5678
|
+
await trace({
|
|
5679
|
+
action: "get",
|
|
5680
|
+
found: Boolean(record),
|
|
5681
|
+
key
|
|
5682
|
+
});
|
|
5683
|
+
return record?.value;
|
|
5684
|
+
},
|
|
5685
|
+
list: async () => {
|
|
5686
|
+
const records = await input.memory.store.list({
|
|
5687
|
+
assistantId: input.assistantId,
|
|
5688
|
+
namespace
|
|
5689
|
+
});
|
|
5690
|
+
await trace({
|
|
5691
|
+
action: "list",
|
|
5692
|
+
count: records.length
|
|
5693
|
+
});
|
|
5694
|
+
return records;
|
|
5695
|
+
},
|
|
5696
|
+
namespace,
|
|
5697
|
+
set: async (key, value, metadata) => {
|
|
5698
|
+
const record = await input.memory.store.set({
|
|
5699
|
+
assistantId: input.assistantId,
|
|
5700
|
+
key,
|
|
5701
|
+
metadata,
|
|
5702
|
+
namespace,
|
|
5703
|
+
value
|
|
5704
|
+
});
|
|
5705
|
+
await trace({
|
|
5706
|
+
action: "set",
|
|
5707
|
+
key
|
|
5708
|
+
});
|
|
5709
|
+
return record;
|
|
5710
|
+
}
|
|
5711
|
+
};
|
|
5712
|
+
};
|
|
5713
|
+
|
|
5597
5714
|
// src/assistant.ts
|
|
5598
5715
|
var hashString = (value) => {
|
|
5599
5716
|
let hash = 2166136261;
|
|
@@ -5603,6 +5720,47 @@ var hashString = (value) => {
|
|
|
5603
5720
|
}
|
|
5604
5721
|
return hash >>> 0;
|
|
5605
5722
|
};
|
|
5723
|
+
var increment = (record, key) => {
|
|
5724
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
5725
|
+
};
|
|
5726
|
+
var resolveOutcome = (result) => {
|
|
5727
|
+
if (result.transfer) {
|
|
5728
|
+
return "transferred";
|
|
5729
|
+
}
|
|
5730
|
+
if (result.escalate) {
|
|
5731
|
+
return "escalated";
|
|
5732
|
+
}
|
|
5733
|
+
if (result.voicemail) {
|
|
5734
|
+
return "voicemail";
|
|
5735
|
+
}
|
|
5736
|
+
if (result.noAnswer) {
|
|
5737
|
+
return "no-answer";
|
|
5738
|
+
}
|
|
5739
|
+
if (result.complete) {
|
|
5740
|
+
return "completed";
|
|
5741
|
+
}
|
|
5742
|
+
return "continued";
|
|
5743
|
+
};
|
|
5744
|
+
var resolveArtifactPlanName = (artifactPlan) => {
|
|
5745
|
+
const preset = artifactPlan?.preset;
|
|
5746
|
+
if (!preset) {
|
|
5747
|
+
return artifactPlan?.ops ? "custom" : undefined;
|
|
5748
|
+
}
|
|
5749
|
+
return typeof preset === "string" ? preset : preset.name;
|
|
5750
|
+
};
|
|
5751
|
+
var appendAssistantTrace = async (input) => {
|
|
5752
|
+
await input.trace?.append({
|
|
5753
|
+
at: Date.now(),
|
|
5754
|
+
payload: {
|
|
5755
|
+
assistantId: input.assistantId,
|
|
5756
|
+
...input.event
|
|
5757
|
+
},
|
|
5758
|
+
scenarioId: input.session.scenarioId,
|
|
5759
|
+
sessionId: input.session.id,
|
|
5760
|
+
turnId: input.turnId,
|
|
5761
|
+
type: input.type
|
|
5762
|
+
});
|
|
5763
|
+
};
|
|
5606
5764
|
var resolvePresetOps = (artifactPlan) => {
|
|
5607
5765
|
const preset = artifactPlan?.preset;
|
|
5608
5766
|
if (!preset) {
|
|
@@ -5671,6 +5829,7 @@ var createVoiceExperiment = (options) => {
|
|
|
5671
5829
|
};
|
|
5672
5830
|
var createVoiceAssistant = (options) => {
|
|
5673
5831
|
const ops = mergeOps(resolvePresetOps(options.artifactPlan), options.ops);
|
|
5832
|
+
const artifactPlanName = resolveArtifactPlanName(options.artifactPlan);
|
|
5674
5833
|
let agent;
|
|
5675
5834
|
const baseModelOptions = "model" in options && options.model ? {
|
|
5676
5835
|
maxToolRounds: options.maxToolRounds,
|
|
@@ -5700,14 +5859,63 @@ var createVoiceAssistant = (options) => {
|
|
|
5700
5859
|
});
|
|
5701
5860
|
}
|
|
5702
5861
|
const onTurn = async (input) => {
|
|
5862
|
+
const memory = options.memory ? await createVoiceAssistantMemoryHandle({
|
|
5863
|
+
assistantId: options.id,
|
|
5864
|
+
context: input.context,
|
|
5865
|
+
memory: options.memory,
|
|
5866
|
+
session: input.session,
|
|
5867
|
+
trace: options.trace
|
|
5868
|
+
}) : undefined;
|
|
5703
5869
|
const guardrailInput = {
|
|
5704
5870
|
...input,
|
|
5705
|
-
assistantId: options.id
|
|
5871
|
+
assistantId: options.id,
|
|
5872
|
+
memory
|
|
5706
5873
|
};
|
|
5874
|
+
if (memory) {
|
|
5875
|
+
await options.memoryLifecycle?.beforeTurn?.({
|
|
5876
|
+
...input,
|
|
5877
|
+
assistantId: options.id,
|
|
5878
|
+
memory
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5707
5881
|
const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
|
|
5708
5882
|
if (blocked) {
|
|
5883
|
+
if (memory) {
|
|
5884
|
+
await options.memoryLifecycle?.afterTurn?.({
|
|
5885
|
+
...input,
|
|
5886
|
+
assistantId: options.id,
|
|
5887
|
+
memory,
|
|
5888
|
+
result: blocked
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
await appendAssistantTrace({
|
|
5892
|
+
assistantId: options.id,
|
|
5893
|
+
event: {
|
|
5894
|
+
action: "blocked",
|
|
5895
|
+
artifactPlan: artifactPlanName,
|
|
5896
|
+
outcome: resolveOutcome(blocked)
|
|
5897
|
+
},
|
|
5898
|
+
session: input.session,
|
|
5899
|
+
trace: options.trace,
|
|
5900
|
+
turnId: input.turn.id,
|
|
5901
|
+
type: "assistant.guardrail"
|
|
5902
|
+
});
|
|
5903
|
+
await appendAssistantTrace({
|
|
5904
|
+
assistantId: options.id,
|
|
5905
|
+
event: {
|
|
5906
|
+
artifactPlan: artifactPlanName,
|
|
5907
|
+
blocked: true,
|
|
5908
|
+
experimentId: options.experiment?.id,
|
|
5909
|
+
outcome: resolveOutcome(blocked)
|
|
5910
|
+
},
|
|
5911
|
+
session: input.session,
|
|
5912
|
+
trace: options.trace,
|
|
5913
|
+
turnId: input.turn.id,
|
|
5914
|
+
type: "assistant.run"
|
|
5915
|
+
});
|
|
5709
5916
|
return blocked;
|
|
5710
5917
|
}
|
|
5918
|
+
const startedAt = Date.now();
|
|
5711
5919
|
const variant = options.experiment?.resolve({
|
|
5712
5920
|
assistantId: options.id,
|
|
5713
5921
|
context: input.context,
|
|
@@ -5722,12 +5930,56 @@ var createVoiceAssistant = (options) => {
|
|
|
5722
5930
|
trace: options.trace,
|
|
5723
5931
|
tools: variant.tools ?? baseModelOptions.tools
|
|
5724
5932
|
}) : agent;
|
|
5725
|
-
const
|
|
5933
|
+
const runResult = await runner.run(input) ?? {};
|
|
5934
|
+
const result = runResult;
|
|
5726
5935
|
const guarded = await options.guardrails?.afterTurn?.({
|
|
5727
5936
|
...guardrailInput,
|
|
5728
5937
|
result
|
|
5729
5938
|
});
|
|
5730
|
-
|
|
5939
|
+
const finalResult = guarded ?? result;
|
|
5940
|
+
if (memory) {
|
|
5941
|
+
await options.memoryLifecycle?.afterTurn?.({
|
|
5942
|
+
...input,
|
|
5943
|
+
assistantId: options.id,
|
|
5944
|
+
memory,
|
|
5945
|
+
result: finalResult
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
if (guarded) {
|
|
5949
|
+
await appendAssistantTrace({
|
|
5950
|
+
assistantId: options.id,
|
|
5951
|
+
event: {
|
|
5952
|
+
action: "rewritten",
|
|
5953
|
+
artifactPlan: artifactPlanName,
|
|
5954
|
+
experimentId: options.experiment?.id,
|
|
5955
|
+
outcome: resolveOutcome(finalResult),
|
|
5956
|
+
variantId: variant?.id
|
|
5957
|
+
},
|
|
5958
|
+
session: input.session,
|
|
5959
|
+
trace: options.trace,
|
|
5960
|
+
turnId: input.turn.id,
|
|
5961
|
+
type: "assistant.guardrail"
|
|
5962
|
+
});
|
|
5963
|
+
}
|
|
5964
|
+
await appendAssistantTrace({
|
|
5965
|
+
assistantId: options.id,
|
|
5966
|
+
event: {
|
|
5967
|
+
artifactPlan: artifactPlanName,
|
|
5968
|
+
blocked: false,
|
|
5969
|
+
elapsedMs: Date.now() - startedAt,
|
|
5970
|
+
escalated: Boolean(finalResult.escalate),
|
|
5971
|
+
experimentId: options.experiment?.id,
|
|
5972
|
+
outcome: resolveOutcome(finalResult),
|
|
5973
|
+
toolNames: result.toolResults?.map((tool) => tool.toolName) ?? [],
|
|
5974
|
+
transferred: Boolean(finalResult.transfer),
|
|
5975
|
+
variantId: variant?.id
|
|
5976
|
+
},
|
|
5977
|
+
session: input.session,
|
|
5978
|
+
trace: options.trace,
|
|
5979
|
+
turnId: input.turn.id,
|
|
5980
|
+
type: "assistant.run"
|
|
5981
|
+
});
|
|
5982
|
+
return finalResult;
|
|
5731
5983
|
};
|
|
5732
5984
|
return {
|
|
5733
5985
|
agent,
|
|
@@ -5743,9 +5995,408 @@ var createVoiceAssistant = (options) => {
|
|
|
5743
5995
|
})
|
|
5744
5996
|
};
|
|
5745
5997
|
};
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5998
|
+
var summarizeVoiceAssistantRuns = async (input) => {
|
|
5999
|
+
const events = Array.isArray(input) ? input : input.events ?? await input.store?.list() ?? [];
|
|
6000
|
+
const assistantRuns = events.filter((event) => event.type === "assistant.run");
|
|
6001
|
+
const guardrails = events.filter((event) => event.type === "assistant.guardrail");
|
|
6002
|
+
const byAssistant = new Map;
|
|
6003
|
+
const getSummary = (assistantId) => {
|
|
6004
|
+
let summary = byAssistant.get(assistantId);
|
|
6005
|
+
if (!summary) {
|
|
6006
|
+
summary = {
|
|
6007
|
+
assistantId,
|
|
6008
|
+
artifactPlans: {},
|
|
6009
|
+
blockedGuardrailCount: 0,
|
|
6010
|
+
elapsedCount: 0,
|
|
6011
|
+
elapsedTotal: 0,
|
|
6012
|
+
escalationCount: 0,
|
|
6013
|
+
experiments: {},
|
|
6014
|
+
guardrailCount: 0,
|
|
6015
|
+
memory: {
|
|
6016
|
+
deletes: 0,
|
|
6017
|
+
gets: 0,
|
|
6018
|
+
lists: 0,
|
|
6019
|
+
sets: 0
|
|
6020
|
+
},
|
|
6021
|
+
outcomes: {},
|
|
6022
|
+
runCount: 0,
|
|
6023
|
+
sessionIds: new Set,
|
|
6024
|
+
sessions: 0,
|
|
6025
|
+
toolCalls: {},
|
|
6026
|
+
transferCount: 0,
|
|
6027
|
+
variants: {}
|
|
6028
|
+
};
|
|
6029
|
+
byAssistant.set(assistantId, summary);
|
|
6030
|
+
}
|
|
6031
|
+
return summary;
|
|
6032
|
+
};
|
|
6033
|
+
for (const event of assistantRuns) {
|
|
6034
|
+
const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
|
|
6035
|
+
const summary = getSummary(assistantId);
|
|
6036
|
+
summary.runCount += 1;
|
|
6037
|
+
summary.sessionIds.add(event.sessionId);
|
|
6038
|
+
if (typeof event.payload.artifactPlan === "string") {
|
|
6039
|
+
increment(summary.artifactPlans, event.payload.artifactPlan);
|
|
6040
|
+
}
|
|
6041
|
+
if (typeof event.payload.experimentId === "string") {
|
|
6042
|
+
increment(summary.experiments, event.payload.experimentId);
|
|
6043
|
+
}
|
|
6044
|
+
if (typeof event.payload.variantId === "string") {
|
|
6045
|
+
increment(summary.variants, event.payload.variantId);
|
|
6046
|
+
}
|
|
6047
|
+
if (typeof event.payload.outcome === "string") {
|
|
6048
|
+
increment(summary.outcomes, event.payload.outcome);
|
|
6049
|
+
}
|
|
6050
|
+
if (event.payload.escalated === true) {
|
|
6051
|
+
summary.escalationCount += 1;
|
|
6052
|
+
}
|
|
6053
|
+
if (event.payload.transferred === true) {
|
|
6054
|
+
summary.transferCount += 1;
|
|
6055
|
+
}
|
|
6056
|
+
if (event.payload.blocked === true) {
|
|
6057
|
+
summary.blockedGuardrailCount += 1;
|
|
6058
|
+
}
|
|
6059
|
+
if (typeof event.payload.elapsedMs === "number") {
|
|
6060
|
+
summary.elapsedCount += 1;
|
|
6061
|
+
summary.elapsedTotal += event.payload.elapsedMs;
|
|
6062
|
+
}
|
|
6063
|
+
if (Array.isArray(event.payload.toolNames)) {
|
|
6064
|
+
for (const toolName of event.payload.toolNames) {
|
|
6065
|
+
if (typeof toolName === "string") {
|
|
6066
|
+
increment(summary.toolCalls, toolName);
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
for (const event of guardrails) {
|
|
6072
|
+
const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
|
|
6073
|
+
const summary = getSummary(assistantId);
|
|
6074
|
+
summary.guardrailCount += 1;
|
|
6075
|
+
}
|
|
6076
|
+
for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
|
|
6077
|
+
const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
|
|
6078
|
+
const summary = getSummary(assistantId);
|
|
6079
|
+
switch (event.payload.action) {
|
|
6080
|
+
case "delete":
|
|
6081
|
+
summary.memory.deletes += 1;
|
|
6082
|
+
break;
|
|
6083
|
+
case "get":
|
|
6084
|
+
summary.memory.gets += 1;
|
|
6085
|
+
break;
|
|
6086
|
+
case "list":
|
|
6087
|
+
summary.memory.lists += 1;
|
|
6088
|
+
break;
|
|
6089
|
+
case "set":
|
|
6090
|
+
summary.memory.sets += 1;
|
|
6091
|
+
break;
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
|
|
6095
|
+
...summary,
|
|
6096
|
+
averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
|
|
6097
|
+
sessions: sessionIds.size
|
|
6098
|
+
}));
|
|
6099
|
+
return {
|
|
6100
|
+
assistants: assistants.sort((left, right) => left.assistantId.localeCompare(right.assistantId)),
|
|
6101
|
+
totalRuns: assistantRuns.length
|
|
6102
|
+
};
|
|
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, replayHref) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string") || event.type === "assistant.guardrail" && event.payload.action === "blocked").toReversed().slice(0, maxFailures).map((event) => {
|
|
6307
|
+
const failure = {
|
|
6308
|
+
at: event.at,
|
|
6309
|
+
assistantId: getString2(event.payload.assistantId),
|
|
6310
|
+
error: getString2(event.payload.error),
|
|
6311
|
+
provider: getString2(event.payload.provider),
|
|
6312
|
+
rateLimited: event.payload.rateLimited === true ? true : undefined,
|
|
6313
|
+
sessionId: event.sessionId,
|
|
6314
|
+
status: getString2(event.payload.providerStatus),
|
|
6315
|
+
turnId: event.turnId,
|
|
6316
|
+
type: event.type
|
|
6317
|
+
};
|
|
6318
|
+
const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
6319
|
+
return {
|
|
6320
|
+
...failure,
|
|
6321
|
+
replayHref: href
|
|
6322
|
+
};
|
|
6323
|
+
});
|
|
6324
|
+
var summarizeVoiceAssistantHealth = async (options) => {
|
|
6325
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6326
|
+
return {
|
|
6327
|
+
assistantRuns: await summarizeVoiceAssistantRuns({ events }),
|
|
6328
|
+
providerHealth: await summarizeVoiceProviderHealth({
|
|
6329
|
+
events,
|
|
6330
|
+
providers: options.providers
|
|
6331
|
+
}),
|
|
6332
|
+
recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
|
|
6333
|
+
};
|
|
6334
|
+
};
|
|
6335
|
+
var renderVoiceAssistantHealthHTML = (summary) => {
|
|
6336
|
+
const assistant = summary.assistantRuns.assistants[0];
|
|
6337
|
+
const failures = summary.recentFailures;
|
|
6338
|
+
return [
|
|
6339
|
+
'<div class="voice-assistant-health">',
|
|
6340
|
+
'<section class="voice-assistant-health-grid">',
|
|
6341
|
+
`<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
|
|
6342
|
+
`<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
|
|
6343
|
+
`<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
|
|
6344
|
+
`<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
|
|
6345
|
+
"</section>",
|
|
6346
|
+
"<section>",
|
|
6347
|
+
"<h3>Provider Health</h3>",
|
|
6348
|
+
renderVoiceProviderHealthHTML(summary.providerHealth),
|
|
6349
|
+
"</section>",
|
|
6350
|
+
'<section class="voice-assistant-health-columns">',
|
|
6351
|
+
`<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
|
|
6352
|
+
`<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
|
|
6353
|
+
`<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
|
|
6354
|
+
`<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
|
|
6355
|
+
"</section>",
|
|
6356
|
+
"<section>",
|
|
6357
|
+
"<h3>Recent Failures</h3>",
|
|
6358
|
+
failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
|
|
6359
|
+
'<div class="voice-assistant-health-failures">',
|
|
6360
|
+
...failures.map((failure) => [
|
|
6361
|
+
"<article>",
|
|
6362
|
+
`<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
|
|
6363
|
+
`<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
|
|
6364
|
+
failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
|
|
6365
|
+
`<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
|
|
6366
|
+
failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
|
|
6367
|
+
"</article>"
|
|
6368
|
+
].join("")),
|
|
6369
|
+
"</div>"
|
|
6370
|
+
].join(""),
|
|
6371
|
+
"</section>",
|
|
6372
|
+
"</div>"
|
|
6373
|
+
].join("");
|
|
6374
|
+
};
|
|
6375
|
+
var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
|
|
6376
|
+
var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
|
|
6377
|
+
const summary = await summarizeVoiceAssistantHealth(options);
|
|
6378
|
+
const render = options.render ?? renderVoiceAssistantHealthHTML;
|
|
6379
|
+
const body = await render(summary);
|
|
6380
|
+
return new Response(body, {
|
|
6381
|
+
headers: {
|
|
6382
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6383
|
+
...options.headers
|
|
6384
|
+
}
|
|
6385
|
+
});
|
|
6386
|
+
};
|
|
6387
|
+
var createVoiceAssistantHealthRoutes = (options) => {
|
|
6388
|
+
const path = options.path ?? "/api/assistant-health";
|
|
6389
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6390
|
+
const routes = new Elysia3({
|
|
6391
|
+
name: options.name ?? "absolutejs-voice-assistant-health"
|
|
6392
|
+
}).get(path, createVoiceAssistantHealthJSONHandler(options));
|
|
6393
|
+
if (htmlPath) {
|
|
6394
|
+
routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
|
|
6395
|
+
}
|
|
6396
|
+
return routes;
|
|
6397
|
+
};
|
|
6398
|
+
// src/sessionReplay.ts
|
|
6399
|
+
import { Elysia as Elysia4 } from "elysia";
|
|
5749
6400
|
|
|
5750
6401
|
// src/trace.ts
|
|
5751
6402
|
var createVoiceTraceEventId = (event) => [
|
|
@@ -6079,7 +6730,7 @@ var exportVoiceTrace = async (input) => {
|
|
|
6079
6730
|
};
|
|
6080
6731
|
};
|
|
6081
6732
|
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
6082
|
-
var
|
|
6733
|
+
var escapeHtml5 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6083
6734
|
var formatTraceValue = (value) => {
|
|
6084
6735
|
if (value === undefined || value === null) {
|
|
6085
6736
|
return "";
|
|
@@ -6357,10 +7008,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6357
7008
|
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
6358
7009
|
return [
|
|
6359
7010
|
"<tr>",
|
|
6360
|
-
`<td>${
|
|
6361
|
-
`<td>${
|
|
6362
|
-
`<td>${
|
|
6363
|
-
`<td><code>${
|
|
7011
|
+
`<td>${escapeHtml5(String(offset))}</td>`,
|
|
7012
|
+
`<td>${escapeHtml5(event.type)}</td>`,
|
|
7013
|
+
`<td>${escapeHtml5(event.turnId ?? "")}</td>`,
|
|
7014
|
+
`<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
|
|
6364
7015
|
"</tr>"
|
|
6365
7016
|
].join("");
|
|
6366
7017
|
}).join(`
|
|
@@ -6371,7 +7022,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6371
7022
|
"<head>",
|
|
6372
7023
|
'<meta charset="utf-8" />',
|
|
6373
7024
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
6374
|
-
`<title>${
|
|
7025
|
+
`<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
|
|
6375
7026
|
"<style>",
|
|
6376
7027
|
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
6377
7028
|
"main{max-width:1100px;margin:auto}",
|
|
@@ -6385,7 +7036,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6385
7036
|
"</style>",
|
|
6386
7037
|
"</head>",
|
|
6387
7038
|
"<body><main>",
|
|
6388
|
-
`<h1>${
|
|
7039
|
+
`<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
6389
7040
|
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
6390
7041
|
'<section class="summary">',
|
|
6391
7042
|
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
@@ -6399,7 +7050,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6399
7050
|
eventRows,
|
|
6400
7051
|
"</tbody></table>",
|
|
6401
7052
|
"<h2>Markdown Export</h2>",
|
|
6402
|
-
`<pre>${
|
|
7053
|
+
`<pre>${escapeHtml5(markdown)}</pre>`,
|
|
6403
7054
|
"</main></body></html>"
|
|
6404
7055
|
].join(`
|
|
6405
7056
|
`);
|
|
@@ -6411,7 +7062,119 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
|
6411
7062
|
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
6412
7063
|
});
|
|
6413
7064
|
|
|
7065
|
+
// src/sessionReplay.ts
|
|
7066
|
+
var getString3 = (value) => typeof value === "string" ? value : undefined;
|
|
7067
|
+
var buildReplayTurns = (events) => {
|
|
7068
|
+
const turns = new Map;
|
|
7069
|
+
const getTurn = (turnId) => {
|
|
7070
|
+
const existing = turns.get(turnId);
|
|
7071
|
+
if (existing) {
|
|
7072
|
+
return existing;
|
|
7073
|
+
}
|
|
7074
|
+
const turn = {
|
|
7075
|
+
assistantReplies: [],
|
|
7076
|
+
errors: [],
|
|
7077
|
+
id: turnId,
|
|
7078
|
+
modelCalls: [],
|
|
7079
|
+
tools: [],
|
|
7080
|
+
transcripts: []
|
|
7081
|
+
};
|
|
7082
|
+
turns.set(turnId, turn);
|
|
7083
|
+
return turn;
|
|
7084
|
+
};
|
|
7085
|
+
for (const event of events) {
|
|
7086
|
+
const turnId = event.turnId ?? "session";
|
|
7087
|
+
const turn = getTurn(turnId);
|
|
7088
|
+
switch (event.type) {
|
|
7089
|
+
case "turn.transcript":
|
|
7090
|
+
turn.transcripts.push({
|
|
7091
|
+
isFinal: event.payload.isFinal === true,
|
|
7092
|
+
text: getString3(event.payload.text)
|
|
7093
|
+
});
|
|
7094
|
+
break;
|
|
7095
|
+
case "turn.committed":
|
|
7096
|
+
turn.committedText = getString3(event.payload.text);
|
|
7097
|
+
break;
|
|
7098
|
+
case "turn.assistant": {
|
|
7099
|
+
const text = getString3(event.payload.text);
|
|
7100
|
+
if (text) {
|
|
7101
|
+
turn.assistantReplies.push(text);
|
|
7102
|
+
}
|
|
7103
|
+
break;
|
|
7104
|
+
}
|
|
7105
|
+
case "agent.model":
|
|
7106
|
+
case "assistant.run":
|
|
7107
|
+
turn.modelCalls.push(event.payload);
|
|
7108
|
+
break;
|
|
7109
|
+
case "agent.tool":
|
|
7110
|
+
turn.tools.push(event.payload);
|
|
7111
|
+
break;
|
|
7112
|
+
case "session.error":
|
|
7113
|
+
turn.errors.push(event.payload);
|
|
7114
|
+
break;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
return [...turns.values()];
|
|
7118
|
+
};
|
|
7119
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
7120
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
7121
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
7122
|
+
sessionId: options.sessionId
|
|
7123
|
+
});
|
|
7124
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
7125
|
+
evaluation: options.evaluation,
|
|
7126
|
+
redact: options.redact,
|
|
7127
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
7128
|
+
});
|
|
7129
|
+
const startedAt = replay.summary.startedAt;
|
|
7130
|
+
return {
|
|
7131
|
+
evaluation: replay.evaluation,
|
|
7132
|
+
events,
|
|
7133
|
+
html: replay.html,
|
|
7134
|
+
markdown: replay.markdown,
|
|
7135
|
+
sessionId: options.sessionId,
|
|
7136
|
+
summary: replay.summary,
|
|
7137
|
+
timeline: events.map((event) => ({
|
|
7138
|
+
at: event.at,
|
|
7139
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
7140
|
+
payload: event.payload,
|
|
7141
|
+
turnId: event.turnId,
|
|
7142
|
+
type: event.type
|
|
7143
|
+
})),
|
|
7144
|
+
turns: buildReplayTurns(events)
|
|
7145
|
+
};
|
|
7146
|
+
};
|
|
7147
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
7148
|
+
...options,
|
|
7149
|
+
sessionId: params.sessionId ?? ""
|
|
7150
|
+
});
|
|
7151
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
7152
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
7153
|
+
...options,
|
|
7154
|
+
sessionId: params.sessionId ?? ""
|
|
7155
|
+
});
|
|
7156
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
7157
|
+
return new Response(body, {
|
|
7158
|
+
headers: {
|
|
7159
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7160
|
+
...options.headers
|
|
7161
|
+
}
|
|
7162
|
+
});
|
|
7163
|
+
};
|
|
7164
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
7165
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
7166
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7167
|
+
const routes = new Elysia4({
|
|
7168
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
7169
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
7170
|
+
if (htmlPath) {
|
|
7171
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
7172
|
+
}
|
|
7173
|
+
return routes;
|
|
7174
|
+
};
|
|
6414
7175
|
// src/fileStore.ts
|
|
7176
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
7177
|
+
import { join } from "path";
|
|
6415
7178
|
var listJsonFiles = async (directory) => {
|
|
6416
7179
|
try {
|
|
6417
7180
|
const entries = await readdir(directory, {
|
|
@@ -6427,6 +7190,7 @@ var listJsonFiles = async (directory) => {
|
|
|
6427
7190
|
};
|
|
6428
7191
|
var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
|
|
6429
7192
|
var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
|
|
7193
|
+
var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
6430
7194
|
var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
6431
7195
|
var writeJsonFile = async (path, value, options) => {
|
|
6432
7196
|
await mkdir(options.directory, {
|
|
@@ -6646,18 +7410,56 @@ var createVoiceFileTraceSinkDeliveryStore = (options) => {
|
|
|
6646
7410
|
};
|
|
6647
7411
|
return { get, list, remove, set };
|
|
6648
7412
|
};
|
|
6649
|
-
var
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
7413
|
+
var createVoiceFileAssistantMemoryStore = (options) => {
|
|
7414
|
+
const get = async (input) => {
|
|
7415
|
+
const path = resolveFilePath(options.directory, createMemoryStoreId(input));
|
|
7416
|
+
try {
|
|
7417
|
+
return await readJsonFile(path);
|
|
7418
|
+
} catch (error) {
|
|
7419
|
+
if (error.code === "ENOENT") {
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
throw error;
|
|
7423
|
+
}
|
|
7424
|
+
};
|
|
7425
|
+
const list = async (input) => {
|
|
7426
|
+
const files = await listJsonFiles(options.directory);
|
|
7427
|
+
const records = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7428
|
+
return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
7429
|
+
};
|
|
7430
|
+
const set = async (input) => {
|
|
7431
|
+
const existing = await get(input);
|
|
7432
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
7433
|
+
...input,
|
|
7434
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
7435
|
+
updatedAt: input.updatedAt
|
|
7436
|
+
});
|
|
7437
|
+
await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
|
|
7438
|
+
return record;
|
|
7439
|
+
};
|
|
7440
|
+
const remove = async (input) => {
|
|
7441
|
+
await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
|
|
7442
|
+
force: true
|
|
7443
|
+
});
|
|
7444
|
+
};
|
|
7445
|
+
return { delete: remove, get, list, set };
|
|
7446
|
+
};
|
|
7447
|
+
var createVoiceFileRuntimeStorage = (options) => ({
|
|
7448
|
+
events: createVoiceFileIntegrationEventStore({
|
|
7449
|
+
...options,
|
|
7450
|
+
directory: join(options.directory, "events")
|
|
7451
|
+
}),
|
|
7452
|
+
externalObjects: createVoiceFileExternalObjectMapStore({
|
|
7453
|
+
...options,
|
|
7454
|
+
directory: join(options.directory, "external-objects")
|
|
7455
|
+
}),
|
|
7456
|
+
memories: createVoiceFileAssistantMemoryStore({
|
|
7457
|
+
...options,
|
|
7458
|
+
directory: join(options.directory, "memories")
|
|
7459
|
+
}),
|
|
7460
|
+
reviews: createVoiceFileReviewStore({
|
|
7461
|
+
...options,
|
|
7462
|
+
directory: join(options.directory, "reviews")
|
|
6661
7463
|
}),
|
|
6662
7464
|
session: createVoiceFileSessionStore({
|
|
6663
7465
|
...options,
|
|
@@ -6687,6 +7489,777 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
|
|
|
6687
7489
|
sourceId: mapping.sourceId,
|
|
6688
7490
|
sourceType: mapping.sourceType
|
|
6689
7491
|
});
|
|
7492
|
+
// src/modelAdapters.ts
|
|
7493
|
+
var OUTPUT_SCHEMA = {
|
|
7494
|
+
additionalProperties: false,
|
|
7495
|
+
properties: {
|
|
7496
|
+
assistantText: {
|
|
7497
|
+
type: "string"
|
|
7498
|
+
},
|
|
7499
|
+
complete: {
|
|
7500
|
+
type: "boolean"
|
|
7501
|
+
},
|
|
7502
|
+
escalate: {
|
|
7503
|
+
additionalProperties: false,
|
|
7504
|
+
properties: {
|
|
7505
|
+
metadata: {
|
|
7506
|
+
additionalProperties: true,
|
|
7507
|
+
type: "object"
|
|
7508
|
+
},
|
|
7509
|
+
reason: {
|
|
7510
|
+
type: "string"
|
|
7511
|
+
}
|
|
7512
|
+
},
|
|
7513
|
+
required: ["reason"],
|
|
7514
|
+
type: "object"
|
|
7515
|
+
},
|
|
7516
|
+
noAnswer: {
|
|
7517
|
+
additionalProperties: false,
|
|
7518
|
+
properties: {
|
|
7519
|
+
metadata: {
|
|
7520
|
+
additionalProperties: true,
|
|
7521
|
+
type: "object"
|
|
7522
|
+
}
|
|
7523
|
+
},
|
|
7524
|
+
type: "object"
|
|
7525
|
+
},
|
|
7526
|
+
result: {
|
|
7527
|
+
additionalProperties: true,
|
|
7528
|
+
type: "object"
|
|
7529
|
+
},
|
|
7530
|
+
transfer: {
|
|
7531
|
+
additionalProperties: false,
|
|
7532
|
+
properties: {
|
|
7533
|
+
metadata: {
|
|
7534
|
+
additionalProperties: true,
|
|
7535
|
+
type: "object"
|
|
7536
|
+
},
|
|
7537
|
+
reason: {
|
|
7538
|
+
type: "string"
|
|
7539
|
+
},
|
|
7540
|
+
target: {
|
|
7541
|
+
type: "string"
|
|
7542
|
+
}
|
|
7543
|
+
},
|
|
7544
|
+
required: ["target"],
|
|
7545
|
+
type: "object"
|
|
7546
|
+
},
|
|
7547
|
+
voicemail: {
|
|
7548
|
+
additionalProperties: false,
|
|
7549
|
+
properties: {
|
|
7550
|
+
metadata: {
|
|
7551
|
+
additionalProperties: true,
|
|
7552
|
+
type: "object"
|
|
7553
|
+
}
|
|
7554
|
+
},
|
|
7555
|
+
type: "object"
|
|
7556
|
+
}
|
|
7557
|
+
},
|
|
7558
|
+
type: "object"
|
|
7559
|
+
};
|
|
7560
|
+
var ROUTE_RESULT_INSTRUCTION = "Return only a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools. Only set transfer, escalate, voicemail, or noAnswer when the user explicitly asks for that lifecycle outcome or a tool result says that exact outcome. Do not infer voicemail from generic words like voice, voice app, or voice integration.";
|
|
7561
|
+
var stripJSONCodeFence = (value) => {
|
|
7562
|
+
const trimmed = value.trim();
|
|
7563
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
7564
|
+
return match?.[1]?.trim() ?? value;
|
|
7565
|
+
};
|
|
7566
|
+
var parseJSON = (value) => {
|
|
7567
|
+
try {
|
|
7568
|
+
const parsed = JSON.parse(stripJSONCodeFence(value));
|
|
7569
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
7570
|
+
} catch {
|
|
7571
|
+
return {
|
|
7572
|
+
assistantText: value
|
|
7573
|
+
};
|
|
7574
|
+
}
|
|
7575
|
+
};
|
|
7576
|
+
var parseJSONValue = (value) => {
|
|
7577
|
+
try {
|
|
7578
|
+
return JSON.parse(value);
|
|
7579
|
+
} catch {
|
|
7580
|
+
return value;
|
|
7581
|
+
}
|
|
7582
|
+
};
|
|
7583
|
+
var getMessageToolCalls = (message) => {
|
|
7584
|
+
const toolCalls = message.metadata?.toolCalls;
|
|
7585
|
+
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
7586
|
+
};
|
|
7587
|
+
var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
|
|
7588
|
+
var sleep4 = (ms) => new Promise((resolve2) => {
|
|
7589
|
+
setTimeout(resolve2, ms);
|
|
7590
|
+
});
|
|
7591
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
7592
|
+
var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
|
|
7593
|
+
var normalizeRouteOutput = (output) => {
|
|
7594
|
+
const result = {};
|
|
7595
|
+
if (typeof output.assistantText === "string") {
|
|
7596
|
+
result.assistantText = output.assistantText;
|
|
7597
|
+
}
|
|
7598
|
+
if (typeof output.complete === "boolean") {
|
|
7599
|
+
result.complete = output.complete;
|
|
7600
|
+
}
|
|
7601
|
+
if (output.result !== undefined) {
|
|
7602
|
+
result.result = output.result;
|
|
7603
|
+
}
|
|
7604
|
+
if (output.transfer && typeof output.transfer === "object") {
|
|
7605
|
+
const transfer = output.transfer;
|
|
7606
|
+
if (typeof transfer.target === "string") {
|
|
7607
|
+
result.transfer = {
|
|
7608
|
+
metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
|
|
7609
|
+
reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
|
|
7610
|
+
target: transfer.target
|
|
7611
|
+
};
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
if (output.escalate && typeof output.escalate === "object") {
|
|
7615
|
+
const escalate = output.escalate;
|
|
7616
|
+
if (typeof escalate.reason === "string") {
|
|
7617
|
+
result.escalate = {
|
|
7618
|
+
metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
|
|
7619
|
+
reason: escalate.reason
|
|
7620
|
+
};
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
if (output.voicemail && typeof output.voicemail === "object") {
|
|
7624
|
+
const voicemail = output.voicemail;
|
|
7625
|
+
result.voicemail = {
|
|
7626
|
+
metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
|
|
7627
|
+
};
|
|
7628
|
+
}
|
|
7629
|
+
if (output.noAnswer && typeof output.noAnswer === "object") {
|
|
7630
|
+
const noAnswer = output.noAnswer;
|
|
7631
|
+
result.noAnswer = {
|
|
7632
|
+
metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
|
|
7633
|
+
};
|
|
7634
|
+
}
|
|
7635
|
+
return result;
|
|
7636
|
+
};
|
|
7637
|
+
var createJSONVoiceAssistantModel = (options) => ({
|
|
7638
|
+
generate: async (input) => {
|
|
7639
|
+
const output = await options.generate(input);
|
|
7640
|
+
if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
|
|
7641
|
+
return output;
|
|
7642
|
+
}
|
|
7643
|
+
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
7644
|
+
}
|
|
7645
|
+
});
|
|
7646
|
+
var createVoiceProviderRouter = (options) => {
|
|
7647
|
+
const providerIds = Object.keys(options.providers);
|
|
7648
|
+
const firstProvider = providerIds[0];
|
|
7649
|
+
const policy = typeof options.policy === "string" ? {
|
|
7650
|
+
strategy: options.policy
|
|
7651
|
+
} : options.policy;
|
|
7652
|
+
const strategy = policy?.strategy ?? "prefer-selected";
|
|
7653
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
7654
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
7655
|
+
const healthState = new Map;
|
|
7656
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
7657
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
7658
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
7659
|
+
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
7660
|
+
const getHealth = (provider) => {
|
|
7661
|
+
const existing = healthState.get(provider);
|
|
7662
|
+
if (existing) {
|
|
7663
|
+
return existing;
|
|
7664
|
+
}
|
|
7665
|
+
const next = {
|
|
7666
|
+
consecutiveFailures: 0,
|
|
7667
|
+
provider,
|
|
7668
|
+
status: "healthy"
|
|
7669
|
+
};
|
|
7670
|
+
healthState.set(provider, next);
|
|
7671
|
+
return next;
|
|
7672
|
+
};
|
|
7673
|
+
const cloneHealth = (provider) => {
|
|
7674
|
+
if (!healthOptions) {
|
|
7675
|
+
return;
|
|
7676
|
+
}
|
|
7677
|
+
return {
|
|
7678
|
+
...getHealth(provider)
|
|
7679
|
+
};
|
|
7680
|
+
};
|
|
7681
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
7682
|
+
if (!healthOptions) {
|
|
7683
|
+
return;
|
|
7684
|
+
}
|
|
7685
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
7686
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
7687
|
+
};
|
|
7688
|
+
const isSuppressed = (provider) => {
|
|
7689
|
+
if (!healthOptions) {
|
|
7690
|
+
return false;
|
|
7691
|
+
}
|
|
7692
|
+
const health = getHealth(provider);
|
|
7693
|
+
return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
|
|
7694
|
+
};
|
|
7695
|
+
const recordProviderSuccess = (provider) => {
|
|
7696
|
+
if (!healthOptions) {
|
|
7697
|
+
return;
|
|
7698
|
+
}
|
|
7699
|
+
const health = getHealth(provider);
|
|
7700
|
+
health.consecutiveFailures = 0;
|
|
7701
|
+
health.status = "healthy";
|
|
7702
|
+
health.suppressedUntil = undefined;
|
|
7703
|
+
return cloneHealth(provider);
|
|
7704
|
+
};
|
|
7705
|
+
const recordProviderError = (provider, isProviderError, rateLimited) => {
|
|
7706
|
+
if (!healthOptions || !isProviderError) {
|
|
7707
|
+
return cloneHealth(provider);
|
|
7708
|
+
}
|
|
7709
|
+
const currentTime = now();
|
|
7710
|
+
const health = getHealth(provider);
|
|
7711
|
+
health.consecutiveFailures += 1;
|
|
7712
|
+
health.lastFailureAt = currentTime;
|
|
7713
|
+
if (rateLimited) {
|
|
7714
|
+
health.lastRateLimitedAt = currentTime;
|
|
7715
|
+
}
|
|
7716
|
+
if (rateLimited || health.consecutiveFailures >= failureThreshold) {
|
|
7717
|
+
health.status = "suppressed";
|
|
7718
|
+
health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
|
|
7719
|
+
}
|
|
7720
|
+
return cloneHealth(provider);
|
|
7721
|
+
};
|
|
7722
|
+
const resolveAllowedProviders = async (input) => {
|
|
7723
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
7724
|
+
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
7725
|
+
return new Set(allowed ?? providerIds);
|
|
7726
|
+
};
|
|
7727
|
+
const sortProviders = (providers) => {
|
|
7728
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
7729
|
+
return providers;
|
|
7730
|
+
}
|
|
7731
|
+
return [...providers].sort((left, right) => {
|
|
7732
|
+
const leftProfile = options.providerProfiles?.[left];
|
|
7733
|
+
const rightProfile = options.providerProfiles?.[right];
|
|
7734
|
+
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
7735
|
+
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
7736
|
+
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
7737
|
+
});
|
|
7738
|
+
};
|
|
7739
|
+
const resolveOrder = async (input) => {
|
|
7740
|
+
const selectedProvider = await options.selectProvider?.(input);
|
|
7741
|
+
const allowedProviders = await resolveAllowedProviders(input);
|
|
7742
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
7743
|
+
const rankedProviders = sortProviders([
|
|
7744
|
+
...fallbackOrder ?? providerIds
|
|
7745
|
+
]).filter((provider) => allowedProviders.has(provider));
|
|
7746
|
+
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
7747
|
+
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
7748
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
7749
|
+
const seen = new Set;
|
|
7750
|
+
const order = [];
|
|
7751
|
+
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
7752
|
+
preferred,
|
|
7753
|
+
...candidateRankedProviders,
|
|
7754
|
+
...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
|
|
7755
|
+
];
|
|
7756
|
+
for (const provider of candidates) {
|
|
7757
|
+
if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
|
|
7758
|
+
continue;
|
|
7759
|
+
}
|
|
7760
|
+
seen.add(provider);
|
|
7761
|
+
order.push(provider);
|
|
7762
|
+
}
|
|
7763
|
+
return {
|
|
7764
|
+
order,
|
|
7765
|
+
selectedProvider: preferred
|
|
7766
|
+
};
|
|
7767
|
+
};
|
|
7768
|
+
const emit = async (event, input) => {
|
|
7769
|
+
await options.onProviderEvent?.(event, input);
|
|
7770
|
+
};
|
|
7771
|
+
return {
|
|
7772
|
+
generate: async (input) => {
|
|
7773
|
+
const { order, selectedProvider } = await resolveOrder(input);
|
|
7774
|
+
if (!selectedProvider || order.length === 0) {
|
|
7775
|
+
throw new Error("Voice provider router has no available providers.");
|
|
7776
|
+
}
|
|
7777
|
+
let lastError;
|
|
7778
|
+
for (const [index, provider] of order.entries()) {
|
|
7779
|
+
const model = options.providers[provider];
|
|
7780
|
+
if (!model) {
|
|
7781
|
+
continue;
|
|
7782
|
+
}
|
|
7783
|
+
const startedAt = Date.now();
|
|
7784
|
+
try {
|
|
7785
|
+
const output = await model.generate(input);
|
|
7786
|
+
const providerHealth = recordProviderSuccess(provider);
|
|
7787
|
+
await emit({
|
|
7788
|
+
at: Date.now(),
|
|
7789
|
+
elapsedMs: Date.now() - startedAt,
|
|
7790
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
7791
|
+
provider,
|
|
7792
|
+
providerHealth,
|
|
7793
|
+
recovered: provider !== selectedProvider,
|
|
7794
|
+
selectedProvider,
|
|
7795
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
7796
|
+
}, input);
|
|
7797
|
+
return output;
|
|
7798
|
+
} catch (error) {
|
|
7799
|
+
lastError = error;
|
|
7800
|
+
const hasNextProvider = index < order.length - 1;
|
|
7801
|
+
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
7802
|
+
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
7803
|
+
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
7804
|
+
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
7805
|
+
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
7806
|
+
await emit({
|
|
7807
|
+
at: Date.now(),
|
|
7808
|
+
elapsedMs: Date.now() - startedAt,
|
|
7809
|
+
error: errorMessage(error),
|
|
7810
|
+
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
7811
|
+
provider,
|
|
7812
|
+
providerHealth,
|
|
7813
|
+
rateLimited,
|
|
7814
|
+
selectedProvider,
|
|
7815
|
+
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
7816
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
7817
|
+
status: "error"
|
|
7818
|
+
}, input);
|
|
7819
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
7820
|
+
throw error;
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
throw lastError ?? new Error("Voice provider router did not run a provider.");
|
|
7825
|
+
}
|
|
7826
|
+
};
|
|
7827
|
+
};
|
|
7828
|
+
var messageToOpenAIInput = (message) => {
|
|
7829
|
+
if (message.role === "tool") {
|
|
7830
|
+
return [
|
|
7831
|
+
{
|
|
7832
|
+
call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
|
|
7833
|
+
output: message.content,
|
|
7834
|
+
type: "function_call_output"
|
|
7835
|
+
}
|
|
7836
|
+
];
|
|
7837
|
+
}
|
|
7838
|
+
const toolCalls = getMessageToolCalls(message);
|
|
7839
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
7840
|
+
return toolCalls.map((toolCall) => ({
|
|
7841
|
+
arguments: JSON.stringify(toolCall.args),
|
|
7842
|
+
call_id: toolCall.id ?? crypto.randomUUID(),
|
|
7843
|
+
name: toolCall.name,
|
|
7844
|
+
type: "function_call"
|
|
7845
|
+
}));
|
|
7846
|
+
}
|
|
7847
|
+
return [
|
|
7848
|
+
{
|
|
7849
|
+
content: message.content,
|
|
7850
|
+
role: message.role === "system" ? "developer" : message.role
|
|
7851
|
+
}
|
|
7852
|
+
];
|
|
7853
|
+
};
|
|
7854
|
+
var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
|
|
7855
|
+
var messageToAnthropicMessage = (message) => {
|
|
7856
|
+
if (message.role === "system") {
|
|
7857
|
+
return;
|
|
7858
|
+
}
|
|
7859
|
+
if (message.role === "tool") {
|
|
7860
|
+
if (!message.toolCallId) {
|
|
7861
|
+
return {
|
|
7862
|
+
content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
|
|
7863
|
+
role: "user"
|
|
7864
|
+
};
|
|
7865
|
+
}
|
|
7866
|
+
return {
|
|
7867
|
+
content: [
|
|
7868
|
+
{
|
|
7869
|
+
content: message.content,
|
|
7870
|
+
tool_use_id: message.toolCallId,
|
|
7871
|
+
type: "tool_result"
|
|
7872
|
+
}
|
|
7873
|
+
],
|
|
7874
|
+
role: "user"
|
|
7875
|
+
};
|
|
7876
|
+
}
|
|
7877
|
+
const toolCalls = getMessageToolCalls(message);
|
|
7878
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
7879
|
+
return {
|
|
7880
|
+
content: [
|
|
7881
|
+
...message.content ? [
|
|
7882
|
+
{
|
|
7883
|
+
text: message.content,
|
|
7884
|
+
type: "text"
|
|
7885
|
+
}
|
|
7886
|
+
] : [],
|
|
7887
|
+
...toolCalls.map((toolCall) => ({
|
|
7888
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
7889
|
+
input: toolCall.args,
|
|
7890
|
+
name: toolCall.name,
|
|
7891
|
+
type: "tool_use"
|
|
7892
|
+
}))
|
|
7893
|
+
],
|
|
7894
|
+
role: "assistant"
|
|
7895
|
+
};
|
|
7896
|
+
}
|
|
7897
|
+
return {
|
|
7898
|
+
content: message.content,
|
|
7899
|
+
role: message.role
|
|
7900
|
+
};
|
|
7901
|
+
};
|
|
7902
|
+
var toGeminiSchema = (schema) => {
|
|
7903
|
+
const next = {};
|
|
7904
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
7905
|
+
if (key === "additionalProperties") {
|
|
7906
|
+
continue;
|
|
7907
|
+
}
|
|
7908
|
+
if (key === "type" && typeof value === "string") {
|
|
7909
|
+
next[key] = value.toUpperCase();
|
|
7910
|
+
continue;
|
|
7911
|
+
}
|
|
7912
|
+
if (Array.isArray(value)) {
|
|
7913
|
+
next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
|
|
7914
|
+
continue;
|
|
7915
|
+
}
|
|
7916
|
+
if (value && typeof value === "object") {
|
|
7917
|
+
next[key] = toGeminiSchema(value);
|
|
7918
|
+
continue;
|
|
7919
|
+
}
|
|
7920
|
+
next[key] = value;
|
|
7921
|
+
}
|
|
7922
|
+
return next;
|
|
7923
|
+
};
|
|
7924
|
+
var messageToGeminiContent = (message) => {
|
|
7925
|
+
if (message.role === "system") {
|
|
7926
|
+
return;
|
|
7927
|
+
}
|
|
7928
|
+
if (message.role === "tool") {
|
|
7929
|
+
return {
|
|
7930
|
+
parts: [
|
|
7931
|
+
{
|
|
7932
|
+
functionResponse: {
|
|
7933
|
+
id: message.toolCallId,
|
|
7934
|
+
name: message.name ?? "tool",
|
|
7935
|
+
response: {
|
|
7936
|
+
result: parseJSONValue(message.content)
|
|
7937
|
+
}
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7940
|
+
],
|
|
7941
|
+
role: "user"
|
|
7942
|
+
};
|
|
7943
|
+
}
|
|
7944
|
+
const toolCalls = getMessageToolCalls(message);
|
|
7945
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
7946
|
+
return {
|
|
7947
|
+
parts: [
|
|
7948
|
+
...message.content ? [
|
|
7949
|
+
{
|
|
7950
|
+
text: message.content
|
|
7951
|
+
}
|
|
7952
|
+
] : [],
|
|
7953
|
+
...toolCalls.map((toolCall) => ({
|
|
7954
|
+
functionCall: {
|
|
7955
|
+
args: toolCall.args,
|
|
7956
|
+
id: toolCall.id,
|
|
7957
|
+
name: toolCall.name
|
|
7958
|
+
}
|
|
7959
|
+
}))
|
|
7960
|
+
],
|
|
7961
|
+
role: "model"
|
|
7962
|
+
};
|
|
7963
|
+
}
|
|
7964
|
+
return {
|
|
7965
|
+
parts: [
|
|
7966
|
+
{
|
|
7967
|
+
text: message.content
|
|
7968
|
+
}
|
|
7969
|
+
],
|
|
7970
|
+
role: message.role === "assistant" ? "model" : "user"
|
|
7971
|
+
};
|
|
7972
|
+
};
|
|
7973
|
+
var extractText = (response) => {
|
|
7974
|
+
if (typeof response.output_text === "string") {
|
|
7975
|
+
return response.output_text;
|
|
7976
|
+
}
|
|
7977
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
7978
|
+
for (const item of output) {
|
|
7979
|
+
if (!item || typeof item !== "object") {
|
|
7980
|
+
continue;
|
|
7981
|
+
}
|
|
7982
|
+
const record = item;
|
|
7983
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
7984
|
+
for (const contentItem of content) {
|
|
7985
|
+
if (!contentItem || typeof contentItem !== "object") {
|
|
7986
|
+
continue;
|
|
7987
|
+
}
|
|
7988
|
+
const contentRecord = contentItem;
|
|
7989
|
+
if (typeof contentRecord.text === "string") {
|
|
7990
|
+
return contentRecord.text;
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
return "";
|
|
7995
|
+
};
|
|
7996
|
+
var extractToolCalls = (response) => {
|
|
7997
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
7998
|
+
const toolCalls = [];
|
|
7999
|
+
for (const item of output) {
|
|
8000
|
+
if (!item || typeof item !== "object") {
|
|
8001
|
+
continue;
|
|
8002
|
+
}
|
|
8003
|
+
const record = item;
|
|
8004
|
+
if (record.type !== "function_call" || typeof record.name !== "string") {
|
|
8005
|
+
continue;
|
|
8006
|
+
}
|
|
8007
|
+
const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
|
|
8008
|
+
toolCalls.push({
|
|
8009
|
+
args,
|
|
8010
|
+
id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
|
|
8011
|
+
name: record.name
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
return toolCalls;
|
|
8015
|
+
};
|
|
8016
|
+
var createOpenAIVoiceAssistantModel = (options) => {
|
|
8017
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8018
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
8019
|
+
const model = options.model ?? "gpt-4.1-mini";
|
|
8020
|
+
return {
|
|
8021
|
+
generate: async (input) => {
|
|
8022
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
|
|
8023
|
+
body: JSON.stringify({
|
|
8024
|
+
input: messagesToOpenAIInput(input.messages),
|
|
8025
|
+
instructions: [
|
|
8026
|
+
input.system,
|
|
8027
|
+
"Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
|
|
8028
|
+
].filter(Boolean).join(`
|
|
8029
|
+
|
|
8030
|
+
`),
|
|
8031
|
+
max_output_tokens: options.maxOutputTokens,
|
|
8032
|
+
model,
|
|
8033
|
+
temperature: options.temperature,
|
|
8034
|
+
text: {
|
|
8035
|
+
format: {
|
|
8036
|
+
name: "voice_route_result",
|
|
8037
|
+
schema: OUTPUT_SCHEMA,
|
|
8038
|
+
strict: false,
|
|
8039
|
+
type: "json_schema"
|
|
8040
|
+
}
|
|
8041
|
+
},
|
|
8042
|
+
tool_choice: input.tools.length ? "auto" : "none",
|
|
8043
|
+
tools: input.tools.map((tool) => ({
|
|
8044
|
+
description: tool.description,
|
|
8045
|
+
name: tool.name,
|
|
8046
|
+
parameters: tool.parameters ?? {
|
|
8047
|
+
additionalProperties: true,
|
|
8048
|
+
type: "object"
|
|
8049
|
+
},
|
|
8050
|
+
strict: false,
|
|
8051
|
+
type: "function"
|
|
8052
|
+
}))
|
|
8053
|
+
}),
|
|
8054
|
+
headers: {
|
|
8055
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
8056
|
+
"content-type": "application/json"
|
|
8057
|
+
},
|
|
8058
|
+
method: "POST"
|
|
8059
|
+
});
|
|
8060
|
+
if (!response.ok) {
|
|
8061
|
+
throw createHTTPError("OpenAI", response);
|
|
8062
|
+
}
|
|
8063
|
+
const body = await response.json();
|
|
8064
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8065
|
+
await options.onUsage?.(body.usage);
|
|
8066
|
+
}
|
|
8067
|
+
const toolCalls = extractToolCalls(body);
|
|
8068
|
+
if (toolCalls.length) {
|
|
8069
|
+
return {
|
|
8070
|
+
toolCalls
|
|
8071
|
+
};
|
|
8072
|
+
}
|
|
8073
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
8074
|
+
}
|
|
8075
|
+
};
|
|
8076
|
+
};
|
|
8077
|
+
var extractAnthropicText = (response) => {
|
|
8078
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8079
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
8080
|
+
`);
|
|
8081
|
+
};
|
|
8082
|
+
var extractAnthropicToolCalls = (response) => {
|
|
8083
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8084
|
+
const toolCalls = [];
|
|
8085
|
+
for (const item of content) {
|
|
8086
|
+
if (!item || typeof item !== "object") {
|
|
8087
|
+
continue;
|
|
8088
|
+
}
|
|
8089
|
+
const record = item;
|
|
8090
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
8091
|
+
continue;
|
|
8092
|
+
}
|
|
8093
|
+
toolCalls.push({
|
|
8094
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
8095
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8096
|
+
name: record.name
|
|
8097
|
+
});
|
|
8098
|
+
}
|
|
8099
|
+
return toolCalls;
|
|
8100
|
+
};
|
|
8101
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
8102
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8103
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
8104
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
8105
|
+
return {
|
|
8106
|
+
generate: async (input) => {
|
|
8107
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
8108
|
+
body: JSON.stringify({
|
|
8109
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
8110
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
8111
|
+
model,
|
|
8112
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8113
|
+
|
|
8114
|
+
`),
|
|
8115
|
+
temperature: options.temperature,
|
|
8116
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
8117
|
+
tools: input.tools.map((tool) => ({
|
|
8118
|
+
description: tool.description,
|
|
8119
|
+
input_schema: tool.parameters ?? {
|
|
8120
|
+
additionalProperties: true,
|
|
8121
|
+
type: "object"
|
|
8122
|
+
},
|
|
8123
|
+
name: tool.name
|
|
8124
|
+
}))
|
|
8125
|
+
}),
|
|
8126
|
+
headers: {
|
|
8127
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
8128
|
+
"content-type": "application/json",
|
|
8129
|
+
"x-api-key": options.apiKey
|
|
8130
|
+
},
|
|
8131
|
+
method: "POST"
|
|
8132
|
+
});
|
|
8133
|
+
if (!response.ok) {
|
|
8134
|
+
throw createHTTPError("Anthropic", response);
|
|
8135
|
+
}
|
|
8136
|
+
const body = await response.json();
|
|
8137
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8138
|
+
await options.onUsage?.(body.usage);
|
|
8139
|
+
}
|
|
8140
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
8141
|
+
if (toolCalls.length) {
|
|
8142
|
+
return {
|
|
8143
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
8144
|
+
toolCalls
|
|
8145
|
+
};
|
|
8146
|
+
}
|
|
8147
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
8148
|
+
}
|
|
8149
|
+
};
|
|
8150
|
+
};
|
|
8151
|
+
var extractGeminiCandidateParts = (response) => {
|
|
8152
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
8153
|
+
const first = candidates[0];
|
|
8154
|
+
if (!first || typeof first !== "object") {
|
|
8155
|
+
return [];
|
|
8156
|
+
}
|
|
8157
|
+
const content = first.content;
|
|
8158
|
+
if (!content || typeof content !== "object") {
|
|
8159
|
+
return [];
|
|
8160
|
+
}
|
|
8161
|
+
const parts = content.parts;
|
|
8162
|
+
return Array.isArray(parts) ? parts : [];
|
|
8163
|
+
};
|
|
8164
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
8165
|
+
`);
|
|
8166
|
+
var extractGeminiToolCalls = (response) => {
|
|
8167
|
+
const toolCalls = [];
|
|
8168
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
8169
|
+
if (!part || typeof part !== "object") {
|
|
8170
|
+
continue;
|
|
8171
|
+
}
|
|
8172
|
+
const functionCall = part.functionCall;
|
|
8173
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
8174
|
+
continue;
|
|
8175
|
+
}
|
|
8176
|
+
const record = functionCall;
|
|
8177
|
+
if (typeof record.name !== "string") {
|
|
8178
|
+
continue;
|
|
8179
|
+
}
|
|
8180
|
+
toolCalls.push({
|
|
8181
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
8182
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8183
|
+
name: record.name
|
|
8184
|
+
});
|
|
8185
|
+
}
|
|
8186
|
+
return toolCalls;
|
|
8187
|
+
};
|
|
8188
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
8189
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8190
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
8191
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
8192
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
8193
|
+
return {
|
|
8194
|
+
generate: async (input) => {
|
|
8195
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
8196
|
+
let response;
|
|
8197
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
8198
|
+
response = await fetchImpl(endpoint, {
|
|
8199
|
+
body: JSON.stringify({
|
|
8200
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
8201
|
+
generationConfig: {
|
|
8202
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
8203
|
+
...input.tools.length ? {} : {
|
|
8204
|
+
responseMimeType: "application/json",
|
|
8205
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
8206
|
+
},
|
|
8207
|
+
temperature: options.temperature
|
|
8208
|
+
},
|
|
8209
|
+
systemInstruction: {
|
|
8210
|
+
parts: [
|
|
8211
|
+
{
|
|
8212
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8213
|
+
|
|
8214
|
+
`)
|
|
8215
|
+
}
|
|
8216
|
+
]
|
|
8217
|
+
},
|
|
8218
|
+
tools: input.tools.length ? [
|
|
8219
|
+
{
|
|
8220
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
8221
|
+
description: tool.description,
|
|
8222
|
+
name: tool.name,
|
|
8223
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
8224
|
+
additionalProperties: true,
|
|
8225
|
+
type: "object"
|
|
8226
|
+
})
|
|
8227
|
+
}))
|
|
8228
|
+
}
|
|
8229
|
+
] : undefined
|
|
8230
|
+
}),
|
|
8231
|
+
headers: {
|
|
8232
|
+
"content-type": "application/json"
|
|
8233
|
+
},
|
|
8234
|
+
method: "POST"
|
|
8235
|
+
});
|
|
8236
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
8237
|
+
break;
|
|
8238
|
+
}
|
|
8239
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
8240
|
+
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
8241
|
+
}
|
|
8242
|
+
if (!response) {
|
|
8243
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
8244
|
+
}
|
|
8245
|
+
if (!response.ok) {
|
|
8246
|
+
throw createHTTPError("Gemini", response);
|
|
8247
|
+
}
|
|
8248
|
+
const body = await response.json();
|
|
8249
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
8250
|
+
await options.onUsage?.(body.usageMetadata);
|
|
8251
|
+
}
|
|
8252
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
8253
|
+
if (toolCalls.length) {
|
|
8254
|
+
return {
|
|
8255
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
8256
|
+
toolCalls
|
|
8257
|
+
};
|
|
8258
|
+
}
|
|
8259
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
8260
|
+
}
|
|
8261
|
+
};
|
|
8262
|
+
};
|
|
6690
8263
|
// src/sqliteStore.ts
|
|
6691
8264
|
import { Database } from "bun:sqlite";
|
|
6692
8265
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
@@ -7884,10 +9457,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
|
|
|
7884
9457
|
result.completed += 1;
|
|
7885
9458
|
} catch (error) {
|
|
7886
9459
|
await options.onError?.(error, task);
|
|
7887
|
-
const
|
|
9460
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
7888
9461
|
const failedTask = failVoiceOpsTask(task, {
|
|
7889
9462
|
actor: task.claimedBy ?? "ops-worker",
|
|
7890
|
-
error:
|
|
9463
|
+
error: errorMessage2
|
|
7891
9464
|
});
|
|
7892
9465
|
if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
|
|
7893
9466
|
const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
|
|
@@ -9137,10 +10710,14 @@ export {
|
|
|
9137
10710
|
transcodePCMToTwilioOutboundPayload,
|
|
9138
10711
|
summarizeVoiceTraceSinkDeliveries,
|
|
9139
10712
|
summarizeVoiceTrace,
|
|
10713
|
+
summarizeVoiceSessionReplay,
|
|
10714
|
+
summarizeVoiceProviderHealth,
|
|
9140
10715
|
summarizeVoiceOpsTasks,
|
|
9141
10716
|
summarizeVoiceOpsTaskQueue,
|
|
9142
10717
|
summarizeVoiceOpsTaskAnalytics,
|
|
9143
10718
|
summarizeVoiceIntegrationEvents,
|
|
10719
|
+
summarizeVoiceAssistantRuns,
|
|
10720
|
+
summarizeVoiceAssistantHealth,
|
|
9144
10721
|
startVoiceOpsTask,
|
|
9145
10722
|
shapeTelephonyAssistantText,
|
|
9146
10723
|
selectVoiceTraceEventsForPrune,
|
|
@@ -9152,14 +10729,17 @@ export {
|
|
|
9152
10729
|
resolveVoiceOpsTaskAssignment,
|
|
9153
10730
|
resolveVoiceOpsTaskAgeBucket,
|
|
9154
10731
|
resolveVoiceOpsPreset,
|
|
10732
|
+
resolveVoiceAssistantMemoryNamespace,
|
|
9155
10733
|
resolveTurnDetectionConfig,
|
|
9156
10734
|
resolveAudioConditioningConfig,
|
|
9157
10735
|
requeueVoiceOpsTask,
|
|
9158
10736
|
reopenVoiceOpsTask,
|
|
9159
10737
|
renderVoiceTraceMarkdown,
|
|
9160
10738
|
renderVoiceTraceHTML,
|
|
10739
|
+
renderVoiceProviderHealthHTML,
|
|
9161
10740
|
renderVoiceCallReviewMarkdown,
|
|
9162
10741
|
renderVoiceCallReviewHTML,
|
|
10742
|
+
renderVoiceAssistantHealthHTML,
|
|
9163
10743
|
redactVoiceTraceText,
|
|
9164
10744
|
redactVoiceTraceEvents,
|
|
9165
10745
|
redactVoiceTraceEvent,
|
|
@@ -9197,6 +10777,9 @@ export {
|
|
|
9197
10777
|
createVoiceTaskUpdatedEvent,
|
|
9198
10778
|
createVoiceTaskSLABreachedEvent,
|
|
9199
10779
|
createVoiceTaskCreatedEvent,
|
|
10780
|
+
createVoiceSessionReplayRoutes,
|
|
10781
|
+
createVoiceSessionReplayJSONHandler,
|
|
10782
|
+
createVoiceSessionReplayHTMLHandler,
|
|
9200
10783
|
createVoiceSessionRecord,
|
|
9201
10784
|
createVoiceSession,
|
|
9202
10785
|
createVoiceSTTRoutingCorrectionHandler,
|
|
@@ -9212,6 +10795,10 @@ export {
|
|
|
9212
10795
|
createVoiceReviewSavedEvent,
|
|
9213
10796
|
createVoiceRedisTaskLeaseCoordinator,
|
|
9214
10797
|
createVoiceRedisIdempotencyStore,
|
|
10798
|
+
createVoiceProviderRouter,
|
|
10799
|
+
createVoiceProviderHealthRoutes,
|
|
10800
|
+
createVoiceProviderHealthJSONHandler,
|
|
10801
|
+
createVoiceProviderHealthHTMLHandler,
|
|
9215
10802
|
createVoicePostgresTraceSinkDeliveryStore,
|
|
9216
10803
|
createVoicePostgresTraceEventStore,
|
|
9217
10804
|
createVoicePostgresTaskStore,
|
|
@@ -9227,6 +10814,7 @@ export {
|
|
|
9227
10814
|
createVoiceMemoryTraceSinkDeliveryStore,
|
|
9228
10815
|
createVoiceMemoryTraceEventStore,
|
|
9229
10816
|
createVoiceMemoryStore,
|
|
10817
|
+
createVoiceMemoryAssistantMemoryStore,
|
|
9230
10818
|
createVoiceLinearIssueUpdateSink,
|
|
9231
10819
|
createVoiceLinearIssueSyncSinks,
|
|
9232
10820
|
createVoiceLinearIssueSink,
|
|
@@ -9246,6 +10834,7 @@ export {
|
|
|
9246
10834
|
createVoiceFileReviewStore,
|
|
9247
10835
|
createVoiceFileIntegrationEventStore,
|
|
9248
10836
|
createVoiceFileExternalObjectMapStore,
|
|
10837
|
+
createVoiceFileAssistantMemoryStore,
|
|
9249
10838
|
createVoiceExternalObjectMapId,
|
|
9250
10839
|
createVoiceExternalObjectMap,
|
|
9251
10840
|
createVoiceExperiment,
|
|
@@ -9254,6 +10843,11 @@ export {
|
|
|
9254
10843
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
9255
10844
|
createVoiceCallCompletedEvent,
|
|
9256
10845
|
createVoiceCRMActivitySink,
|
|
10846
|
+
createVoiceAssistantMemoryRecord,
|
|
10847
|
+
createVoiceAssistantMemoryHandle,
|
|
10848
|
+
createVoiceAssistantHealthRoutes,
|
|
10849
|
+
createVoiceAssistantHealthJSONHandler,
|
|
10850
|
+
createVoiceAssistantHealthHTMLHandler,
|
|
9257
10851
|
createVoiceAssistant,
|
|
9258
10852
|
createVoiceAgentTool,
|
|
9259
10853
|
createVoiceAgentSquad,
|
|
@@ -9266,9 +10860,13 @@ export {
|
|
|
9266
10860
|
createStoredVoiceCallReviewArtifact,
|
|
9267
10861
|
createRiskyTurnCorrectionHandler,
|
|
9268
10862
|
createPhraseHintCorrectionHandler,
|
|
10863
|
+
createOpenAIVoiceAssistantModel,
|
|
10864
|
+
createJSONVoiceAssistantModel,
|
|
9269
10865
|
createId,
|
|
10866
|
+
createGeminiVoiceAssistantModel,
|
|
9270
10867
|
createDomainPhraseHints,
|
|
9271
10868
|
createDomainLexicon,
|
|
10869
|
+
createAnthropicVoiceAssistantModel,
|
|
9272
10870
|
conditionAudioChunk,
|
|
9273
10871
|
completeVoiceOpsTask,
|
|
9274
10872
|
claimVoiceOpsTask,
|