@llm-newsletter-kit/core 2.0.6 → 3.0.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 +89 -16
- package/dist/index.d.ts +109 -39
- package/dist/index.js +102 -12
- package/dist/index.js.map +1 -1
- package/package.json +20 -22
- package/dist/index.cjs +0 -1958
- package/dist/index.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ A type‑first, extensible toolkit that automates LLM‑based newsletter creatio
|
|
|
21
21
|
- Type-first design (TypeScript, ESM) with strong contracts
|
|
22
22
|
- Flexible dependency injection: easily swap Crawling/Analysis/ContentGenerate/Task/Logging/Email
|
|
23
23
|
- Operational features built-in: retries, chain options, preview email sending, etc.
|
|
24
|
-
- Rollup build (ESM+
|
|
24
|
+
- Rollup build (ESM+d.ts), Vitest 100% coverage, GitHub Actions CI included
|
|
25
25
|
|
|
26
26
|
## Project Background
|
|
27
27
|
|
|
@@ -101,12 +101,8 @@ const config: GenerateNewsletterConfig<string> = {
|
|
|
101
101
|
},
|
|
102
102
|
crawlingProvider: {
|
|
103
103
|
// customFetch: myProxyFetch, // Optional: custom fetch for proxy support
|
|
104
|
-
crawlingTargetGroups: [
|
|
105
|
-
|
|
106
|
-
],
|
|
107
|
-
fetchExistingArticlesByUrls: async (urls) => [
|
|
108
|
-
/* ... */
|
|
109
|
-
],
|
|
104
|
+
crawlingTargetGroups: [/* ... */],
|
|
105
|
+
fetchExistingArticlesByUrls: async (urls) => [/* ... */],
|
|
110
106
|
saveCrawledArticles: async (articles, context) => articles.length,
|
|
111
107
|
},
|
|
112
108
|
analysisProvider: {
|
|
@@ -114,12 +110,8 @@ const config: GenerateNewsletterConfig<string> = {
|
|
|
114
110
|
classifyTagOptions: { model: openai('gpt-5-mini') },
|
|
115
111
|
analyzeImagesOptions: { model: openai('gpt-5.1') },
|
|
116
112
|
determineScoreOptions: { model: openai('gpt-5.1') },
|
|
117
|
-
fetchUnscoredArticles: async () => [
|
|
118
|
-
|
|
119
|
-
],
|
|
120
|
-
fetchTags: async () => [
|
|
121
|
-
/* ... */
|
|
122
|
-
],
|
|
113
|
+
fetchUnscoredArticles: async () => [/* ... */],
|
|
114
|
+
fetchTags: async () => [/* ... */],
|
|
123
115
|
update: async (article) => {},
|
|
124
116
|
},
|
|
125
117
|
contentGenerateProvider: {
|
|
@@ -128,9 +120,7 @@ const config: GenerateNewsletterConfig<string> = {
|
|
|
128
120
|
issueOrder: 1,
|
|
129
121
|
newsletterBrandName: 'Tech Insight Weekly',
|
|
130
122
|
publicationCriteria: { minimumArticleCountForIssue: 5 },
|
|
131
|
-
fetchArticleCandidates: async () => [
|
|
132
|
-
/* ... */
|
|
133
|
-
],
|
|
123
|
+
fetchArticleCandidates: async () => [/* ... */],
|
|
134
124
|
htmlTemplate: ({ content }) => `<html>...</html>`,
|
|
135
125
|
saveNewsletter: async ({ newsletter }) => ({ id: 1 }),
|
|
136
126
|
},
|
|
@@ -149,6 +139,88 @@ const newsletterId = await generator.generate();
|
|
|
149
139
|
|
|
150
140
|
👉 **See the reference implementation: https://github.com/heripo-lab/heripo-research-radar**
|
|
151
141
|
|
|
142
|
+
## Customizing LLM Prompts (PromptProvider)
|
|
143
|
+
|
|
144
|
+
The kit ships with built-in prompts designed to cover a wide range of domains. However, these general-purpose prompts may not be flexible enough for specialized requirements. For example:
|
|
145
|
+
|
|
146
|
+
- Your domain uses unique terminology or jargon that the default prompts don't account for
|
|
147
|
+
- You need a specific newsletter tone or structure (e.g., academic style, casual briefing)
|
|
148
|
+
- Tag classification requires domain-specific taxonomy rules
|
|
149
|
+
- Importance scoring needs custom criteria tailored to your industry
|
|
150
|
+
|
|
151
|
+
`PromptProvider` lets you **replace** any built-in prompt — system prompt, user prompt, or both — on a per-stage basis while keeping the rest of the pipeline intact.
|
|
152
|
+
|
|
153
|
+
### How It Works
|
|
154
|
+
|
|
155
|
+
PromptProvider is organized by pipeline stage. Every field is optional — omitted prompts fall back to the built-in defaults.
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
PromptProvider
|
|
159
|
+
├── analysis
|
|
160
|
+
│ ├── classifyTags — Tag classification (system / user)
|
|
161
|
+
│ ├── analyzeImages — Image analysis (system / user)
|
|
162
|
+
│ └── determineImportance — Importance scoring (system / user)
|
|
163
|
+
└── contentGenerate
|
|
164
|
+
└── generateNewsletter — Final newsletter generation (system / user)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Each prompt slot is a `PromptBuilder<TContext>` — an object with optional `system` and `user` functions. The function receives a typed context object containing all the data the default prompt would use (articles, tags, expert fields, dates, etc.), so you have full control over prompt construction.
|
|
168
|
+
|
|
169
|
+
> **Dependency note:** Analysis prompts (classifyTags → analyzeImages → determineImportance) produce data that flows into the content generation prompt. Changing an analysis prompt may indirectly affect the final newsletter output.
|
|
170
|
+
|
|
171
|
+
### Example
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import type {
|
|
175
|
+
GenerateNewsletterConfig,
|
|
176
|
+
PromptProvider,
|
|
177
|
+
} from '@llm-newsletter-kit/core';
|
|
178
|
+
|
|
179
|
+
const promptProvider: PromptProvider = {
|
|
180
|
+
analysis: {
|
|
181
|
+
// Override only the system prompt for tag classification
|
|
182
|
+
classifyTags: {
|
|
183
|
+
system: (ctx) =>
|
|
184
|
+
`You are a legal-domain specialist. Classify articles using ` +
|
|
185
|
+
`legal taxonomy standards. Available tags: ${ctx.existTags.join(', ')}. ` +
|
|
186
|
+
`Output language: ${ctx.outputLanguage}.`,
|
|
187
|
+
// user prompt falls back to the built-in default
|
|
188
|
+
},
|
|
189
|
+
// Override importance scoring with domain-specific criteria
|
|
190
|
+
determineImportance: {
|
|
191
|
+
system: (ctx) =>
|
|
192
|
+
`Score article importance for ${ctx.expertFields.join(', ')} professionals. ` +
|
|
193
|
+
`Regulatory changes and court rulings score 8+. ` +
|
|
194
|
+
`Commentary and opinion pieces score 3-5.`,
|
|
195
|
+
user: (ctx) =>
|
|
196
|
+
`Article: ${ctx.targetArticle.title}\n` +
|
|
197
|
+
`Content: ${ctx.targetArticle.detailContent}\n` +
|
|
198
|
+
`Score this article 1-10.`,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
contentGenerate: {
|
|
202
|
+
// Override the newsletter generation prompt entirely
|
|
203
|
+
generateNewsletter: {
|
|
204
|
+
system: (ctx) =>
|
|
205
|
+
`You produce a weekly legal digest for "${ctx.newsletterBrandName}". ` +
|
|
206
|
+
`Write in ${ctx.outputLanguage}. Use formal academic tone.`,
|
|
207
|
+
user: (ctx) =>
|
|
208
|
+
`Publication date: ${ctx.dateService.getPublicationDisplayDateString()}\n\n` +
|
|
209
|
+
ctx.targetArticles
|
|
210
|
+
.map((a) => `- [${a.title}](${a.url}) (score: ${a.importanceScore})`)
|
|
211
|
+
.join('\n'),
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const config: GenerateNewsletterConfig<string> = {
|
|
217
|
+
// ... other config (contentOptions, dateService, taskService, providers, etc.)
|
|
218
|
+
promptProvider, // Inject custom prompts
|
|
219
|
+
};
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For full context types (`ClassifyTagsPromptContext`, `GenerateNewsletterPromptContext`, etc.), see `src/generate-newsletter/models/prompt-provider.ts`.
|
|
223
|
+
|
|
152
224
|
## Public API Overview
|
|
153
225
|
|
|
154
226
|
Entry point: `src/index.ts`
|
|
@@ -163,6 +235,7 @@ Entry point: `src/index.ts`
|
|
|
163
235
|
- AnalysisProvider { classifyTagOptions.model, analyzeImagesOptions.model, determineScoreOptions(model, minimumImportanceScoreRules), fetchUnscoredArticles, fetchTags, update }
|
|
164
236
|
- ContentGenerateProvider { model and generation options, issueOrder, publicationCriteria, subscribePageUrl, newsletterBrandName, fetchArticleCandidates, htmlTemplate, saveNewsletter }
|
|
165
237
|
- GenerateNewsletterOptions { logger, llm, chain, previewNewsletter(emailService/emailMessage/fetchNewsletterForPreview) }
|
|
238
|
+
- PromptProvider { analysis?(classifyTags, analyzeImages, determineImportance), contentGenerate?(generateNewsletter) }
|
|
166
239
|
- Domain models: DateService, EmailService, Newsletter, etc.
|
|
167
240
|
|
|
168
241
|
For detailed field descriptions, see `src/generate-newsletter/models/interfaces.ts` and type definitions under `src/models/*`.
|
package/dist/index.d.ts
CHANGED
|
@@ -289,44 +289,6 @@ type ChainOptions = {
|
|
|
289
289
|
stopAfterAttempt?: number;
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
/**
|
|
293
|
-
* Token markers in template.
|
|
294
|
-
*/
|
|
295
|
-
type HtmlTemplateMarkers = {
|
|
296
|
-
/**
|
|
297
|
-
* Title token
|
|
298
|
-
*
|
|
299
|
-
* When the title is set to "NEWSLETTER_TITLE", it replaces the "{{NEWSLETTER_TITLE}}" pattern in the template string.
|
|
300
|
-
*
|
|
301
|
-
* @default "NEWSLETTER_TITLE"
|
|
302
|
-
*/
|
|
303
|
-
title?: string;
|
|
304
|
-
/**
|
|
305
|
-
* Content HTML token
|
|
306
|
-
*
|
|
307
|
-
* When content is set to "NEWSLETTER_CONTENT", it replaces the "{{NEWSLETTER_CONTENT}}" pattern in the template string.
|
|
308
|
-
*
|
|
309
|
-
* @default "NEWSLETTER_CONTENT"
|
|
310
|
-
*/
|
|
311
|
-
content?: string;
|
|
312
|
-
};
|
|
313
|
-
/**
|
|
314
|
-
* String template and marker set
|
|
315
|
-
*/
|
|
316
|
-
type HtmlTemplate = {
|
|
317
|
-
/**
|
|
318
|
-
* Original template string
|
|
319
|
-
*/
|
|
320
|
-
html: string;
|
|
321
|
-
/**
|
|
322
|
-
* Uses default markers when not specified
|
|
323
|
-
*/
|
|
324
|
-
markers?: HtmlTemplateMarkers;
|
|
325
|
-
};
|
|
326
|
-
type RequiredHtmlTemplate = Pick<HtmlTemplate, 'html'> & {
|
|
327
|
-
markers: Required<HtmlTemplateMarkers>;
|
|
328
|
-
};
|
|
329
|
-
|
|
330
292
|
/**
|
|
331
293
|
* Type representing an email address.
|
|
332
294
|
* @example "user@example.com"
|
|
@@ -488,6 +450,108 @@ interface DateService {
|
|
|
488
450
|
getPublicationDisplayDateString: () => string;
|
|
489
451
|
}
|
|
490
452
|
|
|
453
|
+
/**
|
|
454
|
+
* A pair of optional prompt builder functions.
|
|
455
|
+
* The returned string **replaces** the built-in default prompt entirely.
|
|
456
|
+
* If omitted, the default prompt is used.
|
|
457
|
+
*/
|
|
458
|
+
type PromptBuilder<TContext> = {
|
|
459
|
+
system?: (context: TContext) => string;
|
|
460
|
+
user?: (context: TContext) => string;
|
|
461
|
+
};
|
|
462
|
+
type ClassifyTagsPromptContext = {
|
|
463
|
+
expertFields: string[];
|
|
464
|
+
outputLanguage: string;
|
|
465
|
+
targetArticle: UnscoredArticle;
|
|
466
|
+
existTags: string[];
|
|
467
|
+
};
|
|
468
|
+
type AnalyzeImagesPromptContext = {
|
|
469
|
+
expertFields: string[];
|
|
470
|
+
outputLanguage: string;
|
|
471
|
+
targetArticle: UnscoredArticle;
|
|
472
|
+
};
|
|
473
|
+
type DetermineImportancePromptContext = {
|
|
474
|
+
expertFields: string[];
|
|
475
|
+
targetArticle: UnscoredArticle;
|
|
476
|
+
dateService: DateService;
|
|
477
|
+
minimumImportanceScoreRules: MinimumImportanceScoreRule[];
|
|
478
|
+
};
|
|
479
|
+
type GenerateNewsletterPromptContext = {
|
|
480
|
+
expertFields: string[];
|
|
481
|
+
outputLanguage: string;
|
|
482
|
+
dateService: DateService;
|
|
483
|
+
targetArticles: ArticleForGenerateContent[];
|
|
484
|
+
freeFormIntro?: boolean;
|
|
485
|
+
titleContext?: string;
|
|
486
|
+
subscribePageUrl?: UrlString;
|
|
487
|
+
newsletterBrandName: string;
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Optional provider for customizing LLM prompts.
|
|
491
|
+
* Grouped by pipeline stage. All fields are optional —
|
|
492
|
+
* omitted prompts fall back to built-in defaults.
|
|
493
|
+
*
|
|
494
|
+
* **Dependency note:** Analysis prompts (❶❷❸) produce data that flows
|
|
495
|
+
* into the content generation prompt (❹). Changing an analysis prompt
|
|
496
|
+
* may indirectly affect the final newsletter output.
|
|
497
|
+
*/
|
|
498
|
+
interface PromptProvider {
|
|
499
|
+
/**
|
|
500
|
+
* Analysis stage prompts (❶ classifyTags → ❷ analyzeImages → ❸ determineImportance).
|
|
501
|
+
* Results feed into the content generation stage.
|
|
502
|
+
*/
|
|
503
|
+
analysis?: {
|
|
504
|
+
classifyTags?: PromptBuilder<ClassifyTagsPromptContext>;
|
|
505
|
+
analyzeImages?: PromptBuilder<AnalyzeImagesPromptContext>;
|
|
506
|
+
determineImportance?: PromptBuilder<DetermineImportancePromptContext>;
|
|
507
|
+
};
|
|
508
|
+
/**
|
|
509
|
+
* Content generation stage prompt (❹ generateNewsletter).
|
|
510
|
+
* Produces the final newsletter title and body.
|
|
511
|
+
*/
|
|
512
|
+
contentGenerate?: {
|
|
513
|
+
generateNewsletter?: PromptBuilder<GenerateNewsletterPromptContext>;
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Token markers in template.
|
|
519
|
+
*/
|
|
520
|
+
type HtmlTemplateMarkers = {
|
|
521
|
+
/**
|
|
522
|
+
* Title token
|
|
523
|
+
*
|
|
524
|
+
* When the title is set to "NEWSLETTER_TITLE", it replaces the "{{NEWSLETTER_TITLE}}" pattern in the template string.
|
|
525
|
+
*
|
|
526
|
+
* @default "NEWSLETTER_TITLE"
|
|
527
|
+
*/
|
|
528
|
+
title?: string;
|
|
529
|
+
/**
|
|
530
|
+
* Content HTML token
|
|
531
|
+
*
|
|
532
|
+
* When content is set to "NEWSLETTER_CONTENT", it replaces the "{{NEWSLETTER_CONTENT}}" pattern in the template string.
|
|
533
|
+
*
|
|
534
|
+
* @default "NEWSLETTER_CONTENT"
|
|
535
|
+
*/
|
|
536
|
+
content?: string;
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* String template and marker set
|
|
540
|
+
*/
|
|
541
|
+
type HtmlTemplate = {
|
|
542
|
+
/**
|
|
543
|
+
* Original template string
|
|
544
|
+
*/
|
|
545
|
+
html: string;
|
|
546
|
+
/**
|
|
547
|
+
* Uses default markers when not specified
|
|
548
|
+
*/
|
|
549
|
+
markers?: HtmlTemplateMarkers;
|
|
550
|
+
};
|
|
551
|
+
type RequiredHtmlTemplate = Pick<HtmlTemplate, 'html'> & {
|
|
552
|
+
markers: Required<HtmlTemplateMarkers>;
|
|
553
|
+
};
|
|
554
|
+
|
|
491
555
|
/**
|
|
492
556
|
* Publishable final Newsletter entity.
|
|
493
557
|
*/
|
|
@@ -807,6 +871,11 @@ type GenerateNewsletterConfig<TaskId> = {
|
|
|
807
871
|
* Optional behavior/settings.
|
|
808
872
|
*/
|
|
809
873
|
options?: GenerateNewsletterOptions;
|
|
874
|
+
/**
|
|
875
|
+
* Optional provider for customizing LLM prompts.
|
|
876
|
+
* When omitted, built-in default prompts are used.
|
|
877
|
+
*/
|
|
878
|
+
promptProvider?: PromptProvider;
|
|
810
879
|
};
|
|
811
880
|
|
|
812
881
|
/**
|
|
@@ -822,6 +891,7 @@ declare class GenerateNewsletter<TaskId> {
|
|
|
822
891
|
private readonly contentGenerateProvider;
|
|
823
892
|
private readonly logger;
|
|
824
893
|
private readonly options;
|
|
894
|
+
private readonly promptProvider?;
|
|
825
895
|
private readonly previewNewsletterOptions?;
|
|
826
896
|
/** Independent internal field **/
|
|
827
897
|
private taskId;
|
|
@@ -858,4 +928,4 @@ declare class GenerateNewsletter<TaskId> {
|
|
|
858
928
|
}
|
|
859
929
|
|
|
860
930
|
export { DateType, GenerateNewsletter };
|
|
861
|
-
export type { AnalysisProvider, AppLogger, ArticleForGenerateContent, ArticleForUpdateByAnalysis, ContentGenerateProvider, CrawlingProvider, CrawlingTarget, CrawlingTargetGroup, DateService, EmailAddress, EmailAttachment, EmailMessage, EmailService, GenerateNewsletterConfig, GenerateNewsletterOptions, HtmlString, HtmlTemplate, HtmlTemplateMarkers, IsoDateString, LogLevel, LogMessage, MarkdownString, MinimumImportanceScoreRule, Newsletter, ParsedTarget, ParsedTargetDetail, ParsedTargetListItem, RequiredHtmlTemplate, TaskService, UniqueIdentifier, UnscoredArticle, UrlString };
|
|
931
|
+
export type { AnalysisProvider, AnalyzeImagesPromptContext, AppLogger, ArticleForGenerateContent, ArticleForUpdateByAnalysis, ClassifyTagsPromptContext, ContentGenerateProvider, CrawlingProvider, CrawlingTarget, CrawlingTargetGroup, DateService, DetermineImportancePromptContext, EmailAddress, EmailAttachment, EmailMessage, EmailService, GenerateNewsletterConfig, GenerateNewsletterOptions, GenerateNewsletterPromptContext, HtmlString, HtmlTemplate, HtmlTemplateMarkers, IsoDateString, LogLevel, LogMessage, MarkdownString, MinimumImportanceScoreRule, Newsletter, ParsedTarget, ParsedTargetDetail, ParsedTargetListItem, PromptBuilder, PromptProvider, RequiredHtmlTemplate, TaskService, UniqueIdentifier, UnscoredArticle, UrlString };
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { pick, omit } from 'es-toolkit';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { generateText, Output } from 'ai';
|
|
5
5
|
import { JSDOM } from 'jsdom';
|
|
6
|
+
import juice from 'juice';
|
|
6
7
|
import safeMarkdown2Html from 'safe-markdown2html';
|
|
7
8
|
import { randomUUID } from 'node:crypto';
|
|
8
9
|
|
|
@@ -191,8 +192,10 @@ class AnalyzeImages extends LLMQuery {
|
|
|
191
192
|
.string()
|
|
192
193
|
.describe('A high-level overview of the general theme, mood, and context conveyed by the images (without specific names, titles, or exact figures from text within images)'),
|
|
193
194
|
});
|
|
195
|
+
promptBuilder;
|
|
194
196
|
constructor(config) {
|
|
195
197
|
super(config);
|
|
198
|
+
this.promptBuilder = config.promptBuilder;
|
|
196
199
|
}
|
|
197
200
|
async execute() {
|
|
198
201
|
if (!this.targetArticle.hasAttachedImage ||
|
|
@@ -206,7 +209,7 @@ class AnalyzeImages extends LLMQuery {
|
|
|
206
209
|
model: this.model,
|
|
207
210
|
maxRetries: this.options.llm.maxRetries,
|
|
208
211
|
schema: this.schema,
|
|
209
|
-
|
|
212
|
+
instructions: this.systemPrompt,
|
|
210
213
|
messages: [
|
|
211
214
|
{
|
|
212
215
|
role: 'user',
|
|
@@ -216,7 +219,17 @@ class AnalyzeImages extends LLMQuery {
|
|
|
216
219
|
});
|
|
217
220
|
return { result: output.imageContext, usage };
|
|
218
221
|
}
|
|
222
|
+
get promptContext() {
|
|
223
|
+
return {
|
|
224
|
+
expertFields: this.expertFields,
|
|
225
|
+
outputLanguage: this.options.content.outputLanguage,
|
|
226
|
+
targetArticle: this.targetArticle,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
219
229
|
get systemPrompt() {
|
|
230
|
+
if (this.promptBuilder?.system) {
|
|
231
|
+
return this.promptBuilder.system(this.promptContext);
|
|
232
|
+
}
|
|
220
233
|
return `# Image Analysis Expert System
|
|
221
234
|
|
|
222
235
|
## Identity & Expertise
|
|
@@ -282,11 +295,18 @@ You are a specialized image analysis expert in: ${this.expertFields.join(', ')}
|
|
|
282
295
|
}
|
|
283
296
|
get imageMessages() {
|
|
284
297
|
return this.imageUrls.map((url) => ({
|
|
285
|
-
type: '
|
|
286
|
-
|
|
298
|
+
type: 'file',
|
|
299
|
+
mediaType: 'image',
|
|
300
|
+
data: url,
|
|
287
301
|
}));
|
|
288
302
|
}
|
|
289
303
|
get textMessage() {
|
|
304
|
+
if (this.promptBuilder?.user) {
|
|
305
|
+
return {
|
|
306
|
+
type: 'text',
|
|
307
|
+
text: this.promptBuilder.user(this.promptContext),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
290
310
|
return {
|
|
291
311
|
type: 'text',
|
|
292
312
|
text: `## Analysis Task
|
|
@@ -324,9 +344,11 @@ class ClassifyTags extends LLMQuery {
|
|
|
324
344
|
tag2: z.string(),
|
|
325
345
|
tag3: z.string(),
|
|
326
346
|
});
|
|
347
|
+
promptBuilder;
|
|
327
348
|
existTags = [];
|
|
328
349
|
constructor(config) {
|
|
329
350
|
super(config);
|
|
351
|
+
this.promptBuilder = config.promptBuilder;
|
|
330
352
|
}
|
|
331
353
|
async execute({ existTags }) {
|
|
332
354
|
this.existTags = existTags;
|
|
@@ -334,12 +356,23 @@ class ClassifyTags extends LLMQuery {
|
|
|
334
356
|
model: this.model,
|
|
335
357
|
maxRetries: this.options.llm.maxRetries,
|
|
336
358
|
schema: this.schema,
|
|
337
|
-
|
|
359
|
+
instructions: this.systemPrompt,
|
|
338
360
|
prompt: this.userPrompt,
|
|
339
361
|
});
|
|
340
362
|
return { result: output, usage };
|
|
341
363
|
}
|
|
364
|
+
get promptContext() {
|
|
365
|
+
return {
|
|
366
|
+
expertFields: this.expertFields,
|
|
367
|
+
outputLanguage: this.options.content.outputLanguage,
|
|
368
|
+
targetArticle: this.targetArticle,
|
|
369
|
+
existTags: this.existTags,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
342
372
|
get systemPrompt() {
|
|
373
|
+
if (this.promptBuilder?.system) {
|
|
374
|
+
return this.promptBuilder.system(this.promptContext);
|
|
375
|
+
}
|
|
343
376
|
return `You are an AI specializing in analyzing and categorizing articles for professionals in ${this.expertFields.join(', ')}.
|
|
344
377
|
|
|
345
378
|
## Core Responsibility
|
|
@@ -353,7 +386,7 @@ All classifications must be written in ${this.options.content.outputLanguage}.
|
|
|
353
386
|
2. **New Tag Criteria**: Create new classifications only when:
|
|
354
387
|
- Best existing match scores below 80% compatibility
|
|
355
388
|
- New tag demonstrates versatility across 10+ similar articles
|
|
356
|
-
3. **Naming Standards**:
|
|
389
|
+
3. **Naming Standards**:
|
|
357
390
|
- Length: 3-15 characters
|
|
358
391
|
- Style: Clear, intuitive ${this.options.content.outputLanguage} terms
|
|
359
392
|
- Balance industry precision with general reader comprehension
|
|
@@ -367,6 +400,9 @@ Prioritize in order:
|
|
|
367
400
|
- Long-term scalability and maintainability`;
|
|
368
401
|
}
|
|
369
402
|
get userPrompt() {
|
|
403
|
+
if (this.promptBuilder?.user) {
|
|
404
|
+
return this.promptBuilder.user(this.promptContext);
|
|
405
|
+
}
|
|
370
406
|
return `**Task**: Classify this article with 3 optimal detailed tags.
|
|
371
407
|
|
|
372
408
|
**Article Information**
|
|
@@ -404,17 +440,19 @@ class DetermineArticleImportance extends LLMQuery {
|
|
|
404
440
|
.describe('Article importance score (1-10, 10 is most important)'),
|
|
405
441
|
});
|
|
406
442
|
dateService;
|
|
443
|
+
promptBuilder;
|
|
407
444
|
constructor(config) {
|
|
408
445
|
super(config);
|
|
409
446
|
this.minimumImportanceScoreRules = config.minimumImportanceScoreRules ?? [];
|
|
410
447
|
this.dateService = config.dateService;
|
|
448
|
+
this.promptBuilder = config.promptBuilder;
|
|
411
449
|
}
|
|
412
450
|
async execute() {
|
|
413
451
|
const { output, usage } = await generateObjectByLLM({
|
|
414
452
|
model: this.model,
|
|
415
453
|
maxRetries: this.options.llm.maxRetries,
|
|
416
454
|
schema: this.schema,
|
|
417
|
-
|
|
455
|
+
instructions: this.systemPrompt,
|
|
418
456
|
prompt: this.userPrompt,
|
|
419
457
|
});
|
|
420
458
|
return { result: output.importanceScore, usage };
|
|
@@ -426,7 +464,18 @@ class DetermineArticleImportance extends LLMQuery {
|
|
|
426
464
|
get hasHigherMinimumScore() {
|
|
427
465
|
return this.minPoint > 1;
|
|
428
466
|
}
|
|
467
|
+
get promptContext() {
|
|
468
|
+
return {
|
|
469
|
+
expertFields: this.expertFields,
|
|
470
|
+
targetArticle: this.targetArticle,
|
|
471
|
+
dateService: this.dateService,
|
|
472
|
+
minimumImportanceScoreRules: this.minimumImportanceScoreRules,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
429
475
|
get systemPrompt() {
|
|
476
|
+
if (this.promptBuilder?.system) {
|
|
477
|
+
return this.promptBuilder.system(this.promptContext);
|
|
478
|
+
}
|
|
430
479
|
return `You are an expert in importance evaluation in the field of ${this.expertFields.join(', ')}.
|
|
431
480
|
|
|
432
481
|
Role:
|
|
@@ -463,6 +512,9 @@ Important Notes:
|
|
|
463
512
|
- Be sensitive to core keywords, events, policies considered important in the field.`;
|
|
464
513
|
}
|
|
465
514
|
get userPrompt() {
|
|
515
|
+
if (this.promptBuilder?.user) {
|
|
516
|
+
return this.promptBuilder.user(this.promptContext);
|
|
517
|
+
}
|
|
466
518
|
return `Please rate the importance of this article from ${this.minPoint} to 10.
|
|
467
519
|
|
|
468
520
|
**Newsletter Publication Date:** ${this.dateService.getPublicationISODateString()}${this.targetArticle.publishedDate
|
|
@@ -507,9 +559,11 @@ class PrivateChain extends BaseChain {
|
|
|
507
559
|
|
|
508
560
|
class ArticleInsightsChain extends PrivateChain {
|
|
509
561
|
dateService;
|
|
562
|
+
promptProvider;
|
|
510
563
|
constructor(config) {
|
|
511
564
|
super(config);
|
|
512
565
|
this.dateService = config.dateService;
|
|
566
|
+
this.promptProvider = config.promptProvider;
|
|
513
567
|
}
|
|
514
568
|
/* istanbul ignore next - pipeline arrow functions are exercised via higher-level tests */
|
|
515
569
|
get chain() {
|
|
@@ -605,7 +659,10 @@ class ArticleInsightsChain extends PrivateChain {
|
|
|
605
659
|
},
|
|
606
660
|
});
|
|
607
661
|
try {
|
|
608
|
-
const classifyTags = new ClassifyTags(
|
|
662
|
+
const classifyTags = new ClassifyTags({
|
|
663
|
+
...this.getLlmQueryConfig(this.provider.classifyTagOptions.model, article),
|
|
664
|
+
promptBuilder: this.promptProvider?.classifyTags,
|
|
665
|
+
});
|
|
609
666
|
const { result: generatedTags } = await classifyTags.execute({
|
|
610
667
|
existTags,
|
|
611
668
|
});
|
|
@@ -680,7 +737,10 @@ class ArticleInsightsChain extends PrivateChain {
|
|
|
680
737
|
},
|
|
681
738
|
});
|
|
682
739
|
try {
|
|
683
|
-
const analyzeImages = new AnalyzeImages(
|
|
740
|
+
const analyzeImages = new AnalyzeImages({
|
|
741
|
+
...this.getLlmQueryConfig(this.provider.analyzeImagesOptions.model, article),
|
|
742
|
+
promptBuilder: this.promptProvider?.analyzeImages,
|
|
743
|
+
});
|
|
684
744
|
const { result: imageContextByLlm } = await analyzeImages.execute();
|
|
685
745
|
if (imageContextByLlm) {
|
|
686
746
|
articlesWithImageContext.push({
|
|
@@ -779,6 +839,7 @@ class ArticleInsightsChain extends PrivateChain {
|
|
|
779
839
|
...this.getLlmQueryConfig(this.provider.determineScoreOptions.model, article),
|
|
780
840
|
minimumImportanceScoreRules: this.provider.determineScoreOptions.minimumImportanceScoreRules,
|
|
781
841
|
dateService: this.dateService,
|
|
842
|
+
promptBuilder: this.promptProvider?.determineImportance,
|
|
782
843
|
});
|
|
783
844
|
const { result: importanceScore } = await determineArticleImportance.execute();
|
|
784
845
|
const processedArticle = {
|
|
@@ -843,9 +904,11 @@ class ArticleInsightsChain extends PrivateChain {
|
|
|
843
904
|
|
|
844
905
|
class AnalysisChain extends Chain {
|
|
845
906
|
dateService;
|
|
907
|
+
promptProvider;
|
|
846
908
|
constructor(config) {
|
|
847
909
|
super(config);
|
|
848
910
|
this.dateService = config.dateService;
|
|
911
|
+
this.promptProvider = config.promptProvider;
|
|
849
912
|
}
|
|
850
913
|
get chain() {
|
|
851
914
|
return RunnablePassthrough.assign({
|
|
@@ -901,6 +964,7 @@ class AnalysisChain extends Chain {
|
|
|
901
964
|
options: this.options,
|
|
902
965
|
loggingExecutor: new LoggingExecutor(this.logger, this.taskId),
|
|
903
966
|
dateService: this.dateService,
|
|
967
|
+
promptProvider: this.promptProvider,
|
|
904
968
|
});
|
|
905
969
|
return await articleInsightsChain.generateInsights();
|
|
906
970
|
});
|
|
@@ -934,6 +998,7 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
|
|
|
934
998
|
dateService;
|
|
935
999
|
subscribePageUrl;
|
|
936
1000
|
newsletterBrandName;
|
|
1001
|
+
promptBuilder;
|
|
937
1002
|
schema = z.object({
|
|
938
1003
|
title: z.string().max(70).min(20).describe('Title of the newsletter email'),
|
|
939
1004
|
content: z.string().describe('Email content in markdown format'),
|
|
@@ -959,6 +1024,7 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
|
|
|
959
1024
|
this.dateService = config.dateService;
|
|
960
1025
|
this.subscribePageUrl = config.subscribePageUrl;
|
|
961
1026
|
this.newsletterBrandName = config.newsletterBrandName;
|
|
1027
|
+
this.promptBuilder = config.promptBuilder;
|
|
962
1028
|
}
|
|
963
1029
|
async execute() {
|
|
964
1030
|
const { output, usage, finishReason } = await generateObjectByLLM({
|
|
@@ -971,7 +1037,7 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
|
|
|
971
1037
|
presencePenalty: this.presencePenalty,
|
|
972
1038
|
frequencyPenalty: this.frequencyPenalty,
|
|
973
1039
|
schema: this.schema,
|
|
974
|
-
|
|
1040
|
+
instructions: this.systemPrompt,
|
|
975
1041
|
prompt: this.userPrompt,
|
|
976
1042
|
});
|
|
977
1043
|
if (finishReason === 'length') {
|
|
@@ -993,7 +1059,22 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
|
|
|
993
1059
|
}
|
|
994
1060
|
return { result: pick(output, ['title', 'content']), usage };
|
|
995
1061
|
}
|
|
1062
|
+
get promptContext() {
|
|
1063
|
+
return {
|
|
1064
|
+
expertFields: this.expertFields,
|
|
1065
|
+
outputLanguage: this.options.content.outputLanguage,
|
|
1066
|
+
dateService: this.dateService,
|
|
1067
|
+
targetArticles: this.targetArticles,
|
|
1068
|
+
freeFormIntro: this.options.content.freeFormIntro,
|
|
1069
|
+
titleContext: this.options.content.titleContext,
|
|
1070
|
+
subscribePageUrl: this.subscribePageUrl,
|
|
1071
|
+
newsletterBrandName: this.newsletterBrandName,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
996
1074
|
get systemPrompt() {
|
|
1075
|
+
if (this.promptBuilder?.system) {
|
|
1076
|
+
return this.promptBuilder.system(this.promptContext);
|
|
1077
|
+
}
|
|
997
1078
|
return `You are a newsletter production expert for "${this.newsletterBrandName}" who analyzes and delivers trends in the fields of ${this.expertFields.join(', ')}. Your goal is to provide in-depth analysis that helps industry professionals easily understand complex information and make informed decisions.
|
|
998
1079
|
|
|
999
1080
|
Important rule for displaying date ranges: When displaying date ranges, you must use a hyphen (-) instead of a tilde (~). For example, use 'June 1-2, 2025' instead of 'June 1~2, 2025'. The tilde (~) can be rendered as strikethrough in markdown.
|
|
@@ -1125,6 +1206,9 @@ Output Format & Requirements:
|
|
|
1125
1206
|
${this.subscribePageUrl ? `- Add \`${this.subscribePageUrl}\`(Subscribe to ${this.newsletterBrandName}) page access link button at appropriate attention-worthy spot for natural recommendation to others.` : ''}`;
|
|
1126
1207
|
}
|
|
1127
1208
|
get userPrompt() {
|
|
1209
|
+
if (this.promptBuilder?.user) {
|
|
1210
|
+
return this.promptBuilder.user(this.promptContext);
|
|
1211
|
+
}
|
|
1128
1212
|
return `Below is the complete list of newly collected ${this.expertFields.join(', ')} related news:
|
|
1129
1213
|
|
|
1130
1214
|
${this.targetArticles
|
|
@@ -1192,8 +1276,7 @@ function addUsage(a, b) {
|
|
|
1192
1276
|
};
|
|
1193
1277
|
}
|
|
1194
1278
|
|
|
1195
|
-
|
|
1196
|
-
const { default: juice } = await import('juice');
|
|
1279
|
+
function inlineCss(html) {
|
|
1197
1280
|
return juice(html);
|
|
1198
1281
|
}
|
|
1199
1282
|
class ContentGenerateChain extends Chain {
|
|
@@ -1201,9 +1284,11 @@ class ContentGenerateChain extends Chain {
|
|
|
1201
1284
|
minimumArticleCountForIssue;
|
|
1202
1285
|
priorityArticleScoreThreshold;
|
|
1203
1286
|
htmlTemplate;
|
|
1287
|
+
promptProvider;
|
|
1204
1288
|
constructor(config) {
|
|
1205
1289
|
super(config);
|
|
1206
1290
|
this.dateService = config.dateService;
|
|
1291
|
+
this.promptProvider = config.promptProvider;
|
|
1207
1292
|
this.minimumArticleCountForIssue =
|
|
1208
1293
|
config.provider.publicationCriteria?.minimumArticleCountForIssue ?? 5;
|
|
1209
1294
|
this.priorityArticleScoreThreshold =
|
|
@@ -1286,6 +1371,7 @@ class ContentGenerateChain extends Chain {
|
|
|
1286
1371
|
subscribePageUrl: this.provider.subscribePageUrl,
|
|
1287
1372
|
newsletterBrandName: this.provider.newsletterBrandName,
|
|
1288
1373
|
dateService: this.dateService,
|
|
1374
|
+
promptBuilder: this.promptProvider?.generateNewsletter,
|
|
1289
1375
|
});
|
|
1290
1376
|
const { result } = await generateNewsletter.execute();
|
|
1291
1377
|
return result;
|
|
@@ -1325,7 +1411,7 @@ class ContentGenerateChain extends Chain {
|
|
|
1325
1411
|
const { id } = await this.provider.saveNewsletter({
|
|
1326
1412
|
newsletter: {
|
|
1327
1413
|
...coreContent,
|
|
1328
|
-
htmlBody:
|
|
1414
|
+
htmlBody: inlineCss(html),
|
|
1329
1415
|
issueOrder: this.provider.issueOrder,
|
|
1330
1416
|
date: this.dateService.getPublicationISODateString(),
|
|
1331
1417
|
},
|
|
@@ -1775,6 +1861,7 @@ class GenerateNewsletter {
|
|
|
1775
1861
|
contentGenerateProvider;
|
|
1776
1862
|
logger;
|
|
1777
1863
|
options;
|
|
1864
|
+
promptProvider;
|
|
1778
1865
|
previewNewsletterOptions;
|
|
1779
1866
|
/** Independent internal field **/
|
|
1780
1867
|
taskId = null;
|
|
@@ -1818,6 +1905,7 @@ class GenerateNewsletter {
|
|
|
1818
1905
|
...config.options?.chain,
|
|
1819
1906
|
},
|
|
1820
1907
|
};
|
|
1908
|
+
this.promptProvider = config.promptProvider;
|
|
1821
1909
|
// Default logger (no-op)
|
|
1822
1910
|
this.logger = config.options?.logger ?? {
|
|
1823
1911
|
info: (_msg) => { },
|
|
@@ -1847,6 +1935,7 @@ class GenerateNewsletter {
|
|
|
1847
1935
|
options: this.options,
|
|
1848
1936
|
loggingExecutor,
|
|
1849
1937
|
dateService: this.dateService,
|
|
1938
|
+
promptProvider: this.promptProvider?.analysis,
|
|
1850
1939
|
});
|
|
1851
1940
|
const contentGenerateChain = new ContentGenerateChain({
|
|
1852
1941
|
logger: this.logger,
|
|
@@ -1855,6 +1944,7 @@ class GenerateNewsletter {
|
|
|
1855
1944
|
options: this.options,
|
|
1856
1945
|
loggingExecutor,
|
|
1857
1946
|
dateService: this.dateService,
|
|
1947
|
+
promptProvider: this.promptProvider?.contentGenerate,
|
|
1858
1948
|
});
|
|
1859
1949
|
const taskChain = RunnableSequence.from([
|
|
1860
1950
|
crawlingChain.chain,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|