supafone-labs 0.4.0 → 0.4.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/README.md +84 -1
- package/dist/cjs/index.d.ts +171 -2
- package/dist/cjs/index.js +139 -3
- package/dist/index.d.ts +171 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +274 -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,14 +320,65 @@ 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
|
|
|
345
|
+
## Campaigns & real calls (account-scoped)
|
|
346
|
+
|
|
347
|
+
The outbound campaign engine behind app.supafone.ai, packaged. Authenticate
|
|
348
|
+
with your account (not an API key) — pass `accountToken`, or
|
|
349
|
+
`accountEmail` + `accountPassword` and the client logs in lazily (and
|
|
350
|
+
re-logs-in transparently when the token expires):
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const sf = new Supafone({ accountEmail: "you@company.com", accountPassword: "..." });
|
|
354
|
+
|
|
355
|
+
const { agents } = await sf.listVoiceAgents();
|
|
356
|
+
const { campaign } = await sf.campaigns.create({ name: "Q3 win-back", goal: "reengage", agentId: agents[0].id });
|
|
357
|
+
await sf.campaigns.applyPreset(campaign.id, "win_back"); // or your custom_… preset
|
|
358
|
+
await sf.campaigns.addRecipients(campaign.id, [
|
|
359
|
+
{ name: "Jane Doe", phone: "+15551234567", outreach_consent: "yes" },
|
|
360
|
+
]);
|
|
361
|
+
await sf.campaigns.launch(campaign.id); // real calls + emails begin
|
|
362
|
+
|
|
363
|
+
const live = await sf.campaigns.live(campaign.id); // in-flight calls + listen links
|
|
364
|
+
await sf.placeCall({ agentId: agents[0].id, toNumber: "+15551234567" }); // ring a phone right now
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`campaigns.live()` returns a portal link (`app.supafone.ai/app/developer`) and
|
|
368
|
+
a listen link per in-flight call; poll `campaigns.getCall(id)` to follow the
|
|
369
|
+
live transcript while a call is in progress.
|
|
370
|
+
|
|
371
|
+
## Prefer natural language? Use the MCP server
|
|
372
|
+
|
|
373
|
+
The repo ships an MCP stdio server (`services/supafone-labs/mcp/supafone_mcp.py`)
|
|
374
|
+
exposing this same surface — plus hosted-agent provisioning — as tools for
|
|
375
|
+
Claude Desktop / Claude Code. Configure it with `SUPAFONE_EMAIL` +
|
|
376
|
+
`SUPAFONE_PASSWORD` (campaigns/calls) and/or `SUPAFONE_API_KEY` (hosted
|
|
377
|
+
agents), then just ask: *"create a win-back campaign, add these leads, launch
|
|
378
|
+
it, and show me the calls as they happen"* — Claude replies with developer-
|
|
379
|
+
portal links to watch the calls live. Full tool reference lives in the repo's
|
|
380
|
+
`gitbook/mcp-server.md`.
|
|
381
|
+
|
|
300
382
|
## Module format
|
|
301
383
|
|
|
302
384
|
Ships **both ESM and CommonJS**. `import { Supafone } from "supafone-labs"`
|
|
@@ -318,8 +400,9 @@ and `const { Supafone } = require("supafone-labs")` both work, with full types.
|
|
|
318
400
|
| `stt(audio, opts?)` | `POST /v1/stt` |
|
|
319
401
|
| `liveTranscribe(opts?)` | `WS /v1/stt/live` |
|
|
320
402
|
| `balance()` · `models()` · `voices()` · `usage()` | reads |
|
|
403
|
+
| `reportCall(report)` · `classifyCall(input)` | `/v1/events/call_report` · `/v1/calls/classify` (auto with `postCallAnalysis: true`) |
|
|
321
404
|
| `builder.chat/finish/config` | `/v1/builder/*` |
|
|
322
|
-
| `qa.run/history` | `/v1/qa/*` |
|
|
405
|
+
| `qa.run/generate/suite/history` | `/v1/qa/*` |
|
|
323
406
|
| `optimizer.improve/standing` | `/v1/optimizer/*` |
|
|
324
407
|
|
|
325
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
|
/**
|
|
@@ -912,8 +1018,25 @@ export declare class SupafoneLabs {
|
|
|
912
1018
|
reportNudge(event: NudgeEvent): Promise<{
|
|
913
1019
|
ok?: boolean;
|
|
914
1020
|
}>;
|
|
915
|
-
/**
|
|
916
|
-
|
|
1021
|
+
/**
|
|
1022
|
+
* File a post-call report — the fuel optimizer.improve() learns from.
|
|
1023
|
+
*
|
|
1024
|
+
* With `postCallAnalysis: true` on the client and a transcript (or
|
|
1025
|
+
* messages) present, the call is automatically classified first: the
|
|
1026
|
+
* oracle labels it against the agent's objective (achieved/missed,
|
|
1027
|
+
* per-criterion verdicts, failure reasons) and files the enriched report
|
|
1028
|
+
* server-side. The generated labels come back on `analysis`. Analysis is
|
|
1029
|
+
* best-effort — on any failure the plain zero-billed report still lands.
|
|
1030
|
+
*/
|
|
1031
|
+
reportCall(report: CallReportInput): Promise<CallReportResult>;
|
|
1032
|
+
/**
|
|
1033
|
+
* Post-call analysis for one finished call: classify it against the agent's
|
|
1034
|
+
* objective and get labels back — achieved/missed, per-criterion verdicts,
|
|
1035
|
+
* failure reasons, and the blended objective value. Files an enriched call
|
|
1036
|
+
* report server-side (feeding optimizer.improve() and objective stats).
|
|
1037
|
+
* Billed one oracle call.
|
|
1038
|
+
*/
|
|
1039
|
+
classifyCall(input: ClassifyCallInput): Promise<CallClassification>;
|
|
917
1040
|
/** Available oracle model ids (live vendor catalog). */
|
|
918
1041
|
models(): Promise<string[]>;
|
|
919
1042
|
/** Available TTS voice ids. */
|
|
@@ -1080,6 +1203,29 @@ declare class CampaignsNamespace {
|
|
|
1080
1203
|
}): Promise<{
|
|
1081
1204
|
link: Record<string, unknown>;
|
|
1082
1205
|
}>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Upload the PDF this campaign sends for e-signature. Pass raw bytes
|
|
1208
|
+
* (Uint8Array/ArrayBuffer/Blob — in Node: fs.readFileSync(path)). The server
|
|
1209
|
+
* auto-detects signature/date/initials lines and returns their placements —
|
|
1210
|
+
* apply them with setSignatureFields.
|
|
1211
|
+
*/
|
|
1212
|
+
uploadSigningDocument(campaignId: string, file: Uint8Array | ArrayBuffer | Blob, filename?: string): Promise<{
|
|
1213
|
+
campaign: CampaignSummary;
|
|
1214
|
+
asset: Record<string, unknown>;
|
|
1215
|
+
detected_fields: Record<string, unknown>[];
|
|
1216
|
+
}>;
|
|
1217
|
+
detectSignatureFields(campaignId: string): Promise<{
|
|
1218
|
+
fields: Record<string, unknown>[];
|
|
1219
|
+
detected: boolean;
|
|
1220
|
+
}>;
|
|
1221
|
+
/**
|
|
1222
|
+
* Place the signing fields: [{key, type: "signature"|"date"|"initials"|"text",
|
|
1223
|
+
* label, required, placement: {page, x, y, width, height}}] in PDF points
|
|
1224
|
+
* (origin bottom-left, 612x792 page). Merges onto the stored doc config.
|
|
1225
|
+
*/
|
|
1226
|
+
setSignatureFields(campaignId: string, fields: Record<string, unknown>[]): Promise<{
|
|
1227
|
+
campaign: CampaignSummary;
|
|
1228
|
+
}>;
|
|
1083
1229
|
}
|
|
1084
1230
|
declare class LabsNamespace {
|
|
1085
1231
|
private sm;
|
|
@@ -1234,6 +1380,29 @@ declare class QANamespace {
|
|
|
1234
1380
|
scenarios?: string[];
|
|
1235
1381
|
turns?: number;
|
|
1236
1382
|
}): Promise<QAResult>;
|
|
1383
|
+
/**
|
|
1384
|
+
* Auto-generate adversarial test scenarios from the agent's own prompt
|
|
1385
|
+
* (key-scoped). Each scenario carries a persona, an opener, and the one
|
|
1386
|
+
* assertion the agent must (or must not) satisfy.
|
|
1387
|
+
*/
|
|
1388
|
+
generate(opts: {
|
|
1389
|
+
agentPrompt: string;
|
|
1390
|
+
count?: number;
|
|
1391
|
+
}): Promise<{
|
|
1392
|
+
scenarios: QAScenario[];
|
|
1393
|
+
}>;
|
|
1394
|
+
/**
|
|
1395
|
+
* Build + run a bespoke adversarial suite in one call: scenarios are
|
|
1396
|
+
* generated from the agent's own objective, each is played as a mock call
|
|
1397
|
+
* against the REAL configured agent, and every call is judged twice —
|
|
1398
|
+
* pass/fail on the scenario's assertion AND an SSR grade (poorly/ok/good/
|
|
1399
|
+
* great/perfectly) against the objective. Session-scoped — login() first.
|
|
1400
|
+
*/
|
|
1401
|
+
suite(opts?: {
|
|
1402
|
+
count?: number;
|
|
1403
|
+
turns?: number;
|
|
1404
|
+
supervised?: boolean;
|
|
1405
|
+
}): Promise<QASuiteResult>;
|
|
1237
1406
|
/** Past QA runs (works with the API key). */
|
|
1238
1407
|
history(agent?: string, limit?: number): Promise<unknown>;
|
|
1239
1408
|
}
|
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();
|
|
@@ -369,9 +403,55 @@ class SupafoneLabs {
|
|
|
369
403
|
reportNudge(event) {
|
|
370
404
|
return this.request("POST", "/v1/events/nudge", event);
|
|
371
405
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
|
|
406
|
+
/**
|
|
407
|
+
* File a post-call report — the fuel optimizer.improve() learns from.
|
|
408
|
+
*
|
|
409
|
+
* With `postCallAnalysis: true` on the client and a transcript (or
|
|
410
|
+
* messages) present, the call is automatically classified first: the
|
|
411
|
+
* oracle labels it against the agent's objective (achieved/missed,
|
|
412
|
+
* per-criterion verdicts, failure reasons) and files the enriched report
|
|
413
|
+
* server-side. The generated labels come back on `analysis`. Analysis is
|
|
414
|
+
* best-effort — on any failure the plain zero-billed report still lands.
|
|
415
|
+
*/
|
|
416
|
+
async reportCall(report) {
|
|
417
|
+
const { transcript, messages, ground_truth, ...plain } = report;
|
|
418
|
+
const analyzable = !!transcript || !!(messages && messages.length);
|
|
419
|
+
if (this.postCallAnalysis && analyzable) {
|
|
420
|
+
try {
|
|
421
|
+
const analysis = await this.classifyCall({
|
|
422
|
+
sessionId: report.session_id,
|
|
423
|
+
agent: report.agent,
|
|
424
|
+
transcript,
|
|
425
|
+
messages,
|
|
426
|
+
groundTruth: ground_truth,
|
|
427
|
+
nudges: report.nudges,
|
|
428
|
+
});
|
|
429
|
+
// classifyCall files the enriched report server-side — don't double-file.
|
|
430
|
+
return { recorded: true, analysis };
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
/* analysis is best-effort — fall through to the plain report */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const out = await this.request("POST", "/v1/events/call_report", plain);
|
|
437
|
+
return { recorded: true, ...out };
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Post-call analysis for one finished call: classify it against the agent's
|
|
441
|
+
* objective and get labels back — achieved/missed, per-criterion verdicts,
|
|
442
|
+
* failure reasons, and the blended objective value. Files an enriched call
|
|
443
|
+
* report server-side (feeding optimizer.improve() and objective stats).
|
|
444
|
+
* Billed one oracle call.
|
|
445
|
+
*/
|
|
446
|
+
classifyCall(input) {
|
|
447
|
+
return this.request("POST", "/v1/calls/classify", compact({
|
|
448
|
+
session_id: input.sessionId,
|
|
449
|
+
agent: input.agent ?? "builder",
|
|
450
|
+
transcript: input.transcript,
|
|
451
|
+
messages: input.messages,
|
|
452
|
+
ground_truth: input.groundTruth,
|
|
453
|
+
nudges: input.nudges,
|
|
454
|
+
}));
|
|
375
455
|
}
|
|
376
456
|
/** Available oracle model ids (live vendor catalog). */
|
|
377
457
|
async models() {
|
|
@@ -559,6 +639,37 @@ class CampaignsNamespace {
|
|
|
559
639
|
createSignLink(campaignId, recipientId, opts = {}) {
|
|
560
640
|
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients/${encodeURIComponent(recipientId)}/sign-link`, compact({ title: opts.title, message: opts.message }));
|
|
561
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Upload the PDF this campaign sends for e-signature. Pass raw bytes
|
|
644
|
+
* (Uint8Array/ArrayBuffer/Blob — in Node: fs.readFileSync(path)). The server
|
|
645
|
+
* auto-detects signature/date/initials lines and returns their placements —
|
|
646
|
+
* apply them with setSignatureFields.
|
|
647
|
+
*/
|
|
648
|
+
uploadSigningDocument(campaignId, file, filename = "document.pdf") {
|
|
649
|
+
return this.sm.requestAccountUpload(`/api/v1/campaigns/${encodeURIComponent(campaignId)}/signing/document`, file, filename);
|
|
650
|
+
}
|
|
651
|
+
detectSignatureFields(campaignId) {
|
|
652
|
+
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/signing/detect-fields`, {});
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Place the signing fields: [{key, type: "signature"|"date"|"initials"|"text",
|
|
656
|
+
* label, required, placement: {page, x, y, width, height}}] in PDF points
|
|
657
|
+
* (origin bottom-left, 612x792 page). Merges onto the stored doc config.
|
|
658
|
+
*/
|
|
659
|
+
async setSignatureFields(campaignId, fields) {
|
|
660
|
+
if (!Array.isArray(fields) || !fields.length) {
|
|
661
|
+
throw new SupafoneLabsError("fields must be a non-empty array of placed fields");
|
|
662
|
+
}
|
|
663
|
+
const { campaign } = await this.get(campaignId);
|
|
664
|
+
const settings = { ...(campaign.settings ?? {}) };
|
|
665
|
+
const native = { ...(settings.native_signing ?? {}) };
|
|
666
|
+
if (!native.pdfUrl && !native.storedName) {
|
|
667
|
+
throw new SupafoneLabsError("Upload the signing PDF first (uploadSigningDocument)");
|
|
668
|
+
}
|
|
669
|
+
native.enabled = true;
|
|
670
|
+
native.fields = fields;
|
|
671
|
+
return this.update(campaignId, { settings: { ...settings, native_signing: native } });
|
|
672
|
+
}
|
|
562
673
|
}
|
|
563
674
|
class LabsNamespace {
|
|
564
675
|
sm;
|
|
@@ -926,6 +1037,31 @@ class QANamespace {
|
|
|
926
1037
|
run(opts = {}) {
|
|
927
1038
|
return this.sm.request("POST", "/v1/qa/run", { scenarios: opts.scenarios ?? [], turns: opts.turns ?? 2 }, true);
|
|
928
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Auto-generate adversarial test scenarios from the agent's own prompt
|
|
1042
|
+
* (key-scoped). Each scenario carries a persona, an opener, and the one
|
|
1043
|
+
* assertion the agent must (or must not) satisfy.
|
|
1044
|
+
*/
|
|
1045
|
+
generate(opts) {
|
|
1046
|
+
return this.sm.request("POST", "/v1/qa/generate", {
|
|
1047
|
+
agent_prompt: opts.agentPrompt,
|
|
1048
|
+
count: opts.count ?? 5,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Build + run a bespoke adversarial suite in one call: scenarios are
|
|
1053
|
+
* generated from the agent's own objective, each is played as a mock call
|
|
1054
|
+
* against the REAL configured agent, and every call is judged twice —
|
|
1055
|
+
* pass/fail on the scenario's assertion AND an SSR grade (poorly/ok/good/
|
|
1056
|
+
* great/perfectly) against the objective. Session-scoped — login() first.
|
|
1057
|
+
*/
|
|
1058
|
+
suite(opts = {}) {
|
|
1059
|
+
return this.sm.request("POST", "/v1/qa/suite", {
|
|
1060
|
+
count: opts.count ?? 4,
|
|
1061
|
+
turns: opts.turns ?? 2,
|
|
1062
|
+
supervised: opts.supervised ?? false,
|
|
1063
|
+
}, true);
|
|
1064
|
+
}
|
|
929
1065
|
/** Past QA runs (works with the API key). */
|
|
930
1066
|
history(agent = "builder", limit = 40) {
|
|
931
1067
|
return this.sm.request("GET", `/v1/qa/runs?agent=${encodeURIComponent(agent)}&limit=${limit}`);
|