@promptbook/cli 0.104.0-3 → 0.104.0-4

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.
Files changed (27) hide show
  1. package/apps/agents-server/src/database/migrate.ts +34 -1
  2. package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
  3. package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
  4. package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
  5. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
  6. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
  7. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
  8. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
  9. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
  10. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
  11. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
  12. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
  13. package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
  14. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +43 -0
  15. package/apps/agents-server/src/message-providers/index.ts +13 -0
  16. package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
  17. package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
  18. package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
  19. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +6 -2
  20. package/esm/index.es.js +32 -2
  21. package/esm/index.es.js.map +1 -1
  22. package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
  23. package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
  24. package/esm/typings/src/version.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/umd/index.umd.js +32 -2
  27. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { stringifyEmailAddresses } from './stringifyEmailAddresses';
3
+
4
+ describe('how stringifyEmailAddresses works', () => {
5
+ it('should work with single email', () => {
6
+ expect(
7
+ stringifyEmailAddresses([
8
+ {
9
+ fullName: null,
10
+ baseEmail: 'pavol@webgpt.cz',
11
+ fullEmail: 'pavol@webgpt.cz',
12
+ plus: [],
13
+ },
14
+ ]),
15
+ ).toEqual('pavol@webgpt.cz');
16
+ });
17
+
18
+ it('should work with simple emails', () => {
19
+ expect(
20
+ stringifyEmailAddresses([
21
+ {
22
+ fullName: null,
23
+ baseEmail: 'pavol@webgpt.cz',
24
+ fullEmail: 'pavol@webgpt.cz',
25
+ plus: [],
26
+ },
27
+ {
28
+ fullName: null,
29
+ baseEmail: 'jirka@webgpt.cz',
30
+ fullEmail: 'jirka@webgpt.cz',
31
+ plus: [],
32
+ },
33
+ {
34
+ fullName: null,
35
+ baseEmail: 'tomas@webgpt.cz',
36
+ fullEmail: 'tomas@webgpt.cz',
37
+ plus: [],
38
+ },
39
+ ]),
40
+ ).toEqual('pavol@webgpt.cz, jirka@webgpt.cz, tomas@webgpt.cz');
41
+ });
42
+
43
+ it('should work with fullname', () => {
44
+ expect(
45
+ stringifyEmailAddresses([
46
+ {
47
+ fullName: 'Pavol Hejný',
48
+ baseEmail: 'pavol@webgpt.cz',
49
+ fullEmail: 'pavol@webgpt.cz',
50
+ plus: [],
51
+ },
52
+ {
53
+ fullName: 'Jiří Jahn',
54
+ baseEmail: 'jirka@webgpt.cz',
55
+ fullEmail: 'jirka@webgpt.cz',
56
+ plus: [],
57
+ },
58
+ {
59
+ fullName: 'Tomáš Studeník',
60
+ baseEmail: 'tomas@webgpt.cz',
61
+ fullEmail: 'tomas@webgpt.cz',
62
+ plus: [],
63
+ },
64
+ ]),
65
+ ).toEqual('"Pavol Hejný" <pavol@webgpt.cz>, "Jiří Jahn" <jirka@webgpt.cz>, "Tomáš Studeník" <tomas@webgpt.cz>');
66
+ });
67
+
68
+ // TODO: [🎾] Implement and test here escaping
69
+ });
70
+
71
+
72
+ /**
73
+ * TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
74
+ */
@@ -0,0 +1,14 @@
1
+ import type { string_emails } from '@promptbook-local/types';
2
+ import type { EmailAddress } from '../Email';
3
+ import { stringifyEmailAddress } from './stringifyEmailAddress';
4
+
5
+ /**
6
+ * Makes string email from multiple EmailAddress
7
+ */
8
+ export function stringifyEmailAddresses(emailAddresses: Array<EmailAddress>): string_emails {
9
+ return emailAddresses.map((emailAddress) => stringifyEmailAddress(emailAddress)).join(', ');
10
+ }
11
+
12
+ /**
13
+ * TODO: [🎾] Implement and test here escaping
14
+ */
@@ -0,0 +1,44 @@
1
+ import { removeMarkdownFormatting } from '@promptbook-local/markdown-utils';
2
+ import type { really_any } from '@promptbook-local/types';
3
+ import sendgridEmailClient from '@sendgrid/mail';
4
+ import { marked } from 'marked';
5
+ import { MessageProvider } from '../../interfaces/MessageProvider';
6
+ import { OutboundEmail } from '../_common/Email';
7
+ import { parseEmailAddress } from '../_common/utils/parseEmailAddress';
8
+
9
+ export class SendgridMessageProvider implements MessageProvider {
10
+ constructor(private readonly apiKey: string) {
11
+ sendgridEmailClient.setApiKey(this.apiKey);
12
+ }
13
+
14
+ public async send(message: OutboundEmail): Promise<really_any> {
15
+ const sender = message.sender;
16
+ const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
17
+ Boolean,
18
+ ) as really_any[];
19
+
20
+ const text = removeMarkdownFormatting(message.content);
21
+ const html = await marked.parse(message.content);
22
+
23
+ const { fullEmail, fullName } = parseEmailAddress(sender);
24
+
25
+ const response = await sendgridEmailClient.send({
26
+ from: {
27
+ email: fullEmail,
28
+ name: fullName || undefined,
29
+ },
30
+ to: recipients.map((r) => {
31
+ const { fullEmail, fullName } = parseEmailAddress(r.email || r.baseEmail || r);
32
+ return {
33
+ email: fullEmail,
34
+ name: r.name || fullName || undefined,
35
+ };
36
+ }),
37
+ subject: message.metadata?.subject || 'No Subject',
38
+ text,
39
+ html,
40
+ });
41
+
42
+ return response;
43
+ }
44
+ }
@@ -0,0 +1,43 @@
1
+ import { removeMarkdownFormatting } from '@promptbook-local/markdown-utils';
2
+ import type { really_any } from '@promptbook-local/types';
3
+ import { marked } from 'marked';
4
+ // @ts-expect-error: Zeptomail types are not resolving correctly
5
+ import { SendMailClient } from 'zeptomail';
6
+ import { MessageProvider } from '../../interfaces/MessageProvider';
7
+ import { OutboundEmail } from '../_common/Email';
8
+
9
+ export class ZeptomailMessageProvider implements MessageProvider {
10
+ constructor(private readonly apiKey: string) {}
11
+
12
+ public async send(message: OutboundEmail): Promise<really_any> {
13
+ const client = new SendMailClient({ url: 'api.zeptomail.com/', token: this.apiKey });
14
+
15
+ const sender = message.sender as really_any;
16
+ const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
17
+ Boolean,
18
+ ) as really_any[];
19
+
20
+ const textbody = removeMarkdownFormatting(message.content);
21
+ const htmlbody = await marked.parse(message.content);
22
+
23
+ const response = await client.sendMail({
24
+ from: {
25
+ address: sender.email || sender.baseEmail || sender,
26
+ name: sender.name || sender.fullName || undefined,
27
+ },
28
+ to: recipients.map((r) => ({
29
+ email_address: {
30
+ address: r.email || r.baseEmail || r,
31
+ name: r.name || r.fullName || undefined,
32
+ },
33
+ })),
34
+ subject: message.metadata?.subject || 'No Subject',
35
+ textbody,
36
+ htmlbody,
37
+ track_clicks: true,
38
+ track_opens: true,
39
+ });
40
+
41
+ return response;
42
+ }
43
+ }
@@ -0,0 +1,13 @@
1
+ import { SendgridMessageProvider } from './email/sendgrid/SendgridMessageProvider';
2
+ import { ZeptomailMessageProvider } from './email/zeptomail/ZeptomailMessageProvider';
3
+ import { MessageProvider } from './interfaces/MessageProvider';
4
+
5
+ export const EMAIL_PROVIDERS: Record<string, MessageProvider> = {};
6
+
7
+ if (process.env.ZEPTOMAIL_API_KEY) {
8
+ EMAIL_PROVIDERS['ZEPTOMAIL'] = new ZeptomailMessageProvider(process.env.ZEPTOMAIL_API_KEY);
9
+ }
10
+
11
+ if (process.env.SENDGRID_API_KEY) {
12
+ EMAIL_PROVIDERS['SENDGRID'] = new SendgridMessageProvider(process.env.SENDGRID_API_KEY);
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { Message, really_any, string_email } from '@promptbook-local/types';
2
+
3
+ export type MessageProvider = {
4
+ /**
5
+ * Sends a message through the provider
6
+ *
7
+ * @param message The message to send
8
+ * @returns Raw response from the provider
9
+ */
10
+ send(message: Message<string_email>): Promise<really_any>;
11
+ };
@@ -0,0 +1,91 @@
1
+ import type { really_any } from '@promptbook-local/types';
2
+ import { $getTableName } from '../../database/$getTableName';
3
+ import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
4
+ import { EMAIL_PROVIDERS } from '../../message-providers';
5
+ import { OutboundEmail } from '../../message-providers/email/_common/Email';
6
+
7
+ /**
8
+ * Sends a message
9
+ */
10
+ export async function sendMessage(message: OutboundEmail): Promise<void> {
11
+ const supabase = await $provideSupabaseForServer();
12
+ // @ts-expect-error: Tables are not yet in types
13
+ const messageTable = await $getTableName('Message');
14
+ // @ts-expect-error: Tables are not yet in types
15
+ const messageSendAttemptTable = await $getTableName('MessageSendAttempt');
16
+
17
+ // 1. Insert message
18
+ const { data: insertedMessage, error: insertError } = await supabase
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ .from(messageTable as any)
21
+ .insert({
22
+ channel: message.channel || 'UNKNOWN',
23
+ direction: message.direction || 'OUTBOUND',
24
+ sender: message.sender,
25
+ recipients: message.recipients,
26
+ content: message.content,
27
+ threadId: message.threadId,
28
+ metadata: message.metadata,
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ } as any)
31
+ .select()
32
+ .single();
33
+
34
+ if (insertError) {
35
+ throw new Error(`Failed to insert message: ${insertError.message}`);
36
+ }
37
+
38
+ if (!insertedMessage) {
39
+ throw new Error('Failed to insert message: No data returned');
40
+ }
41
+
42
+ // 2. If outbound and email, try to send
43
+ if (message.direction === 'OUTBOUND' && message.channel === 'EMAIL') {
44
+ const providers = Object.keys(EMAIL_PROVIDERS);
45
+
46
+ if (providers.length === 0) {
47
+ console.warn('No email providers configured');
48
+ return;
49
+ }
50
+
51
+ let isSent = false;
52
+
53
+ for (const providerName of providers) {
54
+ const provider = EMAIL_PROVIDERS[providerName];
55
+ let isSuccessful = false;
56
+ let raw: really_any = null;
57
+
58
+ try {
59
+ raw = await provider.send(message);
60
+ isSuccessful = true;
61
+ isSent = true;
62
+ } catch (error) {
63
+ console.error(`Failed to send email via ${providerName}`, error);
64
+ raw = { error: error instanceof Error ? error.message : String(error) };
65
+ }
66
+
67
+ // 3. Log attempt
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ await supabase.from(messageSendAttemptTable as any).insert({
70
+ // @ts-expect-error: insertedMessage is any
71
+ messageId: insertedMessage.id,
72
+ providerName,
73
+ isSuccessful,
74
+ raw,
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ } as any);
77
+
78
+ if (isSuccessful) {
79
+ break;
80
+ }
81
+ }
82
+
83
+ if (!isSent) {
84
+ throw new Error('Failed to send email via any provider');
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * TODO: !!!! Move to `message-providers` and rename `message-providers` -> `messages`
91
+ */
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { filenameToPrompt } from './filenameToPrompt';
3
+
4
+ describe('how filenameToPrompt works', () => {
5
+ it('will convert filename with dashes', () => {
6
+ expect(filenameToPrompt('cat-sitting-on-keyboard.png')).toEqual('Cat sitting on keyboard');
7
+ expect(filenameToPrompt('hello-world.jpg')).toEqual('Hello world');
8
+ });
9
+
10
+ it('will convert filename with underscores', () => {
11
+ expect(filenameToPrompt('cat_sitting_on_keyboard.png')).toEqual('Cat sitting on keyboard');
12
+ expect(filenameToPrompt('hello_world.jpg')).toEqual('Hello world');
13
+ });
14
+
15
+ it('will convert filename with mixed separators', () => {
16
+ expect(filenameToPrompt('cat-sitting_on-keyboard.png')).toEqual('Cat sitting on keyboard');
17
+ });
18
+
19
+ it('will handle single word filename', () => {
20
+ expect(filenameToPrompt('cat.png')).toEqual('Cat');
21
+ expect(filenameToPrompt('HELLO.jpg')).toEqual('HELLO');
22
+ });
23
+
24
+ it('will handle filename without extension', () => {
25
+ expect(filenameToPrompt('cat-sitting-on-keyboard')).toEqual('Cat sitting on keyboard');
26
+ });
27
+
28
+ it('will handle filename with multiple dots', () => {
29
+ expect(filenameToPrompt('cat.sitting.on.keyboard.png')).toEqual('Cat.sitting.on.keyboard');
30
+ });
31
+
32
+ it('will handle capitalized words after first word', () => {
33
+ expect(filenameToPrompt('Cat-Sitting-On-Keyboard.png')).toEqual('Cat sitting on keyboard');
34
+ expect(filenameToPrompt('HELLO-WORLD.png')).toEqual('HELLO world');
35
+ });
36
+ });
@@ -1,4 +1,4 @@
1
- import { capitalize } from '../../../../../src/utils/normalization/capitalize';
1
+ import { capitalize } from '@promptbook/utils';
2
2
 
3
3
  /**
4
4
  * Converts a filename like "cat-sitting-on-keyboard.png" to a prompt like "Cat sitting on keyboard"
@@ -15,7 +15,11 @@ export function filenameToPrompt(filename: string): string {
15
15
 
16
16
  // Capitalize each word
17
17
  const words = withSpaces.split(' ');
18
- const capitalizedWords = words.map(word => capitalize(word));
18
+ const capitalizedWords = words.map((word, index) => (index === 0 ? capitalize(word) : word.toLowerCase()));
19
19
 
20
20
  return capitalizedWords.join(' ');
21
21
  }
22
+
23
+ /**
24
+ * TODO: [🧠][🏰] Make standard normalization function exported from `@promptbook/utils`
25
+ */
package/esm/index.es.js CHANGED
@@ -47,7 +47,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
47
47
  * @generated
48
48
  * @see https://github.com/webgptorg/promptbook
49
49
  */
50
- const PROMPTBOOK_ENGINE_VERSION = '0.104.0-3';
50
+ const PROMPTBOOK_ENGINE_VERSION = '0.104.0-4';
51
51
  /**
52
52
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
53
53
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -3721,6 +3721,7 @@ class RemoteLlmExecutionTools {
3721
3721
  }
3722
3722
  }
3723
3723
  /**
3724
+ * TODO: !!!! Deprecate pipeline server and all of its components
3724
3725
  * TODO: Maybe use `$exportJson`
3725
3726
  * TODO: [🧠][🛍] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
3726
3727
  * TODO: [🍓] Allow to list compatible models with each variant
@@ -4249,6 +4250,9 @@ function cacheLlmTools(llmTools, options = {}) {
4249
4250
  case 'EMBEDDING':
4250
4251
  promptResult = await llmTools.callEmbeddingModel(prompt);
4251
4252
  break variant;
4253
+ case 'IMAGE_GENERATION':
4254
+ promptResult = await llmTools.callImageGenerationModel(prompt);
4255
+ break variant;
4252
4256
  // <- case [🤖]:
4253
4257
  default:
4254
4258
  throw new PipelineExecutionError(`Unknown model variant "${prompt.modelRequirements.modelVariant}"`);
@@ -4336,6 +4340,11 @@ function cacheLlmTools(llmTools, options = {}) {
4336
4340
  return /* not await */ callCommonModel(prompt);
4337
4341
  };
4338
4342
  }
4343
+ if (llmTools.callImageGenerationModel !== undefined) {
4344
+ proxyTools.callImageGenerationModel = async (prompt) => {
4345
+ return /* not await */ callCommonModel(prompt);
4346
+ };
4347
+ }
4339
4348
  // <- Note: [🤖]
4340
4349
  return proxyTools;
4341
4350
  }
@@ -4524,6 +4533,15 @@ function countUsage(llmTools) {
4524
4533
  return promptResult;
4525
4534
  };
4526
4535
  }
