@promptbook/remote-server 0.105.0-21 → 0.105.0-23

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 (43) hide show
  1. package/esm/index.es.js +1050 -68
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/typings/src/_packages/browser.index.d.ts +2 -0
  4. package/esm/typings/src/_packages/core.index.d.ts +9 -13
  5. package/esm/typings/src/_packages/node.index.d.ts +2 -0
  6. package/esm/typings/src/_packages/types.index.d.ts +12 -2
  7. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -1
  8. package/esm/typings/src/book-components/Chat/AgentChip/AgentChip.d.ts +67 -0
  9. package/esm/typings/src/book-components/Chat/AgentChip/index.d.ts +2 -0
  10. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +15 -0
  11. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +10 -0
  12. package/esm/typings/src/book-components/Chat/LlmChat/FriendlyErrorMessage.d.ts +20 -0
  13. package/esm/typings/src/book-components/Chat/LlmChat/LlmChatProps.d.ts +8 -0
  14. package/esm/typings/src/book-components/Chat/SourceChip/SourceChip.d.ts +35 -0
  15. package/esm/typings/src/book-components/Chat/SourceChip/index.d.ts +2 -0
  16. package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +21 -0
  17. package/esm/typings/src/book-components/Chat/utils/getToolCallChipletText.d.ts +21 -0
  18. package/esm/typings/src/book-components/Chat/utils/parseCitationsFromContent.d.ts +53 -0
  19. package/esm/typings/src/commitments/TEMPLATE/TEMPLATE.d.ts +44 -0
  20. package/esm/typings/src/commitments/TEMPLATE/TEMPLATE.test.d.ts +1 -0
  21. package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.d.ts +16 -2
  22. package/esm/typings/src/commitments/USE_BROWSER/fetchUrlContent.d.ts +22 -0
  23. package/esm/typings/src/commitments/USE_BROWSER/fetchUrlContentViaBrowser.d.ts +13 -0
  24. package/esm/typings/src/commitments/USE_EMAIL/USE_EMAIL.d.ts +47 -0
  25. package/esm/typings/src/commitments/_common/getAllCommitmentDefinitions.d.ts +8 -0
  26. package/esm/typings/src/commitments/_common/getAllCommitmentTypes.d.ts +8 -0
  27. package/esm/typings/src/commitments/_common/getAllCommitmentsToolFunctionsForBrowser.d.ts +10 -0
  28. package/esm/typings/src/commitments/_common/getAllCommitmentsToolFunctionsForNode.d.ts +14 -0
  29. package/esm/typings/src/commitments/_common/getAllCommitmentsToolTitles.d.ts +7 -0
  30. package/esm/typings/src/commitments/_common/getCommitmentDefinition.d.ts +10 -0
  31. package/esm/typings/src/commitments/_common/getGroupedCommitmentDefinitions.d.ts +17 -0
  32. package/esm/typings/src/commitments/_common/isCommitmentSupported.d.ts +9 -0
  33. package/esm/typings/src/commitments/index.d.ts +3 -64
  34. package/esm/typings/src/executables/$provideExecutablesForNode.d.ts +1 -0
  35. package/esm/typings/src/execution/utils/$provideExecutionToolsForNode.d.ts +1 -0
  36. package/esm/typings/src/scrapers/_common/register/$provideFilesystemForNode.d.ts +1 -0
  37. package/esm/typings/src/scrapers/_common/register/$provideScrapersForNode.d.ts +1 -0
  38. package/esm/typings/src/scrapers/_common/register/$provideScriptingForNode.d.ts +1 -0
  39. package/esm/typings/src/version.d.ts +1 -1
  40. package/esm/typings/src/wizard/wizard.d.ts +1 -4
  41. package/package.json +5 -2
  42. package/umd/index.umd.js +1050 -71
  43. package/umd/index.umd.js.map +1 -1
package/esm/index.es.js CHANGED
@@ -9,13 +9,16 @@ import http from 'http';
9
9
  import { Server } from 'socket.io';
10
10
  import swaggerUi from 'swagger-ui-express';
11
11
  import { randomBytes } from 'crypto';
12
- import { stat, access, constants, readFile, writeFile, readdir, mkdir, watch } from 'fs/promises';
12
+ import { stat, access, constants, readFile, writeFile, readdir, mkdir, watch, rm } from 'fs/promises';
13
13
  import { Subject } from 'rxjs';
14
14
  import hexEncoder from 'crypto-js/enc-hex';
15
15
  import sha256 from 'crypto-js/sha256';
16
16
  import { SHA256 } from 'crypto-js';
17
17
  import { lookup, extension } from 'mime-types';
18
18
  import { parse, unparse } from 'papaparse';
19
+ import { Readability } from '@mozilla/readability';
20
+ import { JSDOM } from 'jsdom';
21
+ import { Converter } from 'showdown';
19
22
  import moment from 'moment';
20
23
  import { createElement } from 'react';
21
24
  import { renderToStaticMarkup } from 'react-dom/server';
@@ -34,7 +37,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
34
37
  * @generated
35
38
  * @see https://github.com/webgptorg/promptbook
36
39
  */
37
- const PROMPTBOOK_ENGINE_VERSION = '0.105.0-21';
40
+ const PROMPTBOOK_ENGINE_VERSION = '0.105.0-23';
38
41
  /**
39
42
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
40
43
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1918,6 +1921,7 @@ function $provideFilesystemForNode(options) {
1918
1921
  }
1919
1922
  /**
1920
1923
  * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
1924
+ * TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
1921
1925
  */
1922
1926
 
1923
1927
  /**
@@ -2104,6 +2108,7 @@ async function $provideExecutablesForNode(options) {
2104
2108
  /**
2105
2109
  * TODO: [🧠] Allow to override the executables without need to call `locatePandoc` / `locateLibreoffice` in case of provided
2106
2110
  * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
2111
+ * TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
2107
2112
  */
2108
2113
 
2109
2114
  /**
@@ -7977,6 +7982,7 @@ async function $provideScrapersForNode(tools, options) {
7977
7982
  }
7978
7983
  /**
7979
7984
  * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
7985
+ * TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
7980
7986
  */
7981
7987
 
7982
7988
  /**
@@ -8275,6 +8281,536 @@ function isValidAgentUrl(url) {
8275
8281
  * TODO: [🐠] Maybe more info why the URL is invalid
8276
8282
  */
8277
8283
 
