supafone-labs 0.4.14 → 0.5.2

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/index.js CHANGED
@@ -19,6 +19,127 @@
19
19
  * number: { search: { areaCode: "415" } },
20
20
  * });
21
21
  */
22
+ export const OUTBOUND_CALL_MODE_BOUNDS = {
23
+ maxDurationSeconds: { default: 180, min: 30, max: 900 },
24
+ maxKeypresses: { default: 12, min: 1, max: 64 },
25
+ repeatedMenuLimit: { default: 3, min: 1, max: 10 },
26
+ noProgressTimeoutSeconds: { default: 30, min: 5, max: 120 },
27
+ };
28
+ export const OUTBOUND_CALL_MODE_DEFAULTS = {
29
+ enabled: false,
30
+ autoDetect: true,
31
+ dtmfToolEnabled: true,
32
+ maxDurationSeconds: 180,
33
+ maxKeypresses: 12,
34
+ repeatedMenuLimit: 3,
35
+ noProgressTimeoutSeconds: 30,
36
+ humanDetectionEnabled: true,
37
+ resumeOnHuman: true,
38
+ observability: {
39
+ enabled: true,
40
+ includeTrigger: true,
41
+ includeTransitions: true,
42
+ includeTerminationReason: true,
43
+ },
44
+ };
45
+ const OUTBOUND_PROVIDER_PROFILE_BASE = {
46
+ contractSupported: true,
47
+ execution: "adapter_runtime_dependent",
48
+ capabilityPolicy: "fail_closed",
49
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
50
+ };
51
+ export const OUTBOUND_CALL_MODE_PROVIDER_MATRIX = {
52
+ supafoneManaged: {
53
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
54
+ transportFamily: "supafone_managed",
55
+ providers: ["supafone", "supafone_managed", "managed"],
56
+ },
57
+ twilio: {
58
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
59
+ transportFamily: "twilio",
60
+ providers: ["twilio", "byo_twilio"],
61
+ },
62
+ telnyx: {
63
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
64
+ transportFamily: "telnyx",
65
+ providers: ["telnyx", "byo_telnyx"],
66
+ },
67
+ plivo: {
68
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
69
+ transportFamily: "plivo",
70
+ providers: ["plivo", "byo_plivo"],
71
+ },
72
+ signalWire: {
73
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
74
+ transportFamily: "signalwire",
75
+ providers: ["signalwire", "signal_wire", "byo_signalwire"],
76
+ },
77
+ sipByoc: {
78
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
79
+ transportFamily: "sip_byoc",
80
+ providers: ["sip", "custom_sip", "byo_sip", "byoc", "sip_byoc"],
81
+ },
82
+ };
83
+ export function outboundCallModeProviderProfile(provider) {
84
+ const normalized = String(provider ?? "").trim().toLowerCase();
85
+ for (const profile of Object.values(OUTBOUND_CALL_MODE_PROVIDER_MATRIX)) {
86
+ if (profile.providers.includes(normalized)) {
87
+ return { ...profile, providers: [...profile.providers] };
88
+ }
89
+ }
90
+ return {
91
+ contractSupported: false,
92
+ execution: "adapter_runtime_dependent",
93
+ capabilityPolicy: "fail_closed",
94
+ transportFamily: "unknown",
95
+ providers: normalized ? [normalized] : [],
96
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
97
+ };
98
+ }
99
+ export function outboundCallModeReadiness(config, capabilities) {
100
+ const provider = String(capabilities?.provider ?? "");
101
+ const profile = outboundCallModeProviderProfile(provider);
102
+ const normalized = outboundCallModePayload(config ?? {});
103
+ if (normalized.enabled !== true) {
104
+ return {
105
+ ready: true,
106
+ status: "disabled",
107
+ provider,
108
+ transportFamily: profile.transportFamily,
109
+ requiredCapabilities: [],
110
+ missingCapabilities: [],
111
+ unsupportedCapabilities: [],
112
+ reasons: [],
113
+ };
114
+ }
115
+ const requiredCapabilities = [...normalized.required_capabilities];
116
+ const reported = {
117
+ dtmf: capabilities?.dtmf,
118
+ state_persistence: capabilities?.statePersistence,
119
+ human_detection: capabilities?.humanDetection,
120
+ observability: capabilities?.observability,
121
+ };
122
+ const missingCapabilities = requiredCapabilities.filter((key) => reported[key] === undefined);
123
+ const unsupportedCapabilities = requiredCapabilities.filter((key) => reported[key] === false);
124
+ const status = unsupportedCapabilities.length
125
+ ? "unsupported"
126
+ : missingCapabilities.length
127
+ ? "unknown"
128
+ : "ready";
129
+ const reasons = unsupportedCapabilities.length
130
+ ? unsupportedCapabilities.map((key) => `Adapter reports ${key} is unsupported.`)
131
+ : missingCapabilities.map((key) => `Adapter did not report ${key} support.`);
132
+ return {
133
+ ready: status === "ready",
134
+ status,
135
+ provider,
136
+ transportFamily: profile.transportFamily,
137
+ requiredCapabilities,
138
+ missingCapabilities,
139
+ unsupportedCapabilities,
140
+ reasons,
141
+ };
142
+ }
22
143
  export class SupafoneLabsError extends Error {
23
144
  status;
24
145
  body;
@@ -259,6 +380,33 @@ export class SupafoneLabs {
259
380
  clearTimeout(timer);
260
381
  }
261
382
  }
