Compare commits
34 Commits
main
...
feat/promp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fd84216cf | ||
|
|
d8997fdf0e | ||
|
|
bb6ee0dc58 | ||
|
|
c4e86539c6 | ||
|
|
f1bc15b3d5 | ||
|
|
58678be0f8 | ||
|
|
441e69181c | ||
|
|
2c1d7a6b71 | ||
|
|
384d6c870b | ||
|
|
013c002cbb | ||
|
|
e062ed5832 | ||
|
|
d07d05a8d0 | ||
|
|
5b38ce8fd9 | ||
|
|
0a61e3cb39 | ||
|
|
a52c37faad | ||
|
|
1f49c569c3 | ||
|
|
479ce5df48 | ||
|
|
c37e368d98 | ||
|
|
fd29cbed4f | ||
|
|
277a321155 | ||
|
|
0dba5c6450 | ||
|
|
93490764e6 | ||
|
|
094320fcd9 | ||
|
|
cee11d3353 | ||
|
|
69772317b2 | ||
|
|
607a5a2fcf | ||
|
|
7c3356e10b | ||
|
|
d4fd0047cb | ||
|
|
797fdf4286 | ||
|
|
623dfa5b63 | ||
|
|
600641d02f | ||
|
|
d65accddc1 | ||
|
|
195d2e2014 | ||
|
|
c0ae6f277f |
@@ -51,6 +51,7 @@ const createGroupPipeline = (query, skip, limit) => {
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
'productionPrompt.prompt': 1,
|
||||
'productionPrompt.tool_resources': 1,
|
||||
// 'productionPrompt._id': 1,
|
||||
// 'productionPrompt.type': 1,
|
||||
},
|
||||
@@ -328,6 +329,7 @@ async function getListPromptGroupsByAccess({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
'productionPrompt.prompt': 1,
|
||||
'productionPrompt.tool_resources': 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -411,7 +413,10 @@ module.exports = {
|
||||
prompt: newPrompt,
|
||||
group: {
|
||||
...newPromptGroup,
|
||||
productionPrompt: { prompt: newPrompt.prompt },
|
||||
productionPrompt: {
|
||||
prompt: newPrompt.prompt,
|
||||
tool_resources: newPrompt.tool_resources,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -562,3 +562,884 @@ describe('Prompt ACL Permissions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt Model - File Attachments', () => {
|
||||
describe('Creating Prompts with tool_resources', () => {
|
||||
it('should create a prompt with file attachments in tool_resources', async () => {
|
||||
const testGroup = await PromptGroup.create({
|
||||
name: 'Attachment Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
const promptData = {
|
||||
prompt: {
|
||||
prompt: 'Test prompt with file attachments',
|
||||
type: 'text',
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
image_edit: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
},
|
||||
},
|
||||
author: testUsers.owner._id,
|
||||
};
|
||||
|
||||
const result = await promptFns.savePrompt(promptData);
|
||||
|
||||
expect(result.prompt).toBeTruthy();
|
||||
expect(result.prompt.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
image_edit: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
});
|
||||
|
||||
const savedPrompt = await Prompt.findById(result.prompt._id);
|
||||
expect(savedPrompt.tool_resources).toEqual(promptData.prompt.tool_resources);
|
||||
});
|
||||
|
||||
it('should create a prompt without tool_resources when none provided', async () => {
|
||||
const testGroup = await PromptGroup.create({
|
||||
name: 'No Attachment Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
const promptData = {
|
||||
prompt: {
|
||||
prompt: 'Test prompt without attachments',
|
||||
type: 'text',
|
||||
groupId: testGroup._id,
|
||||
},
|
||||
author: testUsers.owner._id,
|
||||
};
|
||||
|
||||
const result = await promptFns.savePrompt(promptData);
|
||||
|
||||
expect(result.prompt).toBeTruthy();
|
||||
expect(result.prompt.tool_resources).toEqual({});
|
||||
|
||||
const savedPrompt = await Prompt.findById(result.prompt._id);
|
||||
expect(savedPrompt.tool_resources).toEqual({});
|
||||
});
|
||||
|
||||
it('should create a prompt group with tool_resources', async () => {
|
||||
const saveData = {
|
||||
prompt: {
|
||||
type: 'text',
|
||||
prompt: 'Test prompt with file attachments',
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
ocr: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
group: {
|
||||
name: 'Test Prompt Group with Attachments',
|
||||
category: 'test-category',
|
||||
oneliner: 'Test description',
|
||||
},
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
};
|
||||
|
||||
const result = await promptFns.createPromptGroup(saveData);
|
||||
|
||||
expect(result.prompt).toBeTruthy();
|
||||
expect(result.group).toBeTruthy();
|
||||
expect(result.prompt.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
ocr: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.group.productionPrompt.tool_resources).toEqual(result.prompt.tool_resources);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Retrieving Prompts with tool_resources', () => {
|
||||
let testGroup;
|
||||
let testPrompt;
|
||||
|
||||
beforeEach(async () => {
|
||||
testGroup = await PromptGroup.create({
|
||||
name: 'Retrieval Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
testPrompt = await Prompt.create({
|
||||
prompt: 'Test prompt with attachments for retrieval',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Prompt.deleteMany({});
|
||||
await PromptGroup.deleteMany({});
|
||||
});
|
||||
|
||||
it('should retrieve a prompt with tool_resources', async () => {
|
||||
const result = await promptFns.getPrompt({ _id: testPrompt._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve prompts with tool_resources by groupId', async () => {
|
||||
const result = await promptFns.getPrompts({ groupId: testGroup._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prompts without tool_resources', async () => {
|
||||
const promptWithoutAttachments = await Prompt.create({
|
||||
prompt: 'Test prompt without attachments',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithoutAttachments._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Updating Prompts with tool_resources', () => {
|
||||
let testGroup;
|
||||
|
||||
beforeEach(async () => {
|
||||
testGroup = await PromptGroup.create({
|
||||
name: 'Update Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
await Prompt.create({
|
||||
prompt: 'Original prompt',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Prompt.deleteMany({});
|
||||
await PromptGroup.deleteMany({});
|
||||
});
|
||||
|
||||
it('should update prompt with new tool_resources', async () => {
|
||||
const updatedPromptData = {
|
||||
prompt: {
|
||||
prompt: 'Updated prompt with new attachments',
|
||||
type: 'text',
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
author: testUsers.owner._id,
|
||||
};
|
||||
|
||||
const result = await promptFns.savePrompt(updatedPromptData);
|
||||
|
||||
expect(result.prompt).toBeTruthy();
|
||||
expect(result.prompt.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update prompt to remove tool_resources', async () => {
|
||||
const updatedPromptData = {
|
||||
prompt: {
|
||||
prompt: 'Updated prompt without attachments',
|
||||
type: 'text',
|
||||
groupId: testGroup._id,
|
||||
// No tool_resources field
|
||||
},
|
||||
author: testUsers.owner._id,
|
||||
};
|
||||
|
||||
const result = await promptFns.savePrompt(updatedPromptData);
|
||||
|
||||
expect(result.prompt).toBeTruthy();
|
||||
expect(result.prompt.tool_resources).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deleting Prompts with tool_resources', () => {
|
||||
let testGroup;
|
||||
let testPrompt;
|
||||
|
||||
beforeEach(async () => {
|
||||
testGroup = await PromptGroup.create({
|
||||
name: 'Deletion Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
testPrompt = await Prompt.create({
|
||||
prompt: 'Prompt to be deleted',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Prompt.deleteMany({});
|
||||
await PromptGroup.deleteMany({});
|
||||
});
|
||||
|
||||
it('should delete a prompt with tool_resources', async () => {
|
||||
const result = await promptFns.deletePrompt({
|
||||
promptId: testPrompt._id,
|
||||
groupId: testGroup._id,
|
||||
author: testUsers.owner._id,
|
||||
role: SystemRoles.USER,
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe('Prompt deleted successfully');
|
||||
|
||||
const deletedPrompt = await Prompt.findById(testPrompt._id);
|
||||
expect(deletedPrompt).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete prompt group when last prompt with tool_resources is deleted', async () => {
|
||||
const result = await promptFns.deletePrompt({
|
||||
promptId: testPrompt._id,
|
||||
groupId: testGroup._id,
|
||||
author: testUsers.owner._id,
|
||||
role: SystemRoles.USER,
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe('Prompt deleted successfully');
|
||||
expect(result.promptGroup).toBeTruthy();
|
||||
expect(result.promptGroup.message).toBe('Prompt group deleted successfully');
|
||||
|
||||
const deletedPrompt = await Prompt.findById(testPrompt._id);
|
||||
const deletedGroup = await PromptGroup.findById(testGroup._id);
|
||||
expect(deletedPrompt).toBeNull();
|
||||
expect(deletedGroup).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Making Prompts Production with tool_resources', () => {
|
||||
let testGroup;
|
||||
let testPrompt;
|
||||
|
||||
beforeEach(async () => {
|
||||
testGroup = await PromptGroup.create({
|
||||
name: 'Production Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
testPrompt = await Prompt.create({
|
||||
prompt: 'Prompt to be made production',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
},
|
||||
image_edit: {
|
||||
file_ids: ['file-3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Prompt.deleteMany({});
|
||||
await PromptGroup.deleteMany({});
|
||||
});
|
||||
|
||||
it('should make a prompt with tool_resources production', async () => {
|
||||
const result = await promptFns.makePromptProduction(testPrompt._id.toString());
|
||||
|
||||
expect(result.message).toBe('Prompt production made successfully');
|
||||
|
||||
const updatedGroup = await PromptGroup.findById(testGroup._id);
|
||||
expect(updatedGroup.productionId.toString()).toBe(testPrompt._id.toString());
|
||||
});
|
||||
|
||||
it('should return error message when prompt not found', async () => {
|
||||
const nonExistentId = new mongoose.Types.ObjectId().toString();
|
||||
|
||||
const result = await promptFns.makePromptProduction(nonExistentId);
|
||||
expect(result.message).toBe('Error making prompt production');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt Groups with tool_resources projection', () => {
|
||||
let testGroup;
|
||||
let testPrompt;
|
||||
|
||||
beforeEach(async () => {
|
||||
testGroup = await PromptGroup.create({
|
||||
name: 'Projection Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
testPrompt = await Prompt.create({
|
||||
prompt: 'Test prompt for projection',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-2', 'file-3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await PromptGroup.findByIdAndUpdate(testGroup._id, {
|
||||
productionId: testPrompt._id,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Prompt.deleteMany({});
|
||||
await PromptGroup.deleteMany({});
|
||||
});
|
||||
|
||||
it('should include tool_resources in prompt group projection', async () => {
|
||||
const mockReq = { user: { id: testUsers.owner._id } };
|
||||
const filter = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
category: 'testing',
|
||||
};
|
||||
|
||||
const result = await promptFns.getPromptGroups(mockReq, filter);
|
||||
|
||||
expect(result.promptGroups).toBeTruthy();
|
||||
expect(Array.isArray(result.promptGroups)).toBe(true);
|
||||
expect(result.promptGroups.length).toBeGreaterThan(0);
|
||||
|
||||
const foundGroup = result.promptGroups.find(
|
||||
(group) => group._id.toString() === testGroup._id.toString(),
|
||||
);
|
||||
expect(foundGroup).toBeTruthy();
|
||||
expect(foundGroup.productionPrompt.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-2', 'file-3'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling with tool_resources', () => {
|
||||
it('should handle errors when creating prompt with tool_resources', async () => {
|
||||
const invalidPromptData = {
|
||||
prompt: {
|
||||
prompt: 'Test prompt',
|
||||
type: 'text',
|
||||
groupId: 'invalid-id',
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
author: testUsers.owner._id,
|
||||
};
|
||||
|
||||
const result = await promptFns.savePrompt(invalidPromptData);
|
||||
|
||||
expect(result.message).toBe('Error saving prompt');
|
||||
});
|
||||
|
||||
it('should handle errors when retrieving prompt with tool_resources', async () => {
|
||||
const result = await promptFns.getPrompt({ _id: 'invalid-id' });
|
||||
|
||||
expect(result.message).toBe('Error getting prompt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases - File Attachment Scenarios', () => {
|
||||
let testGroup;
|
||||
let testPrompt;
|
||||
|
||||
beforeEach(async () => {
|
||||
testGroup = await PromptGroup.create({
|
||||
name: 'Edge Case Test Group',
|
||||
category: 'testing',
|
||||
author: testUsers.owner._id,
|
||||
authorName: testUsers.owner.name,
|
||||
productionId: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
testPrompt = await Prompt.create({
|
||||
prompt: 'Test prompt with file attachments for edge cases',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2', 'file-3'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
image_edit: {
|
||||
file_ids: ['file-5', 'file-6'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Prompt.deleteMany({});
|
||||
await PromptGroup.deleteMany({});
|
||||
});
|
||||
|
||||
describe('Orphaned File References', () => {
|
||||
it('should maintain prompt functionality when referenced files are deleted', async () => {
|
||||
const result = await promptFns.getPrompt({ _id: testPrompt._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2', 'file-3'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
image_edit: {
|
||||
file_ids: ['file-5', 'file-6'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe('Test prompt with file attachments for edge cases');
|
||||
expect(result.type).toBe('text');
|
||||
});
|
||||
|
||||
it('should handle prompts with empty file_ids arrays', async () => {
|
||||
const promptWithEmptyFileIds = await Prompt.create({
|
||||
prompt: 'Prompt with empty file_ids',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: [],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithEmptyFileIds._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: [],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prompts with null/undefined file_ids', async () => {
|
||||
const promptWithNullFileIds = await Prompt.create({
|
||||
prompt: 'Prompt with null file_ids',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: null,
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithNullFileIds._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid File References', () => {
|
||||
it('should handle prompts with malformed file_ids', async () => {
|
||||
const promptWithMalformedIds = await Prompt.create({
|
||||
prompt: 'Prompt with malformed file_ids',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['', null, undefined, 'invalid-id', 'file-valid'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: [123, {}, []],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithMalformedIds._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['', null, null, 'invalid-id', 'file-valid'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: [123, {}, []],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prompts with duplicate file_ids', async () => {
|
||||
const promptWithDuplicates = await Prompt.create({
|
||||
prompt: 'Prompt with duplicate file_ids',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2', 'file-1', 'file-3', 'file-2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithDuplicates._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2', 'file-1', 'file-3', 'file-2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Resource Edge Cases', () => {
|
||||
it('should handle prompts with unknown tool resource types', async () => {
|
||||
const promptWithUnknownTools = await Prompt.create({
|
||||
prompt: 'Prompt with unknown tool resources',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
unknown_tool: {
|
||||
file_ids: ['file-1'],
|
||||
},
|
||||
another_unknown: {
|
||||
file_ids: ['file-2', 'file-3'],
|
||||
},
|
||||
file_search: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithUnknownTools._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
unknown_tool: {
|
||||
file_ids: ['file-1'],
|
||||
},
|
||||
another_unknown: {
|
||||
file_ids: ['file-2', 'file-3'],
|
||||
},
|
||||
file_search: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prompts with malformed tool_resources structure', async () => {
|
||||
const promptWithMalformedTools = await Prompt.create({
|
||||
prompt: 'Prompt with malformed tool_resources',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: 'not-an-object',
|
||||
execute_code: {
|
||||
file_ids: 'not-an-array',
|
||||
},
|
||||
image_edit: {
|
||||
wrong_property: ['file-1'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithMalformedTools._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources).toEqual({
|
||||
file_search: 'not-an-object',
|
||||
execute_code: {
|
||||
file_ids: 'not-an-array',
|
||||
},
|
||||
image_edit: {
|
||||
wrong_property: ['file-1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt Deletion vs File Persistence', () => {
|
||||
it('should delete prompt but preserve file references in tool_resources', async () => {
|
||||
const beforeDelete = await promptFns.getPrompt({ _id: testPrompt._id });
|
||||
expect(beforeDelete.tool_resources).toEqual({
|
||||
file_search: {
|
||||
file_ids: ['file-1', 'file-2', 'file-3'],
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: ['file-4'],
|
||||
},
|
||||
image_edit: {
|
||||
file_ids: ['file-5', 'file-6'],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.deletePrompt({
|
||||
promptId: testPrompt._id,
|
||||
groupId: testGroup._id,
|
||||
author: testUsers.owner._id,
|
||||
role: SystemRoles.USER,
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe('Prompt deleted successfully');
|
||||
|
||||
const deletedPrompt = await Prompt.findById(testPrompt._id);
|
||||
expect(deletedPrompt).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle prompt deletion when tool_resources contain non-existent files', async () => {
|
||||
const promptWithNonExistentFiles = await Prompt.create({
|
||||
prompt: 'Prompt with non-existent file references',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['non-existent-file-1', 'non-existent-file-2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.deletePrompt({
|
||||
promptId: promptWithNonExistentFiles._id,
|
||||
groupId: testGroup._id,
|
||||
author: testUsers.owner._id,
|
||||
role: SystemRoles.USER,
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe('Prompt deleted successfully');
|
||||
|
||||
const deletedPrompt = await Prompt.findById(promptWithNonExistentFiles._id);
|
||||
expect(deletedPrompt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Large File Collections', () => {
|
||||
it('should handle prompts with many file attachments', async () => {
|
||||
const manyFileIds = Array.from({ length: 100 }, (_, i) => `file-${i + 1}`);
|
||||
|
||||
const promptWithManyFiles = await Prompt.create({
|
||||
prompt: 'Prompt with many file attachments',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: manyFileIds.slice(0, 50),
|
||||
},
|
||||
execute_code: {
|
||||
file_ids: manyFileIds.slice(50, 100),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithManyFiles._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources.file_search.file_ids).toHaveLength(50);
|
||||
expect(result.tool_resources.execute_code.file_ids).toHaveLength(50);
|
||||
expect(result.tool_resources.file_search.file_ids[0]).toBe('file-1');
|
||||
expect(result.tool_resources.execute_code.file_ids[49]).toBe('file-100');
|
||||
});
|
||||
|
||||
it('should handle prompts with very long file_ids', async () => {
|
||||
const longFileId = 'a'.repeat(1000);
|
||||
|
||||
const promptWithLongFileId = await Prompt.create({
|
||||
prompt: 'Prompt with very long file ID',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: [longFileId],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptFns.getPrompt({ _id: promptWithLongFileId._id });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.tool_resources.file_search.file_ids[0]).toBe(longFileId);
|
||||
expect(result.tool_resources.file_search.file_ids[0].length).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Operations', () => {
|
||||
it('should handle concurrent updates to prompts with tool_resources', async () => {
|
||||
const concurrentPrompts = await Promise.all([
|
||||
Prompt.create({
|
||||
prompt: 'Concurrent prompt 1',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['shared-file-1', 'unique-file-1'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
Prompt.create({
|
||||
prompt: 'Concurrent prompt 2',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['shared-file-1', 'unique-file-2'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
Prompt.create({
|
||||
prompt: 'Concurrent prompt 3',
|
||||
type: 'text',
|
||||
author: testUsers.owner._id,
|
||||
groupId: testGroup._id,
|
||||
tool_resources: {
|
||||
file_search: {
|
||||
file_ids: ['shared-file-1', 'unique-file-3'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(concurrentPrompts).toHaveLength(3);
|
||||
concurrentPrompts.forEach((prompt, index) => {
|
||||
expect(prompt.tool_resources.file_search.file_ids).toContain('shared-file-1');
|
||||
expect(prompt.tool_resources.file_search.file_ids).toContain(`unique-file-${index + 1}`);
|
||||
});
|
||||
|
||||
const retrievedPrompts = await promptFns.getPrompts({ groupId: testGroup._id });
|
||||
expect(retrievedPrompts.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -702,6 +702,8 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
|
||||
returnFile: true,
|
||||
});
|
||||
filepath = result.filepath;
|
||||
width = result.width;
|
||||
height = result.height;
|
||||
}
|
||||
|
||||
const fileInfo = removeNullishValues({
|
||||
|
||||
520
api/server/services/Files/process.spec.js
Normal file
520
api/server/services/Files/process.spec.js
Normal file
@@ -0,0 +1,520 @@
|
||||
const { EToolResources, FileSources, FileContext } = require('librechat-data-provider');
|
||||
|
||||
jest.mock('~/server/services/Files/strategies', () => {
|
||||
const mockHandleFileUpload = jest.fn();
|
||||
const mockHandleImageUpload = jest.fn();
|
||||
|
||||
return {
|
||||
getStrategyFunctions: jest.fn((source) => ({
|
||||
handleFileUpload: mockHandleFileUpload.mockImplementation(({ file, file_id }) =>
|
||||
Promise.resolve({
|
||||
filepath: `/uploads/${source}/${file_id}`,
|
||||
bytes: file?.size || 20,
|
||||
}),
|
||||
),
|
||||
handleImageUpload: mockHandleImageUpload.mockImplementation(({ file, file_id }) =>
|
||||
Promise.resolve({
|
||||
filepath: `/uploads/${source}/images/${file_id}`,
|
||||
bytes: file.size,
|
||||
width: 800,
|
||||
height: 600,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('~/models/File', () => {
|
||||
const mockCreateFile = jest.fn();
|
||||
return {
|
||||
createFile: mockCreateFile.mockImplementation((fileInfo) =>
|
||||
Promise.resolve({ _id: 'test-file-id', ...fileInfo }),
|
||||
),
|
||||
updateFileUsage: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('~/models/Agent', () => ({
|
||||
addAgentResourceFile: jest.fn().mockResolvedValue(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Config/getEndpointsConfig', () => ({
|
||||
checkCapability: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/utils/getFileStrategy', () => ({
|
||||
getFileStrategy: jest.fn(() => {
|
||||
return 'local';
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Files/VectorDB/crud', () => ({
|
||||
uploadVectors: jest.fn(({ file_id }) =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
vectorIds: [`vector-${file_id}-1`, `vector-${file_id}-2`],
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/controllers/assistants/helpers', () => ({
|
||||
getOpenAIClient: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Tools/credentials', () => ({
|
||||
loadAuthValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
...jest.requireActual('fs'),
|
||||
createReadStream: jest.fn(() => 'mock-stream'),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/utils/queue', () => ({
|
||||
LB_QueueAsyncCall: jest.fn((fn, args, callback) => {
|
||||
if (callback) {
|
||||
callback(null, { success: true });
|
||||
}
|
||||
return Promise.resolve({ success: true });
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Config/app', () => ({
|
||||
getAppConfig: jest.fn().mockResolvedValue({
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Files/images', () => ({
|
||||
processImageFile: jest.fn().mockResolvedValue({
|
||||
filepath: '/test/image/path',
|
||||
width: 800,
|
||||
height: 600,
|
||||
}),
|
||||
handleImageUpload: jest.fn().mockResolvedValue({
|
||||
filepath: '/test/image/uploaded/path',
|
||||
bytes: 1024,
|
||||
width: 800,
|
||||
height: 600,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('File Processing - processAgentFileUpload', () => {
|
||||
let processAgentFileUpload;
|
||||
let mockHandleFileUpload;
|
||||
let mockHandleImageUpload;
|
||||
let mockCreateFile;
|
||||
let mockAddAgentResourceFile;
|
||||
let mockUploadVectors;
|
||||
let mockCheckCapability;
|
||||
let mockGetFileStrategy;
|
||||
|
||||
beforeAll(() => {
|
||||
const processModule = require('./process');
|
||||
processAgentFileUpload = processModule.processAgentFileUpload;
|
||||
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const mockStrategies = getStrategyFunctions();
|
||||
mockHandleFileUpload = mockStrategies.handleFileUpload;
|
||||
mockHandleImageUpload = mockStrategies.handleImageUpload;
|
||||
|
||||
mockCreateFile = require('~/models/File').createFile;
|
||||
mockAddAgentResourceFile = require('~/models/Agent').addAgentResourceFile;
|
||||
mockUploadVectors = require('~/server/services/Files/VectorDB/crud').uploadVectors;
|
||||
mockCheckCapability = require('~/server/services/Config/getEndpointsConfig').checkCapability;
|
||||
mockGetFileStrategy = require('~/server/utils/getFileStrategy').getFileStrategy;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('processAgentFileUpload', () => {
|
||||
it('should process image file upload for agent with proper file handling', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('test image data'),
|
||||
mimetype: 'image/jpeg',
|
||||
size: 1024,
|
||||
originalname: 'test-image.jpg',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
agent_id: 'test-agent-id',
|
||||
tool_resource: EToolResources.image_edit,
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
await processAgentFileUpload({ req: mockReq, res: mockRes, metadata });
|
||||
|
||||
expect(mockGetFileStrategy).toHaveBeenCalledWith(mockReq.config, { isImage: true });
|
||||
|
||||
expect(mockHandleImageUpload).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
req: mockReq,
|
||||
file: mockReq.file,
|
||||
file_id: expect.any(String),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockCreateFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
user: 'test-user-id',
|
||||
file_id: 'test-file-id',
|
||||
bytes: 1024,
|
||||
filename: 'test-image.jpg',
|
||||
context: FileContext.agents,
|
||||
type: 'image/jpeg',
|
||||
source: FileSources.local,
|
||||
width: 800,
|
||||
height: 600,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
expect(mockAddAgentResourceFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agent_id: 'test-agent-id',
|
||||
file_id: 'test-file-id',
|
||||
tool_resource: EToolResources.image_edit,
|
||||
req: mockReq,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Agent file uploaded and processed successfully',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should process file_search tool resource with dual storage (file + vector)', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('test file data'),
|
||||
mimetype: 'application/pdf',
|
||||
size: 2048,
|
||||
originalname: 'test-document.pdf',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
agent_id: 'test-agent-id',
|
||||
tool_resource: EToolResources.file_search,
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
await processAgentFileUpload({ req: mockReq, res: mockRes, metadata });
|
||||
|
||||
expect(mockGetFileStrategy).toHaveBeenCalledWith(mockReq.config, { isImage: false });
|
||||
|
||||
expect(mockHandleFileUpload).toHaveBeenCalledWith({
|
||||
req: mockReq,
|
||||
file: mockReq.file,
|
||||
file_id: 'test-file-id',
|
||||
basePath: 'uploads',
|
||||
entity_id: 'test-agent-id',
|
||||
});
|
||||
|
||||
expect(mockUploadVectors).toHaveBeenCalledWith({
|
||||
req: mockReq,
|
||||
file: mockReq.file,
|
||||
file_id: 'test-file-id',
|
||||
entity_id: 'test-agent-id',
|
||||
});
|
||||
|
||||
expect(mockCreateFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
user: 'test-user-id',
|
||||
file_id: 'test-file-id',
|
||||
filename: 'test-document.pdf',
|
||||
context: FileContext.agents,
|
||||
type: 'application/pdf',
|
||||
source: FileSources.local,
|
||||
bytes: 2048,
|
||||
filepath: '/uploads/local/test-file-id',
|
||||
metadata: {},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
expect(mockAddAgentResourceFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agent_id: 'test-agent-id',
|
||||
file_id: 'test-file-id',
|
||||
tool_resource: EToolResources.file_search,
|
||||
req: mockReq,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing tool_resource parameter', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('test file data'),
|
||||
mimetype: 'application/pdf',
|
||||
size: 2048,
|
||||
originalname: 'test-document.pdf',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
agent_id: 'test-agent-id',
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
await expect(
|
||||
processAgentFileUpload({ req: mockReq, res: mockRes, metadata }),
|
||||
).rejects.toThrow('No tool resource provided for agent file upload');
|
||||
});
|
||||
|
||||
it('should handle missing agent_id parameter', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('test file data'),
|
||||
mimetype: 'application/pdf',
|
||||
size: 2048,
|
||||
originalname: 'test-document.pdf',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
tool_resource: EToolResources.file_search,
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
await expect(
|
||||
processAgentFileUpload({ req: mockReq, res: mockRes, metadata }),
|
||||
).rejects.toThrow('No agent ID provided for agent file upload');
|
||||
});
|
||||
|
||||
it('should handle image uploads for non-image tool resources', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('test image data'),
|
||||
mimetype: 'image/jpeg',
|
||||
size: 1024,
|
||||
originalname: 'test-image.jpg',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
agent_id: 'test-agent-id',
|
||||
tool_resource: EToolResources.file_search,
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
await expect(
|
||||
processAgentFileUpload({ req: mockReq, res: mockRes, metadata }),
|
||||
).rejects.toThrow('Image uploads are not supported for file search tool resources');
|
||||
});
|
||||
|
||||
it('should check execute_code capability and load auth values when processing code files', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('print("hello world")'),
|
||||
mimetype: 'text/x-python',
|
||||
size: 20,
|
||||
originalname: 'test.py',
|
||||
path: '/tmp/test-file.py',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
agent_id: 'test-agent-id',
|
||||
tool_resource: EToolResources.execute_code,
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
const mockLoadAuthValues = require('~/server/services/Tools/credentials').loadAuthValues;
|
||||
mockLoadAuthValues.mockResolvedValue({ CODE_API_KEY: 'test-key' });
|
||||
|
||||
await processAgentFileUpload({ req: mockReq, res: mockRes, metadata });
|
||||
|
||||
expect(mockCheckCapability).toHaveBeenCalledWith(mockReq, 'execute_code');
|
||||
|
||||
expect(mockLoadAuthValues).toHaveBeenCalledWith({
|
||||
userId: 'test-user-id',
|
||||
authFields: ['LIBRECHAT_CODE_API_KEY'],
|
||||
});
|
||||
|
||||
expect(mockHandleFileUpload).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
req: mockReq,
|
||||
stream: 'mock-stream',
|
||||
filename: 'test.py',
|
||||
entity_id: 'test-agent-id',
|
||||
apiKey: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockHandleFileUpload).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
req: mockReq,
|
||||
file: mockReq.file,
|
||||
file_id: 'test-file-id',
|
||||
basePath: 'uploads',
|
||||
entity_id: 'test-agent-id',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockAddAgentResourceFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agent_id: 'test-agent-id',
|
||||
file_id: 'test-file-id',
|
||||
tool_resource: EToolResources.execute_code,
|
||||
req: mockReq,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when example capability (execute_code) is not enabled', async () => {
|
||||
const mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
file: {
|
||||
buffer: Buffer.from('print("hello world")'),
|
||||
mimetype: 'text/x-python',
|
||||
size: 20,
|
||||
originalname: 'test.py',
|
||||
},
|
||||
body: {
|
||||
file_id: 'test-file-id',
|
||||
},
|
||||
config: {
|
||||
fileStrategy: 'local',
|
||||
fileStrategies: {
|
||||
agents: 'local',
|
||||
},
|
||||
imageOutputType: 'jpeg',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
agent_id: 'test-agent-id',
|
||||
tool_resource: EToolResources.execute_code,
|
||||
file_id: 'test-file-id',
|
||||
};
|
||||
|
||||
mockCheckCapability.mockResolvedValueOnce(false);
|
||||
|
||||
await expect(
|
||||
processAgentFileUpload({ req: mockReq, res: mockRes, metadata }),
|
||||
).rejects.toThrow('Code execution is not enabled for Agents');
|
||||
|
||||
expect(mockCheckCapability).toHaveBeenCalledWith(mockReq, 'execute_code');
|
||||
|
||||
expect(mockHandleFileUpload).not.toHaveBeenCalled();
|
||||
expect(mockCreateFile).not.toHaveBeenCalled();
|
||||
expect(mockAddAgentResourceFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -350,6 +350,7 @@ export type TAskProps = {
|
||||
conversationId?: string | null;
|
||||
messageId?: string | null;
|
||||
clientTimestamp?: string;
|
||||
toolResources?: t.AgentToolResources;
|
||||
};
|
||||
|
||||
export type TOptions = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AutoSizer, List } from 'react-virtualized';
|
||||
import { Spinner, useCombobox } from '@librechat/client';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import type { TPromptGroup, AgentToolResources } from 'librechat-data-provider';
|
||||
import type { PromptOption } from '~/common';
|
||||
import { removeCharIfLast, detectVariables } from '~/utils';
|
||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||
@@ -51,7 +51,7 @@ function PromptsCommand({
|
||||
}: {
|
||||
index: number;
|
||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
submitPrompt: (textPrompt: string) => void;
|
||||
submitPrompt: (textPrompt: string, toolResources?: AgentToolResources) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const hasAccess = useHasAccess({
|
||||
@@ -95,7 +95,6 @@ function PromptsCommand({
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasVariables = detectVariables(group.productionPrompt?.prompt ?? '');
|
||||
if (hasVariables) {
|
||||
if (e && e.key === 'Tab') {
|
||||
@@ -105,7 +104,7 @@ function PromptsCommand({
|
||||
setVariableDialogOpen(true);
|
||||
return;
|
||||
} else {
|
||||
submitPrompt(group.productionPrompt?.prompt ?? '');
|
||||
submitPrompt(group.productionPrompt?.prompt ?? '', group.productionPrompt?.tool_resources);
|
||||
}
|
||||
},
|
||||
[setSearchValue, setOpen, setShowPromptsPopover, textAreaRef, promptsMap, submitPrompt],
|
||||
|
||||
166
client/src/components/Prompts/Files/AttachFileButton.tsx
Normal file
166
client/src/components/Prompts/Files/AttachFileButton.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import React, { useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider';
|
||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
||||
import { FileUpload, DropdownPopup, AttachmentIcon, SharePointIcon } from '@librechat/client';
|
||||
import {
|
||||
useLocalize,
|
||||
useAgentCapabilities,
|
||||
useGetAgentsConfig,
|
||||
useSharePointFileHandling,
|
||||
} from '~/hooks';
|
||||
import { SharePointPickerDialog } from '~/components/SharePoint';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import { MenuItemProps } from '~/common';
|
||||
|
||||
interface AttachFileButtonProps {
|
||||
handleFileChange?: (event: React.ChangeEvent<HTMLInputElement>, toolResource?: string) => void;
|
||||
disabled?: boolean | null;
|
||||
}
|
||||
|
||||
const AttachFileButton = ({ handleFileChange, disabled }: AttachFileButtonProps) => {
|
||||
const localize = useLocalize();
|
||||
const isUploadDisabled = disabled ?? false;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
|
||||
const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false);
|
||||
|
||||
const { handleSharePointFiles, isProcessing, downloadProgress } = useSharePointFileHandling({
|
||||
toolResource,
|
||||
});
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled;
|
||||
const { agentsConfig } = useGetAgentsConfig();
|
||||
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||
|
||||
const handleUploadClick = useCallback((isImage?: boolean) => {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
inputRef.current.value = '';
|
||||
inputRef.current.accept = isImage === true ? 'image/*' : '';
|
||||
inputRef.current.click();
|
||||
inputRef.current.accept = '';
|
||||
}, []);
|
||||
|
||||
const dropdownItems = useMemo(() => {
|
||||
const createMenuItems = (onAction: (isImage?: boolean) => void) => {
|
||||
const items: MenuItemProps[] = [
|
||||
{
|
||||
label: localize('com_ui_upload_image_input'),
|
||||
onClick: () => {
|
||||
setToolResource(EToolResources.image_edit);
|
||||
onAction(true);
|
||||
},
|
||||
icon: <ImageUpIcon className="icon-md" />,
|
||||
},
|
||||
];
|
||||
|
||||
if (capabilities.ocrEnabled) {
|
||||
items.push({
|
||||
label: localize('com_ui_upload_ocr_text'),
|
||||
onClick: () => {
|
||||
setToolResource(EToolResources.ocr);
|
||||
onAction();
|
||||
},
|
||||
icon: <FileType2Icon className="icon-md" />,
|
||||
});
|
||||
}
|
||||
|
||||
if (capabilities.fileSearchEnabled) {
|
||||
items.push({
|
||||
label: localize('com_ui_upload_file_search'),
|
||||
onClick: () => {
|
||||
setToolResource(EToolResources.file_search);
|
||||
onAction();
|
||||
},
|
||||
icon: <FileSearch className="icon-md" />,
|
||||
});
|
||||
}
|
||||
|
||||
if (capabilities.codeEnabled) {
|
||||
items.push({
|
||||
label: localize('com_ui_upload_code_files'),
|
||||
onClick: () => {
|
||||
setToolResource(EToolResources.execute_code);
|
||||
onAction();
|
||||
},
|
||||
icon: <TerminalSquareIcon className="icon-md" />,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const localItems = createMenuItems(handleUploadClick);
|
||||
|
||||
if (sharePointEnabled) {
|
||||
const sharePointItems = createMenuItems(() => {
|
||||
setIsSharePointDialogOpen(true);
|
||||
});
|
||||
localItems.push({
|
||||
label: localize('com_files_upload_sharepoint'),
|
||||
onClick: () => {},
|
||||
icon: <SharePointIcon className="icon-md" />,
|
||||
subItems: sharePointItems,
|
||||
});
|
||||
return localItems;
|
||||
}
|
||||
|
||||
return localItems;
|
||||
}, [capabilities, localize, handleUploadClick, sharePointEnabled, setIsSharePointDialogOpen]);
|
||||
|
||||
const menuTrigger = (
|
||||
<Ariakit.MenuButton
|
||||
disabled={isUploadDisabled}
|
||||
id="attach-file-button-menu"
|
||||
aria-label="Attach File Options"
|
||||
className="flex items-center gap-2 rounded-md border border-border-medium bg-surface-primary px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<AttachmentIcon className="h-4 w-4" />
|
||||
{localize('com_ui_attach_files')}
|
||||
</Ariakit.MenuButton>
|
||||
);
|
||||
|
||||
const handleSharePointFilesSelected = async (sharePointFiles: any[]) => {
|
||||
try {
|
||||
await handleSharePointFiles(sharePointFiles);
|
||||
setIsSharePointDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error('SharePoint file processing error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileUpload
|
||||
ref={inputRef}
|
||||
handleFileChange={(e) => {
|
||||
handleFileChange?.(e, toolResource);
|
||||
}}
|
||||
>
|
||||
<DropdownPopup
|
||||
menuId="attach-file-button"
|
||||
className="overflow-visible"
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
modal={true}
|
||||
unmountOnHide={true}
|
||||
trigger={menuTrigger}
|
||||
items={dropdownItems}
|
||||
iconClassName="mr-0"
|
||||
/>
|
||||
</FileUpload>
|
||||
<SharePointPickerDialog
|
||||
isOpen={isSharePointDialogOpen}
|
||||
onOpenChange={setIsSharePointDialogOpen}
|
||||
onFilesSelected={handleSharePointFilesSelected}
|
||||
isDownloading={isProcessing}
|
||||
downloadProgress={downloadProgress}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AttachFileButton);
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@librechat/client';
|
||||
import { PermissionBits } from 'librechat-data-provider';
|
||||
import { PermissionBits, ResourceType } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import { useLocalize, useSubmitMessage, useCustomLink, useResourcePermissions } from '~/hooks';
|
||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||
@@ -34,9 +34,18 @@ function ChatGroupItem({
|
||||
);
|
||||
|
||||
// Check permissions for the promptGroup
|
||||
const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
|
||||
const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || '');
|
||||
const canEdit = hasPermission(PermissionBits.EDIT);
|
||||
|
||||
const hasFiles = useMemo(() => {
|
||||
const toolResources = group.productionPrompt?.tool_resources;
|
||||
if (!toolResources) return false;
|
||||
|
||||
return Object.values(toolResources).some(
|
||||
(resource) => resource?.file_ids && resource.file_ids.length > 0,
|
||||
);
|
||||
}, [group.productionPrompt?.tool_resources]);
|
||||
|
||||
const onCardClick: React.MouseEventHandler<HTMLButtonElement> = () => {
|
||||
const text = group.productionPrompt?.prompt;
|
||||
if (!text?.trim()) {
|
||||
@@ -48,7 +57,7 @@ function ChatGroupItem({
|
||||
return;
|
||||
}
|
||||
|
||||
submitPrompt(text);
|
||||
submitPrompt(text, group.productionPrompt?.tool_resources);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,6 +66,7 @@ function ChatGroupItem({
|
||||
name={group.name}
|
||||
category={group.category ?? ''}
|
||||
onClick={onCardClick}
|
||||
hasFiles={hasFiles}
|
||||
snippet={
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
|
||||
@@ -3,11 +3,12 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Button, TextareaAutosize, Input } from '@librechat/client';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { LocalStorageKeys, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { AgentToolResources } from 'librechat-data-provider';
|
||||
import PromptVariablesAndFiles from '~/components/Prompts/PromptVariablesAndFiles';
|
||||
import CategorySelector from '~/components/Prompts/Groups/CategorySelector';
|
||||
import { useLocalize, useHasAccess, usePromptFileHandling } from '~/hooks';
|
||||
import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
|
||||
import PromptVariables from '~/components/Prompts/PromptVariables';
|
||||
import Description from '~/components/Prompts/Description';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
import Command from '~/components/Prompts/Command';
|
||||
import { useCreatePrompt } from '~/data-provider';
|
||||
import { cn } from '~/utils';
|
||||
@@ -19,6 +20,7 @@ type CreateFormValues = {
|
||||
category: string;
|
||||
oneliner?: string;
|
||||
command?: string;
|
||||
tool_resources?: AgentToolResources;
|
||||
};
|
||||
|
||||
const defaultPrompt: CreateFormValues = {
|
||||
@@ -37,6 +39,14 @@ const CreatePromptForm = ({
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
promptFiles: files,
|
||||
setFiles,
|
||||
handleFileChange,
|
||||
getToolResources,
|
||||
} = usePromptFileHandling();
|
||||
|
||||
const hasAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.CREATE,
|
||||
@@ -88,8 +98,15 @@ const CreatePromptForm = ({
|
||||
if ((command?.length ?? 0) > 0) {
|
||||
groupData.command = command;
|
||||
}
|
||||
|
||||
const promptData = { ...rest };
|
||||
const toolResources = getToolResources();
|
||||
if (toolResources) {
|
||||
promptData.tool_resources = toolResources;
|
||||
}
|
||||
|
||||
createPromptMutation.mutate({
|
||||
prompt: rest,
|
||||
prompt: promptData,
|
||||
group: groupData,
|
||||
});
|
||||
};
|
||||
@@ -161,7 +178,13 @@ const CreatePromptForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PromptVariables promptText={promptText} />
|
||||
<PromptVariablesAndFiles
|
||||
promptText={promptText}
|
||||
files={files}
|
||||
onFilesChange={setFiles}
|
||||
handleFileChange={handleFileChange}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Description
|
||||
onValueChange={(value) => methods.setValue('oneliner', value)}
|
||||
tabIndex={0}
|
||||
|
||||
@@ -93,7 +93,11 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2 truncate pr-2">
|
||||
<CategoryIcon category={group.category ?? ''} className="icon-lg" aria-hidden="true" />
|
||||
<CategoryIcon
|
||||
category={group.category ?? ''}
|
||||
className="icon-lg flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<Label className="text-md cursor-pointer truncate font-semibold text-text-primary">
|
||||
{group.name}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Label } from '@librechat/client';
|
||||
import { Paperclip } from 'lucide-react';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
|
||||
export default function ListCard({
|
||||
@@ -8,12 +9,14 @@ export default function ListCard({
|
||||
snippet,
|
||||
onClick,
|
||||
children,
|
||||
hasFiles,
|
||||
}: {
|
||||
category: string;
|
||||
name: string;
|
||||
snippet: string;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
|
||||
children?: React.ReactNode;
|
||||
hasFiles?: boolean;
|
||||
}) {
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement | HTMLButtonElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
@@ -35,7 +38,7 @@ export default function ListCard({
|
||||
>
|
||||
<div className="flex w-full justify-between gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<CategoryIcon category={category} className="icon-md" aria-hidden="true" />
|
||||
<CategoryIcon category={category} className="icon-md flex-shrink-0" aria-hidden="true" />
|
||||
<Label
|
||||
id={`card-title-${name}`}
|
||||
className="break-word select-none text-balance text-sm font-semibold text-text-primary"
|
||||
@@ -43,6 +46,7 @@ export default function ListCard({
|
||||
>
|
||||
{name}
|
||||
</Label>
|
||||
{hasFiles && <Paperclip className="icon-xs mt-1 flex-shrink-0 text-text-secondary" />}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function VariableForm({
|
||||
text = text.replace(regex, value);
|
||||
});
|
||||
|
||||
submitPrompt(text);
|
||||
submitPrompt(text, group.productionPrompt?.tool_resources);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import supersub from 'remark-supersub';
|
||||
import { Label } from '@librechat/client';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import type { TPromptGroup, AgentToolResources } from 'librechat-data-provider';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import PromptFilesPreview from './PromptFilesPreview';
|
||||
import CategoryIcon from './Groups/CategoryIcon';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
@@ -25,6 +26,17 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
return replaceSpecialVars({ text: initialText, user });
|
||||
}, [group?.productionPrompt?.prompt, user]);
|
||||
|
||||
const toolResources = useMemo((): AgentToolResources | undefined => {
|
||||
return group?.productionPrompt?.tool_resources;
|
||||
}, [group?.productionPrompt?.tool_resources]);
|
||||
|
||||
const hasFiles = useMemo(() => {
|
||||
if (!toolResources) return false;
|
||||
return Object.values(toolResources).some(
|
||||
(resource) => resource?.file_ids && resource.file_ids.length > 0,
|
||||
);
|
||||
}, [toolResources]);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
@@ -72,6 +84,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
</div>
|
||||
</div>
|
||||
<PromptVariables promptText={mainText} showInfo={false} />
|
||||
{hasFiles && toolResources && <PromptFilesPreview toolResources={toolResources} />}
|
||||
<Description initialValue={group.oneliner} disabled={true} />
|
||||
<Command initialValue={group.command} disabled={true} />
|
||||
</div>
|
||||
|
||||
141
client/src/components/Prompts/PromptFile.tsx
Normal file
141
client/src/components/Prompts/PromptFile.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
||||
import Image from '~/components/Chat/Input/Files/Image';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function PromptFile({
|
||||
files: _files,
|
||||
setFiles,
|
||||
abortUpload,
|
||||
setFilesLoading,
|
||||
fileFilter,
|
||||
isRTL = false,
|
||||
Wrapper,
|
||||
}: {
|
||||
files: Map<string, ExtendedFile> | undefined;
|
||||
abortUpload?: () => void;
|
||||
setFiles: React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
|
||||
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
fileFilter?: (file: ExtendedFile) => boolean;
|
||||
isRTL?: boolean;
|
||||
Wrapper?: React.FC<{ children: React.ReactNode }>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const files = Array.from(_files?.values() ?? []).filter((file) =>
|
||||
fileFilter ? fileFilter(file) : true,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 0) {
|
||||
setFilesLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.some((file) => file.progress < 1)) {
|
||||
setFilesLoading(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.every((file) => file.progress === 1)) {
|
||||
setFilesLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files]);
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderFiles = () => {
|
||||
const rowStyle = isRTL
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
}
|
||||
: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={rowStyle as React.CSSProperties}>
|
||||
{files
|
||||
.reduce(
|
||||
(acc, current) => {
|
||||
if (!acc.map.has(current.file_id)) {
|
||||
acc.map.set(current.file_id, true);
|
||||
acc.uniqueFiles.push(current);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ map: new Map(), uniqueFiles: [] as ExtendedFile[] },
|
||||
)
|
||||
.uniqueFiles.map((file: ExtendedFile, index: number) => {
|
||||
const handleDelete = () => {
|
||||
showToast({
|
||||
message: localize('com_ui_deleting_file'),
|
||||
status: 'info',
|
||||
});
|
||||
|
||||
if (abortUpload && file.progress < 1) {
|
||||
abortUpload();
|
||||
}
|
||||
|
||||
if (file.preview && file.preview.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
|
||||
setFiles((currentFiles) => {
|
||||
const updatedFiles = new Map(currentFiles);
|
||||
updatedFiles.delete(file.file_id);
|
||||
if (file.temp_file_id) {
|
||||
updatedFiles.delete(file.temp_file_id);
|
||||
}
|
||||
return updatedFiles;
|
||||
});
|
||||
};
|
||||
|
||||
const isImage = file.type?.startsWith('image') ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
flexBasis: '70px',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isImage ? (
|
||||
<Image
|
||||
url={file.preview ?? file.filepath}
|
||||
onDelete={handleDelete}
|
||||
progress={file.progress}
|
||||
source={file.source}
|
||||
/>
|
||||
) : (
|
||||
<FileContainer file={file} onDelete={handleDelete} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (Wrapper) {
|
||||
return <Wrapper>{renderFiles()}</Wrapper>;
|
||||
}
|
||||
|
||||
return renderFiles();
|
||||
}
|
||||
80
client/src/components/Prompts/PromptFiles.tsx
Normal file
80
client/src/components/Prompts/PromptFiles.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Separator } from '@librechat/client';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import AttachFileButton from '~/components/Prompts/Files/AttachFileButton';
|
||||
import PromptFile from '~/components/Prompts/PromptFile';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const PromptFiles = ({
|
||||
files,
|
||||
onFilesChange,
|
||||
handleFileChange,
|
||||
disabled,
|
||||
}: {
|
||||
files: ExtendedFile[];
|
||||
onFilesChange?: (files: ExtendedFile[]) => void;
|
||||
handleFileChange?: (event: React.ChangeEvent<HTMLInputElement>, toolResource?: string) => void;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
const filesMap = useMemo(() => {
|
||||
const map = new Map<string, ExtendedFile>();
|
||||
files.forEach((file) => {
|
||||
const key = file.file_id || file.temp_file_id || '';
|
||||
if (key) {
|
||||
map.set(key, file);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-xl border border-border-light bg-transparent p-4 shadow-md">
|
||||
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||
<FileText className="icon-sm" aria-hidden="true" />
|
||||
{localize('com_ui_files')}
|
||||
</h3>
|
||||
<div className="flex flex-1 flex-col space-y-4">
|
||||
<div className="flex-1">
|
||||
{!files.length && (
|
||||
<>
|
||||
<div className="text-sm text-text-secondary">
|
||||
<ReactMarkdown className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_files_info')}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
<Separator className="my-3 text-text-primary" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<PromptFile
|
||||
files={filesMap}
|
||||
setFiles={(newMapOrUpdater) => {
|
||||
const newMap =
|
||||
typeof newMapOrUpdater === 'function'
|
||||
? newMapOrUpdater(filesMap)
|
||||
: newMapOrUpdater;
|
||||
const newFiles = Array.from(newMap.values()) as ExtendedFile[];
|
||||
onFilesChange?.(newFiles);
|
||||
}}
|
||||
setFilesLoading={() => {}}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start text-text-secondary">
|
||||
<AttachFileButton handleFileChange={handleFileChange} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFiles;
|
||||
121
client/src/components/Prompts/PromptFilesPreview.tsx
Normal file
121
client/src/components/Prompts/PromptFilesPreview.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Paperclip, FileText, Image, FileType } from 'lucide-react';
|
||||
import type { AgentToolResources } from 'librechat-data-provider';
|
||||
import { useGetFiles } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface PromptFilesPreviewProps {
|
||||
toolResources: AgentToolResources;
|
||||
}
|
||||
|
||||
const PromptFilesPreview: React.FC<PromptFilesPreviewProps> = ({ toolResources }) => {
|
||||
const localize = useLocalize();
|
||||
const { data: allFiles } = useGetFiles();
|
||||
|
||||
const fileMap = useMemo(() => {
|
||||
const map: Record<string, any> = {};
|
||||
if (Array.isArray(allFiles)) {
|
||||
allFiles.forEach((file) => {
|
||||
if (file.file_id) {
|
||||
map[file.file_id] = file;
|
||||
}
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [allFiles]);
|
||||
|
||||
const attachedFiles = useMemo(() => {
|
||||
const files: Array<{ file: any; toolResource: string }> = [];
|
||||
|
||||
Object.entries(toolResources).forEach(([toolResource, resource]) => {
|
||||
if (resource?.file_ids) {
|
||||
resource.file_ids.forEach((fileId) => {
|
||||
const dbFile = fileMap[fileId];
|
||||
if (dbFile) {
|
||||
files.push({ file: dbFile, toolResource });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
}, [toolResources, fileMap]);
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
if (type?.startsWith('image/')) {
|
||||
return <Image className="h-4 w-4" />;
|
||||
}
|
||||
if (type?.includes('text') || type?.includes('document')) {
|
||||
return <FileText className="h-4 w-4" />;
|
||||
}
|
||||
return <FileType className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
const getToolResourceLabel = (toolResource: string) => {
|
||||
if (toolResource === 'file_search') {
|
||||
return localize('com_ui_upload_file_search');
|
||||
}
|
||||
if (toolResource === 'execute_code') {
|
||||
return localize('com_ui_upload_code_files');
|
||||
}
|
||||
if (toolResource === 'ocr') {
|
||||
return localize('com_ui_upload_ocr_text');
|
||||
}
|
||||
if (toolResource === 'image_edit') {
|
||||
return localize('com_ui_upload_image_input');
|
||||
}
|
||||
return toolResource;
|
||||
};
|
||||
|
||||
if (attachedFiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
{localize('com_ui_files')} ({attachedFiles.length})
|
||||
</div>
|
||||
</h2>
|
||||
<div className="rounded-b-lg border border-border-light p-4">
|
||||
<div className="space-y-3">
|
||||
{attachedFiles.map(({ file, toolResource }, index) => (
|
||||
<div
|
||||
key={`${file.file_id}-${index}`}
|
||||
className="flex items-center justify-between rounded-lg border border-border-light p-3 transition-colors hover:bg-surface-tertiary"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-surface-secondary text-text-secondary">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-text-primary" title={file.filename}>
|
||||
{file.filename}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-xs text-text-secondary">
|
||||
<span>{getToolResourceLabel(toolResource)}</span>
|
||||
{file.bytes && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{(file.bytes / 1024).toFixed(1)} KB</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{file.type?.startsWith('image/') && file.width && file.height && (
|
||||
<div className="text-xs text-text-secondary">
|
||||
{file.width} × {file.height}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFilesPreview;
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
PermissionBits,
|
||||
PermissionTypes,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider';
|
||||
import type {
|
||||
TCreatePrompt,
|
||||
TPrompt,
|
||||
TPromptGroup,
|
||||
AgentToolResources,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import {
|
||||
useGetPrompts,
|
||||
useGetPromptGroup,
|
||||
@@ -20,11 +26,11 @@ import {
|
||||
useUpdatePromptGroup,
|
||||
useMakePromptProduction,
|
||||
} from '~/data-provider';
|
||||
import { useResourcePermissions, useHasAccess, useLocalize } from '~/hooks';
|
||||
import { useResourcePermissions, useHasAccess, useLocalize, usePromptFileHandling } from '~/hooks';
|
||||
import PromptVariablesAndFiles from './PromptVariablesAndFiles';
|
||||
import CategorySelector from './Groups/CategorySelector';
|
||||
import { usePromptGroupsContext } from '~/Providers';
|
||||
import NoPromptGroup from './Groups/NoPromptGroup';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import { cn, findPromptGroup } from '~/utils';
|
||||
import PromptVersions from './PromptVersions';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
@@ -119,7 +125,12 @@ const RightPanel = React.memo(
|
||||
makeProductionMutation.mutate({
|
||||
id: promptVersionId,
|
||||
groupId,
|
||||
productionPrompt: { prompt },
|
||||
productionPrompt: {
|
||||
prompt,
|
||||
...(selectedPrompt.tool_resources && {
|
||||
tool_resources: selectedPrompt.tool_resources,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
@@ -179,6 +190,21 @@ const PromptForm = () => {
|
||||
const [showSidePanel, setShowSidePanel] = useState(false);
|
||||
const sidePanelWidth = '320px';
|
||||
|
||||
const {
|
||||
loadFromToolResources,
|
||||
getToolResources,
|
||||
promptFiles: hookPromptFiles,
|
||||
handleFileChange,
|
||||
setFiles,
|
||||
} = usePromptFileHandling({
|
||||
onFileChange: (updatedFiles) => {
|
||||
if (canEdit && selectedPrompt) {
|
||||
const currentPromptText = getValues('prompt');
|
||||
onSave(currentPromptText, updatedFiles);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId);
|
||||
const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts(
|
||||
{ groupId: promptId },
|
||||
@@ -200,7 +226,7 @@ const PromptForm = () => {
|
||||
category: group ? group.category : '',
|
||||
},
|
||||
});
|
||||
const { handleSubmit, setValue, reset, watch } = methods;
|
||||
const { handleSubmit, setValue, reset, watch, getValues } = methods;
|
||||
const promptText = watch('prompt');
|
||||
|
||||
const selectedPrompt = useMemo(
|
||||
@@ -237,7 +263,10 @@ const PromptForm = () => {
|
||||
makeProductionMutation.mutate({
|
||||
id: data.prompt._id,
|
||||
groupId: data.prompt.groupId,
|
||||
productionPrompt: { prompt: data.prompt.prompt },
|
||||
productionPrompt: {
|
||||
prompt: data.prompt.prompt,
|
||||
...(data.prompt.tool_resources && { tool_resources: data.prompt.tool_resources }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -249,8 +278,30 @@ const PromptForm = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const getToolResourcesFromFiles = useCallback((files: ExtendedFile[]) => {
|
||||
if (files.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const toolResources: AgentToolResources = {};
|
||||
|
||||
files.forEach((file) => {
|
||||
if (!file.file_id || !file.tool_resource) return; // Skip files that haven't been uploaded yet
|
||||
|
||||
if (!toolResources[file.tool_resource]) {
|
||||
toolResources[file.tool_resource] = { file_ids: [] };
|
||||
}
|
||||
|
||||
if (!toolResources[file.tool_resource]!.file_ids!.includes(file.file_id)) {
|
||||
toolResources[file.tool_resource]!.file_ids!.push(file.file_id);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(toolResources).length > 0 ? toolResources : undefined;
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(
|
||||
(value: string) => {
|
||||
(value: string, updatedFiles?: ExtendedFile[]) => {
|
||||
if (!canEdit) {
|
||||
return;
|
||||
}
|
||||
@@ -268,22 +319,36 @@ const PromptForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolResources = updatedFiles
|
||||
? getToolResourcesFromFiles(updatedFiles)
|
||||
: getToolResources();
|
||||
const tempPrompt: TCreatePrompt = {
|
||||
prompt: {
|
||||
type: selectedPrompt.type ?? 'text',
|
||||
groupId: groupId,
|
||||
prompt: value,
|
||||
...(toolResources && { tool_resources: toolResources }),
|
||||
},
|
||||
};
|
||||
|
||||
if (value === selectedPrompt.prompt) {
|
||||
const promptTextChanged = value !== selectedPrompt.prompt;
|
||||
const toolResourcesChanged =
|
||||
JSON.stringify(toolResources) !== JSON.stringify(selectedPrompt.tool_resources);
|
||||
|
||||
if (!promptTextChanged && !toolResourcesChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're adding to an existing group, so use the addPromptToGroup mutation
|
||||
addPromptToGroupMutation.mutate({ ...tempPrompt, groupId });
|
||||
},
|
||||
[selectedPrompt, group, addPromptToGroupMutation, canEdit],
|
||||
[
|
||||
selectedPrompt,
|
||||
group,
|
||||
addPromptToGroupMutation,
|
||||
canEdit,
|
||||
getToolResources,
|
||||
getToolResourcesFromFiles,
|
||||
],
|
||||
);
|
||||
|
||||
const handleLoadingComplete = useCallback(() => {
|
||||
@@ -307,7 +372,13 @@ const PromptForm = () => {
|
||||
useEffect(() => {
|
||||
setValue('prompt', selectedPrompt ? selectedPrompt.prompt : '', { shouldDirty: false });
|
||||
setValue('category', group ? group.category : '', { shouldDirty: false });
|
||||
}, [selectedPrompt, group, setValue]);
|
||||
|
||||
if (selectedPrompt?.tool_resources) {
|
||||
loadFromToolResources(selectedPrompt.tool_resources);
|
||||
} else {
|
||||
loadFromToolResources(undefined);
|
||||
}
|
||||
}, [selectedPrompt, group, setValue, loadFromToolResources]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -447,7 +518,19 @@ const PromptForm = () => {
|
||||
isEditing={isEditing}
|
||||
setIsEditing={(value) => canEdit && setIsEditing(value)}
|
||||
/>
|
||||
<PromptVariables promptText={promptText} />
|
||||
<PromptVariablesAndFiles
|
||||
promptText={promptText}
|
||||
files={hookPromptFiles}
|
||||
onFilesChange={(files) => {
|
||||
setFiles(files);
|
||||
if (canEdit && selectedPrompt) {
|
||||
const currentPromptText = getValues('prompt');
|
||||
onSave(currentPromptText, files);
|
||||
}
|
||||
}}
|
||||
handleFileChange={handleFileChange}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
<Description
|
||||
initialValue={group.oneliner ?? ''}
|
||||
onValueChange={canEdit ? handleUpdateOneliner : undefined}
|
||||
|
||||
43
client/src/components/Prompts/PromptVariablesAndFiles.tsx
Normal file
43
client/src/components/Prompts/PromptVariablesAndFiles.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import PromptFiles from './PromptFiles';
|
||||
|
||||
interface PromptVariablesAndFilesProps {
|
||||
promptText: string;
|
||||
files?: ExtendedFile[];
|
||||
onFilesChange?: (files: ExtendedFile[]) => void;
|
||||
handleFileChange?: (event: React.ChangeEvent<HTMLInputElement>, toolResource?: string) => void;
|
||||
disabled?: boolean;
|
||||
showVariablesInfo?: boolean;
|
||||
}
|
||||
|
||||
const PromptVariablesAndFiles: React.FC<PromptVariablesAndFilesProps> = ({
|
||||
promptText,
|
||||
files = [],
|
||||
onFilesChange,
|
||||
handleFileChange,
|
||||
disabled,
|
||||
showVariablesInfo = true,
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-stretch">
|
||||
{/* Variables Section */}
|
||||
<div className="w-full">
|
||||
<PromptVariables promptText={promptText} showInfo={showVariablesInfo} />
|
||||
</div>
|
||||
|
||||
{/* Files Section */}
|
||||
<div className="w-full">
|
||||
<PromptFiles
|
||||
files={files}
|
||||
onFilesChange={onFilesChange}
|
||||
handleFileChange={handleFileChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptVariablesAndFiles;
|
||||
@@ -8,3 +8,5 @@ export { default as DashGroupItem } from './Groups/DashGroupItem';
|
||||
export { default as EmptyPromptPreview } from './EmptyPromptPreview';
|
||||
export { default as PromptSidePanel } from './Groups/GroupSidePanel';
|
||||
export { default as CreatePromptForm } from './Groups/CreatePromptForm';
|
||||
export { default as PromptVariablesAndFiles } from './PromptVariablesAndFiles';
|
||||
export { default as PromptFiles } from './PromptFiles';
|
||||
|
||||
@@ -118,6 +118,8 @@ export const useCreatePrompt = (
|
||||
},
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries([QueryKeys.files]);
|
||||
|
||||
if (group) {
|
||||
queryClient.setQueryData<t.PromptGroupListData>(
|
||||
[QueryKeys.promptGroups, name, category, pageSize],
|
||||
@@ -163,6 +165,8 @@ export const useAddPromptToGroup = (
|
||||
},
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries([QueryKeys.files]);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(response, variables, context);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export default function useChatFunctions({
|
||||
parentMessageId = null,
|
||||
conversationId = null,
|
||||
messageId = null,
|
||||
toolResources,
|
||||
},
|
||||
{
|
||||
editedContent = null,
|
||||
@@ -204,6 +205,7 @@ export default function useChatFunctions({
|
||||
messageId: isContinued && messageId != null && messageId ? messageId : intermediateId,
|
||||
thread_id,
|
||||
error: false,
|
||||
...(toolResources && { tool_resources: toolResources }),
|
||||
};
|
||||
|
||||
const submissionFiles = overrideFiles ?? targetParentMessage?.files;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Constants, replaceSpecialVars } from 'librechat-data-provider';
|
||||
import type { AgentToolResources, TFile } from 'librechat-data-provider';
|
||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||
import useUpdateFiles from '~/hooks/Files/useUpdateFiles';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useGetFiles } from '~/data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
const appendIndex = (index: number, value?: string) => {
|
||||
@@ -16,15 +20,67 @@ const appendIndex = (index: number, value?: string) => {
|
||||
export default function useSubmitMessage() {
|
||||
const { user } = useAuthContext();
|
||||
const methods = useChatFormContext();
|
||||
const { ask, index, getMessages, setMessages, latestMessage } = useChatContext();
|
||||
const { ask, index, getMessages, setMessages, latestMessage, setFiles } = useChatContext();
|
||||
const { addedIndex, ask: askAdditional, conversation: addedConvo } = useAddedChatContext();
|
||||
const { data: allFiles = [] } = useGetFiles();
|
||||
const { addFile } = useUpdateFiles(setFiles);
|
||||
|
||||
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
||||
|
||||
const fileMap = useMemo(() => {
|
||||
const map: Record<string, TFile> = {};
|
||||
if (Array.isArray(allFiles)) {
|
||||
allFiles.forEach((file) => {
|
||||
if (file.file_id) {
|
||||
map[file.file_id] = file;
|
||||
}
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [allFiles]);
|
||||
|
||||
const convertToolResourcesToFiles = useCallback(
|
||||
(toolResources: AgentToolResources): ExtendedFile[] => {
|
||||
const promptFiles: ExtendedFile[] = [];
|
||||
|
||||
Object.entries(toolResources).forEach(([toolResource, resource]) => {
|
||||
if (resource?.file_ids) {
|
||||
resource.file_ids.forEach((fileId) => {
|
||||
const dbFile = fileMap[fileId];
|
||||
if (dbFile) {
|
||||
const extendedFile = {
|
||||
file_id: dbFile.file_id,
|
||||
temp_file_id: dbFile.file_id,
|
||||
filename: dbFile.filename,
|
||||
filepath: dbFile.filepath,
|
||||
type: dbFile.type,
|
||||
size: dbFile.bytes,
|
||||
width: dbFile.width,
|
||||
height: dbFile.height,
|
||||
progress: 1, // Already uploaded
|
||||
attached: true,
|
||||
tool_resource: toolResource,
|
||||
preview: dbFile.type?.startsWith('image/') ? dbFile.filepath : undefined,
|
||||
};
|
||||
promptFiles.push(extendedFile);
|
||||
} else {
|
||||
console.warn(`File not found in fileMap: ${fileId}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(`No file_ids in resource "${toolResource}"`);
|
||||
}
|
||||
});
|
||||
|
||||
return promptFiles;
|
||||
},
|
||||
[fileMap],
|
||||
);
|
||||
|
||||
const submitMessage = useCallback(
|
||||
(data?: { text: string }) => {
|
||||
(data?: { text: string; toolResources?: AgentToolResources; files?: ExtendedFile[] }) => {
|
||||
if (!data) {
|
||||
return console.warn('No data provided to submitMessage');
|
||||
}
|
||||
@@ -46,12 +102,18 @@ export default function useSubmitMessage() {
|
||||
const rootIndex = addedIndex - 1;
|
||||
const clientTimestamp = new Date().toISOString();
|
||||
|
||||
ask({
|
||||
text: data.text,
|
||||
overrideConvoId: appendIndex(rootIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(rootIndex, overrideUserMessageId),
|
||||
clientTimestamp,
|
||||
});
|
||||
ask(
|
||||
{
|
||||
text: data.text,
|
||||
overrideConvoId: appendIndex(rootIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(rootIndex, overrideUserMessageId),
|
||||
clientTimestamp,
|
||||
toolResources: data.toolResources,
|
||||
},
|
||||
{
|
||||
overrideFiles: data.files,
|
||||
},
|
||||
);
|
||||
|
||||
if (hasAdded) {
|
||||
askAdditional(
|
||||
@@ -60,8 +122,12 @@ export default function useSubmitMessage() {
|
||||
overrideConvoId: appendIndex(addedIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(addedIndex, overrideUserMessageId),
|
||||
clientTimestamp,
|
||||
toolResources: data.toolResources,
|
||||
},
|
||||
{
|
||||
overrideMessages: rootMessages,
|
||||
overrideFiles: data.files,
|
||||
},
|
||||
{ overrideMessages: rootMessages },
|
||||
);
|
||||
}
|
||||
methods.reset();
|
||||
@@ -80,18 +146,36 @@ export default function useSubmitMessage() {
|
||||
);
|
||||
|
||||
const submitPrompt = useCallback(
|
||||
(text: string) => {
|
||||
(text: string, toolResources?: AgentToolResources) => {
|
||||
const parsedText = replaceSpecialVars({ text, user });
|
||||
|
||||
if (autoSendPrompts) {
|
||||
submitMessage({ text: parsedText });
|
||||
const promptFiles = toolResources ? convertToolResourcesToFiles(toolResources) : [];
|
||||
submitMessage({ text: parsedText, toolResources, files: promptFiles });
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolResources) {
|
||||
const promptFiles = convertToolResourcesToFiles(toolResources);
|
||||
|
||||
promptFiles.forEach((file, _index) => {
|
||||
addFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
const currentText = methods.getValues('text');
|
||||
const newText = currentText.trim().length > 1 ? `\n${parsedText}` : parsedText;
|
||||
setActivePrompt(newText);
|
||||
},
|
||||
[autoSendPrompts, submitMessage, setActivePrompt, methods, user],
|
||||
[
|
||||
autoSendPrompts,
|
||||
submitMessage,
|
||||
setActivePrompt,
|
||||
methods,
|
||||
user,
|
||||
addFile,
|
||||
convertToolResourcesToFiles,
|
||||
],
|
||||
);
|
||||
|
||||
return { submitMessage, submitPrompt };
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as useCategories } from './useCategories';
|
||||
export { default as usePromptGroupsNav } from './usePromptGroupsNav';
|
||||
export { default as usePromptFileHandling } from './usePromptFileHandling';
|
||||
|
||||
398
client/src/hooks/Prompts/usePromptFileHandling.ts
Normal file
398
client/src/hooks/Prompts/usePromptFileHandling.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
import { EModelEndpoint, EToolResources, FileSources } from 'librechat-data-provider';
|
||||
import type { AgentToolResources, TFile } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { useUploadFileMutation, useGetFiles } from '~/data-provider';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
interface UsePromptFileHandling {
|
||||
fileSetter?: (files: ExtendedFile[]) => void;
|
||||
initialFiles?: ExtendedFile[];
|
||||
onFileChange?: (updatedFiles: ExtendedFile[]) => void;
|
||||
}
|
||||
|
||||
export const usePromptFileHandling = (params?: UsePromptFileHandling) => {
|
||||
const { showToast } = useToastContext();
|
||||
const { data: allFiles = [] } = useGetFiles();
|
||||
|
||||
const fileMap = useMemo(() => {
|
||||
const map: Record<string, TFile> = {};
|
||||
if (Array.isArray(allFiles)) {
|
||||
allFiles.forEach((file) => {
|
||||
if (file.file_id) {
|
||||
map[file.file_id] = file;
|
||||
}
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [allFiles]);
|
||||
const [files, setFiles] = useState<ExtendedFile[]>(() => {
|
||||
return params?.initialFiles || [];
|
||||
});
|
||||
const [, setFilesLoading] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const uploadFile = useUploadFileMutation({
|
||||
onSuccess: (data) => {
|
||||
logger.log('File uploaded successfully', data);
|
||||
|
||||
setFiles((prev) => {
|
||||
return prev.map((file) => {
|
||||
if (file.temp_file_id === data.temp_file_id) {
|
||||
return {
|
||||
...file,
|
||||
file_id: data.file_id,
|
||||
filepath: data.filepath,
|
||||
progress: 1,
|
||||
attached: true,
|
||||
preview: data.filepath || file.preview,
|
||||
filename: data.filename || file.filename,
|
||||
type: data.type || file.type,
|
||||
size: data.bytes || file.size,
|
||||
width: data.width || file.width,
|
||||
height: data.height || file.height,
|
||||
source: data.source || file.source,
|
||||
};
|
||||
}
|
||||
return file;
|
||||
});
|
||||
});
|
||||
|
||||
setFilesLoading(false);
|
||||
showToast({
|
||||
message: 'File uploaded successfully',
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
const updatedFiles = files.map((file) => {
|
||||
if (file.temp_file_id === data.temp_file_id) {
|
||||
return {
|
||||
...file,
|
||||
file_id: data.file_id,
|
||||
filepath: data.filepath,
|
||||
progress: 1,
|
||||
attached: true,
|
||||
preview: data.filepath || file.preview,
|
||||
filename: data.filename || file.filename,
|
||||
type: data.type || file.type,
|
||||
size: data.bytes || file.size,
|
||||
width: data.width || file.width,
|
||||
height: data.height || file.height,
|
||||
source: data.source || file.source,
|
||||
};
|
||||
}
|
||||
return file;
|
||||
});
|
||||
params?.onFileChange?.(updatedFiles);
|
||||
},
|
||||
onError: (error, body) => {
|
||||
logger.error('File upload error:', error);
|
||||
setFilesLoading(false);
|
||||
|
||||
const file_id = body.get('file_id');
|
||||
if (file_id) {
|
||||
setFiles((prev) => {
|
||||
return prev.filter((file) => {
|
||||
if (file.file_id === file_id || file.temp_file_id === file_id) {
|
||||
if (file.preview && file.preview.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let errorMessage = 'Failed to upload file';
|
||||
if ((error as any)?.response?.data?.message) {
|
||||
errorMessage = (error as any).response.data.message;
|
||||
} else if ((error as any)?.message) {
|
||||
errorMessage = (error as any).message;
|
||||
}
|
||||
|
||||
showToast({
|
||||
message: errorMessage,
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const promptFiles = files;
|
||||
|
||||
useEffect(() => {
|
||||
if (params?.fileSetter) {
|
||||
params.fileSetter(files);
|
||||
}
|
||||
}, [files, params]);
|
||||
|
||||
const loadImage = useCallback(
|
||||
(extendedFile: ExtendedFile, preview: string) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
extendedFile.width = img.width;
|
||||
extendedFile.height = img.height;
|
||||
extendedFile.progress = 0.6;
|
||||
|
||||
const updatedFile = {
|
||||
...extendedFile,
|
||||
};
|
||||
|
||||
setFiles((prev) =>
|
||||
prev.map((file) => (file.file_id === extendedFile.file_id ? updatedFile : file)),
|
||||
);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('endpoint', EModelEndpoint.agents);
|
||||
formData.append(
|
||||
'file',
|
||||
extendedFile.file!,
|
||||
encodeURIComponent(extendedFile.filename || ''),
|
||||
);
|
||||
formData.append('file_id', extendedFile.file_id);
|
||||
formData.append('message_file', 'true');
|
||||
|
||||
formData.append('width', img.width.toString());
|
||||
formData.append('height', img.height.toString());
|
||||
|
||||
if (extendedFile.tool_resource) {
|
||||
formData.append('tool_resource', extendedFile.tool_resource.toString());
|
||||
}
|
||||
|
||||
uploadFile.mutate(formData);
|
||||
};
|
||||
img.src = preview;
|
||||
},
|
||||
[uploadFile],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>, toolResource?: EToolResources | string) => {
|
||||
event.stopPropagation();
|
||||
if (!event.target.files) return;
|
||||
|
||||
const fileList = Array.from(event.target.files);
|
||||
setFilesLoading(true);
|
||||
|
||||
fileList.forEach(async (file) => {
|
||||
const file_id = v4();
|
||||
const temp_file_id = file_id;
|
||||
|
||||
const extendedFile: ExtendedFile = {
|
||||
file_id,
|
||||
temp_file_id,
|
||||
type: file.type,
|
||||
filename: file.name,
|
||||
filepath: '',
|
||||
progress: 0,
|
||||
preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : '',
|
||||
size: file.size,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
attached: false,
|
||||
file,
|
||||
tool_resource: typeof toolResource === 'string' ? toolResource : undefined,
|
||||
};
|
||||
|
||||
setFiles((prev) => [...prev, extendedFile]);
|
||||
|
||||
if (file.type.startsWith('image/') && extendedFile.preview) {
|
||||
loadImage(extendedFile, extendedFile.preview);
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
formData.append('endpoint', EModelEndpoint.agents);
|
||||
formData.append('file', file, encodeURIComponent(file.name));
|
||||
formData.append('file_id', file_id);
|
||||
formData.append('message_file', 'true');
|
||||
|
||||
if (toolResource) {
|
||||
formData.append('tool_resource', toolResource.toString());
|
||||
}
|
||||
|
||||
uploadFile.mutate(formData);
|
||||
}
|
||||
});
|
||||
|
||||
event.target.value = '';
|
||||
},
|
||||
[uploadFile, loadImage],
|
||||
);
|
||||
|
||||
const handleFileRemove = useCallback(
|
||||
(fileId: string) => {
|
||||
setFiles((prev) => {
|
||||
return prev.filter((file) => {
|
||||
if (file.file_id === fileId || file.temp_file_id === fileId) {
|
||||
if (file.preview && file.preview.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const updatedFiles = files.filter((file) => {
|
||||
if (file.file_id === fileId || file.temp_file_id === fileId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
params?.onFileChange?.(updatedFiles);
|
||||
},
|
||||
[files, params],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (params?.fileSetter) {
|
||||
params.fileSetter(promptFiles);
|
||||
}
|
||||
}, [promptFiles, params]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
files.forEach((file) => {
|
||||
if (file.preview && file.preview.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [files]);
|
||||
|
||||
const getToolResources = useCallback((): AgentToolResources | undefined => {
|
||||
if (promptFiles.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const toolResources: AgentToolResources = {};
|
||||
|
||||
promptFiles.forEach((file) => {
|
||||
if (!file.file_id || !file.tool_resource) return;
|
||||
|
||||
if (!toolResources[file.tool_resource]) {
|
||||
toolResources[file.tool_resource] = { file_ids: [] };
|
||||
}
|
||||
|
||||
if (!toolResources[file.tool_resource]!.file_ids!.includes(file.file_id)) {
|
||||
toolResources[file.tool_resource]!.file_ids!.push(file.file_id);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(toolResources).length > 0 ? toolResources : undefined;
|
||||
}, [promptFiles]);
|
||||
|
||||
const loadFromToolResources = useCallback(
|
||||
async (toolResources?: AgentToolResources) => {
|
||||
if (!toolResources) {
|
||||
setFiles([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const filesArray: ExtendedFile[] = [];
|
||||
|
||||
for (const [toolResource, resource] of Object.entries(toolResources)) {
|
||||
if (resource?.file_ids) {
|
||||
for (const fileId of resource.file_ids) {
|
||||
const dbFile = fileMap[fileId];
|
||||
const source =
|
||||
toolResource === EToolResources.file_search
|
||||
? FileSources.vectordb
|
||||
: (dbFile?.source ?? FileSources.local);
|
||||
|
||||
let file: ExtendedFile;
|
||||
|
||||
if (dbFile) {
|
||||
file = {
|
||||
file_id: dbFile.file_id,
|
||||
temp_file_id: dbFile.file_id,
|
||||
type: dbFile.type,
|
||||
filename: dbFile.filename,
|
||||
filepath: dbFile.filepath,
|
||||
progress: 1,
|
||||
preview: dbFile.filepath,
|
||||
size: dbFile.bytes || 0,
|
||||
width: dbFile.width,
|
||||
height: dbFile.height,
|
||||
attached: true,
|
||||
tool_resource: toolResource,
|
||||
metadata: dbFile.metadata,
|
||||
source,
|
||||
};
|
||||
} else {
|
||||
file = {
|
||||
file_id: fileId,
|
||||
temp_file_id: fileId,
|
||||
type: 'application/octet-stream',
|
||||
filename: `File ${fileId}`,
|
||||
filepath: '',
|
||||
progress: 1,
|
||||
preview: '',
|
||||
size: 0,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
attached: true,
|
||||
tool_resource: toolResource,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
filesArray.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFiles(filesArray);
|
||||
},
|
||||
[fileMap],
|
||||
);
|
||||
|
||||
const areFilesReady = useMemo(() => {
|
||||
return promptFiles.every((file) => file.file_id && file.progress === 1);
|
||||
}, [promptFiles]);
|
||||
|
||||
const fileStats = useMemo(() => {
|
||||
const stats = {
|
||||
total: promptFiles.length,
|
||||
images: 0,
|
||||
documents: 0,
|
||||
uploading: 0,
|
||||
};
|
||||
|
||||
promptFiles.forEach((file) => {
|
||||
if (file.progress < 1) {
|
||||
stats.uploading++;
|
||||
} else if (file.type?.startsWith('image/')) {
|
||||
stats.images++;
|
||||
} else {
|
||||
stats.documents++;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}, [promptFiles]);
|
||||
|
||||
const abortUpload = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
logger.log('files', 'Aborting upload');
|
||||
abortControllerRef.current.abort('User aborted upload');
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleFileChange,
|
||||
abortUpload,
|
||||
files,
|
||||
setFiles,
|
||||
promptFiles,
|
||||
getToolResources,
|
||||
loadFromToolResources,
|
||||
areFilesReady,
|
||||
fileStats,
|
||||
handleFileRemove,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePromptFileHandling;
|
||||
@@ -703,6 +703,7 @@
|
||||
"com_ui_attach_error_openai": "Cannot attach Assistant files to other endpoints",
|
||||
"com_ui_attach_error_size": "File size limit exceeded for endpoint:",
|
||||
"com_ui_attach_error_type": "Unsupported file type for endpoint:",
|
||||
"com_ui_attach_files": "Attach Files",
|
||||
"com_ui_attach_remove": "Remove file",
|
||||
"com_ui_attach_warn_endpoint": "Non-Assistant files may be ignored without a compatible tool",
|
||||
"com_ui_attachment": "Attachment",
|
||||
@@ -897,6 +898,7 @@
|
||||
"com_ui_file_token_limit": "File Token Limit",
|
||||
"com_ui_file_token_limit_desc": "Set maximum token limit for file processing to control costs and resource usage",
|
||||
"com_ui_files": "Files",
|
||||
"com_ui_files_info": "Attach files to enhance your prompt with additional context",
|
||||
"com_ui_filter_prompts": "Filter Prompts",
|
||||
"com_ui_filter_prompts_name": "Filter prompts by name",
|
||||
"com_ui_final_touch": "Final touch",
|
||||
|
||||
@@ -9,10 +9,10 @@ import type {
|
||||
TConversationTag,
|
||||
TAttachment,
|
||||
} from './schemas';
|
||||
import type { Agent, AgentToolResources } from './types/assistants';
|
||||
import type { SettingDefinition } from './generate';
|
||||
import type { TMinimalFeedback } from './feedback';
|
||||
import type { ContentTypes } from './types/runs';
|
||||
import type { Agent } from './types/assistants';
|
||||
|
||||
export * from './schemas';
|
||||
|
||||
@@ -499,6 +499,7 @@ export type TPrompt = {
|
||||
author: string;
|
||||
prompt: string;
|
||||
type: 'text' | 'chat';
|
||||
tool_resources?: AgentToolResources;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
_id?: string;
|
||||
@@ -512,7 +513,7 @@ export type TPromptGroup = {
|
||||
category?: string;
|
||||
projectIds?: string[];
|
||||
productionId?: string | null;
|
||||
productionPrompt?: Pick<TPrompt, 'prompt'> | null;
|
||||
productionPrompt?: Pick<TPrompt, 'prompt' | 'tool_resources'> | null;
|
||||
author: string;
|
||||
authorName: string;
|
||||
createdAt?: Date;
|
||||
@@ -521,7 +522,7 @@ export type TPromptGroup = {
|
||||
};
|
||||
|
||||
export type TCreatePrompt = {
|
||||
prompt: Pick<TPrompt, 'prompt' | 'type'> & { groupId?: string };
|
||||
prompt: Pick<TPrompt, 'prompt' | 'type' | 'tool_resources'> & { groupId?: string };
|
||||
group?: { name: string; category?: string; oneliner?: string; command?: string };
|
||||
};
|
||||
|
||||
@@ -591,7 +592,7 @@ export type TMakePromptProductionResponse = {
|
||||
export type TMakePromptProductionRequest = {
|
||||
id: string;
|
||||
groupId: string;
|
||||
productionPrompt: Pick<TPrompt, 'prompt'>;
|
||||
productionPrompt: Pick<TPrompt, 'prompt' | 'tool_resources'>;
|
||||
};
|
||||
|
||||
export type TUpdatePromptLabelsRequest = {
|
||||
|
||||
@@ -23,6 +23,10 @@ const promptSchema: Schema<IPrompt> = new Schema(
|
||||
enum: ['text', 'chat'],
|
||||
required: true,
|
||||
},
|
||||
tool_resources: {
|
||||
type: Schema.Types.Mixed,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AgentToolResources } from 'librechat-data-provider';
|
||||
import type { Document, Types } from 'mongoose';
|
||||
|
||||
export interface IPrompt extends Document {
|
||||
@@ -5,6 +6,7 @@ export interface IPrompt extends Document {
|
||||
author: Types.ObjectId;
|
||||
prompt: string;
|
||||
type: 'text' | 'chat';
|
||||
tool_resources?: AgentToolResources;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user