Skip to content
Merged
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
7 changes: 7 additions & 0 deletions packages/ai/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
OPENAI_API_KEY=

# Get these by following https://pipedream.com/docs/connect/managed-auth/quickstart/#getting-started
PIPEDREAM_CLIENT_ID=
PIPEDREAM_CLIENT_SECRET=
PIPEDREAM_PROJECT_ID=
PIPEDREAM_PROJECT_ENVIRONMENT=
3 changes: 3 additions & 0 deletions packages/ai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env
node_modules
dist
37 changes: 37 additions & 0 deletions packages/ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# @pipedream/ai

> This library is in alpha status. The API is subject to breaking changes.

Create a .env file based on the .env.example file.

Basic example:
```ts
import { OpenAiTools } from "@pipedream/ai"
import { OpenAI } from "openai"

const openai = new OpenAI()

// Replace with a unique identifier for your user
const userId = <add user id here>

const openAiTools = new OpenAiTools(userId)
const tools = await openAiTools.getTools({
app: "slack",
})

const completion = await openai.chat.completions.create({
messages: [
{
role: "user",
content: "Send a joke to #random channel",
},
],
model: "gpt-4o",
tools,
})

const results = await openAiTools.handleCompletion(completion)

console.log(JSON.stringify(results, null, 2))

```
33 changes: 33 additions & 0 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@pipedream/ai",
"type": "module",
"version": "0.0.1",
"description": "Pipedream AI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"package.json"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"@pipedream/sdk": "workspace:^",
"zod": "^3.24.4",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
"bun": "^1.2.13",
"openai": "^4.77.0"
Comment on lines +29 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider making OpenAI a peer dependency

OpenAI is currently listed as a dev dependency, but the example in the README shows it being imported by the user.

Move the OpenAI package to peerDependencies to indicate to users that they need to install it themselves:

  "dependencies": {
    "@pipedream/sdk": "workspace:^",
    "zod": "^3.24.4",
    "zod-to-json-schema": "^3.24.5"
  },
+ "peerDependencies": {
+   "openai": "^4.77.0"
+ },
  "devDependencies": {
    "bun": "^1.2.13",
-   "openai": "^4.77.0"
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"devDependencies": {
"bun": "^1.2.13",
"openai": "^4.77.0"
"dependencies": {
"@pipedream/sdk": "workspace:^",
"zod": "^3.24.4",
"zod-to-json-schema": "^3.24.5"
},
"peerDependencies": {
"openai": "^4.77.0"
},
"devDependencies": {
"bun": "^1.2.13"
}
🤖 Prompt for AI Agents
In packages/ai/package.json around lines 29 to 31, the OpenAI package is listed
under devDependencies but should be a peer dependency since users import it
directly. Move "openai" from devDependencies to peerDependencies to indicate
that users must install it themselves. Update the package.json accordingly by
removing "openai" from devDependencies and adding it under peerDependencies with
the appropriate version.

}
}
Comment on lines +1 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Package.json needs additional metadata fields

The package structure is well-defined with appropriate ESM configuration, entry points, and dependencies. However, several standard metadata fields are missing that would improve package discoverability and usability.

Add the following missing fields to the package.json file:

{
  "name": "@pipedream/ai",
  "type": "module",
  "version": "0.0.1",
  "description": "Pipedream AI",
+ "license": "MIT",
+ "repository": {
+   "type": "git",
+   "url": "https://github.com/PipedreamHQ/pipedream.git",
+   "directory": "packages/ai"
+ },
+ "engines": {
+   "node": ">=18.0.0"
+ },
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": [
    "dist",
    "package.json"
  ],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@pipedream/sdk": "workspace:^",
    "zod": "^3.24.4",
    "zod-to-json-schema": "^3.24.5"
  },
  "devDependencies": {
    "bun": "^1.2.13",
    "openai": "^4.77.0"
  }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"name": "@pipedream/ai",