4536
+ if (llmTools.callImageGenerationModel !== undefined) {
4537
+ proxyTools.callImageGenerationModel = async (prompt) => {
4538
+ // console.info('[🚕] callImageGenerationModel through countTotalUsage');
4539
+ const promptResult = await llmTools.callImageGenerationModel(prompt);
4540
+ totalUsage = addUsage(totalUsage, promptResult.usage);
4541
+ spending.next(promptResult.usage);
4542
+ return promptResult;
4543
+ };
4544
+ }
4527
4545
  // <- Note: [🤖]
4528
4546
  return proxyTools;
4529
4547
  }
@@ -4646,6 +4664,12 @@ class MultipleLlmExecutionTools {
4646
4664
  callEmbeddingModel(prompt) {
4647
4665
  return this.callCommonModel(prompt);
4648
4666
  }
4667
+ /**
4668
+ * Calls the best available embedding model
4669
+ */
4670
+ callImageGenerationModel(prompt) {
4671
+ return this.callCommonModel(prompt);
4672
+ }
4649
4673
  // <- Note: [🤖]
4650
4674
  /**
4651
4675
  * Calls the best available model
@@ -4672,6 +4696,11 @@ class MultipleLlmExecutionTools {
4672
4696
  continue llm;
4673
4697
  }
4674
4698
  return await llmExecutionTools.callEmbeddingModel(prompt);
4699
+ case 'IMAGE_GENERATION':
4700
+ if (llmExecutionTools.callImageGenerationModel === undefined) {
4701
+ continue llm;
4702
+ }
4703
+ return await llmExecutionTools.callImageGenerationModel(prompt);
4675
4704
  // <- case [🤖]:
4676
4705
  default:
4677
4706
  throw new UnexpectedError(`Unknown model variant "${prompt.modelRequirements.modelVariant}" in ${llmExecutionTools.title}`);
@@ -7594,8 +7623,9 @@ async function executeAttempts(options) {
7594
7623
  $ongoingTaskResult.$resultString = $ongoingTaskResult.$completionResult.content;
7595
7624
  break variant;
7596
7625
  case 'EMBEDDING':
7626
+ case 'IMAGE_GENERATION':
7597
7627
  throw new PipelineExecutionError(spaceTrim$1((block) => `
7598
- Embedding model can not be used in pipeline
7628
+ ${modelRequirements.modelVariant} model can not be used in pipeline
7599
7629
 
7600
7630
  This should be catched during parsing
7601
7631