best-email-builder 0.5.1 → 0.7.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/CHANGELOG.md +63 -0
- package/README.md +74 -8
- package/dist/ai-server.js +116 -1
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.js +639 -176
- package/dist/ai.js.map +1 -1
- package/dist/builder.js +4 -4
- package/dist/{chunk-2Y7CDXZM.js → chunk-3RPWH5PC.js} +3 -3
- package/dist/{chunk-H4W65GBX.js → chunk-C6WHIBGH.js} +2 -2
- package/dist/{chunk-H4W65GBX.js.map → chunk-C6WHIBGH.js.map} +1 -1
- package/dist/{chunk-BAA5XMWY.js → chunk-CTZ56YJG.js} +252 -28
- package/dist/chunk-CTZ56YJG.js.map +1 -0
- package/dist/{chunk-7QQH6MTV.js → chunk-QH5OI43L.js} +4 -4
- package/dist/{chunk-N7DEVAIP.js → chunk-QRUMXCGW.js} +2 -2
- package/dist/{chunk-2RKVJQSD.js → chunk-UMVBT7NW.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/library.js +4 -4
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +5 -0
- package/dist/types/ai/ModelPicker.d.ts +34 -0
- package/dist/types/ai/agent.d.ts +31 -0
- package/dist/types/ai/agentEndpoint.d.ts +69 -0
- package/dist/types/ai/claude.d.ts +31 -5
- package/dist/types/ai/createAi.d.ts +5 -3
- package/dist/types/ai/draft.d.ts +10 -4
- package/dist/types/ai/generator.d.ts +10 -4
- package/dist/types/ai/index.d.ts +3 -2
- package/dist/types/ai/keys.d.ts +28 -26
- package/dist/types/ai/providers.d.ts +58 -1
- package/dist/types/ai/rewrite.d.ts +2 -1
- package/dist/types/ai/server.d.ts +3 -1
- package/dist/types/ai/subjects.d.ts +2 -1
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/engine/NativeAdapter.d.ts +1 -1
- package/dist/types/server/handler.d.ts +3 -0
- package/package.json +1 -1
- package/dist/chunk-BAA5XMWY.js.map +0 -1
- /package/dist/{chunk-2Y7CDXZM.js.map → chunk-3RPWH5PC.js.map} +0 -0
- /package/dist/{chunk-7QQH6MTV.js.map → chunk-QH5OI43L.js.map} +0 -0
- /package/dist/{chunk-N7DEVAIP.js.map → chunk-QRUMXCGW.js.map} +0 -0
- /package/dist/{chunk-2RKVJQSD.js.map → chunk-UMVBT7NW.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,69 @@ All notable changes are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] — 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **The user chooses the model.** In the Connect dialog, under the key, a
|
|
13
|
+
field opens the provider's list of models in place: the default first,
|
|
14
|
+
then the free ones (marked, with a *Free only* filter), then the newest;
|
|
15
|
+
searchable, and an id the list does not have can be used as typed. The
|
|
16
|
+
list is asked with the key (OpenRouter's needs none, so it shows before
|
|
17
|
+
any key); embeddings, speech, pictures and video are left out. The model
|
|
18
|
+
chosen is remembered for each provider and sent with each request.
|
|
19
|
+
`ai.key` gains `model(id?)`, `chooseModel(model, id?)` and
|
|
20
|
+
`listModels(id?, { apiKey, signal })`.
|
|
21
|
+
- **`listProviderModels({ provider, apiKey })`** — the models a provider
|
|
22
|
+
offers for writing, from `/ai` and `/ai/server`. `claudeProvider` lists
|
|
23
|
+
Claude's through the application's SDK and takes the model chosen.
|
|
24
|
+
- The live demo says which model is connected, and its link is now *Change
|
|
25
|
+
provider or model*.
|
|
26
|
+
- **Your own AI agent as the builder's AI** — `agentEmailModel({ url, headers })`
|
|
27
|
+
sends each model turn to an agent of the application's own over a small
|
|
28
|
+
HTTP protocol, `best-email-builder/agent@1` (`{ task, system, messages,
|
|
29
|
+
schema }` in, `{ text }` out, errors as codes the interface explains).
|
|
30
|
+
Generate, Rewrite and Subjects then go through the agent (its tools, its
|
|
31
|
+
knowledge, its users' rights) and its answers are checked like any model's.
|
|
32
|
+
On the server, `headers({ task, user })` gets the user `authorize` returned,
|
|
33
|
+
to forward who asks; the handler now hands that user to the generator and
|
|
34
|
+
its model. The agent's side: `createAgentEndpoint({ complete })` in
|
|
35
|
+
JavaScript, a FastAPI router to copy in `examples/agent/python`;
|
|
36
|
+
`checkAgent({ url, headers })` tries an agent with the builder's own checks.
|
|
37
|
+
See docs/AGENT.md.
|
|
38
|
+
- **`MODEL_UNAVAILABLE`** — a model the provider does not have, or no longer
|
|
39
|
+
(a 404 about the model, OpenRouter's "not a valid model ID"): *Choose
|
|
40
|
+
another one*, instead of *Try again*.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- *Get a key* opens the provider's page in a new tab with `window.open`,
|
|
44
|
+
also where a link's target is ignored (some embedded browsers).
|
|
45
|
+
|
|
46
|
+
## [0.6.0] — 2026-10-15
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- **The user chooses the provider.** `createAi({ provider: [...], askKey: true })`
|
|
50
|
+
takes a list: the Connect dialog shows the providers as cards (name, model,
|
|
51
|
+
free or not, a dot on those whose key this browser keeps; arrows move
|
|
52
|
+
through them). The choice and one key per provider are remembered, and a
|
|
53
|
+
provider whose key is kept needs nothing pasted. `ai.key` gains `provider`,
|
|
54
|
+
`providers`, `choose(id)` and `get(id)`; `ai.key.request()` opens the dialog
|
|
55
|
+
to change provider.
|
|
56
|
+
- **`claudeProvider({ client })`** — Claude as one of those providers, with
|
|
57
|
+
the user's own Anthropic key; the application's SDK is loaded when Claude
|
|
58
|
+
is chosen.
|
|
59
|
+
- **`failureCode(status, message)`** — the rule that turns a provider's
|
|
60
|
+
failure into the interface's error, exported for a model of your own.
|
|
61
|
+
- The live demo offers OpenAI, Claude, Mistral, Gemini and OpenRouter, says
|
|
62
|
+
which one is connected, and has a *Change provider* link.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
- **An exhausted quota said "busy, try again".** OpenAI answers a quota
|
|
66
|
+
spent with a 429 (`insufficient_quota`), Anthropic an empty credit balance
|
|
67
|
+
with a 400: both are now `MODEL_CREDITS`.
|
|
68
|
+
- **Claude's failures were all "could not answer".** The SDK's errors are now
|
|
69
|
+
read like every provider's: a refused key is `KEY_REFUSED` (asked again with
|
|
70
|
+
`askKey`), no credits `MODEL_CREDITS`, overloaded (529) `MODEL_BUSY`.
|
|
71
|
+
|
|
9
72
|
## [0.5.1] — 2026-10-15
|
|
10
73
|
|
|
11
74
|
### Fixed
|
package/README.md
CHANGED
|
@@ -61,7 +61,8 @@ Best Email Builder is the second option done once, properly, and given away:
|
|
|
61
61
|
model's MJML is cleaned and checked like any template before it gets there.
|
|
62
62
|
OpenRouter, Gemini, Groq, Mistral, OpenAI, DeepSeek, Cerebras, Ollama or any
|
|
63
63
|
OpenAI-compatible API by its key — free tiers included — and Claude through
|
|
64
|
-
its SDK; on your server, or in the page with each user's own key
|
|
64
|
+
its SDK; on your server, or in the page with each user's own key — and the
|
|
65
|
+
provider they choose.
|
|
65
66
|
- **Any text rewritten in one click** — select a text or a button: *Shorter*,
|
|
66
67
|
*Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate* into 13
|
|
67
68
|
languages, or say what to change in your own words. The text changes in
|
|
@@ -363,13 +364,55 @@ the provider refuses is forgotten and asked again. Off by default:
|
|
|
363
364
|
const ai = createAi({ provider: 'openrouter', askKey: true });
|
|
364
365
|
```
|
|
365
366
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
Give a list and each user chooses: the dialog shows the providers as cards
|
|
368
|
+
(name, model, free or not, a dot on those whose key this browser keeps). The
|
|
369
|
+
choice and one key per provider are remembered; going back to a provider whose
|
|
370
|
+
key is kept needs nothing pasted.
|
|
371
|
+
|
|
372
|
+
```jsx
|
|
373
|
+
const ai = createAi({ provider: ['openai', 'mistral', 'gemini', 'openrouter'], askKey: true });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Under the key, the user can choose the model: the field opens the provider's
|
|
377
|
+
own list (asked with the key; OpenRouter's needs none), the default model
|
|
378
|
+
first, then the free ones, marked, then the newest — searchable, with *Free
|
|
379
|
+
only*, and an id the list does not have can be typed. Embeddings, speech,
|
|
380
|
+
pictures and video are left out. The model chosen is remembered for each
|
|
381
|
+
provider; without the list, the default one still works.
|
|
382
|
+
|
|
383
|
+
`ai.key` reads and changes it: `provider` (the one in use), `providers`,
|
|
384
|
+
`choose(id)`, `get(id?)`, `set(key)`, `forget()`, `model(id?)`,
|
|
385
|
+
`chooseModel(model, id?)`, `listModels(id?, { apiKey, signal })`, `request()`
|
|
386
|
+
(opens the dialog — to change provider or model, for one), `subscribe(fn)`,
|
|
387
|
+
`state()` (`{ asking, refused, connected }`). This is what the live demo does.
|
|
369
388
|
|
|
370
389
|
**In the page, with one key for everyone** — prototypes and internal tools
|
|
371
390
|
only, the key is in the page's code: `createAi({ provider: 'groq', apiKey })`.
|
|
372
391
|
|
|
392
|
+
**Your own AI agent** — the assistant your application already has (its
|
|
393
|
+
model, its tools, its knowledge base, its users' rights) becomes the
|
|
394
|
+
builder's AI: Generate, Rewrite and Subjects go through it. It speaks a small
|
|
395
|
+
HTTP protocol, `best-email-builder/agent@1`: one POST per model turn
|
|
396
|
+
(`{ task, system, messages, schema }` in, `{ text }` out). The builder still
|
|
397
|
+
writes the prompts and checks every answer, so the agent needs to know nothing
|
|
398
|
+
of MJML.
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
generator: createEmailGenerator({
|
|
402
|
+
model: agentEmailModel({
|
|
403
|
+
url: 'http://agent.internal:8800/email-builder',
|
|
404
|
+
headers: ({ user }) => ({ 'X-Auth-User': signFor(user) }), // who asks: the user authorize returned
|
|
405
|
+
}),
|
|
406
|
+
compile: compileMjml,
|
|
407
|
+
}),
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The agent's side is one FastAPI file to copy
|
|
411
|
+
([`examples/agent/python`](examples/agent/python)) or `createAgentEndpoint`
|
|
412
|
+
in JavaScript, and `checkAgent({ url, headers })` tries an agent with the
|
|
413
|
+
builder's own checks. The protocol, the errors and both sides:
|
|
414
|
+
[docs/AGENT.md](docs/AGENT.md).
|
|
415
|
+
|
|
373
416
|
#### Providers
|
|
374
417
|
|
|
375
418
|
Every provider here speaks the OpenAI *chat completions* API; nothing to
|
|
@@ -402,6 +445,10 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
|
|
|
402
445
|
(`reasoning: { effort: 'low' }`) and given room for it (`maxTokens: 32000`;
|
|
403
446
|
16 000 elsewhere). For drafts in seconds, use a free tier with a key of its
|
|
404
447
|
own (Gemini, Groq, Cerebras) or a paid model.
|
|
448
|
+
- `listProviderModels({ provider, apiKey })` lists the models a provider
|
|
449
|
+
offers for writing, as the dialog does (`{ id, name, free }`, the default
|
|
450
|
+
first): its `/models`, or the provider's own `listModels` (`claudeProvider`
|
|
451
|
+
has one).
|
|
405
452
|
- `body` adds fields of your own to each request — OpenRouter's fallback list,
|
|
406
453
|
for one: `providerEmailModel({ provider: 'openrouter', apiKey, body: { models: [...] } })`.
|
|
407
454
|
It cannot change the model, the messages or the output limit.
|
|
@@ -410,8 +457,12 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
|
|
|
410
457
|
remembered. A model whose structured output breaks the JSON (some do on the
|
|
411
458
|
quotes of HTML) can be told to skip it: `provider: { …, format: 'text' }`.
|
|
412
459
|
- The provider's failures become errors the interface explains, in its
|
|
413
|
-
language: `KEY_MISSING`, `KEY_REFUSED`, `
|
|
414
|
-
|
|
460
|
+
language: `KEY_MISSING`, `KEY_REFUSED`, `MODEL_UNAVAILABLE` (the provider
|
|
461
|
+
does not have that model, or no longer), `MODEL_CREDITS`, `MODEL_BUSY`,
|
|
462
|
+
`MODEL_ERROR` (with the provider's own text in `detail`). An exhausted
|
|
463
|
+
quota is `MODEL_CREDITS`, not `MODEL_BUSY`, even when it comes as a 429
|
|
464
|
+
(OpenAI's `insufficient_quota`) or a 400 (Anthropic's credit balance);
|
|
465
|
+
`failureCode(status, message)` is that rule, for a model of your own. None is a 401:
|
|
415
466
|
the key is the AI's, not the user's session. A 403 about the model (not
|
|
416
467
|
available here, in this region) or the content is not `KEY_REFUSED`: with
|
|
417
468
|
`askKey`, the user's key is not thrown away for it.
|
|
@@ -419,7 +470,21 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
|
|
|
419
470
|
Anthropic SDK (`npm install @anthropic-ai/sdk`) — `claude-opus-5-5` by
|
|
420
471
|
default, `effort: 'medium'`, structured output, streamed so a long email
|
|
421
472
|
does not hit a request timeout — or through OpenRouter with an
|
|
422
|
-
`anthropic/…` model and one key for every model.
|
|
473
|
+
`anthropic/…` model and one key for every model. With `askKey`, Claude is
|
|
474
|
+
one more provider to choose, with the user's own Anthropic key and the SDK
|
|
475
|
+
loaded only when it is chosen:
|
|
476
|
+
|
|
477
|
+
```js
|
|
478
|
+
import { claudeProvider, createAi } from 'best-email-builder/ai';
|
|
479
|
+
|
|
480
|
+
const claude = claudeProvider({
|
|
481
|
+
client: async (apiKey) => {
|
|
482
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
483
|
+
return new Anthropic({ apiKey, dangerouslyAllowBrowser: true });
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
createAi({ provider: ['openai', claude, 'gemini'], askKey: true });
|
|
487
|
+
```
|
|
423
488
|
- **Anything else** is a function `({ system, messages, schema, signal, task })
|
|
424
489
|
=> Promise<string>` returning the JSON text — `createEmailGenerator({ model })`
|
|
425
490
|
on the server, `createAi({ model })` in the page.
|
|
@@ -474,7 +539,8 @@ another click another one, the first row brings the user's subject back.
|
|
|
474
539
|
are exported.
|
|
475
540
|
|
|
476
541
|
The live demo has no server: there, the AI runs in the page with the
|
|
477
|
-
visitor's own
|
|
542
|
+
visitor's own key, for the provider they choose — OpenAI, Claude, Mistral,
|
|
543
|
+
Gemini or OpenRouter (`createAi({ provider: ['openai', claude, 'mistral', 'gemini', 'openrouter'], askKey: true })`).
|
|
478
544
|
|
|
479
545
|
### 5. Your brand (optional)
|
|
480
546
|
|
package/dist/ai-server.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AGENT_ERROR_CODES,
|
|
3
|
+
AGENT_PROTOCOL,
|
|
2
4
|
AI_MESSAGES,
|
|
3
5
|
AI_PROVIDERS,
|
|
4
6
|
DEFAULT_CLAUDE_MODEL,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
REWRITE_SYSTEM_PROMPT,
|
|
14
16
|
SUBJECTS_SYSTEM_PROMPT,
|
|
15
17
|
SYSTEM_PROMPT,
|
|
18
|
+
agentEmailModel,
|
|
16
19
|
buildUserPrompt,
|
|
17
20
|
claudeEmailModel,
|
|
18
21
|
cleanGenerateInput,
|
|
@@ -20,6 +23,8 @@ import {
|
|
|
20
23
|
createRewriter,
|
|
21
24
|
createSubjectSuggester,
|
|
22
25
|
emailWords,
|
|
26
|
+
failureCode,
|
|
27
|
+
listProviderModels,
|
|
23
28
|
normalizeGeneratedMjml,
|
|
24
29
|
parseDraft,
|
|
25
30
|
providerEmailModel,
|
|
@@ -29,11 +34,116 @@ import {
|
|
|
29
34
|
reviewDraft,
|
|
30
35
|
reviewRewrite,
|
|
31
36
|
reviewSubjects
|
|
32
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-CTZ56YJG.js";
|
|
33
38
|
import "./chunk-OWVXKMLI.js";
|
|
34
39
|
import "./chunk-W424KPCR.js";
|
|
35
40
|
import "./chunk-JI36FNWE.js";
|
|
41
|
+
|
|
42
|
+
// src/ai/agentEndpoint.js
|
|
43
|
+
var TASKS = ["generate", "rewrite", "subjects"];
|
|
44
|
+
var MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
45
|
+
var MAX_MESSAGES = 20;
|
|
46
|
+
var json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
|
|
47
|
+
var refuse = (error, code, status) => json({ error, code }, status);
|
|
48
|
+
function problemOf(body) {
|
|
49
|
+
if (!body || typeof body !== "object") return "The body is not a JSON object";
|
|
50
|
+
if (body.protocol !== AGENT_PROTOCOL) return `Unsupported protocol ${JSON.stringify(body.protocol)} \u2014 this endpoint speaks ${AGENT_PROTOCOL}`;
|
|
51
|
+
if (!TASKS.includes(body.task)) return `Unknown task ${JSON.stringify(body.task)}`;
|
|
52
|
+
if (typeof body.system !== "string") return "`system` must be a string";
|
|
53
|
+
if (!body.schema || typeof body.schema !== "object") return "`schema` must be an object";
|
|
54
|
+
const { messages } = body;
|
|
55
|
+
if (!Array.isArray(messages) || !messages.length || messages.length > MAX_MESSAGES) return `\`messages\` must hold 1 to ${MAX_MESSAGES} messages`;
|
|
56
|
+
for (const message of messages) {
|
|
57
|
+
if (!message || message.role !== "user" && message.role !== "assistant" || typeof message.content !== "string") {
|
|
58
|
+
return 'Each message is { role: "user"|"assistant", content: string }';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function createAgentEndpoint({ complete, onError = (error) => console.error("[agent]", error) } = (
|
|
64
|
+
/** @type {any} */
|
|
65
|
+
{}
|
|
66
|
+
)) {
|
|
67
|
+
if (typeof complete !== "function") throw new TypeError("createAgentEndpoint: `complete` must be a function \u2014 your agent");
|
|
68
|
+
return async function agentEndpoint(request) {
|
|
69
|
+
if (request.method.toUpperCase() !== "POST") return refuse("POST only", "METHOD_NOT_ALLOWED", 405);
|
|
70
|
+
const length = Number(request.headers.get("content-length") || 0);
|
|
71
|
+
if (length > MAX_BODY_BYTES) return refuse("Request too large", "TOO_LARGE", 413);
|
|
72
|
+
const raw = await request.text();
|
|
73
|
+
if (raw.length > MAX_BODY_BYTES) return refuse("Request too large", "TOO_LARGE", 413);
|
|
74
|
+
let body;
|
|
75
|
+
try {
|
|
76
|
+
body = JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return refuse("Invalid JSON", "INVALID_JSON", 400);
|
|
79
|
+
}
|
|
80
|
+
const problem = problemOf(body);
|
|
81
|
+
if (problem) return refuse(problem, "INVALID_REQUEST", 400);
|
|
82
|
+
try {
|
|
83
|
+
const answer = await complete({
|
|
84
|
+
task: body.task,
|
|
85
|
+
system: body.system,
|
|
86
|
+
messages: body.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
87
|
+
schema: body.schema,
|
|
88
|
+
request,
|
|
89
|
+
signal: request.signal
|
|
90
|
+
});
|
|
91
|
+
if (typeof answer === "string") return json({ text: answer });
|
|
92
|
+
if (answer && typeof answer === "object") return json({ output: answer });
|
|
93
|
+
return refuse("The agent gave no answer", "MODEL_ERROR", 502);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const code = (
|
|
96
|
+
/** @type {any} */
|
|
97
|
+
error?.code
|
|
98
|
+
);
|
|
99
|
+
if (AGENT_ERROR_CODES.includes(code)) {
|
|
100
|
+
const known = GENERATION_ERRORS[
|
|
101
|
+
/** @type {keyof typeof GENERATION_ERRORS} */
|
|
102
|
+
code
|
|
103
|
+
];
|
|
104
|
+
return refuse(String(
|
|
105
|
+
/** @type {any} */
|
|
106
|
+
error?.detail || /** @type {any} */
|
|
107
|
+
error?.message || known.message
|
|
108
|
+
), code, known.status);
|
|
109
|
+
}
|
|
110
|
+
onError(error, request);
|
|
111
|
+
return refuse("The agent could not answer", "MODEL_ERROR", 502);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
var SAMPLE_TEXT = '<p>Our summer sale starts on Monday: 30% off everything in store, for three days only. It ends on Wednesday at midnight. <a href="https://example.com/sale">See the offers</a>.</p>';
|
|
116
|
+
async function checkAgent({ url, headers, user, fetch: fetchImpl, timeout, compile, language = "en" }) {
|
|
117
|
+
const model = agentEmailModel({ url, headers, fetch: fetchImpl, timeout });
|
|
118
|
+
const generator = createEmailGenerator({ model, compile });
|
|
119
|
+
const steps = [
|
|
120
|
+
["generate", () => generator.generate({ purpose: "welcome", language }, { user }), (r) => ({ subject: r.subject, warnings: r.warnings })],
|
|
121
|
+
["rewrite", () => generator.rewrite({ action: "shorter", text: SAMPLE_TEXT, format: "html" }, { user }), (r) => r.text],
|
|
122
|
+
[
|
|
123
|
+
"subjects",
|
|
124
|
+
() => generator.suggestSubjects({ subject: "Summer sale: 30% off", text: "Our summer sale starts on Monday: 30% off everything in store, for three days only.", language }, { user }),
|
|
125
|
+
(r) => r.subjects
|
|
126
|
+
]
|
|
127
|
+
];
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const [task, run, sampleOf] of steps) {
|
|
130
|
+
const started = Date.now();
|
|
131
|
+
try {
|
|
132
|
+
const result = await run();
|
|
133
|
+
results.push({ task, ok: true, ms: Date.now() - started, attempts: result.attempts, sample: sampleOf(result) });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const failure = (
|
|
136
|
+
/** @type {any} */
|
|
137
|
+
error
|
|
138
|
+
);
|
|
139
|
+
results.push({ task, ok: false, ms: Date.now() - started, code: failure?.code || "ERROR", error: String(failure?.detail || failure?.message || error) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { ok: results.every((result) => result.ok), results };
|
|
143
|
+
}
|
|
36
144
|
export {
|
|
145
|
+
AGENT_ERROR_CODES,
|
|
146
|
+
AGENT_PROTOCOL,
|
|
37
147
|
AI_MESSAGES,
|
|
38
148
|
AI_PROVIDERS,
|
|
39
149
|
DEFAULT_CLAUDE_MODEL,
|
|
@@ -48,13 +158,18 @@ export {
|
|
|
48
158
|
REWRITE_SYSTEM_PROMPT,
|
|
49
159
|
SUBJECTS_SYSTEM_PROMPT,
|
|
50
160
|
SYSTEM_PROMPT,
|
|
161
|
+
agentEmailModel,
|
|
51
162
|
buildUserPrompt,
|
|
163
|
+
checkAgent,
|
|
52
164
|
claudeEmailModel,
|
|
53
165
|
cleanGenerateInput,
|
|
166
|
+
createAgentEndpoint,
|
|
54
167
|
createEmailGenerator,
|
|
55
168
|
createRewriter,
|
|
56
169
|
createSubjectSuggester,
|
|
57
170
|
emailWords,
|
|
171
|
+
failureCode,
|
|
172
|
+
listProviderModels,
|
|
58
173
|
normalizeGeneratedMjml,
|
|
59
174
|
parseDraft,
|
|
60
175
|
providerEmailModel,
|
package/dist/ai-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/ai/agentEndpoint.js"],"sourcesContent":["// @ts-check\n/**\n * THE AGENT'S SIDE of `best-email-builder/agent@1` (see agent.js), for an\n * agent written in JavaScript — and `checkAgent`, to try any agent with the\n * builder's own checks before plugging it in.\n *\n * const endpoint = createAgentEndpoint({\n * complete: async ({ task, system, messages, schema, request }) => {\n * const who = await sessionOf(request); // your rights\n * return myAgent.answer({ system, messages, who }); // text, or the answer as an object\n * },\n * });\n * // Next.js: export const POST = endpoint; Express: app.post('/email-builder', toNodeListener(endpoint))\n *\n * const report = await checkAgent({ url: 'http://localhost:8800/email-builder', headers, compile: compileMjml });\n * // { ok, results: [{ task: 'generate', ok, ms, attempts, sample }, …] }\n */\n\nimport { AGENT_ERROR_CODES, AGENT_PROTOCOL, agentEmailModel } from './agent.js';\nimport { GENERATION_ERRORS } from './draft.js';\nimport { createEmailGenerator } from './generator.js';\n\nconst TASKS = ['generate', 'rewrite', 'subjects'];\n/** A repair turn carries the last answer: a whole email at most. */\nconst MAX_BODY_BYTES = 2 * 1024 * 1024;\nconst MAX_MESSAGES = 20;\n\n/**\n * @typedef {{\n * task: 'generate'|'rewrite'|'subjects',\n * system: string,\n * messages: {role: 'user'|'assistant', content: string}[],\n * schema: object,\n * request: Request,\n * signal: AbortSignal,\n * }} AgentTask\n */\n\n/** @param {unknown} body @param {number} status */\nconst json = (body, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });\n\n/** @param {string} error @param {string} code @param {number} status */\nconst refuse = (error, code, status) => json({ error, code }, status);\n\n/**\n * What the builder sent, checked; or why not.\n * @param {any} body\n * @returns {string|null} the problem\n */\nfunction problemOf(body) {\n if (!body || typeof body !== 'object') return 'The body is not a JSON object';\n if (body.protocol !== AGENT_PROTOCOL) return `Unsupported protocol ${JSON.stringify(body.protocol)} — this endpoint speaks ${AGENT_PROTOCOL}`;\n if (!TASKS.includes(body.task)) return `Unknown task ${JSON.stringify(body.task)}`;\n if (typeof body.system !== 'string') return '`system` must be a string';\n if (!body.schema || typeof body.schema !== 'object') return '`schema` must be an object';\n const { messages } = body;\n if (!Array.isArray(messages) || !messages.length || messages.length > MAX_MESSAGES) return `\\`messages\\` must hold 1 to ${MAX_MESSAGES} messages`;\n for (const message of messages) {\n if (!message || (message.role !== 'user' && message.role !== 'assistant') || typeof message.content !== 'string') {\n return 'Each message is { role: \"user\"|\"assistant\", content: string }';\n }\n }\n return null;\n}\n\n/**\n * @param {{\n * complete: (task: AgentTask) => Promise<string | object> | string | object,\n * onError?: (error: unknown, request: Request) => void,\n * }} options\n * `complete`: your agent. It answers with the text of its model, or with\n * the answer as an object. To fail with a reason the builder explains, throw\n * an error whose `code` is one of AGENT_ERROR_CODES ('REFUSED',\n * 'MODEL_BUSY'…); any other error is logged with `onError` and answered as\n * MODEL_ERROR, its message kept out of the response.\n * @returns {(request: Request) => Promise<Response>}\n */\nexport function createAgentEndpoint({ complete, onError = (error) => console.error('[agent]', error) } = /** @type {any} */ ({})) {\n if (typeof complete !== 'function') throw new TypeError('createAgentEndpoint: `complete` must be a function — your agent');\n\n return async function agentEndpoint(request) {\n if (request.method.toUpperCase() !== 'POST') return refuse('POST only', 'METHOD_NOT_ALLOWED', 405);\n const length = Number(request.headers.get('content-length') || 0);\n if (length > MAX_BODY_BYTES) return refuse('Request too large', 'TOO_LARGE', 413);\n const raw = await request.text();\n if (raw.length > MAX_BODY_BYTES) return refuse('Request too large', 'TOO_LARGE', 413);\n /** @type {any} */\n let body;\n try {\n body = JSON.parse(raw);\n } catch {\n return refuse('Invalid JSON', 'INVALID_JSON', 400);\n }\n const problem = problemOf(body);\n if (problem) return refuse(problem, 'INVALID_REQUEST', 400);\n\n try {\n const answer = await complete({\n task: body.task,\n system: body.system,\n messages: body.messages.map((/** @type {any} */ m) => ({ role: m.role, content: m.content })),\n schema: body.schema,\n request,\n signal: request.signal,\n });\n if (typeof answer === 'string') return json({ text: answer });\n if (answer && typeof answer === 'object') return json({ output: answer });\n return refuse('The agent gave no answer', 'MODEL_ERROR', 502);\n } catch (error) {\n const code = /** @type {any} */ (error)?.code;\n if (AGENT_ERROR_CODES.includes(code)) {\n const known = GENERATION_ERRORS[/** @type {keyof typeof GENERATION_ERRORS} */ (code)];\n return refuse(String(/** @type {any} */ (error)?.detail || /** @type {any} */ (error)?.message || known.message), code, known.status);\n }\n onError(error, request);\n return refuse('The agent could not answer', 'MODEL_ERROR', 502);\n }\n };\n}\n\nconst SAMPLE_TEXT =\n '<p>Our summer sale starts on Monday: 30% off everything in store, for three days only. ' +\n 'It ends on Wednesday at midnight. <a href=\"https://example.com/sale\">See the offers</a>.</p>';\n\n/**\n * Tries an agent with the builder's own checks: one email written, one text\n * rewritten, subject ideas — in turn, each as the builder would ask it (with\n * its repair turn). Three to six calls to the agent.\n * @param {{\n * url: string,\n * headers?: import('./agent.js').AgentHeaders,\n * user?: any,\n * fetch?: typeof fetch,\n * timeout?: number,\n * compile?: (mjml: string) => Promise<{warnings?: {message: string}[]}>,\n * language?: string,\n * }} options\n * `user`: what `headers` gets, as the handler's `authorize` would give it.\n * `compile`: `compileMjml` (best-email-builder/server), for MJML's own\n * warnings as on the server.\n * @returns {Promise<{ok: boolean, results: {task: string, ok: boolean, ms: number, attempts?: number, sample?: unknown, code?: string, error?: string}[]}>}\n */\nexport async function checkAgent({ url, headers, user, fetch: fetchImpl, timeout, compile, language = 'en' }) {\n const model = agentEmailModel({ url, headers, fetch: fetchImpl, timeout });\n const generator = createEmailGenerator({ model, compile });\n /** @type {[string, () => Promise<any>, (result: any) => unknown][]} */\n const steps = [\n ['generate', () => generator.generate({ purpose: 'welcome', language }, { user }), (r) => ({ subject: r.subject, warnings: r.warnings })],\n ['rewrite', () => generator.rewrite({ action: 'shorter', text: SAMPLE_TEXT, format: 'html' }, { user }), (r) => r.text],\n [\n 'subjects',\n () => generator.suggestSubjects({ subject: 'Summer sale: 30% off', text: 'Our summer sale starts on Monday: 30% off everything in store, for three days only.', language }, { user }),\n (r) => r.subjects,\n ],\n ];\n const results = [];\n for (const [task, run, sampleOf] of steps) {\n const started = Date.now();\n try {\n const result = await run();\n results.push({ task, ok: true, ms: Date.now() - started, attempts: result.attempts, sample: sampleOf(result) });\n } catch (error) {\n const failure = /** @type {any} */ (error);\n results.push({ task, ok: false, ms: Date.now() - started, code: failure?.code || 'ERROR', error: String(failure?.detail || failure?.message || error) });\n }\n }\n return { ok: results.every((result) => result.ok), results };\n}\n"],"mappings":"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","names":[]}
|