"type": "module",
"version": "0.0.1",
"description": "Pipedream AI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"package.json"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"@pipedream/sdk": "workspace:^",
"zod": "^3.24.4",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
"bun": "^1.2.13",
"openai": "^4.77.0"
}
}
{
"name": "@pipedream/ai",
"type": "module",
"version": "0.0.1",
"description": "Pipedream AI",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/PipedreamHQ/pipedream.git",
"directory": "packages/ai"
},
"engines": {
"node": ">=18.0.0"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"package.json"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"@pipedream/sdk": "workspace:^",
"zod": "^3.24.4",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
"bun": "^1.2.13",
"openai": "^4.77.0"
}
}
🤖 Prompt for AI Agents
In packages/ai/package.json lines 1 to 33, the package.json is missing standard
metadata fields that improve package discoverability and usability. Add fields
such as "author", "license", "repository", "keywords", and "homepage" with
appropriate values to provide more context about the package, its ownership,
licensing, source code location, relevant tags, and project homepage.

107 changes: 107 additions & 0 deletions packages/ai/src/configurablePropsToZod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
ConfigurableProps, V1Component,
} from "@pipedream/sdk";
import {
z, ZodRawShape,
} from "zod";
import {
extractEnumValues, toNonEmptyTuple,
} from "./lib/helpers";

export const configurablePropsToZod = (
component: V1Component,
options?: {
asyncOptionsDescription?: string;
configureComponentDescription?: string;
configurableProps?: ConfigurableProps;
},
) => {
const schema: ZodRawShape = {};

for (const cp of options?.configurableProps ||
(component.configurable_props as ConfigurableProps)) {
if (cp.hidden) {
continue;
}

if (cp.type === "app") {
// XXX handle directly in implementation
continue;
} else if (cp.type === "string") {
if (cp.options && Array.isArray(cp.options) && cp.options.length > 0) {
const enumValues = toNonEmptyTuple(extractEnumValues(cp.options));
if (enumValues) {
schema[cp.name] = z.enum(enumValues);
} else {
schema[cp.name] = z.string();
}
} else {
schema[cp.name] = z.string();
}
} else if (cp.type === "string[]") {
if (cp.options && Array.isArray(cp.options) && cp.options.length > 0) {
const enumValues = toNonEmptyTuple(extractEnumValues(cp.options));
if (enumValues) {
schema[cp.name] = z.array(z.enum(enumValues));
} else {
schema[cp.name] = z.array(z.string());
}
} else {
schema[cp.name] = z.array(z.string());
}
} else if (cp.type === "$.discord.channel") {
schema[cp.name] = z.string();
} else if (cp.type === "$.discord.channel[]") {
schema[cp.name] = z.array(z.string());
} else if (cp.type === "object") {
schema[cp.name] = z.object({}).passthrough();
} else if (cp.type === "any") {
schema[cp.name] = z.any();
} else if (cp.type === "integer") {
schema[cp.name] = z.number().int();
} else if (cp.type === "integer[]") {
schema[cp.name] = z.array(z.number().int());
} else if (cp.type === "boolean") {
schema[cp.name] = z.boolean();
} else if (cp.type === "boolean[]") {
schema[cp.name] = z.array(z.boolean());
// ignore alerts, as no user input required
} else {
console.error("unhandled type. Skipping", cp.name, cp.type);
}

if (schema[cp.name]) {
if (cp.optional) {
schema[cp.name] = schema[cp.name].optional().nullable();
}

let description: string = cp.description || "";

if (cp.hidden) {
description +=
"\n\nIMPORTANT: This property is hidden. Do not configure it and leave it blank.\n";
}

if (cp.remoteOptions) {
if (options?.asyncOptionsDescription) {
if (options.configureComponentDescription) {
description += `\n\n${options.asyncOptionsDescription}`;
}
} else {
if (options?.configureComponentDescription) {
description += `\n\n${options.configureComponentDescription}`;
}
}
// if (cp.name.includes("id")) {
// description += `\n\nIMPORTANT: An ID is required for this property. If you don't have the id and only have the name, use the "${CONFIGURE_COMPONENT_TOOL_NAME}" tool to get the values.`;
// }
}

if (description.trim()) {
schema[cp.name] = schema[cp.name].describe(description.trim());
}
}
}

return schema;
};
3 changes: 3 additions & 0 deletions packages/ai/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {
OpenAiTools,
} from "./tool-sets/openai"
5 changes: 5 additions & 0 deletions packages/ai/src/lib/componentAppKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ConfigurableProps } from "@pipedream/sdk";