8284
+ /**
8285
+ * Retrieves an intermediate source for a scraper based on the knowledge source.
8286
+ * Manages the caching and retrieval of intermediate scraper results for optimized performance.
8287
+ *
8288
+ * @private as internal utility for scrapers
8289
+ */
8290
+ async function getScraperIntermediateSource(source, options) {
8291
+ const { filename: sourceFilename, url } = source;
8292
+ const { rootDirname, cacheDirname, intermediateFilesStrategy, extension, isVerbose } = options;
8293
+ // TODO: [đŸ‘Ŧ] DRY
8294
+ const hash = SHA256(
8295
+ // <- TODO: [đŸĨŦ] Encapsulate sha256 to some private utility function
8296
+ hexEncoder.parse(sourceFilename || url || 'untitled'))
8297
+ .toString( /* hex */)
8298
+ .substring(0, 20);
8299
+ // <- TODO: [đŸĨŦ] Make some system for hashes and ids of promptbook
8300
+ const semanticName = normalizeToKebabCase(titleToName((sourceFilename || url || '').split('intermediate').join(''))).substring(0, 20);
8301
+ // <- TODO: [🐱‍🐉]
8302
+ const pieces = ['intermediate', semanticName, hash].filter((piece) => piece !== '');
8303
+ const name = pieces.join('-').split('--').join('-');
8304
+ const cacheFilename = join(process.cwd(), cacheDirname, ...nameToSubfolderPath(hash /* <- TODO: [🎎] Maybe add some SHA256 prefix */), name)
8305
+ .split('\\')
8306
+ .join('/') +
8307
+ '.' +
8308
+ extension;
8309
+ // Note: Try to create cache directory, but don't fail if filesystem has issues
8310
+ try {
8311
+ await mkdir(dirname(cacheFilename), { recursive: true });
8312
+ }
8313
+ catch (error) {
8314
+ // Note: If we can't create cache directory, continue without it
8315
+ // This handles read-only filesystems, permission issues, and missing parent directories
8316
+ if (error instanceof Error &&
8317
+ (error.message.includes('EROFS') ||
8318
+ error.message.includes('read-only') ||
8319
+ error.message.includes('EACCES') ||
8320
+ error.message.includes('EPERM') ||
8321
+ error.message.includes('ENOENT'))) ;
8322
+ else {
8323
+ // Re-throw other unexpected errors
8324
+ throw error;
8325
+ }
8326
+ }
8327
+ let isDestroyed = true;
8328
+ const fileHandler = {
8329
+ filename: cacheFilename,
8330
+ get isDestroyed() {
8331
+ return isDestroyed;
8332
+ },
8333
+ async destroy() {
8334
+ if (intermediateFilesStrategy === 'HIDE_AND_CLEAN') {
8335
+ if (isVerbose) {
8336
+ console.info('legacyDocumentScraper: Clening cache');
8337
+ }
8338
+ await rm(cacheFilename);
8339
+ // TODO: [đŸŋ][🧠] Maybe remove empty folders
8340
+ }
8341
+ isDestroyed = true;
8342
+ },
8343
+ };
8344
+ return fileHandler;
8345
+ }
8346
+ /**
8347
+ * Note: Not using `FileCacheStorage` for two reasons:
8348
+ * 1) Need to store more than serialized JSONs
8349
+ * 2) Need to switch between a `rootDirname` and `cacheDirname` <- TODO: [😡]
8350
+ * TODO: [🐱‍🐉][🧠] Make some smart crop
8351
+ * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
8352
+ */
8353
+
8354
+ /**
8355
+ * Metadata of the scraper
8356
+ *
8357
+ * @private within the scraper directory
8358
+ */
8359
+ const markdownScraperMetadata = $deepFreeze({
8360
+ title: 'Markdown scraper',
8361
+ packageName: '@promptbook/markdown-utils',
8362
+ className: 'MarkdownScraper',
8363
+ mimeTypes: ['text/markdown', 'text/plain'],
8364
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/@@',
8365
+ isAvailableInBrowser: true,
8366
+ // <- Note: [🌏] This is the only scraper which makes sense to be available in the browser, for scraping non-markdown sources in the browser use a remote server
8367
+ requiredExecutables: [],
8368
+ }); /* <- Note: [🤛] */
8369
+ /**
8370
+ * Registration of known scraper metadata
8371
+ *
8372
+ * Warning: This is not useful for the end user, it is just a side effect of the mechanism that handles all available known scrapers
8373
+ *
8374
+ * @public exported from `@promptbook/core`
8375
+ * @public exported from `@promptbook/wizard`
8376
+ * @public exported from `@promptbook/cli`
8377
+ */
8378
+ $scrapersMetadataRegister.register(markdownScraperMetadata);
8379
+ /**
8380
+ * Note: [💞] Ignore a discrepancy between file name and entity name
8381
+ */
8382
+
8383
+ /**
8384
+ * Scraper for markdown files
8385
+ *
8386
+ * @see `documentationUrl` for more details
8387
+ * @public exported from `@promptbook/markdown-utils`
8388
+ */
8389
+ class MarkdownScraper {
8390
+ /**
8391
+ * Metadata of the scraper which includes title, mime types, etc.
8392
+ */
8393
+ get metadata() {
8394
+ return markdownScraperMetadata;
8395
+ }
8396
+ constructor(tools, options) {
8397
+ this.tools = tools;
8398
+ this.options = options;
8399
+ }
8400
+ /**
8401
+ * Scrapes the markdown file and returns the knowledge pieces or `null` if it can't scrape it
8402
+ */
8403
+ async scrape(source) {
8404
+ const { maxParallelCount = DEFAULT_MAX_PARALLEL_COUNT, isVerbose = DEFAULT_IS_VERBOSE } = this.options;
8405
+ const { llm } = this.tools;
8406
+ if (llm === undefined) {
8407
+ throw new MissingToolsError('LLM tools are required for scraping external files');
8408
+ // <- Note: This scraper is used in all other scrapers, so saying "external files" not "markdown files"
8409
+ }
8410
+ const llmTools = getSingleLlmExecutionTools(llm);
8411
+ // TODO: [đŸŒŧ] In future use `ptbk make` and made getPipelineCollection
8412
+ const collection = createPipelineCollectionFromJson(...PipelineCollection);
8413
+ const prepareKnowledgeFromMarkdownExecutor = createPipelineExecutor({
8414
+ pipeline: await collection.getPipelineByUrl('https://promptbook.studio/promptbook/prepare-knowledge-from-markdown.book'),
8415
+ tools: {
8416
+ llm: llm,
8417
+ },
8418
+ });
8419
+ const prepareTitleExecutor = createPipelineExecutor({
8420
+ pipeline: await collection.getPipelineByUrl('https://promptbook.studio/promptbook/prepare-knowledge-title.book'),
8421
+ tools: {
8422
+ llm: llm,
8423
+ },
8424
+ });
8425
+ const prepareKeywordsExecutor = createPipelineExecutor({
8426
+ pipeline: await collection.getPipelineByUrl('https://promptbook.studio/promptbook/prepare-knowledge-keywords.book'),
8427
+ tools: {
8428
+ llm: llm,
8429
+ },
8430
+ });
8431
+ const knowledgeContent = await source.asText();
8432
+ const result = await prepareKnowledgeFromMarkdownExecutor({ knowledgeContent }).asPromise({
8433
+ isCrashedOnError: true,
8434
+ });
8435
+ const { outputParameters } = result;
8436
+ const { knowledgePieces: knowledgePiecesRaw } = outputParameters;
8437
+ const knowledgeTextPieces = (knowledgePiecesRaw || '').split('\n---\n');
8438
+ // <- TODO: [main] Smarter split and filter out empty pieces
8439
+ if (isVerbose) {
8440
+ console.info('knowledgeTextPieces:', knowledgeTextPieces);
8441
+ }
8442
+ // const usage = ;
8443
+ const knowledge = await Promise.all(
8444
+ // TODO: [đŸĒ‚] Do not send all at once but in chunks
8445
+ knowledgeTextPieces.map(async (knowledgeTextPiece, i) => {
8446
+ // Note: These are just default values, they will be overwritten by the actual values:
8447
+ let name = `piece-${i}`;
8448
+ let title = spaceTrim$2(knowledgeTextPiece.substring(0, 100));
8449
+ const knowledgePieceContent = spaceTrim$2(knowledgeTextPiece);
8450
+ let keywords = [];
8451
+ const index = [];
8452
+ /*
8453
+ TODO: [☀] Track line and column of the source
8454
+ const sources: KnowledgePiecePreparedJson['sources'] = [
8455
+ ];
8456
+ */
8457
+ try {
8458
+ const titleResult = await prepareTitleExecutor({ knowledgePieceContent }).asPromise({
8459
+ isCrashedOnError: true,
8460
+ });
8461
+ const { title: titleRaw = 'Untitled' } = titleResult.outputParameters;
8462
+ title = spaceTrim$2(titleRaw) /* <- TODO: Maybe do in pipeline */;
8463
+ name = titleToName(title);
8464
+ // --- Keywords
8465
+ const keywordsResult = await prepareKeywordsExecutor({ knowledgePieceContent }).asPromise({
8466
+ isCrashedOnError: true,
8467
+ });
8468
+ const { keywords: keywordsRaw = '' } = keywordsResult.outputParameters;
8469
+ keywords = (keywordsRaw || '')
8470
+ .split(',')
8471
+ .map((keyword) => keyword.trim())
8472
+ .filter((keyword) => keyword !== '');
8473
+ if (isVerbose) {
8474
+ console.info(`Keywords for "${title}":`, keywords);
8475
+ }
8476
+ // ---
8477
+ if (!llmTools.callEmbeddingModel) {
8478
+ // TODO: [đŸŸĨ] Detect browser / node and make it colorful
8479
+ console.error('No callEmbeddingModel function provided');
8480
+ }
8481
+ else {
8482
+ // TODO: [🧠][🎛] Embedding via multiple models
8483
+ const embeddingResult = await llmTools.callEmbeddingModel({
8484
+ title: `Embedding for ${title}` /* <- Note: No impact on embedding result itself, just for logging */,
8485
+ parameters: {},
8486
+ content: knowledgePieceContent,
8487
+ modelRequirements: {
8488
+ modelVariant: 'EMBEDDING',
8489
+ },
8490
+ });
8491
+ index.push({
8492
+ modelName: embeddingResult.modelName,
8493
+ position: [...embeddingResult.content],
8494
+ // <- TODO: [đŸĒ“] Here should be no need for spreading new array, just `position: embeddingResult.content`
8495
+ });
8496
+ }
8497
+ }
8498
+ catch (error) {
8499
+ // Note: Here is expected error:
8500
+ // > PipelineExecutionError: You have not provided any `LlmExecutionTools` that support model variant "EMBEDDING
8501
+ if (!(error instanceof PipelineExecutionError)) {
8502
+ throw error;
8503
+ }
8504
+ // TODO: [đŸŸĨ] Detect browser / node and make it colorful
8505
+ console.error(error, "<- Note: This error is not critical to prepare the pipeline, just knowledge pieces won't have embeddings");
8506
+ }
8507
+ return {
8508
+ name,
8509
+ title,
8510
+ content: knowledgePieceContent,
8511
+ keywords,
8512
+ index,
8513
+ // <- TODO: [☀] sources,
8514
+ };
8515
+ }));
8516
+ return knowledge;
8517
+ }
8518
+ }
8519
+ /**
8520
+ * TODO: [đŸĒ‚] Do it in parallel 11:11
8521
+ * Note: No need to aggregate usage here, it is done by intercepting the llmTools
8522
+ */
8523
+
8524
+ /**
8525
+ * Metadata of the scraper
8526
+ *
8527
+ * @private within the scraper directory
8528
+ */
8529
+ const websiteScraperMetadata = $deepFreeze({
8530
+ title: 'Website scraper',
8531
+ packageName: '@promptbook/website-crawler',
8532
+ className: 'WebsiteScraper',
8533
+ mimeTypes: ['text/html'],
8534
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/@@',
8535
+ isAvailableInBrowser: false,
8536
+ // <- Note: [🌏] Only `MarkdownScraper` makes sense to be available in the browser, for scraping non-markdown sources in the browser use a remote server
8537
+ requiredExecutables: [],
8538
+ }); /* <- Note: [🤛] */
8539
+ /**
8540
+ * Registration of known scraper metadata
8541
+ *
8542
+ * Warning: This is not useful for the end user, it is just a side effect of the mechanism that handles all available known scrapers
8543
+ *
8544
+ * @public exported from `@promptbook/core`
8545
+ * @public exported from `@promptbook/wizard`
8546
+ * @public exported from `@promptbook/cli`
8547
+ */
8548
+ $scrapersMetadataRegister.register(websiteScraperMetadata);
8549
+ /**
8550
+ * Note: [💞] Ignore a discrepancy between file name and entity name
8551
+ */
8552
+
8553
+ /**
8554
+ * Create a new showdown converter instance
8555
+ *
8556
+ * @private utility of `WebsiteScraper`
8557
+ */
8558
+ function createShowdownConverter() {
8559
+ return new Converter({
8560
+ flavor: 'github',
8561
+ /*
8562
+ > import showdownHighlight from 'showdown-highlight';
8563
+ > extensions: [
8564
+ > showdownHighlight({
8565
+ > // Whether to add the classes to the <pre> tag, default is false
8566
+ > pre: true,
8567
+ > // Whether to use hljs' auto language detection, default is true
8568
+ > auto_detection: true,
8569
+ > }),
8570
+ > ],
8571
+ */
8572
+ });
8573
+ }
8574
+
8575
+ // TODO: [đŸŗâ€đŸŒˆ] Finally take pick of .json vs .ts
8576
+ /**
8577
+ * Scraper for websites
8578
+ *
8579
+ * @see `documentationUrl` for more details
8580
+ * @public exported from `@promptbook/website-crawler`
8581
+ */
8582
+ class WebsiteScraper {
8583
+ /**
8584
+ * Metadata of the scraper which includes title, mime types, etc.
8585
+ */
8586
+ get metadata() {
8587
+ return websiteScraperMetadata;
8588
+ }
8589
+ constructor(tools, options) {
8590
+ this.tools = tools;
8591
+ this.options = options;
8592
+ this.markdownScraper = new MarkdownScraper(tools, options);
8593
+ this.showdownConverter = createShowdownConverter();
8594
+ }
8595
+ /**
8596
+ * Convert the website to `.md` file and returns intermediate source
8597
+ *
8598
+ * Note: `$` is used to indicate that this function is not a pure function - it leaves files on the disk and you are responsible for cleaning them by calling `destroy` method of returned object
8599
+ */
8600
+ async $convert(source) {
8601
+ const {
8602
+ // TODO: [🧠] Maybe in node use headless browser not just JSDOM
8603
+ rootDirname = process.cwd(), cacheDirname = DEFAULT_SCRAPE_CACHE_DIRNAME, intermediateFilesStrategy = DEFAULT_INTERMEDIATE_FILES_STRATEGY, isVerbose = DEFAULT_IS_VERBOSE, } = this.options;
8604
+ if (source.url === null) {
8605
+ throw new KnowledgeScrapeError('Website scraper requires URL');
8606
+ }
8607
+ if (this.tools.fs === undefined) {
8608
+ throw new EnvironmentMismatchError('Can not scrape websites without filesystem tools');
8609
+ }
8610
+ const jsdom = new JSDOM(await source.asText(), {
8611
+ url: source.url,
8612
+ });
8613
+ const reader = new Readability(jsdom.window.document);
8614
+ const article = reader.parse();
8615
+ // console.log(article);
8616
+ // await forTime(10000);
8617
+ let html = (article === null || article === void 0 ? void 0 : article.content) || (article === null || article === void 0 ? void 0 : article.textContent) || jsdom.window.document.body.innerHTML;
8618
+ // Note: Unwrap html such as it is convertable by `markdownConverter`
8619
+ for (let i = 0; i < 2; i++) {
8620
+ html = html.replace(/<div\s*(?:id="readability-page-\d+"\s+class="page")?>(.*)<\/div>/is, '$1');
8621
+ }
8622
+ if (html.includes('<div')) {
8623
+ html = (article === null || article === void 0 ? void 0 : article.textContent) || '';
8624
+ }
8625
+ const cacheFilehandler = await getScraperIntermediateSource(source, {
8626
+ rootDirname,
8627
+ cacheDirname,
8628
+ intermediateFilesStrategy,
8629
+ extension: 'html',
8630
+ isVerbose,
8631
+ });
8632
+ // Note: Try to cache the scraped content, but don't fail if the filesystem is read-only
8633
+ try {
8634
+ await this.tools.fs.writeFile(cacheFilehandler.filename, html, 'utf-8');
8635
+ }
8636
+ catch (error) {
8637
+ // Note: If we can't write to cache, we'll continue without caching
8638
+ // This handles read-only filesystems like Vercel
8639
+ if (error instanceof Error &&
8640
+ (error.message.includes('EROFS') ||
8641
+ error.message.includes('read-only') ||
8642
+ error.message.includes('EACCES') ||
8643
+ error.message.includes('EPERM') ||
8644
+ error.message.includes('ENOENT'))) ;
8645
+ else {
8646
+ // Re-throw other unexpected errors
8647
+ throw error;
8648
+ }
8649
+ }
8650
+ const markdown = this.showdownConverter.makeMarkdown(html, jsdom.window.document);
8651
+ return { ...cacheFilehandler, markdown };
8652
+ }
8653
+ /**
8654
+ * Scrapes the website and returns the knowledge pieces or `null` if it can't scrape it
8655
+ */
8656
+ async scrape(source) {
8657
+ const cacheFilehandler = await this.$convert(source);
8658
+ const markdownSource = {
8659
+ source: source.source,
8660
+ filename: cacheFilehandler.filename,
8661
+ url: null,
8662
+ mimeType: 'text/markdown',
8663
+ asText() {
8664
+ return cacheFilehandler.markdown;
8665
+ },
8666
+ asJson() {
8667
+ throw new UnexpectedError('Did not expect that `markdownScraper` would need to get the content `asJson`');
8668
+ },
8669
+ /*
8670
+ TODO: [đŸĨŊ]
8671
+ > asBlob() {
8672
+ > throw new UnexpectedError(
8673
+ > 'Did not expect that `markdownScraper` would need to get the content `asBlob`',
8674
+ > );
8675
+ > },
8676
+ */
8677
+ };
8678
+ const knowledge = this.markdownScraper.scrape(markdownSource);
8679
+ await cacheFilehandler.destroy();
8680
+ return knowledge;
8681
+ }
8682
+ }
8683
+ /**
8684
+ * TODO: [đŸ‘Ŗ] Scraped website in .md can act as cache item - there is no need to run conversion each time
8685
+ * TODO: [đŸĒ‚] Do it in parallel 11:11
8686
+ * Note: No need to aggregate usage here, it is done by intercepting the llmTools
8687
+ * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
8688
+ */
8689
+
8690
+ /**
8691
+ * Fetches and scrapes content from a URL (SERVER-SIDE ONLY)
8692
+ *
8693
+ * This function:
8694
+ * 1. Fetches the URL content using promptbookFetch
8695
+ * 2. Determines the content type (HTML, PDF, etc.)
8696
+ * 3. Uses the appropriate scraper to convert to markdown
8697
+ * 4. Returns the scraped markdown content
8698
+ *
8699
+ * @param url The URL to fetch and scrape
8700
+ * @returns Markdown content from the URL
8701
+ *
8702
+ * @private internal utility for USE BROWSER commitment
8703
+ *
8704
+ * WARNING: This function should NOT be used directly in browser environments.
8705
+ * For browser environments, use fetchUrlContentViaBrowser which proxies through
8706
+ * the Agents Server API endpoint at /api/scrape
8707
+ */
8708
+ async function fetchUrlContent(url) {
8709
+ try {
8710
+ // Validate URL
8711
+ let parsedUrl;
8712
+ try {
8713
+ parsedUrl = new URL(url);
8714
+ }
8715
+ catch (error) {
8716
+ throw new Error(`Invalid URL: ${url}`);
8717
+ }
8718
+ // Fetch the URL content
8719
+ const response = await promptbookFetch(url);
8720
+ if (!response.ok) {
8721
+ throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
8722
+ }
8723
+ // Get content type
8724
+ const contentType = response.headers.get('content-type') || 'text/html';
8725
+ const content = await response.text();
8726
+ // Determine the appropriate scraper based on content type
8727
+ const mimeType = (contentType.split(';')[0] || 'text/html').trim();
8728
+ // Handle PDFs
8729
+ if (mimeType === 'application/pdf' || url.toLowerCase().endsWith('.pdf')) {
8730
+ // TODO: [🔰] Implement PDF scraping using PdfScraper
8731
+ // For now, return a message indicating PDF support is coming
8732
+ return spaceTrim$1(`
8733
+ # PDF Document: ${parsedUrl.pathname.split('/').pop()}
8734
+
8735
+ **Note:** PDF scraping is not yet fully implemented.
8736
+
8737
+ **URL:** ${url}
8738
+
8739
+ The PDF scraper will be integrated to provide full document content extraction.
8740
+ For now, please try to find an HTML version of this content or use a different source.
8741
+ `);
8742
+ }
8743
+ // Handle HTML/websites
8744
+ if (mimeType === 'text/html' || mimeType === 'application/xhtml+xml' || !mimeType.includes('/')) {
8745
+ // Create a scraper source handler
8746
+ const sourceHandler = {
8747
+ source: url,
8748
+ filename: null,
8749
+ url,
8750
+ mimeType: 'text/html',
8751
+ asText: () => content,
8752
+ asJson: () => {
8753
+ throw new Error('JSON conversion not supported for HTML content');
8754
+ },
8755
+ };
8756
+ // Use WebsiteScraper to convert HTML to markdown
8757
+ // Note: We need minimal tools for the scraper
8758
+ const scraper = new WebsiteScraper(// <- TODO: !!!!! Things marked with [đŸŸĸ] should never be never released in packages that could be imported into browser environment
8759
+ {
8760
+ // Minimal tools - fs and llm are optional for basic scraping
8761
+ fs: undefined,
8762
+ llm: undefined,
8763
+ }, {
8764
+ isVerbose: false,
8765
+ });
8766
+ // Convert to markdown
8767
+ const intermediateSource = await scraper.$convert(sourceHandler);
8768
+ const markdown = intermediateSource.markdown;
8769
+ // Clean up intermediate files
8770
+ await intermediateSource.destroy();
8771
+ // Add URL header to the content
8772
+ return spaceTrim$1(`
8773
+ # Content from: ${url}
8774
+
8775
+ ${markdown}
8776
+
8777
+ ---
8778
+
8779
+ *Source: ${url}*
8780
+ `);
8781
+ }
8782
+ // For other content types, return the raw content with a note
8783
+ return spaceTrim$1(`
8784
+ # Content from: ${url}
8785
+
8786
+ **Content Type:** ${contentType}
8787
+
8788
+ ${content}
8789
+
8790
+ ---
8791
+
8792
+ *Source: ${url}*
8793
+ `);
8794
+ }
8795
+ catch (error) {
8796
+ // Handle errors gracefully
8797
+ const errorMessage = error instanceof Error ? error.message : String(error);
8798
+ return spaceTrim$1(`
8799
+ # Error fetching content from URL
8800
+
8801
+ **URL:** ${url}
8802
+
8803
+ **Error:** ${errorMessage}
8804
+
8805
+ Unable to fetch and scrape the content from this URL.
8806
+ Please verify the URL is correct and accessible.
8807
+ `);
8808
+ }
8809
+ }
8810
+ /**
8811
+ * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
8812
+ */
8813
+
8278
8814
  /**
8279
8815
  * Generates a regex pattern to match a specific commitment
8280
8816
  *
@@ -11650,6 +12186,129 @@ function stripToolCallLines(text) {
11650
12186
  * Note: [💞] Ignore a discrepancy between file name and entity name
11651
12187
  */
