@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 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+CJS+d.ts), Vitest 100% coverage, GitHub Actions CI included
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
- system: this.systemPrompt,
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: 'image',
286
- image: url,
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
- system: this.systemPrompt,
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
- system: this.systemPrompt,
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(this.getLlmQueryConfig(this.provider.classifyTagOptions.model, article));
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(this.getLlmQueryConfig(this.provider.analyzeImagesOptions.model, article));
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
- system: this.systemPrompt,
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
- async function inlineCss(html) {
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: await inlineCss(html),
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}