@postalsys/email-text-tools 1.2.3 → 1.2.5
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/examples/{reply.js → summary.js} +5 -3
- package/index.js +2 -2
- package/lib/generate-summary.js +143 -0
- package/package.json +2 -2
- package/lib/extract-reply.js +0 -93
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs').promises;
|
|
4
|
-
const
|
|
4
|
+
const generateSummary = require('../lib/generate-summary');
|
|
5
5
|
const simpleParser = require('mailparser').simpleParser;
|
|
6
6
|
|
|
7
7
|
async function main() {
|
|
@@ -9,15 +9,17 @@ async function main() {
|
|
|
9
9
|
|
|
10
10
|
const parsed = await simpleParser(eml);
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const summary = await generateSummary(
|
|
13
13
|
{
|
|
14
|
+
from: parsed.from.value,
|
|
15
|
+
subject: parsed.subject,
|
|
14
16
|
html: parsed.html,
|
|
15
17
|
text: parsed.text
|
|
16
18
|
},
|
|
17
19
|
process.env.OPENAI_API_KEY
|
|
18
20
|
);
|
|
19
21
|
|
|
20
|
-
console.log(
|
|
22
|
+
console.log(summary);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
main();
|
package/index.js
CHANGED
|
@@ -5,6 +5,6 @@ const htmlToText = require('./lib/html-to-text');
|
|
|
5
5
|
const inlineHtml = require('./lib/inline-html');
|
|
6
6
|
const inlineText = require('./lib/inline-text');
|
|
7
7
|
const mimeHtml = require('./lib/mime-html');
|
|
8
|
-
const
|
|
8
|
+
const generateSummary = require('./lib/generate-summary');
|
|
9
9
|
|
|
10
|
-
module.exports = { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml,
|
|
10
|
+
module.exports = { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml, generateSummary };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const packageData = require('../package.json');
|
|
4
|
+
const htmlToText = require('../lib/html-to-text');
|
|
5
|
+
const nodeFetch = require('node-fetch');
|
|
6
|
+
const fetchCmd = global.fetch || nodeFetch;
|
|
7
|
+
|
|
8
|
+
const MAX_TEXT_LENGTH = 6 * 1024;
|
|
9
|
+
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
10
|
+
|
|
11
|
+
const SYSTEM_PROMPT = `
|
|
12
|
+
I want you to act as an email client that processes emails and highlights important parts.
|
|
13
|
+
You do not comment or explain anything.
|
|
14
|
+
`
|
|
15
|
+
.replace(/\s+/g, ' ')
|
|
16
|
+
.trim();
|
|
17
|
+
|
|
18
|
+
const USER_PROMPT = `
|
|
19
|
+
Describe the sentiment of the email using one word. Use either "positive", "neutral" or "negative". Prefix the sentiment with "Sentiment:".
|
|
20
|
+
On a separate line, write a one-sentence summary of the email. Prefix the summary with "Summary:".
|
|
21
|
+
I want you to only reply with the sentiment and summary, do not write explanations.
|
|
22
|
+
All the text that follows from now on is the email message:
|
|
23
|
+
`
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim();
|
|
26
|
+
|
|
27
|
+
async function generateSummary(message, apiToken) {
|
|
28
|
+
let text = message.text;
|
|
29
|
+
if (message.html && (!text || message.html.length > text.length * 10)) {
|
|
30
|
+
text = htmlToText(message.html);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
text = text.trim();
|
|
34
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
35
|
+
text = text.substring(0, MAX_TEXT_LENGTH);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let header = [];
|
|
39
|
+
if (message.subject) {
|
|
40
|
+
header.push(`Subject: ${message.subject}`);
|
|
41
|
+
}
|
|
42
|
+
if (message.from) {
|
|
43
|
+
let from = [].concat(message.from || []).shift();
|
|
44
|
+
header.push(`From: ${from.name || from.address}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (header.length) {
|
|
48
|
+
text = `${header.join('\n')}
|
|
49
|
+
|
|
50
|
+
${text}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let headers = {
|
|
54
|
+
'User-Agent': `${packageData.name}/${packageData.version}`,
|
|
55
|
+
Authorization: `Bearer ${apiToken}`,
|
|
56
|
+
'Content-Type': 'application/json'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let payload = {
|
|
60
|
+
model: 'gpt-3.5-turbo',
|
|
61
|
+
messages: [
|
|
62
|
+
{
|
|
63
|
+
role: 'system',
|
|
64
|
+
content: `${SYSTEM_PROMPT}`
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
role: 'user',
|
|
68
|
+
content: `${USER_PROMPT}
|
|
69
|
+
|
|
70
|
+
${text}`
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let res = await fetchCmd(OPENAI_API_URL, {
|
|
76
|
+
method: 'post',
|
|
77
|
+
headers,
|
|
78
|
+
body: JSON.stringify(payload)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let data = await res.json();
|
|
82
|
+
|
|
83
|
+
console.log(`-----
|
|
84
|
+
${text}
|
|
85
|
+
----
|
|
86
|
+
${JSON.stringify(data)}`);
|
|
87
|
+
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
if (data && data.error) {
|
|
90
|
+
let error = new Error(data.error.message || data.error);
|
|
91
|
+
if (data.error.code) {
|
|
92
|
+
error.code = data.error.code;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
error.statusCode = res.status;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let error = new Error('Failed to run API request');
|
|
100
|
+
error.statusCode = res.status;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!data) {
|
|
105
|
+
throw new Error(`Failed to POST API request`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let output =
|
|
109
|
+
data &&
|
|
110
|
+
data.choices &&
|
|
111
|
+
data.choices
|
|
112
|
+
.filter(msg => msg && msg.message && msg.message.role === 'assistant' && msg.message.content)
|
|
113
|
+
.sort((a, b) => ((a && a.index) || 0) - ((b && b.index) || 0))
|
|
114
|
+
.map(msg => msg.message.content)
|
|
115
|
+
.join('\n')
|
|
116
|
+
.trim()
|
|
117
|
+
.replace(/^\s*"|"\s*$/g, '')
|
|
118
|
+
.trim();
|
|
119
|
+
|
|
120
|
+
let structured = {};
|
|
121
|
+
|
|
122
|
+
let summaryMatch = output.match(/\bsummary:\s*(.*)$/im);
|
|
123
|
+
if (summaryMatch) {
|
|
124
|
+
structured.summary = summaryMatch[1].replace(/^["'\s]+|["'\s]+$/g, '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let sentimentMatch = output.match(/\bsentiment:\s*(.*)$/im);
|
|
128
|
+
if (sentimentMatch) {
|
|
129
|
+
structured.sentiment = sentimentMatch[1].replace(/^["'\s]+|["'\s]+$/g, '').toLowerCase();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = Object.assign(
|
|
133
|
+
{
|
|
134
|
+
id: data && data.id,
|
|
135
|
+
tokens: data && data.usage && data.usage.total_tokens
|
|
136
|
+
},
|
|
137
|
+
structured
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = generateSummary;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "Email text tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"moment-timezone": "0.5.41",
|
|
35
35
|
"node-fetch": "2.6.7",
|
|
36
36
|
"node-html-parser": "6.1.5",
|
|
37
|
-
"tlds": "1.
|
|
37
|
+
"tlds": "1.237.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/lib/extract-reply.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const packageData = require('../package.json');
|
|
4
|
-
const htmlToText = require('../lib/html-to-text');
|
|
5
|
-
const nodeFetch = require('node-fetch');
|
|
6
|
-
const fetchCmd = global.fetch || nodeFetch;
|
|
7
|
-
|
|
8
|
-
const MAX_TEXT_LENGTH = 6 * 1024;
|
|
9
|
-
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
10
|
-
|
|
11
|
-
const PROMPT = `If the following email message includes a reply text, return it. If the message ends with a courtesy greeting, do not include it in the response. Extract the main content text if the message does not contain a reply. Do not add a comment, only return the message text or an empty response if you can not detect any text content. Here is the email message content:`;
|
|
12
|
-
|
|
13
|
-
async function extractReply(message, apiToken) {
|
|
14
|
-
let text = message.text;
|
|
15
|
-
if (!text && message.html) {
|
|
16
|
-
text = htmlToText(message.text);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
text = text.trim();
|
|
20
|
-
if (text.length > MAX_TEXT_LENGTH) {
|
|
21
|
-
text = text.substring(0, MAX_TEXT_LENGTH);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let headers = {
|
|
25
|
-
'User-Agent': `${packageData.name}/${packageData.version}`,
|
|
26
|
-
Authorization: `Bearer ${apiToken}`,
|
|
27
|
-
'Content-Type': 'application/json'
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
let payload = {
|
|
31
|
-
model: 'gpt-3.5-turbo',
|
|
32
|
-
messages: [
|
|
33
|
-
{
|
|
34
|
-
role: 'user',
|
|
35
|
-
content: `${PROMPT}
|
|
36
|
-
|
|
37
|
-
${text}`
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
let res = await fetchCmd(OPENAI_API_URL, {
|
|
43
|
-
method: 'post',
|
|
44
|
-
headers,
|
|
45
|
-
body: JSON.stringify(payload)
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
let data = await res.json();
|
|
49
|
-
|
|
50
|
-
if (!res.ok) {
|
|
51
|
-
if (data && data.error) {
|
|
52
|
-
let error = new Error(data.error.message || data.error);
|
|
53
|
-
if (data.error.code) {
|
|
54
|
-
error.code = data.error.code;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
error.statusCode = res.status;
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let error = new Error('Failed to run API request');
|
|
62
|
-
error.statusCode = res.status;
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!data) {
|
|
67
|
-
throw new Error(`Failed to POST API request`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
console.log('OPENAI RESPONSE', JSON.stringify(data, false, 2));
|
|
71
|
-
|
|
72
|
-
let output =
|
|
73
|
-
data &&
|
|
74
|
-
data.choices &&
|
|
75
|
-
data.choices
|
|
76
|
-
.filter(msg => msg && msg.message && msg.message.role === 'assistant' && msg.message.content)
|
|
77
|
-
.sort((a, b) => ((a && a.index) || 0) - ((b && b.index) || 0))
|
|
78
|
-
.map(msg => msg.message.content)
|
|
79
|
-
.join('\n')
|
|
80
|
-
.trim()
|
|
81
|
-
.replace(/^\s*"|"\s*$/g, '')
|
|
82
|
-
.trim();
|
|
83
|
-
|
|
84
|
-
const response = {
|
|
85
|
-
id: data && data.id,
|
|
86
|
-
tokens: data && data.usage && data.usage.total_tokens,
|
|
87
|
-
text: output
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
return response;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = extractReply;
|