@postalsys/email-text-tools 1.3.2 → 1.4.1
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/.ncurc.js +0 -1
- package/examples/risk.js +6 -1
- package/examples/summary.js +9 -3
- package/lib/generate-summary.js +143 -67
- package/lib/risk-analysis.js +48 -28
- package/package.json +4 -4
package/.ncurc.js
CHANGED
package/examples/risk.js
CHANGED
|
@@ -21,7 +21,12 @@ async function analyzeEmail(path) {
|
|
|
21
21
|
html: parsed.html,
|
|
22
22
|
text: parsed.text
|
|
23
23
|
},
|
|
24
|
-
process.env.OPENAI_API_KEY
|
|
24
|
+
process.env.OPENAI_API_KEY,
|
|
25
|
+
{
|
|
26
|
+
//gptModel: 'gpt-3.5-turbo'
|
|
27
|
+
gptModel: 'gpt-4',
|
|
28
|
+
maxTokens: 6000
|
|
29
|
+
}
|
|
25
30
|
);
|
|
26
31
|
|
|
27
32
|
result.file = Path.basename(path);
|
package/examples/summary.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs').promises;
|
|
4
4
|
const generateSummary = require('../lib/generate-summary');
|
|
5
5
|
const simpleParser = require('mailparser').simpleParser;
|
|
6
|
+
const libmime = require('libmime');
|
|
6
7
|
|
|
7
8
|
async function main() {
|
|
8
9
|
const eml = await fs.readFile(process.argv[2]);
|
|
@@ -11,12 +12,17 @@ async function main() {
|
|
|
11
12
|
|
|
12
13
|
const summary = await generateSummary(
|
|
13
14
|
{
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
|
|
16
|
+
attachments: parsed.attachments,
|
|
16
17
|
html: parsed.html,
|
|
17
18
|
text: parsed.text
|
|
18
19
|
},
|
|
19
|
-
process.env.OPENAI_API_KEY
|
|
20
|
+
process.env.OPENAI_API_KEY,
|
|
21
|
+
{
|
|
22
|
+
//gptModel: 'gpt-3.5-turbo'
|
|
23
|
+
gptModel: 'gpt-4',
|
|
24
|
+
maxTokens: 6000
|
|
25
|
+
}
|
|
20
26
|
);
|
|
21
27
|
|
|
22
28
|
console.log(summary);
|
package/lib/generate-summary.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const packageData = require('../package.json');
|
|
4
4
|
const htmlToText = require('../lib/html-to-text');
|
|
5
|
-
const
|
|
6
|
-
const fetchCmd = global.fetch || nodeFetch;
|
|
5
|
+
const GPT3Tokenizer = require('gpt3-tokenizer').default;
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const { fetch: fetchCmd, Agent } = require('undici');
|
|
8
|
+
const fetchAgent = new Agent({ connect: { timeout: 90 * 1000 } });
|
|
9
|
+
|
|
10
|
+
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
|
|
11
|
+
|
|
12
|
+
const MAX_ALLOWED_TEXT_LENGTH = 32 * 1024;
|
|
13
|
+
const MAX_ALLOWED_TOKENS = 4000;
|
|
9
14
|
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
10
15
|
|
|
11
16
|
const SYSTEM_PROMPT = `
|
|
@@ -16,38 +21,89 @@ You do not comment or explain anything.
|
|
|
16
21
|
.trim();
|
|
17
22
|
|
|
18
23
|
const USER_PROMPT = `
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
Instructions:
|
|
25
|
+
- You are an executive assistant scanning incoming emails to inform your superiors about what is important and what is not.
|
|
26
|
+
- Your task is to monitor and analyze incoming emails, which consist of the message headers, a list of attachments, and text content
|
|
27
|
+
- Describe the sentiment of the email using one word. Use either "positive", "neutral" or "negative". Include this value in the response as a "sentiment" property.
|
|
28
|
+
- Generate a one-sentence summary of the email. Include this value in the response as a "summary" property.
|
|
29
|
+
- Does it seem like the sender of the email would expect a reply to this email? Include this information in the response as a "shouldReply" property with the value "true" if they expect it and "false" if not.
|
|
30
|
+
- If this email is a reply to a previous email, then extract the text content that only the email's sender wrote, and include this as a "replyText" property in the response.
|
|
31
|
+
- Do not include message signatures in the extracted reply text
|
|
32
|
+
- Respond with a JSON formatted structure. Do not write any other explanations.
|
|
33
|
+
|
|
34
|
+
Facts:
|
|
35
|
+
- An email consists of message headers, an attachments list, and text content
|
|
36
|
+
- The email to analyze is formatted in a JSON format
|
|
37
|
+
- An email is a reply to a previous email only if it includes a "in-reply-to" header
|
|
38
|
+
- The email structure includes a property "headers" that contains an array of header values.
|
|
39
|
+
- Each header contains two properties, "key" as the header field key name and "value" as the header value without the key prefix
|
|
40
|
+
- The email structure includes a property "attachments" that contains an array of attachments.
|
|
41
|
+
- Each attachment includes a "filename" property that describes the file name, a "contentType" property that describes the Content-Type value of the attachment
|
|
42
|
+
- The email includes a "text" property for the text content
|
|
43
|
+
|
|
44
|
+
Analyze the following email:
|
|
45
|
+
`.trim();
|
|
46
|
+
|
|
47
|
+
async function generateSummary(message, apiToken, opts) {
|
|
48
|
+
opts = opts || {};
|
|
49
|
+
|
|
50
|
+
let maxAllowedTokens = opts.maxTokens || MAX_ALLOWED_TOKENS;
|
|
51
|
+
let gptModel = opts.gptModel || 'gpt-3.5-turbo';
|
|
52
|
+
|
|
53
|
+
let text = message.text || '';
|
|
54
|
+
if (message.html && (!text || message.html.length > text.length * 2)) {
|
|
30
55
|
text = htmlToText(message.html);
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
if (text.length > MAX_TEXT_LENGTH) {
|
|
35
|
-
text = text.substring(0, MAX_TEXT_LENGTH);
|
|
36
|
-
}
|
|
58
|
+
let prompt;
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
const content = {
|
|
61
|
+
headers: [].concat(message.headers || []).filter(
|
|
62
|
+
// use a whitelist
|
|
63
|
+
header => ['from', 'to', 'cc', 'bcc', 'subject', 'mime-version', 'date', 'content-type', 'list-id'].includes(header.key)
|
|
64
|
+
),
|
|
65
|
+
attachments: [].concat(message.attachments || []).map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType }))
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let charactersRemoved = 0;
|
|
69
|
+
let promptText = text;
|
|
70
|
+
|
|
71
|
+
if (promptText.length > MAX_ALLOWED_TEXT_LENGTH) {
|
|
72
|
+
charactersRemoved += promptText.length - MAX_ALLOWED_TEXT_LENGTH;
|
|
73
|
+
promptText = promptText.substr(0, MAX_ALLOWED_TEXT_LENGTH);
|
|
45
74
|
}
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
text =
|
|
76
|
+
while (promptText.length) {
|
|
77
|
+
content.text = promptText;
|
|
78
|
+
prompt = `${USER_PROMPT}
|
|
49
79
|
|
|
50
|
-
${
|
|
80
|
+
${JSON.stringify(content)}`;
|
|
81
|
+
|
|
82
|
+
let tokens = tokenizer.encode(prompt);
|
|
83
|
+
if (tokens.text.length <= maxAllowedTokens) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (promptText.length > 2 * 1024 * 1024) {
|
|
87
|
+
promptText = promptText.substring(0, promptText.length - 1024 * 1024);
|
|
88
|
+
charactersRemoved += 1024 * 1024;
|
|
89
|
+
} else if (promptText.length > 2 * 1024) {
|
|
90
|
+
promptText = promptText.substring(0, promptText.length - 1024);
|
|
91
|
+
charactersRemoved += 1024;
|
|
92
|
+
} else if (promptText.length > 2 * 256) {
|
|
93
|
+
promptText = promptText.substring(0, promptText.length - 256);
|
|
94
|
+
charactersRemoved += 255;
|
|
95
|
+
} else if (promptText.length > 2 * 100) {
|
|
96
|
+
promptText = promptText.substring(0, promptText.length - 100);
|
|
97
|
+
charactersRemoved += 100;
|
|
98
|
+
} else if (promptText.length > 2 * 10) {
|
|
99
|
+
promptText = promptText.substring(0, promptText.length - 10);
|
|
100
|
+
charactersRemoved += 10;
|
|
101
|
+
} else if (promptText.length > 1) {
|
|
102
|
+
promptText = promptText.substring(0, promptText.length - 1);
|
|
103
|
+
charactersRemoved += 1;
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(`Prompt too long. Removed ${charactersRemoved} characters.`);
|
|
106
|
+
}
|
|
51
107
|
}
|
|
52
108
|
|
|
53
109
|
let headers = {
|
|
@@ -57,7 +113,7 @@ ${text}`;
|
|
|
57
113
|
};
|
|
58
114
|
|
|
59
115
|
let payload = {
|
|
60
|
-
model:
|
|
116
|
+
model: gptModel,
|
|
61
117
|
messages: [
|
|
62
118
|
{
|
|
63
119
|
role: 'system',
|
|
@@ -65,42 +121,58 @@ ${text}`;
|
|
|
65
121
|
},
|
|
66
122
|
{
|
|
67
123
|
role: 'user',
|
|
68
|
-
content:
|
|
69
|
-
|
|
70
|
-
${text}`
|
|
124
|
+
content: prompt
|
|
71
125
|
}
|
|
72
126
|
]
|
|
73
127
|
};
|
|
74
128
|
|
|
75
|
-
let res
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
let res;
|
|
130
|
+
let data;
|
|
131
|
+
let retries = 0;
|
|
132
|
+
|
|
133
|
+
let run = async () => {
|
|
134
|
+
res = await fetchCmd(OPENAI_API_URL, {
|
|
135
|
+
method: 'post',
|
|
136
|
+
headers,
|
|
137
|
+
body: JSON.stringify(payload),
|
|
138
|
+
dispatcher: fetchAgent
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
data = await res.json();
|
|
142
|
+
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
if (res.status === 429 && ++retries < 5) {
|
|
145
|
+
// try again
|
|
146
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
147
|
+
return await run();
|
|
148
|
+
}
|
|
80
149
|
|
|
81
|
-
|
|
150
|
+
if (data && data.error) {
|
|
151
|
+
let error = new Error(data.error.message || data.error);
|
|
152
|
+
if (data.error.code) {
|
|
153
|
+
error.code = data.error.code;
|
|
154
|
+
}
|
|
82
155
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let error = new Error(data.error.message || data.error);
|
|
86
|
-
if (data.error.code) {
|
|
87
|
-
error.code = data.error.code;
|
|
156
|
+
error.statusCode = res.status;
|
|
157
|
+
throw error;
|
|
88
158
|
}
|
|
89
159
|
|
|
160
|
+
let error = new Error('Failed to run API request');
|
|
90
161
|
error.statusCode = res.status;
|
|
91
162
|
throw error;
|
|
92
163
|
}
|
|
93
164
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
165
|
+
if (!data) {
|
|
166
|
+
throw new Error(`Failed to POST API request`);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
98
169
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
const reqStartTime = Date.now();
|
|
171
|
+
await run();
|
|
172
|
+
const reqEndTime = Date.now();
|
|
102
173
|
|
|
103
|
-
let
|
|
174
|
+
let values;
|
|
175
|
+
const output =
|
|
104
176
|
data &&
|
|
105
177
|
data.choices &&
|
|
106
178
|
data.choices
|
|
@@ -112,25 +184,29 @@ ${text}`
|
|
|
112
184
|
.replace(/^\s*"|"\s*$/g, '')
|
|
113
185
|
.trim();
|
|
114
186
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
187
|
+
try {
|
|
188
|
+
values = JSON.parse(output);
|
|
189
|
+
if (typeof values.shouldReply === 'string') {
|
|
190
|
+
values.shouldReply = values.shouldReply === 'true';
|
|
191
|
+
}
|
|
192
|
+
if (typeof values.shouldReply !== 'undefined' && typeof values.shouldReply !== 'boolean') {
|
|
193
|
+
delete values.shouldReply;
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
let error = new Error('Failed to parse output from OpenAI API');
|
|
197
|
+
error.textContent = output;
|
|
198
|
+
throw error;
|
|
120
199
|
}
|
|
121
200
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
201
|
+
const response = Object.assign({ id: null, tokens: null, model: null }, values, {
|
|
202
|
+
id: data && data.id,
|
|
203
|
+
tokens: data && data.usage && data.usage.total_tokens,
|
|
204
|
+
model: gptModel,
|
|
126
205
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
structured
|
|
133
|
-
);
|
|
206
|
+
_text: content.text,
|
|
207
|
+
_time: reqEndTime - reqStartTime,
|
|
208
|
+
_cr: charactersRemoved
|
|
209
|
+
});
|
|
134
210
|
|
|
135
211
|
return response;
|
|
136
212
|
}
|
package/lib/risk-analysis.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const packageData = require('../package.json');
|
|
4
4
|
const htmlToText = require('../lib/html-to-text');
|
|
5
|
-
const nodeFetch = require('node-fetch');
|
|
6
5
|
const GPT3Tokenizer = require('gpt3-tokenizer').default;
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
const { fetch: fetchCmd, Agent } = require('undici');
|
|
8
|
+
const fetchAgent = new Agent({ connect: { timeout: 90 * 1000 } });
|
|
8
9
|
|
|
9
10
|
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
|
|
10
11
|
|
|
@@ -60,7 +61,12 @@ Facts:
|
|
|
60
61
|
Analyze the following email:
|
|
61
62
|
`.trim();
|
|
62
63
|
|
|
63
|
-
async function riskAnalysis(message, apiToken) {
|
|
64
|
+
async function riskAnalysis(message, apiToken, opts) {
|
|
65
|
+
opts = opts || {};
|
|
66
|
+
|
|
67
|
+
let maxAllowedTokens = opts.maxTokens || MAX_ALLOWED_TOKENS;
|
|
68
|
+
let gptModel = opts.gptModel || 'gpt-3.5-turbo';
|
|
69
|
+
|
|
64
70
|
let text = message.text || '';
|
|
65
71
|
if (message.html && (!text || message.html.length > text.length * 2)) {
|
|
66
72
|
text = htmlToText(message.html);
|
|
@@ -105,7 +111,7 @@ async function riskAnalysis(message, apiToken) {
|
|
|
105
111
|
${JSON.stringify(content)}`;
|
|
106
112
|
|
|
107
113
|
let tokens = tokenizer.encode(prompt);
|
|
108
|
-
if (tokens.text.length <=
|
|
114
|
+
if (tokens.text.length <= maxAllowedTokens) {
|
|
109
115
|
break;
|
|
110
116
|
}
|
|
111
117
|
if (promptText.length > 2 * 1024 * 1024) {
|
|
@@ -138,7 +144,7 @@ ${JSON.stringify(content)}`;
|
|
|
138
144
|
};
|
|
139
145
|
|
|
140
146
|
let payload = {
|
|
141
|
-
model:
|
|
147
|
+
model: gptModel,
|
|
142
148
|
messages: [
|
|
143
149
|
{
|
|
144
150
|
role: 'system',
|
|
@@ -151,37 +157,50 @@ ${JSON.stringify(content)}`;
|
|
|
151
157
|
]
|
|
152
158
|
};
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
let res;
|
|
161
|
+
let data;
|
|
162
|
+
let retries = 0;
|
|
163
|
+
|
|
164
|
+
let run = async () => {
|
|
165
|
+
res = await fetchCmd(OPENAI_API_URL, {
|
|
166
|
+
method: 'post',
|
|
167
|
+
headers,
|
|
168
|
+
body: JSON.stringify(payload),
|
|
169
|
+
dispatcher: fetchAgent
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
data = await res.json();
|
|
173
|
+
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
if (res.status === 429 && ++retries < 5) {
|
|
176
|
+
// try again
|
|
177
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
178
|
+
return await run();
|
|
179
|
+
}
|
|
163
180
|
|
|
164
|
-
|
|
181
|
+
if (data && data.error) {
|
|
182
|
+
let error = new Error(data.error.message || data.error);
|
|
183
|
+
if (data.error.code) {
|
|
184
|
+
error.code = data.error.code;
|
|
185
|
+
}
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
let error = new Error(data.error.message || data.error);
|
|
169
|
-
if (data.error.code) {
|
|
170
|
-
error.code = data.error.code;
|
|
187
|
+
error.statusCode = res.status;
|
|
188
|
+
throw error;
|
|
171
189
|
}
|
|
172
190
|
|
|
191
|
+
let error = new Error('Failed to run API request');
|
|
173
192
|
error.statusCode = res.status;
|
|
174
193
|
throw error;
|
|
175
194
|
}
|
|
176
195
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
196
|
+
if (!data) {
|
|
197
|
+
throw new Error(`Failed to POST API request`);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
181
200
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
const reqStartTime = Date.now();
|
|
202
|
+
await run();
|
|
203
|
+
const reqEndTime = Date.now();
|
|
185
204
|
|
|
186
205
|
let values;
|
|
187
206
|
const output =
|
|
@@ -205,9 +224,10 @@ ${JSON.stringify(content)}`;
|
|
|
205
224
|
throw error;
|
|
206
225
|
}
|
|
207
226
|
|
|
208
|
-
const response = Object.assign({ id: null, tokens: null }, values, {
|
|
227
|
+
const response = Object.assign({ id: null, tokens: null, model: null }, values, {
|
|
209
228
|
id: data && data.id,
|
|
210
229
|
tokens: data && data.usage && data.usage.total_tokens,
|
|
230
|
+
model: gptModel,
|
|
211
231
|
|
|
212
232
|
_text: content.text,
|
|
213
233
|
_time: reqEndTime - reqStartTime,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Email text tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"mailparser": "3.6.4"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"dompurify": "3.0.
|
|
26
|
+
"dompurify": "3.0.2",
|
|
27
27
|
"gpt3-tokenizer": "1.1.5",
|
|
28
28
|
"handlebars": "4.7.7",
|
|
29
29
|
"he": "1.2.0",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"juice": "9.0.0",
|
|
34
34
|
"linkify-it": "4.0.1",
|
|
35
35
|
"moment-timezone": "0.5.43",
|
|
36
|
-
"node-fetch": "2.6.7",
|
|
37
36
|
"node-html-parser": "6.1.5",
|
|
38
|
-
"tlds": "1.238.0"
|
|
37
|
+
"tlds": "1.238.0",
|
|
38
|
+
"undici": "5.22.0"
|
|
39
39
|
}
|
|
40
40
|
}
|