mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-07 01:02:55 +08:00
76d6234de6
* Agent features (#6345) * Test agent (#6220) * squash: compress all commits into one * feat: plan response in ui * response ui * perf: agent config * merge * tool select ux * perf: chat ui * perf: agent editform * tmp code * feat: save chat * Complete agent parent (#6049) * add role and tools filling * add: file-upload --------- Co-authored-by: xxyyh <2289112474@qq> * perf: top agent code * top agent (#6062) Co-authored-by: xxyyh <2289112474@qq> * fix: ts * skill editor ui * ui * perf: rewrite type with zod * skill edit ui * skill agent (#6089) * cp skill chat * rebasefdf933dand add skill chat * 1. skill 的 CRUD 2. skill 的信息渲染到前端界面 * solve comment * remove chatid and chatItemId * skill match * perf: skill manage * fix: ts --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * fix: ts * fix: loop import * skill tool config (#6114) Co-authored-by: xxyyh <2289112474@qq> * feat: load tool in agent * skill memory (#6126) Co-authored-by: xxyyh <2289112474@qq> * perf: agent skill editor * perf: helperbot ui * agent code * perf: context * fix: request context * agent usage * perf: agent context and pause * perf: plan response * Test agent sigle skill (#6184) * feat:top box fill * prompt fix --------- Co-authored-by: xxyyh <2289112474@qq> * perf: agent chat ui * Test agent new (#6219) * have-replan * agent --------- Co-authored-by: xxyyh <2289112474@qq> * fix: ts --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * feat: consolidate agent and MCP improvements This commit consolidates 17 commits including: - MCP tools enhancements and fixes - Agent system improvements and optimizations - Auth limit and prompt updates - Tool response compression and error tracking - Simple app adaptation - Code quality improvements (TypeScript, ESLint, Zod) - Version type migration to schema - Remove deprecated useRequest2 - Add LLM error tracking - Toolset ID validation fixes --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * fix: transform avatar copy;perf: filter invalid tool * update llm response storage time * fix: openapi schema * update skill desc * feat: cache hit data * i18n * lock * chat logs support error filter & user search (#6373) * chat log support searching by user name * support error filter * fix * fix overflow * optimize * fix init script * fix * perf: get log users * updat ecomment * fix: ts * fix: test --------- Co-authored-by: archer <545436317@qq.com> * Fix: agent (#6376) * Agent features (#6345) * Test agent (#6220) * squash: compress all commits into one * feat: plan response in ui * response ui * perf: agent config * merge * tool select ux * perf: chat ui * perf: agent editform * tmp code * feat: save chat * Complete agent parent (#6049) * add role and tools filling * add: file-upload --------- Co-authored-by: xxyyh <2289112474@qq> * perf: top agent code * top agent (#6062) Co-authored-by: xxyyh <2289112474@qq> * fix: ts * skill editor ui * ui * perf: rewrite type with zod * skill edit ui * skill agent (#6089) * cp skill chat * rebasefdf933dand add skill chat * 1. skill 的 CRUD 2. skill 的信息渲染到前端界面 * solve comment * remove chatid and chatItemId * skill match * perf: skill manage * fix: ts --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * fix: ts * fix: loop import * skill tool config (#6114) Co-authored-by: xxyyh <2289112474@qq> * feat: load tool in agent * skill memory (#6126) Co-authored-by: xxyyh <2289112474@qq> * perf: agent skill editor * perf: helperbot ui * agent code * perf: context * fix: request context * agent usage * perf: agent context and pause * perf: plan response * Test agent sigle skill (#6184) * feat:top box fill * prompt fix --------- Co-authored-by: xxyyh <2289112474@qq> * perf: agent chat ui * Test agent new (#6219) * have-replan * agent --------- Co-authored-by: xxyyh <2289112474@qq> * fix: ts --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * feat: consolidate agent and MCP improvements This commit consolidates 17 commits including: - MCP tools enhancements and fixes - Agent system improvements and optimizations - Auth limit and prompt updates - Tool response compression and error tracking - Simple app adaptation - Code quality improvements (TypeScript, ESLint, Zod) - Version type migration to schema - Remove deprecated useRequest2 - Add LLM error tracking - Toolset ID validation fixes --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * 1. 把辅助生成前端上的 system prompt 加入到上下文中 2. mcp工具的前端渲染(图标) 3. 文件读取工具和文件上传进行关联 4. 添加了辅助生成返回格式出错的重试方案 5. ask 不出现在 plan 步骤中 6. 添加了辅助生成的头像和交互 UI * fix:read_file * helperbot ui * ts error * helper ui * delete Unused import * perf: helper bot * lock --------- Co-authored-by: Archer <545436317@qq.com> Co-authored-by: xxyyh <2289112474@qq> * fix date variable required & model auth (#6386) * fix date variable required & model auth * doc * feat: add chat id to finish callback * fix: iphone safari shareId (#6387) * fix: iphone safari shareId * fix: mcp file list can't setting * fix: reason output field * fix: skip JSON validation for HTTP tool body with variable (#6392) * fix: skip JSON validation for HTTP tool body with variable * doc * workflow fitview * perf: selecting memory * perf: cp api * ui * perf: toolcall auto adapt * fix: catch workflow error * fix: ts * perf: pagination type * remove * ignore * update doc * fix: simple app tool select * add default avatar to logs user * perf: loading user * select dataset ui * rename version * feat: add global/common test * perf: packages/global/common test * feat: package/global/ai,app test * add global/chat test * global/core test * global/core test * feat: packages/global all test * perf: test * add server api test * perf: init shell * perf: init4150 shell * remove invalid code * update doc * remove log * fix: chat effect * fix: plan fake tool (#6398) * 1. 提示词防注入功能 2. 无工具不进入 plan,防止虚拟工具生成 * Agent-dataset * dataset * dataset presetInfo * prefix * perf: prompt --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * fix: review * adapt kimi2.5 think toolcall * feat: invoke fastgpt user info (#6403) feat: invoke fastgpt user info * fix: invoke fastgpt user info return orgs (#6404) * skill and version * retry helperbot (#6405) Co-authored-by: xxyyh <2289112474@qq> * update template * remove log * doc * update doc * doc * perf: internal ip check * adapt get paginationRecords * tool call adapt * fix: test * doc * fix: agent initial version * adapt completions v1 * feat: instrumentation check * rename skill * add workflow demo mode tracks (#6407) * chore: 统一 skills 目录命名为小写 将 .claude/Skills/ 重命名为 .claude/skills/ 以保持命名一致性。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * add workflow demo mode tracks * code * optimize * fix: improve workflowDemoTrack based on PR review - Add comment to empty catch block for maintainability - Add @param docs to onDemoChange clarifying nodeCount usage - Replace silent .catch with console.debug for dev debugging - Handle appId changes by reporting old data before re-init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: archer <545436317@qq.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * remove repeat skill * fix(workflow): filter out orphan edges to prevent runtime errors (#6399) * fix(workflow): filter out orphan edges to prevent runtime errors Runtime edges that reference non-existent nodes (orphan edges) can cause unexpected behavior or crashes during workflow dispatch. This change adds a pre-check to filter out such edges before execution begins, ensuring system stability even with inconsistent graph data. * fix(workflow): enhance orphan edge filtering with logging and tests - Refactor: Extract logic to 'filterOrphanEdges' in utils.ts for better reusability - Feat: Add performance monitoring (warn if >100ms) and comprehensive logging - Feat: Support detailed edge inspection in debug mode - Docs: Add JSDoc explaining causes of orphan edges (migration, manual edits) - Test: Add unit tests covering edge cases and performance (1000 edges) Addresses PR review feedback regarding logging, variable naming, and testing." * move code * move code * add more unit test --------- Co-authored-by: archer <545436317@qq.com> * test * perf: test * add server/common/string test * fix: resolve $ref references in MCP tool input schemas (#6395) (#6409) * fix: resolve $ref references in MCP tool input schemas (#6395) * add test code --------- Co-authored-by: archer <545436317@qq.com> * chore(docs): add fastgpt, fastgpt-plugin version choice guide (#6411) * chore(doc): add fastgpt version description * doc * doc --------- Co-authored-by: archer <545436317@qq.com> * fix:dataset cite and description info (#6410) * 1. 添加知识库引用(plan 步骤和直接知识库调用) 2. 提示词框中的@知识库工具 3. plan 中 step 的 description dataset_search 改为中文 * fix: i18n * prompt * prompt --------- Co-authored-by: xxyyh <2289112474@qq> * fix: tool call * perf: workflow props * fix: merge ECharts toolbox options instead of overwriting (#6269) (#6412) * feat: integrate logtape and otel (#6400) * fix: deps * feat(logger): integrate logtape and otel * wip(log): add basic infras logs * wip(log): add request id and inject it into context * wip(log): add basic tx logs * wip(log): migrate * wip(log): category * wip(log): more sub category * fix: type * fix: sessionRun * fix: export getLogger from client.ts * chore: improve logs * docs: update signoz and changelog * change type * fix: ts * remove skill.md * fix: lockfile specifier * fix: test --------- Co-authored-by: archer <545436317@qq.com> * init log * doc * remove invalid log * fix: review * template * replace new log * fix: ts * remove log * chore: migrate all addLog to logtape * move skill * chore: migrate all addLog to logtape (#6417) * update skill * remove log * fix: tool check --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: xuyafei1996 <54217479+xuyafei1996@users.noreply.github.com> Co-authored-by: ToukoYui <2331631097@qq.com> Co-authored-by: roy <whoeverimf5@gmail.com>
1021 lines
29 KiB
TypeScript
1021 lines
29 KiB
TypeScript
import { describe, expect, it, beforeEach } from 'vitest';
|
|
import {
|
|
type Props,
|
|
pushChatRecords,
|
|
updateInteractiveChat
|
|
} from '@fastgpt/service/core/chat/saveChat';
|
|
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
|
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
|
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
|
import { MongoAppChatLog } from '@fastgpt/service/core/app/logs/chatLogsSchema';
|
|
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
|
|
import { ChatFileTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
|
import {
|
|
FlowNodeTypeEnum,
|
|
FlowNodeInputTypeEnum
|
|
} from '@fastgpt/global/core/workflow/node/constant';
|
|
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
|
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
|
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
|
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
|
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
|
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
|
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
|
|
|
const createMockProps = (
|
|
overrides?: Partial<Props>,
|
|
ids?: { appId?: string; teamId?: string; tmbId?: string }
|
|
): Props => ({
|
|
chatId: 'test-chat-id',
|
|
appId: ids?.appId || '67e0d5535c02d1d5cdede71f',
|
|
teamId: ids?.teamId || '654a4107c32f3bf5f998452f',
|
|
tmbId: ids?.tmbId || '65ab7007462ada7dbb899948',
|
|
nodes: [
|
|
{
|
|
nodeId: 'node-1',
|
|
name: 'test-node',
|
|
flowNodeType: FlowNodeTypeEnum.systemConfig,
|
|
inputs: [],
|
|
outputs: []
|
|
}
|
|
],
|
|
newTitle: 'Test Chat',
|
|
source: 'online' as any,
|
|
userContent: {
|
|
obj: ChatRoleEnum.Human,
|
|
value: [
|
|
{
|
|
text: {
|
|
content: 'Hello, how are you?'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [
|
|
{
|
|
text: {
|
|
content: 'I am doing well, thank you!'
|
|
}
|
|
}
|
|
],
|
|
responseData: []
|
|
},
|
|
durationSeconds: 2.5,
|
|
...overrides
|
|
});
|
|
|
|
describe('pushChatRecords', () => {
|
|
let testAppId: string;
|
|
let testTeamId: string;
|
|
let testTmbId: string;
|
|
let testUserId: string;
|
|
|
|
beforeEach(async () => {
|
|
// Create test user
|
|
const user = await MongoUser.create({
|
|
username: 'test-user',
|
|
password: 'test-password'
|
|
});
|
|
testUserId = String(user._id);
|
|
|
|
// Create test team
|
|
const team = await MongoTeam.create({
|
|
name: 'Test Team',
|
|
ownerId: user._id,
|
|
avatar: 'test-avatar',
|
|
createTime: new Date(),
|
|
balance: 0,
|
|
teamDomain: 'test-domain'
|
|
});
|
|
testTeamId = String(team._id);
|
|
|
|
// Create team member
|
|
const teamMember = await MongoTeamMember.create({
|
|
teamId: team._id,
|
|
userId: user._id,
|
|
name: 'Test Member',
|
|
role: TeamMemberRoleEnum.owner,
|
|
status: 'active',
|
|
createTime: new Date(),
|
|
defaultTeam: true
|
|
});
|
|
testTmbId = String(teamMember._id);
|
|
|
|
// Create a test app for use in tests
|
|
const app = await MongoApp.create({
|
|
name: 'Test App',
|
|
type: AppTypeEnum.simple,
|
|
teamId: team._id,
|
|
tmbId: teamMember._id,
|
|
avatar: 'test-avatar',
|
|
intro: 'Test intro'
|
|
});
|
|
testAppId = String(app._id);
|
|
});
|
|
|
|
describe('pushChatRecords function', () => {
|
|
it('should skip saving if chatId is empty', async () => {
|
|
const props = createMockProps(
|
|
{ chatId: '' },
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
await pushChatRecords(props);
|
|
|
|
const chatItems = await MongoChatItem.find({ appId: testAppId });
|
|
expect(chatItems).toHaveLength(0);
|
|
});
|
|
|
|
it('should skip saving if chatId is NO_RECORD_HISTORIES', async () => {
|
|
const props = createMockProps(
|
|
{ chatId: 'NO_RECORD_HISTORIES' },
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
await pushChatRecords(props);
|
|
|
|
const chatItems = await MongoChatItem.find({ appId: testAppId });
|
|
expect(chatItems).toHaveLength(0);
|
|
});
|
|
|
|
it('should remove file URLs from user content before processing', async () => {
|
|
const props = createMockProps({
|
|
userContent: {
|
|
obj: ChatRoleEnum.Human,
|
|
value: [
|
|
{
|
|
file: {
|
|
type: ChatFileTypeEnum.image,
|
|
name: 'test.jpg',
|
|
url: 'https://example.com/test.jpg',
|
|
key: 'file-key-123'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
await pushChatRecords(props);
|
|
|
|
// Verify that the URL was removed
|
|
expect(props.userContent.value[0].file?.url).toBe('');
|
|
});
|
|
|
|
it('should create chat items and update chat record', async () => {
|
|
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
|
|
|
|
await pushChatRecords(props);
|
|
|
|
// Check chat items were created
|
|
const chatItems = await MongoChatItem.find({ appId: testAppId, chatId: props.chatId });
|
|
expect(chatItems).toHaveLength(2);
|
|
|
|
// Check human message
|
|
const humanItem = chatItems.find((item) => item.obj === ChatRoleEnum.Human);
|
|
expect(humanItem).toBeDefined();
|
|
expect(humanItem?.value[0].text?.content).toBe('Hello, how are you?');
|
|
|
|
// Check AI message
|
|
const aiItem = chatItems.find((item) => item.obj === ChatRoleEnum.AI);
|
|
expect(aiItem).toBeDefined();
|
|
expect(aiItem?.value[0].text?.content).toBe('I am doing well, thank you!');
|
|
|
|
// Check chat record
|
|
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
|
|
expect(chat).toBeDefined();
|
|
expect(chat?.title).toBe('Test Chat');
|
|
expect(String(chat?.teamId)).toBe(props.teamId);
|
|
});
|
|
|
|
it('should create chat item responses when responseData is provided', async () => {
|
|
const props = createMockProps({
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [
|
|
{
|
|
text: { content: 'Response' }
|
|
}
|
|
],
|
|
responseData: [
|
|
{
|
|
nodeId: 'xx',
|
|
id: 'xx',
|
|
moduleType: FlowNodeTypeEnum.chatNode,
|
|
moduleName: 'Chat',
|
|
runningTime: 1.5,
|
|
totalPoints: 10
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
await pushChatRecords(props);
|
|
|
|
const responses = await MongoChatItemResponse.find({
|
|
appId: testAppId,
|
|
chatId: props.chatId
|
|
});
|
|
// ResponseData is only created when dataId exists on the AI chat item
|
|
// Since we're using real database, check if responses were created
|
|
if (responses.length > 0) {
|
|
expect(responses[0].data.moduleType).toBe(FlowNodeTypeEnum.chatNode);
|
|
expect(responses[0].data.totalPoints).toBe(10);
|
|
}
|
|
});
|
|
|
|
it('should handle dataset search node with quoteList', async () => {
|
|
const quote: SearchDataResponseItemType = {
|
|
id: 'quote-1',
|
|
chunkIndex: 0,
|
|
datasetId: 'dataset-1',
|
|
collectionId: 'collection-1',
|
|
sourceId: 'source-1',
|
|
sourceName: 'doc.pdf',
|
|
score: [{ type: 'embedding' as const, value: 0.95, index: 0 }],
|
|
q: 'What is AI?',
|
|
a: 'AI stands for Artificial Intelligence...',
|
|
updateTime: new Date()
|
|
};
|
|
const props = createMockProps(
|
|
{
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [
|
|
{
|
|
text: { content: 'Based on the search results...' }
|
|
}
|
|
],
|
|
responseData: [
|
|
{
|
|
nodeId: 'xx',
|
|
id: 'xx',
|
|
moduleType: FlowNodeTypeEnum.datasetSearchNode,
|
|
moduleName: 'Dataset Search',
|
|
runningTime: 0.5,
|
|
totalPoints: 5,
|
|
quoteList: [quote]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props);
|
|
|
|
const responses = await MongoChatItemResponse.find({
|
|
appId: testAppId,
|
|
chatId: props.chatId
|
|
});
|
|
// ResponseData is only created when dataId exists on the AI chat item
|
|
if (responses.length > 0) {
|
|
expect(responses[0].data.quoteList).toBeDefined();
|
|
expect(responses[0].data.quoteList?.[0]).toMatchObject({
|
|
id: quote.id,
|
|
chunkIndex: quote.chunkIndex,
|
|
datasetId: quote.datasetId,
|
|
collectionId: quote.collectionId,
|
|
sourceId: quote.sourceId,
|
|
sourceName: quote.sourceName,
|
|
score: quote.score
|
|
});
|
|
// q and a should be removed
|
|
expect(responses[0].data.quoteList?.[0]?.q).toBeUndefined();
|
|
expect(responses[0].data.quoteList?.[0]?.a).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should create chat data log with error count when response has error', async () => {
|
|
const props = createMockProps(
|
|
{
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [],
|
|
responseData: [
|
|
{
|
|
nodeId: 'xx',
|
|
id: 'xx',
|
|
moduleType: FlowNodeTypeEnum.chatNode,
|
|
moduleName: 'Chat',
|
|
runningTime: 1.0,
|
|
totalPoints: 5,
|
|
errorText: 'API rate limit exceeded'
|
|
}
|
|
]
|
|
},
|
|
errorMsg: 'API rate limit exceeded'
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props);
|
|
|
|
const logs = await MongoAppChatLog.find({ appId: testAppId, chatId: props.chatId });
|
|
expect(logs).toHaveLength(1);
|
|
expect(logs[0].errorCount).toBe(1);
|
|
});
|
|
|
|
it('should calculate total points from response data', async () => {
|
|
const props = createMockProps(
|
|
{
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [],
|
|
responseData: [
|
|
{
|
|
nodeId: 'xx',
|
|
id: 'xx',
|
|
moduleType: FlowNodeTypeEnum.chatNode,
|
|
moduleName: 'Chat',
|
|
runningTime: 1.0,
|
|
totalPoints: 10
|
|
},
|
|
{
|
|
nodeId: '22',
|
|
id: '33',
|
|
moduleType: FlowNodeTypeEnum.datasetSearchNode,
|
|
moduleName: 'Dataset Search',
|
|
runningTime: 0.5,
|
|
totalPoints: 5
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props);
|
|
|
|
const logs = await MongoAppChatLog.find({ appId: testAppId, chatId: props.chatId });
|
|
expect(logs).toHaveLength(1);
|
|
expect(logs[0].totalPoints).toBe(15);
|
|
});
|
|
|
|
it('should merge metadata from existing chat', async () => {
|
|
const props1 = createMockProps(
|
|
{
|
|
metadata: { source: 'web', version: '1.0' }
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props1);
|
|
|
|
const props2 = createMockProps(
|
|
{
|
|
chatId: props1.chatId,
|
|
metadata: { user: 'test-user' }
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props2);
|
|
|
|
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props1.chatId });
|
|
expect(chat?.metadata).toMatchObject({
|
|
source: 'web',
|
|
version: '1.0',
|
|
user: 'test-user'
|
|
});
|
|
});
|
|
|
|
it('should track duration seconds', async () => {
|
|
const props = createMockProps(
|
|
{
|
|
durationSeconds: 3.75
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props);
|
|
|
|
const aiItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
if (aiItem) {
|
|
if ('durationSeconds' in aiItem) {
|
|
expect(aiItem?.durationSeconds).toBe(3.75);
|
|
} else {
|
|
throw new Error('aiItem does not have durationSeconds');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should store citeCollectionIds from dataset search', async () => {
|
|
const props = createMockProps(
|
|
{
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [],
|
|
responseData: [
|
|
{
|
|
nodeId: 'xx',
|
|
id: 'xx',
|
|
moduleType: FlowNodeTypeEnum.datasetSearchNode,
|
|
moduleName: 'Dataset Search',
|
|
runningTime: 0.5,
|
|
totalPoints: 5,
|
|
quoteList: [
|
|
{
|
|
id: 'quote-1',
|
|
chunkIndex: 0,
|
|
datasetId: 'dataset-1',
|
|
collectionId: 'collection-1',
|
|
sourceId: 'source-1',
|
|
sourceName: 'doc1.pdf',
|
|
score: [{ type: 'embedding', value: 0.95, index: 0 }],
|
|
q: 'What is AI?',
|
|
a: 'AI stands for Artificial Intelligence...',
|
|
updateTime: new Date()
|
|
},
|
|
{
|
|
id: 'quote-2',
|
|
chunkIndex: 1,
|
|
datasetId: 'dataset-1',
|
|
collectionId: 'collection-2',
|
|
sourceId: 'source-2',
|
|
sourceName: 'doc2.pdf',
|
|
score: [{ type: 'embedding', value: 0.85, index: 0 }],
|
|
q: 'What is AI?',
|
|
a: 'AI stands for Artificial Intelligence...',
|
|
updateTime: new Date()
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
await pushChatRecords(props);
|
|
|
|
const aiItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
|
|
if (aiItem) {
|
|
if ('citeCollectionIds' in aiItem) {
|
|
expect(aiItem?.citeCollectionIds).toHaveLength(2);
|
|
expect(aiItem?.citeCollectionIds).toContain('collection-1');
|
|
expect(aiItem?.citeCollectionIds).toContain('collection-2');
|
|
} else {
|
|
throw new Error('aiItem does not have citeCollectionIds');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('updateInteractiveChat function', () => {
|
|
it('should skip update if chatId is empty', async () => {
|
|
const props = createMockProps(
|
|
{ chatId: '' },
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'opt1', value: 'Option 1' },
|
|
{ key: 'opt2', value: 'Option 2' }
|
|
],
|
|
userSelectedVal: undefined
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chatItems = await MongoChatItem.find({ appId: testAppId });
|
|
expect(chatItems).toHaveLength(0);
|
|
});
|
|
|
|
it('should skip update if no AI chat item found', async () => {
|
|
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'opt1', value: 'Option 1' },
|
|
{ key: 'opt2', value: 'Option 2' }
|
|
],
|
|
userSelectedVal: undefined
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
|
|
expect(chat).toBeNull();
|
|
});
|
|
|
|
it('should skip update if chat item is not from AI', async () => {
|
|
// Create a human chat item
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.Human,
|
|
value: [
|
|
{
|
|
text: { content: 'Hello' }
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'opt1', value: 'Option 1' },
|
|
{ key: 'opt2', value: 'Option 2' }
|
|
],
|
|
userSelectedVal: undefined
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
// Should not create a chat record
|
|
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
|
|
expect(chat).toBeNull();
|
|
});
|
|
|
|
it('should skip update if no interactive value found', async () => {
|
|
// Create an AI chat item without interactive
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
value: [
|
|
{
|
|
text: { content: 'Hello' }
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'opt1', value: 'Option 1' },
|
|
{ key: 'opt2', value: 'Option 2' }
|
|
],
|
|
userSelectedVal: undefined
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
// Should not create a chat record
|
|
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
|
|
expect(chat).toBeNull();
|
|
});
|
|
|
|
it('should update userSelect interactive value', async () => {
|
|
// Create an AI chat item with userSelect interactive
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
dataId: 'data-id-1',
|
|
value: [
|
|
{
|
|
interactive: {
|
|
type: 'userSelect',
|
|
params: {
|
|
userSelectedVal: undefined,
|
|
options: ['Option 1', 'Option 2']
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps(
|
|
{
|
|
appId: testAppId,
|
|
userContent: {
|
|
obj: ChatRoleEnum.Human,
|
|
value: [
|
|
{
|
|
text: { content: 'Option 1' }
|
|
}
|
|
]
|
|
},
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [],
|
|
responseData: []
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'opt1', value: 'Option 1' },
|
|
{ key: 'opt2', value: 'Option 2' }
|
|
],
|
|
userSelectedVal: undefined
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chatItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
console.log(chatItem?.value, 1212);
|
|
if (chatItem) {
|
|
if (chatItem.obj === ChatRoleEnum.AI) {
|
|
const lastValue = chatItem.value[chatItem.value.length - 1];
|
|
if (lastValue.interactive?.type === 'userSelect') {
|
|
expect(lastValue.interactive?.params.userSelectedVal).toBe('Option 1');
|
|
} else {
|
|
throw new Error('chatItem does not have userSelect interactive');
|
|
}
|
|
} else {
|
|
throw new Error('chatItem does not have value');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should update userInput interactive value', async () => {
|
|
// Create an AI chat item with userInput interactive
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
dataId: 'data-id-1',
|
|
value: [
|
|
{
|
|
interactive: {
|
|
type: 'userInput',
|
|
params: {
|
|
submitted: false,
|
|
inputForm: [
|
|
{
|
|
key: 'username',
|
|
type: 'input',
|
|
label: 'Username',
|
|
value: undefined
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps(
|
|
{
|
|
userContent: {
|
|
obj: ChatRoleEnum.Human,
|
|
value: [
|
|
{
|
|
text: { content: JSON.stringify({ username: 'john_doe' }) }
|
|
}
|
|
]
|
|
},
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [],
|
|
responseData: []
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
const interactive = {
|
|
type: 'userInput' as const,
|
|
params: {
|
|
description: '',
|
|
submitted: false,
|
|
inputForm: [
|
|
{
|
|
key: 'username',
|
|
type: FlowNodeInputTypeEnum.input,
|
|
label: 'Username',
|
|
value: undefined,
|
|
valueType: WorkflowIOValueTypeEnum.string,
|
|
required: false
|
|
}
|
|
]
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chatItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
if (chatItem) {
|
|
if (chatItem.obj === ChatRoleEnum.AI) {
|
|
const lastValue = chatItem.value[chatItem.value.length - 1];
|
|
if (lastValue.interactive?.type === 'userInput') {
|
|
expect(lastValue.interactive?.params.submitted).toBe(true);
|
|
expect(lastValue.interactive?.params.inputForm[0].value).toBe('john_doe');
|
|
} else {
|
|
throw new Error('chatItem does not have userInput interactive');
|
|
}
|
|
} else {
|
|
throw new Error('chatItem does not have value');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should remove paymentPause interactive value', async () => {
|
|
// Create an AI chat item with paymentPause interactive
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
dataId: 'data-id-1',
|
|
value: [
|
|
{
|
|
text: { content: 'Payment required' }
|
|
},
|
|
{
|
|
interactive: {
|
|
type: 'paymentPause',
|
|
params: {}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
|
|
|
|
const interactive = {
|
|
type: 'paymentPause' as const,
|
|
params: {
|
|
description: 'Payment required',
|
|
continue: false
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chatItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
// PaymentPause is removed, and AI response is appended
|
|
expect(chatItem?.value.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should merge AI response values', async () => {
|
|
// Create an AI chat item with interactive
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
dataId: 'data-id-1',
|
|
value: [
|
|
{
|
|
text: { content: 'First response' }
|
|
},
|
|
{
|
|
interactive: {
|
|
type: 'userSelect',
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'a', value: 'A' },
|
|
{ key: 'b', value: 'B' }
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps(
|
|
{
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [
|
|
{
|
|
text: { content: 'Second response' }
|
|
}
|
|
],
|
|
responseData: []
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'a', value: 'A' },
|
|
{ key: 'b', value: 'B' }
|
|
]
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chatItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
// Original has 2 values (text + interactive), AI adds 1 more (text)
|
|
expect(chatItem?.value.length).toBeGreaterThanOrEqual(3);
|
|
expect(chatItem?.value[chatItem.value.length - 1].text?.content).toBe('Second response');
|
|
});
|
|
|
|
it('should accumulate duration seconds', async () => {
|
|
// Create an AI chat item
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
dataId: 'data-id-1',
|
|
durationSeconds: 1.5,
|
|
value: [
|
|
{
|
|
interactive: {
|
|
type: 'userSelect',
|
|
params: { options: ['A', 'B'] }
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
const props = createMockProps(
|
|
{
|
|
durationSeconds: 2.3
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'a', value: 'A' },
|
|
{ key: 'b', value: 'B' }
|
|
]
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const chatItem = await MongoChatItem.findOne({
|
|
appId: testAppId,
|
|
chatId: props.chatId,
|
|
obj: ChatRoleEnum.AI
|
|
});
|
|
if (chatItem) {
|
|
if (chatItem.obj === ChatRoleEnum.AI) {
|
|
expect(chatItem?.durationSeconds).toBe(3.8);
|
|
} else {
|
|
throw new Error('chatItem does not have durationSeconds');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should merge chat item responses', async () => {
|
|
// Create an AI chat item
|
|
await MongoChatItem.create({
|
|
chatId: 'test-chat-id',
|
|
teamId: testTeamId,
|
|
tmbId: testTmbId,
|
|
appId: testAppId,
|
|
obj: ChatRoleEnum.AI,
|
|
dataId: 'data-id-1',
|
|
value: [
|
|
{
|
|
interactive: {
|
|
type: 'userSelect',
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'a', value: 'A' },
|
|
{ key: 'b', value: 'B' }
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
// Create an existing response
|
|
await MongoChatItemResponse.create({
|
|
teamId: testTeamId,
|
|
appId: testAppId,
|
|
chatId: 'test-chat-id',
|
|
chatItemDataId: 'data-id-1',
|
|
data: {
|
|
moduleType: FlowNodeTypeEnum.chatNode,
|
|
moduleName: 'Chat',
|
|
runningTime: 1.0,
|
|
totalPoints: 10
|
|
}
|
|
});
|
|
|
|
const props = createMockProps(
|
|
{
|
|
aiContent: {
|
|
obj: ChatRoleEnum.AI,
|
|
value: [],
|
|
responseData: [
|
|
{
|
|
nodeId: 'xx',
|
|
id: 'xx',
|
|
moduleType: FlowNodeTypeEnum.datasetSearchNode,
|
|
moduleName: 'Dataset Search',
|
|
runningTime: 0.5,
|
|
totalPoints: 5
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
|
|
);
|
|
|
|
const interactive = {
|
|
type: 'userSelect' as const,
|
|
params: {
|
|
description: '',
|
|
userSelectOptions: [
|
|
{ key: 'a', value: 'A' },
|
|
{ key: 'b', value: 'B' }
|
|
]
|
|
},
|
|
entryNodeIds: [],
|
|
memoryEdges: [],
|
|
nodeOutputs: []
|
|
};
|
|
|
|
await updateInteractiveChat({ interactive, ...props });
|
|
|
|
const responses = await MongoChatItemResponse.find({
|
|
appId: testAppId,
|
|
chatId: props.chatId
|
|
});
|
|
// Should have merged responses
|
|
expect(responses.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|