@promptbook/core
Version:
Promptbook: Run AI apps in plain human language across multiple models and platforms
1,556 lines (1,465 loc) โข 545 kB
JavaScript
import spaceTrim$1, { spaceTrim } from 'spacetrim';
import { format } from 'prettier';
import parserHtml from 'prettier/parser-html';
import { randomBytes } from 'crypto';
import { Subject } from 'rxjs';
import { forTime } from 'waitasecond';
import { parse, unparse } from 'papaparse';
import hexEncoder from 'crypto-js/enc-hex';
import sha256 from 'crypto-js/sha256';
import { basename, join, dirname } from 'path';
import { SHA256 } from 'crypto-js';
import { lookup, extension } from 'mime-types';
import moment from 'moment';
import colors from 'colors';
// โ ๏ธ WARNING: This code has been generated so that any manual changes will be overwritten
/**
* The version of the Book language
*
* @generated
* @see https://github.com/webgptorg/book
*/
const BOOK_LANGUAGE_VERSION = '1.0.0';
/**
* The version of the Promptbook engine
*
* @generated
* @see https://github.com/webgptorg/promptbook
*/
const PROMPTBOOK_ENGINE_VERSION = '0.100.0-36';
/**
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Extracts profile image URL from agent definition text and returns cleaned system message
* @param systemMessage The original system message that may contain META IMAGE line
* @returns Object with profileImageUrl (if found) and cleanedSystemMessage (without META IMAGE line)
*
* @private - TODO: [๐ง ] Maybe should be public?
*/
/**
* Generates a gravatar URL based on agent name for fallback avatar
* @param name The agent name to generate avatar for
* @returns Gravatar URL
*
* @private - TODO: [๐ง ] Maybe should be public?
*/
function generateGravatarUrl(name) {
// Use a default name if none provided
const safeName = name || 'Anonymous Agent';
// Create a simple hash from the name for consistent avatar
let hash = 0;
for (let i = 0; i < safeName.length; i++) {
const char = safeName.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
const avatarId = Math.abs(hash).toString();
return `https://www.gravatar.com/avatar/${avatarId}?default=robohash&size=200&rating=x`;
}
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Freezes the given object and all its nested objects recursively
*
* Note: `$` is used to indicate that this function is not a pure function - it mutates given object
* Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
*
* @returns The same object as the input, but deeply frozen
* @public exported from `@promptbook/utils`
*/
function $deepFreeze(objectValue) {
if (Array.isArray(objectValue)) {
return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
}
const propertyNames = Object.getOwnPropertyNames(objectValue);
for (const propertyName of propertyNames) {
const value = objectValue[propertyName];
if (value && typeof value === 'object') {
$deepFreeze(value);
}
}
Object.freeze(objectValue);
return objectValue;
}
/**
* TODO: [๐ง ] Is there a way how to meaningfully test this utility
*/
/**
* Generates a regex pattern to match a specific commitment
*
* Note: It always creates new Regex object
* Note: Uses word boundaries to ensure only full words are matched (e.g., "PERSONA" matches but "PERSONALITY" does not)
*
* @private
*/
function createCommitmentRegex(commitment) {
const escapedCommitment = commitment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const keywordPattern = escapedCommitment.split(/\s+/).join('\\s+');
const regex = new RegExp(`^\\s*(?<type>${keywordPattern})\\b\\s+(?<contents>.+)$`, 'gim');
return regex;
}
/**
* Generates a regex pattern to match a specific commitment type
*
* Note: It just matches the type part of the commitment
* Note: It always creates new Regex object
* Note: Uses word boundaries to ensure only full words are matched (e.g., "PERSONA" matches but "PERSONALITY" does not)
*
* @private
*/
function createCommitmentTypeRegex(commitment) {
const escapedCommitment = commitment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const keywordPattern = escapedCommitment.split(/\s+/).join('\\s+');
const regex = new RegExp(`^\\s*(?<type>${keywordPattern})\\b`, 'gim');
return regex;
}
/**
* Base implementation of CommitmentDefinition that provides common functionality
* Most commitments can extend this class and only override the applyToAgentModelRequirements method
*
* @private
*/
class BaseCommitmentDefinition {
constructor(type) {
this.type = type;
}
/**
* Creates a regex pattern to match this commitment in agent source
* Uses the existing createCommitmentRegex function as internal helper
*/
createRegex() {
return createCommitmentRegex(this.type);
}
/**
* Creates a regex pattern to match just the commitment type
* Uses the existing createCommitmentTypeRegex function as internal helper
*/
createTypeRegex() {
return createCommitmentTypeRegex(this.type);
}
/**
* Helper method to create a new requirements object with updated system message
* This is commonly used by many commitments
*/
updateSystemMessage(requirements, messageUpdate) {
const newMessage = typeof messageUpdate === 'string' ? messageUpdate : messageUpdate(requirements.systemMessage);
return {
...requirements,
systemMessage: newMessage,
};
}
/**
* Helper method to append content to the system message
*/
appendToSystemMessage(requirements, content, separator = '\n\n') {
return this.updateSystemMessage(requirements, (currentMessage) => {
if (!currentMessage.trim()) {
return content;
}
return currentMessage + separator + content;
});
}
/**
* Helper method to add a comment section to the system message
* Comments are lines starting with # that will be removed from the final system message
* but can be useful for organizing and structuring the message during processing
*/
addCommentSection(requirements, commentTitle, content, position = 'end') {
const commentSection = `# ${commentTitle.toUpperCase()}\n${content}`;
if (position === 'beginning') {
return this.updateSystemMessage(requirements, (currentMessage) => {
if (!currentMessage.trim()) {
return commentSection;
}
return commentSection + '\n\n' + currentMessage;
});
}
else {
return this.appendToSystemMessage(requirements, commentSection);
}
}
}
/**
* ACTION commitment definition
*
* The ACTION commitment defines specific actions or capabilities that the agent can perform.
* This helps define what the agent is capable of doing and how it should approach tasks.
*
* Example usage in agent source:
*
* ```book
* ACTION Can generate code snippets and explain programming concepts
* ACTION Able to analyze data and provide insights
* ```
*
* @private [๐ช] Maybe export the commitments through some package
*/
class ActionCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('ACTION');
}
/**
* Short one-line description of ACTION.
*/
get description() {
return 'Define agent capabilities and actions it can perform.';
}
/**
* Markdown documentation for ACTION commitment.
*/
get documentation() {
return spaceTrim(`
# ACTION
Defines specific actions or capabilities that the agent can perform.
## Key behaviors
- Multiple \`ACTION\` commitments are applied sequentially.
- Each action adds to the agent's capability list.
- Actions help users understand what the agent can do.
## Examples
\`\`\`book
Code Assistant
PERSONA You are a programming assistant
ACTION Can generate code snippets and explain programming concepts
ACTION Able to debug existing code and suggest improvements
ACTION Can create unit tests for functions
\`\`\`
\`\`\`book
Data Scientist
PERSONA You are a data analysis expert
ACTION Able to analyze data and provide insights
ACTION Can create visualizations and charts
ACTION Capable of statistical analysis and modeling
KNOWLEDGE Data analysis best practices and statistical methods
\`\`\`
`);
}
applyToAgentModelRequirements(requirements, content) {
const trimmedContent = content.trim();
if (!trimmedContent) {
return requirements;
}
// Add action capability to the system message
const actionSection = `Capability: ${trimmedContent}`;
return this.appendToSystemMessage(requirements, actionSection, '\n\n');
}
}
/**
* Singleton instance of the ACTION commitment definition
*
* @private [๐ช] Maybe export the commitments through some package
*/
new ActionCommitmentDefinition();
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* FORMAT commitment definition
*
* The FORMAT commitment defines the specific output structure and formatting
* that the agent should use in its responses. This includes data formats,
* response templates, and structural requirements.
*
* Example usage in agent source:
*
* ```book
* FORMAT Always respond in JSON format with 'status' and 'data' fields
* FORMAT Use markdown formatting for all code blocks
* ```
*
* @private [๐ช] Maybe export the commitments through some package
*/
class FormatCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('FORMAT');
}
/**
* Short one-line description of FORMAT.
*/
get description() {
return 'Specify output structure or formatting requirements.';
}
/**
* Markdown documentation for FORMAT commitment.
*/
get documentation() {
return spaceTrim(`
# FORMAT
Defines the specific output structure and formatting for responses (data formats, templates, structure).
## Key behaviors
- Multiple \`FORMAT\` commitments are applied sequentially.
- If they are in conflict, the last one takes precedence.
- You can specify both data formats and presentation styles.
## Examples
\`\`\`book
Customer Support Bot
PERSONA You are a helpful customer support agent
FORMAT Always respond in JSON format with 'status' and 'data' fields
FORMAT Use markdown formatting for all code blocks
\`\`\`
\`\`\`book
Data Analyst
PERSONA You are a data analysis expert
FORMAT Present results in structured tables
FORMAT Include confidence scores for all predictions
STYLE Be concise and precise in explanations
\`\`\`
`);
}
applyToAgentModelRequirements(requirements, content) {
const trimmedContent = content.trim();
if (!trimmedContent) {
return requirements;
}
// Add format instructions to the system message
const formatSection = `Output Format: ${trimmedContent}`;
return this.appendToSystemMessage(requirements, formatSection, '\n\n');
}
}
/**
* Singleton instance of the FORMAT commitment definition
*
* @private [๐ช] Maybe export the commitments through some package
*/
new FormatCommitmentDefinition();
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Error thrown when a fetch request fails
*
* @public exported from `@promptbook/core`
*/
class PromptbookFetchError extends Error {
constructor(message) {
super(message);
this.name = 'PromptbookFetchError';
Object.setPrototypeOf(this, PromptbookFetchError.prototype);
}
}
/**
* Available remote servers for the Promptbook
*
* @public exported from `@promptbook/core`
*/
const REMOTE_SERVER_URLS = [
{
title: 'Promptbook',
description: `Servers of Promptbook.studio`,
owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)',
isAnonymousModeAllowed: true,
urls: [
'https://promptbook.s5.ptbk.io/',
// Note: Servers 1-4 are not running
],
},
/*
Note: Working on older version of Promptbook and not supported anymore
{
title: 'Pavol Promptbook Server',
description: `Personal server of Pavol Hejnรฝ with simple testing server, DO NOT USE IT FOR PRODUCTION`,
owner: 'Pavol Hejnรฝ <pavol@ptbk.io> (https://www.pavolhejny.com/)',
isAnonymousModeAllowed: true,
urls: ['https://api.pavolhejny.com/promptbook'],
},
*/
];
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Returns the same value that is passed as argument.
* No side effects.
*
* Note: It can be useful for:
*
* 1) Leveling indentation
* 2) Putting always-true or always-false conditions without getting eslint errors
*
* @param value any values
* @returns the same values
* @private within the repository
*/
function just(value) {
if (value === undefined) {
return undefined;
}
return value;
}
/**
* Warning message for the generated sections and files files
*
* @private within the repository
*/
const GENERATOR_WARNING = `โ ๏ธ WARNING: This code has been generated so that any manual changes will be overwritten`;
/**
* Name for the Promptbook
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const NAME = `Promptbook`;
/**
* Email of the responsible person
*
* @public exported from `@promptbook/core`
*/
const ADMIN_EMAIL = 'pavol@ptbk.io';
/**
* Name of the responsible person for the Promptbook on GitHub
*
* @public exported from `@promptbook/core`
*/
const ADMIN_GITHUB_NAME = 'hejny';
/**
* Claim for the Promptbook
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const CLAIM = `It's time for a paradigm shift. The future of software in plain English, French or Latin`;
// <- TODO: [๐] Pick the best claim
/**
* When the title is not provided, the default title is used
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_BOOK_TITLE = `โจ Untitled Book`;
/**
* When the title of task is not provided, the default title is used
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_TASK_TITLE = `Task`;
/**
* When the title of the prompt task is not provided, the default title is used
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_PROMPT_TASK_TITLE = `Prompt`;
/**
* When the pipeline is flat and no name of return parameter is provided, this name is used
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_BOOK_OUTPUT_PARAMETER_NAME = 'result';
/**
* Maximum file size limit
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
/**
* Threshold value that determines when a dataset is considered "big"
* and may require special handling or optimizations
*
* For example, when error occurs in one item of the big dataset, it will not fail the whole pipeline
*
* @public exported from `@promptbook/core`
*/
const BIG_DATASET_TRESHOLD = 50;
/**
* Placeholder text used to represent a placeholder value of failed operation
*
* @public exported from `@promptbook/core`
*/
const FAILED_VALUE_PLACEHOLDER = '!?';
/**
* Placeholder text used to represent operations or values that are still in progress
* or awaiting completion in UI displays and logging
*
* @public exported from `@promptbook/core`
*/
const PENDING_VALUE_PLACEHOLDER = 'โฆ';
// <- TODO: [๐ง ] Better system for generator warnings - not always "code" and "by `@promptbook/cli`"
/**
* The maximum number of iterations for a loops
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const LOOP_LIMIT = 1000;
/**
* Strings to represent various values in the context of parameter values
*
* @public exported from `@promptbook/utils`
*/
const VALUE_STRINGS = {
empty: '(nothing; empty string)',
null: '(no value; null)',
undefined: '(unknown value; undefined)',
nan: '(not a number; NaN)',
infinity: '(infinity; โ)',
negativeInfinity: '(negative infinity; -โ)',
unserializable: '(unserializable value)',
circular: '(circular JSON)',
};
/**
* Small number limit
*
* @public exported from `@promptbook/utils`
*/
const SMALL_NUMBER = 0.001;
/**
* Short time interval to prevent race conditions in milliseconds
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const IMMEDIATE_TIME = 10;
/**
* The maximum length of the (generated) filename
*
* @public exported from `@promptbook/core`
*/
const MAX_FILENAME_LENGTH = 30;
/**
* Strategy for caching the intermediate results for knowledge sources
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_INTERMEDIATE_FILES_STRATEGY = 'HIDE_AND_KEEP';
// <- TODO: [๐ก] Change to 'VISIBLE'
/**
* The maximum number of (LLM) tasks running in parallel
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_PARALLEL_COUNT = 5; // <- TODO: [๐คนโโ๏ธ]
/**
* The maximum number of attempts to execute LLM task before giving up
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_EXECUTION_ATTEMPTS = 7; // <- TODO: [๐คนโโ๏ธ]
/**
* The maximum depth to which knowledge sources will be scraped when building a knowledge base.
* This prevents infinite recursion and limits resource usage.
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_KNOWLEDGE_SOURCES_SCRAPING_DEPTH = 3;
// <- TODO: [๐]
/**
* The maximum total number of knowledge sources that will be scraped in a single operation.
* This acts as a global limit to avoid excessive resource consumption.
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_KNOWLEDGE_SOURCES_SCRAPING_TOTAL = 200;
// <- TODO: [๐]
/**
* Where to store your books
* This is kind of a "src" for your books
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_BOOKS_DIRNAME = './books';
// <- TODO: [๐] Make also `BOOKS_DIRNAME_ALTERNATIVES`
// TODO: Just `.promptbook` in config, hardcode subfolders like `download-cache` or `execution-cache`
/**
* Where to store the temporary downloads
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_DOWNLOAD_CACHE_DIRNAME = './.promptbook/download-cache';
/**
* Where to store the cache of executions for promptbook CLI
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_EXECUTION_CACHE_DIRNAME = './.promptbook/execution-cache';
/**
* Where to store the scrape cache
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_SCRAPE_CACHE_DIRNAME = './.promptbook/scrape-cache';
/**
* Id of application for the CLI when using remote server
*
* @public exported from `@promptbook/core`
*/
const CLI_APP_ID = 'cli';
/**
* Id of application for the playground
*
* @public exported from `@promptbook/core`
*/
const PLAYGROUND_APP_ID = 'playground';
/*
TODO: [๐]
/**
* Id of application for the wizard when using remote server
*
* @public exported from `@promptbook/core`
* /
ex-port const WIZARD_APP_ID: string_app_id = 'wizard';
*/
/**
* The name of the builded pipeline collection made by CLI `ptbk make` and for lookup in `createCollectionFromDirectory`
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_PIPELINE_COLLECTION_BASE_FILENAME = `index`;
/**
* The thresholds for the relative time in the `moment` NPM package.
*
* @see https://momentjscom.readthedocs.io/en/latest/moment/07-customization/13-relative-time-threshold/
* @private within the repository - too low-level in comparison with other constants
*/
const MOMENT_ARG_THRESHOLDS = {
ss: 3, // <- least number of seconds to be counted in seconds, minus 1. Must be set after setting the `s` unit or without setting the `s` unit.
};
/**
* Default remote server URL for the Promptbook
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_REMOTE_SERVER_URL = REMOTE_SERVER_URLS[0].urls[0];
// <- TODO: [๐งโโ๏ธ]
/**
* Default settings for parsing and generating CSV files in Promptbook.
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_CSV_SETTINGS = Object.freeze({
delimiter: ',',
quoteChar: '"',
newline: '\n',
skipEmptyLines: true,
});
/**
* Controls whether verbose logging is enabled by default throughout the application.
*
* @public exported from `@promptbook/core`
*/
let DEFAULT_IS_VERBOSE = false;
/**
* Enables or disables verbose logging globally at runtime.
*
* Note: This is an experimental feature.
*
* @public exported from `@promptbook/core`
*/
function SET_IS_VERBOSE(isVerbose) {
DEFAULT_IS_VERBOSE = isVerbose;
}
/**
* Controls whether auto-installation of dependencies is enabled by default.
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_IS_AUTO_INSTALLED = false;
/**
* Default simulated duration for a task in milliseconds (used for progress reporting)
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_TASK_SIMULATED_DURATION_MS = 5 * 60 * 1000; // 5 minutes
/**
* Function name for generated function via `ptbk make` to get the pipeline collection
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_GET_PIPELINE_COLLECTION_FUNCTION_NAME = `getPipelineCollection`;
/**
* Default rate limits (requests per minute)
*
* Note: Adjust based on the provider tier you are have
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_REQUESTS_PER_MINUTE = 60;
/**
* Indicates whether pipeline logic validation is enabled. When true, the pipeline logic is checked for consistency.
*
* @private within the repository
*/
const IS_PIPELINE_LOGIC_VALIDATED = just(
/**/
// Note: In normal situations, we check the pipeline logic:
true);
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
* TODO: [๐ง ][๐งโโ๏ธ] Maybe join remoteServerUrl and path into single value
*/
/**
* Make error report URL for the given error
*
* @private private within the repository
*/
function getErrorReportUrl(error) {
const report = {
title: `๐ Error report from ${NAME}`,
body: spaceTrim$1((block) => `
\`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}.
\`\`\`
${block(error.message || '(no error message)')}
\`\`\`
## More info:
- **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION}
- **Book language version:** ${BOOK_LANGUAGE_VERSION}
- **Time:** ${new Date().toISOString()}
<details>
<summary>Stack trace:</summary>
## Stack trace:
\`\`\`stacktrace
${block(error.stack || '(empty)')}
\`\`\`
</details>
`),
};
const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`);
reportUrl.searchParams.set('labels', 'bug');
reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME);
reportUrl.searchParams.set('title', report.title);
reportUrl.searchParams.set('body', report.body);
return reportUrl;
}
/**
* This error type indicates that the error should not happen and its last check before crashing with some other error
*
* @public exported from `@promptbook/core`
*/
class UnexpectedError extends Error {
constructor(message) {
super(spaceTrim((block) => `
${block(message)}
Note: This error should not happen.
It's probably a bug in the pipeline collection
Please report issue:
${block(getErrorReportUrl(new Error(message)).href)}
Or contact us on ${ADMIN_EMAIL}
`));
this.name = 'UnexpectedError';
Object.setPrototypeOf(this, UnexpectedError.prototype);
}
}
/**
* This error type indicates that somewhere in the code non-Error object was thrown and it was wrapped into the `WrappedError`
*
* @public exported from `@promptbook/core`
*/
class WrappedError extends Error {
constructor(whatWasThrown) {
const tag = `[๐คฎ]`;
console.error(tag, whatWasThrown);
super(spaceTrim(`
Non-Error object was thrown
Note: Look for ${tag} in the console for more details
Please report issue on ${ADMIN_EMAIL}
`));
this.name = 'WrappedError';
Object.setPrototypeOf(this, WrappedError.prototype);
}
}
/**
* Helper used in catch blocks to assert that the error is an instance of `Error`
*
* @param whatWasThrown Any object that was thrown
* @returns Nothing if the error is an instance of `Error`
* @throws `WrappedError` or `UnexpectedError` if the error is not standard
*
* @private within the repository
*/
function assertsError(whatWasThrown) {
// Case 1: Handle error which was rethrown as `WrappedError`
if (whatWasThrown instanceof WrappedError) {
const wrappedError = whatWasThrown;
throw wrappedError;
}
// Case 2: Handle unexpected errors
if (whatWasThrown instanceof UnexpectedError) {
const unexpectedError = whatWasThrown;
throw unexpectedError;
}
// Case 3: Handle standard errors - keep them up to consumer
if (whatWasThrown instanceof Error) {
return;
}
// Case 4: Handle non-standard errors - wrap them into `WrappedError` and throw
throw new WrappedError(whatWasThrown);
}
/**
* The built-in `fetch' function with a lightweight error handling wrapper as default fetch function used in Promptbook scrapers
*
* @public exported from `@promptbook/core`
*/
const promptbookFetch = async (urlOrRequest, init) => {
try {
return await fetch(urlOrRequest, init);
}
catch (error) {
assertsError(error);
let url;
if (typeof urlOrRequest === 'string') {
url = urlOrRequest;
}
else if (urlOrRequest instanceof Request) {
url = urlOrRequest.url;
}
throw new PromptbookFetchError(spaceTrim$1((block) => `
Can not fetch "${url}"
Fetch error:
${block(error.message)}
`));
}
};
/**
* TODO: [๐ง ] Maybe rename because it is not used only for scrapers but also in `$getCompiledBook`
*/
/**
* Frontend RAG Service that uses backend APIs for processing
* This avoids Node.js dependencies in the frontend
*
* @private - TODO: [๐ง ] Maybe should be public?
*/
class FrontendRAGService {
constructor(config) {
this.chunks = [];
this.sources = [];
this.isInitialized = false;
this.config = {
maxChunkSize: 1000,
chunkOverlap: 200,
maxRetrievedChunks: 5,
minRelevanceScore: 0.1,
...config,
};
}
/**
* Initialize knowledge sources by processing them on the backend
*/
async initializeKnowledgeSources(sources) {
if (sources.length === 0) {
this.isInitialized = true;
return;
}
try {
const response = await promptbookFetch('/api/knowledge/process-sources', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sources,
config: {
maxChunkSize: this.config.maxChunkSize,
chunkOverlap: this.config.chunkOverlap,
},
}),
});
if (!response.ok) {
throw new Error(`Failed to process knowledge sources: ${response.status}`);
}
const result = (await response.json());
if (!result.success) {
throw new Error(result.message || 'Failed to process knowledge sources');
}
this.chunks = result.chunks;
this.sources = sources;
this.isInitialized = true;
console.log(`Initialized RAG service with ${this.chunks.length} chunks from ${sources.length} sources`);
}
catch (error) {
console.error('Failed to initialize knowledge sources:', error);
// Don't throw - allow the system to continue without RAG
this.isInitialized = true;
}
}
/**
* Get relevant context for a user query
*/
async getContextForQuery(query) {
if (!this.isInitialized) {
console.warn('RAG service not initialized');
return '';
}
if (this.chunks.length === 0) {
return '';
}
try {
const response = await promptbookFetch('/api/knowledge/retrieve-context', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
chunks: this.chunks,
config: {
maxRetrievedChunks: this.config.maxRetrievedChunks,
minRelevanceScore: this.config.minRelevanceScore,
},
}),
});
if (!response.ok) {
console.error(`Failed to retrieve context: ${response.status}`);
return '';
}
const result = (await response.json());
if (!result.success) {
console.error('Context retrieval failed:', result.message);
return '';
}
return result.context;
}
catch (error) {
console.error('Error retrieving context:', error);
return '';
}
}
/**
* Get relevant chunks for a query (for debugging/inspection)
*/
async getRelevantChunks(query) {
if (!this.isInitialized || this.chunks.length === 0) {
return [];
}
try {
const response = await promptbookFetch('/api/knowledge/retrieve-context', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
chunks: this.chunks,
config: {
maxRetrievedChunks: this.config.maxRetrievedChunks,
minRelevanceScore: this.config.minRelevanceScore,
},
}),
});
if (!response.ok) {
return [];
}
const result = (await response.json());
return result.success ? result.relevantChunks : [];
}
catch (error) {
console.error('Error retrieving relevant chunks:', error);
return [];
}
}
/**
* Get knowledge base statistics
*/
getStats() {
return {
sources: this.sources.length,
chunks: this.chunks.length,
isInitialized: this.isInitialized,
};
}
/**
* Check if the service is ready to use
*/
isReady() {
return this.isInitialized;
}
/**
* Clear all knowledge sources
*/
clearKnowledgeBase() {
this.chunks = [];
this.sources = [];
this.isInitialized = false;
}
/**
* Add a single knowledge source (for incremental updates)
*/
async addKnowledgeSource(url) {
if (this.sources.includes(url)) {
console.log(`Knowledge source already exists: ${url}`);
return;
}
try {
const response = await promptbookFetch('/api/knowledge/process-sources', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sources: [url],
config: {
maxChunkSize: this.config.maxChunkSize,
chunkOverlap: this.config.chunkOverlap,
},
}),
});
if (!response.ok) {
throw new Error(`Failed to process knowledge source: ${response.status}`);
}
const result = (await response.json());
if (!result.success) {
throw new Error(result.message || 'Failed to process knowledge source');
}
// Add new chunks to existing ones
this.chunks.push(...result.chunks);
this.sources.push(url);
console.log(`Added knowledge source: ${url} (${result.chunks.length} chunks)`);
}
catch (error) {
console.error(`Failed to add knowledge source ${url}:`, error);
throw error;
}
}
}
/**
* KNOWLEDGE commitment definition
*
* The KNOWLEDGE commitment adds specific knowledge, facts, or context to the agent
* using RAG (Retrieval-Augmented Generation) approach for external sources.
*
* Supports both direct text knowledge and external sources like PDFs.
*
* Example usage in agent source:
*
* ```book
* KNOWLEDGE The company was founded in 2020 and specializes in AI-powered solutions
* KNOWLEDGE https://example.com/company-handbook.pdf
* KNOWLEDGE https://example.com/product-documentation.pdf
* ```
*
* @private [๐ช] Maybe export the commitments through some package
*/
class KnowledgeCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('KNOWLEDGE');
this.ragService = new FrontendRAGService();
}
/**
* Short one-line description of KNOWLEDGE.
*/
get description() {
return 'Add domain **knowledge** via direct text or external sources (RAG).';
}
/**
* Markdown documentation for KNOWLEDGE commitment.
*/
get documentation() {
return spaceTrim(`
# KNOWLEDGE
Adds specific knowledge, facts, or context to the agent using a RAG (Retrieval-Augmented Generation) approach for external sources.
## Key behaviors
- Multiple \`KNOWLEDGE\` commitments are applied sequentially.
- Supports both direct text knowledge and external URLs.
- External sources (PDFs, websites) are processed via RAG for context retrieval.
## Supported formats
- Direct text: Immediate knowledge incorporated into agent
- URLs: External documents processed for contextual retrieval
- Supported file types: PDF, text, markdown, HTML
## Examples
\`\`\`book
Customer Support Bot
PERSONA You are a helpful customer support agent for TechCorp
KNOWLEDGE TechCorp was founded in 2020 and specializes in AI-powered solutions
KNOWLEDGE https://example.com/company-handbook.pdf
KNOWLEDGE https://example.com/product-documentation.pdf
RULE Always be polite and professional
\`\`\`
\`\`\`book
Research Assistant
PERSONA You are a knowledgeable research assistant
KNOWLEDGE Academic research requires careful citation and verification
KNOWLEDGE https://example.com/research-guidelines.pdf
ACTION Can help with literature reviews and data analysis
STYLE Present information in clear, academic format
\`\`\`
`);
}
applyToAgentModelRequirements(requirements, content) {
var _a;
const trimmedContent = content.trim();
if (!trimmedContent) {
return requirements;
}
// Check if content is a URL (external knowledge source)
if (this.isUrl(trimmedContent)) {
// Store the URL for later async processing
const updatedRequirements = {
...requirements,
metadata: {
...requirements.metadata,
ragService: this.ragService,
knowledgeSources: [
...(((_a = requirements.metadata) === null || _a === void 0 ? void 0 : _a.knowledgeSources) || []),
trimmedContent,
],
},
};
// Add placeholder information about knowledge sources to system message
const knowledgeInfo = `Knowledge Source URL: ${trimmedContent} (will be processed for retrieval during chat)`;
return this.appendToSystemMessage(updatedRequirements, knowledgeInfo, '\n\n');
}
else {
// Direct text knowledge - add to system message
const knowledgeSection = `Knowledge: ${trimmedContent}`;
return this.appendToSystemMessage(requirements, knowledgeSection, '\n\n');
}
}
/**
* Check if content is a URL
*/
isUrl(content) {
try {
new URL(content);
return true;
}
catch (_a) {
return false;
}
}
/**
* Get RAG service instance for retrieving context during chat
*/
getRagService() {
return this.ragService;
}
}
/**
* Singleton instance of the KNOWLEDGE commitment definition
*
* @private [๐ช] Maybe export the commitments through some package
*/
new KnowledgeCommitmentDefinition();
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* META IMAGE commitment definition
*
* The META IMAGE commitment sets the agent's avatar/profile image URL.
* This commitment is special because it doesn't affect the system message,
* but is handled separately in the parsing logic.
*
* Example usage in agent source:
*
* ```book
* META IMAGE https://example.com/avatar.jpg
* META IMAGE /assets/agent-avatar.png
* ```
*
* @private [๐ช] Maybe export the commitments through some package
*/
class MetaImageCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('META IMAGE');
}
/**
* Short one-line description of META IMAGE.
*/
get description() {
return "Set the agent's profile image URL.";
}
/**
* Markdown documentation for META IMAGE commitment.
*/
get documentation() {
return spaceTrim(`
# META IMAGE
Sets the agent's avatar/profile image URL.
## Key behaviors
- Does not modify the agent's behavior or responses.
- Only one \`META IMAGE\` should be used per agent.
- If multiple are specified, the last one takes precedence.
- Used for visual representation in user interfaces.
## Examples
\`\`\`book
Professional Assistant
META IMAGE https://example.com/professional-avatar.jpg
PERSONA You are a professional business assistant
STYLE Maintain a formal and courteous tone
\`\`\`
\`\`\`book
Creative Helper
META IMAGE /assets/creative-bot-avatar.png
PERSONA You are a creative and inspiring assistant
STYLE Be enthusiastic and encouraging
ACTION Can help with brainstorming and ideation
\`\`\`
`);
}
applyToAgentModelRequirements(requirements, content) {
// META IMAGE doesn't modify the system message or model requirements
// It's handled separately in the parsing logic for profile image extraction
// This method exists for consistency with the CommitmentDefinition interface
return requirements;
}
/**
* Extracts the profile image URL from the content
* This is used by the parsing logic
*/
extractProfileImageUrl(content) {
const trimmedContent = content.trim();
return trimmedContent || null;
}
}
/**
* Singleton instance of the META IMAGE commitment definition
*
* @private [๐ช] Maybe export the commitments through some package
*/
new MetaImageCommitmentDefinition();
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* META LINK commitment definition
*
* The `META LINK` commitment represents the link to the person from whom the agent is created.
* This commitment is special because it doesn't affect the system message,
* but is handled separately in the parsing logic for profile display.
*
* Example usage in agent source:
*
* ```
* META LINK https://twitter.com/username
* META LINK https://linkedin.com/in/profile
* META LINK https://github.com/username
* ```
*
* Multiple `META LINK` commitments can be used when there are multiple sources:
*
* ```book
* META LINK https://twitter.com/username
* META LINK https://linkedin.com/in/profile
* ```
*
* @private [๐ช] Maybe export the commitments through some package
*/
class MetaLinkCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('META LINK');
}
/**
* Short one-line description of META LINK.
*/
get description() {
return 'Provide profile/source links for the person the agent models.';
}
/**
* Markdown documentation for META LINK commitment.
*/
get documentation() {
return spaceTrim(`
# META LINK
Represents a profile or source link for the person the agent is modeled after.
## Key behaviors
- Does not modify the agent's behavior or responses.
- Multiple \`META LINK\` commitments can be used for different social profiles.
- Used for attribution and crediting the original person.
- Displayed in user interfaces for transparency.
## Examples
\`\`\`book
Expert Consultant
META LINK https://twitter.com/expertname
META LINK https://linkedin.com/in/expertprofile
PERSONA You are Dr. Smith, a renowned expert in artificial intelligence
KNOWLEDGE Extensive background in machine learning and neural networks
\`\`\`
\`\`\`book
Open Source Developer
META LINK https://github.com/developer
META LINK https://twitter.com/devhandle
PERSONA You are an experienced open source developer
ACTION Can help with code reviews and architecture decisions
STYLE Be direct and technical in explanations
\`\`\`
`);
}
applyToAgentModelRequirements(requirements, content) {
// META LINK doesn't modify the system message or model requirements
// It's handled separately in the parsing logic for profile link extraction
// This method exists for consistency with the CommitmentDefinition interface
return requirements;
}
/**
* Extracts the profile link URL from the content
* This is used by the parsing logic
*/
extractProfileLinkUrl(content) {
const trimmedContent = content.trim();
return trimmedContent || null;
}
/**
* Validates if the provided content is a valid URL
*/
isValidUrl(content) {
try {
new URL(content.trim());
return true;
}
catch (_a) {
return false;
}
}
}
/**
* Singleton instance of the META LINK commitment definition
*
* @private [๐ช] Maybe export the commitments through some package
*/
new MetaLinkCommitmentDefinition();
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* MODEL commitment definition
*
* The MODEL commitment specifies which AI model to use and can also set
* model-specific parameters like temperature, topP, and topK.
*
* Example usage in agent source:
*
* ```book
* MODEL gpt-4
* MODEL claude-3-opus temperature=0.3
* MODEL gpt-3.5-turbo temperature=0.8 topP=0.9
* ```
*
* @private [๐ช] Maybe export the commitments through some package
*/
class ModelCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('MODEL');
}
/**
* Short one-line description of MODEL.
*/
get description() {
return 'Select the AI model and optional decoding parameters.';
}
/**
* Markdown documentation for MODEL commitment.
*/
get documentation() {
return spaceTrim(`
# MODEL
Specifies which AI model to use and optional decoding parameters.
## Key behaviors
- Only one \`MODEL\` commitment should be used per agent.
- If multiple are specified, the last one takes precedence.
- Parameters control the randomness and creativity of responses.
## Supported parameters
- \`temperature\`: Controls randomness (0.0 = deterministic, 1.0+ = creative)
- \`topP\` (aka \`top_p\`): Nucleus sampling parameter
- \`topK\` (aka \`top_k\`): Top-k sampling parameter
## Examples
\`\`\`book
Precise Assistant
PERSONA You are a precise and accurate assistant
MODEL gpt-4 temperature=0.1
RULE Always provide factual information
\`\`\`
\`\`\`book
Creative Writer
PERSONA You are a creative writing assistant
MODEL claude-3-opus temperature=0.8 topP=0.9
STYLE Be imaginative and expressive
ACTION Can help with storytelling and character development
\`\`\`
`);
}
applyToAgentModelRequirements(requirements, content) {
const trimmedContent = content.trim();
if (!trimmedContent) {
return requirements;
}
// Parse the model specification
const parts = trimmedContent.split(/\s+/);
const modelName = parts[0];
if (!modelName) {
return requirements;
}
// Start with the model name
const updatedRequirements = {
...requirements,
modelName,
};
// Parse additional parameters
const result = { ...updatedRequirements };
for (let i = 1; i < parts.length; i++) {
const param = parts[i];
if (param && param.includes('=')) {
const [key, value] = param.split('=');
if (key && value) {
const numValue = parseFloat(value);
if (!isNaN(numValue)) {
switch (key.toLowerCase()) {
case 'temperature':
result.temperature = numValue;
break;
case 'topp':
case 'top_p':
result.topP = numValue;
break;
case 'topk':
case 'top_k':
result.topK = Math.round(numValue);
break;
}
}
}
}
}
return result;
}
}
/**
* Singleton instance of the MODEL commitment definition
*
* @private [๐ช] Maybe export the commitments through some package
*/
new ModelCommitmentDefinition();
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* NOTE commitment definition
*
* The NOTE commitment is used to add comments to the agent source without making any changes
* to the system message or agent model requirements. It serves as a documentation mechanism
* for developers to add explanatory comments, reminders, or annotations directly in the agent source.
*
* Key features:
* - Makes no changes to the system message
* - Makes no changes to agent model requirements
* - Content is preserved in metadata.NOTE for debugging and inspection
* - Multiple NOTE commitments are aggregated together
* - Comments (# NOTE) are removed from the final system message
*
* Example usage in agent source:
*
* ```book
* NOTE This agent was designed for customer support scenarios
* NOTE Remember to update the knowledge base monthly
* NOTE Performance optimized for quick response times
* ```
*
* The above notes will be stored in metadata but won't affect the agent's behavior.
*
* @private [๐ช] Maybe export the commitments through some package
*/
class NoteCommitmentDefinition extends BaseCommitmentDefinition {
constructor() {
super('NOTE');
}
/**
* Short one-line description of NOTE.
*/
get description() {
return 'Add developer-facing notes without changing behavior or output.';
}
/**
* Markdown