11652
12188
 
12189
+ /**
12190
+ * TEMPLATE commitment definition
12191
+ *
12192
+ * The TEMPLATE commitment enforces a specific response structure or template
12193
+ * that the agent must follow when generating responses. This helps ensure
12194
+ * consistent message formatting across all agent interactions.
12195
+ *
12196
+ * Example usage in agent source:
12197
+ *
12198
+ * ```book
12199
+ * TEMPLATE Always structure your response with: 1) Summary, 2) Details, 3) Next steps
12200
+ * TEMPLATE Use the following format: **Question:** [user question] | **Answer:** [your answer]
12201
+ * ```
12202
+ *
12203
+ * When used without content, it enables template mode which instructs the agent
12204
+ * to follow any template patterns defined in other commitments or context.
12205
+ *
12206
+ * @private [đŸĒ”] Maybe export the commitments through some package
12207
+ */
12208
+ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
12209
+ constructor(type = 'TEMPLATE') {
12210
+ super(type);
12211
+ }
12212
+ /**
12213
+ * Short one-line description of TEMPLATE.
12214
+ */
12215
+ get description() {
12216
+ return 'Enforce a specific message structure or response template.';
12217
+ }
12218
+ /**
12219
+ * Icon for this commitment.
12220
+ */
12221
+ get icon() {
12222
+ return '📋';
12223
+ }
12224
+ /**
12225
+ * Markdown documentation for TEMPLATE commitment.
12226
+ */
12227
+ get documentation() {
12228
+ return spaceTrim$1(`
12229
+ # ${this.type}
12230
+
12231
+ Enforces a specific response structure or template that the agent must follow when generating responses.
12232
+
12233
+ ## Key aspects
12234
+
12235
+ - Both terms work identically and can be used interchangeably.
12236
+ - Can be used with or without content.
12237
+ - When used without content, enables template mode for structured responses.
12238
+ - When used with content, defines the specific template structure to follow.
12239
+ - Multiple templates can be combined, with later ones taking precedence.
12240
+
12241
+ ## Examples
12242
+
12243
+ \`\`\`book
12244
+ Customer Support Agent
12245
+
12246
+ PERSONA You are a helpful customer support representative
12247
+ TEMPLATE Always structure your response with: 1) Acknowledgment, 2) Solution, 3) Follow-up question
12248
+ STYLE Be professional and empathetic
12249
+ \`\`\`
12250
+
12251
+ \`\`\`book
12252
+ Technical Documentation Assistant
12253
+
12254
+ PERSONA You are a technical writing expert
12255
+ TEMPLATE Use the following format: **Topic:** [topic] | **Explanation:** [details] | **Example:** [code]
12256
+ FORMAT Use markdown with clear headings
12257
+ \`\`\`
12258
+
12259
+ \`\`\`book
12260
+ Simple Agent
12261
+
12262
+ PERSONA You are a virtual assistant
12263
+ TEMPLATE
12264
+ \`\`\`
12265
+ `);
12266
+ }
12267
+ /**
12268
+ * TEMPLATE can be used with or without content.
12269
+ */
12270
+ get requiresContent() {
12271
+ return false;
12272
+ }
12273
+ applyToAgentModelRequirements(requirements, content) {
12274
+ var _a;
12275
+ const trimmedContent = content.trim();
12276
+ // If no content is provided, enable template mode
12277
+ if (!trimmedContent) {
12278
+ // Store template mode flag in metadata
12279
+ const updatedMetadata = {
12280
+ ...requirements.metadata,
12281
+ templateMode: true,
12282
+ };
12283
+ // Add a general instruction about using structured templates
12284
+ const templateModeInstruction = spaceTrim$1(`
12285
+ Use a clear, structured template format for your responses.
12286
+ Maintain consistency in how you organize and present information.
12287
+ `);
12288
+ return {
12289
+ ...this.appendToSystemMessage(requirements, templateModeInstruction, '\n\n'),
12290
+ metadata: updatedMetadata,
12291
+ };
12292
+ }
12293
+ // If content is provided, add the specific template instructions
12294
+ const templateSection = `Response Template: ${trimmedContent}`;
12295
+ // Store the template in metadata for potential programmatic access
12296
+ const existingTemplates = ((_a = requirements.metadata) === null || _a === void 0 ? void 0 : _a.templates) || [];
12297
+ const updatedMetadata = {
12298
+ ...requirements.metadata,
12299
+ templates: [...existingTemplates, trimmedContent],
12300
+ templateMode: true,
12301
+ };
12302
+ return {
12303
+ ...this.appendToSystemMessage(requirements, templateSection, '\n\n'),
12304
+ metadata: updatedMetadata,
12305
+ };
12306
+ }
12307
+ }
12308
+ /**
12309
+ * Note: [💞] Ignore a discrepancy between file name and entity name
12310
+ */
12311
+
11653
12312
  /**
11654
12313
  * USE commitment definition
11655
12314
  *
@@ -11764,12 +12423,56 @@ class UseCommitmentDefinition extends BaseCommitmentDefinition {
11764
12423
  * Note: [💞] Ignore a discrepancy between file name and entity name
11765
12424
  */
