@avakado.ai/schemas 1.1.15 → 1.1.17

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/media.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ export interface AudioWire {
2
+ encoding: string;
3
+ rate: number;
4
+ }
5
+
6
+ export interface MediaPlan {
7
+ channel: AudioWire;
8
+ provider: {
9
+ name: string;
10
+ input: AudioWire;
11
+ output: AudioWire;
12
+ };
13
+ uplink: string;
14
+ downlink: string;
15
+ }
16
+
17
+ export class MediaPlanError extends Error {
18
+ name: "MediaPlanError";
19
+ }
20
+
21
+ export const CONVERTERS: Set<string>;
22
+ export const CHANNEL_CATALOGS: Record<string, {
23
+ symmetric: boolean;
24
+ wires: AudioWire[];
25
+ defaultWire: AudioWire;
26
+ }>;
27
+ export const PROVIDER_CARDS: Record<string, {
28
+ symmetric: boolean;
29
+ wires?: AudioWire[];
30
+ input?: AudioWire;
31
+ output?: AudioWire;
32
+ }>;
33
+
34
+ export function sameWire(a: AudioWire | null | undefined, b: AudioWire | null | undefined): boolean;
35
+ export function wireKey(value: AudioWire): string;
36
+ export function converterKey(from: AudioWire, to: AudioWire): string;
37
+ export function normalizeChannelName(name: string): string;
38
+ export function normalizeProviderName(name: string): string;
39
+ export function resolveChannelWire(channelName: string, config?: { media?: AudioWire }): AudioWire;
40
+ export function selectChannelMedia(channelName: string, selected: AudioWire): AudioWire;
41
+ export function negotiateCall(input: {
42
+ channelName: string;
43
+ channelConfig?: { media?: AudioWire };
44
+ provider: string;
45
+ }): MediaPlan;
46
+ export function providerAudioFormat(provider: string, value: AudioWire): { type?: string; rate?: number; mimeType?: string };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avakado.ai/schemas",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "description": "Canonical Mongo schemas, models, enums and index declarations shared by the Avakado services.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -31,6 +31,11 @@
31
31
  "import": "./src/enums/index.js",
32
32
  "default": "./src/enums/index.js"
33
33
  },
