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:*