Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔧 feat: Share Assistant Actions between Users #2116

Merged
merged 8 commits into from
Mar 16, 2024
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
Loading