11766
12425
 
12426
+ /**
12427
+ * Client-side safe wrapper for fetching URL content
12428
+ *
12429
+ * This function proxies requests to the Agents Server API endpoint for scraping,
12430
+ * making it safe to use in browser environments.
12431
+ *
12432
+ * @param url The URL to fetch and scrape
12433
+ * @param agentsServerUrl The base URL of the agents server (defaults to current origin)
12434
+ * @returns Markdown content from the URL
12435
+ *
12436
+ * @private internal utility for USE BROWSER commitment
12437
+ */
12438
+ async function fetchUrlContentViaBrowser(url, agentsServerUrl) {
12439
+ try {
12440
+ // Determine the agents server URL
12441
+ const baseUrl = agentsServerUrl || (typeof window !== 'undefined' ? window.location.origin : '');
12442
+ if (!baseUrl) {
12443
+ throw new Error('Agents server URL is required in non-browser environments');
12444
+ }
12445
+ // Build the API endpoint URL
12446
+ const apiUrl = new URL('/api/scrape', baseUrl);
12447
+ apiUrl.searchParams.set('url', url);
12448
+ // Fetch from the API endpoint
12449
+ const response = await fetch(apiUrl.toString());
12450
+ if (!response.ok) {
12451
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
12452
+ throw new Error(`Failed to scrape URL: ${errorData.error || response.statusText}`);
12453
+ }
12454
+ const data = await response.json();
12455
+ if (!data.success || !data.content) {
12456
+ throw new Error(`Scraping failed: ${data.error || 'No content returned'}`);
12457
+ }
12458
+ return data.content;
12459
+ }
12460
+ catch (error) {
12461
+ const errorMessage = error instanceof Error ? error.message : String(error);
12462
+ throw new Error(`Error fetching URL content via browser: ${errorMessage}`);
12463
+ }
12464
+ }
12465
+
11767
12466
  /**
11768
12467
  * USE BROWSER commitment definition
11769
12468
  *
11770
- * The `USE BROWSER` commitment indicates that the agent should utilize a web browser tool
12469
+ * The `USE BROWSER` commitment indicates that the agent should utilize browser tools
11771
12470
  * to access and retrieve up-to-date information from the internet when necessary.
11772
12471
  *
12472
+ * This commitment provides two levels of browser access:
12473
+ * 1. One-shot URL fetching: Simple function to fetch and scrape URL content
12474
+ * 2. Running browser: For complex tasks like scrolling, clicking, etc. (prepared but not active yet)
12475
+ *
11773
12476
  * The content following `USE BROWSER` is ignored (similar to NOTE).
11774
12477
  *
11775
12478
  * Example usage in agent source:
@@ -11795,7 +12498,7 @@ class UseBrowserCommitmentDefinition extends BaseCommitmentDefinition {
11795
12498
  * Short one-line description of USE BROWSER.
11796
12499
  */
