supafone-labs 0.4.1 → 0.4.3
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/README.md +47 -1
- package/dist/cjs/index.d.ts +264 -2
- package/dist/cjs/index.js +205 -3
- package/dist/index.d.ts +264 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +407 -3
package/README.md
CHANGED
|
@@ -269,6 +269,37 @@ await supafone.nudges(); // structured whisper feed
|
|
|
269
269
|
await supafone.metrics(7); // injection rate, latency, by-dimension
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
+
### Automatic post-call analysis (`postCallAnalysis: true`)
|
|
273
|
+
|
|
274
|
+
Turn on `postCallAnalysis` and every `reportCall()` that carries a transcript
|
|
275
|
+
(or structured `messages`) is automatically classified against the agent's
|
|
276
|
+
objective before the report is filed — generating labels: achieved/missed,
|
|
277
|
+
per-criterion verdicts, failure reasons, and the blended objective value.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const supafone = new Supafone({
|
|
281
|
+
apiKey: process.env.SUPAFONE_LABS_API_KEY!,
|
|
282
|
+
postCallAnalysis: true,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const { analysis } = await supafone.reportCall({
|
|
286
|
+
session_id: "call-1",
|
|
287
|
+
agent: "intake",
|
|
288
|
+
transcript: "agent: Hi, how can I help?\ncaller: I want to book...\nagent: Booked for 3pm.",
|
|
289
|
+
ground_truth: { booking_requested: true, booking_verified: true },
|
|
290
|
+
});
|
|
291
|
+
// analysis.achieved -> true
|
|
292
|
+
// analysis.criteria -> { intent_satisfied: true, actions_verified: true, … }
|
|
293
|
+
// analysis.failure_reasons -> []
|
|
294
|
+
// analysis.objective_value -> 0.93 (LLM score blended with ground truth)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The enriched report is filed server-side (feeding `optimizer.improve()` and
|
|
298
|
+
`/v1/optimizer/objective/stats`); billed one oracle call per analyzed call.
|
|
299
|
+
Reports without a transcript — or any analysis failure — fall back to the
|
|
300
|
+
plain zero-billed report. You can also classify explicitly with
|
|
301
|
+
`supafone.classifyCall({ transcript, agent })`.
|
|
302
|
+
|
|
272
303
|
## Agent builder & QA (session-scoped — call `login()` first)
|
|
273
304
|
|
|
274
305
|
The builder and `qa.run` are account features, so authenticate with a console
|
|
@@ -289,11 +320,25 @@ await supafone.builder.saveConfig({ agent_prompt: "…", agent_label: "intake" }
|
|
|
289
320
|
const qa = await supafone.qa.run({ turns: 2 });
|
|
290
321
|
console.log(`+${Math.round(qa.summary.avg_lift * 100)} avg lift`);
|
|
291
322
|
|
|
323
|
+
// One-call auto suite: scenarios generated from the agent's OWN objective,
|
|
324
|
+
// each played as a mock call vs the real config, judged twice — pass/fail on
|
|
325
|
+
// the scenario's assertion AND an SSR grade (poorly/ok/good/great/perfectly).
|
|
326
|
+
const suite = await supafone.qa.suite({ count: 4, turns: 2 });
|
|
327
|
+
console.log(suite.summary.ssr_histogram); // { poorly: 0, ok: 1, good: 2, great: 1, perfectly: 0 }
|
|
328
|
+
console.log(suite.summary.avg_ssr_score); // 0.57
|
|
329
|
+
|
|
330
|
+
// Or just generate scenarios from any prompt (key-scoped, no login needed).
|
|
331
|
+
const { scenarios } = await supafone.qa.generate({ agentPrompt: "You are…", count: 5 });
|
|
332
|
+
|
|
292
333
|
// Improve the standing directive from graded calls (OPRO-style).
|
|
293
334
|
const better = await supafone.optimizer.improve("builder");
|
|
294
335
|
const reports = await supafone.optimizer.reports("builder");
|
|
295
336
|
```
|
|
296
337
|
|
|
338
|
+
How this compares to Hamming, Coval, Roark, Cekura, and the rest of the 2026
|
|
339
|
+
voice-QA field — and the roadmap it drives — lives in the docs:
|
|
340
|
+
[Testing Voice Agents (QA)](../gitbook/voice-qa-landscape.md).
|
|
341
|
+
|
|
297
342
|
All errors throw `SupafoneLabsError` (with `.status` and `.body`); catch it to
|
|
298
343
|
inspect gateway responses.
|
|
299
344
|
|
|
@@ -355,8 +400,9 @@ and `const { Supafone } = require("supafone-labs")` both work, with full types.
|
|
|
355
400
|
| `stt(audio, opts?)` | `POST /v1/stt` |
|
|
356
401
|
| `liveTranscribe(opts?)` | `WS /v1/stt/live` |
|
|
357
402
|
| `balance()` · `models()` · `voices()` · `usage()` | reads |
|
|
403
|
+
| `reportCall(report)` · `classifyCall(input)` | `/v1/events/call_report` · `/v1/calls/classify` (auto with `postCallAnalysis: true`) |
|
|
358
404
|
| `builder.chat/finish/config` | `/v1/builder/*` |
|
|
359
|
-
| `qa.run/history` | `/v1/qa/*` |
|
|
405
|
+
| `qa.run/generate/suite/history` | `/v1/qa/*` |
|
|
360
406
|
| `optimizer.improve/standing` | `/v1/optimizer/*` |
|
|
361
407
|
|
|
362
408
|
Get a key (5 free minutes, no card): <https://labs.supafone.ai/get-key.html>
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -43,6 +43,15 @@ export interface SupafoneLabsOptions {
|
|
|
43
43
|
accountPassword?: string;
|
|
44
44
|
/** Portal base for listen/monitor links (default https://app.supafone.ai). */
|
|
45
45
|
appUrl?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Automatic post-call analysis. When true, reportCall() with a transcript
|
|
48
|
+
* (or structured messages) first classifies the finished call against the
|
|
49
|
+
* agent's objective — generating labels (achieved/missed, per-criterion
|
|
50
|
+
* verdicts, failure reasons) — and files the enriched report server-side.
|
|
51
|
+
* Billed one oracle call per analyzed call; reports without a transcript
|
|
52
|
+
* fall back to the plain zero-billed report.
|
|
53
|
+
*/
|
|
54
|
+
postCallAnalysis?: boolean;
|
|
46
55
|
}
|
|
47
56
|
export interface ChatMessage {
|
|
48
57
|
role: "system" | "user" | "assistant";
|
|
@@ -766,6 +775,59 @@ export interface CallReportInput {
|
|
|
766
775
|
nudges?: number;
|
|
767
776
|
turns?: number;
|
|
768
777
|
language?: string;
|
|
778
|
+
/** Full "role: text" transcript — enables automatic post-call analysis. */
|
|
779
|
+
transcript?: string;
|
|
780
|
+
/** Structured turns (alternative to transcript) for post-call analysis. */
|
|
781
|
+
messages?: ClassifyMessage[];
|
|
782
|
+
/** Deterministic runtime signals blended into the objective value. */
|
|
783
|
+
ground_truth?: GroundTruthInput;
|
|
784
|
+
[extra: string]: unknown;
|
|
785
|
+
}
|
|
786
|
+
/** One turn of a finished call handed to the classifier. */
|
|
787
|
+
export interface ClassifyMessage {
|
|
788
|
+
role: "caller" | "agent" | "whisper" | string;
|
|
789
|
+
text: string;
|
|
790
|
+
}
|
|
791
|
+
/** Deterministic runtime ground truth (the same signals postcall scoring reads). */
|
|
792
|
+
export interface GroundTruthInput {
|
|
793
|
+
booking_requested?: boolean;
|
|
794
|
+
booking_verified?: boolean;
|
|
795
|
+
delivery_requested?: boolean;
|
|
796
|
+
delivery_verified?: boolean;
|
|
797
|
+
end_call_claims_verified?: boolean;
|
|
798
|
+
unverified_claims?: string[];
|
|
799
|
+
}
|
|
800
|
+
export interface ClassifyCallInput {
|
|
801
|
+
sessionId?: string;
|
|
802
|
+
agent?: string;
|
|
803
|
+
transcript?: string;
|
|
804
|
+
messages?: ClassifyMessage[];
|
|
805
|
+
groundTruth?: GroundTruthInput;
|
|
806
|
+
nudges?: number;
|
|
807
|
+
}
|
|
808
|
+
/** The labels post-call analysis generates for one finished call. */
|
|
809
|
+
export interface CallClassification {
|
|
810
|
+
/** Did the call achieve the agent's objective? */
|
|
811
|
+
achieved: boolean;
|
|
812
|
+
objective_achieved: boolean;
|
|
813
|
+
/** LLM objective score in [0,1] (before ground-truth blending). */
|
|
814
|
+
objective_score: number;
|
|
815
|
+
/** Blended objective value: (1-w)*LLM + w*ground-truth when supplied. */
|
|
816
|
+
objective_value: number;
|
|
817
|
+
ground_truth_score: number | null;
|
|
818
|
+
/** Per-criterion verdicts keyed by the objective's criterion names. */
|
|
819
|
+
criteria: Record<string, boolean | string>;
|
|
820
|
+
failure_reasons: string[];
|
|
821
|
+
summary: string;
|
|
822
|
+
/** Standing-directive version in force during the call (for A/B trends). */
|
|
823
|
+
directive_version: number;
|
|
824
|
+
agent: string;
|
|
825
|
+
[extra: string]: unknown;
|
|
826
|
+
}
|
|
827
|
+
export interface CallReportResult {
|
|
828
|
+
recorded: boolean;
|
|
829
|
+
/** Present when automatic post-call analysis ran — the generated labels. */
|
|
830
|
+
analysis?: CallClassification;
|
|
769
831
|
[extra: string]: unknown;
|
|
770
832
|
}
|
|
771
833
|
export interface BuilderTurn {
|
|
@@ -781,6 +843,47 @@ export interface BuilderChatResult {
|
|
|
781
843
|
oracle_ms?: number;
|
|
782
844
|
standing_version?: number;
|
|
783
845
|
}
|
|
846
|
+
/** One auto-generated adversarial scenario (from the agent's own prompt). */
|
|
847
|
+
export interface QAScenario {
|
|
848
|
+
title: string;
|
|
849
|
+
persona: string;
|
|
850
|
+
opener: string;
|
|
851
|
+
assertion: string;
|
|
852
|
+
}
|
|
853
|
+
/** The 5-level SSR nominal scale. */
|
|
854
|
+
export type SSRLabel = "poorly" | "ok" | "good" | "great" | "perfectly";
|
|
855
|
+
/** An SSR grade: nominal label -> deterministic score + bucket distribution. */
|
|
856
|
+
export interface SSRGrade {
|
|
857
|
+
label: SSRLabel;
|
|
858
|
+
score: number;
|
|
859
|
+
/** Probability mass over 10 score buckets [0-0.1, ..., 0.9-1.0]. */
|
|
860
|
+
distribution: number[];
|
|
861
|
+
rationale: string;
|
|
862
|
+
}
|
|
863
|
+
/** Result of qa.suite(): generated scenarios played vs the real agent config. */
|
|
864
|
+
export interface QASuiteResult {
|
|
865
|
+
agent: string;
|
|
866
|
+
objective: string;
|
|
867
|
+
supervised: boolean;
|
|
868
|
+
turns: number;
|
|
869
|
+
results: Array<{
|
|
870
|
+
scenario: string;
|
|
871
|
+
title: string;
|
|
872
|
+
persona: string;
|
|
873
|
+
assertion: string;
|
|
874
|
+
passed: boolean;
|
|
875
|
+
evidence: string;
|
|
876
|
+
ssr: SSRGrade;
|
|
877
|
+
transcript: BuilderTurn[];
|
|
878
|
+
}>;
|
|
879
|
+
summary: {
|
|
880
|
+
tests: number;
|
|
881
|
+
passed: number;
|
|
882
|
+
avg_ssr_score: number;
|
|
883
|
+
ssr_histogram: Record<SSRLabel, number>;
|
|
884
|
+
oracle_calls_billed: number;
|
|
885
|
+
};
|
|
886
|
+
}
|
|
784
887
|
export interface QAResult {
|
|
785
888
|
agent: string;
|
|
786
889
|
turns: number;
|
|
@@ -822,6 +925,7 @@ export declare class SupafoneLabs {
|
|
|
822
925
|
private readonly apiKey;
|
|
823
926
|
private readonly supafoneApiKey;
|
|
824
927
|
private readonly timeoutMs;
|
|
928
|
+
private readonly postCallAnalysis;
|
|
825
929
|
private sessionToken?;
|
|
826
930
|
private accountToken?;
|
|
827
931
|
private accountSessionToken?;
|
|
@@ -856,6 +960,8 @@ export declare class SupafoneLabs {
|
|
|
856
960
|
* re-login; an explicit accountToken is the caller's to refresh.
|
|
857
961
|
*/
|
|
858
962
|
requestAccountApi<T>(method: string, path: string, body?: unknown): Promise<T>;
|
|
963
|
+
/** @internal Multipart file POST with the same auth/re-login as requestAccountApi. */
|
|
964
|
+
requestAccountUpload<T>(path: string, file: Uint8Array | ArrayBuffer | Blob, filename: string): Promise<T>;
|
|
859
965
|
/** @internal Raw product-API request with an explicit bearer ("" = none). */
|
|
860
966
|
private accountHttp;
|
|
861
967
|
/**
|
|
@@ -870,6 +976,22 @@ export declare class SupafoneLabs {
|
|
|
870
976
|
listVoiceAgents(): Promise<{
|
|
871
977
|
agents: Record<string, unknown>[];
|
|
872
978
|
}>;
|
|
979
|
+
/**
|
|
980
|
+
* Scan a website for its branding: business name, brand colors, logo,
|
|
981
|
+
* favicon, Open Graph metadata, page images, and key same-domain pages.
|
|
982
|
+
*/
|
|
983
|
+
scanBrand(url: string): Promise<BrandScanResult>;
|
|
984
|
+
/**
|
|
985
|
+
* Generate a guided intake form (IntakeConfig) from a plain-language
|
|
986
|
+
* description. Pass agentId to ground it in that agent's business, and
|
|
987
|
+
* apply:true to write it onto the agent.
|
|
988
|
+
*/
|
|
989
|
+
generateIntakeForm(opts: {
|
|
990
|
+
description: string;
|
|
991
|
+
agentId?: string;
|
|
992
|
+
industry?: string;
|
|
993
|
+
apply?: boolean;
|
|
994
|
+
}): Promise<GenerateIntakeResult>;
|
|
873
995
|
/** Raw oracle completion — full control over messages and model. */
|
|
874
996
|
oracle(req: OracleRequest): Promise<OracleResult>;
|
|
875
997
|
/**
|
|
@@ -912,8 +1034,25 @@ export declare class SupafoneLabs {
|
|
|
912
1034
|
reportNudge(event: NudgeEvent): Promise<{
|
|
913
1035
|
ok?: boolean;
|
|
914
1036
|
}>;
|
|
915
|
-
/**
|
|
916
|
-
|
|
1037
|
+
/**
|
|
1038
|
+
* File a post-call report — the fuel optimizer.improve() learns from.
|
|
1039
|
+
*
|
|
1040
|
+
* With `postCallAnalysis: true` on the client and a transcript (or
|
|
1041
|
+
* messages) present, the call is automatically classified first: the
|
|
1042
|
+
* oracle labels it against the agent's objective (achieved/missed,
|
|
1043
|
+
* per-criterion verdicts, failure reasons) and files the enriched report
|
|
1044
|
+
* server-side. The generated labels come back on `analysis`. Analysis is
|
|
1045
|
+
* best-effort — on any failure the plain zero-billed report still lands.
|
|
1046
|
+
*/
|
|
1047
|
+
reportCall(report: CallReportInput): Promise<CallReportResult>;
|
|
1048
|
+
/**
|
|
1049
|
+
* Post-call analysis for one finished call: classify it against the agent's
|
|
1050
|
+
* objective and get labels back — achieved/missed, per-criterion verdicts,
|
|
1051
|
+
* failure reasons, and the blended objective value. Files an enriched call
|
|
1052
|
+
* report server-side (feeding optimizer.improve() and objective stats).
|
|
1053
|
+
* Billed one oracle call.
|
|
1054
|
+
*/
|
|
1055
|
+
classifyCall(input: ClassifyCallInput): Promise<CallClassification>;
|
|
917
1056
|
/** Available oracle model ids (live vendor catalog). */
|
|
918
1057
|
models(): Promise<string[]>;
|
|
919
1058
|
/** Available TTS voice ids. */
|
|
@@ -998,6 +1137,48 @@ export interface CampaignUpdateInput {
|
|
|
998
1137
|
}[];
|
|
999
1138
|
settings?: Record<string, unknown>;
|
|
1000
1139
|
}
|
|
1140
|
+
export interface BrandScanResult {
|
|
1141
|
+
url: string;
|
|
1142
|
+
business_name: string;
|
|
1143
|
+
colors: string[];
|
|
1144
|
+
primary_color: string;
|
|
1145
|
+
theme_color: string;
|
|
1146
|
+
css_brand_colors: string[];
|
|
1147
|
+
logo_url: string;
|
|
1148
|
+
favicon_url: string;
|
|
1149
|
+
og: {
|
|
1150
|
+
title: string;
|
|
1151
|
+
description: string;
|
|
1152
|
+
image: string;
|
|
1153
|
+
};
|
|
1154
|
+
images: string[];
|
|
1155
|
+
key_urls: string[];
|
|
1156
|
+
scrape_source: string;
|
|
1157
|
+
fallback_used: boolean;
|
|
1158
|
+
error?: string | null;
|
|
1159
|
+
}
|
|
1160
|
+
export interface GenerateIntakeResult {
|
|
1161
|
+
intake: Record<string, unknown>;
|
|
1162
|
+
/** false means the deterministic industry-workflow fallback was used. */
|
|
1163
|
+
generated: boolean;
|
|
1164
|
+
applied: boolean;
|
|
1165
|
+
agent?: Record<string, unknown>;
|
|
1166
|
+
}
|
|
1167
|
+
export interface CampaignConfigReport {
|
|
1168
|
+
valid: boolean;
|
|
1169
|
+
errors: string[];
|
|
1170
|
+
warnings: string[];
|
|
1171
|
+
summary: Record<string, unknown>;
|
|
1172
|
+
}
|
|
1173
|
+
export interface CampaignConfigApplyResult {
|
|
1174
|
+
campaign: CampaignSummary;
|
|
1175
|
+
created: boolean;
|
|
1176
|
+
added: number;
|
|
1177
|
+
launched: boolean;
|
|
1178
|
+
/** Branding scan / intake generation results for the doc's agent-facing blocks. */
|
|
1179
|
+
extras?: Record<string, unknown> | null;
|
|
1180
|
+
report: CampaignConfigReport;
|
|
1181
|
+
}
|
|
1001
1182
|
/**
|
|
1002
1183
|
* Typical flow:
|
|
1003
1184
|
* ```ts
|
|
@@ -1080,6 +1261,64 @@ declare class CampaignsNamespace {
|
|
|
1080
1261
|
}): Promise<{
|
|
1081
1262
|
link: Record<string, unknown>;
|
|
1082
1263
|
}>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Upload the PDF this campaign sends for e-signature. Pass raw bytes
|
|
1266
|
+
* (Uint8Array/ArrayBuffer/Blob — in Node: fs.readFileSync(path)). The server
|
|
1267
|
+
* auto-detects signature/date/initials lines and returns their placements —
|
|
1268
|
+
* apply them with setSignatureFields.
|
|
1269
|
+
*/
|
|
1270
|
+
uploadSigningDocument(campaignId: string, file: Uint8Array | ArrayBuffer | Blob, filename?: string): Promise<{
|
|
1271
|
+
campaign: CampaignSummary;
|
|
1272
|
+
asset: Record<string, unknown>;
|
|
1273
|
+
detected_fields: Record<string, unknown>[];
|
|
1274
|
+
}>;
|
|
1275
|
+
detectSignatureFields(campaignId: string): Promise<{
|
|
1276
|
+
fields: Record<string, unknown>[];
|
|
1277
|
+
detected: boolean;
|
|
1278
|
+
}>;
|
|
1279
|
+
/**
|
|
1280
|
+
* Place the signing fields: [{key, type: "signature"|"date"|"initials"|"text",
|
|
1281
|
+
* label, required, placement: {page, x, y, width, height}}] in PDF points
|
|
1282
|
+
* (origin bottom-left, 612x792 page). Merges onto the stored doc config.
|
|
1283
|
+
*/
|
|
1284
|
+
setSignatureFields(campaignId: string, fields: Record<string, unknown>[]): Promise<{
|
|
1285
|
+
campaign: CampaignSummary;
|
|
1286
|
+
}>;
|
|
1287
|
+
/** Pure dry-run: {valid, errors[], warnings[], summary} — no side effects. */
|
|
1288
|
+
validateConfig(config: string, opts?: {
|
|
1289
|
+
accountId?: string;
|
|
1290
|
+
launch?: boolean;
|
|
1291
|
+
}): Promise<CampaignConfigReport>;
|
|
1292
|
+
/**
|
|
1293
|
+
* Upsert a campaign from a campaign-as-code YAML/JSON document (by slug).
|
|
1294
|
+
* The doc's branding:/intake_form: blocks restyle the campaign's agent and
|
|
1295
|
+
* generate its intake form on apply. launch:true starts REAL calls/emails.
|
|
1296
|
+
*/
|
|
1297
|
+
applyConfig(config: string, opts?: {
|
|
1298
|
+
accountId?: string;
|
|
1299
|
+
launch?: boolean;
|
|
1300
|
+
}): Promise<CampaignConfigApplyResult>;
|
|
1301
|
+
/** The campaign as its canonical YAML document — round-trips through applyConfig. */
|
|
1302
|
+
exportConfig(campaignId: string): Promise<{
|
|
1303
|
+
config: string;
|
|
1304
|
+
format: string;
|
|
1305
|
+
slug: string;
|
|
1306
|
+
}>;
|
|
1307
|
+
/**
|
|
1308
|
+
* Draft a campaign-as-code YAML document from a plain-language description
|
|
1309
|
+
* (+ optional CSV of leads). No side effects — review, then applyConfig.
|
|
1310
|
+
*/
|
|
1311
|
+
generateConfig(opts: {
|
|
1312
|
+
prompt: string;
|
|
1313
|
+
csv?: string;
|
|
1314
|
+
agentId?: string;
|
|
1315
|
+
accountId?: string;
|
|
1316
|
+
}): Promise<{
|
|
1317
|
+
config: string;
|
|
1318
|
+
format: string;
|
|
1319
|
+
recipients_parsed: number;
|
|
1320
|
+
generated: boolean;
|
|
1321
|
+
}>;
|
|
1083
1322
|
}
|
|
1084
1323
|
declare class LabsNamespace {
|
|
1085
1324
|
private sm;
|
|
@@ -1234,6 +1473,29 @@ declare class QANamespace {
|
|
|
1234
1473
|
scenarios?: string[];
|
|
1235
1474
|
turns?: number;
|
|
1236
1475
|
}): Promise<QAResult>;
|
|
1476
|
+
/**
|
|
1477
|
+
* Auto-generate adversarial test scenarios from the agent's own prompt
|
|
1478
|
+
* (key-scoped). Each scenario carries a persona, an opener, and the one
|
|
1479
|
+
* assertion the agent must (or must not) satisfy.
|
|
1480
|
+
*/
|
|
1481
|
+
generate(opts: {
|
|
1482
|
+
agentPrompt: string;
|
|
1483
|
+
count?: number;
|
|
1484
|
+
}): Promise<{
|
|
1485
|
+
scenarios: QAScenario[];
|
|
1486
|
+
}>;
|
|
1487
|
+
/**
|
|
1488
|
+
* Build + run a bespoke adversarial suite in one call: scenarios are
|
|
1489
|
+
* generated from the agent's own objective, each is played as a mock call
|
|
1490
|
+
* against the REAL configured agent, and every call is judged twice —
|
|
1491
|
+
* pass/fail on the scenario's assertion AND an SSR grade (poorly/ok/good/
|
|
1492
|
+
* great/perfectly) against the objective. Session-scoped — login() first.
|
|
1493
|
+
*/
|
|
1494
|
+
suite(opts?: {
|
|
1495
|
+
count?: number;
|
|
1496
|
+
turns?: number;
|
|
1497
|
+
supervised?: boolean;
|
|
1498
|
+
}): Promise<QASuiteResult>;
|
|
1237
1499
|
/** Past QA runs (works with the API key). */
|
|
1238
1500
|
history(agent?: string, limit?: number): Promise<unknown>;
|
|
1239
1501
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -47,6 +47,7 @@ class SupafoneLabs {
|
|
|
47
47
|
apiKey;
|
|
48
48
|
supafoneApiKey;
|
|
49
49
|
timeoutMs;
|
|
50
|
+
postCallAnalysis;
|
|
50
51
|
sessionToken;
|
|
51
52
|
accountToken;
|
|
52
53
|
accountSessionToken;
|
|
@@ -68,6 +69,7 @@ class SupafoneLabs {
|
|
|
68
69
|
this.supafoneApiBaseUrl = (opts.supafoneApiBaseUrl ?? DEFAULT_SUPAFONE_API_BASE).replace(/\/$/, "");
|
|
69
70
|
this.appUrl = (opts.appUrl ?? "https://app.supafone.ai").replace(/\/$/, "");
|
|
70
71
|
this.timeoutMs = opts.timeoutMs ?? 30_000;
|
|
72
|
+
this.postCallAnalysis = opts.postCallAnalysis ?? false;
|
|
71
73
|
this.sessionToken = opts.sessionToken;
|
|
72
74
|
this.accountToken = opts.accountToken;
|
|
73
75
|
this.accountEmail = opts.accountEmail;
|
|
@@ -176,6 +178,38 @@ class SupafoneLabs {
|
|
|
176
178
|
throw err;
|
|
177
179
|
}
|
|
178
180
|
}
|
|
181
|
+
/** @internal Multipart file POST with the same auth/re-login as requestAccountApi. */
|
|
182
|
+
async requestAccountUpload(path, file, filename) {
|
|
183
|
+
const token = this.accountToken || this.accountSessionToken || (await this.accountLogin());
|
|
184
|
+
const send = async (bearer) => {
|
|
185
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: "application/pdf" });
|
|
186
|
+
const form = new FormData();
|
|
187
|
+
form.append("file", blob, filename);
|
|
188
|
+
const res = await fetch(this.supafoneApiBaseUrl + path, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { Authorization: `Bearer ${bearer}` },
|
|
191
|
+
body: form,
|
|
192
|
+
});
|
|
193
|
+
const text = await res.text();
|
|
194
|
+
const parsed = text ? safeJson(text) : {};
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
|
|
197
|
+
throw new SupafoneLabsError(`POST ${path}: ${detail}`, res.status, parsed);
|
|
198
|
+
}
|
|
199
|
+
return parsed;
|
|
200
|
+
};
|
|
201
|
+
try {
|
|
202
|
+
return await send(token);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
const expired = err instanceof SupafoneLabsError && err.status === 401;
|
|
206
|
+
if (expired && !this.accountToken && this.accountEmail && this.accountPassword) {
|
|
207
|
+
this.accountSessionToken = undefined;
|
|
208
|
+
return send(await this.accountLogin());
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
179
213
|
/** @internal Raw product-API request with an explicit bearer ("" = none). */
|
|
180
214
|
async accountHttp(method, path, body, token) {
|
|
181
215
|
const ctrl = new AbortController();
|
|
@@ -220,6 +254,36 @@ class SupafoneLabs {
|
|
|
220
254
|
async listVoiceAgents() {
|
|
221
255
|
return this.requestAccountApi("GET", "/api/v1/agents");
|
|
222
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Scan a website for its branding: business name, brand colors, logo,
|
|
259
|
+
* favicon, Open Graph metadata, page images, and key same-domain pages.
|
|
260
|
+
*/
|
|
261
|
+
async scanBrand(url) {
|
|
262
|
+
if (!url?.trim())
|
|
263
|
+
throw new SupafoneLabsError("url is required (the website to scan)");
|
|
264
|
+
return this.requestAccountApi("POST", "/api/v1/agents/brand-scan", { url: url.trim() });
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Generate a guided intake form (IntakeConfig) from a plain-language
|
|
268
|
+
* description. Pass agentId to ground it in that agent's business, and
|
|
269
|
+
* apply:true to write it onto the agent.
|
|
270
|
+
*/
|
|
271
|
+
async generateIntakeForm(opts) {
|
|
272
|
+
if (!opts?.description?.trim()) {
|
|
273
|
+
throw new SupafoneLabsError("description is required — what should the form collect?");
|
|
274
|
+
}
|
|
275
|
+
if (opts.apply && !opts.agentId) {
|
|
276
|
+
throw new SupafoneLabsError("apply:true needs an agentId (see listVoiceAgents())");
|
|
277
|
+
}
|
|
278
|
+
const payload = { description: opts.description.trim() };
|
|
279
|
+
if (opts.industry)
|
|
280
|
+
payload.industry = opts.industry;
|
|
281
|
+
if (opts.agentId) {
|
|
282
|
+
payload.apply = Boolean(opts.apply);
|
|
283
|
+
return this.requestAccountApi("POST", `/api/v1/agents/${encodeURIComponent(opts.agentId)}/generate-intake`, payload);
|
|
284
|
+
}
|
|
285
|
+
return this.requestAccountApi("POST", "/api/v1/agents/generate-intake", payload);
|
|
286
|
+
}
|
|
223
287
|
/** Raw oracle completion — full control over messages and model. */
|
|
224
288
|
async oracle(req) {
|
|
225
289
|
return this.request("POST", "/v1/oracle/complete", {
|
|
@@ -369,9 +433,55 @@ class SupafoneLabs {
|
|
|
369
433
|
reportNudge(event) {
|
|
370
434
|
return this.request("POST", "/v1/events/nudge", event);
|
|
371
435
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
|
|
436
|
+
/**
|
|
437
|
+
* File a post-call report — the fuel optimizer.improve() learns from.
|
|
438
|
+
*
|
|
439
|
+
* With `postCallAnalysis: true` on the client and a transcript (or
|
|
440
|
+
* messages) present, the call is automatically classified first: the
|
|
441
|
+
* oracle labels it against the agent's objective (achieved/missed,
|
|
442
|
+
* per-criterion verdicts, failure reasons) and files the enriched report
|
|
443
|
+
* server-side. The generated labels come back on `analysis`. Analysis is
|
|
444
|
+
* best-effort — on any failure the plain zero-billed report still lands.
|
|
445
|
+
*/
|
|
446
|
+
async reportCall(report) {
|
|
447
|
+
const { transcript, messages, ground_truth, ...plain } = report;
|
|
448
|
+
const analyzable = !!transcript || !!(messages && messages.length);
|
|
449
|
+
if (this.postCallAnalysis && analyzable) {
|
|
450
|
+
try {
|
|
451
|
+
const analysis = await this.classifyCall({
|
|
452
|
+
sessionId: report.session_id,
|
|
453
|
+
agent: report.agent,
|
|
454
|
+
transcript,
|
|
455
|
+
messages,
|
|
456
|
+
groundTruth: ground_truth,
|
|
457
|
+
nudges: report.nudges,
|
|
458
|
+
});
|
|
459
|
+
// classifyCall files the enriched report server-side — don't double-file.
|
|
460
|
+
return { recorded: true, analysis };
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
/* analysis is best-effort — fall through to the plain report */
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const out = await this.request("POST", "/v1/events/call_report", plain);
|
|
467
|
+
return { recorded: true, ...out };
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Post-call analysis for one finished call: classify it against the agent's
|
|
471
|
+
* objective and get labels back — achieved/missed, per-criterion verdicts,
|
|
472
|
+
* failure reasons, and the blended objective value. Files an enriched call
|
|
473
|
+
* report server-side (feeding optimizer.improve() and objective stats).
|
|
474
|
+
* Billed one oracle call.
|
|
475
|
+
*/
|
|
476
|
+
classifyCall(input) {
|
|
477
|
+
return this.request("POST", "/v1/calls/classify", compact({
|
|
478
|
+
session_id: input.sessionId,
|
|
479
|
+
agent: input.agent ?? "builder",
|
|
480
|
+
transcript: input.transcript,
|
|
481
|
+
messages: input.messages,
|
|
482
|
+
ground_truth: input.groundTruth,
|
|
483
|
+
nudges: input.nudges,
|
|
484
|
+
}));
|
|
375
485
|
}
|
|
376
486
|
/** Available oracle model ids (live vendor catalog). */
|
|
377
487
|
async models() {
|
|
@@ -559,6 +669,73 @@ class CampaignsNamespace {
|
|
|
559
669
|
createSignLink(campaignId, recipientId, opts = {}) {
|
|
560
670
|
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients/${encodeURIComponent(recipientId)}/sign-link`, compact({ title: opts.title, message: opts.message }));
|
|
561
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Upload the PDF this campaign sends for e-signature. Pass raw bytes
|
|
674
|
+
* (Uint8Array/ArrayBuffer/Blob — in Node: fs.readFileSync(path)). The server
|
|
675
|
+
* auto-detects signature/date/initials lines and returns their placements —
|
|
676
|
+
* apply them with setSignatureFields.
|
|
677
|
+
*/
|
|
678
|
+
uploadSigningDocument(campaignId, file, filename = "document.pdf") {
|
|
679
|
+
return this.sm.requestAccountUpload(`/api/v1/campaigns/${encodeURIComponent(campaignId)}/signing/document`, file, filename);
|
|
680
|
+
}
|
|
681
|
+
detectSignatureFields(campaignId) {
|
|
682
|
+
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/signing/detect-fields`, {});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Place the signing fields: [{key, type: "signature"|"date"|"initials"|"text",
|
|
686
|
+
* label, required, placement: {page, x, y, width, height}}] in PDF points
|
|
687
|
+
* (origin bottom-left, 612x792 page). Merges onto the stored doc config.
|
|
688
|
+
*/
|
|
689
|
+
async setSignatureFields(campaignId, fields) {
|
|
690
|
+
if (!Array.isArray(fields) || !fields.length) {
|
|
691
|
+
throw new SupafoneLabsError("fields must be a non-empty array of placed fields");
|
|
692
|
+
}
|
|
693
|
+
const { campaign } = await this.get(campaignId);
|
|
694
|
+
const settings = { ...(campaign.settings ?? {}) };
|
|
695
|
+
const native = { ...(settings.native_signing ?? {}) };
|
|
696
|
+
if (!native.pdfUrl && !native.storedName) {
|
|
697
|
+
throw new SupafoneLabsError("Upload the signing PDF first (uploadSigningDocument)");
|
|
698
|
+
}
|
|
699
|
+
native.enabled = true;
|
|
700
|
+
native.fields = fields;
|
|
701
|
+
return this.update(campaignId, { settings: { ...settings, native_signing: native } });
|
|
702
|
+
}
|
|
703
|
+
// -- campaign-as-code (one YAML/JSON document per campaign) ----------------
|
|
704
|
+
/** Pure dry-run: {valid, errors[], warnings[], summary} — no side effects. */
|
|
705
|
+
validateConfig(config, opts = {}) {
|
|
706
|
+
return this.sm.requestAccountApi("POST", "/api/v1/campaigns/config/validate", {
|
|
707
|
+
config,
|
|
708
|
+
...compact({ account_id: opts.accountId, launch: opts.launch }),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Upsert a campaign from a campaign-as-code YAML/JSON document (by slug).
|
|
713
|
+
* The doc's branding:/intake_form: blocks restyle the campaign's agent and
|
|
714
|
+
* generate its intake form on apply. launch:true starts REAL calls/emails.
|
|
715
|
+
*/
|
|
716
|
+
applyConfig(config, opts = {}) {
|
|
717
|
+
return this.sm.requestAccountApi("POST", "/api/v1/campaigns/config/apply", {
|
|
718
|
+
config,
|
|
719
|
+
...compact({ account_id: opts.accountId, launch: opts.launch }),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
/** The campaign as its canonical YAML document — round-trips through applyConfig. */
|
|
723
|
+
exportConfig(campaignId) {
|
|
724
|
+
return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/config`);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Draft a campaign-as-code YAML document from a plain-language description
|
|
728
|
+
* (+ optional CSV of leads). No side effects — review, then applyConfig.
|
|
729
|
+
*/
|
|
730
|
+
generateConfig(opts) {
|
|
731
|
+
if (!opts?.prompt?.trim()) {
|
|
732
|
+
throw new SupafoneLabsError("prompt is required — describe the campaign to draft");
|
|
733
|
+
}
|
|
734
|
+
return this.sm.requestAccountApi("POST", "/api/v1/campaigns/config/generate", {
|
|
735
|
+
prompt: opts.prompt.trim(),
|
|
736
|
+
...compact({ csv: opts.csv, agent_id: opts.agentId, account_id: opts.accountId }),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
562
739
|
}
|
|
563
740
|
class LabsNamespace {
|
|
564
741
|
sm;
|
|
@@ -926,6 +1103,31 @@ class QANamespace {
|
|
|
926
1103
|
run(opts = {}) {
|
|
927
1104
|
return this.sm.request("POST", "/v1/qa/run", { scenarios: opts.scenarios ?? [], turns: opts.turns ?? 2 }, true);
|
|
928
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Auto-generate adversarial test scenarios from the agent's own prompt
|
|
1108
|
+
* (key-scoped). Each scenario carries a persona, an opener, and the one
|
|
1109
|
+
* assertion the agent must (or must not) satisfy.
|
|
1110
|
+
*/
|
|
1111
|
+
generate(opts) {
|
|
1112
|
+
return this.sm.request("POST", "/v1/qa/generate", {
|
|
1113
|
+
agent_prompt: opts.agentPrompt,
|
|
1114
|
+
count: opts.count ?? 5,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Build + run a bespoke adversarial suite in one call: scenarios are
|
|
1119
|
+
* generated from the agent's own objective, each is played as a mock call
|
|
1120
|
+
* against the REAL configured agent, and every call is judged twice —
|
|
1121
|
+
* pass/fail on the scenario's assertion AND an SSR grade (poorly/ok/good/
|
|
1122
|
+
* great/perfectly) against the objective. Session-scoped — login() first.
|
|
1123
|
+
*/
|
|
1124
|
+
suite(opts = {}) {
|
|
1125
|
+
return this.sm.request("POST", "/v1/qa/suite", {
|
|
1126
|
+
count: opts.count ?? 4,
|
|
1127
|
+
turns: opts.turns ?? 2,
|
|
1128
|
+
supervised: opts.supervised ?? false,
|
|
1129
|
+
}, true);
|
|
1130
|
+
}
|
|
929
1131
|
/** Past QA runs (works with the API key). */
|
|
930
1132
|
history(agent = "builder", limit = 40) {
|
|
931
1133
|
return this.sm.request("GET", `/v1/qa/runs?agent=${encodeURIComponent(agent)}&limit=${limit}`);
|