Global variables support external variable; Extract module support default value (#921)

This commit is contained in:
Archer 2024-03-05 14:13:22 +08:00 committed by GitHub
parent 42a8184ea0
commit af581bc903
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 306 additions and 200 deletions

View File

@ -9,7 +9,7 @@ weight: 1221
最后更新时间2024年3月3日 最后更新时间2024年3月3日
我们非常重视您的隐私保护在您使用FastGPT云服务时我们将按照以下政策收集、使用、披露和保护您的个人信息。请您仔细阅读并充分理解本隐私政策。 我们非常重视您的隐私保护在您使用FastGPT云服务时(以下简称为“本服务”),我们将按照以下政策收集、使用、披露和保护您的个人信息。请您仔细阅读并充分理解本隐私政策。
**我们可能需要收集的信息** **我们可能需要收集的信息**
@ -39,7 +39,7 @@ weight: 1221
2. 我们会定期对收集、存储和处理的个人信息进行安全评估,以确保个人信息安全。 2. 我们会定期对收集、存储和处理的个人信息进行安全评估,以确保个人信息安全。
3. 在发生个人信息泄露等安全事件时,我们会立即启动应急预案,并在法律法规规定的范围内向您及时告知。 3. 在发生个人信息泄露等安全事件时,我们会立即启动应急预案,并在法律法规规定的范围内向您及时告知。
4. 我们不会使用您的数据进行额外的备份存储或用于模型训练。 4. 我们不会使用您的数据进行额外的备份存储或用于模型训练。
5. 您在本服务进行的数据删除均为物理删除,不可恢复。如有非物理删除的操作,我们会在服务中特别指出。 5. 您在本服务进行的数据删除均为物理删除,不可恢复。如有非物理删除的操作,我们会在服务中特别指出。
**用户权利** **用户权利**

View File

@ -169,6 +169,8 @@ curl --location --request POST '{{host}}/shareAuth/start' \
响应值与[chat 接口格式相同](/docs/development/openapi/chat/#响应),仅多了一个`token` 响应值与[chat 接口格式相同](/docs/development/openapi/chat/#响应),仅多了一个`token`
重点关注:`totalPoints`(总消耗AI积分)`token`(Token消耗总数)
```bash ```bash
curl --location --request POST '{{host}}/shareAuth/finish' \ curl --location --request POST '{{host}}/shareAuth/finish' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \

View File

@ -20,15 +20,21 @@ curl --location --request POST 'https://{{host}}/api/init/v469' \
1. 重置计量表。 1. 重置计量表。
2. 执行脏数据清理(清理无效的文件、清理无效的图片、清理无效的知识库集合、清理无效的向量) 2. 执行脏数据清理(清理无效的文件、清理无效的图片、清理无效的知识库集合、清理无效的向量)
## 外部接口更新
1. 由于计费系统变更,[分享链接对话上报接口](/docs/development/openapi/share/#5-编写对话结果上报接口可选)需要做一些调整price字段被totalPoints字段取代。inputToken和outputToken不再提供只提供`token`字段总token数量
## V4.6.9 更新说明 ## V4.6.9 更新说明
1. 商业版新增 - 知识库新增“增强处理”训练模式,可生成更多类型索引。 1. 商业版新增 - 知识库新增“增强处理”训练模式,可生成更多类型索引。
2. 新增 - 完善了HTTP模块的变量提示。 2. 新增 - 完善了HTTP模块的变量提示。
3. 新增 - HTTP模块支持OpenAI单接口导入。 3. 新增 - HTTP模块支持OpenAI单接口导入。
4. 优化 - 问题补全。增加英文类型。同时可以设置为单独模块,方便复用。 4. 新增 - 全局变量支持增加外部变量。可通过分享链接的Query或 API 的 variables 参数传入。
5. 优化 - 重写了计量模式 5. 新增 - 内容提取模块增加默认值。
6. 优化 - Token 过滤历史记录,保持偶数条,防止部分模型报错。 6. 优化 - 问题补全。增加英文类型。同时可以设置为单独模块,方便复用。
7. 优化 - 分享链接SEO可直接展示应用名和头像。 7. 优化 - 重写了计量模式
8. 修复 - 标注功能。 8. 优化 - Token 过滤历史记录,保持偶数条,防止部分模型报错。
9. 修复 - qa生成线程计数错误。 9. 优化 - 分享链接SEO可直接展示应用名和头像。
10. 修复 - 标注功能。
11. 修复 - qa生成线程计数错误。
12. 修复 - 问题分类连线类型错误

View File

@ -116,17 +116,29 @@ export enum ModuleOutputKeyEnum {
export enum VariableInputEnum { export enum VariableInputEnum {
input = 'input', input = 'input',
textarea = 'textarea', textarea = 'textarea',
select = 'select' select = 'select',
external = 'external'
} }
export const variableMap = { export const variableMap = {
[VariableInputEnum.input]: { [VariableInputEnum.input]: {
icon: 'core/app/variable/input' icon: 'core/app/variable/input',
title: 'core.module.variable.input type',
desc: ''
}, },
[VariableInputEnum.textarea]: { [VariableInputEnum.textarea]: {
icon: 'core/app/variable/textarea' icon: 'core/app/variable/textarea',
title: 'core.module.variable.textarea type',
desc: '允许用户最多输入4000字的对话框。'
}, },
[VariableInputEnum.select]: { [VariableInputEnum.select]: {
icon: 'core/app/variable/select' icon: 'core/app/variable/select',
title: 'core.module.variable.select type',
desc: ''
},
[VariableInputEnum.external]: {
icon: 'core/app/variable/external',
title: 'core.module.variable.External type',
desc: '可以通过API接口或分享链接的Query传递变量。增加该类型变量的主要目的是用于变量提示。使用例子: 你可以通过分享链接Query中拼接Token来实现内部系统身份鉴权。'
} }
}; };

View File

@ -57,7 +57,7 @@ export const ContextExtractModule: FlowModuleTemplateType = {
{ {
key: ModuleInputKeyEnum.extractKeys, key: ModuleInputKeyEnum.extractKeys,
type: FlowNodeInputTypeEnum.custom, type: FlowNodeInputTypeEnum.custom,
label: '目标字段', label: '',
valueType: ModuleIOValueTypeEnum.any, valueType: ModuleIOValueTypeEnum.any,
description: "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段", description: "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段",
value: [], // {desc: string; key: string; required: boolean; enum: string[]}[] value: [], // {desc: string; key: string; required: boolean; enum: string[]}[]
@ -76,6 +76,7 @@ export const ContextExtractModule: FlowModuleTemplateType = {
{ {
key: ModuleOutputKeyEnum.failed, key: ModuleOutputKeyEnum.failed,
label: '提取字段缺失', label: '提取字段缺失',
description: '存在一个或多个字段未提取成功。尽管使用了默认值也算缺失。',
valueType: ModuleIOValueTypeEnum.boolean, valueType: ModuleIOValueTypeEnum.boolean,
type: FlowNodeOutputTypeEnum.source, type: FlowNodeOutputTypeEnum.source,
targets: [] targets: []

View File

@ -80,6 +80,7 @@ export type ContextExtractAgentItemType = {
desc: string; desc: string;
key: string; key: string;
required: boolean; required: boolean;
defaultValue?: string;
enum?: string; enum?: string;
}; };

View File

@ -1,3 +1,5 @@
import { delay } from '@fastgpt/global/common/system/utils';
import { addLog } from '../../system/log';
import { Pool } from 'pg'; import { Pool } from 'pg';
import type { QueryResultRow } from 'pg'; import type { QueryResultRow } from 'pg';
@ -15,10 +17,13 @@ export const connectPg = async (): Promise<Pool> => {
connectionTimeoutMillis: 20000 connectionTimeoutMillis: 20000
}); });
global.pgClient.on('error', (err) => { global.pgClient.on('error', async (err) => {
console.log(err); console.log(err);
global.pgClient?.end(); global.pgClient?.end();
global.pgClient = null; global.pgClient = null;
await delay(1000);
addLog.info(`Retry connect pg`);
connectPg(); connectPg();
}); });
@ -27,7 +32,12 @@ export const connectPg = async (): Promise<Pool> => {
console.log('pg connected'); console.log('pg connected');
return global.pgClient; return global.pgClient;
} catch (error) { } catch (error) {
global.pgClient?.end();
global.pgClient = null; global.pgClient = null;
await delay(1000);
addLog.info(`Retry connect pg`);
return connectPg(); return connectPg();
} }
}; };

View File

@ -69,6 +69,7 @@ export const iconPaths = {
'core/app/simpleMode/tts': () => import('./icons/core/app/simpleMode/tts.svg'), 'core/app/simpleMode/tts': () => import('./icons/core/app/simpleMode/tts.svg'),
'core/app/simpleMode/variable': () => import('./icons/core/app/simpleMode/variable.svg'), 'core/app/simpleMode/variable': () => import('./icons/core/app/simpleMode/variable.svg'),
'core/app/ttsFill': () => import('./icons/core/app/ttsFill.svg'), 'core/app/ttsFill': () => import('./icons/core/app/ttsFill.svg'),
'core/app/variable/external': () => import('./icons/core/app/variable/external.svg'),
'core/app/variable/input': () => import('./icons/core/app/variable/input.svg'), 'core/app/variable/input': () => import('./icons/core/app/variable/input.svg'),
'core/app/variable/select': () => import('./icons/core/app/variable/select.svg'), 'core/app/variable/select': () => import('./icons/core/app/variable/select.svg'),
'core/app/variable/textarea': () => import('./icons/core/app/variable/textarea.svg'), 'core/app/variable/textarea': () => import('./icons/core/app/variable/textarea.svg'),

View File

@ -0,0 +1,6 @@
<svg t="1709524607561" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5121"
width="128" height="128">
<path
d="M310.4 454.4v-118.4h435.2c44.8 0 86.4 25.6 105.6 64 16-22.4 22.4-48 22.4-73.6 0-73.6-57.6-131.2-131.2-131.2H310.4V76.8c0-16-19.2-28.8-35.2-19.2L9.6 246.4c-12.8 9.6-12.8 25.6 0 35.2l268.8 188.8c12.8 12.8 32 3.2 32-16zM1014.4 742.4l-268.8-188.8c-12.8-9.6-35.2 0-35.2 19.2v118.4H278.4c-44.8 0-86.4-25.6-105.6-64-16 22.4-22.4 48-22.4 73.6 0 73.6 57.6 131.2 131.2 131.2h432v118.4c0 16 19.2 28.8 35.2 19.2l268.8-188.8c9.6-12.8 9.6-32-3.2-38.4z"
p-id="5122"></path>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@ -2,7 +2,7 @@ import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { editorStateToText } from './utils'; import { editorStateToText } from './utils';
import Editor from './Editor'; import Editor from './Editor';
import MyModal from '../../MyModal'; import MyModal from '../../CustomModal';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { $getRoot, EditorState, type LexicalEditor } from 'lexical'; import { $getRoot, EditorState, type LexicalEditor } from 'lexical';
import { EditorVariablePickerType } from './type.d'; import { EditorVariablePickerType } from './type.d';

11
pnpm-lock.yaml generated
View File

@ -317,6 +317,9 @@ importers:
jschardet: jschardet:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
json5:
specifier: ^2.2.3
version: 2.2.3
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
@ -5128,7 +5131,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001591 caniuse-lite: 1.0.30001593
electron-to-chromium: 1.4.690 electron-to-chromium: 1.4.690
node-releases: 2.0.14 node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0) update-browserslist-db: 1.0.13(browserslist@4.23.0)
@ -5196,8 +5199,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/caniuse-lite@1.0.30001591: /caniuse-lite@1.0.30001593:
resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==} resolution: {integrity: sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==}
/canvas@2.11.2: /canvas@2.11.2:
resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==}
@ -9124,7 +9127,7 @@ packages:
'@next/env': 13.5.2 '@next/env': 13.5.2
'@swc/helpers': 0.5.2 '@swc/helpers': 0.5.2
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001591 caniuse-lite: 1.0.30001593
postcss: 8.4.14 postcss: 8.4.14
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)