export const componentAppKey = (configuredProps: ConfigurableProps) => {
return configuredProps.find((prop) => prop.type === "app")?.app;
};
21 changes: 21 additions & 0 deletions packages/ai/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from "zod";

const configSchema = z.object({
PIPEDREAM_CLIENT_ID: z.string().min(1, {
message: "PIPEDREAM_CLIENT_ID is required",
}),
PIPEDREAM_CLIENT_SECRET: z.string().min(1, {
message: "PIPEDREAM_CLIENT_SECRET is required",
}),
PIPEDREAM_PROJECT_ID: z.string().min(1, {
message: "PIPEDREAM_PROJECT_ID is required",
}),
PIPEDREAM_PROJECT_ENVIRONMENT: z.enum([
"development",
"production",
]),
});
Comment on lines +3 to +17
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add JSDoc comments for better documentation

The schema definition lacks documentation that explains what each environment variable is used for.

Add JSDoc comments to document the purpose of each environment variable:

+ /**
+  * Schema for validating required Pipedream environment variables
+  */
const configSchema = z.object({
+  /**
+   * The client ID for authenticating with Pipedream API
+   */
  PIPEDREAM_CLIENT_ID: z.string().min(1, {
    message: "PIPEDREAM_CLIENT_ID is required",
  }),
+  /**
+   * The client secret for authenticating with Pipedream API
+   */
  PIPEDREAM_CLIENT_SECRET: z.string().min(1, {
    message: "PIPEDREAM_CLIENT_SECRET is required",
  }),
+  /**
+   * The project ID where Pipedream components will be executed
+   */
  PIPEDREAM_PROJECT_ID: z.string().min(1, {
    message: "PIPEDREAM_PROJECT_ID is required",
  }),
+  /**
+   * The environment (development or production) where Pipedream components will be executed
+   */
  PIPEDREAM_PROJECT_ENVIRONMENT: z.enum([
    "development",
    "production",
  ]),
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const configSchema = z.object({
PIPEDREAM_CLIENT_ID: z.string().min(1, {
message: "PIPEDREAM_CLIENT_ID is required",
}),
PIPEDREAM_CLIENT_SECRET: z.string().min(1, {
message: "PIPEDREAM_CLIENT_SECRET is required",
}),
PIPEDREAM_PROJECT_ID: z.string().min(1, {
message: "PIPEDREAM_PROJECT_ID is required",
}),
PIPEDREAM_PROJECT_ENVIRONMENT: z.enum([
"development",
"production",
]),
});
/**
* Schema for validating required Pipedream environment variables
*/
const configSchema = z.object({
/**
* The client ID for authenticating with Pipedream API
*/
PIPEDREAM_CLIENT_ID: z.string().min(1, {
message: "PIPEDREAM_CLIENT_ID is required",
}),
/**
* The client secret for authenticating with Pipedream API
*/
PIPEDREAM_CLIENT_SECRET: z.string().min(1, {
message: "PIPEDREAM_CLIENT_SECRET is required",
}),
/**
* The project ID where Pipedream components will be executed
*/
PIPEDREAM_PROJECT_ID: z.string().min(1, {
message: "PIPEDREAM_PROJECT_ID is required",
}),
/**
* The environment (development or production) where Pipedream components will be executed
*/
PIPEDREAM_PROJECT_ENVIRONMENT: z.enum([
"development",
"production",
]),
});
🤖 Prompt for AI Agents
In packages/ai/src/lib/config.ts around lines 3 to 17, the configSchema object
defining environment variables lacks JSDoc comments. Add JSDoc comments above
the configSchema declaration to describe the purpose of each environment
variable, explaining what PIPEDREAM_CLIENT_ID, PIPEDREAM_CLIENT_SECRET,
PIPEDREAM_PROJECT_ID, and PIPEDREAM_PROJECT_ENVIRONMENT represent and how they
are used in the application.


export const config = configSchema.parse(process?.env);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Config validation should be more robust

The current implementation immediately parses environment variables on import, which will throw an error if any required variables are missing.

Make the config validation more robust by providing a function that validates the config when called, with better error handling:

- export const config = configSchema.parse(process?.env);
+ export function getConfig() {
+   try {
+     return configSchema.parse(process.env);
+   } catch (error) {
+     if (error instanceof z.ZodError) {
+       const missingVars = error.issues.map(issue => issue.path[0]).join(", ");
+       throw new Error(`Missing or invalid environment variables: ${missingVars}. Please check your .env file.`);
+     }
+     throw error;
+   }
+ }
+ 
+ // For backward compatibility
+ export const config = getConfig();
🤖 Prompt for AI Agents
In packages/ai/src/lib/config.ts at line 19, the config is currently parsed
immediately on import, which causes the application to throw errors if required
environment variables are missing. To fix this, refactor the code to export a
function that performs the configSchema.parse call when invoked, allowing for
controlled validation and error handling. This function should catch validation
errors and handle them gracefully, rather than throwing on import.


export type Config = z.infer<typeof configSchema>;
15 changes: 15 additions & 0 deletions packages/ai/src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function toNonEmptyTuple<T extends string>(
arr: T[],
): [T, ...T[]] | undefined {
return arr.length > 0
? (arr as [T, ...T[]])
: undefined;
}

type EnumLike = string | { value: string };

export function extractEnumValues(values: EnumLike[]): string[] {
return values.map((v) => (typeof v === "string"
? v
: v.value));
}
11 changes: 11 additions & 0 deletions packages/ai/src/lib/pd-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createBackendClient } from "@pipedream/sdk";
import { config } from "./config";

export const pd = createBackendClient({
credentials: {
clientId: config.PIPEDREAM_CLIENT_ID,
clientSecret: config.PIPEDREAM_CLIENT_SECRET,
},
projectId: config.PIPEDREAM_PROJECT_ID,
environment: config.PIPEDREAM_PROJECT_ENVIRONMENT,
});
101 changes: 101 additions & 0 deletions packages/ai/src/tool-sets/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { z } from "zod";
import { configurablePropsToZod } from "../configurablePropsToZod";
import { pd } from "../lib/pd-client";
import {
Account, V1Component,
} from "@pipedream/sdk";
import { componentAppKey } from "../lib/componentAppKey";

type Tool = {
name: string;
description?: string;
schema: z.ZodObject<z.ZodRawShape>;
execute: (args: Record<string, unknown>) => Promise<unknown>;
};

export class CoreTools {
userId: string;
tools: Tool[] = [];

constructor(userId: string) {
this.userId = userId;
}

async getTools(options?: { app?: string; query?: string }) {
const { data: components } = await pd.getComponents({
app: options?.app,
q: options?.query,
});

for (const component of components) {
this.tools.push({
name: component.name.replace(/[^a-zA-Z0-9_-]/g, "_"),
description: component.description,
schema: z.object(configurablePropsToZod(component)),
execute: (args) => this.executeTool(component, args),
});
}

return this.tools;
}
Comment on lines +24 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent duplicate tools when fetching multiple times.

The current implementation adds tools to the array without checking for duplicates, which could lead to duplicates if getTools is called multiple times.

async getTools(options?: { app?: string; query?: string }) {
  const { data: components } = await pd.getComponents({
    app: options?.app,
    q: options?.query,
  });

+  // Clear existing tools or use a Set to prevent duplicates
+  this.tools = [];

  for (const component of components) {
    this.tools.push({
      name: component.name.replace(/[^a-zA-Z0-9_-]/g, "_"),
      description: component.description,
      schema: z.object(configurablePropsToZod(component)),
      execute: (args) => this.executeTool(component, args),
    });
  }

  return this.tools;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async getTools(options?: { app?: string; query?: string }) {
const { data: components } = await pd.getComponents({
app: options?.app,
q: options?.query,
});
for (const component of components) {
this.tools.push({
name: component.name.replace(/[^a-zA-Z0-9_-]/g, "_"),
description: component.description,
schema: z.object(configurablePropsToZod(component)),
execute: (args) => this.executeTool(component, args),
});
}
return this.tools;
}
async getTools(options?: { app?: string; query?: string }) {
const { data: components } = await pd.getComponents({
app: options?.app,
q: options?.query,
});
// Clear existing tools or use a Set to prevent duplicates
this.tools = [];
for (const component of components) {
this.tools.push({
name: component.name.replace(/[^a-zA-Z0-9_-]/g, "_"),
description: component.description,
schema: z.object(configurablePropsToZod(component)),
execute: (args) => this.executeTool(component, args),
});
}
return this.tools;
}
🤖 Prompt for AI Agents
In packages/ai/src/tool-sets/core.ts around lines 22 to 38, the getTools method
appends tools to this.tools without checking for duplicates, causing repeated
entries if called multiple times. Modify the method to check if a tool with the
same name already exists in this.tools before pushing a new one, ensuring each
tool is unique and preventing duplicates on multiple calls.


async getTool(name: string) {
return this.tools.find((tool) => tool.name === name);
}

async executeTool(component: V1Component, args: Record<string, unknown>) {
const appKey = componentAppKey(component.configurable_props);

if (!appKey) {
throw new Error("App name not found");
}

const authProvision = await this.getAuthProvision({
app: appKey,
uuid: this.userId,
});

if (typeof authProvision === "string") {
return authProvision;
}

return pd.runAction({
actionId: component.key,
configuredProps: {
...args,
[appKey]: {
authProvisionId: authProvision.id,
},
},
externalUserId: this.userId,
});
}

async getAuthProvision({
app,
uuid,
}: {
app: string;
uuid: string;
}): Promise<Account | string> {
const authProvisions = await pd.getAccounts({
external_user_id: uuid,
include_credentials: false,
app,
});

const authProvision = authProvisions.data.find((ap) => ap.healthy);

if (!authProvision) {
const token = await pd.createConnectToken({
external_user_id: uuid,
webhook_uri: "https://eokyfjps7uqmmrk.m.pipedream.net", // https://pipedream.com/@pd/p_G6Ck6Mk/
});
return `
The user MUST be shown the following URL so they can click on it to connect their account and you MUST NOT modify the URL or it will break: https://pipedream.com/_static/connect.html?token=${token.token}&connectLink=true&app=${encodeURIComponent(app)}
`.trim();
}

return authProvision;
}
Comment on lines +74 to +100
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Hardcoded webhook URI might be an issue.

The webhook URI is hardcoded, which could be problematic if this code is deployed across different environments.


🏁 Script executed:

#!/bin/bash
# Check if the webhook URI is referenced elsewhere or configurable
rg "eokyfjps7uqmmrk.m.pipedream.net" --type ts --type js

Length of output: 343


Extract the webhook URI into configuration

Hardcoding the Pipedream webhook URL in multiple places makes it brittle across environments. Move this value into a shared configuration (e.g. an environment variable or central config service) and reference it where needed.

Files to update:

  • packages/ai/src/tool-sets/core.ts
  • modelcontextprotocol/src/lib/authProvisions.ts

Suggested change example:

// config.ts
export const PIPEDREAM_WEBHOOK_URI = process.env.PIPEDREAM_WEBHOOK_URI!
  
// usage
const token = await pd.createConnectToken({
  external_user_id: uuid,
  webhook_uri: PIPEDREAM_WEBHOOK_URI,
});
🤖 Prompt for AI Agents
In packages/ai/src/tool-sets/core.ts between lines 72 and 98, the webhook URI is
hardcoded in the call to pd.createConnectToken, which reduces flexibility across
environments. To fix this, extract the webhook URI into a shared configuration,
such as an environment variable or a central config file, and replace the
hardcoded string with a reference to this configuration value. This will allow
the webhook URI to be easily changed without modifying the code.

}
Loading
Loading