Skip to content

Commit

Permalink
🔧 feat: Share Assistant Actions between Users (danny-avila#2116)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
danny-avila authored Mar 16, 2024
1 parent 06eeeb1 commit 277dfd8
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 16 deletions.
1 change: 0 additions & 1 deletion api/models/schema/assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const assistantSchema = mongoose.Schema(
},
assistant_id: {
type: String,
unique: true,
index: true,
required: true,
},
Expand Down
18 changes: 10 additions & 8 deletions api/server/routes/assistants/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const router = express.Router();
*/
router.get('/', async (req, res) => {
try {
res.json(await getActions({ user: req.user.id }));
res.json(await getActions());
} catch (error) {
res.status(500).json({ error: error.message });
}
Expand Down Expand Up @@ -55,9 +55,9 @@ router.post('/:assistant_id', async (req, res) => {
/** @type {{ openai: OpenAI }} */
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));
!!_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]]} */
const [assistant_data, assistant, actions_result] = await Promise.all(initialPromises);
Expand Down Expand Up @@ -115,14 +115,15 @@ router.post('/:assistant_id', async (req, res) => {
const promises = [];
promises.push(
updateAssistant(
{ assistant_id, user: req.user.id },
{ assistant_id },
{
actions,
user: req.user.id,
},
),
);
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]} */
const resolved = await Promise.all(promises);
Expand Down Expand Up @@ -155,7 +156,7 @@ router.delete('/:assistant_id/:action_id', async (req, res) => {
const { openai } = await initializeClient({ req, res });

const initialPromises = [];
initialPromises.push(getAssistant({ assistant_id, user: req.user.id }));
initialPromises.push(getAssistant({ assistant_id }));
initialPromises.push(openai.beta.assistants.retrieve(assistant_id));

/** @type {[AssistantDocument, Assistant]} */
Expand All @@ -180,14 +181,15 @@ router.delete('/:assistant_id/:action_id', async (req, res) => {
const promises = [];
promises.push(
updateAssistant(
{ assistant_id, user: req.user.id },
{ assistant_id },
{
actions: updatedActions,
user: req.user.id,
},
),
);
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);
res.status(200).json({ message: 'Action deleted successfully' });
Expand Down
3 changes: 2 additions & 1 deletion api/server/routes/assistants/assistants.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,13 @@ router.post('/avatar/:assistant_id', upload.single('file'), async (req, res) =>
const promises = [];
promises.push(
updateAssistant(
{ assistant_id, user: req.user.id },
{ assistant_id },
{
avatar: {
filepath: image.filepath,
source: req.app.locals.fileStrategy,
},
user: req.user.id,
},
),
);
Expand Down
14 changes: 8 additions & 6 deletions api/server/services/ActionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ const { logger } = require('~/config');
/**
* Loads action sets based on the user and assistant ID.
*
* @param {Object} params - The parameters for loading action sets.
* @param {string} params.user - The user identifier.
* @param {string} params.assistant_id - The assistant identifier.
* @param {Object} searchParams - The parameters for loading action sets.
* @param {string} searchParams.user - The user 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.
*/
async function loadActionSets({ user, assistant_id }) {
return await getActions({ user, assistant_id }, true);
async function loadActionSets(searchParams) {
return await getActions(searchParams, true);
}

/**
Expand Down Expand Up @@ -40,7 +40,9 @@ function createActionTool({ action, requestBuilder }) {
logger.error(`API call to ${action.metadata.domain} failed`, error);
if (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.`;
Expand Down
4 changes: 4 additions & 0 deletions api/server/utils/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ function encryptV2(value) {

function decryptV2(encryptedValue) {
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 encrypted = parts.join(':');
const decipher = crypto.createDecipheriv(algorithm, key, gen_iv);
Expand Down
33 changes: 33 additions & 0 deletions packages/data-provider/specs/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,39 @@ describe('ActionRequest', () => {
);
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 () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/data-provider/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export class ActionRequest {
async setParams(params: object) {
this.operationHash = sha1(JSON.stringify(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) {
Expand Down

0 comments on commit 277dfd8

Please sign in to comment.