View File

@ -37,6 +37,7 @@
"immer": "^9.0.19", "immer": "^9.0.19",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jschardet": "^3.0.0", "jschardet": "^3.0.0",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mermaid": "^10.2.3", "mermaid": "^10.2.3",

View File

@ -3,7 +3,9 @@
1. 新增 - 知识库新增“增强处理”训练模式,可生成更多类型索引。 1. 新增 - 知识库新增“增强处理”训练模式,可生成更多类型索引。
2. 新增 - 完善了HTTP模块的变量提示。 2. 新增 - 完善了HTTP模块的变量提示。
3. 新增 - HTTP模块支持OpenAI单接口导入。 3. 新增 - HTTP模块支持OpenAI单接口导入。
4. 优化 - 问题补全。增加英文类型。同时可以设置为单独模块,方便复用。 4. 新增 - 全局变量支持增加外部变量。可通过分享链接的Query或 API 的 variables 参数传入。
5. [点击查看高级编排介绍文档](https://doc.fastgpt.in/docs/workflow/intro) 5. 新增 - 内容提取模块增加默认值。
6. [使用文档](https://doc.fastgpt.in/docs/intro/) 6. 优化 - 问题补全。增加英文类型。同时可以设置为单独模块,方便复用。
7. [点击查看商业版](https://doc.fastgpt.in/docs/commercial/) 7. [点击查看高级编排介绍文档](https://doc.fastgpt.in/docs/workflow/intro)
8. [使用文档](https://doc.fastgpt.in/docs/intro/)
9. [点击查看商业版](https://doc.fastgpt.in/docs/commercial/)

View File

@ -774,6 +774,8 @@
"Input description": "", "Input description": "",
"label": "Dataset quote" "label": "Dataset quote"
}, },
"Default value": "Default ",
"Default value placeholder": "Null characters are returned by default",
"Field Description": "Description", "Field Description": "Description",
"Field Name": "Name", "Field Name": "Name",
"Field Type": "Type", "Field Type": "Type",
@ -795,10 +797,14 @@
"Field Edit": "Field Edit" "Field Edit": "Field Edit"
}, },
"extract": { "extract": {
"Add field": "Add",
"Enum Description": "Lists the possible values for the field, one per row", "Enum Description": "Lists the possible values for the field, one per row",
"Enum Value": "Enum", "Enum Value": "Enum",
"Field Description Placeholder": "Name/age /sql statement......", "Field Description Placeholder": "Name/age /sql statement......",
"Field Setting Title": "Extract field configuration" "Field Setting Title": "Extract field configuration",
"Required": "Required",
"Required Description": "Even if the field cannot be extracted, it is returned with the default value",
"Target field": "Field"
}, },
"http": { "http": {
"Add props": "Add props", "Add props": "Add props",
@ -939,6 +945,7 @@
"string": "String" "string": "String"
}, },
"variable": { "variable": {
"External type": "External",
"add option": "Add Option", "add option": "Add Option",
"input type": "Text", "input type": "Text",
"key": "Key", "key": "Key",

View File

@ -776,6 +776,8 @@
"Input description": "可接收知识库搜索的结果。", "Input description": "可接收知识库搜索的结果。",
"label": "知识库引用" "label": "知识库引用"
}, },
"Default value": "默认值",
"Default value placeholder": "不填则默认返回空字符",
"Field Description": "字段描述", "Field Description": "字段描述",
"Field Name": "字段名", "Field Name": "字段名",
"Field Type": "字段类型", "Field Type": "字段类型",
@ -797,10 +799,14 @@
"Field Edit": "字段编辑" "Field Edit": "字段编辑"
}, },
"extract": { "extract": {
"Add field": "新增字段",
"Enum Description": "列举出该字段可能的值,每行一个", "Enum Description": "列举出该字段可能的值,每行一个",
"Enum Value": "枚举值", "Enum Value": "枚举值",
"Field Description Placeholder": "姓名/年龄/sql语句……", "Field Description Placeholder": "姓名/年龄/sql语句……",
"Field Setting Title": "提取字段配置" "Field Setting Title": "提取字段配置",
"Required": "必须返回",
"Required Description": "即使无法提取该字段,也会使用默认值进行返回",
"Target field": "目标字段"
}, },
"http": { "http": {
"Add props": "添加参数", "Add props": "添加参数",
@ -941,6 +947,7 @@
"string": "字符串" "string": "字符串"
}, },
"variable": { "variable": {
"External type": "外部传入",
"add option": "添加选项", "add option": "添加选项",
"input type": "文本", "input type": "文本",
"key": "变量 key", "key": "变量 key",

View File

@ -186,6 +186,10 @@ const ChatBox = (
() => splitGuideModule(userGuideModule), () => splitGuideModule(userGuideModule),
[userGuideModule] [userGuideModule]
); );
const filterVariableModules = useMemo(
() => variableModules.filter((item) => item.type !== VariableInputEnum.external),
[variableModules]
);
// compute variable input is finish. // compute variable input is finish.
const chatForm = useForm<{ const chatForm = useForm<{
@ -200,17 +204,18 @@ const ChatBox = (
const [variableInputFinish, setVariableInputFinish] = useState(false); // clicked start chat button const [variableInputFinish, setVariableInputFinish] = useState(false); // clicked start chat button
const variableIsFinish = useMemo(() => { const variableIsFinish = useMemo(() => {
if (!variableModules || variableModules.length === 0 || chatHistory.length > 0) return true; if (!filterVariableModules || filterVariableModules.length === 0 || chatHistory.length > 0)
return true;
for (let i = 0; i < variableModules.length; i++) { for (let i = 0; i < filterVariableModules.length; i++) {
const item = variableModules[i]; const item = filterVariableModules[i];
if (item.required && !variables[item.key]) { if (item.required && !variables[item.key]) {
return false; return false;
} }
} }
return variableInputFinish; return variableInputFinish;
}, [chatHistory.length, variableInputFinish, variableModules, variables]); }, [chatHistory.length, variableInputFinish, filterVariableModules, variables]);
// 滚动到底部 // 滚动到底部
const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => { const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => {
@ -495,7 +500,7 @@ const ChatBox = (
getChatHistories: () => chatHistory, getChatHistories: () => chatHistory,
resetVariables(e) { resetVariables(e) {
const defaultVal: Record<string, any> = {}; const defaultVal: Record<string, any> = {};
variableModules?.forEach((item) => { filterVariableModules?.forEach((item) => {
defaultVal[item.key] = ''; defaultVal[item.key] = '';
}); });
@ -519,13 +524,13 @@ const ChatBox = (
feConfigs?.show_emptyChat && feConfigs?.show_emptyChat &&
showEmptyIntro && showEmptyIntro &&
chatHistory.length === 0 && chatHistory.length === 0 &&
!variableModules?.length && !filterVariableModules?.length &&
!welcomeText, !welcomeText,
[ [
chatHistory.length, chatHistory.length,
feConfigs?.show_emptyChat, feConfigs?.show_emptyChat,
showEmptyIntro, showEmptyIntro,
variableModules?.length, filterVariableModules?.length,
welcomeText welcomeText
] ]
); );
@ -604,10 +609,10 @@ const ChatBox = (
{showEmpty && <Empty />} {showEmpty && <Empty />}
{!!welcomeText && <WelcomeText appAvatar={appAvatar} welcomeText={welcomeText} />} {!!welcomeText && <WelcomeText appAvatar={appAvatar} welcomeText={welcomeText} />}
{/* variable input */} {/* variable input */}
{!!variableModules?.length && ( {!!filterVariableModules?.length && (
<VariableInput <VariableInput
appAvatar={appAvatar} appAvatar={appAvatar}
variableModules={variableModules} variableModules={filterVariableModules}
variableIsFinish={variableIsFinish} variableIsFinish={variableIsFinish}
chatForm={chatForm} chatForm={chatForm}
onSubmitVariables={onSubmitVariables} onSubmitVariables={onSubmitVariables}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ModalContentProps } from '@chakra-ui/react'; import { ModalContentProps } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import CustomModal from '@fastgpt/web/components/common/MyModal'; import CustomModal from '@fastgpt/web/components/common/CustomModal';
export interface MyModalProps extends ModalContentProps { export interface MyModalProps extends ModalContentProps {
iconSrc?: string; iconSrc?: string;

View File

@ -178,7 +178,7 @@ export const FlowProvider = ({
const source = nodes.find((node) => node.id === connect.source)?.data; const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => { const sourceType = (() => {
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion) { if (source?.flowType === FlowNodeTypeEnum.classifyQuestion) {
return ModuleIOValueTypeEnum.string; return ModuleIOValueTypeEnum.boolean;
} }
if (source?.flowType === FlowNodeTypeEnum.pluginInput) { if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType; return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
@ -189,7 +189,7 @@ export const FlowProvider = ({
const targetType = nodes const targetType = nodes
.find((node) => node.id === connect.target) .find((node) => node.id === connect.target)
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType; ?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
console.log(source, targetType);
if (!sourceType || !targetType) { if (!sourceType || !targetType) {
return toast({ return toast({
status: 'warning', status: 'warning',

View File

@ -25,7 +25,7 @@ import {
useDisclosure useDisclosure
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons'; import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import { VariableInputEnum } from '@fastgpt/global/core/module/constants'; import { VariableInputEnum, variableMap } from '@fastgpt/global/core/module/constants';
import type { VariableItemType } from '@fastgpt/global/core/module/type.d'; import type { VariableItemType } from '@fastgpt/global/core/module/type.d';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@ -52,23 +52,12 @@ const VariableEdit = ({
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const VariableTypeList = useMemo( const VariableTypeList = useMemo(
() => [ () =>
{ Object.entries(variableMap).map(([key, value]) => ({
title: t('core.module.variable.input type'), title: t(value.title),
icon: 'core/app/variable/input', icon: value.icon,
value: VariableInputEnum.input value: key
}, })),
{
title: t('core.module.variable.textarea type'),
icon: 'core/app/variable/textarea',
value: VariableInputEnum.textarea
},
{
title: t('core.module.variable.select type'),
icon: 'core/app/variable/select',
value: VariableInputEnum.select
}
],
[t] [t]
); );
@ -79,9 +68,12 @@ const VariableEdit = ({
getValues: getValuesEdit, getValues: getValuesEdit,
setValue: setValuesEdit, setValue: setValuesEdit,
control: editVariableController, control: editVariableController,
handleSubmit: handleSubmitEdit handleSubmit: handleSubmitEdit,
watch
} = useForm<{ variable: VariableItemType }>(); } = useForm<{ variable: VariableItemType }>();
const variableType = watch('variable.type');
const { const {
fields: selectEnums, fields: selectEnums,
append: appendEnums, append: appendEnums,
@ -140,11 +132,11 @@ const VariableEdit = ({
<Table bg={'white'}> <Table bg={'white'}>
<Thead> <Thead>
<Tr> <Tr>
<Th w={'18px !important'} p={0} /> <Th w={'18px !important'} p={0} bg={'myGray.50'} />
<Th>{t('core.module.variable.variable name')}</Th> <Th bg={'myGray.50'}>{t('core.module.variable.variable name')}</Th>
<Th>{t('core.module.variable.key')}</Th> <Th bg={'myGray.50'}>{t('core.module.variable.key')}</Th>
<Th>{t('common.Require Input')}</Th> <Th bg={'myGray.50'}>{t('common.Require Input')}</Th>
<Th></Th> <Th bg={'myGray.50'}></Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@ -188,12 +180,15 @@ const VariableEdit = ({
title={t('core.module.Variable Setting')} title={t('core.module.Variable Setting')}
isOpen={isOpenEdit} isOpen={isOpenEdit}
onClose={onCloseEdit} onClose={onCloseEdit}
maxW={['90vw', '500px']}
> >
<ModalBody> <ModalBody>
<Flex alignItems={'center'}> {variableType !== VariableInputEnum.external && (
<Box w={'70px'}>{t('common.Require Input')}</Box> <Flex alignItems={'center'}>
<Switch {...registerEdit('variable.required')} /> <Box w={'70px'}>{t('common.Require Input')}</Box>
</Flex> <Switch {...registerEdit('variable.required')} />
</Flex>
)}
<Flex mt={5} alignItems={'center'}> <Flex mt={5} alignItems={'center'}>
<Box w={'80px'}>{t('core.module.variable.variable name')}</Box> <Box w={'80px'}>{t('core.module.variable.variable name')}</Box>
<Input <Input
@ -216,8 +211,8 @@ const VariableEdit = ({
</Box> </Box>
<MyRadio <MyRadio
gridGap={4} gridGap={4}
gridTemplateColumns={'repeat(3,1fr)'} gridTemplateColumns={'repeat(2,1fr)'}
value={getValuesEdit('variable.type')} value={variableType}
list={VariableTypeList} list={VariableTypeList}
color={'myGray.600'} color={'myGray.600'}
hiddenCircle hiddenCircle
@ -227,7 +222,14 @@ const VariableEdit = ({
}} }}
/> />
{getValuesEdit('variable.type') === VariableInputEnum.input && ( {/* desc */}
{variableMap[variableType]?.desc && (
<Box mt={2} fontSize={'sm'} color={'myGray.500'} whiteSpace={'pre-wrap'}>
{t(variableMap[variableType].desc)}
</Box>
)}
{variableType === VariableInputEnum.input && (
<> <>
<Box mt={5} mb={2}> <Box mt={5} mb={2}>
{t('core.module.variable.text max length')} {t('core.module.variable.text max length')}
@ -251,7 +253,7 @@ const VariableEdit = ({
</> </>
)} )}
{getValuesEdit('variable.type') === VariableInputEnum.select && ( {variableType === VariableInputEnum.select && (
<> <>
<Box mt={5} mb={2}> <Box mt={5} mb={2}>
{t('core.module.variable.variable options')} {t('core.module.variable.variable options')}

View File

@ -16,10 +16,11 @@ import { useTranslation } from 'next-i18next';
import MyTooltip from '@/components/MyTooltip'; import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons'; import { QuestionOutlineIcon } from '@chakra-ui/icons';
export const defaultField = { export const defaultField: ContextExtractAgentItemType = {
required: false,
defaultValue: '',
desc: '', desc: '',
key: '', key: '',
required: true,
enum: '' enum: ''
}; };
@ -33,9 +34,10 @@ const ExtractFieldModal = ({
onSubmit: (data: ContextExtractAgentItemType) => void; onSubmit: (data: ContextExtractAgentItemType) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { register, handleSubmit } = useForm<ContextExtractAgentItemType>({ const { register, handleSubmit, watch } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField defaultValues: defaultField
}); });
const required = watch('required');
return ( return (
<MyModal <MyModal
@ -43,19 +45,41 @@ const ExtractFieldModal = ({
iconSrc="/imgs/module/extract.png" iconSrc="/imgs/module/extract.png"
title={t('core.module.extract.Field Setting Title')} title={t('core.module.extract.Field Setting Title')}
onClose={onClose} onClose={onClose}
w={['90vw', '500px']}
> >
<ModalBody> <ModalBody>
<Flex alignItems={'center'}> <Flex mt={2} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('common.Require Input')}</Box> <Flex alignItems={'center'} flex={['0 0 80px', '0 0 100px']}>
{t('core.module.extract.Required')}
<MyTooltip label={t('core.module.extract.Required Description')} forceShow>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Switch {...register('required')} /> <Switch {...register('required')} />
</Flex> </Flex>
{required && (
<Flex mt={5} alignItems={'center'}>
<Box flex={['0 0 80px', '0 0 100px']}>{t('core.module.Default value')}</Box>
<Input
bg={'myGray.50'}
placeholder={t('core.module.Default value placeholder')}
{...register('defaultValue')}
/>
</Flex>
)}
<Flex mt={5} alignItems={'center'}> <Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box> <Box flex={['0 0 80px', '0 0 100px']}>{t('core.module.Field key')}</Box>
<Input placeholder="name/age/sql" {...register('key', { required: true })} /> <Input
bg={'myGray.50'}
placeholder="name/age/sql"
{...register('key', { required: true })}
/>
</Flex> </Flex>
<Flex mt={5} alignItems={'center'}> <Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Description')}</Box> <Box flex={['0 0 80px', '0 0 100px']}>{t('core.module.Field Description')}</Box>
<Input <Input
bg={'myGray.50'}
placeholder={t('core.module.extract.Field Description Placeholder')} placeholder={t('core.module.extract.Field Description Placeholder')}
{...register('desc', { required: true })} {...register('desc', { required: true })}
/> />
@ -68,14 +92,16 @@ const ExtractFieldModal = ({
</MyTooltip> </MyTooltip>
</Flex> </Flex>
<Textarea rows={5} placeholder={'apple\npeach\nwatermelon'} {...register('enum')} /> <Textarea
rows={5}
bg={'myGray.50'}
placeholder={'apple\npeach\nwatermelon'}
{...register('enum')}
/>
</Box> </Box>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button> <Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter> </ModalFooter>
</MyModal> </MyModal>

View File

@ -1,5 +1,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react'; import {
Box,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex
} from '@chakra-ui/react';
import { NodeProps } from 'reactflow'; import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d'; import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@ -36,75 +47,87 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
}: { }: {
value?: ContextExtractAgentItemType[]; value?: ContextExtractAgentItemType[];
}) => ( }) => (
<Box pt={2}> <Box>
<Box position={'absolute'} top={0} right={0}> <Flex alignItems={'center'}>
<Box flex={'1 0 0'}>{t('core.module.extract.Target field')}</Box>
<Button <Button
size={'sm'}
variant={'whitePrimary'} variant={'whitePrimary'}
leftIcon={<AddIcon fontSize={'10px'} />} leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => setEditExtractField(defaultField)} onClick={() => setEditExtractField(defaultField)}
> >
{t('core.module.extract.Add field')}
</Button> </Button>
</Box> </Flex>
<TableContainer> <Box
<Table> mt={2}
<Thead> borderRadius={'md'}
<Tr> overflow={'hidden'}
<Th> key</Th> borderWidth={'1px'}
<Th></Th> borderBottom="none"
<Th></Th> >
<Th></Th> <TableContainer>
</Tr> <Table bg={'white'}>
</Thead> <Thead>
<Tbody> <Tr>
{extractKeys.map((item, index) => ( <Th bg={'myGray.50'}> key</Th>
<Tr <Th bg={'myGray.50'}></Th>
key={index} <Th bg={'myGray.50'}></Th>
position={'relative'} <Th bg={'myGray.50'}></Th>
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.extractKeys,
value: {
...props,
value: extractKeys.filter((extract) => item.key !== extract.key)
}
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</Td>
</Tr> </Tr>
))} </Thead>
</Tbody> <Tbody>
</Table> {extractKeys.map((item, index) => (
</TableContainer> <Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.extractKeys,
value: {
...props,
value: extractKeys.filter(
(extract) => item.key !== extract.key
)
}
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Box> </Box>
) )
}} }}
@ -142,7 +165,6 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const newOutput = { const newOutput = {
key: data.key, key: data.key,
label: `提取结果-${data.desc}`, label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: ModuleIOValueTypeEnum.string, valueType: ModuleIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.source, type: FlowNodeOutputTypeEnum.source,
targets: [] targets: []

View File

@ -31,10 +31,9 @@ export const Prompt_ExtractJson = `你可以从 <对话记录></对话记录>
<字段说明> <字段说明>
1. JSON JSON Schema 1. JSON JSON Schema
2. key description required enum value 2. key description enum value
3. 3.
4. JSON Schema{{json}}
{{json}}
</字段说明> </字段说明>
<对话记录> <对话记录>

View File

@ -4,7 +4,7 @@ import Script from 'next/script';
import Head from 'next/head'; import Head from 'next/head';
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'; import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
import Layout from '@/components/Layout'; import Layout from '@/components/Layout';
import { theme } from '@/web/styles/theme'; import { theme } from '@fastgpt/web/styles/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router'; import Router from 'next/router';

View File

@ -31,7 +31,6 @@ import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/module/Flow/components/modules/VariableEdit'; import VariableEdit from '@/components/core/module/Flow/components/modules/VariableEdit';
import MyTextarea from '@/components/common/Textarea/MyTextarea/index'; import MyTextarea from '@/components/common/Textarea/MyTextarea/index';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import SelectAiModel from '@/components/Select/SelectAiModel'; import SelectAiModel from '@/components/Select/SelectAiModel';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/module/utils'; import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/module/utils';

View File

@ -30,24 +30,29 @@ import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { OutLinkWithAppType } from '@fastgpt/global/support/outLink/type'; import { OutLinkWithAppType } from '@fastgpt/global/support/outLink/type';
const OutLink = ({ const OutLink = ({
shareId,
chatId,
showHistory,
authToken,
appName, appName,
appIntro, appIntro,
appAvatar appAvatar
}: { }: {
shareId: string;
chatId: string;
showHistory: '0' | '1';
authToken?: string;
appName?: string; appName?: string;
appIntro?: string; appIntro?: string;
appAvatar?: string; appAvatar?: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const {
shareId = '',
chatId = '',
showHistory = '1',
authToken,
...customVariables
} = router.query as {
shareId: string;
chatId: string;
showHistory: '0' | '1';
authToken: string;
[key: string]: string;
};
const { toast } = useToast(); const { toast } = useToast();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { isPc } = useSystemStore(); const { isPc } = useSystemStore();
@ -56,11 +61,7 @@ const OutLink = ({
const initSign = useRef(false); const initSign = useRef(false);
const [isEmbed, setIdEmbed] = useState(true); const [isEmbed, setIdEmbed] = useState(true);
const { const { localUId } = useShareChatStore();
localUId,
shareChatHistory, // abandon
clearLocalHistory // abandon
} = useShareChatStore();
const { const {
histories, histories,
loadHistories, loadHistories,
@ -83,7 +84,10 @@ const OutLink = ({
const { responseText, responseData } = await streamFetch({ const { responseText, responseData } = await streamFetch({
data: { data: {
messages: prompts, messages: prompts,
variables, variables: {
...customVariables,
...variables
},
shareId, shareId,
chatId: completionChatId, chatId: completionChatId,
outLinkUid outLinkUid
@ -159,6 +163,7 @@ const OutLink = ({
}, },
[ [
chatId, chatId,
customVariables,
shareId, shareId,
outLinkUid, outLinkUid,
t, t,
@ -391,9 +396,6 @@ const OutLink = ({
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || ''; const shareId = context?.query?.shareId || '';
const chatId = context?.query?.chatId || '';
const showHistory = context?.query?.showHistory || '1';
const authToken = context?.query?.authToken || '';
const app = await (async () => { const app = await (async () => {
try { try {
@ -413,13 +415,9 @@ export async function getServerSideProps(context: any) {
return { return {
props: { props: {
shareId, appName: app?.appId?.name || '',
chatId, appAvatar: app?.appId?.avatar || '',
showHistory, appIntro: app?.appId?.intro || '',
authToken,
appName: app?.appId?.name,
appAvatar: app?.appId?.avatar,
appIntro: app?.appId?.intro,
...(await serviceSideProps(context)) ...(await serviceSideProps(context))
} }
}; };

View File

@ -85,7 +85,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
} }
setRequesting(false); setRequesting(false);
}, },
[loginSuccess, toast] [loginSuccess, t, toast]
); );
return ( return (

View File

@ -57,11 +57,10 @@ const Login = () => {
/* default login type */ /* default login type */
useEffect(() => { useEffect(() => {
if (!feConfigs.oauth) return;
setPageType( setPageType(
feConfigs.oauth?.wechat ? LoginPageTypeEnum.wechat : LoginPageTypeEnum.passwordLogin feConfigs?.oauth?.wechat ? LoginPageTypeEnum.wechat : LoginPageTypeEnum.passwordLogin
); );
}, [feConfigs.oauth, feConfigs.oauth?.wechat]); }, [feConfigs.oauth]);
useEffect(() => { useEffect(() => {
clearToken(); clearToken();
router.prefetch('/app/list'); router.prefetch('/app/list');

View File

@ -16,6 +16,7 @@ import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { getHistories } from '../utils'; import { getHistories } from '../utils';
import { ModelTypeEnum, getLLMModel } from '@fastgpt/service/core/ai/model'; import { ModelTypeEnum, getLLMModel } from '@fastgpt/service/core/ai/model';
import { formatModelChars2Points } from '@fastgpt/service/support/wallet/usage/utils'; import { formatModelChars2Points } from '@fastgpt/service/support/wallet/usage/utils';
import json5 from 'json5';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[ModuleInputKeyEnum.history]?: ChatItemType[]; [ModuleInputKeyEnum.history]?: ChatItemType[];
@ -64,7 +65,8 @@ export async function dispatchContentExtract(props: Props): Promise<Response> {
// remove invalid key // remove invalid key
for (let key in arg) { for (let key in arg) {
if (!extractKeys.find((item) => item.key === key)) { const item = extractKeys.find((item) => item.key === key);
if (!item) {
delete arg[key]; delete arg[key];
} }
if (arg[key] === '') { if (arg[key] === '') {
@ -72,12 +74,20 @@ export async function dispatchContentExtract(props: Props): Promise<Response> {
} }
} }
// auto fill required fields
extractKeys.forEach((item) => {
if (item.required && !arg[item.key]) {
arg[item.key] = item.defaultValue || '';
}
});
// auth fields // auth fields
let success = !extractKeys.find((item) => !(item.key in arg)); let success = !extractKeys.find((item) => !(item.key in arg));
// auth empty value // auth empty value
if (success) { if (success) {
for (const key in arg) { for (const key in arg) {
if (arg[key] === '') { const item = extractKeys.find((item) => item.key === key);
if (!item) {
success = false; success = false;
break; break;
} }
@ -125,14 +135,8 @@ async function toolChoice({
...histories, ...histories,
{ {
obj: ChatRoleEnum.Human, obj: ChatRoleEnum.Human,
value: `你的任务 value: `你的任务是根据上下文获取适当的 JSON 字符串。要求
""" """
${description || '根据用户要求获取适当的 JSON 字符串。'}
"""
"""
-
- -
- -
""" """
@ -167,8 +171,7 @@ ${description || '根据用户要求获取适当的 JSON 字符串。'}
description, description,
parameters: { parameters: {
type: 'object', type: 'object',
properties, properties
required: extractKeys.filter((item) => item.required).map((item) => item.key)
} }
}; };
const tools: any = [ const tools: any = [
@ -193,7 +196,7 @@ ${description || '根据用户要求获取适当的 JSON 字符串。'}
const arg: Record<string, any> = (() => { const arg: Record<string, any> = (() => {
try { try {
return JSON.parse( return json5.parse(
response?.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments || '{}' response?.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments || '{}'
); );
} catch (error) { } catch (error) {
@ -225,7 +228,7 @@ async function completions({
json: extractKeys json: extractKeys
.map( .map(
(item) => (item) =>
`{"key":"${item.key}", "description":"${item.desc}", "required":${item.required}${ `{"key":"${item.key}", "description":"${item.desc}"${
item.enum ? `, "enum":"[${item.enum.split('\n')}]"` : '' item.enum ? `, "enum":"[${item.enum.split('\n')}]"` : ''
}}` }}`
) )
@ -240,7 +243,6 @@ Human: ${content}`
userKey: user.openaiAccount, userKey: user.openaiAccount,
timeout: 480000 timeout: 480000
}); });
const data = await ai.chat.completions.create({ const data = await ai.chat.completions.create({
model: extractModel.model, model: extractModel.model,
temperature: 0.01, temperature: 0.01,
@ -260,19 +262,14 @@ Human: ${content}`
arg: {} arg: {}
}; };
const jsonStr = answer
.substring(start, end + 1)
.replace(/(\\n|\\)/g, '')
.replace(/ /g, '');
try { try {
return { return {
rawResponse: answer, rawResponse: answer,
tokens: countMessagesTokens(messages), tokens: countMessagesTokens(messages),
arg: json5.parse(answer) as Record<string, any>
arg: JSON.parse(jsonStr) as Record<string, any>
}; };
} catch (error) { } catch (error) {
console.log(error);
return { return {
rawResponse: answer, rawResponse: answer,
tokens: countMessagesTokens(messages), tokens: countMessagesTokens(messages),