@guava-ai/guava-sdk 0.27.0 → 0.28.0
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 +1 -1
- package/bin/example-runner.ts +67 -26
- package/dist/bin/example-runner.d.ts +1 -25
- package/dist/bin/example-runner.js +64 -24
- package/dist/bin/example-runner.js.map +1 -1
- package/dist/examples/help-desk.d.ts +1 -1
- package/dist/examples/help-desk.js +40 -29
- package/dist/examples/help-desk.js.map +1 -1
- package/dist/examples/multiple-agents.d.ts +1 -1
- package/dist/examples/multiple-agents.js +1 -1
- package/dist/examples/multiple-agents.js.map +1 -1
- package/dist/examples/polling-campaign.d.ts +1 -1
- package/dist/examples/polling-campaign.js +9 -7
- package/dist/examples/polling-campaign.js.map +1 -1
- package/dist/examples/property-insurance.d.ts +1 -1
- package/dist/examples/property-insurance.js +34 -23
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.d.ts +1 -1
- package/dist/examples/restaurant-waitlist.js +34 -23
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/examples/scheduling-outbound.d.ts +1 -1
- package/dist/examples/scheduling-outbound.js +24 -14
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/action-item.d.ts +8 -4
- package/dist/src/action-item.js +22 -3
- package/dist/src/action-item.js.map +1 -1
- package/dist/src/agent.d.ts +2 -2
- package/dist/src/agent.js +5 -9
- package/dist/src/agent.js.map +1 -1
- package/dist/src/call.d.ts +2 -0
- package/dist/src/call.js +7 -0
- package/dist/src/call.js.map +1 -1
- package/dist/src/campaigns.d.ts +79 -0
- package/dist/src/campaigns.js +195 -0
- package/dist/src/campaigns.js.map +1 -0
- package/dist/src/client.d.ts +10 -0
- package/dist/src/client.js +28 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands.d.ts +6 -3
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/help-desk.ts +51 -26
- package/examples/multiple-agents.ts +1 -1
- package/examples/polling-campaign.ts +12 -7
- package/examples/property-insurance.ts +42 -22
- package/examples/restaurant-waitlist.ts +42 -22
- package/examples/scheduling-outbound.ts +26 -15
- package/package.json +2 -1
- package/src/action-item.ts +28 -5
- package/src/agent.ts +17 -13
- package/src/call.ts +9 -0
- package/src/campaigns.ts +156 -0
- package/src/client.ts +35 -0
- package/src/index.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
1
2
|
import * as guava from "@guava-ai/guava-sdk";
|
|
2
3
|
import { DocumentQA } from "@guava-ai/guava-sdk/helpers";
|
|
3
4
|
import { PROPERTY_INSURANCE_POLICY } from "@guava-ai/guava-sdk/example-data";
|
|
5
|
+
import { getAgentNumber } from "@guava-ai/guava-sdk/example-utils";
|
|
4
6
|
|
|
5
7
|
const agent = new guava.Agent({
|
|
6
8
|
organization: "Harper Valley Property Insurance",
|
|
@@ -21,26 +23,44 @@ agent.onQuestion(async (call: guava.Call, question: string) => {
|
|
|
21
23
|
return await documentQA.ask(question);
|
|
22
24
|
});
|
|
23
25
|
|
|
24
|
-
export async function run(args: string[]) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
26
|
+
export async function run(prog: string, args: string[]) {
|
|
27
|
+
const program = new Command().name(prog).showHelpAfterError();
|
|
28
|
+
|
|
29
|
+
// Every Agent can be attached to one of many different channels.
|
|
30
|
+
program
|
|
31
|
+
.command("phone [number]")
|
|
32
|
+
.description("Listen for phone calls.")
|
|
33
|
+
.action(async (number?: string) => {
|
|
34
|
+
await agent.listenPhone(number ?? (await getAgentNumber()));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command("webrtc [code]")
|
|
39
|
+
.description("Listen on a WebRTC code.")
|
|
40
|
+
.action(async (code?: string) => {
|
|
41
|
+
await agent.listenWebrtc(code);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command("sip <code>")
|
|
46
|
+
.description("Listen on a SIP code 'guavasip-...'.")
|
|
47
|
+
.action(async (code: string) => {
|
|
48
|
+
await agent.listenSip(code);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command("local")
|
|
53
|
+
.description("Start a local call.")
|
|
54
|
+
.action(async () => {
|
|
55
|
+
await agent.callLocal();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command("chat")
|
|
60
|
+
.description("Start an interactive terminal chat.")
|
|
61
|
+
.action(async () => {
|
|
62
|
+
await agent.chat();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await program.parseAsync(args, { from: "user" });
|
|
46
66
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
1
2
|
import * as guava from "@guava-ai/guava-sdk";
|
|
2
3
|
import { getDefaultLogger } from "@guava-ai/guava-sdk";
|
|
4
|
+
import { getAgentNumber } from "@guava-ai/guava-sdk/example-utils";
|
|
3
5
|
|
|
4
6
|
const logger = getDefaultLogger();
|
|
5
7
|
|
|
@@ -42,26 +44,44 @@ agent.onTaskComplete("waitlist", async (call: guava.Call) => {
|
|
|
42
44
|
await call.hangup("Thank the caller and let them know we'll text when their table is ready.");
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
export async function run(args: string[]) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
export async function run(prog: string, args: string[]) {
|
|
48
|
+
const program = new Command().name(prog).showHelpAfterError();
|
|
49
|
+
|
|
50
|
+
// Every Agent can be attached to one of many different channels.
|
|
51
|
+
program
|
|
52
|
+
.command("phone [number]")
|
|
53
|
+
.description("Listen for phone calls.")
|
|
54
|
+
.action(async (number?: string) => {
|
|
55
|
+
await agent.listenPhone(number ?? (await getAgentNumber()));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command("webrtc [code]")
|
|
60
|
+
.description("Listen on a WebRTC code.")
|
|
61
|
+
.action(async (code?: string) => {
|
|
62
|
+
await agent.listenWebrtc(code);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command("sip <code>")
|
|
67
|
+
.description("Listen on a SIP code 'guavasip-...'.")
|
|
68
|
+
.action(async (code: string) => {
|
|
69
|
+
await agent.listenSip(code);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command("local")
|
|
74
|
+
.description("Start a local call.")
|
|
75
|
+
.action(async () => {
|
|
76
|
+
await agent.callLocal();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command("chat")
|
|
81
|
+
.description("Start an interactive terminal chat.")
|
|
82
|
+
.action(async () => {
|
|
83
|
+
await agent.chat();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await program.parseAsync(args, { from: "user" });
|
|
67
87
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
1
2
|
import * as guava from "@guava-ai/guava-sdk";
|
|
2
3
|
import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers";
|
|
3
4
|
import { mockAppointmentsForFuture } from "@guava-ai/guava-sdk/example-data";
|
|
@@ -42,22 +43,32 @@ agent.onTaskComplete("schedule_appointment", async (call) => {
|
|
|
42
43
|
await call.hangup("Thank them for their time and hang up the call.");
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
export async function run(args: string[]) {
|
|
46
|
-
|
|
47
|
-
const patientName = args[args.indexOf("--chat") + 1] ?? "Benjamin Buttons";
|
|
48
|
-
await agent.chat({ patientName });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
46
|
+
export async function run(prog: string, args: string[]) {
|
|
47
|
+
const program = new Command().name(prog).showHelpAfterError();
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
program
|
|
50
|
+
.command("phone <number>")
|
|
51
|
+
.description("Call a phone number.")
|
|
52
|
+
.option("-n, --name <name>", "Name of the patient.", "Benjamin Buttons")
|
|
53
|
+
.action(async (number: string, options: { name: string }) => {
|
|
54
|
+
await agent.callPhone(await getAgentNumber(), number, { patientName: options.name });
|
|
55
|
+
});
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
program
|
|
58
|
+
.command("local")
|
|
59
|
+
.description("Start a local call (for testing).")
|
|
60
|
+
.option("-n, --name <name>", "Name of the patient.", "Benjamin Buttons")
|
|
61
|
+
.action(async (options: { name: string }) => {
|
|
62
|
+
await agent.callLocal({ patientName: options.name });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command("chat")
|
|
67
|
+
.description("Start a local chat session (for testing).")
|
|
68
|
+
.option("-n, --name <name>", "Name of the patient.", "Benjamin Buttons")
|
|
69
|
+
.action(async (options: { name: string }) => {
|
|
70
|
+
await agent.chat({ patientName: options.name });
|
|
71
|
+
});
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
patientName: patientName,
|
|
62
|
-
});
|
|
73
|
+
await program.parseAsync(args, { from: "user" });
|
|
63
74
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@guava-ai/guava-sdk",
|
|
3
3
|
"description": "The official TypeScript SDK for building Guava voice agents.",
|
|
4
4
|
"homepage": "https://docs.goguava.ai/typescript-sdk/",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.28.0",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"main": "dist/src/index.js",
|
|
8
8
|
"types": "dist/src/index.d.ts",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"guava-example": "dist/bin/example-runner.js"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"commander": "^15.0.0",
|
|
49
50
|
"openai": "^6.16.0",
|
|
50
51
|
"ws": "^8.19.0",
|
|
51
52
|
"zod": "^4.3.5"
|
package/src/action-item.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
3
|
export const FieldItemType = z.union(
|
|
4
|
-
(
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
(
|
|
5
|
+
[
|
|
6
|
+
"text",
|
|
7
|
+
"date",
|
|
8
|
+
"integer",
|
|
9
|
+
"digit_sequence",
|
|
10
|
+
"multiple_choice",
|
|
11
|
+
"calendar_slot",
|
|
12
|
+
"cvv",
|
|
13
|
+
] as const
|
|
14
|
+
).map((val) => z.literal(val)),
|
|
7
15
|
);
|
|
8
16
|
export type FieldItemType = z.input<typeof FieldItemType>;
|
|
9
17
|
|
|
@@ -20,15 +28,27 @@ export const FieldItem = z
|
|
|
20
28
|
choices: z.array(z.string()).default([]),
|
|
21
29
|
choiceGenerator: z.custom<ChoiceGenerator>((val) => typeof val === "function").optional(),
|
|
22
30
|
searchable: z.boolean().default(false),
|
|
31
|
+
sensitive: z.boolean().default(false),
|
|
23
32
|
})
|
|
24
|
-
.
|
|
33
|
+
.superRefine((field, ctx) => {
|
|
34
|
+
const hasChoices = field.choices.length > 0 || field.choiceGenerator !== undefined;
|
|
35
|
+
if (
|
|
36
|
+
hasChoices &&
|
|
37
|
+
field.field_type !== "multiple_choice" &&
|
|
38
|
+
field.field_type !== "calendar_slot"
|
|
39
|
+
) {
|
|
40
|
+
ctx.addIssue({
|
|
41
|
+
code: "custom",
|
|
42
|
+
message: `Field type ${field.field_type} does not support choices attribute.`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
if (field.field_type === "multiple_choice" && field.choices.length > 10) {
|
|
26
47
|
process.emitWarning(
|
|
27
48
|
"Performance degrades with large number of choices for multiple choice field.",
|
|
28
49
|
"ACTION_ITEM",
|
|
29
50
|
);
|
|
30
51
|
}
|
|
31
|
-
return true;
|
|
32
52
|
});
|
|
33
53
|
export type FieldItem = z.input<typeof FieldItem>;
|
|
34
54
|
|
|
@@ -41,6 +61,7 @@ export const SerializableFieldItem = z.object({
|
|
|
41
61
|
required: z.boolean().default(true),
|
|
42
62
|
choices: z.array(z.string()).default([]),
|
|
43
63
|
is_search_field: z.boolean().default(false),
|
|
64
|
+
sensitive: z.boolean().default(false),
|
|
44
65
|
});
|
|
45
66
|
export type SerializableFieldItem = z.input<typeof SerializableFieldItem>;
|
|
46
67
|
|
|
@@ -70,6 +91,7 @@ export function Field(options: {
|
|
|
70
91
|
choices?: string[];
|
|
71
92
|
choiceGenerator?: ChoiceGenerator;
|
|
72
93
|
searchable?: boolean;
|
|
94
|
+
sensitive?: boolean;
|
|
73
95
|
}): FieldItem {
|
|
74
96
|
return FieldItem.parse({
|
|
75
97
|
item_type: "field",
|
|
@@ -81,6 +103,7 @@ export function Field(options: {
|
|
|
81
103
|
choices: options.choices,
|
|
82
104
|
choiceGenerator: options.choiceGenerator,
|
|
83
105
|
searchable: options.searchable,
|
|
106
|
+
sensitive: options.sensitive,
|
|
84
107
|
});
|
|
85
108
|
}
|
|
86
109
|
|
package/src/agent.ts
CHANGED
|
@@ -276,12 +276,14 @@ export class Agent {
|
|
|
276
276
|
return this._listenInbound(healthCtx, { sip_code: sipCode });
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
async callLocal(): Promise<void> {
|
|
279
|
+
async callLocal(variables: Record<string, any> = {}): Promise<void> {
|
|
280
280
|
const webrtcCode = await this._client.createWebrtcAgent(300);
|
|
281
281
|
// No health-server for call_local.
|
|
282
|
-
this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }).catch(
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }, variables).catch(
|
|
283
|
+
(err) => {
|
|
284
|
+
this._logger.error("Listen loop error: %s", err);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
285
287
|
await runWebrtcHelper(webrtcCode, getBaseUrl());
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -574,7 +576,11 @@ export class Agent {
|
|
|
574
576
|
}
|
|
575
577
|
}
|
|
576
578
|
|
|
577
|
-
async _listenInbound(
|
|
579
|
+
async _listenInbound(
|
|
580
|
+
healthCtx: HealthContext,
|
|
581
|
+
conn: InboundConnection,
|
|
582
|
+
initialVariables: Record<string, any> = {},
|
|
583
|
+
): Promise<void> {
|
|
578
584
|
const url = new URL("v2/listen-inbound", this._client.getWebsocketBase());
|
|
579
585
|
if ("agent_number" in conn) {
|
|
580
586
|
url.searchParams.set("phone_number", conn.agent_number);
|
|
@@ -632,9 +638,11 @@ export class Agent {
|
|
|
632
638
|
case "assign-call": {
|
|
633
639
|
const { call_id, call_info } = msg;
|
|
634
640
|
this._logger.info("Received call (session ID: %s)", call_id);
|
|
635
|
-
this._handleAssignedCall(call_id, call_info, socket).catch(
|
|
636
|
-
|
|
637
|
-
|
|
641
|
+
this._handleAssignedCall(call_id, call_info, socket, initialVariables).catch(
|
|
642
|
+
(err) => {
|
|
643
|
+
this._logger.error("Error handling assigned call %s: %s", call_id, err);
|
|
644
|
+
},
|
|
645
|
+
);
|
|
638
646
|
break;
|
|
639
647
|
}
|
|
640
648
|
}
|
|
@@ -892,11 +900,7 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
892
900
|
|
|
893
901
|
async _serveCampaign(healthCtx: HealthContext, campaignCode: string): Promise<void> {
|
|
894
902
|
try {
|
|
895
|
-
const
|
|
896
|
-
const campaignResponse = await fetchOrThrow(campaignUrl, {
|
|
897
|
-
headers: await this._client.headers(),
|
|
898
|
-
});
|
|
899
|
-
const campaign = (await campaignResponse.json()) as { id: string; name: string };
|
|
903
|
+
const campaign = await this._client.getCampaign(campaignCode);
|
|
900
904
|
|
|
901
905
|
const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
|
|
902
906
|
this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
|
package/src/call.ts
CHANGED
|
@@ -404,4 +404,13 @@ TASK COMPLETION REQUIREMENTS:
|
|
|
404
404
|
async getVariable(variableName: string) {
|
|
405
405
|
return this._variables[variableName] ?? null;
|
|
406
406
|
}
|
|
407
|
+
|
|
408
|
+
// Aliases for setVariable / getVariable, matching the Python SDK.
|
|
409
|
+
async setVar(variableName: string, variableValue: any) {
|
|
410
|
+
return this.setVariable(variableName, variableValue);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async getVar(variableName: string) {
|
|
414
|
+
return this.getVariable(variableName);
|
|
415
|
+
}
|
|
407
416
|
}
|
package/src/campaigns.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { fetchOrThrow } from "./utils.ts";
|
|
4
|
+
import { telemetryClient } from "./telemetry.ts";
|
|
5
|
+
import type { Client } from "./client.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modality used for campaign agentic outreach.
|
|
9
|
+
*
|
|
10
|
+
* Currently only SMS is available.
|
|
11
|
+
*/
|
|
12
|
+
export const OutreachModality = z.enum(["sms"]);
|
|
13
|
+
export type OutreachModality = z.infer<typeof OutreachModality>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A contact to upload to a campaign.
|
|
17
|
+
*/
|
|
18
|
+
export const Contact = z.object({
|
|
19
|
+
/** The contact's phone number in E.164 format (e.g. `"+14155550123"`). */
|
|
20
|
+
phoneNumber: z.e164(),
|
|
21
|
+
/**
|
|
22
|
+
* Arbitrary per-contact data. This is delivered to the agent when the call is
|
|
23
|
+
* initiated and can be read during the call (e.g. via `call.getVariable`).
|
|
24
|
+
*/
|
|
25
|
+
data: z.record(z.string(), z.unknown()).default({}),
|
|
26
|
+
/**
|
|
27
|
+
* Modalities used to reach this contact. When omitted, falls back to the
|
|
28
|
+
* `outreachModalities` passed to {@link Campaign.uploadContacts}.
|
|
29
|
+
*/
|
|
30
|
+
outreachModalities: z.array(OutreachModality).optional(),
|
|
31
|
+
});
|
|
32
|
+
export type Contact = z.input<typeof Contact>;
|
|
33
|
+
|
|
34
|
+
/** Result of uploading contacts to a campaign. */
|
|
35
|
+
export interface UploadContactsResult {
|
|
36
|
+
/** The number of contacts that were inserted. */
|
|
37
|
+
created: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Status of a campaign. */
|
|
41
|
+
export interface CampaignStatus {
|
|
42
|
+
/**
|
|
43
|
+
* Mapping of contact status (e.g. `"trying"`, `"failed"`, `"completed"`,
|
|
44
|
+
* `"partially_completed"`, `"do_not_call"`) to the number of contacts in that
|
|
45
|
+
* status. Only statuses with at least one contact are included.
|
|
46
|
+
*/
|
|
47
|
+
statusCounts: Record<string, number>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Options for {@link Campaign.uploadContacts}. */
|
|
51
|
+
export interface UploadContactsOptions {
|
|
52
|
+
/** Whether to insert contacts whose phone number is already in the campaign. */
|
|
53
|
+
allowDuplicates?: boolean;
|
|
54
|
+
/** Confirms you have consent to contact these numbers. Required by the server. */
|
|
55
|
+
acceptedTermsOfService?: boolean;
|
|
56
|
+
/** Default modalities applied to any contact that doesn't specify its own. */
|
|
57
|
+
outreachModalities?: OutreachModality[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wire-format schemas for parsing responses (snake_case, as returned by the API).
|
|
61
|
+
const UploadContactsResultWire = z.object({ created: z.number().int() });
|
|
62
|
+
const CampaignStatusWire = z.object({
|
|
63
|
+
status_counts: z.record(z.string(), z.number()).default({}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A handle to an existing Guava campaign.
|
|
68
|
+
*
|
|
69
|
+
* Obtain instances via {@link Client.getCampaign} or {@link Client.listCampaigns};
|
|
70
|
+
* do not construct directly. Campaigns are created via the Guava dashboard or CLI.
|
|
71
|
+
*/
|
|
72
|
+
@telemetryClient.trackClass()
|
|
73
|
+
export class Campaign {
|
|
74
|
+
private _client: Client;
|
|
75
|
+
private _id: string;
|
|
76
|
+
private _code: string;
|
|
77
|
+
// Snapshot of the name at the time this handle was fetched. Used for
|
|
78
|
+
// display/logging only; identity is keyed off id/code, never name.
|
|
79
|
+
private _name: string;
|
|
80
|
+
|
|
81
|
+
constructor(client: Client, id: string, code: string, name: string) {
|
|
82
|
+
this._client = client;
|
|
83
|
+
this._id = id;
|
|
84
|
+
this._code = code;
|
|
85
|
+
this._name = name;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get id(): string {
|
|
89
|
+
return this._id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get name(): string {
|
|
93
|
+
return this._name;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Upload contacts to this campaign.
|
|
98
|
+
*
|
|
99
|
+
* @param contacts - The contacts to upload.
|
|
100
|
+
* @param options.allowDuplicates - Insert contacts whose number already
|
|
101
|
+
* exists in the campaign. Defaults to `false`.
|
|
102
|
+
* @param options.acceptedTermsOfService - Confirms you have consent to
|
|
103
|
+
* contact these numbers. Defaults to `false`.
|
|
104
|
+
* @param options.outreachModalities - Default modalities for any contact that
|
|
105
|
+
* doesn't set its own {@link Contact.outreachModalities}.
|
|
106
|
+
* @returns The number of contacts that were inserted.
|
|
107
|
+
*/
|
|
108
|
+
async uploadContacts(
|
|
109
|
+
contacts: Contact[],
|
|
110
|
+
options: UploadContactsOptions = {},
|
|
111
|
+
): Promise<UploadContactsResult> {
|
|
112
|
+
const { allowDuplicates = false, acceptedTermsOfService = false, outreachModalities } = options;
|
|
113
|
+
|
|
114
|
+
const wireContacts = contacts.map((contact) => {
|
|
115
|
+
const parsed = Contact.parse(contact);
|
|
116
|
+
return {
|
|
117
|
+
phone_number: parsed.phoneNumber,
|
|
118
|
+
data: parsed.data,
|
|
119
|
+
// Per-contact modalities take precedence; fall back to the method-level
|
|
120
|
+
// default, otherwise leave unset (server applies the campaign default).
|
|
121
|
+
outreach_modalities: parsed.outreachModalities ?? outreachModalities ?? null,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const url = new URL(`v2/campaigns/${this._code}/contacts`, this._client.getHttpBase());
|
|
126
|
+
url.searchParams.set("allow_duplicates", String(allowDuplicates));
|
|
127
|
+
url.searchParams.set("accepted_terms_of_service", String(acceptedTermsOfService));
|
|
128
|
+
|
|
129
|
+
const response = await fetchOrThrow(url, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { ...(await this._client.headers()), "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ contacts: wireContacts }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const { created } = UploadContactsResultWire.parse(await response.json());
|
|
136
|
+
return { created };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Fetch the current status of this campaign. */
|
|
140
|
+
async getStatus(): Promise<CampaignStatus> {
|
|
141
|
+
const url = new URL(`v1/campaigns/${this._id}/status`, this._client.getHttpBase());
|
|
142
|
+
const response = await fetchOrThrow(url, { headers: await this._client.headers() });
|
|
143
|
+
const { status_counts } = CampaignStatusWire.parse(await response.json());
|
|
144
|
+
return { statusCounts: status_counts };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Permanently delete this campaign. */
|
|
148
|
+
async delete(): Promise<void> {
|
|
149
|
+
const url = new URL(`v1/campaigns/${this._id}`, this._client.getHttpBase());
|
|
150
|
+
await fetchOrThrow(url, { method: "DELETE", headers: await this._client.headers() });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
[inspect.custom](): string {
|
|
154
|
+
return `Campaign { name: '${this._name}', code: '${this._code}' }`;
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import { getBaseUrl, fetchOrThrow, sleep } from "./utils.ts";
|
|
6
6
|
import { SmsMessage } from "./sms.ts";
|
|
7
|
+
import { Campaign } from "./campaigns.ts";
|
|
7
8
|
import { telemetryClient } from "./telemetry.ts";
|
|
8
9
|
import {
|
|
9
10
|
type AuthStrategy,
|
|
@@ -156,6 +157,40 @@ export class Client {
|
|
|
156
157
|
return body.webrtc_code;
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
async createSipAgent(): Promise<string> {
|
|
161
|
+
const url = new URL("v1/sip-agents", this.getHttpBase());
|
|
162
|
+
const response = await fetchOrThrow(url, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: await this.headers(),
|
|
165
|
+
});
|
|
166
|
+
const body = (await response.json()) as { sip_code: string };
|
|
167
|
+
return body.sip_code;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fetch an existing campaign by its code.
|
|
172
|
+
*
|
|
173
|
+
* @param campaignCode - The campaign code (e.g. `gcmp-...`).
|
|
174
|
+
*/
|
|
175
|
+
async getCampaign(campaignCode: string): Promise<Campaign> {
|
|
176
|
+
const url = new URL(`v1/campaigns/${campaignCode}`, this.getHttpBase());
|
|
177
|
+
const response = await fetchOrThrow(url, { headers: await this.headers() });
|
|
178
|
+
const body = (await response.json()) as { id: string; campaign_code: string; name: string };
|
|
179
|
+
return new Campaign(this, body.id, body.campaign_code, body.name);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** List all campaigns in your organization. */
|
|
183
|
+
async listCampaigns(): Promise<Campaign[]> {
|
|
184
|
+
const url = new URL("v1/campaigns", this.getHttpBase());
|
|
185
|
+
const response = await fetchOrThrow(url, { headers: await this.headers() });
|
|
186
|
+
const items = (await response.json()) as {
|
|
187
|
+
id: string;
|
|
188
|
+
campaign_code: string;
|
|
189
|
+
name: string;
|
|
190
|
+
}[];
|
|
191
|
+
return items.map((c) => new Campaign(this, c.id, c.campaign_code, c.name));
|
|
192
|
+
}
|
|
193
|
+
|
|
159
194
|
async listNumbers(): Promise<{ phoneNumber: string }[]> {
|
|
160
195
|
const url = new URL("v1/phone-numbers", this.getHttpBase());
|
|
161
196
|
const response = await fetchOrThrow(url, { headers: await this.headers() });
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export { Client, type InboundConnection } from "./client.ts";
|
|
2
2
|
export { Runner } from "./runner.ts";
|
|
3
3
|
export type { SmsMessage } from "./sms.ts";
|
|
4
|
+
export { Campaign } from "./campaigns.ts";
|
|
5
|
+
export type {
|
|
6
|
+
Contact,
|
|
7
|
+
CampaignStatus,
|
|
8
|
+
UploadContactsResult,
|
|
9
|
+
UploadContactsOptions,
|
|
10
|
+
OutreachModality,
|
|
11
|
+
} from "./campaigns.ts";
|
|
4
12
|
export { Say, Field } from "./action-item.ts";
|
|
5
13
|
export { Logger, getConsoleLogger, getDefaultLogger } from "./logging.ts";
|
|
6
14
|
export { Agent, CallInfo } from "./agent.ts";
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.
|
|
1
|
+
export const SDK_VERSION = "0.28.0";
|