From 9e9c77392977b68869d80838c3dead497666cc0c Mon Sep 17 00:00:00 2001 From: Himanshu Dixit Date: Mon, 3 Feb 2025 02:50:14 +0530 Subject: [PATCH 1/2] feat: add action versioning support --- js/src/sdk/base.toolset.ts | 39 ++++++++++++++- js/src/sdk/models/actions.ts | 1 + js/src/sdk/types/action.ts | 1 + js/src/sdk/utils/lock/load.ts | 14 ++++++ js/src/sdk/utils/lockFile.ts | 60 +++++++++++++++++++++++ js/src/sdk/utils/processor/action_lock.ts | 14 ++++++ js/src/types/base_toolset.ts | 2 + 7 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 js/src/sdk/utils/lock/load.ts create mode 100644 js/src/sdk/utils/lockFile.ts create mode 100644 js/src/sdk/utils/processor/action_lock.ts diff --git a/js/src/sdk/base.toolset.ts b/js/src/sdk/base.toolset.ts index 85779bed6d6..77beb27339c 100644 --- a/js/src/sdk/base.toolset.ts +++ b/js/src/sdk/base.toolset.ts @@ -27,6 +27,8 @@ import { Triggers } from "./models/triggers"; import { getUserDataJson } from "./utils/config"; import { CEG } from "./utils/error"; import { COMPOSIO_SDK_ERROR_CODES } from "./utils/errors/src/constants"; +import { getVersionsFromLockFileAsJson } from "./utils/lockFile"; +import { actionLockProcessor } from "./utils/processor/action_lock"; import { fileInputProcessor, fileResponseProcessor, @@ -55,6 +57,11 @@ export class ComposioToolSet { userActionRegistry: ActionRegistry; + lockFile: { + path: string | null; + save: boolean; + }; + private internalProcessors: { pre: TPreProcessor[]; post: TPostProcessor[]; @@ -79,6 +86,9 @@ export class ComposioToolSet { * @param {string|null} config.runtime - Runtime environment * @param {string} config.entityId - Entity ID for operations * @param {Record} config.connectedAccountIds - Map of app names to their connected account IDs + * @param {Object} config.lockFile - Lock file configuration + * @param {string} config.lockFile.path - Path to the lock file, Default: .composio.lock + * @param {boolean} config.lockFile.lock - Whether to lock the file */ constructor({ apiKey, @@ -86,12 +96,17 @@ export class ComposioToolSet { runtime, entityId, connectedAccountIds, + lockFile, }: { apiKey?: string | null; baseUrl?: string | null; runtime?: string | null; entityId?: string; connectedAccountIds?: Record; + lockFile?: { + path: string; + save: boolean; + }; } = {}) { const clientApiKey: string | undefined = apiKey || @@ -114,6 +129,22 @@ export class ComposioToolSet { this.activeTriggers = this.client.activeTriggers; this.connectedAccountIds = connectedAccountIds || {}; + this.lockFile = lockFile || { + path: ".composio.lock", + save: false, + }; + + if (this.lockFile.save) { + if (!this.lockFile.path) { + CEG.getCustomError(COMPOSIO_SDK_ERROR_CODES.SDK.INVALID_PARAMETER, { + message: "Lock file path is required when save is true", + description: "Lock file path is required when save is true", + }); + } + const actionLock = actionLockProcessor.bind(this, this.lockFile.path); + this.internalProcessors.schema.push(actionLock); + } + this.userActionRegistry = new ActionRegistry(this.client); if (entityId && connectedAccountIds) { @@ -149,13 +180,17 @@ export class ComposioToolSet { ): Promise { const parsedFilters = ZToolSchemaFilter.parse(filters); - const apps = await this.client.actions.list({ + const actionVersions = this.lockFile.path + ? getVersionsFromLockFileAsJson(this.lockFile.path) + : {}; + const actions = await this.client.actions.list({ apps: parsedFilters.apps?.join(","), tags: parsedFilters.tags?.join(","), useCase: parsedFilters.useCase, actions: parsedFilters.actions?.join(","), usecaseLimit: parsedFilters.useCaseLimit, filterByAvailableApps: parsedFilters.filterByAvailableApps, + actionVersions: actionVersions, }); const customActions = await this.userActionRegistry.getAllActions(); @@ -171,7 +206,7 @@ export class ComposioToolSet { ); }); - const toolsActions = [...(apps?.items || []), ...toolsWithCustomActions]; + const toolsActions = [...(actions?.items || []), ...toolsWithCustomActions]; const allSchemaProcessor = [ ...this.internalProcessors.schema, diff --git a/js/src/sdk/models/actions.ts b/js/src/sdk/models/actions.ts index 231594dee67..78734402c6b 100644 --- a/js/src/sdk/models/actions.ts +++ b/js/src/sdk/models/actions.ts @@ -121,6 +121,7 @@ export class Actions { showEnabledOnly: data.showEnabledOnly, usecaseLimit: data.usecaseLimit || undefined, useCase: data.useCase as string, + actionVersions: data.actionVersions, }, body: { useCase: data.useCase as string, diff --git a/js/src/sdk/types/action.ts b/js/src/sdk/types/action.ts index 4f0ed86496e..9e287213e48 100644 --- a/js/src/sdk/types/action.ts +++ b/js/src/sdk/types/action.ts @@ -20,6 +20,7 @@ export const ZGetListActionsParams = z.object({ .boolean() .optional() .describe("Filter actions by available apps"), + actionVersions: z.record(z.string()).optional(), }); export const ZParameter = z.object({ diff --git a/js/src/sdk/utils/lock/load.ts b/js/src/sdk/utils/lock/load.ts new file mode 100644 index 00000000000..9af5da358ad --- /dev/null +++ b/js/src/sdk/utils/lock/load.ts @@ -0,0 +1,14 @@ +import fs from "fs"; + +export const loadLockFile = (filePath: string) => { + const fileContent = fs.readFileSync(filePath, "utf8"); + return JSON.parse(fileContent); +}; + +export const saveLockFile = ( + filePath: string, + actionName: string, + version: string +) => { + fs.writeFileSync(filePath, JSON.stringify({ actionName, version }, null, 2)); +}; diff --git a/js/src/sdk/utils/lockFile.ts b/js/src/sdk/utils/lockFile.ts new file mode 100644 index 00000000000..35245230349 --- /dev/null +++ b/js/src/sdk/utils/lockFile.ts @@ -0,0 +1,60 @@ +import logger from "../../utils/logger"; + +export const updateLockFileWithActionVersion = ( + filePath: string, + actionName: string, + version: string +) => { + const actionVersions = getVersionsFromLockFileAsJson(filePath); + actionVersions[actionName] = version; + saveLockFile(filePath, actionVersions); +}; + +export const getVersionsFromLockFileAsJson = (filePath: string) => { + try { + const lockFileContent = require("fs").readFileSync(filePath, "utf8"); + const actionVersions: Record = {}; + const lines = lockFileContent.split("\n"); + for (const line of lines) { + if (line) { + const [actionName, version] = line.split("="); + actionVersions[actionName] = version; + } + } + return actionVersions; + } catch (e) { + const error = e as NodeJS.ErrnoException; + if (error.code === "ENOENT") { + logger.warn("Lock file does not exist, creating new one"); + } else if (error.code === "EACCES" || error.code === "EPERM") { + logger.error("Permission denied accessing lock file", e); + } else { + logger.warn("Error reading lock file", e); + } + return {}; + } +}; + +export const saveLockFile = ( + filePath: string, + actionVersions: Record +) => { + try { + const lockFileContent = Object.entries(actionVersions) + .map(([actionName, version]) => `${actionName}=${version}`) + .join("\n"); + require("fs").writeFileSync(filePath, lockFileContent); + } catch (e) { + const error = e as NodeJS.ErrnoException; + if (error.code === "EACCES" || error.code === "EPERM") { + logger.error("Permission denied writing to lock file", e); + throw new Error("Permission denied writing to lock file"); + } else if (error.code === "ENOENT") { + logger.error("Directory does not exist for lock file", e); + throw new Error("Directory does not exist for lock file"); + } else { + logger.error("Error writing to lock file", e); + throw new Error("Error writing to lock file"); + } + } +}; diff --git a/js/src/sdk/utils/processor/action_lock.ts b/js/src/sdk/utils/processor/action_lock.ts new file mode 100644 index 00000000000..59a6be87689 --- /dev/null +++ b/js/src/sdk/utils/processor/action_lock.ts @@ -0,0 +1,14 @@ +import { RawActionData } from "../../../types/base_toolset"; + +export const actionLockProcessor = ( + filePath: string, + { + actionName, + toolSchema, + }: { + actionName: string; + toolSchema: RawActionData; + } +): RawActionData => { + return toolSchema; +}; diff --git a/js/src/types/base_toolset.ts b/js/src/types/base_toolset.ts index 8b61568e297..b52ac375a5e 100644 --- a/js/src/types/base_toolset.ts +++ b/js/src/types/base_toolset.ts @@ -27,6 +27,8 @@ export const ZRawActionSchema = z.object({ name: z.string(), toolName: z.string().optional(), }), + version: z.string().optional(), + availableVersions: z.array(z.string()).optional(), }); export type RawActionData = z.infer; From f9bc94a2ed25b59825e02a598d5d59c2ae7768fa Mon Sep 17 00:00:00 2001 From: Himanshu Dixit Date: Mon, 3 Feb 2025 03:14:29 +0530 Subject: [PATCH 2/2] feat: update --- js/src/sdk/base.toolset.ts | 8 ++++++++ js/src/sdk/models/Entity.ts | 4 ++++ js/src/sdk/models/actions.ts | 3 +++ js/src/sdk/types/action.ts | 1 + js/src/sdk/types/entity.ts | 1 + js/src/sdk/utils/processor/action_lock.ts | 5 +++++ 6 files changed, 22 insertions(+) diff --git a/js/src/sdk/base.toolset.ts b/js/src/sdk/base.toolset.ts index 77beb27339c..ac5685a2eeb 100644 --- a/js/src/sdk/base.toolset.ts +++ b/js/src/sdk/base.toolset.ts @@ -56,6 +56,7 @@ export class ComposioToolSet { activeTriggers: ActiveTriggers; userActionRegistry: ActionRegistry; + actionVersionMap: Record; lockFile: { path: string | null; @@ -128,6 +129,7 @@ export class ComposioToolSet { this.integrations = this.client.integrations; this.activeTriggers = this.client.activeTriggers; this.connectedAccountIds = connectedAccountIds || {}; + this.actionVersionMap = {}; this.lockFile = lockFile || { path: ".composio.lock", @@ -217,6 +219,10 @@ export class ComposioToolSet { return toolsActions.map((tool) => { let schema = tool as RawActionData; + if (schema?.version) { + // store the version in the map for execution + this.actionVersionMap[schema?.name.toUpperCase()] = schema?.version; + } allSchemaProcessor.forEach((processor) => { schema = processor({ actionName: schema?.name, @@ -312,11 +318,13 @@ export class ComposioToolSet { }); } + const version = this.actionVersionMap[action.toUpperCase()]; const data = await this.client.getEntity(entityId).execute({ actionName: action, params: params, text: nlaText, connectedAccountId: connectedAccountId, + version: version, }); return this.processResponse(data, { diff --git a/js/src/sdk/models/Entity.ts b/js/src/sdk/models/Entity.ts index b83acda1ec3..9feb7433c51 100644 --- a/js/src/sdk/models/Entity.ts +++ b/js/src/sdk/models/Entity.ts @@ -79,6 +79,7 @@ export class Entity { params, text, connectedAccountId, + version, }: ExecuteActionParams): Promise { TELEMETRY_LOGGER.manualTelemetry(TELEMETRY_EVENTS.SDK_METHOD_INVOKED, { method: "execute", @@ -91,6 +92,7 @@ export class Entity { params, text, connectedAccountId, + version, }); const action = await this.actionsModel.get({ actionName: actionName, @@ -104,6 +106,7 @@ export class Entity { if (app.no_auth) { return this.actionsModel.execute({ actionName: actionName, + version: version, requestBody: { input: params, appName: action.appKey, @@ -126,6 +129,7 @@ export class Entity { } return this.actionsModel.execute({ actionName: actionName, + version: version, requestBody: { // @ts-ignore connectedAccountId: connectedAccount?.id as unknown as string, diff --git a/js/src/sdk/models/actions.ts b/js/src/sdk/models/actions.ts index 78734402c6b..0bbffc6eab5 100644 --- a/js/src/sdk/models/actions.ts +++ b/js/src/sdk/models/actions.ts @@ -156,6 +156,9 @@ export class Actions { path: { actionId: parsedData.actionName, }, + query: { + version: parsedData.version, + }, }); return res!; } catch (error) { diff --git a/js/src/sdk/types/action.ts b/js/src/sdk/types/action.ts index 9e287213e48..4fe0e8fb6fa 100644 --- a/js/src/sdk/types/action.ts +++ b/js/src/sdk/types/action.ts @@ -37,6 +37,7 @@ export const ZCustomAuthParams = z.object({ export const ZExecuteParams = z.object({ actionName: z.string(), + version: z.string().optional(), requestBody: z.object({ connectedAccountId: z.string().optional(), input: z.record(z.unknown()).optional(), diff --git a/js/src/sdk/types/entity.ts b/js/src/sdk/types/entity.ts index 49daebcd3c0..a3dd70214dd 100644 --- a/js/src/sdk/types/entity.ts +++ b/js/src/sdk/types/entity.ts @@ -6,6 +6,7 @@ export const ZExecuteActionParams = z.object({ params: z.record(z.any()).optional(), text: z.string().optional(), connectedAccountId: z.string().optional(), + version: z.string().optional(), }); export const ZInitiateConnectionParams = z.object({ diff --git a/js/src/sdk/utils/processor/action_lock.ts b/js/src/sdk/utils/processor/action_lock.ts index 59a6be87689..3fa720af3bd 100644 --- a/js/src/sdk/utils/processor/action_lock.ts +++ b/js/src/sdk/utils/processor/action_lock.ts @@ -1,4 +1,5 @@ import { RawActionData } from "../../../types/base_toolset"; +import { updateLockFileWithActionVersion } from "../lockFile"; export const actionLockProcessor = ( filePath: string, @@ -10,5 +11,9 @@ export const actionLockProcessor = ( toolSchema: RawActionData; } ): RawActionData => { + const version = toolSchema.version; + + updateLockFileWithActionVersion(filePath, actionName, version); + return toolSchema; };