mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🔧 feat: Share Assistant Actions between Users (#2116)
* fix: remove unique field from assistant_id, which can be shared between different users * refactor: remove unique user fields from actions/assistant queries * feat: only allow user who saved action to delete it * refactor: allow deletions for anyone with builder access * refactor: update user.id when updating assistants/actions records, instead of searching with it * fix: stringify response data in case it's an object * fix: correctly handle path input * fix(decryptV2): handle edge case where value is already decrypted
This commit is contained in:
parent
2f90c8764a
commit
a8cdd3460c
7 changed files with 65 additions and 16 deletions
|
|
@ -9,7 +9,6 @@ const assistantSchema = mongoose.Schema(
|
||||||
},
|
},
|
||||||
assistant_id: {
|
assistant_id: {
|
||||||
type: String,
|
type: String,
|
||||||
unique: true,
|
|
||||||
index: true,
|
index: true,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const router = express.Router();
|
||||||
*/
|
*/
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.json(await getActions({ user: req.user.id }));
|
res.json(await getActions());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|
@ -55,9 +55,9 @@ router.post('/:assistant_id', async (req, res) => {
|
||||||
/** @type {{ openai: OpenAI }} */
|
/** @type {{ openai: OpenAI }} */
|
||||||
const { openai } = await initializeClient({ req, res });
|
const { openai } = await initializeClient({ req, res });
|
||||||
|
|
||||||
initialPromises.push(getAssistant({ assistant_id, user: req.user.id }));
|
initialPromises.push(getAssistant({ assistant_id }));
|
||||||
initialPromises.push(openai.beta.assistants.retrieve(assistant_id));
|
initialPromises.push(openai.beta.assistants.retrieve(assistant_id));
|
||||||
!!_action_id && initialPromises.push(getActions({ user: req.user.id, action_id }, true));
|
!!_action_id && initialPromises.push(getActions({ action_id }, true));
|
||||||
|
|
||||||
/** @type {[AssistantDocument, Assistant, [Action|undefined]]} */
|
/** @type {[AssistantDocument, Assistant, [Action|undefined]]} */
|
||||||
const [assistant_data, assistant, actions_result] = await Promise.all(initialPromises);
|
const [assistant_data, assistant, actions_result] = await Promise.all(initialPromises);
|
||||||
|
|
@ -115,14 +115,15 @@ router.post('/:assistant_id', async (req, res) => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
promises.push(
|
promises.push(
|
||||||
updateAssistant(
|
updateAssistant(
|
||||||
{ assistant_id, user: req.user.id },
|
{ assistant_id },
|
||||||
{
|
{
|
||||||
actions,
|
actions,
|
||||||
|
user: req.user.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
promises.push(openai.beta.assistants.update(assistant_id, { tools }));
|
promises.push(openai.beta.assistants.update(assistant_id, { tools }));
|
||||||
promises.push(updateAction({ action_id, user: req.user.id }, { metadata, assistant_id }));
|
promises.push(updateAction({ action_id }, { metadata, assistant_id, user: req.user.id }));
|
||||||
|
|
||||||
/** @type {[AssistantDocument, Assistant, Action]} */
|
/** @type {[AssistantDocument, Assistant, Action]} */
|
||||||
const resolved = await Promise.all(promises);
|
const resolved = await Promise.all(promises);
|
||||||
|
|
@ -155,7 +156,7 @@ router.delete('/:assistant_id/:action_id', async (req, res) => {
|
||||||
const { openai } = await initializeClient({ req, res });
|
const { openai } = await initializeClient({ req, res });
|
||||||
|
|
||||||
const initialPromises = [];
|
const initialPromises = [];
|
||||||
initialPromises.push(getAssistant({ assistant_id, user: req.user.id }));
|
initialPromises.push(getAssistant({ assistant_id }));
|
||||||
initialPromises.push(openai.beta.assistants.retrieve(assistant_id));
|
initialPromises.push(openai.beta.assistants.retrieve(assistant_id));
|
||||||
|
|
||||||
/** @type {[AssistantDocument, Assistant]} */
|
/** @type {[AssistantDocument, Assistant]} */
|
||||||
|
|
@ -180,14 +181,15 @@ router.delete('/:assistant_id/:action_id', async (req, res) => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
promises.push(
|
promises.push(
|
||||||
updateAssistant(
|
updateAssistant(
|
||||||
{ assistant_id, user: req.user.id },
|
{ assistant_id },
|
||||||
{
|
{
|
||||||
actions: updatedActions,
|
actions: updatedActions,
|
||||||
|
user: req.user.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
promises.push(openai.beta.assistants.update(assistant_id, { tools: updatedTools }));
|
promises.push(openai.beta.assistants.update(assistant_id, { tools: updatedTools }));
|
||||||
promises.push(deleteAction({ action_id, user: req.user.id }));
|
promises.push(deleteAction({ action_id }));
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
res.status(200).json({ message: 'Action deleted successfully' });
|
res.status(200).json({ message: 'Action deleted successfully' });
|
||||||
|
|
|
||||||
|
|
@ -241,12 +241,13 @@ router.post('/avatar/:assistant_id', upload.single('file'), async (req, res) =>
|
||||||
const promises = [];
|
const promises = [];
|
||||||
promises.push(
|
promises.push(
|
||||||
updateAssistant(
|
updateAssistant(
|
||||||
{ assistant_id, user: req.user.id },
|
{ assistant_id },
|
||||||
{
|
{
|
||||||
avatar: {
|
avatar: {
|
||||||
filepath: image.filepath,
|
filepath: image.filepath,
|
||||||
source: req.app.locals.fileStrategy,
|
source: req.app.locals.fileStrategy,
|
||||||
},
|
},
|
||||||
|
user: req.user.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ const { logger } = require('~/config');
|
||||||
/**
|
/**
|
||||||
* Loads action sets based on the user and assistant ID.
|
* Loads action sets based on the user and assistant ID.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The parameters for loading action sets.
|
* @param {Object} searchParams - The parameters for loading action sets.
|
||||||
* @param {string} params.user - The user identifier.
|
* @param {string} searchParams.user - The user identifier.
|
||||||
* @param {string} params.assistant_id - The assistant identifier.
|
* @param {string} searchParams.assistant_id - The assistant identifier.
|
||||||
* @returns {Promise<Action[] | null>} A promise that resolves to an array of actions or `null` if no match.
|
* @returns {Promise<Action[] | null>} A promise that resolves to an array of actions or `null` if no match.
|
||||||
*/
|
*/
|
||||||
async function loadActionSets({ user, assistant_id }) {
|
async function loadActionSets(searchParams) {
|
||||||
return await getActions({ user, assistant_id }, true);
|
return await getActions(searchParams, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,7 +40,9 @@ function createActionTool({ action, requestBuilder }) {
|
||||||
logger.error(`API call to ${action.metadata.domain} failed`, error);
|
logger.error(`API call to ${action.metadata.domain} failed`, error);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { status, data } = error.response;
|
const { status, data } = error.response;
|
||||||
return `API call to ${action.metadata.domain} failed with status ${status}: ${data}`;
|
return `API call to ${
|
||||||
|
action.metadata.domain
|
||||||
|
} failed with status ${status}: ${JSON.stringify(data)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `API call to ${action.metadata.domain} failed.`;
|
return `API call to ${action.metadata.domain} failed.`;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ function encryptV2(value) {
|
||||||
|
|
||||||
function decryptV2(encryptedValue) {
|
function decryptV2(encryptedValue) {
|
||||||
const parts = encryptedValue.split(':');
|
const parts = encryptedValue.split(':');
|
||||||
|
// Already decrypted from an earlier invocation
|
||||||
|
if (parts.length === 1) {
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
const gen_iv = Buffer.from(parts.shift(), 'hex');
|
const gen_iv = Buffer.from(parts.shift(), 'hex');
|
||||||
const encrypted = parts.join(':');
|
const encrypted = parts.join(':');
|
||||||
const decipher = crypto.createDecipheriv(algorithm, key, gen_iv);
|
const decipher = crypto.createDecipheriv(algorithm, key, gen_iv);
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,39 @@ describe('ActionRequest', () => {
|
||||||
);
|
);
|
||||||
await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
|
await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('replaces path parameters with values from toolInput', async () => {
|
||||||
|
const actionRequest = new ActionRequest(
|
||||||
|
'https://example.com',
|
||||||
|
'/stocks/{stocksTicker}/bars/{multiplier}',
|
||||||
|
'GET',
|
||||||
|
'getAggregateBars',
|
||||||
|
false,
|
||||||
|
'application/json',
|
||||||
|
);
|
||||||
|
|
||||||
|
await actionRequest.setParams({
|
||||||
|
stocksTicker: 'AAPL',
|
||||||
|
multiplier: 5,
|
||||||
|
startDate: '2023-01-01',
|
||||||
|
endDate: '2023-12-31',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(actionRequest.path).toBe('/stocks/AAPL/bars/5');
|
||||||
|
expect(actionRequest.params).toEqual({
|
||||||
|
startDate: '2023-01-01',
|
||||||
|
endDate: '2023-12-31',
|
||||||
|
});
|
||||||
|
|
||||||
|
await actionRequest.execute();
|
||||||
|
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/stocks/AAPL/bars/5', {
|
||||||
|
headers: expect.anything(),
|
||||||
|
params: {
|
||||||
|
startDate: '2023-01-01',
|
||||||
|
endDate: '2023-12-31',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error for unsupported HTTP method', async () => {
|
it('throws an error for unsupported HTTP method', async () => {
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,14 @@ export class ActionRequest {
|
||||||
async setParams(params: object) {
|
async setParams(params: object) {
|
||||||
this.operationHash = sha1(JSON.stringify(params));
|
this.operationHash = sha1(JSON.stringify(params));
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
const paramPattern = `{${key}}`;
|
||||||
|
if (this.path.includes(paramPattern)) {
|
||||||
|
this.path = this.path.replace(paramPattern, encodeURIComponent(value as string));
|
||||||
|
delete (this.params as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAuth(metadata: ActionMetadata) {
|
async setAuth(metadata: ActionMetadata) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue