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: support custom actions in vercel toolkit (with type safety improvements) #1175

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
51 changes: 51 additions & 0 deletions js/src/frameworks/vercel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { beforeAll, describe, expect, it } from "@jest/globals";
import { z } from "zod";
import { getTestConfig } from "../../config/getTestConfig";
import { VercelAIToolSet } from "./vercel";
import type { CreateActionOptions } from "../sdk/actionRegistry";

Check warning on line 5 in js/src/frameworks/vercel.spec.ts

View workflow job for this annotation

GitHub Actions / lint-and-prettify

'CreateActionOptions' is defined but never used. Allowed unused vars must match /^_/u

describe("Apps class tests", () => {
let vercelAIToolSet: VercelAIToolSet;

beforeAll(() => {
vercelAIToolSet = new VercelAIToolSet({
apiKey: getTestConfig().COMPOSIO_API_KEY,
Expand All @@ -25,4 +28,52 @@
});
expect(Object.keys(tools).length).toBe(1);
});

describe("custom actions", () => {
let customAction: Awaited<ReturnType<typeof vercelAIToolSet.createAction>>;
let tools: Awaited<ReturnType<typeof vercelAIToolSet.getTools>>;

beforeAll(async () => {
const params = z.object({
owner: z.string().describe("The owner of the repository"),
repo: z.string().describe("The name of the repository without the `.git` extension."),
})


customAction = await vercelAIToolSet.createAction({
actionName: "starRepositoryCustomAction",
toolName: "github",
description: "Star A Github Repository For Given `Repo` And `Owner`",
inputParams: params,
callback: async (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error case tests for the callback function. The current test only covers the success case. Add tests for scenarios where the callback might fail or return unsuccessful results.

inputParams,
) => ({ successful: true, data: inputParams })
})

tools = await vercelAIToolSet.getTools({
actions: ["starRepositoryCustomAction"],
});

});

it("check if custom actions are coming", async () => {
expect(Object.keys(tools).length).toBe(1);
expect(tools).toHaveProperty(customAction.name, tools[customAction.name]);
});

it("check if custom actions are executing", async () => {
const res = await vercelAIToolSet.executeAction({
action: customAction.name,
params: {
owner: "composioHQ",
repo: "composio"
},
})
expect(res.successful).toBe(true);
expect(res.data).toHaveProperty("owner", "composioHQ");
expect(res.data).toHaveProperty("repo", "composio");
});
})


});
28 changes: 13 additions & 15 deletions js/src/frameworks/vercel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { jsonSchema, tool } from "ai";
import { jsonSchema, tool, CoreTool } from "ai";
import { z } from "zod";
import { ComposioToolSet as BaseComposioToolSet } from "../sdk/base.toolset";
import { TELEMETRY_LOGGER } from "../sdk/utils/telemetry";
Expand Down Expand Up @@ -62,7 +62,7 @@ export class VercelAIToolSet extends BaseComposioToolSet {
useCase?: Optional<string>;
usecaseLimit?: Optional<number>;
filterByAvailableApps?: Optional<boolean>;
}): Promise<{ [key: string]: RawActionData }> {
}): Promise<{ [key: string]: CoreTool }> {
TELEMETRY_LOGGER.manualTelemetry(TELEMETRY_EVENTS.SDK_METHOD_INVOKED, {
method: "getTools",
file: this.fileName,
Expand All @@ -78,21 +78,19 @@ export class VercelAIToolSet extends BaseComposioToolSet {
actions,
} = ZExecuteToolCallParams.parse(filters);

const actionsList = await this.client.actions.list({
...(apps && { apps: apps?.join(",") }),
...(tags && { tags: tags?.join(",") }),
...(useCase && { useCase: useCase }),
...(actions && { actions: actions?.join(",") }),
...(usecaseLimit && { usecaseLimit: usecaseLimit }),
filterByAvailableApps: filterByAvailableApps ?? undefined,
});
const actionsList = await this.getToolsSchema({
apps,
actions,
tags,
useCase,
useCaseLimit: usecaseLimit,
filterByAvailableApps
})

const tools = {};
actionsList.items?.forEach((actionSchema) => {
// @ts-ignore
const tools: { [key: string]: CoreTool } = {};
actionsList.forEach((actionSchema) => {
tools[actionSchema.name!] = this.generateVercelTool(
// @ts-ignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a type guard instead of non-null assertion on actionSchema.name. This could prevent runtime errors if the name is undefined. Example:

if (!actionSchema.name) {
  console.warn('Action schema missing name, skipping...');
  return;
}
tools[actionSchema.name] = this.generateVercelTool(actionSchema);

actionSchema as ActionData
actionSchema
);
});

Expand Down
28 changes: 22 additions & 6 deletions js/src/sdk/actionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,25 @@ type RawExecuteRequestParam = {
};
};

export type CreateActionOptions = {

type ValidParameters = ZodObject<{ [key: string]: ZodString | ZodOptional<ZodString> }>
export type Parameters = ValidParameters | z.ZodObject<{}>

type inferParameters<PARAMETERS extends Parameters> =
PARAMETERS extends ValidParameters
? z.infer<PARAMETERS>
: z.infer<z.ZodObject<{}>>


export type CreateActionOptions<
P extends Parameters = z.ZodObject<{}>
> = {
actionName?: string;
toolName?: string;
description?: string;
inputParams: ZodObject<{ [key: string]: ZodString | ZodOptional<ZodString> }>;
inputParams?: P;
callback: (
inputParams: Record<string, string>,
inputParams: inferParameters<P>,
authCredentials: Record<string, string> | undefined,
executeRequest: (
data: RawExecuteRequestParam
Expand All @@ -50,15 +62,19 @@ export class ActionRegistry {
client: Composio;
customActions: Map<
string,
{ metadata: CreateActionOptions; schema: Record<string, unknown> }
{
metadata: CreateActionOptions;
schema: Record<string, unknown>
}
>;

constructor(client: Composio) {
this.client = client;
this.customActions = new Map();
}

async createAction(options: CreateActionOptions): Promise<RawActionData> {

async createAction<P extends Parameters = z.ZodObject<{}>>(options: CreateActionOptions<P>): Promise<RawActionData> {
const { callback } = options;
if (typeof callback !== "function") {
throw new Error("Callback must be a function");
Expand All @@ -67,7 +83,7 @@ export class ActionRegistry {
throw new Error("You must provide actionName for this action");
}
if (!options.inputParams) {
options.inputParams = z.object({});
options.inputParams = z.object({}) as P
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion as P here could be unsafe. Consider adding a type guard or validation to ensure the empty object conforms to the expected type P. Alternatively, you could make inputParams required and remove this default initialization.

}
const params = options.inputParams;
const actionName = options.actionName || callback.name || "";
Expand Down
16 changes: 8 additions & 8 deletions js/src/sdk/base.toolset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z, ZodObject } from "zod";

Check warning on line 1 in js/src/sdk/base.toolset.ts

View workflow job for this annotation

GitHub Actions / lint-and-prettify

'ZodObject' is defined but never used. Allowed unused vars must match /^_/u
import { Composio } from "../sdk";
import {
RawActionData,
Expand All @@ -10,7 +10,7 @@
} from "../types/base_toolset";
import type { Optional, Sequence } from "../types/util";
import { getEnvVariable } from "../utils/shared";
import { ActionRegistry, CreateActionOptions } from "./actionRegistry";
import { ActionRegistry, CreateActionOptions, Parameters } from "./actionRegistry";
import { ActionExecutionResDto } from "./client/types.gen";
import { ActionExecuteResponse, Actions } from "./models/actions";
import { ActiveTriggers } from "./models/activeTriggers";
Expand Down Expand Up @@ -54,10 +54,10 @@
post: TPostProcessor[];
schema: TSchemaProcessor[];
} = {
pre: [fileInputProcessor],
post: [fileResponseProcessor],
schema: [fileSchemaProcessor],
};
pre: [fileInputProcessor],
post: [fileResponseProcessor],
schema: [fileSchemaProcessor],
};

private userDefinedProcessors: {
pre?: TPreProcessor;
Expand Down Expand Up @@ -172,8 +172,8 @@
});
}

async createAction(options: CreateActionOptions) {
return this.userActionRegistry.createAction(options);
async createAction<P extends Parameters = z.ZodObject<{}>>(options: CreateActionOptions<P>) {
return this.userActionRegistry.createAction<P>(options);
}

private isCustomAction(action: string) {
Comment on lines 172 to 179

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor: The change ensures type safety by using generics, aligning with the ActionRegistry class.


Expand Down
Loading