11797
12500
  get description() {
11798
- return 'Enable the agent to use a web browser tool for accessing internet information.';
12501
+ return 'Enable the agent to use browser tools for accessing internet information.';
11799
12502
  }
11800
12503
  /**
11801
12504
  * Icon for this commitment.
@@ -11810,14 +12513,18 @@ class UseBrowserCommitmentDefinition extends BaseCommitmentDefinition {
11810
12513
  return spaceTrim$1(`
11811
12514
  # USE BROWSER
11812
12515
 
11813
- Enables the agent to use a web browser tool to access and retrieve up-to-date information from the internet.
12516
+ Enables the agent to use browser tools to access and retrieve up-to-date information from the internet.
11814
12517
 
11815
12518
  ## Key aspects
11816
12519
 
11817
12520
  - The content following \`USE BROWSER\` is ignored (similar to NOTE)
12521
+ - Provides two levels of browser access:
12522
+ 1. **One-shot URL fetching**: Simple function to fetch and scrape URL content (active)
12523
+ 2. **Running browser**: For complex tasks like scrolling, clicking, etc. (prepared but not active yet)
11818
12524
  - The actual browser tool usage is handled by the agent runtime
11819
- - Allows the agent to fetch current information from websites
12525
+ - Allows the agent to fetch current information from websites and documents
11820
12526
  - Useful for research tasks, fact-checking, and accessing dynamic content
12527
+ - Supports various content types including HTML pages and PDF documents
11821
12528
 
11822
12529
  ## Examples
11823
12530
 
@@ -11853,48 +12560,306 @@ class UseBrowserCommitmentDefinition extends BaseCommitmentDefinition {
11853
12560
  */
