@letterbook/ai-chat 0.1.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 +38 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +169 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Letterbook AI Chat TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Capture AI chat turns from your application so conversations can appear in the Letterbook platform when customers need help.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import * as letterbook from "@letterbook/ai-chat";
|
|
7
|
+
|
|
8
|
+
const result = await letterbook.capture({
|
|
9
|
+
userId: "user123",
|
|
10
|
+
event: "user_message",
|
|
11
|
+
model: "gpt_4",
|
|
12
|
+
input: "The checkout page is broken and I was charged twice.",
|
|
13
|
+
output: "I am sorry, I cannot fix billing issues from here.",
|
|
14
|
+
convoId: "conv789",
|
|
15
|
+
customerEmail: "customer@example.com",
|
|
16
|
+
debounceSeconds: 300,
|
|
17
|
+
properties: {
|
|
18
|
+
system_prompt: "you are a helpful assistant",
|
|
19
|
+
experiment: "experiment_a",
|
|
20
|
+
},
|
|
21
|
+
attachments: [
|
|
22
|
+
{
|
|
23
|
+
type: "text",
|
|
24
|
+
name: "Additional Info",
|
|
25
|
+
value: "A very long document",
|
|
26
|
+
role: "input",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(result.queued, result.evaluationAfterSeconds);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Set `LETTERBOOK_API_KEY`, or instantiate `new letterbook.Letterbook({ apiKey })` for explicit configuration.
|
|
35
|
+
Set `debounceSeconds` to control how long Letterbook waits after the last message before evaluating the conversation.
|
|
36
|
+
|
|
37
|
+
If `customerEmail` is omitted, the SDK looks for `properties.customer_email`, `properties.email`, or an email-shaped `userId`.
|
|
38
|
+
Email is only required if Letterbook promotes the capture to a ticket.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type LetterbookPriority = "low" | "medium" | "high" | "urgent";
|
|
2
|
+
export type AttachmentRole = "input" | "output";
|
|
3
|
+
export interface LetterbookAttachment {
|
|
4
|
+
type: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
value: string;
|
|
7
|
+
role: AttachmentRole;
|
|
8
|
+
}
|
|
9
|
+
export interface LetterbookOptions {
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
raiseOnError?: boolean;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export interface CaptureInput {
|
|
17
|
+
userId: string;
|
|
18
|
+
event: string;
|
|
19
|
+
model: string;
|
|
20
|
+
input?: string;
|
|
21
|
+
output?: string;
|
|
22
|
+
convoId?: string;
|
|
23
|
+
properties?: Record<string, unknown>;
|
|
24
|
+
attachments?: LetterbookAttachment[];
|
|
25
|
+
customerEmail?: string;
|
|
26
|
+
customerName?: string;
|
|
27
|
+
ticketEnabled?: boolean;
|
|
28
|
+
debounceSeconds?: number;
|
|
29
|
+
subject?: string;
|
|
30
|
+
priority?: LetterbookPriority;
|
|
31
|
+
}
|
|
32
|
+
export interface CaptureResult {
|
|
33
|
+
issueDetected?: boolean | null;
|
|
34
|
+
ticketCreated: boolean;
|
|
35
|
+
queued?: boolean;
|
|
36
|
+
status?: string;
|
|
37
|
+
existingTicket?: boolean;
|
|
38
|
+
evaluationAfterSeconds?: number;
|
|
39
|
+
priority?: LetterbookPriority;
|
|
40
|
+
reason?: string;
|
|
41
|
+
ticketId?: string;
|
|
42
|
+
ticketUrl?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare class LetterbookError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
export declare class Letterbook {
|
|
49
|
+
private readonly apiKey?;
|
|
50
|
+
private readonly baseUrl;
|
|
51
|
+
private readonly timeoutMs;
|
|
52
|
+
private readonly raiseOnError;
|
|
53
|
+
private readonly fetchFn;
|
|
54
|
+
constructor(options?: LetterbookOptions);
|
|
55
|
+
capture(input: CaptureInput): Promise<CaptureResult>;
|
|
56
|
+
private postJson;
|
|
57
|
+
private fail;
|
|
58
|
+
}
|
|
59
|
+
export declare function capture(input: CaptureInput, options?: LetterbookOptions): Promise<CaptureResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "https://api.letterbook.ai/api";
|
|
2
|
+
export class LetterbookError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "LetterbookError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class Letterbook {
|
|
9
|
+
apiKey;
|
|
10
|
+
baseUrl;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
raiseOnError;
|
|
13
|
+
fetchFn;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.apiKey = options.apiKey ?? readEnv("LETTERBOOK_API_KEY");
|
|
16
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
17
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
18
|
+
this.raiseOnError = options.raiseOnError ?? false;
|
|
19
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
20
|
+
if (!this.fetchFn) {
|
|
21
|
+
throw new LetterbookError("A fetch implementation is required.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async capture(input) {
|
|
25
|
+
if (input.input == null && input.output == null) {
|
|
26
|
+
throw new LetterbookError("letterbook.capture requires at least one of input or output");
|
|
27
|
+
}
|
|
28
|
+
if (input.debounceSeconds != null && input.debounceSeconds < 1) {
|
|
29
|
+
throw new LetterbookError("letterbook.capture debounceSeconds must be at least 1");
|
|
30
|
+
}
|
|
31
|
+
if (input.ticketEnabled === false) {
|
|
32
|
+
return {
|
|
33
|
+
issueDetected: null,
|
|
34
|
+
ticketCreated: false,
|
|
35
|
+
queued: false,
|
|
36
|
+
status: "disabled",
|
|
37
|
+
reason: "Ticket capture is disabled.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!this.apiKey) {
|
|
41
|
+
return this.fail("LETTERBOOK_API_KEY is required to capture AI chat");
|
|
42
|
+
}
|
|
43
|
+
const properties = input.properties ?? {};
|
|
44
|
+
const payload = {
|
|
45
|
+
user_id: input.userId,
|
|
46
|
+
event: input.event,
|
|
47
|
+
model: input.model,
|
|
48
|
+
input: input.input,
|
|
49
|
+
output: input.output,
|
|
50
|
+
convo_id: input.convoId,
|
|
51
|
+
properties,
|
|
52
|
+
attachments: normalizeAttachments(input.attachments ?? []),
|
|
53
|
+
customer_email: resolveCustomerEmail(input.userId, input.customerEmail, properties),
|
|
54
|
+
customer_name: input.customerName ?? stringOrUndefined(properties.customer_name),
|
|
55
|
+
ticket_enabled: input.ticketEnabled ?? true,
|
|
56
|
+
debounce_seconds: input.debounceSeconds,
|
|
57
|
+
subject: input.subject,
|
|
58
|
+
priority: input.priority,
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
const data = await this.postJson("/v1/ai-chat/capture", payload);
|
|
62
|
+
return {
|
|
63
|
+
issueDetected: boolOrNull(data.issue_detected),
|
|
64
|
+
ticketCreated: Boolean(data.ticket_created),
|
|
65
|
+
queued: Boolean(data.queued),
|
|
66
|
+
status: stringOrUndefined(data.status),
|
|
67
|
+
existingTicket: Boolean(data.existing_ticket),
|
|
68
|
+
evaluationAfterSeconds: numberOrUndefined(data.evaluation_after_seconds),
|
|
69
|
+
priority: normalizePriority(data.priority),
|
|
70
|
+
reason: stringOrUndefined(data.reason),
|
|
71
|
+
ticketId: stringOrUndefined(data.ticket_id),
|
|
72
|
+
ticketUrl: stringOrUndefined(data.ticket_url),
|
|
73
|
+
error: stringOrUndefined(data.error),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return this.fail(error instanceof Error ? error.message : String(error));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async postJson(path, payload) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
83
|
+
try {
|
|
84
|
+
const response = await this.fetchFn(`${this.baseUrl}${path}`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
"User-Agent": "@letterbook/ai-chat/0.1.0",
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(payload),
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
});
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new LetterbookError(`Letterbook API returned ${response.status}: ${text}`);
|
|
97
|
+
}
|
|
98
|
+
return text ? JSON.parse(text) : {};
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
fail(message) {
|
|
105
|
+
if (this.raiseOnError) {
|
|
106
|
+
throw new LetterbookError(message);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
issueDetected: null,
|
|
110
|
+
ticketCreated: false,
|
|
111
|
+
error: message,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function capture(input, options = {}) {
|
|
116
|
+
return new Letterbook(options).capture(input);
|
|
117
|
+
}
|
|
118
|
+
function normalizeAttachments(attachments) {
|
|
119
|
+
return attachments.map((attachment) => {
|
|
120
|
+
if (!attachment.value) {
|
|
121
|
+
throw new LetterbookError("Each attachment requires a value");
|
|
122
|
+
}
|
|
123
|
+
if (attachment.role !== "input" && attachment.role !== "output") {
|
|
124
|
+
throw new LetterbookError("Attachment role must be 'input' or 'output'");
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
type: attachment.type || "text",
|
|
128
|
+
name: attachment.name,
|
|
129
|
+
value: attachment.value,
|
|
130
|
+
role: attachment.role,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function resolveCustomerEmail(userId, customerEmail, properties) {
|
|
135
|
+
return (customerEmail ??
|
|
136
|
+
stringOrUndefined(properties.customer_email) ??
|
|
137
|
+
stringOrUndefined(properties.email) ??
|
|
138
|
+
stringOrUndefined(properties.user_email) ??
|
|
139
|
+
(looksLikeEmail(userId) ? userId : undefined));
|
|
140
|
+
}
|
|
141
|
+
function looksLikeEmail(value) {
|
|
142
|
+
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value);
|
|
143
|
+
}
|
|
144
|
+
function normalizePriority(value) {
|
|
145
|
+
if (typeof value !== "string") {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const normalized = value.toLowerCase();
|
|
149
|
+
if (["low", "medium", "high", "urgent"].includes(normalized)) {
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
function stringOrUndefined(value) {
|
|
155
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
156
|
+
}
|
|
157
|
+
function boolOrNull(value) {
|
|
158
|
+
return typeof value === "boolean" ? value : null;
|
|
159
|
+
}
|
|
160
|
+
function numberOrUndefined(value) {
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
function readEnv(name) {
|
|
167
|
+
const maybeGlobal = globalThis;
|
|
168
|
+
return maybeGlobal.process?.env?.[name];
|
|
169
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@letterbook/ai-chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for capturing AI chat issues in Letterbook.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"letterbook",
|
|
24
|
+
"ai",
|
|
25
|
+
"chat",
|
|
26
|
+
"support",
|
|
27
|
+
"helpdesk"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "~5.8.3"
|
|
35
|
+
}
|
|
36
|
+
}
|