diff --git a/packages/cli/package.json b/packages/cli/package.json index 3fad801ef65..981457528ad 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "5.5.2", + "version": "5.5.3", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", @@ -28,7 +28,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.4", "@botpress/client": "1.33.0", - "@botpress/sdk": "5.3.4", + "@botpress/sdk": "5.4.0", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/verel": "^0.2.0", diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index 4cc28757dc1..4104bed9a0b 100644 --- a/packages/cli/templates/empty-bot/package.json +++ b/packages/cli/templates/empty-bot/package.json @@ -6,7 +6,7 @@ "private": true, "dependencies": { "@botpress/client": "1.33.0", - "@botpress/sdk": "5.3.4" + "@botpress/sdk": "5.4.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index 35d4868d68e..a8aa93bfde8 100644 --- a/packages/cli/templates/empty-integration/package.json +++ b/packages/cli/templates/empty-integration/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.33.0", - "@botpress/sdk": "5.3.4" + "@botpress/sdk": "5.4.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index 8291252f15d..f283ed3490b 100644 --- a/packages/cli/templates/empty-plugin/package.json +++ b/packages/cli/templates/empty-plugin/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/sdk": "5.3.4" + "@botpress/sdk": "5.4.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index 268ab132599..317534dd9f3 100644 --- a/packages/cli/templates/hello-world/package.json +++ b/packages/cli/templates/hello-world/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.33.0", - "@botpress/sdk": "5.3.4" + "@botpress/sdk": "5.4.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index f91a23a00b9..a7092ef9452 100644 --- a/packages/cli/templates/webhook-message/package.json +++ b/packages/cli/templates/webhook-message/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.33.0", - "@botpress/sdk": "5.3.4", + "@botpress/sdk": "5.4.0", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/cognitive/package.json b/packages/cognitive/package.json index 03c3e8e7f96..b3a73b565e8 100644 --- a/packages/cognitive/package.json +++ b/packages/cognitive/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cognitive", - "version": "0.3.10", + "version": "0.3.11", "description": "Wrapper around the Botpress Client to call LLMs", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/llmz/package.json b/packages/llmz/package.json index e8b56c704e3..bbdfc40328f 100644 --- a/packages/llmz/package.json +++ b/packages/llmz/package.json @@ -2,7 +2,7 @@ "name": "llmz", "type": "module", "description": "LLMz - An LLM-native Typescript VM built on top of Zui", - "version": "0.0.48", + "version": "0.0.50", "types": "./dist/index.d.ts", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -72,7 +72,7 @@ }, "peerDependencies": { "@botpress/client": "1.33.0", - "@botpress/cognitive": "0.3.10", + "@botpress/cognitive": "0.3.11", "@bpinternal/thicktoken": "^1.0.5", "@bpinternal/zui": "^1.3.2" }, diff --git a/packages/llmz/src/truncator.ts b/packages/llmz/src/truncator.ts index 3aa904e3f55..4cadd9cce87 100644 --- a/packages/llmz/src/truncator.ts +++ b/packages/llmz/src/truncator.ts @@ -13,8 +13,13 @@ const DEFAULT_REMOVE_CHUNK = 250 const WRAP_OPEN_TAG_1 = '【TRUNCATE' const WRAP_OPEN_TAG_2 = '】' const WRAP_CLOSE_TAG = '【/TRUNCATE】' -const getRegex = () => - new RegExp(`(${WRAP_OPEN_TAG_1}(?:\\s+[\\w:]+)*\\s*${WRAP_OPEN_TAG_2})([\\s\\S]*?)(${WRAP_CLOSE_TAG})`, 'g') +const REGEXP = `(${WRAP_OPEN_TAG_1}(?:\\s+[\\w:]+)*\\s*${WRAP_OPEN_TAG_2})([\\s\\S]*?)(${WRAP_CLOSE_TAG})` + +type ParsedMessageContent = { + attributes: SerializedTruncateOptions + wrappedContent: string | undefined + nonTruncatableContent: string | undefined +} type TruncateOptions = { preserve: 'top' | 'bottom' | 'both' @@ -27,6 +32,22 @@ type TruncateOptions = { minTokens: number } +type SerializedTruncateOptions = { + preserve: 'top' | 'bottom' | 'both' + flex: string + min: string +} + +type Part = { + /** the current remaining content */ + content: string + /** the current remaining tokens */ + tokens: number + /** if part is inside a tag, then it's truncatable. when outside the wrapper, it's not truncatable */ + truncatable: boolean + attributes?: Partial +} + const DEFAULT_TRUNCATE_OPTIONS: TruncateOptions = { preserve: 'top', flex: 1, @@ -143,44 +164,26 @@ export function truncateWrappedContent({ }: Options): T[] { const tokenizer = getTokenizer() - type Part = { - /** the current remaining content */ - content: string - /** the current remaining tokens */ - tokens: number - /** if part is inside a tag, then it's truncatable. when outside the wrapper, it's not truncatable */ - truncatable: boolean - attributes?: Partial - } - /** * Before { content: 'content', tokens: 10, truncatable: false } * content { content: 'content', tokens: 10, truncatable: true } * After { content: 'content', tokens: 10, truncatable: false } */ - const parts: Array = [] - + const parts: Part[][] = [] // Split messages into parts and calculate initial tokens for (const msg of messages) { const current: Part[] = [] const content = typeof msg.content === 'string' ? msg.content : '' - let match - const regex = getRegex() - let lastIndex = 0 + let match: ParsedMessageContent | null + const parser = new _MessageContentParser() - while ((match = regex.exec(content)) !== null) { + while ((match = parser.parse(content)) !== null) { // Extract attributes from the open tag - const attributes = match[1]! - .split(/\s+/) - .slice(1) - .filter((x) => x !== WRAP_OPEN_TAG_2) - .map((x) => x.split(':')) - .reduce((acc, [key, value]) => ({ ...acc, [key!]: value }), {} as Record) - - if (match.index > lastIndex) { - const nonTruncatableContent = content.slice(lastIndex, match.index) + const { attributes, nonTruncatableContent, wrappedContent } = match + + if (nonTruncatableContent) { current.push({ content: nonTruncatableContent, tokens: tokenizer.count(nonTruncatableContent), @@ -188,7 +191,6 @@ export function truncateWrappedContent({ }) } - const wrappedContent = match[2] current.push({ content: wrappedContent!, tokens: tokenizer.count(wrappedContent!), @@ -199,12 +201,10 @@ export function truncateWrappedContent({ minTokens: Number(attributes.min) || DEFAULT_TRUNCATE_OPTIONS.minTokens, }, }) - - lastIndex = regex.lastIndex } - if (lastIndex < content.length) { - const remainingContent = content.slice(lastIndex) + const remainingContent = parser.getRemainingContent(content) + if (remainingContent) { current.push({ content: remainingContent, tokens: tokenizer.count(remainingContent), @@ -215,39 +215,13 @@ export function truncateWrappedContent({ parts.push(current) } - const getCount = () => parts.reduce((acc, x) => acc + x.reduce((acc, y) => acc + y.tokens, 0), 0) - const getTwoBiggestTruncatables = () => { - let biggest: Part | null = null - let secondBiggest: Part | null = null - - for (const part of parts.flat()) { - if (part.truncatable) { - const flex = part.attributes?.flex ?? DEFAULT_TRUNCATE_OPTIONS.flex - const tokens = part.tokens * flex - - if (part.tokens <= (part.attributes?.minTokens ?? 0)) { - continue - } - - if (!biggest || tokens > biggest.tokens) { - secondBiggest = biggest - biggest = part - } else if (!secondBiggest || tokens > secondBiggest.tokens) { - secondBiggest = part - } - } - } - - return { biggest, secondBiggest } - } - - let currentCount = getCount() + let currentCount = _countTotalTokens(parts) while (currentCount > tokenLimit) { - const { biggest, secondBiggest } = getTwoBiggestTruncatables() + const { biggest, secondBiggest } = _getTwoBiggestTruncables(parts) if (!biggest || !biggest.truncatable || biggest.tokens <= 0) { if (throwOnFailure) { - throw new Error(`Cannot truncate further, current count: ${getCount()}`) + throw new Error(`Cannot truncate further, current count: ${currentCount}`) } else { break } @@ -259,7 +233,7 @@ export function truncateWrappedContent({ if (toRemove <= 0) { if (throwOnFailure) { - throw new Error(`Cannot truncate further, current count: ${getCount()}`) + throw new Error(`Cannot truncate further, current count: ${currentCount}`) } else { break } @@ -290,10 +264,6 @@ export function truncateWrappedContent({ currentCount -= toRemove } - const removeRedundantWrappers = (content: string) => { - return content.replace(getRegex(), '$2') - } - // Reconstruct the messages return messages.map((msg, i) => { const p = parts[i]! @@ -301,18 +271,84 @@ export function truncateWrappedContent({ ...msg, content: typeof msg.content === 'string' - ? removeRedundantWrappers( - p - .map((part) => { - if (part.truncatable) { - return part.content - } - - return part.content - }) - .join('') - ) + ? _renderRemainingWrappers(p.map((part) => part.content).join('')) : msg.content, } }) } + +class _MessageContentParser { + private _regex: RegExp + private _lastIndex: number = 0 + + public constructor() { + this._regex = _createRegex() + } + + public parse(content: string): ParsedMessageContent | null { + const match = this._regex.exec(content) + if (!match) { + return null + } + + const attributes = match[1]! + .split(/\s+/) + .slice(1) + .filter((x) => x !== WRAP_OPEN_TAG_2) + .map((x) => x.split(':')) + .reduce( + (acc, [key, value]) => ({ ...acc, [key!]: value }), + {} as Record + ) as SerializedTruncateOptions + + let nonTruncatableContent: string | undefined = undefined + if (match.index > this._lastIndex) { + nonTruncatableContent = content.slice(this._lastIndex, match.index) + } + + const wrappedContent = match[2] + + this._lastIndex = this._regex.lastIndex + return { attributes, nonTruncatableContent, wrappedContent } + } + + public getRemainingContent(content: string): string | null { + if (this._lastIndex < content.length) { + const remainingContent = content.slice(this._lastIndex) + return remainingContent + } + return null + } +} + +const _createRegex = () => new RegExp(REGEXP, 'g') + +const _renderRemainingWrappers = (content: string) => content.replace(_createRegex(), '$2') + +const _countTotalTokens = (parts: Part[][]) => + parts.reduce((acc, x) => acc + x.reduce((acc, y) => acc + y.tokens, 0), 0) + +const _getTwoBiggestTruncables = (parts: Part[][]) => { + let biggest: Part | null = null + let secondBiggest: Part | null = null + + for (const part of parts.flat()) { + if (part.truncatable) { + if (part.tokens <= (part.attributes?.minTokens ?? 0)) { + continue + } + + const flex = part.attributes?.flex ?? DEFAULT_TRUNCATE_OPTIONS.flex + const tokens = part.tokens * flex + + if (!biggest || tokens > biggest.tokens) { + secondBiggest = biggest + biggest = part + } else if (!secondBiggest || tokens > secondBiggest.tokens) { + secondBiggest = part + } + } + } + + return { biggest, secondBiggest } +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 61e78019589..71982743d3d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "5.3.4", + "version": "5.4.0", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/message.ts b/packages/sdk/src/message.ts index 23fb62ca395..d8aaa5d0964 100644 --- a/packages/sdk/src/message.ts +++ b/packages/sdk/src/message.ts @@ -15,14 +15,17 @@ const markdownMessageSchema = z.object({ const imageMessageSchema = z.object({ imageUrl: NonEmptyString, + title: NonEmptyString.optional(), }) const audioMessageSchema = z.object({ audioUrl: NonEmptyString, + title: NonEmptyString.optional(), }) const videoMessageSchema = z.object({ videoUrl: NonEmptyString, + title: NonEmptyString.optional(), }) const fileMessageSchema = z.object({ diff --git a/packages/vai/package.json b/packages/vai/package.json index 2ffc49e851d..328b484a674 100644 --- a/packages/vai/package.json +++ b/packages/vai/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/vai", - "version": "0.0.13", + "version": "0.0.14", "description": "Vitest AI (vai) – a vitest extension for testing with LLMs", "types": "./dist/index.d.ts", "exports": { diff --git a/packages/zai/package.json b/packages/zai/package.json index b04e25b674a..a6a2ecdf7c5 100644 --- a/packages/zai/package.json +++ b/packages/zai/package.json @@ -1,7 +1,7 @@ { "name": "@botpress/zai", "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API", - "version": "2.5.13", + "version": "2.5.14", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -32,7 +32,7 @@ "author": "", "license": "ISC", "dependencies": { - "@botpress/cognitive": "0.3.10", + "@botpress/cognitive": "0.3.11", "json5": "^2.2.3", "jsonrepair": "^3.10.0", "lodash-es": "^4.17.21", diff --git a/plugins/conversation-insights/package.json b/plugins/conversation-insights/package.json index a2409cb416b..af80bee5554 100644 --- a/plugins/conversation-insights/package.json +++ b/plugins/conversation-insights/package.json @@ -7,7 +7,7 @@ }, "private": true, "dependencies": { - "@botpress/cognitive": "0.3.10", + "@botpress/cognitive": "0.3.11", "@botpress/sdk": "workspace:*", "browser-or-node": "^2.1.1", "jsonrepair": "^3.10.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12c1fb903d1..e1eef5b8373 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2505,7 +2505,7 @@ importers: specifier: 1.33.0 version: link:../client '@botpress/sdk': - specifier: 5.3.4 + specifier: 5.4.0 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2629,7 +2629,7 @@ importers: specifier: 1.33.0 version: link:../../../client '@botpress/sdk': - specifier: 5.3.4 + specifier: 5.4.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2645,7 +2645,7 @@ importers: specifier: 1.33.0 version: link:../../../client '@botpress/sdk': - specifier: 5.3.4 + specifier: 5.4.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2658,7 +2658,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 5.3.4 + specifier: 5.4.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2674,7 +2674,7 @@ importers: specifier: 1.33.0 version: link:../../../client '@botpress/sdk': - specifier: 5.3.4 + specifier: 5.4.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2690,7 +2690,7 @@ importers: specifier: 1.33.0 version: link:../../../client '@botpress/sdk': - specifier: 5.3.4 + specifier: 5.4.0 version: link:../../../sdk axios: specifier: ^1.6.8 @@ -2844,7 +2844,7 @@ importers: specifier: 1.33.0 version: link:../client '@botpress/cognitive': - specifier: 0.3.10 + specifier: 0.3.11 version: link:../cognitive '@bpinternal/thicktoken': specifier: ^1.0.5 @@ -3030,7 +3030,7 @@ importers: packages/zai: dependencies: '@botpress/cognitive': - specifier: 0.3.10 + specifier: 0.3.11 version: link:../cognitive '@bpinternal/thicktoken': specifier: ^1.0.0 @@ -3149,7 +3149,7 @@ importers: plugins/conversation-insights: dependencies: '@botpress/cognitive': - specifier: 0.3.10 + specifier: 0.3.11 version: link:../../packages/cognitive '@botpress/sdk': specifier: workspace:*