11854
12561
  getToolTitles() {
11855
12562
  return {
11856
- web_browser: 'Web browser',
12563
+ fetch_url_content: 'Fetch URL content',
12564
+ run_browser: 'Run browser',
11857
12565
  };
11858
12566
  }
11859
12567
  applyToAgentModelRequirements(requirements, content) {
11860
12568
  // Get existing tools array or create new one
11861
12569
  const existingTools = requirements.tools || [];
11862
- // Add 'web_browser' to tools if not already present
11863
- const updatedTools = existingTools.some((tool) => tool.name === 'web_browser')
12570
+ // Add browser tools if not already present
12571
+ const toolsToAdd = [];
12572
+ // Tool 1: One-shot URL content fetching
12573
+ if (!existingTools.some((tool) => tool.name === 'fetch_url_content')) {
12574
+ toolsToAdd.push({
12575
+ name: 'fetch_url_content',
12576
+ description: spaceTrim$1(`
12577
+ Fetches and scrapes the content from a URL (webpage or document).
12578
+ This tool retrieves the content of the specified URL and converts it to markdown format.
12579
+ Use this when you need to access information from a specific website or document.
12580
+ Supports various content types including HTML pages and PDF documents.
12581
+ `),
12582
+ parameters: {
12583
+ type: 'object',
12584
+ properties: {
12585
+ url: {
12586
+ type: 'string',
12587
+ description: 'The URL to fetch and scrape (e.g., "https://example.com" or "https://example.com/document.pdf")',
12588
+ },
12589
+ },
12590
+ required: ['url'],
12591
+ },
12592
+ });
12593
+ }
12594
+ // Tool 2: Running browser (prepared but not active yet)
12595
+ if (!existingTools.some((tool) => tool.name === 'run_browser')) {
12596
+ toolsToAdd.push({
12597
+ name: 'run_browser',
12598
+ description: spaceTrim$1(`
12599
+ Launches a browser session for complex interactions.
12600
+ This tool is for advanced browser automation tasks like scrolling, clicking, form filling, etc.
12601
+ Note: This tool is prepared but not yet active. It will be implemented in a future update.
12602
+ `),
12603
+ parameters: {
12604
+ type: 'object',
12605
+ properties: {
12606
+ url: {
12607
+ type: 'string',
12608
+ description: 'The initial URL to navigate to',
12609
+ },
12610
+ actions: {
12611
+ type: 'array',
12612
+ description: 'Array of actions to perform in the browser',
12613
+ items: {
12614
+ type: 'object',
12615
+ properties: {
12616
+ type: {
12617
+ type: 'string',
12618
+ enum: ['navigate', 'click', 'scroll', 'type', 'wait'],
12619
+ },
12620
+ selector: {
12621
+ type: 'string',
12622
+ description: 'CSS selector for the element (for click, type actions)',
12623
+ },
12624
+ value: {
12625
+ type: 'string',
12626
+ description: 'Value to type or navigate to',
12627
+ },
12628
+ },
12629
+ },
12630
+ },
12631
+ },
12632
+ required: ['url'],
12633
+ },
12634
+ });
12635
+ }
12636
+ const updatedTools = [...existingTools, ...toolsToAdd];
12637
+ // Return requirements with updated tools and metadata
12638
+ return this.appendToSystemMessage({
12639
+ ...requirements,
12640
+ tools: updatedTools,
12641
+ metadata: {
12642
+ ...requirements.metadata,
12643
+ useBrowser: true,
12644
+ },
12645
+ }, spaceTrim$1(`
12646
+ You have access to browser tools to fetch and access content from the internet.
12647
+ - Use "fetch_url_content" to retrieve content from specific URLs (webpages or documents)
12648
+ - Use "run_browser" for complex browser interactions (note: not yet active)
12649
+ When you need to know information from a specific website or document, use the fetch_url_content tool.
12650
+ `));
12651
+ }
12652
+ /**
12653
+ * Gets the browser tool function implementations.
12654
+ *
12655
+ * This method automatically detects the environment and uses:
12656
+ * - Server-side: Direct scraping via fetchUrlContent (Node.js)
12657
+ * - Browser: Proxy through Agents Server API via fetchUrlContentViaBrowser
12658
+ */
12659
+ getToolFunctions() {
12660
+ return {
12661
+ /**
12662
+ * @@@
12663
+ *
12664
+ * Note: [đŸ›ē] This function has implementation both for browser and node, this is the proxied one for browser
12665
+ */
12666
+ async fetch_url_content(args) {
12667
+ console.log('!!!! [Tool] fetch_url_content called', { args });
12668
+ const { url } = args;
12669
+ return await fetchUrlContentViaBrowser(url);
12670
+ },
12671
+ /**
12672
+ * @@@
12673
+ */
12674
+ async run_browser(args) {
12675
+ console.log('!!!! [Tool] run_browser called', { args });
12676
+ const { url } = args;
12677
+ // This tool is prepared but not active yet
12678
+ return spaceTrim$1(`
12679
+ # Running browser
12680
+
12681
+ The running browser tool is not yet active.
12682
+
12683
+ Requested URL: ${url}
12684
+
12685
+ This feature will be implemented in a future update to support:
12686
+ - Complex browser interactions
12687
+ - Scrolling and navigation
12688
+ - Clicking and form filling
12689
+ - Taking screenshots
12690
+
12691
+ For now, please use the "fetch_url_content" tool instead.
12692
+ `);
12693
+ },
12694
+ };
12695
+ }
12696
+ }
12697
+ /**
12698
+ * Note: [💞] Ignore a discrepancy between file name and entity name
12699
+ */
12700
+
12701
+ /**
12702
+ * @@@
12703
+ *
12704
+ * @private utility for commitments
12705
+ */
12706
+ function formatOptionalInstructionBlock(label, content) {
12707
+ const trimmedContent = spaceTrim$1(content);
12708
+ if (!trimmedContent) {
12709
+ return '';
12710
+ }
12711
+ return spaceTrim$1((block) => `
12712
+ - ${label}:
12713
+ ${block(trimmedContent
12714
+ .split('\n')
12715
+ .map((line) => `- ${line}`)
12716
+ .join('\n'))}
12717
+ `);
12718
+ }
12719
+
12720
+ /**
12721
+ * USE EMAIL commitment definition
12722
+ *
12723
+ * The `USE EMAIL` commitment enables the agent to send emails.
12724
+ *
12725
+ * Example usage in agent source:
12726
+ *
12727
+ * ```book
12728
+ * USE EMAIL
12729
+ * USE EMAIL Write always formal and polite emails, always greet.
12730
+ * ```
12731
+ *
12732
+ * @private [đŸĒ”] Maybe export the commitments through some package
12733
+ */
12734
+ class UseEmailCommitmentDefinition extends BaseCommitmentDefinition {
12735
+ constructor() {
12736
+ super('USE EMAIL', ['EMAIL', 'MAIL']);
12737
+ }
12738
+ get requiresContent() {
12739
+ return false;
12740
+ }
12741
+ /**
12742
+ * Short one-line description of USE EMAIL.
12743
+ */
12744
+ get description() {
12745
+ return 'Enable the agent to send emails.';
12746
+ }
12747
+ /**
12748
+ * Icon for this commitment.
12749
+ */
12750
+ get icon() {
12751
+ return '📧';
12752
+ }
12753
+ /**
12754
+ * Markdown documentation for USE EMAIL commitment.
12755
+ */
12756
+ get documentation() {
12757
+ return spaceTrim$1(`
12758
+ # USE EMAIL
12759
+
12760
+ Enables the agent to send emails through the email service.
12761
+
12762
+ ## Key aspects
12763
+
12764
+ - The agent can send emails to specified recipients.
12765
+ - Supports multiple recipients, CC, subject, and markdown content.
12766
+ - Emails are queued and sent through configured email providers.
12767
+ - The content following \`USE EMAIL\` can provide additional instructions for email composition (e.g., style, tone, formatting preferences).
12768
+
12769
+ ## Examples
12770
+
12771
+ \`\`\`book
12772
+ Email Assistant
12773
+
12774
+ PERSONA You are a helpful assistant who can send emails.
12775
+ USE EMAIL
12776
+ \`\`\`
12777
+
12778
+ \`\`\`book
12779
+ Formal Email Assistant
12780
+
12781
+ PERSONA You help with professional communication.
12782
+ USE EMAIL Write always formal and polite emails, always greet.
12783
+ \`\`\`
12784
+ `);
12785
+ }
12786
+ applyToAgentModelRequirements(requirements, content) {
12787
+ const extraInstructions = formatOptionalInstructionBlock('Email instructions', content);
12788
+ // Get existing tools array or create new one
12789
+ const existingTools = requirements.tools || [];
12790
+ // Add 'send_email' to tools if not already present
12791
+ const updatedTools = existingTools.some((tool) => tool.name === 'send_email')
11864
12792
  ? existingTools
11865
- : ([
11866
- // TODO: [🔰] Use through proper MCP server
12793
+ : [
11867
12794
  ...existingTools,
11868
12795
  {
11869
- name: 'web_browser',
11870
- description: spaceTrim$1(`
11871
- A tool that can browse the web.
11872
- Use this tool when you need to access specific websites or browse the internet.
11873
- `),
12796
+ name: 'send_email',
12797
+ description: `Send an email to one or more recipients. ${!content ? '' : `Style instructions: ${content}`}`,
11874
12798
  parameters: {
11875
12799
  type: 'object',
11876
12800
  properties: {
11877
- url: {
12801
+ to: {
12802
+ type: 'array',
12803
+ items: { type: 'string' },
12804
+ description: 'Array of recipient email addresses (e.g., ["user@example.com", "Jane Doe <jane@example.com>"])',
12805
+ },
12806
+ cc: {
12807
+ type: 'array',
12808
+ items: { type: 'string' },
12809
+ description: 'Optional array of CC email addresses',
12810
+ },
12811
+ subject: {
12812
+ type: 'string',
12813
+ description: 'Email subject line',
12814
+ },
12815
+ body: {
11878
12816
  type: 'string',
11879
- description: 'The URL to browse',
12817
+ description: 'Email body content in markdown format',
11880
12818
  },
11881
12819
  },
11882
- required: ['url'],
12820
+ required: ['to', 'subject', 'body'],
11883
12821
  },
11884
12822
  },
11885
- ]);
12823
+ // <- TODO: !!!! define the function in LLM tools
12824
+ ];
11886
12825
  // Return requirements with updated tools and metadata
11887
12826
  return this.appendToSystemMessage({
11888
12827
  ...requirements,
11889
12828
  tools: updatedTools,
11890
12829
  metadata: {
11891
12830
  ...requirements.metadata,
11892
- useBrowser: true,
12831
+ useEmail: content || true,
11893
12832
  },
11894
- }, spaceTrim$1(`
11895
- You have access to the web browser. Use it to access specific websites or browse the internet.
11896
- When you need to know some information from a specific website, use the tool provided to you.
11897
- `));
12833
+ }, spaceTrim$1((block) => `
12834
+ Email tool:
12835
+ - You have access to send emails via the tool "send_email".
12836
+ - Use it when you need to send emails to users or other recipients.
12837
+ - The email body should be written in markdown format.
12838
+ - Always ensure the email content is clear, professional, and appropriate.
12839
+ ${block(extraInstructions)}
12840
+ `));
12841
+ }
12842
+ /**
12843
+ * Gets human-readable titles for tool functions provided by this commitment.
12844
+ */
12845
+ getToolTitles() {
12846
+ return {
12847
+ send_email: 'Send email',
12848
+ };
12849
+ }
12850
+ /**
12851
+ * Gets the `send_email` tool function implementation.
12852
+ * Note: This is a placeholder - the actual implementation is provided by the agent server.
12853
+ */
12854
+ getToolFunctions() {
12855
+ return {
12856
+ async send_email(args) {
12857
+ console.log('!!!! [Tool] send_email called', { args });
12858
+ // This is a placeholder implementation
12859
+ // The actual implementation should be provided by the agent server
12860
+ throw new Error('send_email tool not implemented. This commitment requires integration with an email service.');
12861
+ },
12862
+ };
11898
12863
  }