34
+ "./media": {
35
+ "types": "./media.d.ts",
36
+ "import": "./src/media.js",
37
+ "default": "./src/media.js"
38
+ },
34
39
  "./runtime": {
35
40
  "types": "./runtime.d.ts",
36
41
  "import": "./src/runtime.js",
@@ -43,6 +48,7 @@
43
48
  "types.d.ts",
44
49
  "models.d.ts",
45
50
  "enums.d.ts",
51
+ "media.d.ts",
46
52
  "runtime.d.ts",
47
53
  "README.md",
48
54
  "LICENSE"
@@ -58,7 +58,7 @@ export const LEDGER_CATEGORY = {
58
58
  export const CONVERSATION_STATUS = ["open", "pending", "snoozed", "completed", "closed", "archived", "spam"];
59
59
  export const CONVERSATION_PRIORITY = ["low", "normal", "high", "urgent"];
60
60
 
61
- export const CAMPAIGN_STATUS = ["pending", "active", "completed"];
61
+ export const CAMPAIGN_STATUS = ["pending", "active", "completed", "failed", "partial"];
62
62
  export const TASK_STATUS = ["pending", "in-progress", "completed", "failed", "skipped"];
63
63
  export const TASK_TYPES = ["quick", "webhook"];
64
64
  export const TASK_REFERENCE_TYPES = ["Message", "CallSession"];
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import "./models/index.js";
2
2
  export * from "./models/index.js";
3
3
  export * from "./enums/index.js";
4
+ export * from "./media.js";
4
5
  export { configureSchemas, defineModel, getSchemaConfig } from "./runtime.js";
package/src/media.js ADDED
@@ -0,0 +1,200 @@
1
+ // A wire is the only audio description the platform passes around: { encoding, rate }.
2
+ // Channel menus and provider cards live here. A call stores the plan these two produce.
3
+ // Adding a carrier is a catalog entry. Adding a conversion is a key in CONVERTERS.
4
+
5
+ const PROVIDER_ALIASES = {
6
+ openai: "openai",
7
+ gemini: "gemini",
8
+ google: "gemini",
9
+ };
10
+
11
+ function wire(encoding, rate) {
12
+ return { encoding, rate: Number(rate) };
13
+ }
14
+
15
+ function copyWire(value) {
16
+ return wire(value.encoding, value.rate);
17
+ }
18
+
19
+ export function sameWire(a, b) {
20
+ return Boolean(a && b && a.encoding === b.encoding && Number(a.rate) === Number(b.rate));
21
+ }
22
+
23
+ export function wireKey(value) {
24
+ return `${value.encoding}:${Number(value.rate)}`;
25
+ }
26
+
27
+ export function converterKey(from, to) {
28
+ if (sameWire(from, to)) return "identity";
29
+ return `${wireKey(from)}->${wireKey(to)}`;
30
+ }
31
+
32
+ export class MediaPlanError extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "MediaPlanError";
36
+ }
37
+ }
38
+
39
+ // Pairs AudioConverter can run. identity is free. A missing key fails call creation.
40
+ export const CONVERTERS = new Set([
41
+ "identity",
42
+ "pcm16:8000->pcm16:16000",
43
+ "pcm16:8000->pcm16:24000",
44
+ "pcm16:24000->pcm16:8000",
45
+ "pcm16:24000->pcm16:16000",
46
+ "pcm16:24000->g711_ulaw:8000",
47
+ "g711_ulaw:8000->pcm16:16000",
48
+ ]);
49
+
50
+ export const CHANNEL_CATALOGS = {
51
+ exotel: {
52
+ symmetric: true,
53
+ wires: [wire("pcm16", 8000), wire("pcm16", 16000), wire("pcm16", 24000)],
54
+ defaultWire: wire("pcm16", 8000),
55
+ },
56
+ whatsapp: {
57
+ symmetric: true,
58
+ wires: [wire("g711_ulaw", 8000), wire("g711_alaw", 8000)],
59
+ defaultWire: wire("g711_ulaw", 8000),
60
+ },
61
+ twilio: {
62
+ symmetric: true,
63
+ wires: [wire("g711_ulaw", 8000)],
64
+ defaultWire: wire("g711_ulaw", 8000),
65
+ },
66
+ tatatele: {
67
+ symmetric: true,
68
+ wires: [wire("g711_ulaw", 8000)],
69
+ defaultWire: wire("g711_ulaw", 8000),
70
+ },
71
+ };
72
+
73
+ export const PROVIDER_CARDS = {
74
+ openai: {
75
+ symmetric: true,
76
+ wires: [
77
+ wire("pcm16", 8000),
78
+ wire("pcm16", 16000),
79
+ wire("pcm16", 24000),
80
+ wire("g711_ulaw", 8000),
81
+ wire("g711_alaw", 8000),
82
+ ],
83
+ },
84
+ gemini: {
85
+ symmetric: false,
86
+ input: wire("pcm16", 16000),
87
+ output: wire("pcm16", 24000),
88
+ },
89
+ };
90
+
91
+ export function normalizeChannelName(name) {
92
+ const key = String(name || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
93
+ if (key === "tatatele") return "tatatele";
94
+ return key;
95
+ }
96
+
97
+ export function normalizeProviderName(name) {
98
+ const key = String(name || "").trim().toLowerCase();
99
+ return PROVIDER_ALIASES[key] || key;
100
+ }
101
+
102
+ function catalogFor(channelName) {
103
+ const key = normalizeChannelName(channelName);
104
+ const catalog = CHANNEL_CATALOGS[key];
105
+ if (!catalog) {
106
+ throw new MediaPlanError(`No audio catalog for channel "${channelName}"`);
107
+ }
108
+ return { key, catalog };
109
+ }
110
+
111
+ function requireWire(catalog, selected) {
112
+ const match = catalog.wires.find((item) => sameWire(item, selected));
113
+ if (!match) {
114
+ const allowed = catalog.wires.map(wireKey).join(", ");
115
+ throw new MediaPlanError(
116
+ `Channel wire ${wireKey(selected)} is not in [${allowed}]`
117
+ );
118
+ }
119
+ return copyWire(match);
120
+ }
121
+
122
+ export function resolveChannelWire(channelName, config = {}) {
123
+ const { catalog } = catalogFor(channelName);
124
+ const selected = config?.media;
125
+ if (selected?.encoding != null && selected?.rate != null) {
126
+ return requireWire(catalog, selected);
127
+ }
128
+ return copyWire(catalog.defaultWire);
129
+ }
130
+
131
+ export function selectChannelMedia(channelName, selected) {
132
+ const { catalog } = catalogFor(channelName);
133
+ return requireWire(catalog, selected);
134
+ }
135
+
136
+ function conversionCost(from, to) {
137
+ if (sameWire(from, to)) return 0;
138
+ if (Number(from.rate) === Number(to.rate)) return 1;
139
+ if (from.encoding === to.encoding) return 2;
140
+ return 3;
141
+ }
142
+
143
+ function assertConverter(from, to) {
144
+ const key = converterKey(from, to);
145
+ if (!CONVERTERS.has(key)) {
146
+ throw new MediaPlanError(`No converter for ${key}`);
147
+ }
148
+ return key;
149
+ }
150
+
151
+ function planFor(channel, providerName, input, output) {
152
+ return {
153
+ channel: copyWire(channel),
154
+ provider: {
155
+ name: providerName,
156
+ input: copyWire(input),
157
+ output: copyWire(output),
158
+ },
159
+ uplink: assertConverter(channel, input),
160
+ downlink: assertConverter(output, channel),
161
+ };
162
+ }
163
+
164
+ export function negotiateCall({ channelName, channelConfig, provider } = {}) {
165
+ const channel = resolveChannelWire(channelName, channelConfig);
166
+ const providerName = normalizeProviderName(provider);
167
+ const card = PROVIDER_CARDS[providerName];
168
+ if (!card) throw new MediaPlanError(`No audio card for provider "${provider}"`);
169
+
170
+ if (!card.symmetric) {
171
+ return planFor(channel, providerName, card.input, card.output);
172
+ }
173
+
174
+ const ranked = card.wires
175
+ .map((item) => ({ item, cost: conversionCost(channel, item) }))
176
+ .sort((a, b) => a.cost - b.cost);
177
+
178
+ let lastError = null;
179
+ for (const { item } of ranked) {
180
+ try {
181
+ return planFor(channel, providerName, item, item);
182
+ } catch (error) {
183
+ lastError = error;
184
+ }
185
+ }
186
+ throw lastError || new MediaPlanError(`No provider wire for ${wireKey(channel)}`);
187
+ }
188
+
189
+ export function providerAudioFormat(provider, value) {
190
+ const name = normalizeProviderName(provider);
191
+ if (name === "openai") {
192
+ if (value.encoding === "pcm16") return { type: "audio/pcm", rate: Number(value.rate) };
193
+ if (value.encoding === "g711_ulaw") return { type: "audio/pcmu" };
194
+ if (value.encoding === "g711_alaw") return { type: "audio/pcma" };
195
+ }
196
+ if (name === "gemini") {
197
+ return { mimeType: `audio/pcm;rate=${Number(value.rate)}` };
198
+ }
199
+ throw new MediaPlanError(`No session audio format for provider "${provider}"`);
200
+ }
@@ -0,0 +1,84 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { MediaPlanError, negotiateCall, selectChannelMedia } from "./media.js";
4
+
5
+ test("exotel default and openai share pcm16 8 kHz", () => {
6
+ const plan = negotiateCall({ channelName: "Exotel", provider: "openai" });
7
+ assert.deepEqual(plan.channel, { encoding: "pcm16", rate: 8000 });
8
+ assert.deepEqual(plan.provider.input, plan.channel);
9
+ assert.deepEqual(plan.provider.output, plan.channel);
10
+ assert.equal(plan.uplink, "identity");
11
+ assert.equal(plan.downlink, "identity");
12
+ });
13
+
14
+ test("user can move exotel to 16 kHz and openai follows", () => {
15
+ const plan = negotiateCall({
16
+ channelName: "exotel",
17
+ channelConfig: { media: { encoding: "pcm16", rate: 16000 } },
18
+ provider: "openai",
19
+ });
20
+ assert.equal(plan.channel.rate, 16000);
21
+ assert.equal(plan.uplink, "identity");
22
+ assert.equal(plan.downlink, "identity");
23
+ });
24
+
25
+ test("exotel 8 kHz and gemini bridge 16 kHz in and 24 kHz out", () => {
26
+ const plan = negotiateCall({ channelName: "exotel", provider: "gemini" });
27
+ assert.deepEqual(plan.provider.input, { encoding: "pcm16", rate: 16000 });
28
+ assert.deepEqual(plan.provider.output, { encoding: "pcm16", rate: 24000 });
29
+ assert.equal(plan.uplink, "pcm16:8000->pcm16:16000");
30
+ assert.equal(plan.downlink, "pcm16:24000->pcm16:8000");
31
+ });
32
+
33
+ test("exotel 16 kHz into gemini only downsamples the model output", () => {
34
+ const plan = negotiateCall({
35
+ channelName: "exotel",
36
+ channelConfig: { media: { encoding: "pcm16", rate: 16000 } },
37
+ provider: "google",
38
+ });
39
+ assert.equal(plan.uplink, "identity");
40
+ assert.equal(plan.downlink, "pcm16:24000->pcm16:16000");
41
+ });
42
+
43
+ test("exotel 24 kHz into gemini only downsamples the caller audio", () => {
44
+ const plan = negotiateCall({
45
+ channelName: "exotel",
46
+ channelConfig: { media: { encoding: "pcm16", rate: 24000 } },
47
+ provider: "gemini",
48
+ });
49
+ assert.equal(plan.uplink, "pcm16:24000->pcm16:16000");
50
+ assert.equal(plan.downlink, "identity");
51
+ });
52
+
53
+ test("whatsapp and twilio default to g711 ulaw and match openai", () => {
54
+ for (const channelName of ["Whatsapp", "twilio", "tataTele"]) {
55
+ const plan = negotiateCall({ channelName, provider: "openai" });
56
+ assert.deepEqual(plan.channel, { encoding: "g711_ulaw", rate: 8000 });
57
+ assert.equal(plan.uplink, "identity");
58
+ assert.equal(plan.downlink, "identity");
59
+ }
60
+ });
61
+
62
+ test("whatsapp ulaw into gemini uses the registered converters", () => {
63
+ const plan = negotiateCall({ channelName: "whatsapp", provider: "gemini" });
64
+ assert.equal(plan.uplink, "g711_ulaw:8000->pcm16:16000");
65
+ assert.equal(plan.downlink, "pcm16:24000->g711_ulaw:8000");
66
+ });
67
+
68
+ test("a wire outside the channel menu is rejected", () => {
69
+ assert.throws(
70
+ () => selectChannelMedia("exotel", { encoding: "pcm16", rate: 48000 }),
71
+ MediaPlanError
72
+ );
73
+ });
74
+
75
+ test("alaw into gemini fails until a converter exists", () => {
76
+ assert.throws(
77
+ () => negotiateCall({
78
+ channelName: "whatsapp",
79
+ channelConfig: { media: { encoding: "g711_alaw", rate: 8000 } },
80
+ provider: "gemini",
81
+ }),
82
+ /No converter/
83
+ );
84
+ });
@@ -49,6 +49,8 @@ const CampaignSchema = new Schema({
49
49
  completedAt: Date,// end of chain
50
50
  cancelledAt: Date, // breaking the chain
51
51
  lastTaskStartedAt: Date, // last task started time
52
+ partialAt: Date, // partial completion time
53
+ failedAt: Date, // failed time
52
54
  },
53
55
  cancel_requested: { type: Boolean, default: false }, // if true, the next task will be cancelled
54
56
  createdBy: { type: Schema.Types.ObjectId, ref: 'Users', required: true },