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

Add proxy support for executing custom actions #877

Merged
merged 15 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 58 additions & 25 deletions js/src/frameworks/langchain.spec.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,75 @@
import { describe, it, expect, beforeAll } from "@jest/globals";
import { CloudflareToolSet } from "./cloudflare";
import { get } from "http";
import { z } from "zod";
import { getTestConfig } from "../../config/getTestConfig";
import { LangchainToolSet } from "./langchain";


describe("Apps class tests", () => {

let langchainToolSet: LangchainToolSet;
beforeAll(() => {
langchainToolSet = new LangchainToolSet({
apiKey: getTestConfig().COMPOSIO_API_KEY,
baseUrl: getTestConfig().BACKEND_HERMES_URL
});
let langchainToolSet: LangchainToolSet;
beforeAll(() => {
langchainToolSet = new LangchainToolSet({
apiKey: getTestConfig().COMPOSIO_API_KEY,
baseUrl: getTestConfig().BACKEND_HERMES_URL,
});
});

it("getools",async() => {
const tools = await langchainToolSet.getTools({
apps: ['github']
});
it("getools", async () => {
const tools = await langchainToolSet.getTools({
apps: ["github"],
});

expect(tools).toBeInstanceOf(Array);
expect(tools).toBeInstanceOf(Array);
});

it("check if tools are coming", async () => {
const tools = await langchainToolSet.getTools({
actions: ["GITHUB_GITHUB_API_ROOT"],
});

it("check if tools are coming", async () => {
const tools = await langchainToolSet.getTools({
actions: ['GITHUB_GITHUB_API_ROOT']
});
expect(tools.length).toBe(1);
});

expect(tools.length).toBe(1);
it("check if getTools, actions are coming", async () => {
const tools = await langchainToolSet.getTools({
actions: ["GITHUB_GITHUB_API_ROOT"],
});

it("check if getTools, actions are coming", async () => {
const tools = await langchainToolSet.getTools({
actions: ['GITHUB_GITHUB_API_ROOT']
});
expect(tools.length).toBe(1);
});

expect(tools.length).toBe(1)
it("Should create custom action to star a repository", async () => {
const action = await langchainToolSet.createAction({
actionName: "starRepositoryPlxityCustom2",
toolName: "github",
description: "This action stars a repository",
inputParams: z.object({
owner: z.string(),
repo: z.string(),
}),
callback: async (inputParams, authCredentials) => {
try {
const res = await langchainToolSet.executeRequest({
connectedAccountId: "45993fa0-c769-4297-887b-94b5170da306",
endpoint: `/user/starred/${inputParams.owner}/${inputParams.repo}`,
method: "PUT",
parameters: [],
});
return res;
} catch (e) {
console.error(e);
return {};
}
},
});

const actionOuput = await langchainToolSet.executeAction(
"starRepositoryPlxityCustom2",
{
owner: "plxity",
repo: "achievementsof.life",
},
"default"
);

expect(actionOuput).toHaveProperty("successfull", true);
});
});
239 changes: 129 additions & 110 deletions js/src/sdk/actionRegistry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,131 +5,150 @@ import { Composio } from ".";
import { getTestConfig } from "../../config/getTestConfig";

describe("ActionRegistry", () => {
let actionRegistry: ActionRegistry;
let client: Composio;
const testConfig = getTestConfig();

beforeAll(() => {
client = new Composio(testConfig.COMPOSIO_API_KEY!,testConfig.BACKEND_HERMES_URL!);
let actionRegistry: ActionRegistry;
let client: Composio;
const testConfig = getTestConfig();

beforeAll(() => {
client = new Composio(
testConfig.COMPOSIO_API_KEY!,
testConfig.BACKEND_HERMES_URL!
);
});

beforeEach(() => {
actionRegistry = new ActionRegistry(client);
});

it("should create an action with valid parameters", async () => {
const params = z.object({
param1: z.string(),
param2: z.string().optional(),
});

beforeEach(() => {
actionRegistry = new ActionRegistry(client);
const callback = async (params: Record<string, any>) => {
return { success: true };
};

const options = {
actionName: "testAction",
toolName: "testTool",
description: "This is a test action",
inputParams: params,
callback,
};

const action = await actionRegistry.createAction(options);

expect(action.parameters.title).toBe("testAction");
expect(action.parameters.type).toBe("object");
expect(action.parameters.description).toBe("This is a test action");
expect(action.parameters.required).toEqual(["param1"]);
expect(action.parameters.properties).toHaveProperty("param1");
expect(action.parameters.properties).toHaveProperty("param2");
});

it("should throw an error if callback is not a function", async () => {
const params = z.object({
param1: z.string(),
});

it("should create an action with valid parameters", async () => {
const params = z.object({
param1: z.string(),
param2: z.string().optional(),
});

const callback = async (params: Record<string, any>) => {
return { success: true };
};

const options = {
actionName: "testAction",
toolName: "testTool",
description: "This is a test action",
inputParams: params,
callback,
};

const action = await actionRegistry.createAction(options);

expect(action.parameters.title).toBe("testAction");
expect(action.parameters.type).toBe("object");
expect(action.parameters.description).toBe("This is a test action");
expect(action.parameters.required).toEqual(["param1"]);
expect(action.parameters.properties).toHaveProperty("param1");
expect(action.parameters.properties).toHaveProperty("param2");
const options = {
actionName: "testAction1",
toolName: "testTool",
description: "This is a test action",
params,
callback: "notAFunction",
};

await expect(actionRegistry.createAction(options as any)).rejects.toThrow(
"Callback must be a function"
);
});

it("should throw an error if callback is an anonymous function and noActionName is specified", async () => {
const params = z.object({
param1: z.string(),
});

it("should throw an error if callback is not a function", async () => {
const params = z.object({
param1: z.string(),
});

const options = {
actionName: "testAction1",
toolName: "testTool",
description: "This is a test action",
params,
callback: "notAFunction",
};

await expect(actionRegistry.createAction(options as any)).rejects.toThrow("Callback must be a function");
const options = {
toolName: "testTool",
description: "This is a test action",
inputParams: params,
callback: async function () {
return { success: true };
},
};

await expect(actionRegistry.createAction(options)).rejects.toThrow(
"You must provide actionName for this action"
);
});

it("should execute an action with valid parameters", async () => {
const params = z.object({
param1: z.string(),
});

it("should throw an error if callback is an anonymous function and noActionName is specified", async () => {
const params = z.object({
param1: z.string(),
});

const options = {
toolName: "testTool",
description: "This is a test action",
inputParams: params,
callback: async function () { return { success: true }; },
};

await expect(actionRegistry.createAction(options)).rejects.toThrow("You must provide actionName for this action");
const callback = async (params: Record<string, any>) => {
return { success: true };
};

const options = {
actionName: "testAction2",
description: "This is a test action",
inputParams: params,
callback,
};

await actionRegistry.createAction(options);

const result = await actionRegistry.executeAction(
"testAction2",
{ param1: "value1" },
{}
);

expect(result).toEqual({ success: true });
});

it("should throw an error if action does not exist", async () => {
await expect(
actionRegistry.executeAction("nonExistentAction", {}, {})
).rejects.toThrow("Action with name nonExistentAction does not exist");
});

it("should get actions by names", async () => {
const params = z.object({
param1: z.string(),
});

it("should execute an action with valid parameters", async () => {
const params = z.object({
param1: z.string(),
});

const callback = async (params: Record<string, any>) => {
return { success: true };
};
const callback = async (params: Record<string, any>) => {
return { success: true };
};

const options = {
actionName: "testAction2",
description: "This is a test action",
inputParams: params,
callback,
};
const options = {
actionName: "testAction",
toolName: "testTool",
description: "This is a test action",
inputParams: params,
callback,
};

await actionRegistry.createAction(options);
await actionRegistry.createAction(options);

const result = await actionRegistry.executeAction("testAction2", { param1: "value1" }, {});

expect(result).toEqual({ success: true });
const actions = await actionRegistry.getActions({
actions: ["testAction"],
});
expect(actions.length).toBe(1);
expect(actions[0].parameters.properties).toHaveProperty("param1");
});

it("should throw an error if action does not exist", async () => {
await expect(actionRegistry.executeAction("nonExistentAction", {}, {})).rejects.toThrow("Action with name nonExistentAction does not exist");
it("should return an empty array if no actions match the names", async () => {
const actions = await actionRegistry.getActions({
actions: ["nonExistentAction"],
});

it("should get actions by names", async () => {
const params = z.object({
param1: z.string(),
});

const callback = async (params: Record<string, any>) => {
return { success: true };
};

const options = {
actionName: "testAction",
toolName: "testTool",
description: "This is a test action",
inputParams: params,
callback,
};

await actionRegistry.createAction(options);

const actions = await actionRegistry.getActions({actions: ["testAction"]});
expect(actions.length).toBe(1);
expect(actions[0].parameters.properties).toHaveProperty("param1");
});

it("should return an empty array if no actions match the names", async () => {
const actions = await actionRegistry.getActions({actions: ["nonExistentAction"]});

expect(actions.length).toBe(0);
});
expect(actions.length).toBe(0);
});
});
15 changes: 14 additions & 1 deletion js/src/sdk/base.toolset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { getEnvVariable } from "../utils/shared";
import { WorkspaceConfig } from "../env/config";
import { Workspace } from "../env";
import logger from "../utils/logger";
import { CEG } from '../sdk/utils/error';
import { ExecuteActionResDTO } from "./client/types.gen";
import { saveFile } from "./utils/fileUtils";
import { convertReqParams, converReqParamForActionExecution } from "./utils";
import { ActionRegistry, CreateActionOptions } from "./actionRegistry";
import { getUserDataJson } from "./utils/config";

import apiClient from '../sdk/client/client';
import { ActionProxyRequestConfigDTO } from './client';

type GetListActionsResponse = any;

Expand Down Expand Up @@ -279,4 +281,15 @@ export class ComposioToolSet {
logger.warn("execute_action is deprecated, use executeAction instead");
return this.executeAction(action, params, entityId);
}

async executeRequest(data: ActionProxyRequestConfigDTO){
try {
const { data: res } = await apiClient.actionsV2.executeActionProxyV2({
body: data as unknown as ActionProxyRequestConfigDTO
});
return res!;
} catch (error) {
throw CEG.handleError(error);
}
}
}
Loading
Loading