383
+ /** @internal Multipart upload to the Supafone product API with the Labs key. */
384
+ async requestSupafoneUpload(path, file, filename) {
385
+ const ctrl = new AbortController();
386
+ const timer = setTimeout(() => ctrl.abort(), Math.max(this.timeoutMs, 120_000));
387
+ try {
388
+ const safeFilename = filename.replace(/["\\\r\n]/g, "").slice(0, 120) || "document.txt";
389
+ const blob = file instanceof Blob ? file : new Blob([file]);
390
+ const form = new FormData();
391
+ form.append("file", blob, safeFilename);
392
+ const res = await fetch(this.supafoneApiBaseUrl + path, {
393
+ method: "POST",
394
+ signal: ctrl.signal,
395
+ headers: { Authorization: `Bearer ${this.supafoneApiKey}` },
396
+ body: form,
397
+ });
398
+ const text = await res.text();
399
+ const parsed = text ? safeJson(text) : {};
400
+ if (!res.ok) {
401
+ const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
402
+ throw new SupafoneLabsError(`POST ${path}: ${detail}`, res.status, parsed);
403
+ }
404
+ return parsed;
405
+ }
406
+ finally {
407
+ clearTimeout(timer);
408
+ }
409
+ }
262
410
  /** @internal Authenticated binary request to the Supafone app API. */
263
411
  async requestSupafoneBinary(path) {
264
412
  const ctrl = new AbortController();
@@ -763,18 +911,26 @@ class CampaignsNamespace {
763
911
  const query = opts.accountId ? `?${new URLSearchParams({ account_id: opts.accountId })}` : "";
764
912
  return this.sm.requestAccountApi("GET", `/api/v1/campaigns${query}`);
765
913
  }
766
- create(opts = {}) {
767
- return this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
914
+ async create(opts = {}) {
915
+ const created = await this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
768
916
  name: opts.name ?? "New campaign",
769
917
  goal: opts.goal ?? "book",
770
918
  agent_id: opts.agentId,
771
919
  account_id: opts.accountId,
772
920
  }));
921
+ const settings = campaignSettingsPayload(opts.settings, opts.outboundCallMode);
922
+ if (!Object.keys(settings).length)
923
+ return created;
924
+ if (!created.campaign?.id) {
925
+ throw new SupafoneLabsError("Campaign was created but no campaign id was returned; outbound call mode was not saved.");
926
+ }
927
+ return this.update(created.campaign.id, { settings });
773
928
  }
774
929
  get(campaignId) {
775
930
  return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`);
776
931
  }
777
932
  update(campaignId, input) {
933
+ const settings = campaignSettingsPayload(input.settings, input.outboundCallMode);
778
934
  const payload = compact({
779
935
  name: input.name,
780
936
  goal: input.goal,
@@ -782,10 +938,10 @@ class CampaignsNamespace {
782
938
  email_subject: input.emailSubject,
783
939
  email_body: input.emailBody,
784
940
  cadence: input.cadence,
785
- settings: input.settings,
941
+ settings: Object.keys(settings).length ? settings : undefined,
786
942
  });
787
943
  if (!Object.keys(payload).length) {
788
- throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, or settings");
944
+ throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, settings, or outboundCallMode");
789
945
  }
790
946
  return this.sm.requestAccountApi("PUT", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`, payload);
791
947
  }