11899
12864
  }
11900
12865
  /**
@@ -12163,25 +13128,6 @@ class SerpSearchEngine {
12163
13128
  }
12164
13129
  }
12165
13130
 
12166
- /**
12167
- * @@@
12168
- *
12169
- * @private utility for commitments
12170
- */
12171
- function formatOptionalInstructionBlock(label, content) {
12172
- const trimmedContent = spaceTrim$1(content);
12173
- if (!trimmedContent) {
12174
- return '';
12175
- }
12176
- return spaceTrim$1((block) => `
12177
- - ${label}:
12178
- ${block(trimmedContent
12179
- .split('\n')
12180
- .map((line) => `- ${line}`)
12181
- .join('\n'))}
12182
- `);
12183
- }
12184
-
12185
13131
  /**
12186
13132
  * USE SEARCH ENGINE commitment definition
12187
13133
  *
@@ -12610,6 +13556,8 @@ const COMMITMENT_REGISTRY = [
12610
13556
  new SampleCommitmentDefinition('EXAMPLE'),
12611
13557
  new FormatCommitmentDefinition('FORMAT'),
12612
13558
  new FormatCommitmentDefinition('FORMATS'),
13559
+ new TemplateCommitmentDefinition('TEMPLATE'),
13560
+ new TemplateCommitmentDefinition('TEMPLATES'),
12613
13561
  new FromCommitmentDefinition('FROM'),
12614
13562
  new ImportCommitmentDefinition('IMPORT'),
12615
13563
  new ImportCommitmentDefinition('IMPORTS'),
@@ -12648,15 +13596,13 @@ const COMMITMENT_REGISTRY = [
12648
13596
  new UseBrowserCommitmentDefinition(),
12649
13597
  new UseSearchEngineCommitmentDefinition(),
12650
13598
  new UseTimeCommitmentDefinition(),
13599
+ new UseEmailCommitmentDefinition(),
12651
13600
  new UseImageGeneratorCommitmentDefinition('USE IMAGE GENERATOR'),
12652
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12653
- new UseImageGeneratorCommitmentDefinition('USE IMAGE GENERATION'),
12654
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12655
- new UseImageGeneratorCommitmentDefinition('IMAGE GENERATOR'),
12656
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12657
- new UseImageGeneratorCommitmentDefinition('IMAGE GENERATION'),
12658
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12659
- new UseImageGeneratorCommitmentDefinition('USE IMAGE'),
13601
+ new UseImageGeneratorCommitmentDefinition('USE IMAGE GENERATION' /* <- TODO: Remove any */),
13602
+ new UseImageGeneratorCommitmentDefinition('IMAGE GENERATOR' /* <- TODO: Remove any */),
13603
+ new UseImageGeneratorCommitmentDefinition('IMAGE GENERATION' /* <- TODO: Remove any */),
13604
+ new UseImageGeneratorCommitmentDefinition('USE IMAGE' /* <- TODO: Remove any */),
13605
+ // <- Note: [â›šī¸] How to deal with commitment aliases with defined functions
12660
13606
  new UseMcpCommitmentDefinition(),
