@lobehub/lobehub 2.0.0-next.263 → 2.0.0-next.265
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/manual-build-desktop.yml +16 -37
- package/CHANGELOG.md +52 -0
- package/apps/desktop/native-deps.config.mjs +19 -3
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +13 -0
- package/apps/desktop/src/main/core/browser/Browser.ts +14 -0
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +32 -0
- package/apps/desktop/src/main/utils/permissions.ts +86 -22
- package/changelog/v1.json +18 -0
- package/package.json +2 -2
- package/packages/database/src/models/__tests__/agent.test.ts +165 -4
- package/packages/database/src/models/agent.ts +46 -0
- package/packages/database/src/repositories/agentGroup/index.test.ts +498 -0
- package/packages/database/src/repositories/agentGroup/index.ts +150 -0
- package/packages/database/src/repositories/home/__tests__/index.test.ts +113 -1
- package/packages/database/src/repositories/home/index.ts +48 -67
- package/pnpm-workspace.yaml +1 -0
- package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/index.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +2 -6
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +100 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -4
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +149 -0
- package/src/app/[variants]/(main)/home/_layout/hooks/index.ts +0 -1
- package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +1 -1
- package/src/features/ChatInput/InputEditor/index.tsx +1 -0
- package/src/features/EditorCanvas/DiffAllToolbar.tsx +1 -1
- package/src/server/routers/lambda/agent.ts +15 -0
- package/src/server/routers/lambda/agentGroup.ts +16 -0
- package/src/services/agent.ts +11 -0
- package/src/services/chatGroup/index.ts +11 -0
- package/src/store/home/slices/sidebarUI/action.test.ts +23 -22
- package/src/store/home/slices/sidebarUI/action.ts +37 -9
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +0 -62
- package/src/app/[variants]/(main)/home/_layout/hooks/useSessionItemMenuItems.tsx +0 -238
|
@@ -766,4 +766,502 @@ describe('AgentGroupRepository', () => {
|
|
|
766
766
|
expect(virtualAgents).toHaveLength(0);
|
|
767
767
|
});
|
|
768
768
|
});
|
|
769
|
+
|
|
770
|
+
describe('duplicate', () => {
|
|
771
|
+
it('should duplicate a group with all config fields', async () => {
|
|
772
|
+
// Create source group with full config
|
|
773
|
+
await serverDB.insert(chatGroups).values({
|
|
774
|
+
config: {
|
|
775
|
+
maxResponseInRow: 5,
|
|
776
|
+
orchestratorModel: 'gpt-4o',
|
|
777
|
+
orchestratorProvider: 'openai',
|
|
778
|
+
responseOrder: 'sequential',
|
|
779
|
+
scene: 'productive',
|
|
780
|
+
},
|
|
781
|
+
id: 'source-group',
|
|
782
|
+
pinned: true,
|
|
783
|
+
title: 'Source Group',
|
|
784
|
+
userId,
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Create supervisor agent
|
|
788
|
+
await serverDB.insert(agents).values({
|
|
789
|
+
id: 'source-supervisor',
|
|
790
|
+
model: 'gpt-4o',
|
|
791
|
+
provider: 'openai',
|
|
792
|
+
title: 'Supervisor',
|
|
793
|
+
userId,
|
|
794
|
+
virtual: true,
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Link supervisor to group
|
|
798
|
+
await serverDB.insert(chatGroupsAgents).values({
|
|
799
|
+
agentId: 'source-supervisor',
|
|
800
|
+
chatGroupId: 'source-group',
|
|
801
|
+
order: -1,
|
|
802
|
+
role: 'supervisor',
|
|
803
|
+
userId,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const result = await agentGroupRepo.duplicate('source-group');
|
|
807
|
+
|
|
808
|
+
expect(result).not.toBeNull();
|
|
809
|
+
expect(result!.groupId).toBeDefined();
|
|
810
|
+
expect(result!.supervisorAgentId).toBeDefined();
|
|
811
|
+
expect(result!.groupId).not.toBe('source-group');
|
|
812
|
+
expect(result!.supervisorAgentId).not.toBe('source-supervisor');
|
|
813
|
+
|
|
814
|
+
// Verify duplicated group has correct config
|
|
815
|
+
const duplicatedGroup = await serverDB.query.chatGroups.findFirst({
|
|
816
|
+
where: (cg, { eq }) => eq(cg.id, result!.groupId),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
expect(duplicatedGroup).toEqual(
|
|
820
|
+
expect.objectContaining({
|
|
821
|
+
config: {
|
|
822
|
+
maxResponseInRow: 5,
|
|
823
|
+
orchestratorModel: 'gpt-4o',
|
|
824
|
+
orchestratorProvider: 'openai',
|
|
825
|
+
responseOrder: 'sequential',
|
|
826
|
+
scene: 'productive',
|
|
827
|
+
},
|
|
828
|
+
pinned: true,
|
|
829
|
+
title: 'Source Group (Copy)',
|
|
830
|
+
userId,
|
|
831
|
+
}),
|
|
832
|
+
);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should duplicate group with custom title', async () => {
|
|
836
|
+
await serverDB.insert(chatGroups).values({
|
|
837
|
+
id: 'title-group',
|
|
838
|
+
title: 'Original Title',
|
|
839
|
+
userId,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
await serverDB.insert(agents).values({
|
|
843
|
+
id: 'title-supervisor',
|
|
844
|
+
title: 'Supervisor',
|
|
845
|
+
userId,
|
|
846
|
+
virtual: true,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
await serverDB.insert(chatGroupsAgents).values({
|
|
850
|
+
agentId: 'title-supervisor',
|
|
851
|
+
chatGroupId: 'title-group',
|
|
852
|
+
order: -1,
|
|
853
|
+
role: 'supervisor',
|
|
854
|
+
userId,
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const result = await agentGroupRepo.duplicate('title-group', 'Custom New Title');
|
|
858
|
+
|
|
859
|
+
expect(result).not.toBeNull();
|
|
860
|
+
|
|
861
|
+
const duplicatedGroup = await serverDB.query.chatGroups.findFirst({
|
|
862
|
+
where: (cg, { eq }) => eq(cg.id, result!.groupId),
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(duplicatedGroup!.title).toBe('Custom New Title');
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('should copy virtual member agents (create new agents)', async () => {
|
|
869
|
+
// Create source group
|
|
870
|
+
await serverDB.insert(chatGroups).values({
|
|
871
|
+
id: 'virtual-member-group',
|
|
872
|
+
title: 'Virtual Member Group',
|
|
873
|
+
userId,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Create supervisor and virtual member agents
|
|
877
|
+
await serverDB.insert(agents).values([
|
|
878
|
+
{
|
|
879
|
+
id: 'vm-supervisor',
|
|
880
|
+
title: 'Supervisor',
|
|
881
|
+
userId,
|
|
882
|
+
virtual: true,
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
avatar: 'virtual-avatar.png',
|
|
886
|
+
backgroundColor: '#ff0000',
|
|
887
|
+
description: 'Virtual member description',
|
|
888
|
+
id: 'vm-virtual-member',
|
|
889
|
+
model: 'gpt-4',
|
|
890
|
+
provider: 'openai',
|
|
891
|
+
systemRole: 'You are a virtual assistant',
|
|
892
|
+
tags: ['tag1', 'tag2'],
|
|
893
|
+
title: 'Virtual Member',
|
|
894
|
+
userId,
|
|
895
|
+
virtual: true,
|
|
896
|
+
},
|
|
897
|
+
]);
|
|
898
|
+
|
|
899
|
+
// Link agents to group
|
|
900
|
+
await serverDB.insert(chatGroupsAgents).values([
|
|
901
|
+
{
|
|
902
|
+
agentId: 'vm-supervisor',
|
|
903
|
+
chatGroupId: 'virtual-member-group',
|
|
904
|
+
order: -1,
|
|
905
|
+
role: 'supervisor',
|
|
906
|
+
userId,
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
agentId: 'vm-virtual-member',
|
|
910
|
+
chatGroupId: 'virtual-member-group',
|
|
911
|
+
enabled: true,
|
|
912
|
+
order: 0,
|
|
913
|
+
role: 'participant',
|
|
914
|
+
userId,
|
|
915
|
+
},
|
|
916
|
+
]);
|
|
917
|
+
|
|
918
|
+
const result = await agentGroupRepo.duplicate('virtual-member-group');
|
|
919
|
+
|
|
920
|
+
expect(result).not.toBeNull();
|
|
921
|
+
|
|
922
|
+
// Verify new group has agents
|
|
923
|
+
const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
|
|
924
|
+
where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// 1 supervisor + 1 virtual member
|
|
928
|
+
expect(groupAgents).toHaveLength(2);
|
|
929
|
+
|
|
930
|
+
// Verify virtual member agent was copied (new agent created)
|
|
931
|
+
const virtualMemberRelation = groupAgents.find(
|
|
932
|
+
(ga) => ga.role === 'participant' && ga.agentId !== 'vm-virtual-member',
|
|
933
|
+
);
|
|
934
|
+
expect(virtualMemberRelation).toBeDefined();
|
|
935
|
+
|
|
936
|
+
// Verify copied agent has all fields
|
|
937
|
+
const copiedAgent = await serverDB.query.agents.findFirst({
|
|
938
|
+
where: (a, { eq }) => eq(a.id, virtualMemberRelation!.agentId),
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
expect(copiedAgent).toEqual(
|
|
942
|
+
expect.objectContaining({
|
|
943
|
+
avatar: 'virtual-avatar.png',
|
|
944
|
+
backgroundColor: '#ff0000',
|
|
945
|
+
description: 'Virtual member description',
|
|
946
|
+
model: 'gpt-4',
|
|
947
|
+
provider: 'openai',
|
|
948
|
+
systemRole: 'You are a virtual assistant',
|
|
949
|
+
tags: ['tag1', 'tag2'],
|
|
950
|
+
title: 'Virtual Member',
|
|
951
|
+
userId,
|
|
952
|
+
virtual: true,
|
|
953
|
+
}),
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
// Verify original virtual member still exists
|
|
957
|
+
const originalAgent = await serverDB.query.agents.findFirst({
|
|
958
|
+
where: (a, { eq }) => eq(a.id, 'vm-virtual-member'),
|
|
959
|
+
});
|
|
960
|
+
expect(originalAgent).toBeDefined();
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('should reference non-virtual member agents (only add relationship)', async () => {
|
|
964
|
+
// Create source group
|
|
965
|
+
await serverDB.insert(chatGroups).values({
|
|
966
|
+
id: 'nonvirtual-member-group',
|
|
967
|
+
title: 'Non-Virtual Member Group',
|
|
968
|
+
userId,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// Create supervisor and non-virtual member agents
|
|
972
|
+
await serverDB.insert(agents).values([
|
|
973
|
+
{
|
|
974
|
+
id: 'nvm-supervisor',
|
|
975
|
+
title: 'Supervisor',
|
|
976
|
+
userId,
|
|
977
|
+
virtual: true,
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
description: 'Regular agent description',
|
|
981
|
+
id: 'nvm-regular-member',
|
|
982
|
+
model: 'claude-3-opus',
|
|
983
|
+
provider: 'anthropic',
|
|
984
|
+
title: 'Regular Member',
|
|
985
|
+
userId,
|
|
986
|
+
virtual: false,
|
|
987
|
+
},
|
|
988
|
+
]);
|
|
989
|
+
|
|
990
|
+
// Link agents to group
|
|
991
|
+
await serverDB.insert(chatGroupsAgents).values([
|
|
992
|
+
{
|
|
993
|
+
agentId: 'nvm-supervisor',
|
|
994
|
+
chatGroupId: 'nonvirtual-member-group',
|
|
995
|
+
order: -1,
|
|
996
|
+
role: 'supervisor',
|
|
997
|
+
userId,
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
agentId: 'nvm-regular-member',
|
|
1001
|
+
chatGroupId: 'nonvirtual-member-group',
|
|
1002
|
+
enabled: true,
|
|
1003
|
+
order: 0,
|
|
1004
|
+
role: 'participant',
|
|
1005
|
+
userId,
|
|
1006
|
+
},
|
|
1007
|
+
]);
|
|
1008
|
+
|
|
1009
|
+
const result = await agentGroupRepo.duplicate('nonvirtual-member-group');
|
|
1010
|
+
|
|
1011
|
+
expect(result).not.toBeNull();
|
|
1012
|
+
|
|
1013
|
+
// Verify new group has agents
|
|
1014
|
+
const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
|
|
1015
|
+
where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// 1 supervisor + 1 non-virtual member
|
|
1019
|
+
expect(groupAgents).toHaveLength(2);
|
|
1020
|
+
|
|
1021
|
+
// Verify non-virtual member uses the SAME agent ID (just added relationship)
|
|
1022
|
+
const regularMemberRelation = groupAgents.find((ga) => ga.agentId === 'nvm-regular-member');
|
|
1023
|
+
expect(regularMemberRelation).toBeDefined();
|
|
1024
|
+
expect(regularMemberRelation!.role).toBe('participant');
|
|
1025
|
+
expect(regularMemberRelation!.enabled).toBe(true);
|
|
1026
|
+
|
|
1027
|
+
// Verify no new agent was created for the regular member
|
|
1028
|
+
const allAgentsWithTitle = await serverDB.query.agents.findMany({
|
|
1029
|
+
where: (a, { and, eq }) => and(eq(a.userId, userId), eq(a.title, 'Regular Member')),
|
|
1030
|
+
});
|
|
1031
|
+
// Should only have the original one
|
|
1032
|
+
expect(allAgentsWithTitle).toHaveLength(1);
|
|
1033
|
+
expect(allAgentsWithTitle[0].id).toBe('nvm-regular-member');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('should handle mixed virtual and non-virtual members', async () => {
|
|
1037
|
+
// Create source group
|
|
1038
|
+
await serverDB.insert(chatGroups).values({
|
|
1039
|
+
id: 'mixed-member-group',
|
|
1040
|
+
title: 'Mixed Member Group',
|
|
1041
|
+
userId,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
// Create supervisor, virtual member, and non-virtual member agents
|
|
1045
|
+
await serverDB.insert(agents).values([
|
|
1046
|
+
{ id: 'mixed-supervisor', title: 'Supervisor', userId, virtual: true },
|
|
1047
|
+
{ id: 'mixed-virtual', title: 'Virtual Agent', userId, virtual: true },
|
|
1048
|
+
{ id: 'mixed-regular', title: 'Regular Agent', userId, virtual: false },
|
|
1049
|
+
]);
|
|
1050
|
+
|
|
1051
|
+
// Link agents to group
|
|
1052
|
+
await serverDB.insert(chatGroupsAgents).values([
|
|
1053
|
+
{
|
|
1054
|
+
agentId: 'mixed-supervisor',
|
|
1055
|
+
chatGroupId: 'mixed-member-group',
|
|
1056
|
+
order: -1,
|
|
1057
|
+
role: 'supervisor',
|
|
1058
|
+
userId,
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
agentId: 'mixed-virtual',
|
|
1062
|
+
chatGroupId: 'mixed-member-group',
|
|
1063
|
+
order: 0,
|
|
1064
|
+
role: 'participant',
|
|
1065
|
+
userId,
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
agentId: 'mixed-regular',
|
|
1069
|
+
chatGroupId: 'mixed-member-group',
|
|
1070
|
+
order: 1,
|
|
1071
|
+
role: 'participant',
|
|
1072
|
+
userId,
|
|
1073
|
+
},
|
|
1074
|
+
]);
|
|
1075
|
+
|
|
1076
|
+
const result = await agentGroupRepo.duplicate('mixed-member-group');
|
|
1077
|
+
|
|
1078
|
+
expect(result).not.toBeNull();
|
|
1079
|
+
|
|
1080
|
+
const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
|
|
1081
|
+
where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// 1 supervisor + 1 virtual (copied) + 1 non-virtual (referenced)
|
|
1085
|
+
expect(groupAgents).toHaveLength(3);
|
|
1086
|
+
|
|
1087
|
+
// Verify non-virtual member references original agent
|
|
1088
|
+
const regularRelation = groupAgents.find((ga) => ga.agentId === 'mixed-regular');
|
|
1089
|
+
expect(regularRelation).toBeDefined();
|
|
1090
|
+
|
|
1091
|
+
// Verify virtual member was copied (new agent ID)
|
|
1092
|
+
const virtualRelation = groupAgents.find(
|
|
1093
|
+
(ga) => ga.role === 'participant' && ga.agentId !== 'mixed-regular',
|
|
1094
|
+
);
|
|
1095
|
+
expect(virtualRelation).toBeDefined();
|
|
1096
|
+
expect(virtualRelation!.agentId).not.toBe('mixed-virtual');
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('should return null for non-existent group', async () => {
|
|
1100
|
+
const result = await agentGroupRepo.duplicate('non-existent-group');
|
|
1101
|
+
|
|
1102
|
+
expect(result).toBeNull();
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it('should not duplicate group belonging to another user', async () => {
|
|
1106
|
+
// Create group for other user
|
|
1107
|
+
await serverDB.insert(chatGroups).values({
|
|
1108
|
+
id: 'other-user-dup-group',
|
|
1109
|
+
title: 'Other User Group',
|
|
1110
|
+
userId: otherUserId,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const result = await agentGroupRepo.duplicate('other-user-dup-group');
|
|
1114
|
+
|
|
1115
|
+
expect(result).toBeNull();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it('should preserve member order in duplicated group', async () => {
|
|
1119
|
+
// Create source group
|
|
1120
|
+
await serverDB.insert(chatGroups).values({
|
|
1121
|
+
id: 'order-group',
|
|
1122
|
+
title: 'Order Group',
|
|
1123
|
+
userId,
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
// Create agents
|
|
1127
|
+
await serverDB.insert(agents).values([
|
|
1128
|
+
{ id: 'order-supervisor', title: 'Supervisor', userId, virtual: true },
|
|
1129
|
+
{ id: 'order-agent-1', title: 'Agent 1', userId, virtual: false },
|
|
1130
|
+
{ id: 'order-agent-2', title: 'Agent 2', userId, virtual: false },
|
|
1131
|
+
{ id: 'order-agent-3', title: 'Agent 3', userId, virtual: false },
|
|
1132
|
+
]);
|
|
1133
|
+
|
|
1134
|
+
// Link agents with specific order
|
|
1135
|
+
await serverDB.insert(chatGroupsAgents).values([
|
|
1136
|
+
{
|
|
1137
|
+
agentId: 'order-supervisor',
|
|
1138
|
+
chatGroupId: 'order-group',
|
|
1139
|
+
order: -1,
|
|
1140
|
+
role: 'supervisor',
|
|
1141
|
+
userId,
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
agentId: 'order-agent-1',
|
|
1145
|
+
chatGroupId: 'order-group',
|
|
1146
|
+
order: 2,
|
|
1147
|
+
role: 'participant',
|
|
1148
|
+
userId,
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
agentId: 'order-agent-2',
|
|
1152
|
+
chatGroupId: 'order-group',
|
|
1153
|
+
order: 0,
|
|
1154
|
+
role: 'participant',
|
|
1155
|
+
userId,
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
agentId: 'order-agent-3',
|
|
1159
|
+
chatGroupId: 'order-group',
|
|
1160
|
+
order: 1,
|
|
1161
|
+
role: 'participant',
|
|
1162
|
+
userId,
|
|
1163
|
+
},
|
|
1164
|
+
]);
|
|
1165
|
+
|
|
1166
|
+
const result = await agentGroupRepo.duplicate('order-group');
|
|
1167
|
+
|
|
1168
|
+
expect(result).not.toBeNull();
|
|
1169
|
+
|
|
1170
|
+
const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
|
|
1171
|
+
where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// Verify order is preserved
|
|
1175
|
+
const supervisorRelation = groupAgents.find((ga) => ga.role === 'supervisor');
|
|
1176
|
+
expect(supervisorRelation!.order).toBe(-1);
|
|
1177
|
+
|
|
1178
|
+
const agent1Relation = groupAgents.find((ga) => ga.agentId === 'order-agent-1');
|
|
1179
|
+
expect(agent1Relation!.order).toBe(2);
|
|
1180
|
+
|
|
1181
|
+
const agent2Relation = groupAgents.find((ga) => ga.agentId === 'order-agent-2');
|
|
1182
|
+
expect(agent2Relation!.order).toBe(0);
|
|
1183
|
+
|
|
1184
|
+
const agent3Relation = groupAgents.find((ga) => ga.agentId === 'order-agent-3');
|
|
1185
|
+
expect(agent3Relation!.order).toBe(1);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it('should duplicate group with default title when source has no title', async () => {
|
|
1189
|
+
// Create source group without title
|
|
1190
|
+
await serverDB.insert(chatGroups).values({
|
|
1191
|
+
id: 'no-title-group',
|
|
1192
|
+
title: null,
|
|
1193
|
+
userId,
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
await serverDB.insert(agents).values({
|
|
1197
|
+
id: 'no-title-supervisor',
|
|
1198
|
+
title: 'Supervisor',
|
|
1199
|
+
userId,
|
|
1200
|
+
virtual: true,
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
await serverDB.insert(chatGroupsAgents).values({
|
|
1204
|
+
agentId: 'no-title-supervisor',
|
|
1205
|
+
chatGroupId: 'no-title-group',
|
|
1206
|
+
order: -1,
|
|
1207
|
+
role: 'supervisor',
|
|
1208
|
+
userId,
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
const result = await agentGroupRepo.duplicate('no-title-group');
|
|
1212
|
+
|
|
1213
|
+
expect(result).not.toBeNull();
|
|
1214
|
+
|
|
1215
|
+
const duplicatedGroup = await serverDB.query.chatGroups.findFirst({
|
|
1216
|
+
where: (cg, { eq }) => eq(cg.id, result!.groupId),
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
expect(duplicatedGroup!.title).toBe('Copy');
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it('should create new supervisor agent with source supervisor config', async () => {
|
|
1223
|
+
// Create source group
|
|
1224
|
+
await serverDB.insert(chatGroups).values({
|
|
1225
|
+
id: 'supervisor-config-group',
|
|
1226
|
+
title: 'Supervisor Config Group',
|
|
1227
|
+
userId,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// Create supervisor with specific config
|
|
1231
|
+
await serverDB.insert(agents).values({
|
|
1232
|
+
id: 'source-supervisor-with-config',
|
|
1233
|
+
model: 'claude-3-opus',
|
|
1234
|
+
provider: 'anthropic',
|
|
1235
|
+
title: 'Custom Supervisor',
|
|
1236
|
+
userId,
|
|
1237
|
+
virtual: true,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
await serverDB.insert(chatGroupsAgents).values({
|
|
1241
|
+
agentId: 'source-supervisor-with-config',
|
|
1242
|
+
chatGroupId: 'supervisor-config-group',
|
|
1243
|
+
order: -1,
|
|
1244
|
+
role: 'supervisor',
|
|
1245
|
+
userId,
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const result = await agentGroupRepo.duplicate('supervisor-config-group');
|
|
1249
|
+
|
|
1250
|
+
expect(result).not.toBeNull();
|
|
1251
|
+
|
|
1252
|
+
// Verify new supervisor has same config
|
|
1253
|
+
const newSupervisor = await serverDB.query.agents.findFirst({
|
|
1254
|
+
where: (a, { eq }) => eq(a.id, result!.supervisorAgentId),
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
expect(newSupervisor).toEqual(
|
|
1258
|
+
expect.objectContaining({
|
|
1259
|
+
model: 'claude-3-opus',
|
|
1260
|
+
provider: 'anthropic',
|
|
1261
|
+
title: 'Custom Supervisor',
|
|
1262
|
+
virtual: true,
|
|
1263
|
+
}),
|
|
1264
|
+
);
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
769
1267
|
});
|
|
@@ -304,4 +304,154 @@ export class AgentGroupRepository {
|
|
|
304
304
|
removedFromGroup: agentIds.length,
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Duplicate a chat group with all its members.
|
|
310
|
+
* - Creates a new group with the same config
|
|
311
|
+
* - Creates a new supervisor agent
|
|
312
|
+
* - For virtual member agents: creates new copies
|
|
313
|
+
* - For non-virtual member agents: adds relationship only (references same agents)
|
|
314
|
+
*
|
|
315
|
+
* @param groupId - The chat group ID to duplicate
|
|
316
|
+
* @param newTitle - Optional new title for the duplicated group
|
|
317
|
+
* @returns The new group ID and supervisor agent ID, or null if source not found
|
|
318
|
+
*/
|
|
319
|
+
async duplicate(
|
|
320
|
+
groupId: string,
|
|
321
|
+
newTitle?: string,
|
|
322
|
+
): Promise<{ groupId: string; supervisorAgentId: string } | null> {
|
|
323
|
+
// 1. Get the source group
|
|
324
|
+
const sourceGroup = await this.db.query.chatGroups.findFirst({
|
|
325
|
+
where: and(eq(chatGroups.id, groupId), eq(chatGroups.userId, this.userId)),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!sourceGroup) return null;
|
|
329
|
+
|
|
330
|
+
// 2. Get all agents in the group with their details
|
|
331
|
+
const groupAgentsWithDetails = await this.db
|
|
332
|
+
.select({
|
|
333
|
+
agent: agents,
|
|
334
|
+
enabled: chatGroupsAgents.enabled,
|
|
335
|
+
order: chatGroupsAgents.order,
|
|
336
|
+
role: chatGroupsAgents.role,
|
|
337
|
+
})
|
|
338
|
+
.from(chatGroupsAgents)
|
|
339
|
+
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
340
|
+
.where(eq(chatGroupsAgents.chatGroupId, groupId))
|
|
341
|
+
.orderBy(chatGroupsAgents.order);
|
|
342
|
+
|
|
343
|
+
// 3. Separate supervisor, virtual members, and non-virtual members
|
|
344
|
+
let sourceSupervisor: (typeof groupAgentsWithDetails)[number] | undefined;
|
|
345
|
+
const virtualMembers: (typeof groupAgentsWithDetails)[number][] = [];
|
|
346
|
+
const nonVirtualMembers: (typeof groupAgentsWithDetails)[number][] = [];
|
|
347
|
+
|
|
348
|
+
for (const row of groupAgentsWithDetails) {
|
|
349
|
+
if (row.role === 'supervisor') {
|
|
350
|
+
sourceSupervisor = row;
|
|
351
|
+
} else if (row.agent.virtual) {
|
|
352
|
+
virtualMembers.push(row);
|
|
353
|
+
} else {
|
|
354
|
+
nonVirtualMembers.push(row);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Use transaction to ensure atomicity
|
|
359
|
+
return this.db.transaction(async (trx) => {
|
|
360
|
+
// 4. Create the new group
|
|
361
|
+
const [newGroup] = await trx
|
|
362
|
+
.insert(chatGroups)
|
|
363
|
+
.values({
|
|
364
|
+
config: sourceGroup.config,
|
|
365
|
+
pinned: sourceGroup.pinned,
|
|
366
|
+
title: newTitle || (sourceGroup.title ? `${sourceGroup.title} (Copy)` : 'Copy'),
|
|
367
|
+
userId: this.userId,
|
|
368
|
+
})
|
|
369
|
+
.returning();
|
|
370
|
+
|
|
371
|
+
// 5. Create new supervisor agent
|
|
372
|
+
const supervisorAgent = sourceSupervisor?.agent;
|
|
373
|
+
const [newSupervisor] = await trx
|
|
374
|
+
.insert(agents)
|
|
375
|
+
.values({
|
|
376
|
+
model: supervisorAgent?.model,
|
|
377
|
+
provider: supervisorAgent?.provider,
|
|
378
|
+
title: supervisorAgent?.title || 'Supervisor',
|
|
379
|
+
userId: this.userId,
|
|
380
|
+
virtual: true,
|
|
381
|
+
})
|
|
382
|
+
.returning();
|
|
383
|
+
|
|
384
|
+
// 6. Create copies of virtual member agents using include mode
|
|
385
|
+
const newVirtualAgentMap = new Map<string, string>(); // oldId -> newId
|
|
386
|
+
if (virtualMembers.length > 0) {
|
|
387
|
+
const virtualAgentConfigs = virtualMembers.map((member) => ({
|
|
388
|
+
// Metadata
|
|
389
|
+
avatar: member.agent.avatar,
|
|
390
|
+
backgroundColor: member.agent.backgroundColor,
|
|
391
|
+
// Config
|
|
392
|
+
chatConfig: member.agent.chatConfig,
|
|
393
|
+
description: member.agent.description,
|
|
394
|
+
fewShots: member.agent.fewShots,
|
|
395
|
+
|
|
396
|
+
model: member.agent.model,
|
|
397
|
+
openingMessage: member.agent.openingMessage,
|
|
398
|
+
openingQuestions: member.agent.openingQuestions,
|
|
399
|
+
params: member.agent.params,
|
|
400
|
+
plugins: member.agent.plugins,
|
|
401
|
+
provider: member.agent.provider,
|
|
402
|
+
systemRole: member.agent.systemRole,
|
|
403
|
+
tags: member.agent.tags,
|
|
404
|
+
title: member.agent.title,
|
|
405
|
+
tts: member.agent.tts,
|
|
406
|
+
// User & virtual flag
|
|
407
|
+
userId: this.userId,
|
|
408
|
+
virtual: true,
|
|
409
|
+
}));
|
|
410
|
+
|
|
411
|
+
const newVirtualAgents = await trx.insert(agents).values(virtualAgentConfigs).returning();
|
|
412
|
+
|
|
413
|
+
// Map old agent IDs to new agent IDs
|
|
414
|
+
for (const [i, virtualMember] of virtualMembers.entries()) {
|
|
415
|
+
newVirtualAgentMap.set(virtualMember.agent.id, newVirtualAgents[i].id);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 7. Create group-agent relationships
|
|
420
|
+
const groupAgentValues: NewChatGroupAgent[] = [
|
|
421
|
+
// Supervisor
|
|
422
|
+
{
|
|
423
|
+
agentId: newSupervisor.id,
|
|
424
|
+
chatGroupId: newGroup.id,
|
|
425
|
+
order: -1,
|
|
426
|
+
role: 'supervisor',
|
|
427
|
+
userId: this.userId,
|
|
428
|
+
},
|
|
429
|
+
// Virtual members (using new copied agents)
|
|
430
|
+
...virtualMembers.map((member) => ({
|
|
431
|
+
agentId: newVirtualAgentMap.get(member.agent.id)!,
|
|
432
|
+
chatGroupId: newGroup.id,
|
|
433
|
+
enabled: member.enabled,
|
|
434
|
+
order: member.order,
|
|
435
|
+
role: member.role || 'participant',
|
|
436
|
+
userId: this.userId,
|
|
437
|
+
})),
|
|
438
|
+
// Non-virtual members (referencing same agents - only add relationship)
|
|
439
|
+
...nonVirtualMembers.map((member) => ({
|
|
440
|
+
agentId: member.agent.id,
|
|
441
|
+
chatGroupId: newGroup.id,
|
|
442
|
+
enabled: member.enabled,
|
|
443
|
+
order: member.order,
|
|
444
|
+
role: member.role || 'participant',
|
|
445
|
+
userId: this.userId,
|
|
446
|
+
})),
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
await trx.insert(chatGroupsAgents).values(groupAgentValues);
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
groupId: newGroup.id,
|
|
453
|
+
supervisorAgentId: newSupervisor.id,
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
}
|
|
307
457
|
}
|