@@ -1112,6 +1268,66 @@ class LabsAgentsNamespace {
1112
1268
  const suffix = q.toString() ? `?${q}` : "";
1113
1269
  return this.sm.requestSupafoneApi("GET", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}${suffix}`);
1114
1270
  }
1271
+ /** Update an existing durable agent without changing omitted fields. */
1272
+ update(agentKey, input, opts = {}) {
1273
+ const q = new URLSearchParams();
1274
+ if (opts.agencyId)
1275
+ q.set("agency_id", opts.agencyId);
1276
+ const suffix = q.toString() ? `?${q}` : "";
1277
+ return this.sm.requestSupafoneApi("PATCH", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}${suffix}`, labsAgentUpdatePayload(input));
1278
+ }
1279
+ /** Scrape a website and rebuild this agent's managed knowledge corpus. */
1280
+ syncKnowledge(agentId, input = {}) {
1281
+ return this.sm.requestSupafoneApi("POST", `/api/v1/agents/${encodeURIComponent(agentId)}/sync-knowledge`, { url: input.url ?? input.website_url ?? input.websiteUrl ?? "" });
1282
+ }
1283
+ /** Remove website knowledge while preserving uploaded documents and notes. */
1284
+ detachWebsiteKnowledge(agentId) {
1285
+ return this.sm.requestSupafoneApi("DELETE", `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/website`);
1286
+ }
1287
+ /** Ask the managed server whether this agent is ready to activate. */
1288
+ readiness(agentKey, opts = {}) {
1289
+ const q = new URLSearchParams();
1290
+ if (opts.agencyId)
1291
+ q.set("agency_id", opts.agencyId);
1292
+ const suffix = q.toString() ? `?${q}` : "";
1293
+ return this.sm.requestSupafoneApi("GET", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/readiness${suffix}`);
1294
+ }
1295
+ /** Activate a ready hosted agent. */
1296
+ activate(agentKey, opts = {}) {
1297
+ const q = new URLSearchParams();
1298
+ if (opts.agencyId)
1299
+ q.set("agency_id", opts.agencyId);
1300
+ const suffix = q.toString() ? `?${q}` : "";
1301
+ return this.sm.requestSupafoneApi("POST", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/activate${suffix}`);
1302
+ }
1303
+ /** Pause a hosted agent without deleting it. */
1304
+ pause(agentKey, opts = {}) {
1305
+ const q = new URLSearchParams();
1306
+ if (opts.agencyId)
1307
+ q.set("agency_id", opts.agencyId);
1308
+ const suffix = q.toString() ? `?${q}` : "";
1309
+ return this.sm.requestSupafoneApi("POST", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/pause${suffix}`);
1310
+ }
1311
+ /** Start the native provider-neutral browser voice session for this agent. */
1312
+ startWebRtcCall(agentId) {
1313
+ return this.sm.startWebRtcCall({ agentId });
1314
+ }
1315
+ /** Rebuild retrieval data from the agent's already saved knowledge sources. */
1316
+ reindexKnowledge(agentId) {
1317
+ return this.sm.requestSupafoneApi("POST", `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/reindex`);
1318
+ }
1319
+ /** Upload and index a txt, md, csv, or PDF knowledge document. */
1320
+ uploadKnowledgeDocument(agentId, file, filename) {
1321
+ return this.sm.requestSupafoneUpload(`/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/upload`, file, filename);
1322
+ }
1323
+ /** Delete one document and rebuild the agent's managed corpus. */
1324
+ deleteKnowledgeDocument(agentId, documentId) {
1325
+ return this.sm.requestSupafoneApi("DELETE", `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/docs/${encodeURIComponent(documentId)}`);
1326
+ }
1327
+ /** Ask a grounded question against the same corpus used during calls. */
1328
+ chatKnowledge(agentId, message, history = []) {
1329
+ return this.sm.requestSupafoneApi("POST", `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge-chat`, { message, history });
1330
+ }
1115
1331
  /** Delete an agent. Optionally ask the backend to release assigned numbers. */
1116
1332
  delete(agentKey, opts = {}) {
1117
1333
  const q = new URLSearchParams();
@@ -1657,6 +1873,94 @@ class OptimizerNamespace {
1657
1873
  return this.sm.request("GET", `/v1/optimizer/reports?agent=${encodeURIComponent(agent)}&limit=${limit}`);
1658
1874
  }
1659
1875
  }
1876
+ function outboundCallModePayload(config) {
1877
+ const enabled = outboundModeBoolean(config.enabled, false, "enabled");
1878
+ if (!enabled)
1879
+ return { version: 1, enabled: false };
1880
+ const observabilityInput = config.observability ?? {};
1881
+ if (typeof observabilityInput !== "object" || Array.isArray(observabilityInput)) {
1882
+ throw new SupafoneLabsError("outboundCallMode.observability must be an object");
1883
+ }
1884
+ if (observabilityInput.metadata !== undefined
1885
+ && (typeof observabilityInput.metadata !== "object"
1886
+ || observabilityInput.metadata === null
1887
+ || Array.isArray(observabilityInput.metadata))) {
1888
+ throw new SupafoneLabsError("outboundCallMode.observability.metadata must be an object");
1889
+ }
1890
+ const observability = compact({
1891
+ enabled: outboundModeBoolean(observabilityInput.enabled, true, "observability.enabled"),
1892
+ include_trigger: outboundModeBoolean(observabilityInput.includeTrigger, true, "observability.includeTrigger"),
1893
+ include_transitions: outboundModeBoolean(observabilityInput.includeTransitions, true, "observability.includeTransitions"),
1894
+ include_termination_reason: outboundModeBoolean(observabilityInput.includeTerminationReason, true, "observability.includeTerminationReason"),
1895
+ metadata: observabilityInput.metadata,
1896
+ });
1897
+ const payload = {
1898
+ version: 1,
1899
+ enabled: true,
1900
+ initial_mode: "mission",
1901
+ ivr_mode: "dynamic",
1902
+ transport_scope: "provider_agnostic",
1903
+ capability_policy: "fail_closed",
1904
+ auto_detect: outboundModeBoolean(config.autoDetect, true, "autoDetect"),
1905
+ dtmf_tool_enabled: outboundModeBoolean(config.dtmfToolEnabled, true, "dtmfToolEnabled"),
1906
+ max_duration_seconds: outboundModeInteger(config.maxDurationSeconds, "maxDurationSeconds"),
1907
+ max_keypresses: outboundModeInteger(config.maxKeypresses, "maxKeypresses"),
1908
+ repeated_menu_limit: outboundModeInteger(config.repeatedMenuLimit, "repeatedMenuLimit"),
1909
+ no_progress_timeout_seconds: outboundModeInteger(config.noProgressTimeoutSeconds, "noProgressTimeoutSeconds"),
1910
+ human_detection_enabled: outboundModeBoolean(config.humanDetectionEnabled, true, "humanDetectionEnabled"),
1911
+ resume_on_human: outboundModeBoolean(config.resumeOnHuman, true, "resumeOnHuman"),
1912
+ observability,
1913
+ };
1914
+ payload.required_capabilities = outboundCallModeRequiredCapabilities(payload);
1915
+ return payload;
1916
+ }
1917
+ function outboundModeBoolean(value, fallback, key) {
1918
+ if (value === undefined)
1919
+ return fallback;
1920
+ if (typeof value !== "boolean") {
1921
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be a boolean`);
1922
+ }
1923
+ return value;
1924
+ }
1925
+ function outboundModeInteger(value, key) {
1926
+ const bounds = OUTBOUND_CALL_MODE_BOUNDS[key];
1927
+ const normalized = value ?? bounds.default;
1928
+ if (!Number.isInteger(normalized)) {
1929
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be an integer`);
1930
+ }
1931
+ if (normalized < bounds.min || normalized > bounds.max) {
1932
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be between ${bounds.min} and ${bounds.max}`);
1933
+ }
1934
+ return normalized;
1935
+ }
1936
+ function outboundCallModeRequiredCapabilities(payload) {
1937
+ const required = ["state_persistence"];
1938
+ if (payload.dtmf_tool_enabled !== false)
1939
+ required.unshift("dtmf");
1940
+ if (payload.human_detection_enabled !== false || payload.resume_on_human !== false) {
1941
+ required.push("human_detection");
1942
+ }
1943
+ const observability = payload.observability;
1944
+ if (observability?.enabled === true)
1945
+ required.push("observability");
1946
+ return required;
1947
+ }
1948
+ function labsAgentMetadataPayload(input) {
1949
+ const metadata = { ...(input.metadata ?? {}) };
1950
+ const mode = input.outboundCallMode ?? input.outbound_call_mode;
1951
+ if (mode !== undefined)
1952
+ metadata.outbound_call_mode = outboundCallModePayload(mode);
1953
+ return Object.keys(metadata).length ? metadata : undefined;
1954
+ }
1955
+ function labsAgentUpdatePayload(input) {
1956
+ return labsAgentPayload(input);
1957
+ }
1958
+ function campaignSettingsPayload(settings, mode) {
1959
+ const payload = { ...(settings ?? {}) };
1960
+ if (mode !== undefined)
1961
+ payload.outbound_call_mode = outboundCallModePayload(mode);
1962
+ return payload;
1963
+ }
1660
1964
  function labsAgentPayload(input) {
1661
1965
  const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
1662
1966
  return compact({
@@ -1703,7 +2007,7 @@ function labsAgentPayload(input) {
1703
2007
  ultravox: input.ultravox ? ultravoxPayload(input.ultravox) : undefined,
1704
2008
  voice_watcher: input.voice_watcher ?? input.voiceWatcher,
1705
2009
  voice_watcher_model: input.voice_watcher_model ?? input.voiceWatcherModel,
1706
- metadata: input.metadata,
2010
+ metadata: labsAgentMetadataPayload(input),
1707
2011
  });
1708
2012
  }
1709
2013
  function hostedListQuery(opts) {
@@ -1934,6 +2238,9 @@ function voicePayload(input) {
1934
2238
  function languageProfilesPayload(input) {
1935
2239
  if (!Array.isArray(input))
1936
2240
  return undefined;
2241
+ if (input.length > 4) {
2242
+ throw new SupafoneLabsError("languageProfiles supports at most four profiles");
2243
+ }
1937
2244
  const profiles = input.map((profile) => compact({
1938
2245
  language: profile.language,
1939
2246
  language_hint: profile.language_hint ?? profile.languageHint,