12661
13607
  new UseCommitmentDefinition(),
12662
13608
  // Not yet implemented commitments (using placeholder)
@@ -12668,6 +13614,11 @@ const COMMITMENT_REGISTRY = [
12668
13614
  new NotYetImplementedCommitmentDefinition('CONTEXT'),
12669
13615
  // <- TODO: Prompt: Leverage aliases instead of duplicating commitment definitions
12670
13616
  ];
13617
+ /**
13618
+ * TODO: [🧠] Maybe create through standardized $register
13619
+ * Note: [💞] Ignore a discrepancy between file name and entity name
13620
+ */
13621
+
12671
13622
  /**
12672
13623
  * Gets all available commitment definitions
12673
13624
  * @returns Array of all commitment definitions
@@ -12677,24 +13628,65 @@ const COMMITMENT_REGISTRY = [
12677
13628
  function getAllCommitmentDefinitions() {
12678
13629
  return $deepFreeze([...COMMITMENT_REGISTRY]);
12679
13630
  }
13631
+
12680
13632
  /**
12681
13633
  * Gets all function implementations provided by all commitments
12682
13634
  *
12683
- * @public exported from `@promptbook/core`
13635
+ * Note: This function is intended for browser use, there is also equivalent `getAllCommitmentsToolFunctionsForNode` for server use
13636
+ *
13637
+ * @public exported from `@promptbook/browser`
12684
13638
  */
12685
- function getAllCommitmentsToolFunctions() {
13639
+ function getAllCommitmentsToolFunctionsForBrowser() {
12686
13640
  const allToolFunctions = {};
12687
13641
  for (const commitmentDefinition of getAllCommitmentDefinitions()) {
12688
13642
  const toolFunctions = commitmentDefinition.getToolFunctions();
12689
13643
  for (const [funcName, funcImpl] of Object.entries(toolFunctions)) {
13644
+ if (allToolFunctions[funcName] !== undefined &&
13645
+ just(false) /* <- Note: [â›šī¸] How to deal with commitment aliases */) {
13646
+ throw new UnexpectedError(`Duplicate tool function name detected: \`${funcName}\` provided by commitment \`${commitmentDefinition.type}\``);
13647
+ }
12690
13648
  allToolFunctions[funcName] = funcImpl;
12691
13649
  }
12692
13650
  }
12693
13651
  return allToolFunctions;
12694
13652
  }
13653
+
12695
13654
  /**
12696
- * TODO: [🧠] Maybe create through standardized $register
12697
- * Note: [💞] Ignore a discrepancy between file name and entity name
13655
+ * Gets all function implementations provided by all commitments
13656
+ *
13657
+ * Note: This function is intended for server use, there is also equivalent `getAllCommitmentsToolFunctionsForBrowser` for browser use
13658
+ *
13659
+ * @public exported from `@promptbook/node`
13660
+ */
13661
+ function getAllCommitmentsToolFunctionsForNode() {
13662
+ if (!$isRunningInNode()) {
13663
+ throw new EnvironmentMismatchError(spaceTrim(`
13664
+ Function getAllCommitmentsToolFunctionsForNode should be run in Node.js environment.
13665
+
13666
+ - In browser use getAllCommitmentsToolFunctionsForBrowser instead.
13667
+ - This function can include server-only tools which cannot run in browser environment.
13668
+
13669
+ `));
13670
+ }
13671
+ const allToolFunctionsInBrowser = getAllCommitmentsToolFunctionsForBrowser();
13672
+ const allToolFunctionsInNode = {
13673
+ /**
13674
+ * @@@
13675
+ *
13676
+ * Note: [đŸ›ē] This function has implementation both for browser and node, this is the full one for node
13677
+ */
13678
+ async fetch_url_content(args) {
13679
+ console.log('!!!! [Tool] fetch_url_content called', { args });
13680
+ const { url } = args;
13681
+ return await fetchUrlContent(url);
13682
+ },
13683
+ // TODO: !!!! Unhardcode, make proper server function register from definitions
13684
+ };
13685
+ return { ...allToolFunctionsInBrowser, ...allToolFunctionsInNode };
13686
+ }
13687
+ /**
13688
+ * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
13689
+ * TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
12698
13690
  */
12699
13691
 
12700
13692
  /**
@@ -12908,13 +13900,6 @@ class JavascriptEvalExecutionTools {
12908
13900
  `const ${functionName} = buildinFunctions.${functionName};`)
12909
13901
  .join('\n');
12910
13902
  // TODO: DRY [đŸ¯]
12911
- const commitmentsFunctions = getAllCommitmentsToolFunctions();
12912
- const commitmentsFunctionsStatement = Object.keys(commitmentsFunctions)
12913
- .map((functionName) =>
12914
- // Note: Custom functions are exposed to the current scope as variables
12915
- `const ${functionName} = commitmentsFunctions.${functionName};`)
12916
- .join('\n');
12917
- // TODO: DRY [đŸ¯]
12918
13903
  const customFunctions = this.options.functions || {};
12919
13904
  const customFunctionsStatement = Object.keys(customFunctions)
12920
13905
  .map((functionName) =>
@@ -12928,10 +13913,6 @@ class JavascriptEvalExecutionTools {
12928
13913
  // Build-in functions:
12929
13914
  ${block(buildinFunctionsStatement)}
12930
13915
 
12931
- // Commitments functions:
12932
- ${block(commitmentsFunctionsStatement)}
12933
-
12934
-
12935
13916
  // Custom functions:
12936
13917
  ${block(customFunctionsStatement || '// -- No custom functions --')}
12937
13918
 
@@ -13029,10 +14010,11 @@ async function $provideScriptingForNode(options) {
13029
14010
  throw new EnvironmentMismatchError('Function `$provideScriptingForNode` works only in Node.js environment');
13030
14011
  }
13031
14012
  // TODO: [🔱] Do here auto-installation
13032
- return [new JavascriptExecutionTools(options)];
14013
+ return [new JavascriptExecutionTools({ ...options, functions: { ...getAllCommitmentsToolFunctionsForNode() } })];
13033
14014
  }
13034
14015
  /**
13035
14016
  * Note: [đŸŸĸ] Code in this file should never be never released in packages that could be imported into browser environment
14017
+ * TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
13036
14018
  */
13037
14019
 
13038
14020
  // TODO: [đŸĨē] List running services from REMOTE_SERVER_URLS