@guava-ai/guava-sdk 0.1.0 → 0.3.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 +19 -0
- package/dist/examples/credit-card-activation.d.ts +1 -0
- package/dist/examples/credit-card-activation.js +162 -0
- package/dist/examples/credit-card-activation.js.map +1 -0
- package/dist/examples/property-insurance.js +6 -2
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/thai-palace.d.ts +1 -0
- package/dist/examples/thai-palace.js +79 -0
- package/dist/examples/thai-palace.js.map +1 -0
- package/dist/package.json +1 -1
- package/dist/src/action_item.d.ts +2 -2
- package/dist/src/action_item.js +1 -1
- package/dist/src/action_item.js.map +1 -1
- package/dist/src/commands.d.ts +9 -9
- package/dist/src/commands.js +5 -4
- package/dist/src/commands.js.map +1 -1
- package/dist/src/example_data.js.map +1 -1
- package/dist/src/helpers/openai.d.ts +6 -1
- package/dist/src/helpers/openai.js +24 -4
- package/dist/src/helpers/openai.js.map +1 -1
- package/dist/src/index.d.ts +105 -9
- package/dist/src/index.js +197 -53
- package/dist/src/index.js.map +1 -1
- package/examples/credit-card-activation.ts +188 -0
- package/examples/property-insurance.ts +11 -7
- package/examples/thai-palace.ts +74 -0
- package/package.json +2 -2
- package/src/action_item.ts +5 -2
- package/src/commands.ts +11 -5
- package/src/example_data.ts +1 -1
- package/src/helpers/openai.ts +35 -7
- package/src/index.ts +246 -78
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { IntentRecognizer } from "../src/helpers/openai.ts";
|
|
2
|
+
import * as guava from "../src/index.ts";
|
|
3
|
+
import type { Logger } from "../src/logging.ts";
|
|
4
|
+
|
|
5
|
+
interface Customer {
|
|
6
|
+
name: string;
|
|
7
|
+
ssn: string;
|
|
8
|
+
unactivated_cards: Record<string, number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CUSTOMER_DB: Customer[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "John Smith",
|
|
14
|
+
ssn: "123456789",
|
|
15
|
+
unactivated_cards: {
|
|
16
|
+
"6011002980139424": 567,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function findCustomerBySSN(ssn: string): Customer | undefined {
|
|
22
|
+
return CUSTOMER_DB.find((c) => c.ssn == ssn);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ORGANIZATION_NAME = "Harper Valley Bank";
|
|
26
|
+
|
|
27
|
+
class CreditCardActivationController extends guava.CallController {
|
|
28
|
+
private choices = ["activate credit card", "anything else"] as const;
|
|
29
|
+
private intentRecognizer: IntentRecognizer<typeof this.choices>;
|
|
30
|
+
constructor(logger: Logger) {
|
|
31
|
+
super(logger);
|
|
32
|
+
this.intentRecognizer = new IntentRecognizer(this.choices, logger);
|
|
33
|
+
this.setPersona({
|
|
34
|
+
organizationName: ORGANIZATION_NAME,
|
|
35
|
+
agentPurpose: `You are a customer service voice agent that activates credit cards for customers of ${ORGANIZATION_NAME}.`,
|
|
36
|
+
});
|
|
37
|
+
this.readScript(
|
|
38
|
+
`Hello, thank you for calling the credit card activation line for ${ORGANIZATION_NAME}. My name is Grace. Are you here to activate your credit card?`,
|
|
39
|
+
);
|
|
40
|
+
this.acceptCall();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override async onIntent(intent: string) {
|
|
44
|
+
const choice = await this.intentRecognizer.classify(intent);
|
|
45
|
+
this.logger.info(`Chosen intent: ${choice}`);
|
|
46
|
+
if (choice == "activate credit card") {
|
|
47
|
+
await this.activateCreditCard();
|
|
48
|
+
return null;
|
|
49
|
+
} else {
|
|
50
|
+
return "Unfortunately I'm not able to help with that.";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async findCustomer() {
|
|
55
|
+
let customer: Customer | undefined;
|
|
56
|
+
let cardNumber: string;
|
|
57
|
+
while (true) {
|
|
58
|
+
await this.awaitTask({
|
|
59
|
+
checklist: [
|
|
60
|
+
{
|
|
61
|
+
item_type: "field",
|
|
62
|
+
description: "Could you give me your social security number?",
|
|
63
|
+
key: "social_security_number",
|
|
64
|
+
field_type: "integer",
|
|
65
|
+
required: true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const ssn_data = this.getField("social_security_number");
|
|
71
|
+
let ssn: string;
|
|
72
|
+
if (typeof ssn_data == "string") {
|
|
73
|
+
ssn = ssn_data;
|
|
74
|
+
} else {
|
|
75
|
+
// Should we assume all payloads are strings? or leave room by returning unknown
|
|
76
|
+
ssn = JSON.stringify(ssn_data);
|
|
77
|
+
}
|
|
78
|
+
customer = findCustomerBySSN(ssn);
|
|
79
|
+
if (!customer) {
|
|
80
|
+
this.sendInstruction(
|
|
81
|
+
"We were unable to identify the customer using the SSN they provided. Let the caller know this, and ask if they have the correct social security number.",
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
await this.awaitTask({
|
|
85
|
+
objective:
|
|
86
|
+
"We were able to identify the customer using the Social Security Number they have provided. We're going to confirm the client's name.",
|
|
87
|
+
checklist: [
|
|
88
|
+
{
|
|
89
|
+
item_type: "field",
|
|
90
|
+
description: `We're going to confirm the client's name. Am I speaking with ${customer.name}?`,
|
|
91
|
+
key: "is_client",
|
|
92
|
+
field_type: "multiple_choice",
|
|
93
|
+
choices: ["yes", "no"],
|
|
94
|
+
required: true,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (this.getField("is_client") == "no") {
|
|
100
|
+
this.sendInstruction(
|
|
101
|
+
"We were unable to identify the client's name in our files. Let the caller know this, and re-ask their social security number.",
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.sendInstruction(
|
|
110
|
+
"We were able to find the client's name in our files. Proceed to ask for their card number.",
|
|
111
|
+
);
|
|
112
|
+
while (true) {
|
|
113
|
+
await this.awaitTask({
|
|
114
|
+
checklist: [
|
|
115
|
+
{
|
|
116
|
+
item_type: "field",
|
|
117
|
+
field_type: "integer",
|
|
118
|
+
description:
|
|
119
|
+
"Could you read me the digits on the front of your credit card?",
|
|
120
|
+
key: "credit_card_number",
|
|
121
|
+
required: true,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
cardNumber = this.getField("credit_card_number") as string;
|
|
127
|
+
if (!(cardNumber in customer.unactivated_cards)) {
|
|
128
|
+
this.sendInstruction(
|
|
129
|
+
"We were unable to find the matching card number in our system. Let the caller know this, and re-ask for the credit card number.",
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
this.sendInstruction(
|
|
133
|
+
"We were able to find the matching card number in our system. Let the caller know this, and ask for security code on their card.",
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const correctCvv = customer.unactivated_cards[cardNumber];
|
|
140
|
+
while (true) {
|
|
141
|
+
await this.awaitTask({
|
|
142
|
+
checklist: [
|
|
143
|
+
{
|
|
144
|
+
item_type: "field",
|
|
145
|
+
field_type: "integer",
|
|
146
|
+
key: "security_code",
|
|
147
|
+
description:
|
|
148
|
+
"To wrap up, could I get the security code on your card?",
|
|
149
|
+
required: true,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const security_code = this.getField("security_code") as string;
|
|
155
|
+
if (security_code != correctCvv.toString()) {
|
|
156
|
+
this.sendInstruction(
|
|
157
|
+
"We were unable to match the security code to the credit card. Let the caller know this and re-ask for the security code.",
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.hangup(
|
|
164
|
+
"Explain to the caller that their credit card has now been activated. Thank them for using the bank's services, and hang up.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async activateCreditCard() {
|
|
169
|
+
this.sendInstruction(
|
|
170
|
+
"We are starting the credit card activation process, which starts with asking the caller for their social security number.",
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await this.findCustomer();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function run(_args: string[]) {
|
|
178
|
+
const conn = new guava.Client().listenInbound(
|
|
179
|
+
{
|
|
180
|
+
agent_number: process.env.GUAVA_AGENT_NUMBER!,
|
|
181
|
+
},
|
|
182
|
+
(logger) => new CreditCardActivationController(logger),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (import.meta.main) {
|
|
187
|
+
run([]);
|
|
188
|
+
}
|
|
@@ -3,8 +3,8 @@ import * as guava from "../src/index.ts";
|
|
|
3
3
|
import { DocumentQA } from "../src/helpers/openai.ts";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {PROPERTY_INSURANCE_POLICY} from "../src/example_data.ts";
|
|
6
|
+
import type { Logger } from "../src/logging.ts";
|
|
7
|
+
import { PROPERTY_INSURANCE_POLICY } from "../src/example_data.ts";
|
|
8
8
|
|
|
9
9
|
class InsuranceCallController extends guava.CallController {
|
|
10
10
|
private documentQA: DocumentQA;
|
|
@@ -17,11 +17,15 @@ class InsuranceCallController extends guava.CallController {
|
|
|
17
17
|
logger,
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
override async onCallStart(): Promise<void> {
|
|
21
|
-
await this.setPersona(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
await this.setPersona({
|
|
23
|
+
organizationName: "Harper Valley Property Insurance",
|
|
24
|
+
});
|
|
25
|
+
this.setTask({
|
|
26
|
+
objective:
|
|
27
|
+
"You are making an outbound call to a potential customer. Your task is to answer questions regarding property insurance policy until there are no more questions.",
|
|
28
|
+
});
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
override async onQuestion(question: string): Promise<string> {
|
|
@@ -45,5 +49,5 @@ export function run(args: string[]) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
if (import.meta.main) {
|
|
48
|
-
run(process.argv.slice(2))
|
|
52
|
+
run(process.argv.slice(2));
|
|
49
53
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as guava from "../src/index.ts";
|
|
2
|
+
import { getConsoleLogger, type Logger } from "../src/logging.ts";
|
|
3
|
+
import { IntentRecognizer } from "../src/helpers/openai.ts";
|
|
4
|
+
|
|
5
|
+
class ThaiPalaceCallController extends guava.CallController {
|
|
6
|
+
private choices = ["restaurant waitlist", "anything else"] as const;
|
|
7
|
+
private intentRecognizer: IntentRecognizer<typeof this.choices>;
|
|
8
|
+
constructor(logger: Logger) {
|
|
9
|
+
super(logger);
|
|
10
|
+
this.intentRecognizer = new IntentRecognizer(this.choices, logger);
|
|
11
|
+
this.setPersona({
|
|
12
|
+
organizationName: "Thai Palace",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override async onIncomingCall(from_number?: string): Promise<void> {
|
|
17
|
+
await super.onIncomingCall(from_number);
|
|
18
|
+
this.acceptCall();
|
|
19
|
+
this.setThaiPalaceTask();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setThaiPalaceTask() {
|
|
23
|
+
this.setTask(
|
|
24
|
+
{
|
|
25
|
+
objective: `You are a virtual assistant for a restaurant called Thai Palace.
|
|
26
|
+
Your job is to add callers to the waitlist.`,
|
|
27
|
+
checklist: [
|
|
28
|
+
{
|
|
29
|
+
item_type: "field",
|
|
30
|
+
key: "caller_name",
|
|
31
|
+
field_type: "text",
|
|
32
|
+
description: "The name to be added to the waitlist",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
item_type: "field",
|
|
36
|
+
key: "party_size",
|
|
37
|
+
field_type: "integer",
|
|
38
|
+
description: "The number of people attending",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
item_type: "field",
|
|
42
|
+
key: "phone_number",
|
|
43
|
+
field_type: "text",
|
|
44
|
+
description: "phone number to text when table is ready",
|
|
45
|
+
},
|
|
46
|
+
"Read the phone number back to the caller to make sure you got it right",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
() => this.hangup(),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override async onIntent(intent: string) {
|
|
54
|
+
const choice = await this.intentRecognizer.classify(intent);
|
|
55
|
+
this.logger.info(`Chosen intent: ${choice}`);
|
|
56
|
+
if (choice == "restaurant waitlist") {
|
|
57
|
+
this.setThaiPalaceTask();
|
|
58
|
+
return null;
|
|
59
|
+
} else {
|
|
60
|
+
return "Tell them we only handle waitlist additions at this number.";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function run(_args: string[]) {
|
|
66
|
+
new guava.Client().listenInbound(
|
|
67
|
+
{ agent_number: process.env.GUAVA_AGENT_NUMBER! },
|
|
68
|
+
(logger) => new ThaiPalaceCallController(getConsoleLogger("debug")),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (import.meta.main) {
|
|
73
|
+
run([]);
|
|
74
|
+
}
|
package/package.json
CHANGED
package/src/action_item.ts
CHANGED
|
@@ -14,11 +14,14 @@ export const fieldItem = z
|
|
|
14
14
|
description: z.string(),
|
|
15
15
|
field_type: fieldItemType,
|
|
16
16
|
required: z.boolean().default(true),
|
|
17
|
-
choices: z.array(z.string()),
|
|
17
|
+
choices: z.array(z.string()).default([]),
|
|
18
18
|
})
|
|
19
19
|
.refine((field) => {
|
|
20
20
|
if (field.field_type == "multiple_choice" && field.choices.length > 10) {
|
|
21
|
-
process.emitWarning(
|
|
21
|
+
process.emitWarning(
|
|
22
|
+
"Performance degrades with large number of choices for multiple choice field.",
|
|
23
|
+
"ACTION_ITEM",
|
|
24
|
+
);
|
|
22
25
|
}
|
|
23
26
|
return true;
|
|
24
27
|
});
|
package/src/commands.ts
CHANGED
|
@@ -13,12 +13,18 @@ export const listenInboundCommand = z
|
|
|
13
13
|
.strictObject({
|
|
14
14
|
command_type: z.literal("listen-inbound"),
|
|
15
15
|
|
|
16
|
-
agent_number: z.e164().
|
|
17
|
-
webrtc_code: z.string().
|
|
16
|
+
agent_number: z.e164().nullish().default(null),
|
|
17
|
+
webrtc_code: z.string().nullish().default(null),
|
|
18
18
|
})
|
|
19
|
-
.refine(
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
.refine(
|
|
20
|
+
(obj) => {
|
|
21
|
+
return (
|
|
22
|
+
typeof obj.agent_number == "string" ||
|
|
23
|
+
typeof obj.webrtc_code == "string"
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
{ error: "one of ['agent_number', 'webrtc_code'] must be set" },
|
|
27
|
+
);
|
|
22
28
|
export type ListenInboundCommand = z.input<typeof listenInboundCommand>;
|
|
23
29
|
|
|
24
30
|
export const rejectInboundCallCommand = z.strictObject({
|
package/src/example_data.ts
CHANGED
|
@@ -606,4 +606,4 @@ Coverage limitations, definitions, and premium rating methodologies may vary by
|
|
|
606
606
|
The actual insurance policy, including Declarations, core forms, endorsements, and any applicable riders, constitutes the entire contract between HVPI and the policyholder.
|
|
607
607
|
|
|
608
608
|
End of Harper Valley Property Insurance
|
|
609
|
-
Comprehensive Residential Program Manual
|
|
609
|
+
Comprehensive Residential Program Manual.`;
|
package/src/helpers/openai.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import OpenAI, { toFile } from "openai";
|
|
2
2
|
import { type Logger } from "../logging.ts";
|
|
3
|
+
import * as z from "zod";
|
|
3
4
|
|
|
4
5
|
// from beta.py
|
|
5
6
|
// TODO: Remove after beta
|
|
6
7
|
function beta_create_openai_client(logger: Logger) {
|
|
7
8
|
const baseUrl =
|
|
8
|
-
process.env.GUAVA_BASE_URL ?? "
|
|
9
|
+
process.env.GUAVA_BASE_URL ?? "https://guava-dev.gridspace.com/";
|
|
9
10
|
// to get it working with OpenAI TS/JS client
|
|
10
11
|
const basedUrl = new URL("openai/v1/", baseUrl);
|
|
11
|
-
basedUrl.protocol = /^wss:\/\//.test(basedUrl.toString())
|
|
12
|
-
? "https:"
|
|
13
|
-
: "http:";
|
|
14
12
|
logger.info(`Creating beta OpenAI client`);
|
|
15
13
|
return new OpenAI({
|
|
16
14
|
baseURL: basedUrl.toString(),
|
|
@@ -18,13 +16,43 @@ function beta_create_openai_client(logger: Logger) {
|
|
|
18
16
|
});
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
export class IntentRecognizer {
|
|
19
|
+
export class IntentRecognizer<Choices extends readonly string[]> {
|
|
20
|
+
private client: OpenAI;
|
|
21
|
+
private intentChoices: Choices;
|
|
22
|
+
private choiceModel: z.ZodType<Choices[number]>;
|
|
23
|
+
constructor(choices: Choices, logger: Logger, client?: OpenAI) {
|
|
24
|
+
this.intentChoices = choices;
|
|
25
|
+
this.client = client ?? beta_create_openai_client(logger);
|
|
26
|
+
this.choiceModel = z.union(choices.map((s) => z.literal(s)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async classify(intent: string): Promise<Choices[number]> {
|
|
30
|
+
const response = await this.client.responses.parse({
|
|
31
|
+
model: "gpt-5-mini",
|
|
32
|
+
input: `
|
|
33
|
+
Pick the choice in the list of choices that best reflects the given intent.
|
|
34
|
+
Intent: "${intent}".
|
|
35
|
+
Possible Choices: ${this.intentChoices}.
|
|
36
|
+
`.trim(),
|
|
37
|
+
reasoning: {
|
|
38
|
+
effort: "low",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const parsed_output = this.choiceModel.parse(response.output_text);
|
|
42
|
+
return parsed_output;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
|
|
23
46
|
export class DocumentQA {
|
|
24
47
|
private client: OpenAI;
|
|
25
48
|
private vector_store: Promise<OpenAI.VectorStore>;
|
|
26
49
|
private logger: Logger;
|
|
27
|
-
constructor(
|
|
50
|
+
constructor(
|
|
51
|
+
vector_store_name: string,
|
|
52
|
+
document: string,
|
|
53
|
+
logger: Logger,
|
|
54
|
+
client?: OpenAI,
|
|
55
|
+
) {
|
|
28
56
|
this.client = client ?? beta_create_openai_client(logger);
|
|
29
57
|
this.vector_store = this.getOrCreateVectorStore(
|
|
30
58
|
vector_store_name,
|
|
@@ -55,7 +83,7 @@ export class DocumentQA {
|
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
|
|
86
|
+
this.logger.info("Creating vector store...");
|
|
59
87
|
|
|
60
88
|
const vector_store = await this.client.vectorStores.create({
|
|
61
89
|
name: vector_store_name,
|